Inleiding tot Apache OpenNLP

1. Overzicht

Apache OpenNLP is een open source Java-bibliotheek voor natuurlijke taalverwerking.

Het beschikt over een API voor use cases zoals Named Entity Recognition, Sentence Detection, POS-tagging en Tokenization.

In deze zelfstudie bekijken we hoe u deze API voor verschillende gebruiksscenario's kunt gebruiken.

2. Maven-instellingen

Ten eerste moeten we de belangrijkste afhankelijkheid toevoegen aan onze pom.xml:

 org.apache.opennlp opennlp-tools 1.8.4 

De laatste stabiele versie is te vinden op Maven Central.

Sommige use-cases hebben getrainde modellen nodig. U kunt hier voorgedefinieerde modellen downloaden en hier gedetailleerde informatie over deze modellen.

3. Zinsopsporing

Laten we beginnen met te begrijpen wat een zin is.

Zinsherkenning gaat over het identificeren van het begin en het einde van een zin, die meestal afhangt van de taal die u gebruikt. Dit wordt ook wel "Sentence Boundary Disambiguation" (SBD) genoemd.

In sommige gevallen, zinsopsporing is behoorlijk uitdagend vanwege de dubbelzinnige aard van het puntteken. Een punt geeft meestal het einde van een zin aan, maar kan ook voorkomen in een e-mailadres, een afkorting, een decimaal en veel andere plaatsen.

Zoals voor de meeste NLP-taken, hebben we voor zinsdetectie een getraind model nodig als invoer, waarvan we verwachten dat het in het /middelen map.

Om zinsherkenning te implementeren, laden we het model en geven het door aan een instantie van ZinDetectorME. Vervolgens geven we gewoon een tekst door aan het sentDetect () methode om het te splitsen bij de zinsgrenzen:

@Test openbare leegte gegevenEnglishModel_whenDetect_thenSentencesAreDetected () genereert uitzondering {String paragraph = "Dit is een statement. Dit is een andere statement." + "Nu is een abstract woord voor tijd", + "dat vliegt altijd. En mijn e-mailadres is [email protected]"; InputStream is = getClass (). GetResourceAsStream ("/ models / en-sent.bin"); ZinModel-model = nieuw SentenceModel (is); ZinDetectorME sdetector = nieuwe SentenceDetectorME (model); String-zinnen [] = sdetector.sentDetect (paragraaf); assertThat (zinnen) .bevat ("Dit is een verklaring.", "Dit is een andere verklaring.", "Nu is een abstract woord voor tijd, dat vliegt altijd.", "En mijn e-mailadres is [email protected]" ); }

Opmerking:het achtervoegsel "ME" wordt in veel klassenamen in Apache OpenNLP gebruikt en vertegenwoordigt een algoritme dat is gebaseerd op "Maximum Entropy".

4. Tokeniseren

Nu we een corpus tekst in zinnen kunnen verdelen, kunnen we een zin gedetailleerder gaan analyseren.

Het doel van tokenisatie is om een ​​zin op te splitsen in kleinere delen die tokens worden genoemd. Meestal zijn deze tokens woorden, cijfers of leestekens.

Er zijn drie soorten tokenizers beschikbaar in OpenNLP.

4.1. Gebruik makend van TokenizerME

In dit geval moeten we eerst het model laden. We kunnen het modelbestand vanaf hier downloaden, het in het /middelen map en laad het vanaf daar.

Vervolgens maken we een instantie van TokenizerME met behulp van het geladen model en gebruik de tokenize () methode om tokenisatie uit te voeren op een Draad:

@Test openbare leegte gegevenEnglishModel_whenTokenize_thenTokensAreDetected () genereert uitzondering {InputStream inputStream = getClass () .getResourceAsStream ("/ models / en-token.bin"); TokenizerModel-model = nieuw TokenizerModel (inputStream); TokenizerME tokenizer = nieuwe TokenizerME (model); String [] tokens = tokenizer.tokenize ("Baeldung is een bron van bronnen."); assertThat (tokens) .contains ("Baeldung", "is", "a", "Spring", "Resource", "."); }

Zoals we kunnen zien, heeft de tokenizer alle woorden en het puntteken geïdentificeerd als afzonderlijke tokens. Deze tokenizer kan ook worden gebruikt met een op maat gemaakt getraind model.

