Antix5 commited on
Commit
f248ad2
·
verified ·
1 Parent(s): ca8d570

Joint training checkpoint step 0

Browse files
Files changed (8) hide show
  1. README.md +94 -33
  2. categories.json +1 -17
  3. checkpoint.json +9 -9
  4. metrics.json +0 -0
  5. model.safetensors +2 -2
  6. run_config.json +10 -12
  7. train_tabfix.py +338 -216
  8. 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, 18 category BIO outputs, insertion outputs, and the pretrained MLM vocabulary head. Correction is trained in place: the selected corrupted span is replaced by a variable length mask buffer inside its original cell, with no appended JSON request. The MLM head emits replacement tokens followed by `END_EDIT` and visible padding tokens.
13
- Completed optimizer updates: **800**. This checkpoint is a research baseline, not a validated automatic cleaning system.
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: `7da5aa80bc2a68a375930f3bd3ed2f7fdda0152f`. Train/validation formats are held out; test data was not used during training or checkpoint selection. A frozen test diagnostic is reported below. This dataset is mostly synthetic. The detector/correction task weight is `ln(18)/ln(256000) = 0.2321`; the optional conditional-missing pair weight is recorded in `run_config.json` and `metrics.json`. 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. No probability calibration or latency guarantee is claimed. See `metrics.json` for sample sizes, loss, operating-point diagnostics and exact-repair outcomes.
26
 
27
- ## Licensing
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
- Training started from pretrained mmBERT. The selected checkpoint is update **800**, restored after training reached update 1523 and its time budget. Model and optimizer/RNG hashes match the saved best-step-800 checkpoint. Selection used a fixed 252-example validation panel with 393 eligible correction edits.
38
 
39
- | Fixed validation panel | Initial | Selected (800) | Last evaluated (1523) |
40
- |---|---:|---:|---:|
41
- | Detection loss | 1.69733 | 0.20521 | 0.33725 |
42
- | Correction loss | 9.67505 | 0.45218 | 0.44362 |
43
- | Weighted selection score | 3.94294 | 0.31017 | 0.44021 |
44
 
45
- ![Validation curves](evaluation/validation_curves.png)
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
- This pipeline is **not ready for automatic correction**. On this diagnostic it harmed more valid cells than it repaired faulty cells. Date formatting accounts for 28 harmed cells, required missing values 12, spelling 10, temporal consistency 4 and unit consistency 1. Relationship errors were missed in the small positive sample. Missing-value generation can invent content even when a replacement is not inferable.
59
 
60
- The sample is deliberately stratified and small; these are diagnostic counts, not production performance estimates. Category-level denominators are small, and compound cells can count under multiple categories. The current policy refuses overlapping repairs and does not apply structural shifts. Test results have not been used to tune thresholds or weights.
61
 
62
- [Detailed category findings](evaluation/test/FINDINGS.md) · [Raw counts](evaluation/test/summary.json) · [Frozen sample](evaluation/test/selection.json) · [Provenance](evaluation/test/provenance.json). Per-case inputs, outputs and references are retained in `evaluation/test/case_*.json`. Full training and validation measurements remain in [metrics.json](metrics.json).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.invisible",
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": 800,
4
- "dataset_revision": "7da5aa80bc2a68a375930f3bd3ed2f7fdda0152f",
5
  "base_revision": "c5955035435e2bf121cde7f3c8863ef52ff35d82",
