Joint training checkpoint step 0
Browse files- README.md +94 -33
- categories.json +1 -17
- checkpoint.json +9 -9
- metrics.json +0 -0
- model.safetensors +2 -2
- run_config.json +10 -12
- train_tabfix.py +338 -216
- training_state.pt +2 -2
README.md
CHANGED
|
@@ -9,8 +9,8 @@ tags: [tabular-data, masked-lm, token-classification, experimental]
|
|
| 9 |
|
| 10 |
Experimental budget-limited mmBERT-base joint fine-tuning checkpoint.
|
| 11 |
|
| 12 |
-
One shared encoder,
|
| 13 |
-
Completed optimizer updates: **
|
| 14 |
|
| 15 |
## Use and resume
|
| 16 |
|
|
@@ -22,41 +22,102 @@ The recoverable state includes model, tokenizer, optimizer and random generators
|
|
| 22 |
|
| 23 |
Canonical labels refer to decoded Unicode cell coordinates. Prediction is token-granular. Disabled categories are filtered before edits; overlapping proposals and linked structural repairs require abstention/group handling. Suggestions are not automatically applied. Missing values need not have recoverable intended contents. Real-source unlabelled cells are excluded from negative supervision.
|
| 24 |
|
| 25 |
-
Dataset revision: `
|
| 26 |
|
| 27 |
-
##
|
| 28 |
-
|
| 29 |
-
TabFix fine-tuning contributions are CC BY-NC 4.0; commercial use requires the owner's permission. The underlying mmBERT material remains under its MIT terms and attribution, attributed in BASE_MODEL_CARD.md for pretrained checkpoints. Dataset source terms and USDA attribution remain documented in the dataset card.
|
| 30 |
-
|
| 31 |
-
## Training controls
|
| 32 |
-
|
| 33 |
-
The learning rate warms up for 100 updates, then follows cosine decay to 10% of its peak over a fixed 1600-update horizon. A fixed, stratified validation panel of up to 252 records is evaluated before training and every 200 updates or 600 seconds. Selection minimizes detection loss plus 0.232104 times correction loss (equal averages of fully and partially masked views). Training stops after 4 checks without an improvement of at least 0.001. Final publication restores the best complete checkpoint; recoverable latest and best revisions are retained as tags in this repository. These controls reduce risk; they do not guarantee generalization. The test split is not used for checkpoint selection.
|
| 34 |
-
|
| 35 |
-
## Measured validation and end-to-end results
|
| 36 |
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
| Correction loss | 9.67505 | 0.45218 | 0.44362 |
|
| 43 |
-
| Weighted selection score | 3.94294 | 0.31017 | 0.44021 |
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
The final gold-span validation diagnostic repaired 19/28 references exactly. This supplies the correct error location and is **not end-to-end accuracy**.
|
| 48 |
-
|
| 49 |
-
### Frozen test diagnostic (2026-09-20)
|
| 50 |
-
|
| 51 |
-
The complete pipeline scanned 55 synthetic test tables spanning 18 error categories, seven languages and 14 source families, with threshold 0.9 fixed before evaluation. All 55 results use L40S BF16. Model revision evaluated: `044567585e7943aab9252339cfe7bb1c2c44509b`; subsequent documentation commits do not change its weights.
|
| 52 |
-
|
| 53 |
-
- Faulty cells exactly repaired: **23/45 (51.1%)**.
|
| 54 |
-
- Originally valid cells changed incorrectly: **55/6,193 (0.89%)**.
|
| 55 |
-
- Detection precision (cell + category): **35/161 (21.7%)**; recall: **35/46 (76.1%)**.
|
| 56 |
-
- Hard-negative tables harmed: **11/18**.
|
| 57 |
|
| 58 |
-
|
| 59 |
|
| 60 |
-
|
| 61 |
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
Experimental budget-limited mmBERT-base joint fine-tuning checkpoint.
|
| 11 |
|
| 12 |
+
One shared encoder, 2 residual category BIO/insertion outputs (`text.encoding`, `text.spelling`), and the pretrained MLM vocabulary head. Declared schema rules detect deterministic violations before neural classification. Correction generates a complete cell inside `<replacement>` beside the visible `<original>` in the same cell. Valid copies are training targets too. The MLM head emits replacement tokens followed by `END_EDIT` and visible padding tokens.
|
| 13 |
+
Completed optimizer updates: **0**. This checkpoint is a research baseline, not a validated automatic cleaning system.
|
| 14 |
|
| 15 |
## Use and resume
|
| 16 |
|
|
|
|
| 22 |
|
| 23 |
Canonical labels refer to decoded Unicode cell coordinates. Prediction is token-granular. Disabled categories are filtered before edits; overlapping proposals and linked structural repairs require abstention/group handling. Suggestions are not automatically applied. Missing values need not have recoverable intended contents. Real-source unlabelled cells are excluded from negative supervision.
|
| 24 |
|
| 25 |
+
Dataset revision: `602b8a454ca448713d8a5658650d4feaad8d579c`. Train/validation formats are held out; test data was not consumed. This dataset is mostly synthetic. The detector/correction task weight is `ln(2)/ln(256000) = 0.055661`; valid-copy correction examples are sampled with probability 0.30. The run records periodic held-out validation losses for both tasks in `validation_history`, then a final validation report. These are development diagnostics, not deployment precision or end-to-end repair accuracy. Conditional pseudo-perplexity is scored with each response token masked in turn, original visible; an acceptance threshold is fitted on the reported validation sample. Its sample size and observed errors limit its reliability. No latency guarantee is claimed. See `metrics.json` for sample sizes, loss, operating-point diagnostics and exact-repair outcomes.
|
| 26 |
|
| 27 |
+
## Validation results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
Selected step: 0. Stop reason: training in progress.
|
| 30 |
|
| 31 |
+
```json
|
| 32 |
+
{}
|
| 33 |
+
```
|
|
|
|
|
|
|
| 34 |
|
| 35 |
+
## Licensing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
+
TabFix fine-tuning contributions are CC BY-NC 4.0; commercial use requires the owner's permission. The underlying mmBERT material remains under its MIT terms and attribution, attributed in BASE_MODEL_CARD.md for pretrained checkpoints. Dataset source terms and USDA attribution remain documented in the dataset card.
|
| 38 |
|
| 39 |
+
## Training controls
|
| 40 |
|
| 41 |
+
The learning rate warms up for 100 updates, then follows cosine decay to 10% of its peak over a fixed 5000-update horizon. A fixed, stratified validation panel of up to 28 records is evaluated before training and every 300 updates or 600 seconds. Selection minimizes detection loss plus 0.055661 times correction loss (equal averages of fully and partially masked views). Training stops after 4 checks without an improvement of at least 0.001. Final publication restores the best complete checkpoint; recoverable latest and best revisions are retained as tags in this repository. These controls reduce risk; they do not guarantee generalization. The test split is not used for checkpoint selection.
|
| 42 |
+
|
| 43 |
+
## Recorded run results
|
| 44 |
+
|
| 45 |
+
```json
|
| 46 |
+
{
|
| 47 |
+
"tiny_smoke": false,
|
| 48 |
+
"dataset_revision": "602b8a454ca448713d8a5658650d4feaad8d579c",
|
| 49 |
+
"train_records": 27948,
|
| 50 |
+
"correction_eligible_records": 27805,
|
| 51 |
+
"correction_request_version": 4,
|
| 52 |
+
"batch_size": 4,
|
| 53 |
+
"start_step": 0,
|
| 54 |
+
"device": "cuda",
|
| 55 |
+
"parameters": 307794699,
|
| 56 |
+
"validation_history": [
|
| 57 |
+
{
|
| 58 |
+
"step": 0,
|
| 59 |
+
"detection_loss": 1.5472150828157152,
|
| 60 |
+
"correction_loss": 6.555269168309259,
|
| 61 |
+
"correction_full_mask_loss": 7.438174559519841,
|
| 62 |
+
"correction_partial_mask_loss": 5.672363777098676,
|
| 63 |
+
"selection_score": 1.9120902847241386,
|
| 64 |
+
"examples": 28,
|
| 65 |
+
"correction_edits": 52,
|
| 66 |
+
"correction_category_counts": {
|
| 67 |
+
"copy": 10,
|
| 68 |
+
"text.spelling": 7,
|
| 69 |
+
"format.boolean": 7,
|
| 70 |
+
"format.date": 3,
|
| 71 |
+
"text.encoding": 5,
|
| 72 |
+
"schema.category": 3,
|
| 73 |
+
"schema.identifier": 4,
|
| 74 |
+
"schema.type": 2,
|
| 75 |
+
"consistency.dependency": 6,
|
| 76 |
+
"consistency.temporal": 3,
|
| 77 |
+
"consistency.unit": 1,
|
| 78 |
+
"format.case": 1
|
| 79 |
+
},
|
| 80 |
+
"token_and_gap_tp_fp_fn_at_0.9": [
|
| 81 |
+
1,
|
| 82 |
+
234,
|
| 83 |
+
69
|
| 84 |
+
],
|
| 85 |
+
"sample_ids": [
|
| 86 |
+
"7cc2b2069f314821114a2a2b09543c680923083980692c6a95b3371b316390d9-open-clean",
|
| 87 |
+
"2c30950b5459afe6967673e8b23c222adab49967bf80e72af0ef1d4fb96296ca-open",
|
| 88 |
+
"bd4bba00c41c73249e4fd9fe97876df42dbaefbe6aabeec4430d2acb63926318-open",
|
| 89 |
+
"0121e99febc488b49db6e39b07ef71255f5a94d9ef0292f7a34a705e59271bbe-open",
|
| 90 |
+
"4d74e65f621bbb1dfc2848cdb8aefbd32befa5042d8482c3340de2d9833c06ec-open",
|
| 91 |
+
"d120e4d54be4c9692441a06d9cc4bf1db02bd205c6a317a1a7ac4548c09666cc-open",
|
| 92 |
+
"d6b7a9c8fc0552cfcf0d26ce0ce1be2663d5aa86d80527fc77c4ccc02a036984-open",
|
| 93 |
+
"05f04c447b2d5e98f170add5fcdde123cb8db18946eb5e21ea8573fdfdd66ffe-open",
|
| 94 |
+
"b68fd4277af10beef7958e8d2f6b31805d635db888e60b38ae2306bf138ea584-open",
|
| 95 |
+
"c2710a557bc21c7f5d4c38d580e9d54042923e32d22848cc3f025caa864aa51c-open",
|
| 96 |
+
"da8d1a36641b16437fd72e9db23a60fbb6f185acdceec0f217491c232d3602b5-open-clean",
|
| 97 |
+
"55e92ca80f64845e208d3cda2f1e8fc3cb620ca57c66fce22d9bb9a86321af4a-open",
|
| 98 |
+
"53820fc3b14267390c15dfad0a80f83c9d0ed59c95e40c96c2283e4f3b167e09-open",
|
| 99 |
+
"d5f74ad40dca51caabef5cfcfc1845e3b40fcd71608304e4036786b6e27bfad6-open",
|
| 100 |
+
"6481cc1b3a6148ca5e8df1783aa3c15bd97b4224ea44ec304e2448bd613c3a07",
|
| 101 |
+
"8a1d338e2b1a352eac2c0834432588239c231b43df14e68402999151d62a70d8",
|
| 102 |
+
"04434b82c7685003521e12204c5af32606d402dd454be5d270c2b39c5a3c2b55",
|
| 103 |
+
"7aeaf4538d564e7bfc4a20cac6b97e42655170cd4ac40c98db0d24ff056ba388",
|
| 104 |
+
"9d7d44d25b2bd975829249dc3cd673c9a972f47ad8b1fa1bf0206f527d27934c",
|
| 105 |
+
"b01405d247c32e01b05e977fe963ca8ab440e1559f1c7f8adf0a99928d29b997",
|
| 106 |
+
"89f4e9132a30351d7836acfa80b56de26924f82d1b2403e48f1c265b4053ab99",
|
| 107 |
+
"fbe61f1cc2096a02443b68b5dbf6da5694e8cb60ec5fb445ca5bc29fc147b0e9",
|
| 108 |
+
"4dee644acbc9859035ebc82ce0d74ac45065b429f74ec833d9ae5dda3daf02a0",
|
| 109 |
+
"86e7d7c56f4e30ae4e63da4327d92c7a9ff2e18d47611dcba9e794b11a222f93",
|
| 110 |
+
"d515d8316e03100e37bb3be520d62c30463d7d30a235e0921c7ff2d88921c714",
|
| 111 |
+
"b4a926b6d1c343e53f4fd04dc327fb0fd86d3f4532b964fea22340ef73ca68de",
|
| 112 |
+
"3e8dff1e9e6db8d09c388b141194864e22c347550db958abeae4500015fa8ecb",
|
| 113 |
+
"522ef97370ddbad950ea41bb8000b46488a7db7a05af8ee602117f2c0e2c400f"
|
| 114 |
+
]
|
| 115 |
+
}
|
| 116 |
+
],
|
| 117 |
+
"selection": {
|
| 118 |
+
"best_score": 1.9120902847241386,
|
| 119 |
+
"best_step": 0,
|
| 120 |
+
"bad_checks": 0
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
```
|
categories.json
CHANGED
|
@@ -1,20 +1,4 @@
|
|
| 1 |
[
|
| 2 |
"text.encoding",
|
| 3 |
-
"text.
|
| 4 |
-
"text.spelling",
|
| 5 |
-
"format.number",
|
| 6 |
-
"format.date",
|
| 7 |
-
"format.boolean",
|
| 8 |
-
"format.case",
|
| 9 |
-
"format.whitespace",
|
| 10 |
-
"format.unit",
|
| 11 |
-
"schema.type",
|
| 12 |
-
"schema.identifier",
|
| 13 |
-
"schema.category",
|
| 14 |
-
"consistency.unit",
|
| 15 |
-
"consistency.dependency",
|
| 16 |
-
"consistency.temporal",
|
| 17 |
-
"missing.required",
|
| 18 |
-
"missing.conditional",
|
| 19 |
-
"structure.shift"
|
| 20 |
]
|
|
|
|
| 1 |
[
|
| 2 |
"text.encoding",
|
| 3 |
+
"text.spelling"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
]
|
checkpoint.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
| 1 |
{
|
| 2 |
"version": 1,
|
| 3 |
-
"step":
|
| 4 |
-
"dataset_revision": "
|
| 5 |
"base_revision": "c5955035435e2bf121cde7f3c8863ef52ff35d82",
|
| 6 |
"files": {
|
| 7 |
-
"model.safetensors": "
|
| 8 |
"config.json": "9e105ce4ed25ed2d6be2443eeb69932a530668a248ce3211d43a89a136d42531",
|
| 9 |
"tokenizer_config.json": "51d83f5e4c1174f8dad62610076998730a57a9ac7025680653c720bca2a9b0a1",
|
| 10 |
"tokenizer.json": "47e408472c05385fea5fb9f8b02bbd838d55da8f15a22c863a99fd31d89243f5",
|
| 11 |
-
"training_state.pt": "
|
| 12 |
-
"run_config.json": "
|
| 13 |
-
"metrics.json": "
|
| 14 |
-
"categories.json": "
|
| 15 |
-
"train_tabfix.py": "
|
| 16 |
-
"README.md": "
|
| 17 |
"LICENSE": "b6c1c7bb5aae03bb6b3396fc7c65cbe7306c422ed424b0b7b687d9a50cc15412",
|
| 18 |
"BASE_MODEL_CARD.md": "35724415037ee3159aa733c16050620cc19a76219112a6ceeb3949572e0b37e9"
|
| 19 |
},
|
|
|
|
| 1 |
{
|
| 2 |
"version": 1,
|
| 3 |
+
"step": 0,
|
| 4 |
+
"dataset_revision": "602b8a454ca448713d8a5658650d4feaad8d579c",
|
| 5 |
"base_revision": "c5955035435e2bf121cde7f3c8863ef52ff35d82",
|
| 6 |
"files": {
|
| 7 |
+
"model.safetensors": "90b750926e48ae595b38bf48d9b890758a27de4ba5d1f0f00706bbf94b20e18d",
|
| 8 |
"config.json": "9e105ce4ed25ed2d6be2443eeb69932a530668a248ce3211d43a89a136d42531",
|
| 9 |
"tokenizer_config.json": "51d83f5e4c1174f8dad62610076998730a57a9ac7025680653c720bca2a9b0a1",
|
| 10 |
"tokenizer.json": "47e408472c05385fea5fb9f8b02bbd838d55da8f15a22c863a99fd31d89243f5",
|
| 11 |
+
"training_state.pt": "cd9e1448a8ac294f6f6d25dc2d5a1fc00faf3ff28d70475cf1942077b0ca5aae",
|
| 12 |
+
"run_config.json": "d9e42266559b78f89751de2b1f9e9b27229cbd7646c126ea00fd691144495c1f",
|
| 13 |
+
"metrics.json": "10b5cd912f0fa835b3542364cc6ceb7a6a5232babdecfea8e1095dab06b6eab5",
|
| 14 |
+
"categories.json": "da00946a1a8dfd24dd3bd01f47a4493abe6ade281d6a8571ae9728e0406d4aab",
|
| 15 |
+
"train_tabfix.py": "b16a43dbb276461d319a05bd4719de7520e0612fa23f7d83fb5fba58bebbc24f",
|
| 16 |
+
"README.md": "4b63d21bde39281a638ad6333175ebe4b8290646b9bf6434c0bff21c15673fcf",
|
| 17 |
"LICENSE": "b6c1c7bb5aae03bb6b3396fc7c65cbe7306c422ed424b0b7b687d9a50cc15412",
|
| 18 |
"BASE_MODEL_CARD.md": "35724415037ee3159aa733c16050620cc19a76219112a6ceeb3949572e0b37e9"
|
| 19 |
},
|
metrics.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
model.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:90b750926e48ae595b38bf48d9b890758a27de4ba5d1f0f00706bbf94b20e18d
|
| 3 |
+
size 1231193844
|
run_config.json
CHANGED
|
@@ -1,28 +1,26 @@
|
|
| 1 |
{
|
| 2 |
"repo_id": "Antix5/tabfix-preview",
|
| 3 |
-
"output": "
|
| 4 |
"local_data": "",
|
| 5 |
"tokenizer": "",
|
| 6 |
"resume": "",
|
| 7 |
"resume_revision": "main",
|
| 8 |
"tiny": false,
|
| 9 |
-
"steps":
|
| 10 |
-
"train_seconds":
|
| 11 |
-
"save_seconds":
|
| 12 |
"lr": 2e-05,
|
| 13 |
"seed": 20260913,
|
| 14 |
-
"validation_examples":
|
| 15 |
"validation_repairs": 28,
|
| 16 |
"validation_seconds": 600,
|
| 17 |
-
"validation_steps":
|
| 18 |
"patience": 4,
|
| 19 |
"min_delta": 0.001,
|
| 20 |
"warmup_steps": 100,
|
| 21 |
-
"schedule_steps":
|
| 22 |
"max_length": 8192,
|
| 23 |
-
"correction_weight": 0.
|
| 24 |
-
"batch_size":
|
| 25 |
-
"verify_only": false
|
| 26 |
-
"conditional_pair_weight": 0.25,
|
| 27 |
-
"allow_objective_change": false
|
| 28 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"repo_id": "Antix5/tabfix-preview",
|
| 3 |
+
"output": "/tmp/tabfix-v2",
|
| 4 |
"local_data": "",
|
| 5 |
"tokenizer": "",
|
| 6 |
"resume": "",
|
| 7 |
"resume_revision": "main",
|
| 8 |
"tiny": false,
|
| 9 |
+
"steps": 100000,
|
| 10 |
+
"train_seconds": 12000,
|
| 11 |
+
"save_seconds": 600,
|
| 12 |
"lr": 2e-05,
|
| 13 |
"seed": 20260913,
|
| 14 |
+
"validation_examples": 28,
|
| 15 |
"validation_repairs": 28,
|
| 16 |
"validation_seconds": 600,
|
| 17 |
+
"validation_steps": 300,
|
| 18 |
"patience": 4,
|
| 19 |
"min_delta": 0.001,
|
| 20 |
"warmup_steps": 100,
|
| 21 |
+
"schedule_steps": 5000,
|
| 22 |
"max_length": 8192,
|
| 23 |
+
"correction_weight": 0.055661360737461876,
|
| 24 |
+
"batch_size": 4,
|
| 25 |
+
"verify_only": false
|
|
|
|
|
|
|
| 26 |
}
|
train_tabfix.py
CHANGED
|
@@ -22,6 +22,7 @@ from collections import Counter
|
|
| 22 |
from contextlib import nullcontext
|
| 23 |
from dataclasses import asdict, dataclass
|
| 24 |
from decimal import Decimal, InvalidOperation
|
|
|
|
| 25 |
from functools import lru_cache
|
| 26 |
import hashlib
|
| 27 |
import html
|
|
@@ -47,10 +48,10 @@ from tokenizers import Tokenizer, pre_tokenizers
|
|
| 47 |
from transformers import AutoTokenizer, ModernBertConfig, ModernBertForMaskedLM, PreTrainedTokenizerFast
|
| 48 |
|
| 49 |
DATASET = "Antix5/tabular-errors-v1"
|
| 50 |
-
REVISION = "
|
| 51 |
BASE = "jhu-clsp/mmBERT-base"
|
| 52 |
BASE_REVISION = "c5955035435e2bf121cde7f3c8863ef52ff35d82"
|
| 53 |
-
|
| 54 |
"text.encoding", "text.invisible", "text.spelling", "format.number",
|
| 55 |
"format.date", "format.boolean", "format.case", "format.whitespace",
|
| 56 |
"format.unit", "schema.type", "schema.identifier", "schema.category",
|
|
@@ -58,6 +59,7 @@ CATEGORIES = (
|
|
| 58 |
"missing.required", "missing.conditional", "structure.shift",
|
| 59 |
)
|
| 60 |
CONTROL = ("<|tabfix_repair|>", "<|tabfix_end|>", "<|tabfix_pad|>")
|
|
|
|
| 61 |
K = len(CATEGORIES)
|
| 62 |
stop_requested = False
|
| 63 |
|
|
@@ -104,6 +106,191 @@ def emit(event: str, **fields: object) -> None:
|
|
| 104 |
print(json.dumps({"event": event, **fields}, ensure_ascii=False), flush=True)
|
| 105 |
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
@dataclass(frozen=True)
|
| 108 |
class Edit:
|
| 109 |
row: int
|
|
@@ -252,7 +439,10 @@ class Consumer:
|
|
| 252 |
for at in boundaries:
|
| 253 |
gaps[anchor_token(c, offsets, at)] = [0 if trusted else -100] * K
|
| 254 |
collisions = 0
|
| 255 |
-
|
|
|
|
|
|
|
|
|
|
| 256 |
c = next(c for c in cc if (c.row, c.column) == (e.row, e.column))
|
| 257 |
hit, _, _ = interval(c, offsets, e)
|
| 258 |
category = CATEGORIES.index(e.category)
|
|
@@ -264,6 +454,14 @@ class Consumer:
|
|
| 264 |
target[category] = 1 if j == 0 else 2
|
| 265 |
else:
|
| 266 |
gaps.setdefault(anchor_token(c, offsets, e.start), [-100] * K)[category] = 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
pos = sorted(labels)
|
| 268 |
gp = sorted(gaps)
|
| 269 |
return Prepared(record, ids, offsets, cc, pos, [labels[p] for p in pos], gp,
|
|
@@ -272,28 +470,18 @@ class Consumer:
|
|
| 272 |
def correction(self, p: Prepared, e: Edit, rng: random.Random, training: bool,
|
| 273 |
capacity: int | None = None, mask_all: bool = False) -> tuple[list[int], list[int], list[int], list[bool], str]:
|
| 274 |
c = next(c for c in p.cells if (c.row, c.column) == (e.row, e.column))
|
| 275 |
-
|
| 276 |
-
#
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
gold = self.literal(target)
|
| 282 |
-
n = len(hit)
|
| 283 |
-
budget = capacity or min(64, max(8, n + max(2, math.ceil(.2 * n))))
|
| 284 |
-
if training and rng.random() < .25:
|
| 285 |
-
budget = min(64, budget * 2)
|
| 286 |
-
if training and len(gold) > budget:
|
| 287 |
-
budget = min(64, budget * 2)
|
| 288 |
-
self.stats["larger_training_buffer"] += 1
|
| 289 |
if training and len(gold) > budget:
|
| 290 |
-
raise ValueError("
|
| 291 |
-
char_start = c.bounds[lo][0] if lo < len(c.bounds) else c.close
|
| 292 |
-
char_end = c.bounds[hi-1][1] if hi > lo else char_start
|
| 293 |
-
if not c.value:
|
| 294 |
-
char_start, char_end = c.anchor, c.close # Replace the whole <empty/> marker.
|
| 295 |
mask_id = integer(self.tokenizer.mask_token_id)
|
| 296 |
-
|
|
|
|
|
|
|
| 297 |
ids = self.tokenizer.encode(skeleton, add_special_tokens=True) # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 298 |
positions = [i for i, token in enumerate(ids) if token == mask_id]
|
| 299 |
if len(positions) != budget + 1:
|
|
@@ -318,6 +506,11 @@ class Consumer:
|
|
| 318 |
|
| 319 |
|
| 320 |
def eligible(record: Record) -> list[Edit]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
result: list[Edit] = []
|
| 322 |
for value in sequence(record.metadata.get("repair_policies", [])):
|
| 323 |
policy = mapping(value)
|
|
@@ -326,129 +519,6 @@ def eligible(record: Record) -> list[Edit]:
|
|
| 326 |
return result
|
| 327 |
|
| 328 |
|
| 329 |
-
@dataclass
|
| 330 |
-
class ConditionalPair:
|
| 331 |
-
"""Same empty field/context; only the declared determinant value changes."""
|
| 332 |
-
negative: Record
|
| 333 |
-
positive: Record
|
| 334 |
-
row: int
|
| 335 |
-
column: int
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
def conditional_pairs(record: Record) -> list[ConditionalPair]:
|
| 339 |
-
# Only certified clean training records; do not invent labels for real sources.
|
| 340 |
-
if record.edits or record.metadata.get("label_scope") != "validated_synthetic_contracts":
|
| 341 |
-
return []
|
| 342 |
-
schema = ET.fromstring(record.xml).find("schema")
|
| 343 |
-
if schema is None:
|
| 344 |
-
return []
|
| 345 |
-
columns = schema.findall("column")
|
| 346 |
-
cc = cells(record.xml)
|
| 347 |
-
result: list[ConditionalPair] = []
|
| 348 |
-
targets = {integer(x) for x in sequence(record.metadata["target_rows"])}
|
| 349 |
-
supervised = {integer(x) for x in sequence(record.metadata["supervised_columns"])}
|
| 350 |
-
for col, definition in enumerate(columns):
|
| 351 |
-
rules_node = definition.find("constraints")
|
| 352 |
-
rules = mapping(cast(object, json.loads(rules_node.text or "{}"))) if rules_node is not None else {}
|
| 353 |
-
required = rules.get("required_when")
|
| 354 |
-
if not isinstance(required, dict) or col not in supervised:
|
| 355 |
-
continue
|
| 356 |
-
rule = mapping(cast(object, required))
|
| 357 |
-
determinant = rule.get("column")
|
| 358 |
-
if not isinstance(determinant, int) or isinstance(determinant, bool) or not 0 <= determinant < len(columns) or determinant == col:
|
| 359 |
-
continue
|
| 360 |
-
operators = [key for key in ("equals", "not_equals") if key in rule]
|
| 361 |
-
if len(operators) != 1:
|
| 362 |
-
continue
|
| 363 |
-
node = columns[determinant].find("constraints")
|
| 364 |
-
constraints = mapping(cast(object, json.loads(node.text or "{}"))) if node is not None else {}
|
| 365 |
-
allowed = [string(x) for x in sequence(constraints.get("enum", []))]
|
| 366 |
-
op = operators[0]
|
| 367 |
-
positive = [v for v in allowed if (v == rule[op]) == (op == "equals")]
|
| 368 |
-
negative = [v for v in allowed if (v == rule[op]) != (op == "equals")]
|
| 369 |
-
if not positive or not negative:
|
| 370 |
-
continue
|
| 371 |
-
for row in sorted(targets):
|
| 372 |
-
target = next((c for c in cc if (c.row, c.column) == (row, col)), None)
|
| 373 |
-
status = next((c for c in cc if (c.row, c.column) == (row, determinant)), None)
|
| 374 |
-
if target is None or status is None:
|
| 375 |
-
continue
|
| 376 |
-
for yes in positive:
|
| 377 |
-
for no in negative:
|
| 378 |
-
def view(value: str, target: Cell = target, status: Cell = status) -> Record:
|
| 379 |
-
xml = record.xml
|
| 380 |
-
replacements = [(target.anchor, target.close, "<empty/>"),
|
| 381 |
-
(status.anchor, status.close, html.escape(value))]
|
| 382 |
-
for lo, hi, replacement in sorted(replacements, reverse=True):
|
| 383 |
-
xml = xml[:lo] + replacement + xml[hi:]
|
| 384 |
-
return Record(record.id, xml, [], {"target_rows": [], "supervised_columns": [], "label_scope": "pair_only"})
|
| 385 |
-
result.append(ConditionalPair(view(no), view(yes), row, col))
|
| 386 |
-
return result
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
def conditional_pair_loss(model: TabFix, consumer: Consumer, pair: ConditionalPair,
|
| 390 |
-
device: torch.device) -> Tensor:
|
| 391 |
-
"""Proper binary CE on one matched negative/positive pair; no category dilution."""
|
| 392 |
-
losses: list[Tensor] = []
|
| 393 |
-
for record, label in ((pair.negative, 0.), (pair.positive, 1.)):
|
| 394 |
-
p = consumer.prepare(record)
|
| 395 |
-
cell = next(c for c in p.cells if (c.row, c.column) == (pair.row, pair.column))
|
| 396 |
-
position = anchor_token(cell, p.offsets, 0)
|
| 397 |
-
hidden = model.hidden(p.ids, device)
|
| 398 |
-
logit = model.insert.forward(hidden[position])[CATEGORIES.index("missing.conditional")].float()
|
| 399 |
-
losses.append(F.binary_cross_entropy_with_logits(logit, torch.tensor(label, device=device)))
|
| 400 |
-
return torch.stack(losses).mean()
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
def diversify_arithmetic(records: list[Record]) -> list[Record]:
|
| 404 |
-
"""Replace single arithmetic corruptions with bounded signed offsets; preserve sampling mass."""
|
| 405 |
-
output: list[Record] = []
|
| 406 |
-
for record in records:
|
| 407 |
-
if len(record.edits) != 1 or record.edits[0].category != "consistency.dependency" or record.metadata.get("label_scope") != "validated_synthetic_contracts":
|
| 408 |
-
output.append(record)
|
| 409 |
-
continue
|
| 410 |
-
e = record.edits[0]
|
| 411 |
-
schema = ET.fromstring(record.xml).find("schema")
|
| 412 |
-
if schema is None:
|
| 413 |
-
output.append(record)
|
| 414 |
-
continue
|
| 415 |
-
columns = schema.findall("column")
|
| 416 |
-
keys = {c.get("key"): i for i, c in enumerate(columns)}
|
| 417 |
-
node = schema.find("relations")
|
| 418 |
-
relations = sequence(cast(object, json.loads(node.text or "[]"))) if node is not None else []
|
| 419 |
-
if not any(sequence(rel)[0] in ("multiply", "difference", "balance") and keys.get(string(sequence(rel)[-1])) == e.column for rel in relations):
|
| 420 |
-
output.append(record)
|
| 421 |
-
continue
|
| 422 |
-
cell = next(c for c in cells(record.xml) if (c.row, c.column) == (e.row, e.column))
|
| 423 |
-
clean = cell.value[:e.start] + e.replacement + cell.value[e.end:]
|
| 424 |
-
try:
|
| 425 |
-
number_value = Decimal(clean.replace(",", "."))
|
| 426 |
-
except InvalidOperation:
|
| 427 |
-
output.append(record)
|
| 428 |
-
continue
|
| 429 |
-
node = columns[e.column].find("constraints")
|
| 430 |
-
rules = mapping(cast(object, json.loads(node.text or "{}"))) if node is not None else {}
|
| 431 |
-
deltas = [-10, -3, -2, -1, 1, 2, 3, 10]
|
| 432 |
-
random.Random(record.id).shuffle(deltas)
|
| 433 |
-
values: list[str] = []
|
| 434 |
-
for delta in deltas:
|
| 435 |
-
value = number_value + delta
|
| 436 |
-
if ("minimum" in rules and value < Decimal(str(rules["minimum"]))) or ("maximum" in rules and value > Decimal(str(rules["maximum"]))):
|
| 437 |
-
continue
|
| 438 |
-
rendered = str(value).replace(".", ",") if "," in clean else str(value)
|
| 439 |
-
values.append(rendered)
|
| 440 |
-
if not values:
|
| 441 |
-
output.append(record)
|
| 442 |
-
continue
|
| 443 |
-
for i, value in enumerate(values):
|
| 444 |
-
meta = dict(record.metadata)
|
| 445 |
-
meta["sampling_weight"] = number(meta["sampling_weight"]) / len(values)
|
| 446 |
-
xml = record.xml[:cell.anchor] + html.escape(value) + record.xml[cell.close:]
|
| 447 |
-
output.append(Record(record.id + f"-arithmetic-{i}", xml,
|
| 448 |
-
[Edit(e.row, e.column, 0, len(value), clean, e.category)], meta))
|
| 449 |
-
return output
|
| 450 |
-
|
| 451 |
-
|
| 452 |
class Batch(Protocol):
|
| 453 |
def to_pylist(self) -> list[object]: ...
|
| 454 |
|
|
@@ -500,11 +570,11 @@ def detection_loss(model: TabFix, p: Prepared, device: torch.device) -> Tensor:
|
|
| 500 |
|
| 501 |
def detection_from_hidden(model: TabFix, p: Prepared, h: Tensor) -> Tensor:
|
| 502 |
device = h.device
|
| 503 |
-
labels = torch.tensor(p.labels, device=device, dtype=torch.long)
|
| 504 |
logits = model.bio.forward(h[p.positions]).reshape(-1, K, 3)
|
| 505 |
loss = F.cross_entropy(logits.reshape(-1, 3).float(), labels.reshape(-1), ignore_index=-100, reduction="none").reshape(-1, K)
|
| 506 |
bio = mean_parts(loss, labels > 0, labels != -100)
|
| 507 |
-
gl = torch.tensor(p.gap_labels, device=device, dtype=torch.float32)
|
| 508 |
gi = model.insert.forward(h[p.gaps])
|
| 509 |
il = F.binary_cross_entropy_with_logits(gi.float(), gl.clamp_min(0), reduction="none")
|
| 510 |
return bio + .5 * mean_parts(il, gl > 0, gl != -100)
|
|
@@ -548,11 +618,9 @@ class RunConfig:
|
|
| 548 |
warmup_steps: int = 100
|
| 549 |
schedule_steps: int = 2400
|
| 550 |
max_length: int = 8192
|
| 551 |
-
correction_weight: float = math.log(
|
| 552 |
batch_size: int = 2
|
| 553 |
verify_only: bool = False
|
| 554 |
-
conditional_pair_weight: float = 0.0
|
| 555 |
-
allow_objective_change: bool = False
|
| 556 |
|
| 557 |
|
| 558 |
def learning_rate(cfg: RunConfig, step: int) -> float:
|
|
@@ -623,6 +691,9 @@ def load_checkpoint(path: Path, device: torch.device) -> tuple[TabFix, PreTraine
|
|
| 623 |
config._attn_implementation = "sdpa" # pyright: ignore[reportPrivateUsage] — third-party typing boundary
|
| 624 |
mlm = ModernBertForMaskedLM(config)
|
| 625 |
model = TabFix(mlm)
|
|
|
|
|
|
|
|
|
|
| 626 |
# Store tied embedding once; the duplicate decoder weight shares storage.
|
| 627 |
weights = load_file(str(path / "model.safetensors"))
|
| 628 |
weights["mlm.decoder.weight"] = weights["mlm.model.embeddings.tok_embeddings.weight"]
|
|
@@ -685,7 +756,7 @@ tags: [tabular-data, masked-lm, token-classification, experimental]
|
|
| 685 |
|
| 686 |
{'Temporary random tiny smoke-test model, not a pretrained fine-tune.' if cfg.tiny else 'Experimental budget-limited mmBERT-base joint fine-tuning checkpoint.'}
|
| 687 |
|
| 688 |
-
One shared encoder,
|
| 689 |
Completed optimizer updates: **{step}**. This checkpoint is a research baseline, not a validated automatic cleaning system.
|
| 690 |
|
| 691 |
## Use and resume
|
|
@@ -698,7 +769,15 @@ The recoverable state includes model, tokenizer, optimizer and random generators
|
|
| 698 |
|
| 699 |
Canonical labels refer to decoded Unicode cell coordinates. Prediction is token-granular. Disabled categories are filtered before edits; overlapping proposals and linked structural repairs require abstention/group handling. Suggestions are not automatically applied. Missing values need not have recoverable intended contents. Real-source unlabelled cells are excluded from negative supervision.
|
| 700 |
|
| 701 |
-
Dataset revision: `{REVISION}`. Train/validation formats are held out; test data was not consumed. This dataset is mostly synthetic. The detector/correction task weight is `ln(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
|
| 703 |
## Licensing
|
| 704 |
|
|
@@ -719,50 +798,98 @@ The learning rate warms up for {cfg.warmup_steps} updates, then follows cosine d
|
|
| 719 |
@torch.no_grad()
|
| 720 |
def generate_repair(model: TabFix, consumer: Consumer, p: Prepared, edit: Edit, device: torch.device) -> str | None:
|
| 721 |
model.eval()
|
| 722 |
-
for capacity in (None,
|
| 723 |
ids, positions, _, _, _ = consumer.correction(p, edit, random.Random(0), False, capacity)
|
| 724 |
end = integer(consumer.tokenizer.convert_tokens_to_ids(CONTROL[1]))
|
|
|
|
| 725 |
mask = integer(consumer.tokenizer.mask_token_id)
|
| 726 |
limit = len(positions)
|
| 727 |
finish: int | None = None
|
| 728 |
-
|
|
|
|
| 729 |
active = [j for j, pos in enumerate(positions) if ids[pos] == mask and (finish is None or j < finish)]
|
| 730 |
if not active:
|
| 731 |
break
|
| 732 |
with autocast(device):
|
| 733 |
logits = model.vocab(model.hidden(ids, device)[[positions[j] for j in active]]).float()
|
| 734 |
for special in consumer.tokenizer.all_special_ids:
|
| 735 |
-
if special
|
| 736 |
logits[:, special] = -torch.inf
|
| 737 |
if finish is not None:
|
| 738 |
-
logits[:, end] = -torch.inf
|
| 739 |
probs = logits.softmax(-1)
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
# Retain its original probability instead of renormalizing it to 1.
|
| 743 |
for i, j in enumerate(active):
|
|
|
|
|
|
|
| 744 |
if j == limit - 1:
|
| 745 |
-
|
| 746 |
-
|
|
|
|
|
|
|
| 747 |
confidence = torch.nan_to_num(confidence, nan=-1.0)
|
| 748 |
pick = int(confidence.argmax())
|
| 749 |
if float(confidence[pick]) < 0:
|
| 750 |
break
|
| 751 |
-
j = active[pick]
|
| 752 |
-
token = int(prediction[pick])
|
| 753 |
-
if token == end:
|
| 754 |
-
# Never discard committed non-padding tokens to its right.
|
| 755 |
-
if any(ids[positions[k]] != mask for k in range(j + 1, limit)):
|
| 756 |
-
logits[pick, end] = -torch.inf
|
| 757 |
-
token = int(logits[pick].argmax())
|
| 758 |
-
else:
|
| 759 |
-
finish = j
|
| 760 |
ids[positions[j]] = token
|
| 761 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
return consumer.literal_tokenizer.decode([ids[positions[j]] for j in range(finish)], skip_special_tokens=False)
|
| 763 |
return None
|
| 764 |
|
| 765 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
@torch.no_grad()
|
| 767 |
def predict_edits(model: TabFix, consumer: Consumer, p: Prepared, device: torch.device,
|
| 768 |
enabled: set[str], threshold: float = .9) -> list[Edit]:
|
|
@@ -773,7 +900,15 @@ def predict_edits(model: TabFix, consumer: Consumer, p: Prepared, device: torch.
|
|
| 773 |
probs = model.bio.forward(h).reshape(-1, K, 3).float().softmax(-1)
|
| 774 |
insert = model.insert.forward(h).float().sigmoid()
|
| 775 |
proposed: list[Edit] = []
|
|
|
|
|
|
|
| 776 |
for c in p.cells:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 777 |
tok = tokens_for(c, p.offsets)
|
| 778 |
for category, name in enumerate(CATEGORIES):
|
| 779 |
if name not in enabled:
|
|
@@ -843,25 +978,24 @@ def monitor(model: TabFix, consumer: Consumer, records: list[Record], cfg: RunCo
|
|
| 843 |
partial: list[float] = []
|
| 844 |
category_counts: Counter[str] = Counter()
|
| 845 |
confusion = [0, 0, 0]
|
| 846 |
-
|
| 847 |
-
for r in
|
|
|
|
|
|
|
|
|
|
| 848 |
p = consumer.prepare(r)
|
| 849 |
-
pairs = conditional_pairs(r)
|
| 850 |
-
if pairs:
|
| 851 |
-
with autocast(device):
|
| 852 |
-
pair_losses.append(float(conditional_pair_loss(model, consumer, pairs[0], device)))
|
| 853 |
with autocast(device):
|
| 854 |
h = model.hidden(p.ids, device)
|
| 855 |
det.append(float(detection_from_hidden(model, p, h)))
|
| 856 |
bio = model.bio.forward(h[p.positions]).reshape(-1, K, 3).float().softmax(-1)[:, :, 1:].sum(-1)
|
| 857 |
gap = model.insert.forward(h[p.gaps]).float().sigmoid()
|
| 858 |
for probs, raw_labels in ((bio, p.labels), (gap, p.gap_labels)):
|
| 859 |
-
labels = torch.tensor(raw_labels, device=device)
|
| 860 |
valid, actual, predicted = labels != -100, labels > 0, probs >= .9
|
| 861 |
confusion[0] += int((predicted & actual & valid).sum())
|
| 862 |
confusion[1] += int((predicted & ~actual & valid).sum())
|
| 863 |
confusion[2] += int((~predicted & actual & valid).sum())
|
| 864 |
-
for e in eligible(r):
|
| 865 |
category_counts[e.category] += 1
|
| 866 |
for all_masked, output in ((True, full), (False, partial)):
|
| 867 |
view = consumer.correction(p, e, random.Random(r.id + str(e)), True, mask_all=all_masked)
|
|
@@ -876,10 +1010,8 @@ def monitor(model: TabFix, consumer: Consumer, records: list[Record], cfg: RunCo
|
|
| 876 |
"selection_score": d + cfg.correction_weight * c,
|
| 877 |
"examples": len(det), "correction_edits": len(full),
|
| 878 |
"correction_category_counts": dict(category_counts),
|
| 879 |
-
"conditional_pair_loss": sum(pair_losses) / len(pair_losses) if pair_losses else None,
|
| 880 |
-
"conditional_pairs": len(pair_losses),
|
| 881 |
"token_and_gap_tp_fp_fn_at_0.9": confusion,
|
| 882 |
-
"sample_ids": [r.id for r in
|
| 883 |
|
| 884 |
|
| 885 |
@torch.no_grad()
|
|
@@ -896,7 +1028,7 @@ def evaluate(model: TabFix, consumer: Consumer, records: list[Record], cfg: RunC
|
|
| 896 |
losses.append(float(detection_loss(model, p, device)))
|
| 897 |
h = model.hidden(p.ids, device)
|
| 898 |
prob = model.bio.forward(h[p.positions]).reshape(-1, K, 3).float().softmax(-1)[:, :, 1:].sum(-1)
|
| 899 |
-
labels = torch.tensor(p.labels, device=device)
|
| 900 |
valid = labels != -100
|
| 901 |
actual = labels > 0
|
| 902 |
for threshold, v in counts.items():
|
|
@@ -915,14 +1047,15 @@ def evaluate(model: TabFix, consumer: Consumer, records: list[Record], cfg: RunC
|
|
| 915 |
e = eligible_edits[0]
|
| 916 |
_, _, _, _, target = consumer.correction(p, e, random.Random(0), False)
|
| 917 |
answer = generate_repair(model, consumer, p, e, device)
|
| 918 |
-
|
|
|
|
| 919 |
except ValueError as exc:
|
| 920 |
consumer.stats[str(exc)] += 1
|
| 921 |
return {"sample_policy": "seeded language x positive/clean round-robin validation sample",
|
| 922 |
"validation_examples": len(losses), "detection_loss": sum(losses) / max(1, len(losses)),
|
| 923 |
"correction_examples": len(correction_losses), "correction_loss": sum(correction_losses) / max(1, len(correction_losses)),
|
| 924 |
"token_category_confusion_tp_fp_fn_tn": counts,
|
| 925 |
-
"
|
| 926 |
"limitations": "Token diagnostics, not cell/span calibration; limited gold-span repair sample; test untouched."}
|
| 927 |
|
| 928 |
|
|
@@ -941,9 +1074,6 @@ def main(cfg: RunConfig) -> None:
|
|
| 941 |
emit("verified", step=manifest["step"], parameters=sum(p.numel() for p in model.parameters()))
|
| 942 |
return
|
| 943 |
old = read_json(resume_path / "run_config.json")
|
| 944 |
-
previous_pair_weight = number(old.get("conditional_pair_weight", 0.0))
|
| 945 |
-
if previous_pair_weight != cfg.conditional_pair_weight and not cfg.allow_objective_change:
|
| 946 |
-
raise ValueError("Conditional pair weight changed on resume; retain the saved weight or explicitly pass --allow-objective-change for an ablation")
|
| 947 |
for name in ("seed", "lr", "max_length", "correction_weight", "batch_size", "tiny", "warmup_steps", "schedule_steps", "validation_examples", "patience", "min_delta"):
|
| 948 |
if old.get(name) != asdict(cfg)[name]:
|
| 949 |
raise ValueError(f"Resume config mismatch: {name}")
|
|
@@ -965,25 +1095,23 @@ def main(cfg: RunConfig) -> None:
|
|
| 965 |
torch.set_rng_state(cast(Tensor, state["torch_rng"]))
|
| 966 |
if device.type == "cuda":
|
| 967 |
torch.cuda.set_rng_state_all(cast(list[Tensor], state["cuda_rng"]))
|
| 968 |
-
train =
|
| 969 |
validation = read_records("validation", cfg)
|
| 970 |
weights = [number(r.metadata["sampling_weight"]) for r in train]
|
| 971 |
repair_indices = [i for i, r in enumerate(train) if eligible(r)]
|
| 972 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
if cfg.batch_size < 1:
|
| 974 |
raise ValueError("Batch size must be positive")
|
| 975 |
-
if cfg.conditional_pair_weight < 0:
|
| 976 |
-
raise ValueError("Conditional pair weight must be nonnegative")
|
| 977 |
-
pair_pool = [pair for r in train for pair in conditional_pairs(r)] if cfg.conditional_pair_weight else []
|
| 978 |
-
# Deduplicate identical paired contexts so repeated negative records do not increase exposure.
|
| 979 |
-
pair_pool = list({(p.negative.xml, p.positive.xml, p.row, p.column): p for p in pair_pool}.values())
|
| 980 |
-
if cfg.conditional_pair_weight and not pair_pool:
|
| 981 |
-
raise ValueError("No certified conditional pairs available in training split")
|
| 982 |
api = HfApi() if cfg.repo_id else None
|
| 983 |
if api:
|
| 984 |
# A fresh objective intentionally replaces the existing preview repo;
|
| 985 |
# the upload commits remain atomic and the local checkpoint is recoverable.
|
| 986 |
-
_ = api.
|
| 987 |
@lru_cache(maxsize=64)
|
| 988 |
def prepared(i: int) -> Prepared:
|
| 989 |
return consumer.prepare(train[i])
|
|
@@ -993,7 +1121,7 @@ def main(cfg: RunConfig) -> None:
|
|
| 993 |
validation_history.extend(entry for item in sequence(prior_report.get("validation_history", [])) if integer((entry := mapping(item))["step"]) <= step)
|
| 994 |
report: dict[str, object] = {"tiny_smoke": cfg.tiny, "dataset_revision": REVISION,
|
| 995 |
"train_records": len(train), "correction_eligible_records": len(repair_indices),
|
| 996 |
-
"correction_request_version":
|
| 997 |
"start_step": step, "device": str(device), "parameters": sum(p.numel() for p in model.parameters()),
|
| 998 |
"validation_history": validation_history}
|
| 999 |
emit("ready", **report)
|
|
@@ -1008,7 +1136,7 @@ def main(cfg: RunConfig) -> None:
|
|
| 1008 |
selection = Selection(number(state_selection["best_score"]), integer(state_selection["best_step"]), integer(state_selection["bad_checks"]))
|
| 1009 |
if not best_path.exists():
|
| 1010 |
if cfg.repo_id:
|
| 1011 |
-
source_best = Path(snapshot_download(cfg.repo_id, revision=f"best-step-{selection.best_step}"))
|
| 1012 |
elif selection.best_step == step:
|
| 1013 |
source_best = resume_path
|
| 1014 |
else:
|
|
@@ -1031,7 +1159,7 @@ def main(cfg: RunConfig) -> None:
|
|
| 1031 |
shutil.copytree(current, best_path, dirs_exist_ok=True)
|
| 1032 |
if api:
|
| 1033 |
uploaded = read_json(Path(cfg.output) / "last_upload.json")
|
| 1034 |
-
api.create_tag(repo_id=cfg.repo_id, tag=f"best-step-{step}", revision=string(uploaded["commit"]), exist_ok=True)
|
| 1035 |
last_validation_at = saved_at = time.monotonic()
|
| 1036 |
|
| 1037 |
if not resume_path:
|
|
@@ -1044,7 +1172,7 @@ def main(cfg: RunConfig) -> None:
|
|
| 1044 |
optimizer.zero_grad(set_to_none=True)
|
| 1045 |
for group in optimizer.param_groups:
|
| 1046 |
group["lr"] = learning_rate(cfg, step)
|
| 1047 |
-
batch = [prepared(rng.choices(
|
| 1048 |
with autocast(device):
|
| 1049 |
hidden = model.hidden_batch([p.ids for p in batch], device)
|
| 1050 |
det = torch.stack([detection_from_hidden(model, p, hidden[i]) for i, p in enumerate(batch)]).mean()
|
|
@@ -1053,9 +1181,12 @@ def main(cfg: RunConfig) -> None:
|
|
| 1053 |
det.backward() # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 1054 |
views: list[tuple[list[int], list[int], list[int], list[bool], str]] = []
|
| 1055 |
for _ in range(cfg.batch_size * 20):
|
| 1056 |
-
|
|
|
|
|
|
|
|
|
|
| 1057 |
try:
|
| 1058 |
-
views.append(consumer.correction(rp, rng.choice(
|
| 1059 |
except ValueError as exc:
|
| 1060 |
consumer.stats[str(exc)] += 1
|
| 1061 |
if len(views) == cfg.batch_size:
|
|
@@ -1070,15 +1201,6 @@ def main(cfg: RunConfig) -> None:
|
|
| 1070 |
raise FloatingPointError("Nonfinite correction loss")
|
| 1071 |
cor.backward() # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 1072 |
cor_value = float(unweighted_cor.detach())
|
| 1073 |
-
pair_value: float | None = None
|
| 1074 |
-
if pair_pool:
|
| 1075 |
-
with autocast(device):
|
| 1076 |
-
auxiliary = conditional_pair_loss(model, consumer, rng.choice(pair_pool), device)
|
| 1077 |
-
if not bool(torch.isfinite(auxiliary)):
|
| 1078 |
-
raise FloatingPointError("Nonfinite conditional pair loss")
|
| 1079 |
-
(cfg.conditional_pair_weight * auxiliary).backward() # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 1080 |
-
pair_value = float(auxiliary.detach())
|
| 1081 |
-
report["last_conditional_pair_loss"] = pair_value
|
| 1082 |
_ = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0, error_if_nonfinite=True)
|
| 1083 |
optimizer.step() # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 1084 |
step += 1
|
|
@@ -1111,7 +1233,7 @@ def main(cfg: RunConfig) -> None:
|
|
| 1111 |
check_validation()
|
| 1112 |
if api:
|
| 1113 |
last_upload = read_json(Path(cfg.output) / "last_upload.json")
|
| 1114 |
-
api.create_tag(repo_id=cfg.repo_id, tag=f"latest-step-{step}", revision=string(last_upload["commit"]), exist_ok=True)
|
| 1115 |
report["last_training_step"] = step
|
| 1116 |
report["selected_step"] = selection.best_step
|
| 1117 |
report["selection_at_stop"] = asdict(selection)
|
|
@@ -1153,7 +1275,7 @@ def parse_args() -> RunConfig:
|
|
| 1153 |
save_seconds=integer(args["save_seconds"]), lr=number(args["lr"]), seed=integer(args["seed"]),
|
| 1154 |
validation_examples=integer(args["validation_examples"]), validation_repairs=integer(args["validation_repairs"]), validation_seconds=integer(args["validation_seconds"]),
|
| 1155 |
validation_steps=integer(args["validation_steps"]), patience=integer(args["patience"]), min_delta=number(args["min_delta"]), warmup_steps=integer(args["warmup_steps"]), schedule_steps=integer(args["schedule_steps"]),
|
| 1156 |
-
max_length=integer(args["max_length"]), correction_weight=number(args["correction_weight"]), batch_size=integer(args["batch_size"]),
|
| 1157 |
|
| 1158 |
|
| 1159 |
def handle_signal(_signum: int, _frame: object) -> None:
|
|
|
|
| 22 |
from contextlib import nullcontext
|
| 23 |
from dataclasses import asdict, dataclass
|
| 24 |
from decimal import Decimal, InvalidOperation
|
| 25 |
+
from datetime import date, datetime
|
| 26 |
from functools import lru_cache
|
| 27 |
import hashlib
|
| 28 |
import html
|
|
|
|
| 48 |
from transformers import AutoTokenizer, ModernBertConfig, ModernBertForMaskedLM, PreTrainedTokenizerFast
|
| 49 |
|
| 50 |
DATASET = "Antix5/tabular-errors-v1"
|
| 51 |
+
REVISION = "602b8a454ca448713d8a5658650d4feaad8d579c"
|
| 52 |
BASE = "jhu-clsp/mmBERT-base"
|
| 53 |
BASE_REVISION = "c5955035435e2bf121cde7f3c8863ef52ff35d82"
|
| 54 |
+
BUSINESS_CATEGORIES = (
|
| 55 |
"text.encoding", "text.invisible", "text.spelling", "format.number",
|
| 56 |
"format.date", "format.boolean", "format.case", "format.whitespace",
|
| 57 |
"format.unit", "schema.type", "schema.identifier", "schema.category",
|
|
|
|
| 59 |
"missing.required", "missing.conditional", "structure.shift",
|
| 60 |
)
|
| 61 |
CONTROL = ("<|tabfix_repair|>", "<|tabfix_end|>", "<|tabfix_pad|>")
|
| 62 |
+
CATEGORIES = ("text.encoding", "text.spelling")
|
| 63 |
K = len(CATEGORIES)
|
| 64 |
stop_requested = False
|
| 65 |
|
|
|
|
| 106 |
print(json.dumps({"event": event, **fields}, ensure_ascii=False), flush=True)
|
| 107 |
|
| 108 |
|
| 109 |
+
@dataclass(frozen=True)
|
| 110 |
+
class RuleFinding:
|
| 111 |
+
row: int
|
| 112 |
+
columns: tuple[int, ...]
|
| 113 |
+
category: str
|
| 114 |
+
rule: str
|
| 115 |
+
status: str
|
| 116 |
+
evidence: str
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def xml_json(element: ET.Element, name: str, default: object) -> object:
|
| 120 |
+
text = element.findtext(name)
|
| 121 |
+
return cast(object, json.loads(text)) if text else default
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def column_checks(value: str, column: ET.Element) -> list[tuple[str, str, bool | None]]:
|
| 125 |
+
"""Check declared contracts only; None means unsupported/undetermined."""
|
| 126 |
+
rules = mapping(xml_json(column, "constraints", {}))
|
| 127 |
+
formats = [string(x) for x in sequence(xml_json(column, "accepted_formats", []))]
|
| 128 |
+
nulls = sequence(xml_json(column, "null_markers", []))
|
| 129 |
+
if not value or value in nulls:
|
| 130 |
+
return [("missing.required", "nullability", column.get("nullable") != "forbidden")]
|
| 131 |
+
result: list[tuple[str, str, bool | None]] = []
|
| 132 |
+
for key in ("enum", "lexicon"):
|
| 133 |
+
if rules.get(key):
|
| 134 |
+
result.append(("format.boolean" if column.get("type") == "boolean" else "schema.category", key, value in sequence(rules[key])))
|
| 135 |
+
if isinstance(rules.get("pattern"), str):
|
| 136 |
+
try:
|
| 137 |
+
result.append(("schema.identifier", "pattern", re.fullmatch(string(rules["pattern"]), value) is not None))
|
| 138 |
+
except re.error:
|
| 139 |
+
result.append(("schema.identifier", "pattern", None))
|
| 140 |
+
for key in ("upper", "lower"):
|
| 141 |
+
if rules.get("case") == key:
|
| 142 |
+
result.append(("format.case", "case", value == (value.upper() if key == "upper" else value.lower())))
|
| 143 |
+
if rules.get("trimmed") is True:
|
| 144 |
+
result.append(("format.whitespace", "trimmed", value == value.strip()))
|
| 145 |
+
forbidden = rules.get("forbidden_characters")
|
| 146 |
+
if isinstance(forbidden, list):
|
| 147 |
+
result.append(("text.invisible", "forbidden_characters", all(string(c) not in value for c in sequence(cast(object, forbidden)))))
|
| 148 |
+
kind = column.get("type")
|
| 149 |
+
if value in sequence(rules.get("accepted_representations", [])):
|
| 150 |
+
return result
|
| 151 |
+
if kind in ("integer", "decimal", "numeric_syntax"):
|
| 152 |
+
patterns: list[str] = []
|
| 153 |
+
if kind == "integer":
|
| 154 |
+
patterns = [r"[+-]?[0-9]+"]
|
| 155 |
+
elif kind == "numeric_syntax":
|
| 156 |
+
patterns = [r"[+-]?(?=[0-9., \u00a0\u202f]*[0-9])[0-9., \u00a0\u202f]+(?:[eE][+-]?[0-9]+)?"]
|
| 157 |
+
else:
|
| 158 |
+
for fmt in formats:
|
| 159 |
+
if fmt.startswith("decimal_"):
|
| 160 |
+
sep = "," if "comma" in fmt else r"\."
|
| 161 |
+
patterns.append(r"[+-]?[0-9]+" + (sep + r"[0-9]{2}" if "2dp" in fmt else "(?:" + sep + r"[0-9]+)?"))
|
| 162 |
+
valid = any(re.fullmatch(p, value) for p in patterns) if patterns else None
|
| 163 |
+
result.append(("schema.type" if kind == "integer" else "format.number", "numeric_format", bool(valid) if valid is not None else None))
|
| 164 |
+
if valid and kind != "numeric_syntax":
|
| 165 |
+
parsed = Decimal(value.replace(",", "."))
|
| 166 |
+
for key in ("minimum", "maximum"):
|
| 167 |
+
bound = rules.get(key)
|
| 168 |
+
if isinstance(bound, (int, float)):
|
| 169 |
+
result.append(("schema.type", key, parsed >= Decimal(str(bound)) if key == "minimum" else parsed <= Decimal(str(bound))))
|
| 170 |
+
elif kind == "date":
|
| 171 |
+
fmts = {"YYYY-MM-DD": "%Y-%m-%d", "DD/MM/YYYY": "%d/%m/%Y", "MM/DD/YYYY": "%m/%d/%Y"}
|
| 172 |
+
valid_date = False
|
| 173 |
+
supported = bool(formats) and all(f in fmts for f in formats)
|
| 174 |
+
for fmt in formats:
|
| 175 |
+
if fmt in fmts:
|
| 176 |
+
try:
|
| 177 |
+
dt = datetime.strptime(value, fmts[fmt])
|
| 178 |
+
valid_date |= dt.strftime(fmts[fmt]) == value
|
| 179 |
+
except ValueError:
|
| 180 |
+
pass
|
| 181 |
+
result.append(("format.date", "calendar_format", valid_date if supported else None))
|
| 182 |
+
return result
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def deterministic_findings(xml: str) -> list[RuleFinding]:
|
| 186 |
+
root = ET.fromstring(xml)
|
| 187 |
+
columns = root.findall("./schema/column")
|
| 188 |
+
schema = root.find("schema")
|
| 189 |
+
if schema is None:
|
| 190 |
+
return []
|
| 191 |
+
keys = {c.get("key", ""): i for i, c in enumerate(columns) if c.get("key")}
|
| 192 |
+
relations = sequence(xml_json(schema, "relations", []))
|
| 193 |
+
dependencies = mapping(xml_json(schema, "functional_dependencies", {}))
|
| 194 |
+
findings: list[RuleFinding] = []
|
| 195 |
+
for row in root.findall("./rows/row"):
|
| 196 |
+
ri = int(row.attrib["index"])
|
| 197 |
+
values = [c.text or "" for c in row.findall("cell")]
|
| 198 |
+
if len(values) != len(columns):
|
| 199 |
+
continue
|
| 200 |
+
checks = [column_checks(v, c) for v, c in zip(values, columns)]
|
| 201 |
+
for ci, (value, col, cc) in enumerate(zip(values, columns, checks)):
|
| 202 |
+
for cat, rule, valid in cc:
|
| 203 |
+
findings.append(RuleFinding(ri, (ci,), cat, rule, "unknown" if valid is None else "valid" if valid else "invalid", "declared column contract"))
|
| 204 |
+
rules = mapping(xml_json(col, "constraints", {}))
|
| 205 |
+
condition = rules.get("required_when")
|
| 206 |
+
if isinstance(condition, dict):
|
| 207 |
+
cond = mapping(cast(object, condition))
|
| 208 |
+
determinant = cond.get("column")
|
| 209 |
+
valid_condition = isinstance(determinant, int) and 0 <= determinant < len(values)
|
| 210 |
+
known = valid_condition and bool(values[cast(int, determinant)]) and all(v is True for _, _, v in checks[cast(int, determinant)])
|
| 211 |
+
required: bool | None = None
|
| 212 |
+
if known:
|
| 213 |
+
dv = values[cast(int, determinant)]
|
| 214 |
+
if "equals" in cond:
|
| 215 |
+
required = dv == cond["equals"]
|
| 216 |
+
elif "not_equals" in cond:
|
| 217 |
+
required = dv != cond["not_equals"]
|
| 218 |
+
present = bool(value) and value not in sequence(xml_json(col, "null_markers", []))
|
| 219 |
+
findings.append(RuleFinding(ri, (ci,), "missing.conditional", "required_when", "unknown" if required is None else "valid" if present or not required else "invalid", "declared condition; determinant must be valid"))
|
| 220 |
+
for raw in relations:
|
| 221 |
+
if not isinstance(raw, list) or not raw:
|
| 222 |
+
continue
|
| 223 |
+
rel = [string(x) for x in sequence(cast(object, raw))]
|
| 224 |
+
kind, args = rel[0], rel[1:]
|
| 225 |
+
if any(key not in keys for key in args):
|
| 226 |
+
continue
|
| 227 |
+
ix = tuple(keys[key] for key in args)
|
| 228 |
+
cat = "consistency.unit" if kind in ("unit_equivalence", "pack_mass", "unit_factor") else "consistency.temporal" if kind == "interval" else "consistency.dependency"
|
| 229 |
+
if kind == "conditional":
|
| 230 |
+
continue # Executed through required_when.
|
| 231 |
+
status = "unknown"
|
| 232 |
+
if all(values[i] and all(v is True for _, _, v in checks[i]) for i in ix):
|
| 233 |
+
vv = [values[i] for i in ix]
|
| 234 |
+
try:
|
| 235 |
+
valid: bool | None = None
|
| 236 |
+
if kind == "lookup" and len(ix) == 2:
|
| 237 |
+
reference = mapping(dependencies.get(args[1], {})).get(vv[0])
|
| 238 |
+
valid = vv[1] == reference if reference is not None else None
|
| 239 |
+
elif kind == "interval" and len(ix) == 3:
|
| 240 |
+
start, end = date.fromisoformat(vv[0]), date.fromisoformat(vv[1])
|
| 241 |
+
valid = end >= start and Decimal(vv[2]) == (end - start).days
|
| 242 |
+
elif kind == "unit_equivalence" and len(ix) == 4:
|
| 243 |
+
factors = {"kg": Decimal(1000), "g": Decimal(1), "mg": Decimal(".001")}
|
| 244 |
+
if vv[1] in factors and vv[3] in factors:
|
| 245 |
+
valid = Decimal(vv[0].replace(",", ".")) * factors[vv[1]] == Decimal(vv[2].replace(",", ".")) * factors[vv[3]]
|
| 246 |
+
elif kind in ("difference", "multiply", "balance", "le"):
|
| 247 |
+
nums = [Decimal(v.replace(",", ".")) for v in vv]
|
| 248 |
+
if kind == "le" and len(nums) == 2:
|
| 249 |
+
valid = nums[0] <= nums[1]
|
| 250 |
+
elif len(nums) == 3:
|
| 251 |
+
expected = nums[0] * nums[1] if kind == "multiply" else nums[0] - nums[1]
|
| 252 |
+
# Compare at the explicitly declared output precision.
|
| 253 |
+
formats = sequence(xml_json(columns[ix[-1]], "accepted_formats", []))
|
| 254 |
+
if any("2dp" in string(f) for f in formats):
|
| 255 |
+
expected = expected.quantize(Decimal(".01"))
|
| 256 |
+
valid = expected == nums[2]
|
| 257 |
+
status = "unknown" if valid is None else "valid" if valid else "invalid"
|
| 258 |
+
except (ValueError, InvalidOperation):
|
| 259 |
+
status = "unknown"
|
| 260 |
+
findings.append(RuleFinding(ri, ix, cat, kind, status, "relation contradiction identifies a group, not its faulty member"))
|
| 261 |
+
return findings
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def deterministic_repair(value: str, column: ET.Element) -> str | None:
|
| 265 |
+
"""Only unambiguous canonicalization, never fill missing information."""
|
| 266 |
+
if not value:
|
| 267 |
+
return None
|
| 268 |
+
rules = mapping(xml_json(column, "constraints", {}))
|
| 269 |
+
candidate = value.strip() if rules.get("trimmed") is True else value
|
| 270 |
+
if rules.get("case") == "upper":
|
| 271 |
+
candidate = candidate.upper()
|
| 272 |
+
elif rules.get("case") == "lower":
|
| 273 |
+
candidate = candidate.lower()
|
| 274 |
+
enum = sequence(rules.get("enum", []))
|
| 275 |
+
equivalent = [string(v) for v in enum if string(v).casefold() == candidate.casefold()]
|
| 276 |
+
if candidate not in enum and len(equivalent) == 1 and column.get("type") == "boolean":
|
| 277 |
+
candidate = equivalent[0]
|
| 278 |
+
checks = column_checks(candidate, column)
|
| 279 |
+
if candidate != value and checks and all(ok is True for _, _, ok in checks):
|
| 280 |
+
return candidate
|
| 281 |
+
return None
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def replace_cell_xml(xml: str, row: int, column: int, value: str) -> str:
|
| 285 |
+
cell = next(c for c in cells(xml) if (c.row, c.column) == (row, column))
|
| 286 |
+
escaped = html.escape(value, quote=False).replace("\r", " ").replace("\n", " ").replace("\t", "	") if value else "<empty/>"
|
| 287 |
+
return xml[:cell.anchor] + escaped + xml[cell.close:]
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def invalid_cells(xml: str) -> set[tuple[int, int]]:
|
| 291 |
+
return {(f.row, c) for f in deterministic_findings(xml) if f.status == "invalid" for c in f.columns}
|
| 292 |
+
|
| 293 |
+
|
| 294 |
@dataclass(frozen=True)
|
| 295 |
class Edit:
|
| 296 |
row: int
|
|
|
|
| 439 |
for at in boundaries:
|
| 440 |
gaps[anchor_token(c, offsets, at)] = [0 if trusted else -100] * K
|
| 441 |
collisions = 0
|
| 442 |
+
neural_indices = record.metadata.get("neural_error_indices")
|
| 443 |
+
for ei, e in enumerate(record.edits):
|
| 444 |
+
if e.category not in CATEGORIES or (isinstance(neural_indices, list) and ei not in neural_indices):
|
| 445 |
+
continue
|
| 446 |
c = next(c for c in cc if (c.row, c.column) == (e.row, e.column))
|
| 447 |
hit, _, _ = interval(c, offsets, e)
|
| 448 |
category = CATEGORIES.index(e.category)
|
|
|
|
| 454 |
target[category] = 1 if j == 0 else 2
|
| 455 |
else:
|
| 456 |
gaps.setdefault(anchor_token(c, offsets, e.start), [-100] * K)[category] = 1
|
| 457 |
+
# Deterministic cells are not part of the residual neural objective.
|
| 458 |
+
blocked: set[tuple[int, int]] = invalid_cells(record.xml) if record.metadata.get("task_version") == 2 else set()
|
| 459 |
+
for c in cc:
|
| 460 |
+
if (c.row, c.column) in blocked:
|
| 461 |
+
for t in tokens_for(c, offsets):
|
| 462 |
+
labels.pop(t, None)
|
| 463 |
+
for at in range(len(c.value) + 1):
|
| 464 |
+
gaps.pop(anchor_token(c, offsets, at), None)
|
| 465 |
pos = sorted(labels)
|
| 466 |
gp = sorted(gaps)
|
| 467 |
return Prepared(record, ids, offsets, cc, pos, [labels[p] for p in pos], gp,
|
|
|
|
| 470 |
def correction(self, p: Prepared, e: Edit, rng: random.Random, training: bool,
|
| 471 |
capacity: int | None = None, mask_all: bool = False) -> tuple[list[int], list[int], list[int], list[bool], str]:
|
| 472 |
c = next(c for c in p.cells if (c.row, c.column) == (e.row, e.column))
|
| 473 |
+
# Whole-cell target, original retained locally. The edit is supplied by
|
| 474 |
+
# correction_candidates; its replacement is never put into inference input.
|
| 475 |
+
target = c.value[:e.start] + e.replacement + c.value[e.end:]
|
| 476 |
+
gold = self.literal(target) if training else []
|
| 477 |
+
n = len(self.literal(c.value))
|
| 478 |
+
budget = capacity or min(256, max(8, n + max(2, math.ceil(.2 * n))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
if training and len(gold) > budget:
|
| 480 |
+
raise ValueError("Whole-cell repair exceeds source-derived buffer")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
mask_id = integer(self.tokenizer.mask_token_id)
|
| 482 |
+
original = p.record.xml[c.anchor:c.close]
|
| 483 |
+
local = "<original>" + original + "</original><replacement>" + string(self.tokenizer.mask_token) * (budget + 1) + "</replacement>"
|
| 484 |
+
skeleton = p.record.xml[:c.anchor] + local + p.record.xml[c.close:]
|
| 485 |
ids = self.tokenizer.encode(skeleton, add_special_tokens=True) # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 486 |
positions = [i for i, token in enumerate(ids) if token == mask_id]
|
| 487 |
if len(positions) != budget + 1:
|
|
|
|
| 506 |
|
| 507 |
|
| 508 |
def eligible(record: Record) -> list[Edit]:
|
| 509 |
+
"""Whole-cell targets, including certified valid copies, stored in release v2."""
|
| 510 |
+
candidates = record.metadata.get("correction_candidates")
|
| 511 |
+
if isinstance(candidates, list):
|
| 512 |
+
return [Edit.parse(e) for e in sequence(cast(object, candidates))]
|
| 513 |
+
# Local callers/tests can supply one selected edit directly.
|
| 514 |
result: list[Edit] = []
|
| 515 |
for value in sequence(record.metadata.get("repair_policies", [])):
|
| 516 |
policy = mapping(value)
|
|
|
|
| 519 |
return result
|
| 520 |
|
| 521 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
class Batch(Protocol):
|
| 523 |
def to_pylist(self) -> list[object]: ...
|
| 524 |
|
|
|
|
| 570 |
|
| 571 |
def detection_from_hidden(model: TabFix, p: Prepared, h: Tensor) -> Tensor:
|
| 572 |
device = h.device
|
| 573 |
+
labels = torch.tensor(p.labels, device=device, dtype=torch.long).reshape(-1, K)
|
| 574 |
logits = model.bio.forward(h[p.positions]).reshape(-1, K, 3)
|
| 575 |
loss = F.cross_entropy(logits.reshape(-1, 3).float(), labels.reshape(-1), ignore_index=-100, reduction="none").reshape(-1, K)
|
| 576 |
bio = mean_parts(loss, labels > 0, labels != -100)
|
| 577 |
+
gl = torch.tensor(p.gap_labels, device=device, dtype=torch.float32).reshape(-1, K)
|
| 578 |
gi = model.insert.forward(h[p.gaps])
|
| 579 |
il = F.binary_cross_entropy_with_logits(gi.float(), gl.clamp_min(0), reduction="none")
|
| 580 |
return bio + .5 * mean_parts(il, gl > 0, gl != -100)
|
|
|
|
| 618 |
warmup_steps: int = 100
|
| 619 |
schedule_steps: int = 2400
|
| 620 |
max_length: int = 8192
|
| 621 |
+
correction_weight: float = math.log(K) / math.log(256000)
|
| 622 |
batch_size: int = 2
|
| 623 |
verify_only: bool = False
|
|
|
|
|
|
|
| 624 |
|
| 625 |
|
| 626 |
def learning_rate(cfg: RunConfig, step: int) -> float:
|
|
|
|
| 691 |
config._attn_implementation = "sdpa" # pyright: ignore[reportPrivateUsage] — third-party typing boundary
|
| 692 |
mlm = ModernBertForMaskedLM(config)
|
| 693 |
model = TabFix(mlm)
|
| 694 |
+
saved_categories = cast(object, json.loads((path / "categories.json").read_text()))
|
| 695 |
+
if saved_categories != list(CATEGORIES):
|
| 696 |
+
raise ValueError("Checkpoint uses another label map; use its bundled training script")
|
| 697 |
# Store tied embedding once; the duplicate decoder weight shares storage.
|
| 698 |
weights = load_file(str(path / "model.safetensors"))
|
| 699 |
weights["mlm.decoder.weight"] = weights["mlm.model.embeddings.tok_embeddings.weight"]
|
|
|
|
| 756 |
|
| 757 |
{'Temporary random tiny smoke-test model, not a pretrained fine-tune.' if cfg.tiny else 'Experimental budget-limited mmBERT-base joint fine-tuning checkpoint.'}
|
| 758 |
|
| 759 |
+
One shared encoder, {K} residual category BIO/insertion outputs (`text.encoding`, `text.spelling`), and the pretrained MLM vocabulary head. Declared schema rules detect deterministic violations before neural classification. Correction generates a complete cell inside `<replacement>` beside the visible `<original>` in the same cell. Valid copies are training targets too. The MLM head emits replacement tokens followed by `END_EDIT` and visible padding tokens.
|
| 760 |
Completed optimizer updates: **{step}**. This checkpoint is a research baseline, not a validated automatic cleaning system.
|
| 761 |
|
| 762 |
## Use and resume
|
|
|
|
| 769 |
|
| 770 |
Canonical labels refer to decoded Unicode cell coordinates. Prediction is token-granular. Disabled categories are filtered before edits; overlapping proposals and linked structural repairs require abstention/group handling. Suggestions are not automatically applied. Missing values need not have recoverable intended contents. Real-source unlabelled cells are excluded from negative supervision.
|
| 771 |
|
| 772 |
+
Dataset revision: `{REVISION}`. Train/validation formats are held out; test data was not consumed. This dataset is mostly synthetic. The detector/correction task weight is `ln({K})/ln(256000) = {cfg.correction_weight:.6f}`; valid-copy correction examples are sampled with probability 0.30. The run records periodic held-out validation losses for both tasks in `validation_history`, then a final validation report. These are development diagnostics, not deployment precision or end-to-end repair accuracy. Conditional pseudo-perplexity is scored with each response token masked in turn, original visible; an acceptance threshold is fitted on the reported validation sample. Its sample size and observed errors limit its reliability. No latency guarantee is claimed. See `metrics.json` for sample sizes, loss, operating-point diagnostics and exact-repair outcomes.
|
| 773 |
+
|
| 774 |
+
## Validation results
|
| 775 |
+
|
| 776 |
+
Selected step: {report.get('selected_step', step)}. Stop reason: {report.get('stop_reason', 'training in progress')}.
|
| 777 |
+
|
| 778 |
+
```json
|
| 779 |
+
{json.dumps({key: value for key, value in mapping(report.get('validation', {})).items() if key != 'prediction_sample'}, ensure_ascii=False, indent=2)}
|
| 780 |
+
```
|
| 781 |
|
| 782 |
## Licensing
|
| 783 |
|
|
|
|
| 798 |
@torch.no_grad()
|
| 799 |
def generate_repair(model: TabFix, consumer: Consumer, p: Prepared, edit: Edit, device: torch.device) -> str | None:
|
| 800 |
model.eval()
|
| 801 |
+
for capacity in (None, 256):
|
| 802 |
ids, positions, _, _, _ = consumer.correction(p, edit, random.Random(0), False, capacity)
|
| 803 |
end = integer(consumer.tokenizer.convert_tokens_to_ids(CONTROL[1]))
|
| 804 |
+
pad = integer(consumer.tokenizer.convert_tokens_to_ids(CONTROL[2]))
|
| 805 |
mask = integer(consumer.tokenizer.mask_token_id)
|
| 806 |
limit = len(positions)
|
| 807 |
finish: int | None = None
|
| 808 |
+
# PAD hypotheses before the selected END may need one additional pass.
|
| 809 |
+
for _ in range(2 * limit):
|
| 810 |
active = [j for j, pos in enumerate(positions) if ids[pos] == mask and (finish is None or j < finish)]
|
| 811 |
if not active:
|
| 812 |
break
|
| 813 |
with autocast(device):
|
| 814 |
logits = model.vocab(model.hidden(ids, device)[[positions[j] for j in active]]).float()
|
| 815 |
for special in consumer.tokenizer.all_special_ids:
|
| 816 |
+
if special not in (end, pad):
|
| 817 |
logits[:, special] = -torch.inf
|
| 818 |
if finish is not None:
|
| 819 |
+
logits[:, end] = logits[:, pad] = -torch.inf
|
| 820 |
probs = logits.softmax(-1)
|
| 821 |
+
# Keep original probabilities when restricting boundary candidates.
|
| 822 |
+
candidates = probs.clone()
|
|
|
|
| 823 |
for i, j in enumerate(active):
|
| 824 |
+
if finish is None and j == 0:
|
| 825 |
+
candidates[i, pad] = -1
|
| 826 |
if j == limit - 1:
|
| 827 |
+
candidates[i] = -1
|
| 828 |
+
candidates[i, end] = probs[i, end]
|
| 829 |
+
candidates[i, pad] = probs[i, pad]
|
| 830 |
+
confidence, prediction = candidates.max(-1)
|
| 831 |
confidence = torch.nan_to_num(confidence, nan=-1.0)
|
| 832 |
pick = int(confidence.argmax())
|
| 833 |
if float(confidence[pick]) < 0:
|
| 834 |
break
|
| 835 |
+
j, token = active[pick], int(prediction[pick])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
ids[positions[j]] = token
|
| 837 |
+
if token == end:
|
| 838 |
+
finish = j
|
| 839 |
+
# END defines the replacement length, including immediate deletion.
|
| 840 |
+
# Earlier hypotheses to its right must not prevent termination.
|
| 841 |
+
for k, pos in enumerate(positions):
|
| 842 |
+
if k > j:
|
| 843 |
+
ids[pos] = pad
|
| 844 |
+
elif k < j and ids[pos] == pad:
|
| 845 |
+
ids[pos] = mask
|
| 846 |
+
if finish is not None and all(ids[positions[j]] not in (mask, pad, end) for j in range(finish)):
|
| 847 |
return consumer.literal_tokenizer.decode([ids[positions[j]] for j in range(finish)], skip_special_tokens=False)
|
| 848 |
return None
|
| 849 |
|
| 850 |
|
| 851 |
+
@torch.no_grad()
|
| 852 |
+
def conditional_perplexity(model: TabFix, consumer: Consumer, p: Prepared, edit: Edit,
|
| 853 |
+
answer: str, device: torch.device) -> float:
|
| 854 |
+
tokens = consumer.literal(answer)
|
| 855 |
+
end = integer(consumer.tokenizer.convert_tokens_to_ids(CONTROL[1]))
|
| 856 |
+
pad = integer(consumer.tokenizer.convert_tokens_to_ids(CONTROL[2]))
|
| 857 |
+
mask = integer(consumer.tokenizer.mask_token_id)
|
| 858 |
+
ids, positions, _, _, _ = consumer.correction(p, edit, random.Random(0), False,
|
| 859 |
+
max(8, len(tokens)))
|
| 860 |
+
target = tokens + [end]
|
| 861 |
+
for j, pos in enumerate(positions):
|
| 862 |
+
ids[pos] = target[j] if j < len(target) else pad
|
| 863 |
+
losses: list[float] = []
|
| 864 |
+
model.eval()
|
| 865 |
+
for start in range(0, len(target), 2):
|
| 866 |
+
indices = list(range(start, min(start + 2, len(target))))
|
| 867 |
+
views = [ids.copy() for _ in indices]
|
| 868 |
+
for view, j in zip(views, indices):
|
| 869 |
+
view[positions[j]] = mask
|
| 870 |
+
with autocast(device):
|
| 871 |
+
hidden = model.hidden_batch(views, device)
|
| 872 |
+
selected = torch.stack([hidden[k, positions[j]] for k, j in enumerate(indices)])
|
| 873 |
+
logits = model.vocab(selected).float()
|
| 874 |
+
losses.extend(cast(list[float], F.cross_entropy(logits, torch.tensor([target[j] for j in indices], device=device), reduction="none").cpu().tolist())) # pyright: ignore[reportUnknownMemberType] — torch typing boundary
|
| 875 |
+
return math.exp(min(80., sum(losses) / len(losses)))
|
| 876 |
+
|
| 877 |
+
|
| 878 |
+
def calibrate_perplexity(repairs: list[dict[str, object]]) -> dict[str, object]:
|
| 879 |
+
scored = sorted((number(r["perplexity"]), bool(r["exact"])) for r in repairs if r.get("perplexity") is not None)
|
| 880 |
+
threshold = 0.0
|
| 881 |
+
accepted = 0
|
| 882 |
+
# Strict empirical policy: accept no observed incorrect validation repair.
|
| 883 |
+
# Tied scores are considered together; tiny samples are explicitly reported.
|
| 884 |
+
for score in sorted({score for score, _ in scored}):
|
| 885 |
+
group = [(s, ok) for s, ok in scored if s <= score]
|
| 886 |
+
if not all(ok for _, ok in group):
|
| 887 |
+
break
|
| 888 |
+
threshold, accepted = score, len(group)
|
| 889 |
+
return {"threshold": threshold, "scored": len(scored), "accepted": accepted,
|
| 890 |
+
"observed_errors_at_threshold": 0, "policy": "largest validation prefix with zero observed exact-match errors; not a statistical guarantee"}
|
| 891 |
+
|
| 892 |
+
|
| 893 |
@torch.no_grad()
|
| 894 |
def predict_edits(model: TabFix, consumer: Consumer, p: Prepared, device: torch.device,
|
| 895 |
enabled: set[str], threshold: float = .9) -> list[Edit]:
|
|
|
|
| 900 |
probs = model.bio.forward(h).reshape(-1, K, 3).float().softmax(-1)
|
| 901 |
insert = model.insert.forward(h).float().sigmoid()
|
| 902 |
proposed: list[Edit] = []
|
| 903 |
+
blocked = invalid_cells(p.record.xml)
|
| 904 |
+
columns = ET.fromstring(p.record.xml).findall("./schema/column")
|
| 905 |
for c in p.cells:
|
| 906 |
+
if (c.row, c.column) in blocked:
|
| 907 |
+
continue
|
| 908 |
+
if c.column < len(columns):
|
| 909 |
+
rules = mapping(xml_json(columns[c.column], "constraints", {}))
|
| 910 |
+
if rules.get("enum") or rules.get("lexicon"):
|
| 911 |
+
continue
|
| 912 |
tok = tokens_for(c, p.offsets)
|
| 913 |
for category, name in enumerate(CATEGORIES):
|
| 914 |
if name not in enabled:
|
|
|
|
| 978 |
partial: list[float] = []
|
| 979 |
category_counts: Counter[str] = Counter()
|
| 980 |
confusion = [0, 0, 0]
|
| 981 |
+
residual = [r for r in records if r.metadata.get("neural_view")]
|
| 982 |
+
general = [r for r in records if not r.metadata.get("neural_view")]
|
| 983 |
+
monitored = (monitoring_sample(residual, max(1, cfg.validation_examples // 2), cfg.seed)
|
| 984 |
+
+ monitoring_sample(general, max(1, cfg.validation_examples // 2), cfg.seed)) if residual and general else monitoring_sample(records, cfg.validation_examples, cfg.seed)
|
| 985 |
+
for r in monitored:
|
| 986 |
p = consumer.prepare(r)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
with autocast(device):
|
| 988 |
h = model.hidden(p.ids, device)
|
| 989 |
det.append(float(detection_from_hidden(model, p, h)))
|
| 990 |
bio = model.bio.forward(h[p.positions]).reshape(-1, K, 3).float().softmax(-1)[:, :, 1:].sum(-1)
|
| 991 |
gap = model.insert.forward(h[p.gaps]).float().sigmoid()
|
| 992 |
for probs, raw_labels in ((bio, p.labels), (gap, p.gap_labels)):
|
| 993 |
+
labels = torch.tensor(raw_labels, device=device).reshape(-1, K)
|
| 994 |
valid, actual, predicted = labels != -100, labels > 0, probs >= .9
|
| 995 |
confusion[0] += int((predicted & actual & valid).sum())
|
| 996 |
confusion[1] += int((predicted & ~actual & valid).sum())
|
| 997 |
confusion[2] += int((~predicted & actual & valid).sum())
|
| 998 |
+
for e in eligible(r)[:2]:
|
| 999 |
category_counts[e.category] += 1
|
| 1000 |
for all_masked, output in ((True, full), (False, partial)):
|
| 1001 |
view = consumer.correction(p, e, random.Random(r.id + str(e)), True, mask_all=all_masked)
|
|
|
|
| 1010 |
"selection_score": d + cfg.correction_weight * c,
|
| 1011 |
"examples": len(det), "correction_edits": len(full),
|
| 1012 |
"correction_category_counts": dict(category_counts),
|
|
|
|
|
|
|
| 1013 |
"token_and_gap_tp_fp_fn_at_0.9": confusion,
|
| 1014 |
+
"sample_ids": [r.id for r in monitored]}
|
| 1015 |
|
| 1016 |
|
| 1017 |
@torch.no_grad()
|
|
|
|
| 1028 |
losses.append(float(detection_loss(model, p, device)))
|
| 1029 |
h = model.hidden(p.ids, device)
|
| 1030 |
prob = model.bio.forward(h[p.positions]).reshape(-1, K, 3).float().softmax(-1)[:, :, 1:].sum(-1)
|
| 1031 |
+
labels = torch.tensor(p.labels, device=device).reshape(-1, K)
|
| 1032 |
valid = labels != -100
|
| 1033 |
actual = labels > 0
|
| 1034 |
for threshold, v in counts.items():
|
|
|
|
| 1047 |
e = eligible_edits[0]
|
| 1048 |
_, _, _, _, target = consumer.correction(p, e, random.Random(0), False)
|
| 1049 |
answer = generate_repair(model, consumer, p, e, device)
|
| 1050 |
+
score = conditional_perplexity(model, consumer, p, e, answer, device) if answer is not None else math.inf
|
| 1051 |
+
repairs.append({"id": r.id, "category": e.category, "target": target, "prediction": answer, "exact": answer == target, "perplexity": score if math.isfinite(score) else None})
|
| 1052 |
except ValueError as exc:
|
| 1053 |
consumer.stats[str(exc)] += 1
|
| 1054 |
return {"sample_policy": "seeded language x positive/clean round-robin validation sample",
|
| 1055 |
"validation_examples": len(losses), "detection_loss": sum(losses) / max(1, len(losses)),
|
| 1056 |
"correction_examples": len(correction_losses), "correction_loss": sum(correction_losses) / max(1, len(correction_losses)),
|
| 1057 |
"token_category_confusion_tp_fp_fn_tn": counts,
|
| 1058 |
+
"gold_cell_repairs": repairs, "perplexity_calibration": calibrate_perplexity(repairs), "prediction_sample": predictions,
|
| 1059 |
"limitations": "Token diagnostics, not cell/span calibration; limited gold-span repair sample; test untouched."}
|
| 1060 |
|
| 1061 |
|
|
|
|
| 1074 |
emit("verified", step=manifest["step"], parameters=sum(p.numel() for p in model.parameters()))
|
| 1075 |
return
|
| 1076 |
old = read_json(resume_path / "run_config.json")
|
|
|
|
|
|
|
|
|
|
| 1077 |
for name in ("seed", "lr", "max_length", "correction_weight", "batch_size", "tiny", "warmup_steps", "schedule_steps", "validation_examples", "patience", "min_delta"):
|
| 1078 |
if old.get(name) != asdict(cfg)[name]:
|
| 1079 |
raise ValueError(f"Resume config mismatch: {name}")
|
|
|
|
| 1095 |
torch.set_rng_state(cast(Tensor, state["torch_rng"]))
|
| 1096 |
if device.type == "cuda":
|
| 1097 |
torch.cuda.set_rng_state_all(cast(list[Tensor], state["cuda_rng"]))
|
| 1098 |
+
train = read_records("train", cfg)
|
| 1099 |
validation = read_records("validation", cfg)
|
| 1100 |
weights = [number(r.metadata["sampling_weight"]) for r in train]
|
| 1101 |
repair_indices = [i for i, r in enumerate(train) if eligible(r)]
|
| 1102 |
+
repair_only_indices = [i for i in repair_indices if any(e.category != "copy" for e in eligible(train[i]))]
|
| 1103 |
+
copy_indices = [i for i in repair_indices if any(e.category == "copy" for e in eligible(train[i]))]
|
| 1104 |
+
neural_indices = [i for i, r in enumerate(train) if r.metadata.get("neural_view")]
|
| 1105 |
+
if not neural_indices:
|
| 1106 |
+
neural_indices = list(range(len(train))) # Tiny local smoke fixtures.
|
| 1107 |
+
neural_weights = [weights[i] for i in neural_indices]
|
| 1108 |
if cfg.batch_size < 1:
|
| 1109 |
raise ValueError("Batch size must be positive")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1110 |
api = HfApi() if cfg.repo_id else None
|
| 1111 |
if api:
|
| 1112 |
# A fresh objective intentionally replaces the existing preview repo;
|
| 1113 |
# the upload commits remain atomic and the local checkpoint is recoverable.
|
| 1114 |
+
_ = api.repo_info(cfg.repo_id)
|
| 1115 |
@lru_cache(maxsize=64)
|
| 1116 |
def prepared(i: int) -> Prepared:
|
| 1117 |
return consumer.prepare(train[i])
|
|
|
|
| 1121 |
validation_history.extend(entry for item in sequence(prior_report.get("validation_history", [])) if integer((entry := mapping(item))["step"]) <= step)
|
| 1122 |
report: dict[str, object] = {"tiny_smoke": cfg.tiny, "dataset_revision": REVISION,
|
| 1123 |
"train_records": len(train), "correction_eligible_records": len(repair_indices),
|
| 1124 |
+
"correction_request_version": 4, "batch_size": cfg.batch_size,
|
| 1125 |
"start_step": step, "device": str(device), "parameters": sum(p.numel() for p in model.parameters()),
|
| 1126 |
"validation_history": validation_history}
|
| 1127 |
emit("ready", **report)
|
|
|
|
| 1136 |
selection = Selection(number(state_selection["best_score"]), integer(state_selection["best_step"]), integer(state_selection["bad_checks"]))
|
| 1137 |
if not best_path.exists():
|
| 1138 |
if cfg.repo_id:
|
| 1139 |
+
source_best = Path(snapshot_download(cfg.repo_id, revision=f"v2-best-step-{selection.best_step}"))
|
| 1140 |
elif selection.best_step == step:
|
| 1141 |
source_best = resume_path
|
| 1142 |
else:
|
|
|
|
| 1159 |
shutil.copytree(current, best_path, dirs_exist_ok=True)
|
| 1160 |
if api:
|
| 1161 |
uploaded = read_json(Path(cfg.output) / "last_upload.json")
|
| 1162 |
+
api.create_tag(repo_id=cfg.repo_id, tag=f"v2-best-step-{step}", revision=string(uploaded["commit"]), exist_ok=True)
|
| 1163 |
last_validation_at = saved_at = time.monotonic()
|
| 1164 |
|
| 1165 |
if not resume_path:
|
|
|
|
| 1172 |
optimizer.zero_grad(set_to_none=True)
|
| 1173 |
for group in optimizer.param_groups:
|
| 1174 |
group["lr"] = learning_rate(cfg, step)
|
| 1175 |
+
batch = [prepared(rng.choices(neural_indices, weights=neural_weights, k=1)[0]) for _ in range(cfg.batch_size)]
|
| 1176 |
with autocast(device):
|
| 1177 |
hidden = model.hidden_batch([p.ids for p in batch], device)
|
| 1178 |
det = torch.stack([detection_from_hidden(model, p, hidden[i]) for i, p in enumerate(batch)]).mean()
|
|
|
|
| 1181 |
det.backward() # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 1182 |
views: list[tuple[list[int], list[int], list[int], list[bool], str]] = []
|
| 1183 |
for _ in range(cfg.batch_size * 20):
|
| 1184 |
+
copy_task = bool(copy_indices) and (not repair_only_indices or rng.random() < .3)
|
| 1185 |
+
pool = copy_indices if copy_task else repair_only_indices
|
| 1186 |
+
rp = prepared(rng.choice(pool))
|
| 1187 |
+
choices = [e for e in eligible(rp.record) if (e.category == "copy") == copy_task]
|
| 1188 |
try:
|
| 1189 |
+
views.append(consumer.correction(rp, rng.choice(choices), rng, True))
|
| 1190 |
except ValueError as exc:
|
| 1191 |
consumer.stats[str(exc)] += 1
|
| 1192 |
if len(views) == cfg.batch_size:
|
|
|
|
| 1201 |
raise FloatingPointError("Nonfinite correction loss")
|
| 1202 |
cor.backward() # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 1203 |
cor_value = float(unweighted_cor.detach())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
_ = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0, error_if_nonfinite=True)
|
| 1205 |
optimizer.step() # pyright: ignore[reportUnknownMemberType] — third-party typing boundary
|
| 1206 |
step += 1
|
|
|
|
| 1233 |
check_validation()
|
| 1234 |
if api:
|
| 1235 |
last_upload = read_json(Path(cfg.output) / "last_upload.json")
|
| 1236 |
+
api.create_tag(repo_id=cfg.repo_id, tag=f"v2-latest-step-{step}", revision=string(last_upload["commit"]), exist_ok=True)
|
| 1237 |
report["last_training_step"] = step
|
| 1238 |
report["selected_step"] = selection.best_step
|
| 1239 |
report["selection_at_stop"] = asdict(selection)
|
|
|
|
| 1275 |
save_seconds=integer(args["save_seconds"]), lr=number(args["lr"]), seed=integer(args["seed"]),
|
| 1276 |
validation_examples=integer(args["validation_examples"]), validation_repairs=integer(args["validation_repairs"]), validation_seconds=integer(args["validation_seconds"]),
|
| 1277 |
validation_steps=integer(args["validation_steps"]), patience=integer(args["patience"]), min_delta=number(args["min_delta"]), warmup_steps=integer(args["warmup_steps"]), schedule_steps=integer(args["schedule_steps"]),
|
| 1278 |
+
max_length=integer(args["max_length"]), correction_weight=number(args["correction_weight"]), batch_size=integer(args["batch_size"]), verify_only=bool(args["verify_only"]))
|
| 1279 |
|
| 1280 |
|
| 1281 |
def handle_signal(_signum: int, _frame: object) -> None:
|
training_state.pt
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cd9e1448a8ac294f6f6d25dc2d5a1fc00faf3ff28d70475cf1942077b0ca5aae
|
| 3 |
+
size 11293
|