sysone 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sysone-0.2.0 → sysone-0.3.0}/PKG-INFO +19 -10
- {sysone-0.2.0 → sysone-0.3.0}/README.md +14 -5
- {sysone-0.2.0 → sysone-0.3.0}/docs/DESIGN.md +180 -22
- {sysone-0.2.0 → sysone-0.3.0}/examples/decoder_bev_decision.ipynb +33 -4
- {sysone-0.2.0 → sysone-0.3.0}/examples/decoder_typed_decisions.ipynb +31 -3
- {sysone-0.2.0 → sysone-0.3.0}/examples/encoder_bev_decision.ipynb +33 -4
- {sysone-0.2.0 → sysone-0.3.0}/examples/encoder_typed_decisions.ipynb +31 -3
- sysone-0.3.0/examples/encoder_typed_decisions_ordinal_weight.ipynb +268 -0
- {sysone-0.2.0 → sysone-0.3.0}/examples/quickstart.ipynb +9 -2
- {sysone-0.2.0 → sysone-0.3.0}/pyproject.toml +5 -4
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/_util.py +11 -3
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/architectures/base.py +5 -1
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/architectures/modernbert.py +5 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/base.py +132 -16
- sysone-0.3.0/src/sysone/calibration.py +108 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/checkpoint.py +1 -1
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/evaluation.py +18 -4
- sysone-0.3.0/src/sysone/losses.py +39 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/training.py +71 -10
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/tuning.py +1 -1
- sysone-0.3.0/src/sysone/version.py +1 -0
- sysone-0.3.0/tests/test_autosave.py +118 -0
- sysone-0.3.0/tests/test_calibration.py +93 -0
- sysone-0.3.0/tests/test_losses.py +93 -0
- sysone-0.3.0/tests/test_precision.py +54 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_tuning.py +18 -1
- sysone-0.2.0/src/sysone/losses.py +0 -17
- sysone-0.2.0/src/sysone/version.py +0 -1
- {sysone-0.2.0 → sysone-0.3.0}/.gitignore +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/LICENSE +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/__init__.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/answers.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/architectures/__init__.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/architectures/qwen.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/architectures/registry.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/cache.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/data.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/encoding.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/features.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/heads.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/layout.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/models.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/prompts.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/src/sysone/questions.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/__init__.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/conftest.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_architectures.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_cache.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_data.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_evaluation.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_layout.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_model.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_questions.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_real_models.py +0 -0
- {sysone-0.2.0 → sysone-0.3.0}/tests/test_training.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sysone
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Fine-tune and calibrate system-one decision models with a scikit-learn style fit / predict interface.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ForwardMetric/sysone
|
|
6
6
|
Project-URL: Repository, https://github.com/ForwardMetric/sysone
|
|
@@ -24,12 +24,12 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
24
24
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
25
|
Requires-Python: >=3.10
|
|
26
26
|
Requires-Dist: bitsandbytes>=0.45; sys_platform == 'linux' or sys_platform == 'win32'
|
|
27
|
-
Requires-Dist: huggingface-hub>=
|
|
27
|
+
Requires-Dist: huggingface-hub>=1.3
|
|
28
28
|
Requires-Dist: numpy>=1.26
|
|
29
29
|
Requires-Dist: peft>=0.13
|
|
30
|
-
Requires-Dist: safetensors>=0.4
|
|
31
|
-
Requires-Dist: torch>=2.
|
|
32
|
-
Requires-Dist: transformers>=5.
|
|
30
|
+
Requires-Dist: safetensors>=0.4.3
|
|
31
|
+
Requires-Dist: torch>=2.8
|
|
32
|
+
Requires-Dist: transformers>=5.1
|
|
33
33
|
Provides-Extra: pandas
|
|
34
34
|
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
@@ -109,27 +109,36 @@ Example notebooks, each comparing accuracy before and after training:
|
|
|
109
109
|
[encoder_bev_decision.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/encoder_bev_decision.ipynb) (ModernBERT);
|
|
110
110
|
- typed-decisions, soft labels from several raters:
|
|
111
111
|
[decoder_typed_decisions.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/decoder_typed_decisions.ipynb) (Qwen3) and
|
|
112
|
-
[encoder_typed_decisions.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/encoder_typed_decisions.ipynb) (ModernBERT)
|
|
112
|
+
[encoder_typed_decisions.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/encoder_typed_decisions.ipynb) (ModernBERT);
|
|
113
|
+
- measuring `ordinal_weight` for score questions on typed-decisions:
|
|
114
|
+
[encoder_typed_decisions_ordinal_weight.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/encoder_typed_decisions_ordinal_weight.ipynb).
|
|
113
115
|
|
|
114
116
|
Notebooks other than the quickstart are named `<model type>_<dataset>.ipynb`.
|
|
115
117
|
|
|
116
118
|
## Status
|
|
117
119
|
|
|
118
|
-
This is an early version (0.
|
|
120
|
+
This is an early version (0.3). Training covers every `tuning` mode:
|
|
119
121
|
|
|
120
122
|
- `"head"`: the backbone is frozen, its outputs are computed once and cached (in memory, and on
|
|
121
123
|
disk with `cache_dir`), and only the decision head trains;
|
|
122
124
|
- `"lora"`, `"qlora"` and `"full"`: adapters or whole layers of the backbone train along with the
|
|
123
125
|
head. These run on a laptop GPU (Apple MPS) for small datasets; a CUDA GPU is recommended for
|
|
124
126
|
full-size runs. `"qlora"` needs bitsandbytes and an NVIDIA GPU and falls back to `"lora"`
|
|
125
|
-
elsewhere, with a warning. It has trained in real 4-bit on a Colab GPU.
|
|
127
|
+
elsewhere, with a warning. It has trained, saved and reloaded in real 4-bit on a Colab GPU.
|
|
126
128
|
|
|
127
129
|
`DecoderModel` (Qwen3) defaults to `"qlora"`. `EncoderModel` (ModernBERT) defaults to `"full"`:
|
|
128
130
|
adapters barely moved ModernBERT, while full tuning reached 0.79 test accuracy on typed-decisions.
|
|
129
131
|
|
|
130
132
|
Training stops at `epochs`, by early stopping, or at `target_loss`. A run that is interrupted
|
|
131
|
-
(Ctrl-C), or saved and loaded, resumes exactly where it stopped.
|
|
132
|
-
|
|
133
|
+
(Ctrl-C), or saved and loaded, resumes exactly where it stopped. With `checkpoint_dir`, `fit` also
|
|
134
|
+
saves the model when it ends or is interrupted, and every `checkpoint_every` steps if set, so a
|
|
135
|
+
disconnected notebook can pick the run up again; nothing is written to disk by default.
|
|
136
|
+
|
|
137
|
+
Score questions add an ordinal term to the loss (`ordinal_weight`), so a near miss costs less than
|
|
138
|
+
a far one. After training, probabilities are calibrated with temperatures fitted on the validation
|
|
139
|
+
slice, so `answer_confidence` matches how often answers are right; reports include Brier score and
|
|
140
|
+
expected calibration error, before and after calibration. `head="full"` is designed but not yet
|
|
141
|
+
implemented.
|
|
133
142
|
|
|
134
143
|
See [docs/DESIGN.md](https://github.com/ForwardMetric/sysone/blob/main/docs/DESIGN.md) for the full design: every parameter, the layouts,
|
|
135
144
|
architectures, heads, training and checkpoint format.
|
|
@@ -73,27 +73,36 @@ Example notebooks, each comparing accuracy before and after training:
|
|
|
73
73
|
[encoder_bev_decision.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/encoder_bev_decision.ipynb) (ModernBERT);
|
|
74
74
|
- typed-decisions, soft labels from several raters:
|
|
75
75
|
[decoder_typed_decisions.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/decoder_typed_decisions.ipynb) (Qwen3) and
|
|
76
|
-
[encoder_typed_decisions.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/encoder_typed_decisions.ipynb) (ModernBERT)
|
|
76
|
+
[encoder_typed_decisions.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/encoder_typed_decisions.ipynb) (ModernBERT);
|
|
77
|
+
- measuring `ordinal_weight` for score questions on typed-decisions:
|
|
78
|
+
[encoder_typed_decisions_ordinal_weight.ipynb](https://github.com/ForwardMetric/sysone/blob/main/examples/encoder_typed_decisions_ordinal_weight.ipynb).
|
|
77
79
|
|
|
78
80
|
Notebooks other than the quickstart are named `<model type>_<dataset>.ipynb`.
|
|
79
81
|
|
|
80
82
|
## Status
|
|
81
83
|
|
|
82
|
-
This is an early version (0.
|
|
84
|
+
This is an early version (0.3). Training covers every `tuning` mode:
|
|
83
85
|
|
|
84
86
|
- `"head"`: the backbone is frozen, its outputs are computed once and cached (in memory, and on
|
|
85
87
|
disk with `cache_dir`), and only the decision head trains;
|
|
86
88
|
- `"lora"`, `"qlora"` and `"full"`: adapters or whole layers of the backbone train along with the
|
|
87
89
|
head. These run on a laptop GPU (Apple MPS) for small datasets; a CUDA GPU is recommended for
|
|
88
90
|
full-size runs. `"qlora"` needs bitsandbytes and an NVIDIA GPU and falls back to `"lora"`
|
|
89
|
-
elsewhere, with a warning. It has trained in real 4-bit on a Colab GPU.
|
|
91
|
+
elsewhere, with a warning. It has trained, saved and reloaded in real 4-bit on a Colab GPU.
|
|
90
92
|
|
|
91
93
|
`DecoderModel` (Qwen3) defaults to `"qlora"`. `EncoderModel` (ModernBERT) defaults to `"full"`:
|
|
92
94
|
adapters barely moved ModernBERT, while full tuning reached 0.79 test accuracy on typed-decisions.
|
|
93
95
|
|
|
94
96
|
Training stops at `epochs`, by early stopping, or at `target_loss`. A run that is interrupted
|
|
95
|
-
(Ctrl-C), or saved and loaded, resumes exactly where it stopped.
|
|
96
|
-
|
|
97
|
+
(Ctrl-C), or saved and loaded, resumes exactly where it stopped. With `checkpoint_dir`, `fit` also
|
|
98
|
+
saves the model when it ends or is interrupted, and every `checkpoint_every` steps if set, so a
|
|
99
|
+
disconnected notebook can pick the run up again; nothing is written to disk by default.
|
|
100
|
+
|
|
101
|
+
Score questions add an ordinal term to the loss (`ordinal_weight`), so a near miss costs less than
|
|
102
|
+
a far one. After training, probabilities are calibrated with temperatures fitted on the validation
|
|
103
|
+
slice, so `answer_confidence` matches how often answers are right; reports include Brier score and
|
|
104
|
+
expected calibration error, before and after calibration. `head="full"` is designed but not yet
|
|
105
|
+
implemented.
|
|
97
106
|
|
|
98
107
|
See [docs/DESIGN.md](https://github.com/ForwardMetric/sysone/blob/main/docs/DESIGN.md) for the full design: every parameter, the layouts,
|
|
99
108
|
architectures, heads, training and checkpoint format.
|
|
@@ -189,6 +189,7 @@ DecoderModel(
|
|
|
189
189
|
|
|
190
190
|
# objective and data
|
|
191
191
|
loss="soft-ce", # "soft-ce" | "rlcd"
|
|
192
|
+
ordinal_weight=1.0, # how strongly score questions respect level order (RPS term); 0 = off
|
|
192
193
|
label_smoothing=0.0,
|
|
193
194
|
shuffle_options="auto", # "auto" | None | list of question types
|
|
194
195
|
balance=None, # None | "category" | "question"
|
|
@@ -200,6 +201,8 @@ DecoderModel(
|
|
|
200
201
|
cache_max_gb=5, # size cap for cache_dir; least recently used sets are deleted first
|
|
201
202
|
precision="auto", # "auto" | "fp32" | "bf16" | "fp16"
|
|
202
203
|
gradient_checkpointing="auto",
|
|
204
|
+
checkpoint_dir=None, # folder fit saves to when training ends or is interrupted; None = never
|
|
205
|
+
checkpoint_every=None, # also save every this many optimizer steps; None = only at the end
|
|
203
206
|
device="auto", # "auto" | "cuda" | "mps" | "cpu"
|
|
204
207
|
seed=0,
|
|
205
208
|
verbose=1,
|
|
@@ -313,6 +316,8 @@ nothing scales them with the data, which would be guesswork that fails silently.
|
|
|
313
316
|
`"lora"` / `"qlora"` give a warning that they are ignored.
|
|
314
317
|
- `cache_dir` with a backbone that learns gives a warning that it is ignored: features can only
|
|
315
318
|
be cached when the backbone is frozen.
|
|
319
|
+
- `ordinal_weight` must be a number of at least 0; `checkpoint_every` must be None or a positive
|
|
320
|
+
integer, and needs `checkpoint_dir`.
|
|
316
321
|
- `target_loss` must be a positive number or None; `validation_interval` must be `"auto"` or a
|
|
317
322
|
positive integer; `cache_max_gb` must be positive.
|
|
318
323
|
|
|
@@ -553,8 +558,8 @@ can then be used or saved, and a later `fit` resumes from the last completed ste
|
|
|
553
558
|
|
|
554
559
|
**Frozen backbone (`tuning="head"`).** Nothing in the backbone learns, so:
|
|
555
560
|
- the backbone runs without gradients and in eval mode (no dropout);
|
|
556
|
-
- its weights are loaded in bf16 where the device supports
|
|
557
|
-
|
|
561
|
+
- its weights are loaded in half precision, which halves memory: bf16 where the device supports
|
|
562
|
+
it natively, fp16 on CUDA GPUs without native bf16 (such as the T4), otherwise fp32;
|
|
558
563
|
- with `cache_features="auto"`, the option and summary vectors for every example are computed
|
|
559
564
|
once and cached on the CPU in half precision. Every epoch then trains the head on the cache,
|
|
560
565
|
without running the backbone again.
|
|
@@ -565,9 +570,10 @@ fit in memory, the backbone is run again each epoch instead.
|
|
|
565
570
|
|
|
566
571
|
**Backbone that learns (`"lora"`, `"qlora"`, `"full"`).**
|
|
567
572
|
- Weights being trained (adapters, fully trained layers, embeddings) are kept in fp32 for the
|
|
568
|
-
optimizer. The forward pass runs under
|
|
569
|
-
|
|
570
|
-
|
|
573
|
+
optimizer. The forward pass runs under autocast to the backbone's half precision (bf16, or fp16
|
|
574
|
+
on CUDA GPUs without native bf16). With fp16 the loss is scaled before the backward pass, so
|
|
575
|
+
small gradients don't round to zero. With `"qlora"` the frozen weights are stored in 4-bit; with
|
|
576
|
+
`"lora"` and `"full"` they are loaded in half precision, since they are never updated.
|
|
571
577
|
- Gradient checkpointing (`"auto"`) turns on when sequences are long enough that activations,
|
|
572
578
|
not weights, dominate memory: when the estimated activations of one forward pass (about
|
|
573
579
|
`34·s·h + 5·a·s²` bytes per example and layer, at the 95th-percentile length, for the layers
|
|
@@ -607,20 +613,69 @@ and never copied by sysone.
|
|
|
607
613
|
|
|
608
614
|
Losses:
|
|
609
615
|
- `soft-ce` (default): cross-entropy against the target distribution, over each row's own
|
|
610
|
-
options. For hard labels this is ordinary cross-entropy.
|
|
616
|
+
options. For hard labels this is ordinary cross-entropy. Score questions add a
|
|
617
|
+
ranked-probability term (below).
|
|
611
618
|
- `rlcd`: a policy-gradient term over noisy logit samples, rewarded by a proper scoring rule
|
|
612
619
|
(log, spherical, and ranked-probability for score questions), plus soft cross-entropy.
|
|
613
620
|
|
|
621
|
+
**Score questions are ordered.** Cross-entropy treats a score's levels as unrelated categories:
|
|
622
|
+
predicting level 2 when the answer is 3 costs as much as predicting 0. For score questions the
|
|
623
|
+
loss adds `ordinal_weight` times the **ranked probability score** (RPS), the squared distance between the
|
|
624
|
+
predicted and target cumulative distributions, averaged over the `K − 1` thresholds between
|
|
625
|
+
levels:
|
|
626
|
+
|
|
627
|
+
```
|
|
628
|
+
RPS = 1/(K−1) · Σ_{k=1}^{K−1} (P(level < k) − T(level < k))²
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
It is 0 for a perfect prediction and at most 1, whatever the number of levels, and near misses
|
|
632
|
+
cost less than far ones. It is a proper scoring rule, so like cross-entropy it is minimised only
|
|
633
|
+
by honest probabilities, which keeps it compatible with calibration; soft rater targets work as
|
|
634
|
+
they are. Cross-entropy stays in the loss because RPS alone gives a weak signal when the model is
|
|
635
|
+
confidently wrong. Choice and noul questions are unaffected, and so are the head and the
|
|
636
|
+
checkpoint format.
|
|
637
|
+
|
|
638
|
+
`ordinal_weight` names what it controls rather than the method, so the method can change without a
|
|
639
|
+
rename. Its default, 1.0, weights the two terms equally. Equal weight is not equal influence:
|
|
640
|
+
cross-entropy on score questions is typically around 1, while RPS, a squared gap between
|
|
641
|
+
probabilities, is typically 0.05 to 0.2, so cross-entropy still dominates at 1.0. Measured on
|
|
642
|
+
typed-decisions (see milestone 3), the term helps a frozen backbone and is neutral with full
|
|
643
|
+
tuning, so the default stays at 1.0.
|
|
644
|
+
|
|
645
|
+
One weight applies to every score question in the data. That suits most datasets, because RPS is
|
|
646
|
+
on the same 0-to-1 scale for any number of levels, so a 4-level weather scale and a 10-level spam
|
|
647
|
+
scale count evenly. Different score questions could still want different weights, for example a
|
|
648
|
+
scale raters disagree on more. The planned extension, built only if measurements call for it, is
|
|
649
|
+
a dict keyed like `report_["by_question"]`:
|
|
650
|
+
|
|
651
|
+
```python
|
|
652
|
+
DecoderModel(ordinal_weight={"severity": 2.0, "operations/spam": 0.5, "*": 1.0})
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Keys are a question id, or `category/qid` where ids repeat across categories, with `"*"` for all
|
|
656
|
+
other score questions. A plain number keeps meaning "this weight for every score question", so the
|
|
657
|
+
extension changes nothing for existing code. The weight stays a model setting rather than a key
|
|
658
|
+
inside question definitions, which `predict` and the Jev contract share.
|
|
659
|
+
|
|
614
660
|
### Calibration
|
|
615
661
|
|
|
616
|
-
With `calibrate=True`, temperatures are fitted after training on the validation slice
|
|
617
|
-
|
|
662
|
+
With `calibrate=True`, temperatures are fitted after training on the validation slice: logits are
|
|
663
|
+
divided by a temperature `T`, chosen to minimise the cross-entropy against the validation targets
|
|
664
|
+
(soft targets as they are), with LBFGS on `log T`:
|
|
618
665
|
- one temperature per question type;
|
|
619
|
-
- one per (type, option-count bucket), with buckets 2, 3–5, 6–10 and 11+,
|
|
620
|
-
|
|
666
|
+
- one per (type, option-count bucket), with buckets 2, 3–5, 6–10 and 11+, for buckets with at least
|
|
667
|
+
50 validation questions; smaller buckets use their type's temperature;
|
|
668
|
+
- a type with fewer than 50 validation questions keeps `T = 1`, and the log says so;
|
|
621
669
|
- every temperature clamped to [0.5, 5].
|
|
622
670
|
|
|
623
|
-
|
|
671
|
+
Temperatures are refitted at the end of every `fit`, including a resumed one, and saved with the
|
|
672
|
+
model. `predict` and `evaluate` apply them, so `probabilities` and `answer_confidence` are
|
|
673
|
+
calibrated; `answer_confidence` is the calibrated probability of the answer given (the top option,
|
|
674
|
+
or for noul the larger of P(true) and P(false)). `report_` records the temperatures and the
|
|
675
|
+
validation slice's expected calibration error before and after.
|
|
676
|
+
|
|
677
|
+
A per-category temperature is optional, for categories with enough validation rows (not in
|
|
678
|
+
milestone 3).
|
|
624
679
|
|
|
625
680
|
The same slice also chose the stopping epoch. Each temperature is a single number per bucket, so
|
|
626
681
|
reusing the slice biases it very little, and it avoids holding back a second slice of the data.
|
|
@@ -651,7 +706,14 @@ A report is a plain dict: `n_questions`, `skipped`, and the metrics `overall`, `
|
|
|
651
706
|
- **score_error**: for score questions, the absolute gap between predicted and target expected
|
|
652
707
|
level.
|
|
653
708
|
- **auc**: for noul questions, when both labels occur.
|
|
654
|
-
- **
|
|
709
|
+
- **brier**: the squared distance between predicted and target distributions, summed over a
|
|
710
|
+
question's options and averaged over questions; lower is better.
|
|
711
|
+
- **ece**: expected calibration error of the top option. Questions are grouped into 15 equal-width
|
|
712
|
+
bins by the predicted probability of their top option; in each bin, that probability is compared
|
|
713
|
+
with how often the top option is right, and the gaps are averaged, weighted by bin size.
|
|
714
|
+
|
|
715
|
+
`model.evaluate(data, calibrated=False)` scores the probabilities before temperature scaling, so
|
|
716
|
+
the effect of calibration shows as two rows in `compare`.
|
|
655
717
|
|
|
656
718
|
Baselines:
|
|
657
719
|
- **random guess**: every option equally likely.
|
|
@@ -710,6 +772,37 @@ applies when it starts, e.g. `resuming at epoch 12 of 20` or
|
|
|
710
772
|
Either way, a structural parameter changed since the last fit (base model, layers, layout, head
|
|
711
773
|
shape, token limits, tuning mode) starts over instead, as before.
|
|
712
774
|
|
|
775
|
+
#### Autosave
|
|
776
|
+
|
|
777
|
+
A long run on a cloud notebook can be lost to a disconnect. Autosave is off by default, so a
|
|
778
|
+
default run spends no time writing to disk. It is turned on with `checkpoint_dir`:
|
|
779
|
+
|
|
780
|
+
| settings | what is saved |
|
|
781
|
+
|---|---|
|
|
782
|
+
| neither (default) | nothing |
|
|
783
|
+
| `checkpoint_dir` | when training ends or is interrupted (Ctrl-C, or an error during training) |
|
|
784
|
+
| `checkpoint_dir` and `checkpoint_every=N` | also every `N` optimizer steps |
|
|
785
|
+
| `checkpoint_every` alone | rejected: there is nowhere to save |
|
|
786
|
+
|
|
787
|
+
- **How often:** a full-tuning training state can be gigabytes, and writing it to a network drive
|
|
788
|
+
takes time, so `checkpoint_every` is a trade-off between time spent saving and work lost to a
|
|
789
|
+
disconnect. The log shows the size and duration of each save to help choose it.
|
|
790
|
+
- **What:** a complete `save(checkpoint_dir, training_state=True)`. The model files hold the best
|
|
791
|
+
weights so far; the training state holds the last step's. `Model.load(checkpoint_dir)` gives a
|
|
792
|
+
usable model at any time, and `Model.load(checkpoint_dir).fit(<the same data>)` resumes the run
|
|
793
|
+
exactly.
|
|
794
|
+
- **Safely:** each save is written to a temporary folder next to `checkpoint_dir` and swapped in
|
|
795
|
+
when complete, so a disconnect during a save leaves the previous checkpoint intact. Only the
|
|
796
|
+
latest checkpoint is kept.
|
|
797
|
+
- **Restart information:** each save also writes `run.json`, a small readable summary for picking
|
|
798
|
+
the run up again: the model class, sysone version, step and epoch reached out of `epochs`, the
|
|
799
|
+
best validation loss and its step, when it was saved, the training data's fingerprint, the data
|
|
800
|
+
file's path when `fit` was given a path, and the resume command. `fit` logs the folder and the
|
|
801
|
+
interval when it starts.
|
|
802
|
+
|
|
803
|
+
The data itself is never copied into the checkpoint; resuming needs the same data, checked
|
|
804
|
+
against the fingerprint (a different dataset warm-starts, as above).
|
|
805
|
+
|
|
713
806
|
## Repository layout
|
|
714
807
|
|
|
715
808
|
```
|
|
@@ -728,8 +821,8 @@ src/sysone/
|
|
|
728
821
|
layout.py # parallel and sequential position ids, option ids, masks
|
|
729
822
|
heads.py # compact and full decision heads
|
|
730
823
|
tuning.py # layer plans, freezing, LoRA / QLoRA by layer index (peft, bitsandbytes)
|
|
731
|
-
losses.py # soft_ce, rlcd
|
|
732
|
-
calibration.py # temperature fitting
|
|
824
|
+
losses.py # soft_ce, the ranked-probability term for score questions, rlcd
|
|
825
|
+
calibration.py # temperature fitting, ECE
|
|
733
826
|
training.py # training loop, validation split, balancing, stopping, resumable state
|
|
734
827
|
features.py # cached option and summary vectors for a frozen backbone
|
|
735
828
|
cache.py # the on-disk feature cache: sets, size cap, cache_info, clear_cache
|
|
@@ -741,7 +834,9 @@ examples/ # notebooks: quickstart, public datasets with hard and so
|
|
|
741
834
|
docs/
|
|
742
835
|
```
|
|
743
836
|
|
|
744
|
-
Dependencies are torch
|
|
837
|
+
Dependencies are torch (2.8 or later: training the backbone on Apple GPUs fails in earlier
|
|
838
|
+
versions), transformers (5.1 or later: 5.0's ModernBERT ignores per-layer-type masks) and peft. The
|
|
839
|
+
minimums in `pyproject.toml` are the ones CI tests, by installing the lowest allowed versions. Because QLoRA is the default, bitsandbytes is
|
|
745
840
|
also a regular dependency on platforms where it supports 4-bit training, selected with a
|
|
746
841
|
platform marker in `pyproject.toml`. Elsewhere it is not installed and the fallback to `"lora"`
|
|
747
842
|
applies.
|
|
@@ -760,7 +855,7 @@ applies.
|
|
|
760
855
|
the two architectures, the compact head, the in-memory feature cache, `fit` / `predict` /
|
|
761
856
|
`save` / `load`, `evaluate` / `baselines` / `compare`, and three example notebooks.
|
|
762
857
|
|
|
763
|
-
### Milestone 2: training the backbone
|
|
858
|
+
### Milestone 2: training the backbone (done, 0.2.0)
|
|
764
859
|
|
|
765
860
|
**Goal:** let the backbone learn, not only the head, and make long runs manageable: stopping
|
|
766
861
|
when good enough, resuming where a run left off, and not repeating the backbone pass for
|
|
@@ -795,16 +890,79 @@ barely moved ModernBERT; see `tuning`).
|
|
|
795
890
|
questions (best measured once backbone training is in, so the two effects aren't mixed), and
|
|
796
891
|
calibration.
|
|
797
892
|
|
|
798
|
-
### Milestone 3:
|
|
893
|
+
### Milestone 3: better scores, calibrated probabilities, sturdier long runs (done, 0.3.0)
|
|
894
|
+
|
|
895
|
+
**Goal:** close the largest measured accuracy gap (score questions), make the reported
|
|
896
|
+
probabilities mean what they say, and make long GPU runs survive disconnects and older GPUs.
|
|
897
|
+
Release as 0.3.0.
|
|
799
898
|
|
|
800
|
-
|
|
801
|
-
|
|
899
|
+
**In scope:**
|
|
900
|
+
1. **Ranked-probability term for score questions** (`ordinal_weight`; see Losses). It changes the
|
|
901
|
+
model's outputs, so it comes before calibration.
|
|
902
|
+
2. **Calibration** as described under Calibration: temperatures per type and option-count bucket,
|
|
903
|
+
fitted on the validation slice, saved and applied; a calibrated `answer_confidence`.
|
|
904
|
+
3. **Reporting:** `brier` and `ece` in every report, `evaluate(..., calibrated=False)`, and the
|
|
905
|
+
calibration summary in `report_`.
|
|
906
|
+
4. **Autosave**, off by default (`checkpoint_dir`, `checkpoint_every`, `run.json`; see Autosave).
|
|
907
|
+
5. **GPUs without native bf16** (for example the T4): `precision="auto"` uses fp16 there, with loss
|
|
908
|
+
scaling when the backbone learns, instead of slow emulated bf16. Native support is checked
|
|
909
|
+
without counting emulation.
|
|
910
|
+
6. **Dependency minimums** raised to versions that are tested, and tested in CI.
|
|
911
|
+
7. **CI:** GitHub Actions runs ruff and the fast tests on every pull request, on CPU-only torch,
|
|
912
|
+
with both the newest dependencies and the declared minimums. (Publishing from CI is left for
|
|
913
|
+
later.)
|
|
914
|
+
8. **Tests:** RPS against hand-computed values (hard and soft targets, 2 to 11 levels); temperature
|
|
915
|
+
fitting recovers a known temperature on synthetic logits; buckets fall back below 50
|
|
916
|
+
questions; ECE and Brier against hand-computed values; autosave writes atomically, keeps the
|
|
917
|
+
best weights in the model files, resumes exactly from an autosave, and saves on interrupt;
|
|
918
|
+
fp16 with loss scaling takes a training step (CUDA only).
|
|
919
|
+
9. **Measurements** on a CUDA GPU, recorded in the notebooks' saved outputs and in this document:
|
|
920
|
+
- `ordinal_weight`: score error and exact-level accuracy for 0, 0.5, 1 and 2 on typed-decisions and
|
|
921
|
+
bev-decision, to set the default, also per score question (`by_question`) to see whether
|
|
922
|
+
different questions want different weights (the dict extension under Losses);
|
|
923
|
+
- three seeds of fully tuned ModernBERT-base on typed-decisions (the 0.79 result), with
|
|
924
|
+
accuracy per question type;
|
|
925
|
+
- the unmeasured learning-rate defaults: ModernBERT adapters and Qwen3 full tuning;
|
|
926
|
+
- saving and reloading a 4-bit QLoRA model;
|
|
927
|
+
- gradient checkpointing's cost on a GPU, to revisit the `"auto"` rule.
|
|
928
|
+
|
|
929
|
+
**Done when:** all of the above is in and tested, the notebooks show score error, Brier and ECE
|
|
930
|
+
before and after calibration, the 0.79 result holds across seeds, and 0.3.0 is on PyPI with the
|
|
931
|
+
notebooks installing `sysone>=0.3`.
|
|
932
|
+
|
|
933
|
+
**Measured** (ModernBERT-base on typed-decisions, Colab L4, one seed;
|
|
934
|
+
`examples/encoder_typed_decisions_ordinal_weight.ipynb`):
|
|
935
|
+
|
|
936
|
+
| run | accuracy | score accuracy | score error | minutes |
|
|
937
|
+
|---|---|---|---|---|
|
|
938
|
+
| head, `ordinal_weight=0` | — | 0.4262 | 0.5686 | — |
|
|
939
|
+
| head, `ordinal_weight=2` | — | 0.4388 | 0.5255 | — |
|
|
940
|
+
| full, `ordinal_weight=0` | 0.7865 | 0.7662 | 0.2253 | 20.9 |
|
|
941
|
+
| full, `ordinal_weight=2` | 0.7870 | 0.7700 | 0.2240 | 21.0 |
|
|
942
|
+
|
|
943
|
+
- `ordinal_weight`: with the backbone frozen, 2 lowers score error by 7.6% and 0.5 and 1 change
|
|
944
|
+
nothing; with full tuning, 2 changes score error by 0.6% and accuracy not at all. The default
|
|
945
|
+
stays at 1.0. Per score question the full-tuning changes go both ways (−17% to +11%), within
|
|
946
|
+
one-seed noise, so per-question weights are not needed. Soft labels from several raters already
|
|
947
|
+
carry most of the order information once the backbone trains.
|
|
948
|
+
- The 0.79 result held in both full-tuning runs above (same seed).
|
|
949
|
+
- `"qlora"` trained, saved and reloaded in real 4-bit (NF4 projections, fp32 adapters, the
|
|
950
|
+
ModernBERT-base backbone at 130 MB) with identical predictions.
|
|
951
|
+
- fp16 with loss scaling trained, saved and reloaded on a CUDA GPU with identical predictions;
|
|
952
|
+
the CUDA-only tests pass, including real 4-bit loading.
|
|
953
|
+
|
|
954
|
+
**Moved to milestone 4:** the other seeds of fully tuned ModernBERT; the learning-rate defaults
|
|
955
|
+
for ModernBERT adapters and Qwen3 full tuning; gradient checkpointing's cost on a GPU;
|
|
956
|
+
`ordinal_weight` on bev-decision; a CI job on a GPU.
|
|
957
|
+
|
|
958
|
+
**Not in milestone 3:** per-question `ordinal_weight` (unless the measurements call for it);
|
|
959
|
+
per-category temperatures; reading the encoder summary as an average
|
|
960
|
+
instead of `[CLS]` (full tuning already works for ModernBERT); more base models (Llama, Mistral);
|
|
961
|
+
training one model on many decision datasets before specialising; publishing from CI;
|
|
962
|
+
`head="full"`; `loss="rlcd"`.
|
|
802
963
|
|
|
803
964
|
## Open items
|
|
804
965
|
|
|
805
|
-
- `"qlora"` has trained and evaluated in real 4-bit on a Colab NVIDIA GPU (Qwen3-0.6B on
|
|
806
|
-
typed-decisions). Saving and reloading a 4-bit model has only been tested through the fallback;
|
|
807
|
-
it still needs a run on an NVIDIA GPU.
|
|
808
966
|
- Whether bitsandbytes supports 4-bit training on Apple Silicon yet needs checking. If it does,
|
|
809
967
|
the fallback there can be dropped.
|
|
810
968
|
- A state-first causal layout, which would let one state's cache be shared across its questions,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"import importlib.util\n",
|
|
40
40
|
"\n",
|
|
41
41
|
"if importlib.util.find_spec(\"sysone\") is None:\n",
|
|
42
|
-
" %pip install -q \"sysone>=0.
|
|
42
|
+
" %pip install -q \"sysone>=0.3\""
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -223,7 +223,14 @@
|
|
|
223
223
|
" different).\n",
|
|
224
224
|
"- **log_loss**: cross-entropy against the label; lower is better.\n",
|
|
225
225
|
"- **score_error**: for score questions, the gap between predicted and true expected level.\n",
|
|
226
|
-
"- **auc**: for yes/no questions, how well the model ranks yes above no (0.5 = chance)
|
|
226
|
+
"- **auc**: for yes/no questions, how well the model ranks yes above no (0.5 = chance).\n",
|
|
227
|
+
"- **brier**: squared distance between the predicted probabilities and the label; lower is better.\n",
|
|
228
|
+
"- **ece**: expected calibration error, how far \"says X% sure\" is from \"right X% of the time\";\n",
|
|
229
|
+
" lower is better.\n",
|
|
230
|
+
"\n",
|
|
231
|
+
"Probabilities are calibrated after training: a temperature per question type, fitted on the\n",
|
|
232
|
+
"validation slice, softens over-confident answers. This changes **log_loss**, **brier** and **ece**,\n",
|
|
233
|
+
"never **accuracy**, since the top answer stays the same."
|
|
227
234
|
],
|
|
228
235
|
"id": "cell-10"
|
|
229
236
|
},
|
|
@@ -237,6 +244,27 @@
|
|
|
237
244
|
],
|
|
238
245
|
"id": "cell-11"
|
|
239
246
|
},
|
|
247
|
+
{
|
|
248
|
+
"cell_type": "markdown",
|
|
249
|
+
"metadata": {},
|
|
250
|
+
"source": [
|
|
251
|
+
"### Calibration\n",
|
|
252
|
+
"\n",
|
|
253
|
+
"The same model before and after temperature scaling. Accuracy is identical; the probabilities, and\n",
|
|
254
|
+
"so **log_loss**, **brier** and **ece**, change. `lora.report_[\"calibration\"]` has the fitted\n",
|
|
255
|
+
"temperatures and the validation slice's ECE before and after."
|
|
256
|
+
]
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"cell_type": "code",
|
|
260
|
+
"metadata": {},
|
|
261
|
+
"execution_count": null,
|
|
262
|
+
"outputs": [],
|
|
263
|
+
"source": [
|
|
264
|
+
"calibration = {\"lora\": results[\"lora\"], \"lora, before calibration\": lora.evaluate(test, calibrated=False)}\n",
|
|
265
|
+
"compare(calibration, by=\"overall\")[[\"accuracy\", \"log_loss\", \"brier\", \"ece\"]]"
|
|
266
|
+
]
|
|
267
|
+
},
|
|
240
268
|
{
|
|
241
269
|
"cell_type": "markdown",
|
|
242
270
|
"metadata": {},
|
|
@@ -267,8 +295,9 @@
|
|
|
267
295
|
"Training only the head on a frozen backbone is the cheapest setting and the least accurate: the\n",
|
|
268
296
|
"backbone was never adapted to these questions. Expect clear gains over random guessing, and LoRA and\n",
|
|
269
297
|
"QLoRA to do better than head-only on the same rows. QLoRA should land close to LoRA; a gap of a\n",
|
|
270
|
-
"point or two is within run-to-run noise. Score questions are the hardest
|
|
271
|
-
"
|
|
298
|
+
"point or two is within run-to-run noise. Score questions are the hardest. For them the loss adds an\n",
|
|
299
|
+
"ordinal term (`ordinal_weight`), so predicting a level next to the right one costs less than one far\n",
|
|
300
|
+
"away; **score_error** shows how far off the predicted level is on average.\n",
|
|
272
301
|
"\n",
|
|
273
302
|
"Things to try: more training rows (`N_TRAIN`), more `BACKBONE_EPOCHS`, a different `num_layers`\n",
|
|
274
303
|
"(default 0.7 of Qwen's 28 layers), or `lora_targets=\"all\"`. A long run stopped with Ctrl-C continues\n",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"import importlib.util\n",
|
|
42
42
|
"\n",
|
|
43
43
|
"if importlib.util.find_spec(\"sysone\") is None:\n",
|
|
44
|
-
" %pip install -q \"sysone>=0.
|
|
44
|
+
" %pip install -q \"sysone>=0.3\""
|
|
45
45
|
]
|
|
46
46
|
},
|
|
47
47
|
{
|
|
@@ -225,6 +225,13 @@
|
|
|
225
225
|
"- **log_loss**: cross-entropy against the label; lower is better.\n",
|
|
226
226
|
"- **score_error**: for score questions, the gap between predicted and true expected level.\n",
|
|
227
227
|
"- **auc**: for yes/no questions, how well the model ranks yes above no (0.5 = chance).\n",
|
|
228
|
+
"- **brier**: squared distance between the predicted probabilities and the label; lower is better.\n",
|
|
229
|
+
"- **ece**: expected calibration error, how far \"says X% sure\" is from \"right X% of the time\";\n",
|
|
230
|
+
" lower is better.\n",
|
|
231
|
+
"\n",
|
|
232
|
+
"Probabilities are calibrated after training: a temperature per question type, fitted on the\n",
|
|
233
|
+
"validation slice, softens over-confident answers. This changes **log_loss**, **brier** and **ece**,\n",
|
|
234
|
+
"never **accuracy**, since the top answer stays the same.\n",
|
|
228
235
|
"\n",
|
|
229
236
|
"With soft labels, **distance** and **log_loss** matter as much as accuracy: when raters split\n",
|
|
230
237
|
"45/55, either answer is defensible, but the probabilities should reflect the split."
|
|
@@ -241,6 +248,27 @@
|
|
|
241
248
|
],
|
|
242
249
|
"id": "cell-11"
|
|
243
250
|
},
|
|
251
|
+
{
|
|
252
|
+
"cell_type": "markdown",
|
|
253
|
+
"metadata": {},
|
|
254
|
+
"source": [
|
|
255
|
+
"### Calibration\n",
|
|
256
|
+
"\n",
|
|
257
|
+
"The same model before and after temperature scaling. Accuracy is identical; the probabilities, and\n",
|
|
258
|
+
"so **log_loss**, **brier** and **ece**, change. `lora.report_[\"calibration\"]` has the fitted\n",
|
|
259
|
+
"temperatures and the validation slice's ECE before and after."
|
|
260
|
+
]
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"cell_type": "code",
|
|
264
|
+
"metadata": {},
|
|
265
|
+
"execution_count": null,
|
|
266
|
+
"outputs": [],
|
|
267
|
+
"source": [
|
|
268
|
+
"calibration = {\"lora\": results[\"lora\"], \"lora, before calibration\": lora.evaluate(test, calibrated=False)}\n",
|
|
269
|
+
"compare(calibration, by=\"overall\")[[\"accuracy\", \"log_loss\", \"brier\", \"ece\"]]"
|
|
270
|
+
]
|
|
271
|
+
},
|
|
244
272
|
{
|
|
245
273
|
"cell_type": "markdown",
|
|
246
274
|
"metadata": {},
|
|
@@ -269,8 +297,8 @@
|
|
|
269
297
|
"- **average answer** is the bar to beat. With a frozen backbone, the head has limited room to get\n",
|
|
270
298
|
" past it; fine-tuning the backbone with LoRA or QLoRA is what should move these numbers further.\n",
|
|
271
299
|
"- QLoRA should land close to LoRA; a gap of a point or two is within run-to-run noise.\n",
|
|
272
|
-
"- Probabilities are
|
|
273
|
-
" **log_loss**
|
|
300
|
+
"- Probabilities are calibrated on the validation slice, which shows in **ece**, **brier** and\n",
|
|
301
|
+
" **log_loss** (see the calibration table), not in accuracy.\n",
|
|
274
302
|
"- A long run stopped with Ctrl-C continues from where it stopped with `lora.fit(train)`."
|
|
275
303
|
]
|
|
276
304
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"import importlib.util\n",
|
|
40
40
|
"\n",
|
|
41
41
|
"if importlib.util.find_spec(\"sysone\") is None:\n",
|
|
42
|
-
" %pip install -q \"sysone>=0.
|
|
42
|
+
" %pip install -q \"sysone>=0.3\""
|
|
43
43
|
]
|
|
44
44
|
},
|
|
45
45
|
{
|
|
@@ -194,7 +194,14 @@
|
|
|
194
194
|
" different).\n",
|
|
195
195
|
"- **log_loss**: cross-entropy against the label; lower is better.\n",
|
|
196
196
|
"- **score_error**: for score questions, the gap between predicted and true expected level.\n",
|
|
197
|
-
"- **auc**: for yes/no questions, how well the model ranks yes above no (0.5 = chance)
|
|
197
|
+
"- **auc**: for yes/no questions, how well the model ranks yes above no (0.5 = chance).\n",
|
|
198
|
+
"- **brier**: squared distance between the predicted probabilities and the label; lower is better.\n",
|
|
199
|
+
"- **ece**: expected calibration error, how far \"says X% sure\" is from \"right X% of the time\";\n",
|
|
200
|
+
" lower is better.\n",
|
|
201
|
+
"\n",
|
|
202
|
+
"Probabilities are calibrated after training: a temperature per question type, fitted on the\n",
|
|
203
|
+
"validation slice, softens over-confident answers. This changes **log_loss**, **brier** and **ece**,\n",
|
|
204
|
+
"never **accuracy**, since the top answer stays the same."
|
|
198
205
|
],
|
|
199
206
|
"id": "cell-10"
|
|
200
207
|
},
|
|
@@ -208,6 +215,27 @@
|
|
|
208
215
|
],
|
|
209
216
|
"id": "cell-11"
|
|
210
217
|
},
|
|
218
|
+
{
|
|
219
|
+
"cell_type": "markdown",
|
|
220
|
+
"metadata": {},
|
|
221
|
+
"source": [
|
|
222
|
+
"### Calibration\n",
|
|
223
|
+
"\n",
|
|
224
|
+
"The same model before and after temperature scaling. Accuracy is identical; the probabilities, and\n",
|
|
225
|
+
"so **log_loss**, **brier** and **ece**, change. `full.report_[\"calibration\"]` has the fitted\n",
|
|
226
|
+
"temperatures and the validation slice's ECE before and after."
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"cell_type": "code",
|
|
231
|
+
"metadata": {},
|
|
232
|
+
"execution_count": null,
|
|
233
|
+
"outputs": [],
|
|
234
|
+
"source": [
|
|
235
|
+
"calibration = {\"full\": results[\"full\"], \"full, before calibration\": full.evaluate(test, calibrated=False)}\n",
|
|
236
|
+
"compare(calibration, by=\"overall\")[[\"accuracy\", \"log_loss\", \"brier\", \"ece\"]]"
|
|
237
|
+
]
|
|
238
|
+
},
|
|
211
239
|
{
|
|
212
240
|
"cell_type": "markdown",
|
|
213
241
|
"metadata": {},
|
|
@@ -237,8 +265,9 @@
|
|
|
237
265
|
"\n",
|
|
238
266
|
"Training only the head on a frozen backbone is the cheapest setting and the least accurate: the\n",
|
|
239
267
|
"backbone was never adapted to these questions. Expect clear gains over random guessing, and full\n",
|
|
240
|
-
"fine-tuning to do much better than head-only on the same rows. Score questions are the hardest
|
|
241
|
-
"
|
|
268
|
+
"fine-tuning to do much better than head-only on the same rows. Score questions are the hardest. For\n",
|
|
269
|
+
"them the loss adds an ordinal term (`ordinal_weight`), so predicting a level next to the right one\n",
|
|
270
|
+
"costs less than one far away; **score_error** shows how far off the predicted level is on average.\n",
|
|
242
271
|
"\n",
|
|
243
272
|
"Things to try: more training rows (`N_TRAIN`), more `BACKBONE_EPOCHS`, or a larger encoder\n",
|
|
244
273
|
"(`EncoderModel(\"answerdotai/ModernBERT-large\", tuning=\"full\")`, which needs more GPU memory). A\n",
|