6
  "files": {
7
- "model.safetensors": "940f08b9dfd115245a4f2455de8f7304535577a6704f627dcb94a65cd02b6226",
8
  "config.json": "9e105ce4ed25ed2d6be2443eeb69932a530668a248ce3211d43a89a136d42531",
9
  "tokenizer_config.json": "51d83f5e4c1174f8dad62610076998730a57a9ac7025680653c720bca2a9b0a1",
10
  "tokenizer.json": "47e408472c05385fea5fb9f8b02bbd838d55da8f15a22c863a99fd31d89243f5",
11
- "training_state.pt": "6e70e38863b991a90700c7dcdc4db2d774332fdc48dec046d6968ab86d898b02",
12
- "run_config.json": "eec4d8db620354de7883275f2c7282ee3bdd72328c3ff829001e248aeea49962",
13
- "metrics.json": "78d483e3ef5107dee73faaf6c1bbb6d34f6efc7bb67814a9cc527bd8f792a036",
14
- "categories.json": "d95af1bcb70bc78d53297d4e57909776773fff76329098ece751e0734f03b29b",
15
- "train_tabfix.py": "8712d7afc651f2d3c55210f3a062ad6a19a94b6185b60d8b5593cacbdf31f7be",
16
- "README.md": "e7acd7f995d46f0efee4036271ec7d6f2100b2b7280257312d9cca161d9c350a",
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:940f08b9dfd115245a4f2455de8f7304535577a6704f627dcb94a65cd02b6226
3
- size 1231390724
 
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": "tabfix_run",
4
  "local_data": "",
5
  "tokenizer": "",
6
  "resume": "",
7
  "resume_revision": "main",
8
  "tiny": false,
9
- "steps": 1600,
10
- "train_seconds": 2100,
11
- "save_seconds": 300,
12
  "lr": 2e-05,
13
  "seed": 20260913,
14
- "validation_examples": 252,
15
  "validation_repairs": 28,
16
  "validation_seconds": 600,
17
- "validation_steps": 200,
18
  "patience": 4,
19
  "min_delta": 0.001,
20
  "warmup_steps": 100,
21
- "schedule_steps": 1600,
22
  "max_length": 8192,
23
- "correction_weight": 0.23210369975344178,
24
- "batch_size": 2,
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 = "7da5aa80bc2a68a375930f3bd3ed2f7fdda0152f"
51
  BASE = "jhu-clsp/mmBERT-base"
52
  BASE_REVISION = "c5955035435e2bf121cde7f3c8863ef52ff35d82"
53
- CATEGORIES = (
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
- for e in record.edits:
 
 
 
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
- hit, lo, hi = interval(c, p.offsets, e)
276
- # Do not apply two differently categorized edits through the same token.
277
- if training and any(other != e and other.row == e.row and other.column == e.column
278
- and other.start < hi and other.end > lo for other in p.record.edits):
279
- raise ValueError("Overlapping token-expanded repairs require explicit group handling")
280
- target = c.value[lo:e.start] + e.replacement + c.value[e.end:hi]
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("Repair exceeds bounded buffer")
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
- skeleton = p.record.xml[:char_start] + CONTROL[0] + string(self.tokenizer.mask_token) * (budget + 1) + p.record.xml[char_end:]
 
 
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(18) / math.log(256000)
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, 18 category BIO outputs, insertion outputs, and the pretrained MLM vocabulary head. Correction is trained in place: the selected corrupted span is replaced by a variable length mask buffer inside its original cell, with no appended JSON request. The MLM head emits replacement tokens followed by `END_EDIT` and visible padding tokens.
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(18)/ln(256000) = 0.2321`; the optional conditional-missing pair weight is recorded in `run_config.json` and `metrics.json`. 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. No probability calibration or latency guarantee is claimed. See `metrics.json` for sample sizes, loss, operating-point diagnostics and exact-repair outcomes.
 
 
 
 
 
 
 
 
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, 64):
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
- for _ in range(limit):
 
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 != end:
736
  logits[:, special] = -torch.inf
737
  if finish is not None:
738
- logits[:, end] = -torch.inf
739
  probs = logits.softmax(-1)
740
- confidence, prediction = probs.max(-1)
741
- # The reserved final slot may predict END_EDIT, never ordinary text.
742
- # Retain its original probability instead of renormalizing it to 1.
743
  for i, j in enumerate(active):
 
 
744
  if j == limit - 1:
745
- confidence[i] = probs[i, end]
746
- prediction[i] = end
 
 
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
- if finish is not None and all(ids[positions[j]] != mask for j in range(finish)):
 
 
 
 
 
 
 
 
 
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
- pair_losses: list[float] = []
847
- for r in monitoring_sample(records, cfg.validation_examples, cfg.seed):
 
 
 
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 monitoring_sample(records, cfg.validation_examples, cfg.seed)]}
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
- repairs.append({"id": r.id, "category": e.category, "target": target, "prediction": answer, "exact": answer == target})
 
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
- "gold_span_repairs": repairs, "prediction_sample": predictions,
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 = diversify_arithmetic(read_records("train", cfg))
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
- repair_weights = [weights[i] for i in repair_indices]
 
 
 
 
 
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.create_repo(cfg.repo_id, private=False, exist_ok=True)
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": 3, "batch_size": cfg.batch_size, "conditional_pair_weight": cfg.conditional_pair_weight, "conditional_pairs": len(pair_pool),
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(range(len(train)), weights=weights, k=1)[0]) for _ in range(cfg.batch_size)]
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
- rp = prepared(rng.choices(repair_indices, weights=repair_weights, k=1)[0])
 
 
 
1057
  try:
1058
- views.append(consumer.correction(rp, rng.choice(eligible(rp.record)), rng, True))
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"]), conditional_pair_weight=number(args["conditional_pair_weight"]), allow_objective_change=bool(args["allow_objective_change"]), verify_only=bool(args["verify_only"]))
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", "&#13;").replace("\n", "&#10;").replace("\t", "&#9;") 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:6e70e38863b991a90700c7dcdc4db2d774332fdc48dec046d6968ab86d898b02
3
- size 2462881846
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cd9e1448a8ac294f6f6d25dc2d5a1fc00faf3ff28d70475cf1942077b0ca5aae
3
+ size 11293