4.2. WhitespaceTokenizer

Zoals de naam doet vermoeden, splitst deze tokenizer de zin eenvoudigweg op in tokens met behulp van witruimtetekens als scheidingstekens:

@Test openbare leegte gegevenWhitespaceTokenizer_whenTokenize_thenTokensAreDetected () genereert uitzondering {WhitespaceTokenizer tokenizer = WhitespaceTokenizer.INSTANCE; String [] tokens = tokenizer.tokenize ("Baeldung is een bron van bronnen."); assertThat (tokens) .contains ("Baeldung", "is", "a", "Spring", "Resource."); }

We kunnen zien dat de zin is opgesplitst door spaties en daarom krijgen we 'Resource'. (met het punt-teken aan het einde) als een enkel token in plaats van twee verschillende tokens voor het woord "Resource" en het punt-teken.

4.3. SimpleTokenizer

Deze tokenizer is iets geavanceerder dan WhitespaceTokenizer en splitst de zin in woorden, cijfers en leestekens. Dit is het standaardgedrag en vereist geen model:

@Test openbare ongeldige gegevenSimpleTokenizer_whenTokenize_thenTokensAreDetected () gooit uitzondering {SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String [] tokens = tokenizer .tokenize ("Baeldung is een Spring Resource."); assertThat (tokens) .contains ("Baeldung", "is", "a", "Spring", "Resource", "."); }

5. Herkenning van benoemde entiteiten

Nu we tokenisatie hebben begrepen, laten we eens kijken naar een eerste use-case die is gebaseerd op succesvolle tokenisatie: named entity recognition (NER).

Het doel van NER is om benoemde entiteiten zoals personen, locaties, organisaties en andere benoemde dingen in een bepaalde tekst te vinden.

OpenNLP gebruikt vooraf gedefinieerde modellen voor persoonsnamen, datum en tijd, locaties en organisaties. We moeten het model laden met TokenNameFinderModel engeef het door aan een instantie van NaamFinderME. Dan kunnen we de vind() methode om benoemde entiteiten in een bepaalde tekst te vinden:

@Test openbare ongeldigheid gegevenEnglishPersonModel_whenNER_thenPersonsAreDetected () gooit uitzondering {SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String [] tokens = tokenizer .tokenize ("John is 26 jaar oud. De" + "naam van zijn beste vriend is Leonard. Hij heeft een zus genaamd Penny."); InputStream inputStreamNameFinder = getClass () .getResourceAsStream ("/ models / en-ner-person.bin"); TokenNameFinderModel-model = nieuw TokenNameFinderModel (inputStreamNameFinder); NameFinderME nameFinderME = nieuwe NameFinderME (model); List spans = Arrays.asList (nameFinderME.find (tokens)); assertThat (spans.toString ()) .isEqualTo ("[[0..1) persoon, [13..14) persoon, [20..21) persoon]"); }

Zoals we in de bewering kunnen zien, is het resultaat een lijst met Span objecten die de begin- en eindindexen bevatten van de tokens die benoemde entiteiten in de tekst vormen.

6. Tagging van een deel van de spraak

Een andere use case waarvoor een lijst met tokens nodig is als invoer, is het taggen van delen van spraak.

Een part-of-speech (POS) identificeert het type van een woord. OpenNLP gebruikt de volgende tags voor de verschillende delen van spraak:

  • NN - zelfstandig naamwoord, enkelvoud of massa
  • DT - bepaler
  • VB - werkwoord, basisvorm
  • VBD - werkwoord, verleden tijd
  • VBZ - werkwoord, derde persoon enkelvoud aanwezig
  • IN - voorzetsel of ondergeschikte voegwoord
  • NNP - eigennaam, enkelvoud
  • NAAR - het woord "naar"
  • JJ - bijvoeglijk naamwoord

Dit zijn dezelfde tags als gedefinieerd in de Penn Tree Bank. Raadpleeg deze lijst voor een volledige lijst.

Net als bij het NER-voorbeeld laden we het juiste model en gebruiken we vervolgens POSTaggerME en zijn methode label() op een set tokens om de zin te taggen:

