fix(ml): detect PII past the model window and stop inflating confidence - #58
Open
mazzasaverio wants to merge 2 commits into
Open
fix(ml): detect PII past the model window and stop inflating confidence#58mazzasaverio wants to merge 2 commits into
mazzasaverio wants to merge 2 commits into
Conversation
This was referenced Aug 29, 2026
Three release-gate defects, all reproducible on main. The ONNX backend interpolated the originating exception message into BackendExecutionError. The release gate requires that exceptions do not expose matched values, and PR ma2za#40 established the same invariant for adapters; tokenizer and runtime messages can quote the input they failed on. The message is now fixed text and the cause is dropped, matching how every other adapter and backend in the tree raises. Detection also scanned every character with unicodedata.category on every block, building two N-sized lists even when nothing was stripped. No ASCII character has category Cf, so pure ASCII text now takes a single C-level scan and keeps the source offsets untouched, and non-ASCII text only pays for the rebuild when a format character is actually present. This is 13% of end-to-end processing time on a 64 KiB block. Finally, `uv run ruff format --check .` fails on main: the 0.16.0 OCR commit landed unformatted, so CI has been red across 0.16.0 and 0.17.0 even though both shipped. The two files are reformatted here so the gate passes again. uv.lock still recorded 0.16.0 and is refreshed to 0.17.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JiX3zWeEC28kmy5KmAXvf
Two defects in LocalONNXPIIBackend, both of which make the backend report something other than what it found. Personal data past the model's position budget was silently ignored. The shipped tokenizer.json configures truncation at 512 tokens, so `tokenizer.encode` quietly dropped every token after it: on the cached DistilBERT, "Contact Maria Rossi in Milan." was found on its own and after 100 words of context, and returned nothing once roughly 2 KB of text preceded it. No error, no warning, and the caller gets a document that looks sanitized. That is one page of a PDF or one long email. Truncation is now turned off and the block is split into overlapping token windows sized from the tokenizer's own limit (falling back to the config's max_position_embeddings, then 512). Detections are shifted back to absolute offsets and identical spans found in two overlapping windows collapse to the higher-confidence copy. The same text is now detected at 36 KB. The reported confidence was also manufactured. The runner-up label beat "O" whenever it reached `minimum_confidence * 0.01`, and every surviving span was then rescaled from that point onto the policy floor. The consequences: the policy knob could not filter anything, since clearing the boost guaranteed clearing the floor; and it was inverted, because a stricter floor produced a lower boost threshold and therefore more detections. A prediction the model gave 1.6% probability was reported at 0.99 confidence under a 0.99 floor. Confidence is now the model's own probability, so raising minimum_confidence can only ever remove detections. Recall is tuned by a separate, explicit `entity_threshold` (default 0.5): the absolute probability a runner-up label must reach to beat "O". `window_overlap_tokens` is exposed alongside it. Note for review: honest probabilities cost recall at the default 0.8 floor, which is calibrated for deterministic detectors emitting 0.9-1.0 rather than for a probabilistic model. The ai4privacy benchmark number will fall. That is the previously reported number being corrected rather than a regression, and whether the ML default floor should differ from the rules floor is a separate decision worth taking on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JiX3zWeEC28kmy5KmAXvf
mazzasaverio
force-pushed
the
fix/ml-window-and-confidence
branch
from
August 30, 2026 05:34
6e9c788 to
4328880
Compare
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.
Two defects in
LocalONNXPIIBackend. Both make the backend report something other than what it actually found, and the first one silently leaves personal data in the output.Personal data past the model window was silently dropped
The shipped
tokenizer.jsonconfigures truncation at 512 tokens:so
tokenizer.encodequietly discarded every token past it. Measured on the cached DistilBERT with"Contact Maria Rossi in Milan."at the end of a growing block:Maria RossiPERSON,MilanLOCATIONMaria Rossi,MilanRossionlyNo exception, no warning, no statistic. The caller receives a document that looks sanitized. Roughly two kilobytes is one page of a PDF, or one long email.
Truncation is now turned off and the block is split into overlapping token windows sized from the tokenizer's own limit, falling back to the config's
max_position_embeddingsand then to 512. Detections are shifted back to absolute offsets, and a span found in two overlapping windows collapses to its higher-confidence copy. The same entity is now found at 36 KB:Maria Rossi0.998,Milan0.959Maria Rossi0.997,Milan0.971Maria Rossi0.994,Milan0.952Maria Rossi0.995,Milan0.913The reported confidence was manufactured
A runner-up label beat
Oat one hundredth of the policy floor, and every surviving span was then rescaled from that point onto the floor. Two consequences:minimum_confidencecould not filter anything. Clearing the boost guaranteed clearing the floor, by construction.The same weak prediction, on
main:The model's actual probability for that span is 0.016.
Confidence is now the model's own probability, so raising
minimum_confidencecan only ever remove detections. Recall is tuned by a separate explicitentity_threshold(default 0.5): the absolute probability a runner-up label must reach to beatO.window_overlap_tokensis exposed alongside it, and both are validated.For your call
Honest probabilities cost recall at the default 0.8 floor, which is calibrated for deterministic detectors emitting 0.9–1.0 rather than for a probabilistic model. One existing test needed its policy lowered to 0.5 for the second of two
John Smithmentions (true probability 0.717) to be detected.The ai4privacy F1 will fall. That is the previously reported number being corrected rather than a regression: 0.10.0's "calibration" gain came from this rescaling. Whether the ML backend deserves a different default floor from the rules backend is a real question, and worth deciding on its own rather than inside this fix.
Verification
Six new tests: windowing at three block sizes, window coverage and overlap, confidence monotonicity under two floors, threshold-controlled recall, and argument validation.
🤖 Generated with Claude Code
https://claude.ai/code/session_012JiX3zWeEC28kmy5KmAXvf