Natural Language Processing in het Nederlands

Posted on: 31/03/2021 by: Joachim Ganseman

We publiceerden eerder al verschillende artikels over de computationele verwerking van taal, in het vakjargon bekend als Natural Language Processing (NLP):

NLP is een belangrijke component in toepassingen zoals chatbots en voicebots. Het kan ook ingezet worden om data te annoteren en bvb. een knowledge graph te verrijken, wat dan weer beter kennisbeheer en -ontsluiting mogelijk maakt.

Het is niet zo moeilijk voor iemand met wat programmeervaardigheden om met NLP te experimenteren. Veel code is open source, en er zijn talloze goede tutorials te vinden, compleet met educatieve notebooks in Jupyter of Google Colab en duidelijke instructievideo’s. Maar deze beperken zich veelal tot het Engels, of heel af en toe een andere grote wereldtaal.

We lezen dan wel met de regelmaat van de klok over spectaculaire vooruitgang met NLP in het Engels – herinner u GPT-3 – maar over minder courant gesproken talen horen we weinig. Iemand die NLP probeert toe te passen op het Nederlands, komt dan ook plots een paar obstakels tegen. Ook De Standaard merkte dit recent op, en wist daarbij te vertellen dat de trainingsdata van GPT-3 voor 92% uit Engelse tekst bestaat en voor 0,35% uit Nederlandse – toch een stevige grootteorde verschil.

(Noot: de evoluties in het domein van NLP en de beschikbare tools gaan snel; dit artikel is dus accuraat op moment van schrijven maar de situatie kan volgend jaar al helemaal anders zijn.)

SpaCy is een bekende open-source Python library voor NLP. Ze is gebruiksvriendelijk en abstraheert veel van de onderliggende complexiteit van NLP of de achterliggende Machine Learning frameworks zoals PyTorch of Tensorflow weg. Sinds versie 3.0 ondersteunen ze ook de transformer deep learning architecturen, die aan de basis liggen van de meest succesvolle recente taalmodellen. We gebruiken deze versie van SpaCy voor al wat volgt. Alternatieven voor SpaCy zijn o.a. Stanza en Flair.

Entiteiten herkennen

Een ingebouwde functionaliteit van hun taalmodellen is Named Entity Recognition (NER), het herkennen van entiteiten in een tekst. In de documentatie bij elk taalmodel zien we in het label scheme o.a. alle categorieën die het model kan herkennen: DATE, TIME, PERCENT etc., maar ook PERSON (eigennamen), PRODUCT of WORK_OF_ART. Het is gemakkelijk dit zelf uit te proberen op een klein stukje tekst, geïnspireerd op hun tutorial:

import spacy
from spacy import displacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("Apple buys a French company for $1 billion.")
displacy.render(doc, style="ent")

example 1

We merken dat Apple wordt herkend als ORG (organisatie), French als NORP (nationaliteit of religieuze/politieke groep) en $1 billion als een geldhoeveelheid. Proberen we echter hetzelfde in het Nederlands:

import spacy
from spacy import displacy
nlp = spacy.load("nl_core_news_sm")
doc = nlp("Apple koopt een Frans bedrijf voor $1 miljard.")
displacy.render(doc, style="ent")

example 2

In het Nederlands wordt Apple plots aanzien als persoon en wordt 1 aangeduid als simpel telwoord! Eerlijkheid gebiedt ons te zeggen dat de grote versie van het taalmodel dit wel correct annoteert, op het dollarteken na. De kleine taalmodellen die we hier hebben gebruikt illustreren echter wel een belangrijk punt: er is geen garantie dat wat werkt in de ene taal, daarom ook even goed zou werken in een andere taal, ook al is het gebruikte taalmodel zogezegd gelijkwaardig. Het repliceren van Engelstalige NLP-succesverhalen in een andere taal, is dus geen kwestie van even snel copy-pasten.

