C5 — Signature matching: contamination filter + cross-domain model + calibration fix
Deployed: ahu-signature-verify v4 (sigverify-v4-ens-dinov2-signetc5) · verified live on submission e4229ea8 (Darsani). Efran: "spesimen matching is very important."
The problem C5 had to fix
Genuine Darsani's document signature scored ~4% against his real specimen under v3 — a genuine match reading as a non-match. Investigation found three distinct causes, and C5 addresses all three:
- Registry contamination — many "Darsani" registry entries actually hold a different official's signature (the successor-attestation FORMULIR-02 files; flagged to AHU separately). Training on those creates false "same-person" pairs.
- Domain gap — v3 was fine-tuned on clean specimen-vs-specimen pairs, but production compares a stamped, scanned document signature against a clean specimen. v3 never learned that shift.
- Calibration bug — v3's score→% mapping was anchored on the cross-sheet distribution (easier), so it systematically under-scored the harder cross-domain task that production actually runs.
What C5 did
1. Self-consistency contamination filter (also quantifies the AHU issue)
For each officer with ≥2 registry files, embed all crops and keep only the largest mutually-consistent cluster (files whose signatures agree with each other); drop outliers. No OCR needed. Result across 2,149 multi-file officers:
- 758 (35%) had inconsistent files — hard evidence the wrong-person contamination is systemic, not a one-off.
- Clean positive set: 1,391 officers / 2,858 crops (metric learning only needs within-group same-person, so even a consistently-mislabeled cluster is a valid positive).
2. Synthetic document-domain augmentation
We can't get thousands of real document signatures, so we synthesize the document domain from clean specimens: a lighter-than-ink circular official stamp overlay, security-paper texture, ink/contrast jitter, geometric skew, and JPEG/scan softness — then re-isolate (exactly what production does to real documents). Injected into ~half of each training batch so the batch-hard triplets become clean↔document positives, teaching the invariance that matters.
3. Calibration anchored on the right distribution
v4's score→% knots are computed from the held-out cross-domain (document↔specimen) impostor percentiles, not cross-sheet. This alone lifts genuine matches from "looks like 4%" to a meaningful score, because it finally measures the task production actually performs.
Results (held-out frozen corpus — 95 officers, never trained on)
| Metric | v3 (prod) | v4 (c5) |
|---|---|---|
| Cross-sheet AUC | 0.754 | 0.760 |
| Cross-domain AUC | 0.727 | 0.743 |
| Cross-domain genuine-detect @ imp-p95 | 35.8% | 46.3% (+10.5 pts) |
| Self-pair sanity | 100% | 100% |
The headline is the operating-point metric: at a fixed ~5% false-accept rate, v4 clears 46% of genuine cross-domain matches over the "cukup mirip" bar vs v3's 36%. Ensemble weighting was tested (50/50 vs dino-heavy vs dino-only); 50/50 won on the population.
Real Darsani anchor (his actual Kutipan document vs the genuine 2655 specimen, on the live submission): ~4% → 45.8%. A genuine match now sits right at the "cukup mirip, periksa manual" threshold instead of reading as a non-match.
Honest limits
- v4 improves the population modestly (+10.5pp detect) and fixes the calibration, but the synthetic augmentation can't fully close the pathological real gap — Darsani's genuine specimen (2655) is a 2008 rotated, faint scan, near the limit of what's recoverable. It lands at ~46%, not 80%+.
- The real lever remains real document-vs-specimen training pairs, which are now being accumulated as verifikators approve submissions under the mandatory signature-attestation checkbox. That data — real signings, real stamps, real scan conditions — is what will push the hard cases past 80%. C5's harness (
signature-verify-service/sigtrain/c5_*.py) is ready to retrain on it. - The AHU contamination fix (flagged separately) is the other half: while the registry serves successor signatures under retired officers' names, the right specimen (2655) must be manually picked. The candidate-ranking + "Spesimen ✓" chips already help the verifikator do that.
Artifacts
- Model:
models-local/signet-ft.pthon the GPU host (c5last.pth), backupvalbox:~/signet-c5-20260714.pth. Baked into the image via the existing Dockerfile hook. - Calibration: knots
0.409/0.561/0.625/0.741 ↔ 0/50/80/100%(cross-domain-anchored). - Reproducible harness:
sigtrain/c5_augment.py(doc augmentation),c5_clean.py(consistency filter),c5_train.py(cross-domain triplet),c5_eval.py(held-out + weighting sweep).