Spaces:
Runtime error
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.
|
@@ -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
|
|
@@ -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.
|
|
File without changes
|
|
@@ -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
|
|
@@ -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
|