Waarom dat verschil?

Een blik op de onderliggende automatische grammaticale analyse maakt een en ander duidelijk:

displacy.render(list(doc.sents), style="dep")

graphic 1

graphic 2

De betekenis van alle afkortingen voor woordsoorten en -functies kan je terugvinden op de website van Universal Dependencies. Wat blijkt:

  • billion wordt aanzien als telwoord, miljard als zelfstandig naamwoord.
  • $ wordt in het Engels als symbool, in het Nederlands als naamwoord geannoteerd.
  • In het Engels is “for $1 billion” afhankelijk van het werkwoord (een bijwoordelijke bepaling), in het Nederlands wordt “voor $1 miljard” als afhankelijk van het naamwoord bedrijf beschouwd.
  • Apple wordt in beide talen correct aangeduid als eigennaam en onderwerp, dus de grammaticale analyse alleen is niet genoeg om het onderscheid te verklaren.

SpaCy‘s taalmodel voor het Nederlands ziet de zaken dus anders dan dat voor het Engels. De documentatie ervan laat zien dat de componenten tagger en parser, die de zinsontleding voor hun rekening nemen, ook heel anders georganiseerd zijn in het Nederlands en veel complexer (of fijnmaziger?) lijken dan die voor het Engels. Ook lezen we elders dat de dataset waarop de grammaticale analyse is getraind, anders is: OntoNotes 5 voor het Engels, en LassySmall 2.5 en Alpino uit Universal Dependencies voor het Nederlands.

Lassy, Alpino en OntoNotes

Lassy heeft haar oorsprong in de academische wereld van de computationeel taalkundigen. Er bestaan een kleine versie en een grote versie: LassyKlein met ongeveer 1 miljoen woorden, is manueel geverifieerd; LassyGroot met ongeveer 700 miljoen woorden, is automatisch geannoteerd met het programma Alpino . Ze gebruikten een eigen XML-annotatieformaat geïnspireerd op het eerdere project Corpus Gesproken Nederlands. Dit formaat verschilt nogal van het CoNLL formaat dat vaak gebruikt wordt voor opslag van dit soort gegevens, en dat de standaard is voor de Universal Dependencies datasets. Na omzetting met een convertor kon wel een subset van LassyKlein daarin opgenomen worden als UD_Dutch-LassySmall. Men is daarbij wel heel selectief geweest: enkel een subset uit Wikipedia is bewaard, met daarin 7388 zinnen van in totaal 98163 woorden.

Een andere dataset gebaseerd op voornamelijk nieuwsartikels is ook beschikbaar: UD_Dutch-Alpino, met daarin 13578 zinnen met 208601 woorden. Wie enkele van de zinnen leest, merkt dat ze vooral verzameld zijn in de vroege jaren 2000. Onderwerpen die ter sprake komen zijn bijvoorbeeld Brussel-Halle-Vilvoorde, Justine Henin-Hardenne en Wim Kok als premier van Nederland.

OntoNotes 5.0 is het Engelstalige bronmateriaal voor SpaCy’s grammaticale analyse. Het bevat naar schatting 2 miljoen woorden in +/- 300000 zinnen uit gevarieerde bronnen: het merendeel uit nieuwsartikels waarvan een deel ook uit vertaalde internationale bronnen, en dan nog een klein deel uit blogs en stukken uit de bijbel. De dataset kan gezien worden als een opvolger van de Penn Treebank , een van de eerste grote datasets voor automatische syntactische analyse. Ook OntoNotes is niet in het CoNLL formaat en vereist dus een omzetting.

Om een model te kunnen trainen, moet SpaCy de CoNLL-data nog converteren in haar eigen interne formaat.

Nederlandstalige datasets: werk aan de winkel

