Deckname muss die personenbezogenen Daten in einem Dokument finden, bevor es sie ersetzen kann. In deutschen juristischen Texten ist das schwerer, als es klingt, und der Grund ist eine Grammatikregel, über die die meisten nie nachdenken.
Das ganze Problem in einer Zeile. Das Deutsche schreibt jedes Substantiv groß, also sagt die Großschreibung fast nichts darüber, ob ein Wort ein Name ist. Die Basismodelle verfehlen genau die Fälle, auf die es ankommt, eigene Recognizer beheben das meiste, und ein Eval-Harness mit blockierenden CI-Gates hält das Ergebnis ehrlich.
Das Großschreibungs-Problem
Im Englischen ist ein Großbuchstabe mitten im Satz ein Hinweis. „I met Baker“ ist wahrscheinlich eine Person. Das Deutsche nimmt diesen Hinweis weg. „Bauer“, „Vogel“ und „Koch“ sind gewöhnliche Wörter und zugleich häufige Nachnamen, gleich geschrieben, so oder so großgeschrieben. Großschreibung ist kein Signal. Sie ist Rauschen.
Der Basis-Stack ist Microsoft Presidio mit spaCy (de_core_news_lg). Ohne jede eigene Arbeit erzählte der erste Eval-Lauf die ganze Geschichte. E-Mail und IBAN lagen bei 1.0, weil Presidio dafür solide Patterns mitbringt. Jeder deutschlandspezifische Typ (Aktenzeichen, Steuer-IDs, Sozialversicherungsnummern, Kennzeichen, Adressen) lag bei 0.0, weil die Recognizer noch nicht existierten. Die deutsche Person-Recall startete bei 0.86 und verfehlte genau die vorhergesagten schweren Fälle: Nachnamen, die Substantive sind, und kleingeschriebene Namen in E-Mail-Adressen.
Recognizer, die die Domäne kodieren
Der Fix war kein schlaueres Modell. Es war, zu kodieren, wie deutscher juristischer Text tatsächlich geschrieben wird. Ein paar Recognizer, über die ich mich gefreut habe:
- Eine Steuer-ID mit echter ISO-7064-Prüfziffer. Der Algorithmus traf den offiziellen Beispielwert beim ersten Versuch, was so gut wie nie passiert.
- Eine Sozialversicherungsnummer mit einer Geburtsdatums-Plausibilitätsprüfung, die im Format eingebaut ist.
- Ein kontextsensitives Geburtsdatum. Ein nacktes Datum bleibt unangetastet, sonst wird jede Fristangabe zum Fehltreffer. Ein Datum im richtigen Kontext wird markiert.
Die stärksten Person-Recognizer kamen aus juristischen Konventionen, nicht aus dem Modell:
- Parteien um das Versus-Zeichen
./., wo spaCy einen nackten Nachnamen als Ort liest. - Ein Name vor einer geklammerten Vertragsrolle, wie „Gerlinde A. (Darlehensgeberin)“, mit einem Postleitzahl-Look-behind, damit er keine Adresse verschluckt.
- Kleingeschriebene Namen in Grußformeln, wie „danke, fabian“.
Die Bugs waren die Lektion
Über zwei muss ich immer noch schmunzeln.
Presidio kompiliert seine Pattern-Regexes standardmäßig mit IGNORECASE. Mein Anrede-Recognizer machte aus „Herr Bauer“ eine Person. Mit IGNORECASE matchte er auch „Herr über seine Daten“. Die Flags explizit zu setzen, ohne IGNORECASE, behob es, und der Unit-Test hatte es bereits gefangen.
Der zweite: „Uerdinger Straße 214“ wollte nicht matchen, während zusammengeschriebene Formen es taten. Mein Adress-Pattern verlangte mindestens ein Zeichen vor dem Suffix „Straße“, sodass ein alleinstehendes „Straße“ nie matchen konnte. Zusammengeschriebene versus alleinstehende Suffixe sind eine klassische Falle bei deutschen Adressen.
Eval vor Features
Ein Erkennungs-Tool ist nur so vertrauenswürdig wie der Nachweis dessen, was es erkennt. Also kam die Eval vor den meisten Features.
Der Datensatz sind 40 synthetische Dokumente mit rund 150 annotierten Entitäten. Synthetisch mit Absicht: In ein Repository für ein Privacy-Produkt kommen keine echten personenbezogenen Daten. Die Dokumente sind bewusst gemein. Genitivformen („Wexlers“). Substantiv-Nachnamen. Kleingeschriebene E-Mails. IBANs mit und ohne Leerzeichen. Drei Datumsformate. Ein reines Emoji-Dokument als UTF-16-Offset-Test.
Die beste Entscheidung im Harness war, Offsets niemals von Hand zu zählen. Gold-Spans werden aus Inline-Markern im Quelltext erzeugt:
In der Sache [[PERSON|Jonas Wexler]] gegen die [[ORG|Beispiel GmbH]] ...
Ein Skript verwandelt diese Marker in exakte Zeichen-Offsets. UTF-16-Offsets über 40 Dokumente von Hand zu zählen, wäre ein Festival von Off-by-one-Fehlern geworden, und jeder einzelne hätte wie ein Erkennungs-Bug ausgesehen.
Vier Gates laufen in CI und blockieren den Build: strukturierte Recall mindestens 0.98, deutsche Person-Recall mindestens 0.95, gesamte deutsche Recall mindestens 0.95 und Precision mindestens 0.80. Die Baseline lag deutlich unter allen vieren (strukturierte Recall 0.636). Das war der Sinn. Die Gates beschreiben, wo das Produkt sein muss, nicht, wo es startet. Am Ende des Kern-Meilensteins waren alle vier grün (strukturierte Recall 1.0, deutsche Person-Recall 1.0, gesamt Deutsch 0.992, Precision 0.833), und der Eval-Job wechselte von beratend zu blockierend.
Zwei Regeln halten es ehrlich. Senkt eine Änderung eine gegatete Metrik, halte an und melde es, senke niemals die Schwelle. Und wenn das Modell recht hat und das Gold falsch liegt, korrigiere das Gold in Richtung mehr Wahrheit: Als spaCy korrekt Anrede-Namen fand, die ich nicht annotiert hatte, ergänzte ich sie (156 statt 150) und nahm die höhere Latte.
Ein Score-Maßstab ist ein Vertrag
Den Harness gegen das komplette Backend laufen zu lassen statt nur gegen den Erkennungs-Service, legte einen Fehler offen, den ein engerer Test versteckt hätte. Die Adress-Recall fiel von 1.0 auf 0.0. Der Merge-Schritt löst überlappende Spans nach Score auf. spaCy-Entitäten kamen mit 0.85 herein, meine Pattern-Recognizer mit 0.70 bis 0.75, also schlugen Ortsfragmente ganze Adressen.
Die Lektion: Sobald du Konflikte nach Score auflöst, ist der Score-Maßstab ein Vertrag. Pattern-Matches scoren jetzt 0.9, statistisches NER scort 0.85, und Backend-Regex gewinnt über Quellen-Priorität.
Nichts von der finalen Qualität kam daher, dass ein allgemeines Modell schlauer wurde. Sie kam daher, aufzuschreiben, wie deutsche juristische Dokumente tatsächlich gelesen werden, und die Zahlen nicht driften zu lassen.