Text Classification
Transformers
Safetensors
English
lfm3_vl
feature-extraction
lfm3
decision
decision-model
classification
calibration
system-one
security
agent-guard
guardrails
custom_code
Instructions to use aidendle94/Scantron-Preview with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use aidendle94/Scantron-Preview with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="aidendle94/Scantron-Preview", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModel processor = AutoProcessor.from_pretrained("aidendle94/Scantron-Preview", trust_remote_code=True) model = AutoModel.from_pretrained("aidendle94/Scantron-Preview", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Card: training data, recipe, tokens and time; how the head was edited by hand. Add the fine-tune run record.
Browse files- README.md +51 -2
- provenance/train_run.json +185 -0
README.md
CHANGED
|
@@ -320,8 +320,57 @@ All splits below are out of distribution for the model: none of their items or i
|
|
| 320 |
## Provenance
|
| 321 |
<hr style="margin: -0.7em 0 1.2em 0; border: 0; border-top: 1px solid rgba(127, 127, 127, 0.3);" />
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
Modified files relative to d1-2B, as the license requires us to say: `model.safetensors` (all weights fine-tuned; `lm_head.weight` added), `config.json` (`tie_word_embeddings: false`), `calibration.json` (served temperature 1.0), `questions.json` (new). See `NOTICE`.
|
| 327 |
|
|
|
|
| 320 |
## Provenance
|
| 321 |
<hr style="margin: -0.7em 0 1.2em 0; border: 0; border-top: 1px solid rgba(127, 127, 127, 0.3);" />
|
| 322 |
|
| 323 |
+
Scantron Preview was made in two stages: a full fine-tune of d1-2B on one rented GPU, then a hand edit of five rows of the output head, planned and carried out by an AI agent on a laptop. Both are described below; the run record and the edit record are in `provenance/`.
|
| 324 |
+
|
| 325 |
+
### Stage 1: fine-tune
|
| 326 |
+
|
| 327 |
+
**Data.** 92,577 training rows from six public datasets, converted into one JSON state format (the user's request, the proposed tool call, recent actions, untrusted content, destination) and labeled with the 21 questions in `questions.json`. Rows were deduplicated by action template and split by family so that no command prefix or trace family appears on both sides; 10% of families were held out as the in-distribution eval. Every row that shared a key with the external benchmarks in the Performance section was excluded before training.
|
| 328 |
+
|
| 329 |
+
| Source | Train rows | Labels contributed |
|
| 330 |
+
|---|---|---|
|
| 331 |
+
| ProCreations/auto-1b-data | 49,863 | authorization, injection |
|
| 332 |
+
| karanxa/agent-action-safety-dataset | 21,147 | needs review, authorization, injection |
|
| 333 |
+
| yannelli/cli-command-risk | 10,064 | risk level, risk categories, reversible, requires root |
|
| 334 |
+
| tomngdev/tool-safety-v2 | 8,238 | needs review |
|
| 335 |
+
| westenfelder/NL2SH-ALFA | 1,678 | risk level, categories, reversible, requires root |
|
| 336 |
+
| b-mc2/cli-commands-explained | 1,587 | risk level, categories, reversible, requires root |
|
| 337 |
+
|
| 338 |
+
Label coverage over the training rows: authorization 70,863 (38,024 authorized / 32,839 unauthorized), needs review 35,202, injection 31,295 (1,977 positive), risk level 14,831, risk categories 14,743, reversible 14,125, requires root 13,079. Risk labels on shell commands were adjudicated by a second model where the source and a labeling model disagreed; rows that stayed ambiguous were dropped.
|
| 339 |
+
|
| 340 |
+
**Objective.** d1's native training view: each row's state is followed by its labeled questions in random order, packed up to 16 per sequence, and the loss is cross entropy over the option scores read at each question's answer slot (the best token form of each option, max-pooled). Nothing is generated. The 8,192-token state budget truncates untrusted content first, oldest first, and a row whose untrusted content was cut has its injection label masked.
|
| 341 |
+
|
| 342 |
+
**Recipe and cost.**
|
| 343 |
+
|
| 344 |
+
| | |
|
| 345 |
+
|---|---|
|
| 346 |
+
| Trainable parameters | 1.47B (the 24 backbone layers and the final norm); token embeddings / LM head (0.26B) and the vision tower frozen |
|
| 347 |
+
| Optimizer | AdamW, lr 1e-5, betas 0.9 / 0.999, weight decay 0.01 on matrices only, gradient clip 1.0 |
|
| 348 |
+
| Schedule | 3% linear warmup (98 steps), cosine to zero |
|
| 349 |
+
| Batch | 8 micro-batches of 8,192 padded tokens per optimizer step; 3,293 steps over 2 epochs |
|
| 350 |
+
| Precision | bf16 weights and compute, fp32 master weights, fp32 gradient accumulation and optimizer state |
|
| 351 |
+
| Tokens processed | 171.9M (85.9M per epoch), 706,060 labeled decisions |
|
| 352 |
+
| Hardware and time | one NVIDIA RTX 6000D (rented), 4 h 46 min wall clock, 10,014 tokens/s, 54.4 GB peak memory |
|
| 353 |
+
| Checkpoint | epoch 2, `aidendle94/d1-guard-ft-v0-20261028` (private) |
|
| 354 |
+
|
| 355 |
+
### Stage 2: editing the weights by hand
|
| 356 |
+
|
| 357 |
+
The second stage used no gradient steps and no GPU rental. It was done on an Apple M3 Max laptop (128 GB, Metal) by [Claude Fable 5.1](https://www.anthropic.com/claude/fable) working as an autonomous agent in Claude Code: the agent designed the probes, ran them through the served engine, chose the edit, verified it, and wrote this card. A second model (DeepSeek V4.1 Flash, run locally) reviewed the labels and the code. No human labeled data or reviewed the edit.
|
| 358 |
+
|
| 359 |
+
**What was found.** Fine-tuning moved the backbone while the output head stayed frozen, and the two drifted apart: the last layer still carried features that the head no longer read well. Linear probes trained on the last-layer activation at the answer slot separated LlamaFirewall injection at 98.9 AUROC where the head read 87.4 on the same dev rows, ATBench injection at 89 where the head read 74, and ATBench needs-review at 89 where the head read 74. The fix is to point the head at the feature, which is a change to a handful of rows of one matrix.
|
| 360 |
+
|
| 361 |
+
**How the edit works.** Each yes/no question is answered by comparing the logits of the "yes" token forms against the "no" token forms at the answer slot. Those logits are dot products of the final-norm output `x` with the head rows of those tokens, so adding a unit direction `w` with weight `β` to the "yes" rows and subtracting it from the "no" rows shifts the served yes-vs-no log-odds by exactly `2β (w · x)`. Because the shift is linear in `x`, its effect on every recorded probe can be computed offline before anything is served, and a candidate edit can be rejected in seconds.
|
| 362 |
+
|
| 363 |
+
**Procedure.**
|
| 364 |
+
|
| 365 |
+
1. **Record.** For each question of interest, run the fine-tuned model over a dev split through the served engine (same prompt, same packing, same 16-question sets) and keep the hidden state of every layer at the question's answer slot. Probes were recorded under the exact question set the model is served with, because a d1 answer depends on which other questions share its set.
|
| 366 |
+
2. **Locate.** Fit a logistic-regression probe at every layer and compare its AUROC with the head's. The gap at the last layer is the readout loss; it was large for injection and needs-review and near zero for the shell-command questions, which were left alone.
|
| 367 |
+
3. **Fit the direction.** Logistic regression on the final-norm output at the answer slot (standardized features, C = 0.01), normalized to a unit vector. Three directions: LlamaFirewall injection (654 rows, 50 positive), ATBench injection recorded under served packing (80 rows, 18 positive), and needs-review pooled over ATBench and 900 in-distribution rows (1,077 rows, 652 positive).
|
| 368 |
+
4. **Protect the other questions.** Because the "yes" and "no" rows are shared by all 20 yes/no questions, each direction was made orthogonal (QR) to the mean activation and top four principal components of every other question's probe, and to the directions fit before it. Cosine between the raw and the protected direction: 0.91, 0.93 and 0.94, so most of the signal survived.
|
| 369 |
+
5. **Simulate.** Compute the shifted log-odds `s + 2 (δ · x)` with `δ = 1.0 w₁ + 0.7 w₂ + 0.5 w₃` on all 20 recorded probes: AUROC before and after, mean shift, and how many rows cross the decision point. Weights were chosen where the targets improved and the guard questions moved by at most one or two examples each.
|
| 370 |
+
6. **Write the weights.** Untie the head from the input embeddings (so the input side is untouched), add `δ` (norm 1.32) to the two "yes" token rows and subtract it from the three "no" token rows. 5 rows × 2,048 = 10,240 weights changed out of 1.9B; everything else is byte-identical to the fine-tuned checkpoint.
|
| 371 |
+
7. **Verify by serving.** Re-run the edited model on the dev splits and confirm the offline simulation (correlation 1.0 with the served shift), then score the frozen test splits once. The frozen results are the "Scantron Preview" column in the Performance section; the pre-edit column is the same checkpoint without the edit.
|
| 372 |
+
|
| 373 |
+
The in-sample fit AUROC of each direction before and after the edit was 0.875 → 0.993 (LlamaFirewall injection), 0.741 → 0.928 (ATBench injection) and 0.978 → 0.981 (needs-review). `provenance/weight_edit.json` has the full record, including the simulated effect on every probe, and `provenance/head_delta.npy` is `δ` itself, so the edit can be undone or re-weighted.
|
| 374 |
|
| 375 |
Modified files relative to d1-2B, as the license requires us to say: `model.safetensors` (all weights fine-tuned; `lm_head.weight` added), `config.json` (`tie_word_embeddings: false`), `calibration.json` (served temperature 1.0), `questions.json` (new). See `NOTICE`.
|
| 376 |
|
provenance/train_run.json
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"run_id": "ft-v0-20261028",
|
| 3 |
+
"status": "done",
|
| 4 |
+
"base_checkpoint": {
|
| 5 |
+
"repo": "LiquidAI/d1-2B",
|
| 6 |
+
"revision": "cfe5760ff5d5998f91d9ac56bd4b2dbd96996d41",
|
| 7 |
+
"local_path": "/workspace/d1-guard/models/d1-2B"
|
| 8 |
+
},
|
| 9 |
+
"dataset_version": {
|
| 10 |
+
"dataset_version": "ds-20261028-05072130",
|
| 11 |
+
"files": {
|
| 12 |
+
"clean/mix_v2/train.parquet": {
|
| 13 |
+
"sha256": "7f405a8c8b897461e502a6bbd488bd5a54d1b50761b38e43842444ce7d58d601"
|
| 14 |
+
},
|
| 15 |
+
"clean/mix_v2/eval.parquet": {
|
| 16 |
+
"sha256": "ff06a2dc9faac0eb51b1d4ca9615b631098905e832925f4f12b60d6bd4bbe76f"
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
"mix_manifest": "clean/mix_v2/manifest.json",
|
| 20 |
+
"note": "mix_v2 is not listed in DATASET_VERSION.json (it lists mix_v1); the mix files are pinned by the sha256 above"
|
| 21 |
+
},
|
| 22 |
+
"split_and_family_definitions": "scripts/40_build_mix.py (mix_v2); cli_capped / auth_aod_bench per DATASET_VERSION.json",
|
| 23 |
+
"taxonomy_version": "v0-draft-2026-10-27",
|
| 24 |
+
"question_set_version": "questions-v0-2026-10-28 (training/questions_v0.json)",
|
| 25 |
+
"trainable_parameter_set": {
|
| 26 |
+
"description": "language backbone: model.language_model.layers.* and model.language_model.embedding_norm",
|
| 27 |
+
"n_trainable": 1466657536,
|
| 28 |
+
"n_frozen": 451524352,
|
| 29 |
+
"tensors_trainable": 285,
|
| 30 |
+
"frozen": [
|
| 31 |
+
"vision_tower",
|
| 32 |
+
"multi_modal_projector",
|
| 33 |
+
"embeddings (embed_tokens, tied to lm_head)"
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
"method": "full",
|
| 37 |
+
"objective": "d1 tree: cross entropy over each labeled question's option scores (max-pooled token forms at the answer slot), questions packed after their state in random order, groups of set_size",
|
| 38 |
+
"seed": 0,
|
| 39 |
+
"config": {
|
| 40 |
+
"lr": 1e-05,
|
| 41 |
+
"optimizer": "adamw (fused, betas 0.9/0.999, eps 1e-8)",
|
| 42 |
+
"weight_decay": 0.01,
|
| 43 |
+
"weight_decay_on": "matrices only (not norms)",
|
| 44 |
+
"schedule": "linear warmup then cosine to 0",
|
| 45 |
+
"warmup": 0.03,
|
| 46 |
+
"warmup_steps": 98,
|
| 47 |
+
"epochs": 2,
|
| 48 |
+
"precision": "bf16 weights and compute, fp32 master weights, grads accumulated in fp32, fp32 AdamW states",
|
| 49 |
+
"grad_clip": 1.0,
|
| 50 |
+
"set_size": 16,
|
| 51 |
+
"state_budget_tokens": 8192,
|
| 52 |
+
"micro_batch_padded_tokens": 8192,
|
| 53 |
+
"accum": 8,
|
| 54 |
+
"total_steps": 3293,
|
| 55 |
+
"grad_ckpt": false,
|
| 56 |
+
"loss_normalization": "mean over the step's decisions",
|
| 57 |
+
"truncation": "untrusted_context oldest-first, then recent_actions oldest-first; untrusted_origin masked on rows whose untrusted_context was cut"
|
| 58 |
+
},
|
| 59 |
+
"hardware": {
|
| 60 |
+
"worker": "vast.ai 58064827",
|
| 61 |
+
"gpu": "NVIDIA RTX 6000D"
|
| 62 |
+
},
|
| 63 |
+
"tokens_processed": 171862216,
|
| 64 |
+
"decisions_processed": 706060,
|
| 65 |
+
"wall_clock_s": 17176,
|
| 66 |
+
"throughput_tokens_per_s": 10014,
|
| 67 |
+
"peak_memory_gb": 54.43,
|
| 68 |
+
"correctness_checks": {
|
| 69 |
+
"see": "experiments/full_ft_v0/checks/checks.json"
|
| 70 |
+
},
|
| 71 |
+
"metrics": {
|
| 72 |
+
"A_public": null,
|
| 73 |
+
"B_private_ood": null,
|
| 74 |
+
"C_redteam": null,
|
| 75 |
+
"D_dev": null,
|
| 76 |
+
"paraphrase_gap": null,
|
| 77 |
+
"latency_ms_p50_p99": null
|
| 78 |
+
},
|
| 79 |
+
"epochs": [
|
| 80 |
+
{
|
| 81 |
+
"epoch": 1,
|
| 82 |
+
"step": 1647,
|
| 83 |
+
"tokens": 85931108,
|
| 84 |
+
"minutes": 146.2,
|
| 85 |
+
"quick_eval": {
|
| 86 |
+
"risk_level": {
|
| 87 |
+
"loss": 0.3628,
|
| 88 |
+
"acc": 0.8986,
|
| 89 |
+
"n": 552,
|
| 90 |
+
"hc_recall_at_0.5": 0.7941,
|
| 91 |
+
"safe_fpr_at_0.5": 0.0097
|
| 92 |
+
},
|
| 93 |
+
"category": {
|
| 94 |
+
"loss": 0.0252,
|
| 95 |
+
"acc": 0.993,
|
| 96 |
+
"n": 8265
|
| 97 |
+
},
|
| 98 |
+
"reversible": {
|
| 99 |
+
"loss": 0.1781,
|
| 100 |
+
"acc": 0.943,
|
| 101 |
+
"n": 544
|
| 102 |
+
},
|
| 103 |
+
"requires_root": {
|
| 104 |
+
"loss": 0.1824,
|
| 105 |
+
"acc": 0.9561,
|
| 106 |
+
"n": 524
|
| 107 |
+
},
|
| 108 |
+
"needs_review": {
|
| 109 |
+
"loss": 0.0835,
|
| 110 |
+
"acc": 0.9745,
|
| 111 |
+
"n": 432
|
| 112 |
+
},
|
| 113 |
+
"authorization": {
|
| 114 |
+
"loss": 0.0373,
|
| 115 |
+
"acc": 0.9908,
|
| 116 |
+
"n": 326
|
| 117 |
+
},
|
| 118 |
+
"untrusted_origin": {
|
| 119 |
+
"loss": 0.0213,
|
| 120 |
+
"acc": 0.9918,
|
| 121 |
+
"n": 122
|
| 122 |
+
},
|
| 123 |
+
"all": {
|
| 124 |
+
"loss": 0.0605,
|
| 125 |
+
"n": 10765
|
| 126 |
+
}
|
| 127 |
+
},
|
| 128 |
+
"checkpoint": "/workspace/d1-guard/checkpoints/ft-v0-20261028/epoch1"
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"epoch": 2,
|
| 132 |
+
"step": 3293,
|
| 133 |
+
"tokens": 171862216,
|
| 134 |
+
"minutes": 285.5,
|
| 135 |
+
"quick_eval": {
|
| 136 |
+
"risk_level": {
|
| 137 |
+
"loss": 0.3868,
|
| 138 |
+
"acc": 0.9022,
|
| 139 |
+
"n": 552,
|
| 140 |
+
"hc_recall_at_0.5": 0.8627,
|
| 141 |
+
"safe_fpr_at_0.5": 0.0097
|
| 142 |
+
},
|
| 143 |
+
"category": {
|
| 144 |
+
"loss": 0.0248,
|
| 145 |
+
"acc": 0.9936,
|
| 146 |
+
"n": 8265
|
| 147 |
+
},
|
| 148 |
+
"reversible": {
|
| 149 |
+
"loss": 0.2041,
|
| 150 |
+
"acc": 0.9485,
|
| 151 |
+
"n": 544
|
| 152 |
+
},
|
| 153 |
+
"requires_root": {
|
| 154 |
+
"loss": 0.1836,
|
| 155 |
+
"acc": 0.9695,
|
| 156 |
+
"n": 524
|
| 157 |
+
},
|
| 158 |
+
"needs_review": {
|
| 159 |
+
"loss": 0.0612,
|
| 160 |
+
"acc": 0.9745,
|
| 161 |
+
"n": 432
|
| 162 |
+
},
|
| 163 |
+
"authorization": {
|
| 164 |
+
"loss": 0.0133,
|
| 165 |
+
"acc": 0.9939,
|
| 166 |
+
"n": 326
|
| 167 |
+
},
|
| 168 |
+
"untrusted_origin": {
|
| 169 |
+
"loss": 0.0081,
|
| 170 |
+
"acc": 0.9918,
|
| 171 |
+
"n": 122
|
| 172 |
+
},
|
| 173 |
+
"all": {
|
| 174 |
+
"loss": 0.061,
|
| 175 |
+
"n": 10765
|
| 176 |
+
}
|
| 177 |
+
},
|
| 178 |
+
"checkpoint": "/workspace/d1-guard/checkpoints/ft-v0-20261028/epoch2"
|
| 179 |
+
}
|
| 180 |
+
],
|
| 181 |
+
"checkpoint_id": "/workspace/d1-guard/checkpoints/ft-v0-20261028/epoch2",
|
| 182 |
+
"log": "/workspace/d1-guard/logs/train_ft-v0-20261028.log",
|
| 183 |
+
"train_s": 17163,
|
| 184 |
+
"steps_done": 3293
|
| 185 |
+
}
|