Arthur_Diaz commited on
Commit
683fb05
Β·
unverified Β·
1 Parent(s): 14e67ea

docs(evals): M1 classifier report, per-corpus OOD reads and production decision (#3)

Browse files

## What & why
Consolidates the M1 training campaign (3 arms + 2 generalization audits
+
published baseline, all on identical EN test documents) into the
evaluation
report, and records the production decision: en_chunked_weighted.

## Changes
- docs/evals/m1_classifier_report.md: comparison tables, ablation
reading,
multilingual verdict, per-corpus domain-shift quantification,
advantaged-
baseline analysis, limitations
- training/configs/eval_{cambridge,elg}_only.toml: evaluation-only
configs for
pure per-corpus OOD reads (same test docs via per-stratum seeding)
- docs/adr/0003: production decision recorded

## Headline
Doc-level (n=99): macro-F1 0.798 / QWK 0.936 / adjacent 0.990 β€”
chunk-aggregate
beats truncation by +0.14 accuracy. Domain shift quantified per corpus:
βˆ’16–19 pts exact accuracy on an unseen source, adjacent accuracy
untouched
(graceful degradation). The published baseline (likely advantaged by
train/test
overlap) reaches 0.403 macro-F1 through our pipeline and 0.579 in its
native
regime β€” still βˆ’0.22 below our system: the margin is the chunk-aggregate
system, not the encoder.

## How to test
Numbers reproducible via training/README.md (seed 13); reads from
MLflow.

docs/adr/0003-datasets-and-licensing.md CHANGED
@@ -66,3 +66,5 @@ the symmetric run for elg (domain-shift measurement, not the production split).
66
  **Known limitations (mirrored in README):** only 24 A1 documents β†’ per-level
67
  metrics reported, weak A1 recall expected at doc level; product mitigation: A1
68
  recommendations lean on short texts, which is pedagogically appropriate.
 
 
 
66
  **Known limitations (mirrored in README):** only 24 A1 documents β†’ per-level
67
  metrics reported, weak A1 recall expected at doc level; product mitigation: A1
68
  recommendations lean on short texts, which is pedagogically appropriate.
69
+
70
+ **Production decision (2026-06-12):** en_chunked_weighted β€” see docs/evals/m1_classifier_report.md
docs/evals/m1_classifier_report.md ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # M1 β€” CEFR classifier: evaluation report
2
+
3
+ **Date:** 2026-06-12 Β· **Tracking:** MLflow (`sqlite:///mlflow.db`), experiment `cefr-classifier`
4
+ **Decision:** production config = **`en_chunked_weighted`** (arm 1). Rationale below.
5
+
6
+ ## Setup
7
+
8
+ Policies, data mix and experiment arms are defined in
9
+ [ADR 0003](../adr/0003-datasets-and-licensing.md); pipeline in `training/`
10
+ (PR `feat(ml): CEFR dataset builder and XLM-R training pipeline`).
11
+
12
+ - Base model: `xlm-roberta-base`, fine-tuned 4 epochs, class-weighted CE,
13
+ fp16, effective batch 16, RTX 3070 (~10–27 min/run).
14
+ - Split: document-level, stratified by corpus Γ— level, seed 13. **Per-stratum
15
+ seeding makes the English test documents identical across every arm** (unit
16
+ test: `test_arm_comparability_*`), so all rows below are directly comparable.
17
+ - EN test set: **99 documents** (31 cambridge_exams + 68 elg_cefr; supports
18
+ A1:2, A2:20, B1:26, B2:24, C1:15, C2:12) and **1,279 sentences**
19
+ (cefr_sp + readme).
20
+ - Headline view: document-level (the product use case: recommending a reading
21
+ text at the learner's level), chunk probabilities aggregated per document by
22
+ expected-rank mean.
23
+
24
+ Statistical honesty up front: at n=99, the 95% CI on accuracy is roughly
25
+ Β±0.09; per-corpus reads (n=31, n=68) are directional only; A1 (2 docs) and C2
26
+ (12 docs) document-level F1 values are anecdotal.
27
+
28
+ ## Headline: EN document-level (n=99)
29
+
30
+ | run | accuracy | adjacent | macro-F1 | QWK | f1_A1Β² | f1_B1 | f1_C1 | f1_C2 |
31
+ |---|---:|---:|---:|---:|---:|---:|---:|---:|
32
+ | **en_chunked_weighted** | **0.788** | 0.990 | **0.798** | **0.936** | 0.800 | 0.634 | 0.800 | 0.909 |
33
+ | multilingual | 0.717 | 0.990 | 0.681 | 0.918 | 0.500 | 0.605 | 0.545 | 0.923 |
34
+ | en_truncated | 0.646 | 0.990 | 0.684 | 0.892 | 1.000 | 0.537 | 0.320 | 0.800 |
35
+ | audit_no_cambridge | 0.707 | 0.990 | 0.665 | 0.903 | 0.667 | 0.727 | 0.485 | 0.500 |
36
+ | audit_no_elg | 0.576 | 0.980 | 0.475 | 0.851 | 0.000 | 0.524 | 0.424 | 0.600 |
37
+ | baselineΒΉ (our chunk pipeline) | 0.566 | 0.970 | 0.403 | 0.824 | 0.000 | 0.681 | 0.390 | 0.000 |
38
+ | baselineΒΉ (native, truncated docs) | 0.687 | 0.990 | 0.579 | 0.898 | 0.000 | 0.681 | 0.571 | 0.762 |
39
+
40
+ ΒΉ `UniversalCEFR/xlm-roberta-base-cefr-all-classifier`. Β² A1 support = 2 docs.
41
+
42
+ ## EN sentence-level (n=1,279)
43
+
44
+ | run | accuracy | adjacent | macro-F1 | QWK | f1_A1 (n=30) |
45
+ |---|---:|---:|---:|---:|---:|
46
+ | en_chunked_weighted | 0.622 | 0.983 | 0.611 | 0.820 | 0.492 |
47
+ | multilingual | 0.603 | 0.981 | 0.624 | 0.813 | **0.563** |
48
+ | en_truncated | 0.608 | 0.980 | 0.594 | 0.811 | 0.459 |
49
+ | audit_no_cambridge | 0.625 | 0.984 | 0.618 | 0.821 | 0.535 |
50
+ | audit_no_elg | 0.618 | 0.981 | 0.623 | 0.818 | 0.507 |
51
+ | baselineΒΉ | **0.815** | 0.991 | **0.775** | **0.901** | 0.735 |
52
+
53
+ ## Findings
54
+
55
+ ### 1. Chunk-aggregate beats truncation β€” the ablation that justifies the architecture
56
+
57
+ Same data, same test documents, only the length policy differs:
58
+ **+0.14 accuracy / +0.11 macro-F1 / +0.044 QWK** for chunk-aggregate over
59
+ truncate-at-512. The damage is exactly where ADR 0003 predicted: long, high-level
60
+ cambridge texts (truncated f1_C1 collapses to 0.32; the truncated f1_A1 = 1.0 is
61
+ the 2-doc anecdote β€” A1 documents are short, truncation cannot hurt them).
62
+ Expected-rank aggregation also absorbs chunk-level noise: passage-level accuracy
63
+ is ~0.62 while document-level reaches 0.79.
64
+
65
+ ### 2. Multilingual training: A1 hypothesis half-confirmed, headline cost too high
66
+
67
+ The Welsh hypothesis (764 A1 short texts lifting A1 recall) shows in the
68
+ predicted direction at sentence level (f1_A1 0.563 vs 0.492) but on only 30
69
+ positives β€” weak evidence. Meanwhile the EN document headline drops by 0.12
70
+ macro-F1 / 0.07 accuracy: capacity spreads across 13 languages and the class
71
+ weights now reflect the multilingual distribution. **Verdict: not worth it for
72
+ the English-only product.** The run is kept as the reference point for future
73
+ target languages: across all 13 languages it reaches document macro-F1 0.568 /
74
+ QWK 0.762 (n=648 docs) β€” the starting line when a second language ships.
75
+
76
+ ### 3. Generalization audits: real domain shift, gracefully absorbed
77
+
78
+ Mixed-test reads first (excluded corpus = 31/99 or 68/99 of the test):
79
+ removing cambridge from training costs βˆ’0.13 macro-F1 on the mixed test;
80
+ removing elg costs βˆ’0.32 and zeroes A1 (elg is the *only* source of A1
81
+ documents). That apparent asymmetry is mostly a **composition effect** β€” elg
82
+ is 69% of the document test and the sole A1 provider. Per-corpus pure reads
83
+ (same test documents in every run, thanks to per-stratum seeding, via the
84
+ `eval_*_only` configs; accuracy / adjacent / QWK β€” macro-F1 omitted: 6-way
85
+ macro is misleading on single corpora with absent levels) show a shift of
86
+ comparable magnitude on both sides:
87
+
88
+ | corpus (test docs) | en_chunked_weighted (in-domain) | audit model (corpus never seen) |
89
+ |---|---|---|
90
+ | cambridge (31) | 0.871 / 1.000 / 0.966 | 0.677 / 1.000 / 0.900 |
91
+ | elg (68) | 0.750 / 0.985 / 0.919 | 0.588 / 0.985 / 0.850 |
92
+
93
+ Reading: an unseen source costs **16–19 points of exact accuracy** (n=31/68 β†’
94
+ directional), while **adjacent accuracy is untouched** (1.000 and 0.985,
95
+ identical in- and out-of-domain) and QWK stays β‰₯ 0.85: degradation is graceful β€”
96
+ errors move from exact to off-by-one, never further. Consistency check: the two
97
+ in-domain reads recompose the headline exactly ((31Γ—0.871 + 68Γ—0.750)/99 = 0.788).
98
+
99
+ Production consequence: both corpora stay in training, and on authentic
100
+ unseen-source texts the model should be trusted at **level Β± 1** rather than
101
+ exact level β€” which still supports the recommendation use case.
102
+
103
+ ### 4. The published baseline: the decisive margin is the system, not the encoder
104
+
105
+ Two-sided, two-staged result. At sentence level the baseline dominates (0.815
106
+ vs 0.622 accuracy) β€” but it was, to the best of our knowledge, trained on the
107
+ full UniversalCEFR collection, which almost certainly includes our test
108
+ sentences. It is an **advantaged baseline**: "better model" and "saw the test
109
+ data" cannot be separated, so sentence-level numbers are reported without a
110
+ winner declared.
111
+
112
+ At the document level: through *our* chunk-aggregate pipeline the baseline
113
+ collapses (macro-F1 0.403; f1_A1 = f1_C2 = 0.000) β€” its chunk-level probability
114
+ distributions, never trained for this regime, are flat enough that the
115
+ expected-rank mean pulls everything toward the center. In its **native regime**
116
+ (full documents, tokenizer-truncated) it recovers substantially β€” macro-F1
117
+ 0.579, f1_C2 back to 0.762 β€” confirming that part of the collapse was an
118
+ evaluation-regime artifact, which we report rather than hide. But even at its
119
+ native best, the advantaged baseline stays well below the production system
120
+ (0.687 vs 0.788 accuracy, 0.579 vs 0.798 macro-F1, 0.898 vs 0.936 QWK), and is
121
+ merely on par with our own truncated arm (0.687/0.579 vs 0.646/0.684 acc/macro
122
+ β€” mixed). **The decisive margin is therefore not the encoder; it is the
123
+ chunk-aggregate system fine-tuned end-to-end**, a regime the baseline cannot
124
+ exploit. Bottom line: on the product task, our system outperforms the published
125
+ baseline by **+0.22 macro-F1**, each system evaluated in its best regime, with
126
+ the baseline likely advantaged by train/test overlap.
127
+
128
+ ## Decision
129
+
130
+ **Ship `en_chunked_weighted`** (`models/cefr/en_chunked_weighted/`): best
131
+ headline on the product metric (macro-F1 0.798, QWK 0.936, adjacent 0.990 β€”
132
+ 1/99 documents off by more than one level), architecture validated by ablation,
133
+ preprocessing parameters frozen in `preprocessing.json` next to the weights.
134
+ Next step (separate PR): ONNX int8 export + CPU inference service + Reading tab.
135
+
136
+ Deferred, recorded: multilingual checkpoint for future target languages;
137
+ ordinal head (CORAL/CORN, ADR 0003 arm 4) as a possible follow-up β€” QWK 0.936
138
+ leaves limited headroom, so it is not on the critical path.
139
+
140
+ ## Limitations
141
+
142
+ - n=99 test documents (Β±~0.09 CI); A1 = 2 docs, C2 = 12 docs at document level.
143
+ - B1 remains the weakest mid class (f1 0.634); errors are off-by-one at the
144
+ notoriously fuzzy B1/B2 boundary (adjacent accuracy 0.990).
145
+ - Chunk labels are weak labels (inherited from the document) β€” accepted,
146
+ documented, and absorbed by aggregation, but passage-level metrics
147
+ underestimate the model for that reason.
148
+ - Reference corpora are exam/pedagogical texts; authentic web texts are
149
+ out-of-domain β€” expect the audit-sized cost (βˆ’16–19 points of exact accuracy,
150
+ adjacent accuracy intact).
151
+ - All training data is research/non-commercial licensed (ADR 0003); the
152
+ fine-tuned checkpoint inherits CC BY-NC-SA terms if ever published.
153
+
154
+ ## Reproducibility
155
+
156
+ ```bash
157
+ uv run --group train python training/train_cefr.py --config training/configs/<arm>.toml
158
+ uv run --group train python training/eval_cefr.py --model <checkpoint|hub-id> --config training/configs/<cfg>.toml
159
+ uv run --group train mlflow ui --backend-store-uri sqlite:///mlflow.db
160
+ ```
161
+
162
+ Seed 13 everywhere; arms: `en_only`, `multilingual`, `en_truncated`,
163
+ `audit_no_cambridge`, `audit_no_elg`; per-corpus reads: `eval_cambridge_only`,
164
+ `eval_elg_only`; baseline native mode: `eval_cefr.py` with the `en_truncated`
165
+ config.
test DELETED
File without changes
training/configs/eval_cambridge_only.toml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evaluation-only config: the cambridge test split in isolation, for per-corpus
2
+ # OOD reads. Same seed => exactly the same 31 cambridge test documents as every
3
+ # training arm (per-stratum seeding). Never used for training.
4
+
5
+ [run]
6
+ name = "eval_cambridge_only"
7
+ seed = 13
8
+
9
+ [tracking]
10
+ uri = "sqlite:///mlflow.db"
11
+ experiment = "cefr-classifier"
12
+
13
+ [data]
14
+ subsets = ["UniversalCEFR/cambridge_exams_en"]
15
+ chunking = true
16
+ target_words = 200
17
+ max_words = 300
18
+ ratios = [0.8, 0.1, 0.1]
19
+ exclude_corpora_from_train = []
20
+
21
+ [model]
22
+ base = "xlm-roberta-base"
23
+ max_length = 512
training/configs/eval_elg_only.toml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Evaluation-only config: the elg test split in isolation, for per-corpus
2
+ # OOD reads. Same seed => exactly the same 68 elg test documents as every
3
+ # training arm (per-stratum seeding). Never used for training.
4
+
5
+ [run]
6
+ name = "eval_elg_only"
7
+ seed = 13
8
+
9
+ [tracking]
10
+ uri = "sqlite:///mlflow.db"
11
+ experiment = "cefr-classifier"
12
+
13
+ [data]
14
+ subsets = ["UniversalCEFR/elg_cefr_en"]
15
+ chunking = true
16
+ target_words = 200
17
+ max_words = 300
18
+ ratios = [0.8, 0.1, 0.1]
19
+ exclude_corpora_from_train = []
20
+
21
+ [model]
22
+ base = "xlm-roberta-base"
23
+ max_length = 512