invariants + Bench 2 final arm: the convenient artifact is the misleading one (#4523) - #4651
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Member
Author
|
Scope grew: this branch now also carries the final Bench 2 arm, which is the first engine comparison reported honestly (coverage + conditional + unconditional accuracy, one estimator across arms). At matched coverage, on the 100-page Wikisource tier:
Three things worth noting:
Docs + eval only; no production code paths. |
…nch 2 arm (#4523) Three instrument failures in one afternoon, same shape every time: the artifact you reach for FIRST was wrong, because it was complete/always-present/already open - exactly the properties a stale or partial record has. - A failures-only log reads as a complete log. worker-0.jsonl records only failures/skips; reading it produced '2,016 failures, zero successes, job is broken', published as an operational alert. The job was healthy - output dir grew 7,430 -> 7,451 files in 15 min. Retracted. Verify a worker by its OUTPUT GROWING, never by its error log being non-empty. - An err field holding a stderr TAIL reports the last warning, not the cause: every failure showed a benign kenlm notice, hiding the real ones. - pages.image_width is on 100% of rows, image_metadata.width on 27.3%, and where both exist they disagree 4,000/4,000 - image_width is the DISPLAY derivative (1200x800 recorded against a real 3888x2592 master). A corpus figure built on it was wrong by ~21k pages. Also carries the final Bench 2 arm - the first comparison reported honestly (coverage + conditional + unconditional, one estimator). At matched coverage: Greek n=25 kraken 99.3/91.4 @92% vs gemini 97.8/89.7 @92%; Latin n=45 tie; German n=30 kraken 94.7 cond but 44.2 UNCOND @47% - a loss only the new reporting reveals. Guard shipped with it (10 unit tests on the wikitext cleaner) was negative- controlled: reverting the fix turns 3 of them red. Signed-off-by: JDerekLomas <j.d.lomas@tudelft.nl> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JDerekLomas
force-pushed
the
fix/measurement-instruments-lesson
branch
from
September 4, 2026 22:10
c3375cd to
c578edc
Compare
JDerekLomas
added a commit
that referenced
this pull request
Sep 12, 2026
…eleting (#4523) (#4660) * eval: measure the ground truth itself, and fix the 6% of it we were deleting (#4523) Track B item 1 of the OCR measurement program: nothing downstream is quotable until we know how wrong the reference is. Two questions, one answer each. HOW WRONG IS THE REFERENCE? Wikisource pages carry their own second opinion: level 3 means one human transcribed the page, level 4 means a second, different human re-read it against the scan. reference-error-rate.mjs replays each page's revision history and measures what that second reader changed, through the same normalizeForScript folding the bench scores through — so the number lands on the bench's own scale. No hand transcription, and deliberately no VLM re-transcription, which would referee a bench about VLMs with the system under test. n=69 independent validations, median 0.00%: Greek 0.07% CI [0.02, 0.13] (engine gap 1.5pp — stands, 20x the noise) German 0.06% CI [0.02, 0.11] Latin 1.15% CI [0.17, 2.47] (engine gap 1.4pp — INSIDE the noise) So "Latin is a tie" is not a shrug, it is arithmetic. Latin's distribution is skewed rather than uniformly bad: most references are exact, a handful omit a whole printed block (an apparatus criticus, a clause). THE BIGGER ERROR WAS OURS. cleanPageText blanked a template together with the text it wrapped, and {{SperrSchrift|D'Glocke het zwölfi gschlage.}} is a printed line, not scaffolding. Measured 6.0% of Greek reference letters, 3.8% of Latin, 0.9% of German — 5-80x the human error rate — with nested apparatus markup surviving as literal braces on top of that. Re-scoring the stored Gemini outputs against corrected references (no re-runs, no cost): Greek conditional accuracy 97.8% -> 99.6% at coverage 92% -> 88%, driven by one page that went 59.7% -> 100.0% because a perfect transcription had been charged 40 points for our own markup. Coverage falls because a table-of-contents page the corrupt reference waved through now fails the guard honestly. That 1.8pp Greek correction is LARGER than the 1.5pp Greek engine gap it was used to judge, so every Bench 2 Greek arm must be rescored before the number is quoted again. The Kraken ws outputs live in PR #4651 and were not available here. Cleaner rules, each from a page that broke: keep every positional argument, not the longest (a table-of-contents line is nothing but those); emit a sort-key/display pair once; take the fragment THIS page prints from {{hws}}; drop named parameters as configuration but keep numbered ones as content; treat <ref> and {{CRef}} identically, because a validator moving a footnote between the two read as a 13.6% human correction and was the largest delta in the first run. Footnote text is excluded deliberately: it is printed on 9 of 10 footnoted pages (88.6% recall in the Gemini arm against a 97.9% body control), but the tenth is a modern editorial gloss with no syntactic tell, and a reference asserting unprinted text is charged to the engine. - reference-error-rate.mjs — instruments A (level-3 error), B (level-4 residual), C (what our cleaner deleted); bootstrap CIs, change hunks so the number can be read rather than just quoted - refresh-ws-references.mjs — re-clean pinned references in place from live wikitext, so a cleaner fix does not move the bench underneath stored results - 8 new unit tests, one per rule above - 67 of 120 pinned references corrected; ground-truth-wikisource-l4/ adds the matched level-4 samples that gave Greek and Latin a real n Signed-off-by: JDerekLomas <j.d.lomas@tudelft.nl> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014WmTDpvthUQFPnPnVLPC56 * eval: detect fabrication without a reference, and find where it cannot (#4523) (#4665) Track C. Every metric we own compares OCR to a reference, so a model that has memorised the published text scores WELL while never reading the page — Bench 1 caught a folio where Gemini hit 0.790 against the Derge canon while agreeing 0.33 with both specialists' reads of that same image. Production has no reference at all. A CTC line recogniser carries no language model over the target text, so it cannot recite. Two independently-trained ones converging is evidence about the INK rather than about any edition. The detector scores specialist convergence, VLM-to-ink agreement and unit overrun per page, and abstains when the specialists do not converge. VALIDATED on 349 pages across two sets with known answers. Positive: Bench 1 Derge Kangyur, 313 folios, two BDRC recognisers plus production-era Gemini, of which 51 carry an externally-established label (Gemini below 0.2 against the canon where both specialists exceed 0.8 — no reading of the image produces that). Negative: the Bench 2 print arms already in this repo, Kraken + Surya + Gemini on the same 36 pages. 51/51 positives flagged, 0/5 false positives, and the plateau is wide: 100% precision AND 100% recall for every gap threshold from 0.10 to 0.40. The default 0.35 sits mid-plateau, not on a cliff — which is the answer to "the guard threshold was picked by hand". THE BLIND SPOT IS THE FINDING. Recitation and specialist failure share a cause: a hard image is what makes a CTC engine fail and what pushes a VLM onto its memory. So "abstain when the ink is unestablished" silently excuses exactly the pages that matter — the seven most flagrant Bench 1 cases, Gemini at 0.75-0.98 against the canon while BOTH specialists scored 0.00-0.16 against it and it emitted up to 13.5x the units on the page, all sat in INCONCLUSIVE. A canon-anchored rule recovers 9 of them under a separate RECITING? verdict, kept apart from the verified ones because "the specialists are broken on this page" is a live alternative. Two things that cost a page each and are now structural: - ORDER IS NOT FAILURE. On the two-column Bekker Categories page Kraken reads across the gutter while Surya reads down the column: agreement 0.01 between two engines that both transcribe it well. Gating on the sequence number sent 8 of 11 print pages to INCONCLUSIVE for a layout reason. Every quantity is now the better of its ordered and order-free (multiset Dice over 3-unit shingles) form. - OVERRUN IS THE FAMOUS TELL AND NOT THE USEFUL ONE. Median overrun across the 203 flagged Tibetan pages is 0.93x; only 38/203 exceed 1.6x. The agreement gap carries the signal. Coverage, not accuracy, is the constraint: 56 of 349 pages are both labelled and judgeable, and on print 6 of the 11 pages carrying a VLM arm are gated out because Kraken and Surya genuinely disagree (Copernicus 1543, the ~1490 Malleus, Weigel 1618, Zesen 1645, the Poemander apparatus, Bekker). Two specialists that fail together buy nothing. Stated weakness: positive and negative classes differ in script, medium AND engine set, so perfect separation is evidence the statistic orders known-bad above known-good — not that it discriminates within early modern print. That needs a print corpus with known fabrication, which does not exist yet. Claude-Session: https://claude.ai/code/session_014WmTDpvthUQFPnPnVLPC56 Signed-off-by: JDerekLomas <j.d.lomas@tudelft.nl> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Signed-off-by: JDerekLomas <j.d.lomas@tudelft.nl> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…; kept both) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KLkXXbUjg6KD9PRVFQN8b3 Signed-off-by: JDerekLomas <j.d.lomas@tudelft.nl>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three instrument failures in one afternoon, all the same shape: the artifact a reasonable person reaches for first was the wrong one — because it was complete, always-present, or already open, which are exactly the properties a stale or partial record has.
worker-0.jsonlrecords only failures and skips. Reading it produced "2,016 failures, zero successes — the job is broken", which I published as an operational alert on OCR fabricates Sanskrit Hindu scripture on Tibetan manuscript folios — 529 books, 355 of them badged first translations #4523. The job was healthy: its output directory grew 7,430 → 7,451 files in 15 minutes with valid Tibetan in them. Retracted. Verify a worker by its output growing, not by its error log being non-empty.errfield capturing a stderr tail reported the same benign trailing warning (kenlm bindings are not installed, printed on every invocation including successful ones) for every failure, hiding the real causes.pages.image_widthis on 100% of rows;image_metadata.widthon 27.3% — and where both exist they disagree 4,000/4,000 sampled (image_widthis the display derivative; one page reads 1200×800 against a real 3888×2592 master). A corpus-wide "42% of pages are too low-res to re-OCR" was built on the populated field and was wrong.Credit to the peer session that caught (1) and (2) and then self-corrected on (3); I verified all three against the database before writing this.
Docs only — no code paths touched.