Het valt op dat er zowat 10 keer minder trainingsdata beschikbaar is voor Nederlands dan voor Engels. Daarbij komt ook het uitgebreidere label schema in de woordsoort-tagger, dus minder voorbeelden per label, en een beperkte variatie in het bronmateriaal – enkel oude nieuws- en wiki-artikelen. Dan is het geen verrassing dat zelfs het beste (ingebouwde) Nederlandse taalmodel in SpaCy qua accuraatheid in detectie van woordsoort (part-of-speech tags, 95%), zinsstructuur (labeled dependencies, 82%) en entiteiten (F-score 77%) achterblijft op hun slechtste Engelse taalmodel (resp. 97%, 90%, 84%) – laat staan dat het in de buurt komt van het beste Engelse taalmodel (resp. 98%, 94%, 90%). Dat laatste is weliswaar een gloednieuw transformer-model, en het valt te verwachten dat we dit type binnenkort ook voor het Nederlands zullen mogen verwachten in SpaCy, gezien er al enkele beschikbaar zijn in de transformers model hub.

Wil men de de achterstand echt structureel inhalen, dan zal men op een bepaald moment toch werk moeten maken van grotere Nederlandstalige datasets voor het trainen van taalmodellen. Dat is een intensieve bezigheid, maar misschien kan een gedeeltelijke automatisering met behulp van wat vandaag al bestaat qua automatische annotatie, aangevuld met crowdsourcing voor bvb. de validatie, de zaken wat versnellen. (In de zijlijn: voor het trainen van word embeddings / vectors hoeft de tekst niet geannoteerd te zijn. Daarvoor bestaan dus, ook in het Nederlands, wel erg grote datasets, o.a. Wikipedia dumps of Common Crawl). Ondertussen zijn de bestaande taalmodellen zeker niet nutteloos: we kunnen ze gemakkelijk tweaken voor onze eigen toepassingen.

Een taalmodel verbeteren

Wat kunnen we vandaag al doen om fouten te reduceren? Gelukkig is SpaCy als library erg modulair opgezet en iedere component van de tekstverwerkingspijplijn is naar believen aan te passen. We zagen dat het Engelse billion wel als getal werd gezien, maar het Nederlandse miljard niet.

Laat ons eerst kijken naar de taalspecifieke uitzonderingen die SpaCy definieert. Daarin zien we dat in de lexicale attributen het woord miljard wel degelijk als getal wordt aangemerkt, net zoals in het Engels. Alleen samengestelde getallen (“drieëntwintig”) worden op moment van schrijven nog niet zo geannoteerd in het Nederlands – hiermee hebben we al direct een eerste plek gevonden waar ruimte voor verbetering is qua analyse van het Nederlands in SpaCy!

The SpaCy NLP pipeline.

De SpaCy NLP pipeline. De Morphologizer is een Tagger component.

De woordsoorten worden toegekend door een Morphologizer in de pijplijn, dit is een component die getraind is en waarvan de uitkomst dus afhangt van de trainingsdata. En inderdaad , als we de inhoud van UD_Dutch-LassySmall en UD_Dutch-Alpino erop nalezen, zijn woorden zoals miljoen en miljard er enkele keren in terug te vinden, consistent geannoteerd als NOUN.

Je kan desgewenst een eigen versie van de trainingsdata maken waarin dat anders is en proberen een geheel nieuwe Morphologizer component te trainen. Voor een kleine aanpassing zoals dit is een elegant alternatief ook om een custom component toe te voegen aan de pijplijn, die de automatische annotatie door zo’n standaard taalmodel aanpast of aanvult waar nodig of gewenst. In dit geval kunnen we een zelfgeschreven AttributeRuler invoegen achter de Morphologizer, die de woordsoort aanpast van NOUN naar NUM voor NOUNs waarbij “token.like_num == True“:

ruler = nlp.add_pipe("attribute_ruler", name="fix_num", after="morphologizer")
detect = [[{"POS": "NOUN", "LIKE_NUM": True}]]  
assign = {"POS": "NUM"}  
ruler.add(patterns=detect, attrs=assign)