@Test openbare ongeldigheid gegevenPOSModel_whenPOSTagging_thenPOSAreDetected () gooit uitzondering {SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String [] tokens = tokenizer.tokenize ("John heeft een zus genaamd Penny."); InputStream inputStreamPOSTagger = getClass () .getResourceAsStream ("/ models / en-pos-maxent.bin"); POSModel posModel = nieuw POSModel (inputStreamPOSTagger); POSTaggerME posTagger = nieuwe POSTaggerME (posModel); String-tags [] = posTagger.tag (tokens); assertThat (tags) .contains ("NNP", "VBZ", "DT", "NN", "VBN", "NNP", "."); }

De label() methode wijst de tokens toe aan een lijst met POS-tags. Het resultaat in het voorbeeld is:

  1. "John" - NNP (eigennaam)
  2. "Heeft" - VBZ (werkwoord)
  3. "A" - DT (bepaler)
  4. "Sister" - NN (zelfstandig naamwoord)
  5. "Named" - VBZ (werkwoord)
  6. "Cent" -NNP (eigennaam)
  7. "." - periode

7. Lemmatisering

Nu we de woordsoortinformatie van de tokens in een zin hebben, kunnen we de tekst nog verder analyseren.

Lemmatisering is het proces waarbij een woordvorm in kaart wordt gebracht die een gespannen, geslacht, gemoedstoestand of andere informatie kan hebben naar de basisvorm van het woord - ook wel het "lemma" genoemd.

Een lemmatiseerder neemt een token en de bijbehorende woorddeel-tag als invoer en retourneert het lemma van het woord. Daarom moet de zin vóór lemmatisering worden doorgegeven via een tokenizer en POS-tagger.

Apache OpenNLP biedt twee soorten lemmatisering:

  • Statistisch - heeft een lemmatiseringsmodel nodig dat is gebouwd met behulp van trainingsgegevens om het lemma van een bepaald woord te vinden
  • Woordenboek-gebaseerd - vereist een woordenboek dat alle geldige combinaties van een woord, POS-tags en het bijbehorende lemma bevat

Voor statistische lemmatisering moeten we een model trainen, terwijl we voor de woordenboeklemmatisering alleen een woordenboekbestand zoals dit nodig hebben.

Laten we een codevoorbeeld bekijken met een woordenboekbestand:

@Test openbare leegte gegevenEnglishDictionary_whenLemmatize_thenLemmasAreDetected () gooit uitzondering {SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String [] tokens = tokenizer.tokenize ("John heeft een zus genaamd Penny."); InputStream inputStreamPOSTagger = getClass () .getResourceAsStream ("/ models / en-pos-maxent.bin"); POSModel posModel = nieuw POSModel (inputStreamPOSTagger); POSTaggerME posTagger = nieuwe POSTaggerME (posModel); String-tags [] = posTagger.tag (tokens); InputStream dictLemmatizer = getClass () .getResourceAsStream ("/ models / en-lemmatizer.dict"); DictionaryLemmatizer lemmatizer = nieuw DictionaryLemmatizer (dictLemmatizer); String [] lemmas = lemmatizer.lemmatize (tokens, tags); assertThat (lemma's) .contains ("O", "have", "a", "sister", "name", "O", "O"); }

Zoals we kunnen zien, krijgen we het lemma voor elk teken. "O" geeft aan dat het lemma niet kon worden bepaald aangezien het woord een eigennaam is. We hebben dus geen lemma voor "John" en "Penny".

Maar we hebben de lemma's geïdentificeerd voor de andere woorden van de zin:

  • heeft gehad
  • een - een
  • zus - zus
  • naam - naam

8. Chunking

Informatie over spraakgedeelten is ook essentieel bij het opdelen - het verdelen van zinnen in grammaticaal betekenisvolle woordgroepen zoals zelfstandig naamwoordgroepen of werkwoordgroepen.

Net als voorheen, tokeniseren we een zin en gebruiken we part-of-speech-tagging op de tokens voordat de brok() methode:

@Test openbare ongeldig gegevenChunkerModel_whenChunk_thenChunksAreDetected () genereert uitzondering {SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String [] tokens = tokenizer.tokenize ("Hij schat dat het tekort op de lopende rekening zal afnemen tot slechts 8 miljard."); InputStream inputStreamPOSTagger = getClass () .getResourceAsStream ("/ models / en-pos-maxent.bin"); POSModel posModel = nieuw POSModel (inputStreamPOSTagger); POSTaggerME posTagger = nieuwe POSTaggerME (posModel); String-tags [] = posTagger.tag (tokens); InputStream inputStreamChunker = getClass () .getResourceAsStream ("/ models / en-chunker.bin"); ChunkerModel chunkerModel = nieuw ChunkerModel (inputStreamChunker); ChunkerME chunker = nieuwe ChunkerME (chunkerModel); String [] chunks = chunker.chunk (tokens, tags); assertThat (chunks). bevat ("B-NP", "B-VP", "B-NP", "I-NP", "I-NP", "I-NP", "B-VP", " I-VP "," B-PP "," B-NP "," I-NP "," I-NP "," O "); }

Zoals we kunnen zien, krijgen we een uitvoer voor elk token van de chunker. "B" staat voor het begin van een blok, "I" staat voor de voortzetting van het blok en "O" staat voor geen blok.

Als we de uitvoer van ons voorbeeld parseren, krijgen we 6 brokken:

  1. "Hij" - zelfstandig naamwoord
  2. "Reckons" - werkwoordsuitdrukking
  3. "Het tekort op de lopende rekening" - zelfstandig naamwoord
  4. "Zal verkleinen" - werkwoordsuitdrukking
  5. "To" - voorzetsel
  6. "Slechts 8 miljard" - naamwoordelijke zin

9. Taaldetectie

Naast de al besproken use cases, OpenNLP biedt ook een API voor taaldetectie waarmee de taal van een bepaalde tekst kan worden geïdentificeerd.

Voor taaldetectie hebben we een trainingsgegevensbestand nodig. Zo'n bestand bevat regels met zinnen in een bepaalde taal. Elke regel is getagd met de juiste taal om input te leveren voor de algoritmen voor machine learning.

Een voorbeeld van een trainingsgegevensbestand voor taaldetectie kan hier worden gedownload.

We kunnen het trainingsgegevensbestand laden in een LanguageDetectorSampleStream, definieer enkele trainingsgegevensparameters, maak een model en gebruik het model om de taal van een tekst te detecteren:

@Test openbare leegte gegevenLanguageDictionary_whenLanguageDetect_thenLanguageIsDetected () gooit FileNotFoundException, IOException {InputStreamFactory dataIn = nieuw MarkableFileInputStreamFactory (nieuw bestand ("src / main / resources / models / DoccatSample.txt")); ObjectStream lineStream = nieuwe PlainTextByLineStream (dataIn, "UTF-8"); LanguageDetectorSampleStream sampleStream = nieuwe LanguageDetectorSampleStream (lineStream); TrainingParameters params = nieuwe TrainingParameters (); params.put (TrainingParameters.ITERATIONS_PARAM, 100); params.put (TrainingParameters.CUTOFF_PARAM, 5); params.put ("DataIndexer", "TwoPass"); params.put (TrainingParameters.ALGORITHM_PARAM, "NAIVEBAYES"); LanguageDetectorModel model = LanguageDetectorME .train (sampleStream, params, nieuwe LanguageDetectorFactory ()); LanguageDetector ld = nieuwe LanguageDetectorME (model); Taal [] talen = ld .predictLanguages ​​("estava em uma marcenaria na Rua Bruno"); assertThat (Arrays.asList (talen)) .extracting ("lang", "trust") .contains (tuple ("pob", 0.9999999950605625), tuple ("ita", 4.939427661577956E-9), tuple ("spa", 9.665954064665144E-15), tuple ("fra", 8.250349924885834E-25))); }

Het resultaat is een lijst met de meest waarschijnlijke talen samen met een betrouwbaarheidsscore.

En met rijke modellen kunnen we een zeer hogere nauwkeurigheid bereiken met dit type detectie.

5. Conclusie

We hebben hier veel onderzocht vanuit de interessante mogelijkheden van OpenNLP. We hebben ons gericht op enkele interessante functies om NLP-taken uit te voeren, zoals lemmatisering, POS-tagging, tokenisatie, zinsdetectie, taaldetectie en meer.

Zoals altijd is de volledige implementatie van al het bovenstaande te vinden op GitHub.