Named entity recognition (NER)
Wat is named entity recognition (NER)?
Named entity recognition is de stap waarin software een stuk tekst leest en er de benoemde dingen uithaalt. Een persoon, een bedrijf, een plaats, een datum, een bedrag, een productcode, een factuurnummer, een rijksregisternummer. Elke vondst krijgt een label dat zegt om welk soort ding het gaat.
Wat je terugkrijgt is een lijst spans. Een span is een stuk tekst met vier dingen eraan vast: de exacte woorden, waar ze beginnen, waar ze eindigen, en het label. spaCy, de open source bibliotheek waar de meeste Python-teams naar grijpen, omschrijft zijn herkenner als een systeem dat "assigns labels to contiguous spans of tokens". Bij de clouddiensten krijg je er meestal een confidence-score per span bij.
Die posities tellen zwaarder dan je zou denken, en dat komt door wat erna gebeurt. De output vult een kolom in een database en een factuur wordt een rij met een leverancier, een nummer en een totaal. Of ze stuurt een maskeerstap aan, en dan moet je precies weten welke tekens je vervangt voor de tekst verder reist. Of ze voedt een zoekindex, zodat een document terugvindbaar wordt op de bedrijven en de mensen die erin vermeld staan.
NER en classificatie worden vaak door elkaar gehaald, en het verschil zit in waar het label landt. Classificatie plakt één label op het hele document: deze mail is een klacht, deze factuur is een creditnota. NER plakt labels op stukken binnenin het document en zegt waar elk stuk zit. De meeste pipelines doen allebei, in die volgorde: eerst het ticket als klacht bestempelen, dan de klantnaam en het ordernummer eruit halen. NER is ook geen entity resolution, want dat is de aparte klus om te beslissen dat de bedrijfsnaam die je net vond en een record in je ERP hetzelfde bedrijf zijn.
Drie manieren om entiteiten te vinden
Regels en woordenlijsten. Alles met een vaste vorm los je beter op met rekenwerk dan met een model. Een rijksregisternummer telt elf cijfers met een controlegetal dat je kan herberekenen, dus een reguliere expressie zoekt de kandidaten en de controle gooit de typfouten eruit. Hetzelfde geldt voor IBAN's, btw-nummers en elke code die je eigen systemen uitdelen, terwijl een woordenlijst je zeshonderd leveranciers dekt. spaCy heeft daar een component voor, de EntityRuler, en de documentatie geeft meteen de reden om de moeite te doen: je kan "combine both approaches and improve a statistical model with rules to handle very specific cases and boost accuracy".
Een getraind model. Voor de types zonder vaste vorm doet een model dat duizenden gelabelde voorbeelden gezien heeft wat regels niet kunnen, want een persoon is een persoon door de woorden eromheen. De Nederlandstalige pipelines van spaCy herkennen achttien labels uit de doos, waaronder PERSON, ORG, DATE en MONEY. Azure AI Language van Microsoft biedt hetzelfde als dienst, met een vaste lijst die loopt van Person en Organization over Address tot PhoneNumber, plus een custom variant waarin je het op je eigen types traint.
Een taalmodel met een prompt. De derde weg slaat het trainen over. Je beschrijft het type in een zin, geeft het model de tekst, en krijgt de spans terug. "Vind elke verwijzing naar een onderhoudscontract, met contractnummer en einddatum" is een type dat niemand als model verkoopt en waar je geen gelabelde data voor hebt. Om de output bruikbaar te houden leg je ze vast in een schema: de structured outputs van Anthropic geven "valid JSON matching your schema" terug, en de documentatie noemt data uit tekst halen als een van de doelen van die functie.
Een getraind model of een LLM-prompt
De twee algemene opties trekken aan elkaar op één dimensie: kost per rij tegenover flexibiliteit.
Een getraind model is een download die op je eigen CPU draait. De kost per rij is machinetijd, dus honderdduizend tickets op een nacht verandert weinig aan je serverfactuur. Wat je inlevert is bereik, want de labels lagen vast op het moment van trainen en "contractreferentie" toevoegen betekent voorbeelden verzamelen, labelen en opnieuw trainen. Bij een LLM-prompt keert dat om. Elk document is een modelaanroep, dus diezelfde honderdduizend tickets worden een echte factuur en een veel langere run, maar een nieuw type kost je één zin en het model kan overweg met tekst waarvan een getrainde pipeline de vorm nog nooit gezien heeft.
In de praktijk kiezen we vaak een middenweg. Laat het LLM één keer over een paar honderd documenten lopen, laat iemand de output verbeteren, en train het goedkope model daarop. Je betaalt de dure weg één keer in plaats van op elke rij, en je houdt de gelabelde set over die je toch nodig had.
Hoe weet je of het werkt
Precisie is het aandeel van de spans die het systeem aanduidde dat ook echt juist was, en recall is het aandeel van de spans die er stonden dat het gevonden heeft. Gepubliceerde scores zeggen weinig over allebei op jouw tekst. spaCy publiceert zijn cijfers per pipeline, en het grote Nederlandstalige model in versie 3.7.0 komt uit op 78,51 precisie en 75,03 recall op de eigen evaluatieset van spaCy. Jouw aankoopfacturen en jouw Nederlandstalige tickets zijn die tekst niet, dus label je een eigen staal: een paar honderd documenten, nagekeken door iemand die het domein kent, volstaat voor een eerlijk cijfer en dient meteen als de set die je na elke wijziging opnieuw draait.
Welk cijfer voorgaat, hangt af van wat er na de span gebeurt. Een rijksregisternummer dat je maskeerstap mist, is een lek, dus recall is wat je koopt en neem je erbij dat er af en toe een orderreferentie meegaat. Een productcode die je zoekindex mist, kost één document een paar plaatsen in de rangschikking, terwijl een valse treffer die een gewoon woord wegveegt de tekst onleesbaar maakt, dus daar gaat precisie voor. Zet de drempel per label, niet één keer voor de hele pipeline.
Nederlandstalige tekst en Belgische nummers
Vlaamse familienamen dragen tussenvoegsels die gewone Nederlandse woorden zijn. "De Clercq", "Van den Broeck", "Van der Auwera": een model dat vooral op gepubliceerd Nederlands proza getraind is, knipt het tussenvoegsel er graag af en geeft "Clercq" terug. Volgens onze ervaring is dat de Nederlandstalige NER-fout die het meest loont om met een woordenlijst van je eigen namen bij te sturen. Rechtsvormen werken dan weer in je voordeel. Een naam die eindigt op BV, NV, VZW of CV is een organisatie per regel, niet per voorspelling.
Bij de nummers verdienen regels hun plaats. Het ondernemingsnummer uit de Kruispuntbank van Ondernemingen telt tien cijfers en begint met een 0 of een 1, en zodra de belastingadministratie het activeert, is datzelfde nummer met BE ervoor je btw-nummer. De reeks met een 0 raakte op en het eerste nummer met een 1 is toegekend op 19 september 2023, dus een regel die voor die datum geschreven is en nooit herzien, weigert vandaag geldige nummers. Een vestigingseenheidsnummer telt eveneens tien cijfers, maar begint met een cijfer van 2 tot 8, dus een patroon dat op "tien cijfers" matcht, haalt die twee stilletjes door elkaar.
Het rijksregisternummer is het strengste van allemaal, en daardoor het makkelijkste. Elf cijfers: de geboortedatum als jaar, maand en dag, dan een reeksnummer van drie cijfers dat onpaar is voor een man en paar voor een vrouw, dan twee controlecijfers. De controle is het getal van negen cijfers uit de datum en het reeksnummer, gedeeld door 97, waarna je de rest van 97 aftrekt, en wie geboren is vanaf 2000 krijgt een 2 voor die negen cijfers voor je deelt. Herbereken dat en zowat elke valse treffer valt weg.
Dan is er nog de tweetalige realiteit. Eén Belgisch dossier bevat een Franse offerte, Nederlandstalige voorwaarden en een Engelse mailthread, terwijl de meeste diensten één taalcode per document willen. De tekstgebaseerde detectie van persoonsgegevens bij Azure dekt Nederlands en Frans en levert Belgium National Number en Belgium Value Added Tax Number als ingebouwde types, maar de conversatievariant voor chat- en gesprekstranscripties is algemeen beschikbaar in enkel Engels, Frans, Duits en Spaans, dus Nederlandstalige transcripties vallen erbuiten. Kijk de taaltabel na van de precieze functie die je aanroept, niet die van het product.
Twee uitgewerkte voorbeelden
Aankoopfacturen naar de boekhouding
Een gedeelde mailbox verzamelt leveranciersfacturen als pdf, OCR maakt van elke pagina tekst met posities, en het bredere geheel daaromheen heet Intelligent Document Processing. NER is het deel dat beslist wat die herkende tekst betekent. Het btw-nummer op de factuur doet meer werk dan de bedrijfsnaam bovenaan, want dat nummer matcht exact met je leverancierstabel terwijl de naam in drie schrijfwijzen en twee rechtsvormen binnenkomt. Factuurnummers hebben geen gedeeld formaat, dus leer je een patroon per leverancier en val je voor nieuwe terug op het model.
De bedragen zijn het lastige stuk. Het model vindt vier geldbedragen op de pagina en kan je niet zeggen welk het totaal is, dus neemt een regel het bedrag dat het dichtst bij "Totaal" of "Te betalen" staat en controleert een berekening of de lijnen daarop uitkomen. Alles onder de drempel, of een factuur waarvan de totalen niet kloppen, gaat naar een mens met het origineel naast de voorgestelde waarden.
Tickets maskeren voor ze naar een AI-tool gaan
Je wil een assistent antwoorden laten voorbereiden op supporttickets, en de persoonsgegevens moeten er eerst uit. Een detectiestap loopt over de tekst van het ticket op zoek naar namen, mailadressen, telefoonnummers, adressen, IBAN's en rijksregisternummers, en vervangt elke span door een plaatshouder als [KLANT_1]. De vertaaltabel blijft bij jou, zodat het voorgestelde antwoord voor verzending weer de echte naam krijgt. De functie voor persoonsgegevens van Microsoft doet het zoeken en het vervangen in één aanroep en geeft de entiteitscategorieën, de confidence-scores en de gemaskeerde tekst terug.
Waar die detectie draait, bepaalt of het geheel iets oplevert. Stuur je het ruwe ticket naar een gehost LLM om de persoonsgegevens te vinden, dan heb je die persoonsgegevens al naar dat gehoste LLM gestuurd. De detector hoort op je eigen infrastructuur te staan of bij een dienst waarmee je een verwerkersovereenkomst hebt, en enkel de gemaskeerde tekst reist verder. De rest hangt aan recall, want één rijksregisternummer dat de detector mist, belandt gewoon in de AI-tool. Daarom combineert deze pipeline een model voor de namen met een harde regel en een controlegetal voor de nummers.
Onder de GDPR zijn een naam en een identificatienummer persoonsgegevens (artikel 4, lid 1), en ze vervangen door plaatshouders terwijl je de sleutel elders bijhoudt, is pseudonimisering zoals artikel 4, lid 5 het omschrijft. Zinvol, en toch niet de eindmeet: de gemaskeerde tekst blijft een persoonsgegeven en de regels blijven erop van toepassing.
Waar moet je op letten bij named entity recognition
Vraag een LLM de exacte tekst, nooit de karakterposities. Modellen zijn slecht in tellen en geven je een geloofwaardige beginpositie die er zes tekens naast zit. Neem de teruggegeven tekst en zoek die zelf op in je eigen document, en staat ze er niet letterlijk in, laat het dan luid mislukken in plaats van het verkeerde stuk van de zin weg te vegen.
Elk label heeft een geschreven definitie nodig. Is het boekhoudkantoor in de voettekst een organisatie waar je iets mee wil? Is de bank een leverancier? Twee collega's antwoorden anders, en twee runs van hetzelfde model ook. Schrijf de definitie op voor je iets meet.
Detectie is geen toegangsbeheer. Maskeren verkleint de blootstelling, het garandeert geen verwijdering. Waar de eis is dat bepaalde gegevens het huis niet verlaten, hou je ze in een systeem dat niets verstuurt, in plaats van te vertrouwen op een detector die elke vermelding moet vangen.
De cijfers verschuiven. Een nieuwe factuurlay-out, een leverancier die het model achter een endpoint vervangt, een hertraining op verse voorbeelden: elk daarvan kan je recall doen zakken zonder dat iemand het merkt. Hou het gelabelde staal bij, draai het na elke wijziging opnieuw, en behandel een daling als een blokkade voor de release en niet als een curiositeit.