example 3

NER updaten

De Named Entity Recognition component, die het nodig vond om Apple een persoon te noemen, is ook een aparte component in de pijplijn die we kunnen aan- of uitschakelen, bijtrainen of desgewenst in zijn geheel vervangen. Het is niet mogelijk om een lijstje van patronen te maken waarmee alle mogelijke bedrijfsnamen herkend zouden kunnen worden, dus trainen op voorbeelden is hier onvermijdelijk. De NER-component kan hier blijkbaar enkele voorbeeldzinnen over bedrijfsacquisities gebruiken, om te leren dat niet enkel personen iets kunnen kopen.

SpaCy heeft sinds kort een hele projectarchitectuur uitgebracht die het beheer en het uitvoeren van trainingsprojecten voor taalmodellen sterk vereenvoudigt. We hoeven maar een van de templateprojecten te klonen en aan te passen aan onze noden:

python -m spacy project clone pipelines/ner_demo_update

downloadt een kant-en-klaar project dat out-of-the-box werkt. In het bestand project.yml maken we de nodige aanpassingen aan de configuratie: de taal en het basismodel wijzigen naar Nederlands, eventueel kan ook de GPU ingeschakeld worden, etc. Het project.yml bestand werkt zoals een Makefile: het definieert verschillende commando’s voor de voorbereiding van het data, het samenstellen van de trainingsconfiguratie, het uitvoeren van de training, het exporteren en packagen van het resulterende model en het schoonmaken van de bestandsstructuur. Je kan daar zelf naar believen onderdelen aan toevoegen. Er zijn ook mogelijkheden om het resultaat te visualiseren of via een API te publiceren, middels integraties met libraries zoals streamlit, FastAPI, weights&biases en ray.

Dan moeten we enkel nog trainingsdata in de map assets zetten. Er zijn allerlei tools beschikbaar om tekst of andere data te annoteren: die van UD zelf, LabelBox, Doccano, … maar buiten SpaCy’s eigen Prodigy bieden weinigen directe ondersteuning voor SpaCy. Mogelijk is het dus nog nodig om een eigen script te maken om data te converteren naar een ondersteund formaat, en dat is met een extra lijntje code in project.yml snel ingevoegd. Gelukkig is het formaat gebruikt in het demoproject relatief eenvoudig en kunnen we snel manueel een JSON-file schrijven. We nemen bijvoorbeeld enkele titels van recente artikels op Tweakers.net:

[
["OnePlus 9 Pro met nieuwe Sony-sensor verschijnt eind maart voor 899 euro.", {"entities":[[0,7,"ORG"],[25,29,"ORG"],[64,72,"MONEY"]]}],
["Gerucht: Discord voert gesprekken met Microsoft over mogelijke overname.",{"entities":[[9,16,"ORG"],[38,47,"ORG"]]}],
...
]

Om te illustreren dat de context bepalend is om een woord als een bepaalde entiteit te markeren, vermeldt geen enkele van de voorbeeldzinnen die we gebruikten Apple. Eens alle onderdelen van het project zijn gedefinieerd, is alles met 1 commando uit te voeren en even snel te visualiseren:

spacy project run all
spacy project run visualize-model

example 4

Dit geeft dus een gemengd beeld. Het goede nieuws is dat Apple nu wel wordt herkend als een bedrijf. Het systeem lijkt ook extra aandacht te hebben voor cijfers gevolgd door woorden, die in commerciële context wel eens een geldbedrag zouden kunnen zijn. Maar plots worden ook mensen en nationaliteiten aanzien als organisaties – en dat was vroeger niet zo. Wat is hier gebeurd?

Het fenomeen staat bekend als Catastrophic Forgetting: in de ijver om de herkenning van een bepaalde categorie van entiteiten te verfijnen, zijn de andere entiteiten in het model bij het bijtrainen veel te ver naar de achtergrond gedrukt. De standaard oplossing hiervoor is om er voor te zorgen dat genoeg voorbeelden zijn toegevoegd in de trainingsdata die ook nog over al die andere entiteiten gaan, zodat je bijtraint met een gezonde mix aan voorbeelden die alle gevraagde entiteiten bevat. Het blijft dus belangrijk dat trainingsdata, ook als het enkel om een update gaat, goed gebalanceerd blijft.

Custom NER

Naast het updaten van een NER component kan je hem ook integraal vervangen door een andere die je zelf traint. Misschien vindt je de 17 entiteiten aangeboden in de huidige trainingsdata overkill, en heb je genoeg aan wat bijvoorbeeld het Duitse taalmodel biedt: Persoon, Organisatie, Locatie en Miscellaneous, zoals gedefinieerd in de WikiNER dataset, en die ook beschikbaar is voor Nederlands en Frans.

Dan doe je exact hetzelfde als voordien, maar, je haalt de mosterd bij het kant-en-klare SpaCy WikiNER project dat je ook eenvoudig kan klonen:

python -m spacy project clone pipelines/ner_wikiner 

Dan is het een kwestie van de trainingsdata van WikiNER te downloaden en te converteren naar het juiste inputformaat. Die datavoorbereiding is waarschijnlijk nog het meeste werk. Eens het model getraind en bewaard is, kan je de NER component ervan eenvoudig inpluggen in een andere analysepijplijn, op dezelfde manier als we al eerder een stukje hadden toegevoegd aan de Morphologizer.

Het wordt vooral interessant als we zelf nieuwe categorieën van entiteiten gaan definiëren. Er is immers geen reden om ons te beperken tot dat wat voorzien is in een of andere dataset. Voor e-health toepassingen kan het erg nuttig zijn om ziektes, behandelingen en medicijnen in een tekst als dusdanig te markeren. In biomedische tekst kan het gaan over namen van genen of proteïnen. En in de rechtspraak zijn verwijzingen naar wetsartikelen ongetwijfeld ook nuttig.

Zolang je er trainingsdata voor kan aanmaken, en je zorgt dat er een goede balans is tussen alle entiteiten die je wil herkennen, is het allemaal mogelijk. Met wetsartikelen hadden we dat bij Smals Research al eens uitgeprobeerd, met het oog op entity linking – in dit geval, linken naar de eigenlijke wettekst via de ELI:

Entity Recognition van locaties, organisaties en wetsverwijzingen op een juridisch document

Het aanhouden van de projectstructuur van SpaCy, met een workflow definitie in project.yml en een trainingsconfiguratie in config.cfg , maakt dit alles veel gemakkelijker te beheren. We staan bij Smals Research dan ook graag open om verdere experimenten te doen met Named Entity Recognition. We kunnen snel een proof-of-concept aanleveren aan iedereen die een geannoteerde trainingsdataset heeft liggen.

Conclusie

Er is vandaag nog wat achterstand wat betreft Nederlandstalige NLP, maar het veld verandert erg snel. Het ontbreken van grote trainingsdatasets zet een rem op de performantie. Maar met de opkomst van transfomer modellen, ook in het Nederlands, kunnen we in de nabije toekomst zeker verbetering verwachten.

Ondertussen zijn de bestaande taalmodellen misschien niet perfect, maar zeker niet slecht. Ze zijn bovendien erg gemakkelijk om te tweaken en te optimaliseren voor eigen toepassingen. Wie vandaag al begint met computationele analyse van taal, zal dus gemakkelijk kunnen meesurfen met de opeenvolgende verbeteringen die we de komende maanden en jaren zeker zullen zien.

______________________

Dit is een ingezonden bijdrage van Joachim Ganseman, IT consultant bij Smals Research.  Dit artikel werd geschreven in eigen naam en neemt geen standpunt in namens Smals.