Einführung in Apache OpenNLP

1. Übersicht

Apache OpenNLP ist eine Open-Source-Java-Bibliothek zur Verarbeitung natürlicher Sprache.

Es verfügt über eine API für Anwendungsfälle wie Erkennung benannter Entitäten, Satzerkennung, POS-Tagging und Tokenisierung.

In diesem Tutorial sehen wir uns an, wie diese API für verschiedene Anwendungsfälle verwendet wird.

2. Maven Setup

Zuerst müssen wir die Hauptabhängigkeit zu unserer pom.xml hinzufügen :

 org.apache.opennlp opennlp-tools 1.8.4 

Die neueste stabile Version finden Sie auf Maven Central.

Einige Anwendungsfälle erfordern geschulte Modelle. Hier können Sie vordefinierte Modelle und detaillierte Informationen zu diesen Modellen hier herunterladen.

3. Satzerkennung

Beginnen wir mit dem Verständnis, was ein Satz ist.

Bei der Satzerkennung geht es darum, den Anfang und das Ende eines Satzes zu identifizieren , was normalerweise von der jeweiligen Sprache abhängt. Dies wird auch als "Satzgrenzen-Disambiguierung" (SBD) bezeichnet.

In einigen Fällen ist die Satzerkennung aufgrund der Mehrdeutigkeit des Punktzeichens ziemlich schwierig . Ein Punkt kennzeichnet normalerweise das Ende eines Satzes, kann aber auch in einer E-Mail-Adresse, einer Abkürzung, einer Dezimalstelle und vielen anderen Stellen erscheinen.

Wie für die meisten NLP-Aufgaben benötigen wir zur Satzerkennung ein trainiertes Modell als Eingabe, das sich voraussichtlich im Ordner / resources befindet .

Um die Satzerkennung zu implementieren, laden wir das Modell und übergeben es an eine Instanz von SentenceDetectorME . Dann übergeben wir einfach einen Text an die sentDetect () -Methode, um ihn an den Satzgrenzen aufzuteilen:

@Test public void givenEnglishModel_whenDetect_thenSentencesAreDetected() throws Exception { String paragraph = "This is a statement. This is another statement." + "Now is an abstract word for time, " + "that is always flying. And my email address is [email protected]"; InputStream is = getClass().getResourceAsStream("/models/en-sent.bin"); SentenceModel model = new SentenceModel(is); SentenceDetectorME sdetector = new SentenceDetectorME(model); String sentences[] = sdetector.sentDetect(paragraph); assertThat(sentences).contains( "This is a statement.", "This is another statement.", "Now is an abstract word for time, that is always flying.", "And my email address is [email protected]"); }

Hinweis:Das Suffix "ME" wird in vielen Klassennamen in Apache OpenNLP verwendet und stellt einen Algorithmus dar, der auf "Maximum Entropy" basiert.

4. Tokenisierung

Nachdem wir nun einen Textkorpus in Sätze unterteilen können, können wir beginnen, einen Satz genauer zu analysieren.

Das Ziel der Tokenisierung besteht darin, einen Satz in kleinere Teile zu unterteilen, die als Token bezeichnet werden . Normalerweise sind diese Token Wörter, Zahlen oder Satzzeichen.

In OpenNLP stehen drei Arten von Tokenizern zur Verfügung.

4.1. Verwenden von TokenizerME

In diesem Fall müssen wir zuerst das Modell laden. Wir können die Modelldatei von hier herunterladen, in den Ordner / resources legen und von dort laden.

Als Nächstes erstellen wir eine Instanz von TokenizerME mit dem geladenen Modell und verwenden die Methode tokenize () , um die Tokenisierung für einen beliebigen String durchzuführen :

@Test public void givenEnglishModel_whenTokenize_thenTokensAreDetected() throws Exception { InputStream inputStream = getClass() .getResourceAsStream("/models/en-token.bin"); TokenizerModel model = new TokenizerModel(inputStream); TokenizerME tokenizer = new TokenizerME(model); String[] tokens = tokenizer.tokenize("Baeldung is a Spring Resource."); assertThat(tokens).contains( "Baeldung", "is", "a", "Spring", "Resource", "."); }

Wie wir sehen können, hat der Tokenizer alle Wörter und das Punktzeichen als separate Token identifiziert. Dieser Tokenizer kann auch mit einem speziell trainierten Modell verwendet werden.

4.2. WhitespaceTokenizer

Wie der Name schon sagt, teilt dieser Tokenizer den Satz einfach in Token auf, wobei Leerzeichen als Trennzeichen verwendet werden:

@Test public void givenWhitespaceTokenizer_whenTokenize_thenTokensAreDetected() throws Exception { WhitespaceTokenizer tokenizer = WhitespaceTokenizer.INSTANCE; String[] tokens = tokenizer.tokenize("Baeldung is a Spring Resource."); assertThat(tokens) .contains("Baeldung", "is", "a", "Spring", "Resource."); }

Wir können sehen, dass der Satz durch Leerzeichen geteilt wurde und daher "Ressource" erhalten. (mit dem Punkt am Ende) als einzelnes Token anstelle von zwei verschiedenen Token für das Wort „Ressource“ und das Punktzeichen.

4.3. SimpleTokenizer

Dieser Tokenizer ist etwas ausgefeilter als der WhitespaceTokenizer und teilt den Satz in Wörter, Zahlen und Satzzeichen auf. Dies ist das Standardverhalten und erfordert kein Modell:

@Test public void givenSimpleTokenizer_whenTokenize_thenTokensAreDetected() throws Exception { SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String[] tokens = tokenizer .tokenize("Baeldung is a Spring Resource."); assertThat(tokens) .contains("Baeldung", "is", "a", "Spring", "Resource", "."); }

5. Erkennung benannter Entitäten

Nachdem wir die Tokenisierung verstanden haben, werfen wir einen Blick auf einen ersten Anwendungsfall, der auf einer erfolgreichen Tokenisierung basiert: NER (Named Entity Recognition).

Das Ziel von NER ist es, benannte Entitäten wie Personen, Standorte, Organisationen und andere benannte Dinge in einem bestimmten Text zu finden.

OpenNLP verwendet vordefinierte Modelle für Personennamen, Datum und Uhrzeit, Orte und Organisationen. Wir müssen das Modell mit TokenNameFinderModel und ladenÜbergeben Sie es an eine Instanz von NameFinderME. Dann können wir die find () -Methode verwenden, um benannte Entitäten in einem bestimmten Text zu finden:

@Test public void givenEnglishPersonModel_whenNER_thenPersonsAreDetected() throws Exception { SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String[] tokens = tokenizer .tokenize("John is 26 years old. His best friend's " + "name is Leonard. He has a sister named Penny."); InputStream inputStreamNameFinder = getClass() .getResourceAsStream("/models/en-ner-person.bin"); TokenNameFinderModel model = new TokenNameFinderModel( inputStreamNameFinder); NameFinderME nameFinderME = new NameFinderME(model); List spans = Arrays.asList(nameFinderME.find(tokens)); assertThat(spans.toString()) .isEqualTo("[[0..1) person, [13..14) person, [20..21) person]"); }

Wie wir in der Behauptung sehen können, ist das Ergebnis eine Liste von Span- Objekten, die die Start- und Endindizes der Token enthalten, aus denen benannte Entitäten im Text bestehen.

6. Tag-of-Speech-Tagging

Ein weiterer Anwendungsfall, für den eine Liste von Token als Eingabe erforderlich ist, ist das Teil-der-Sprache-Tagging.

Ein Teil der Sprache (POS) identifiziert den Typ eines Wortes. OpenNLP verwendet die folgenden Tags für die verschiedenen Wortarten:

  • NN - Substantiv, Singular oder Masse
  • DT – determiner
  • VB – verb, base form
  • VBD – verb, past tense
  • VBZ – verb, third person singular present
  • IN – preposition or subordinating conjunction
  • NNP – proper noun, singular
  • TO – the word “to”
  • JJ – adjective

These are same tags as defined in the Penn Tree Bank. For a complete list please refer to this list.

Similar to the NER example, we load the appropriate model and then use POSTaggerME and its method tag() on a set of tokens to tag the sentence:

@Test public void givenPOSModel_whenPOSTagging_thenPOSAreDetected() throws Exception { SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String[] tokens = tokenizer.tokenize("John has a sister named Penny."); InputStream inputStreamPOSTagger = getClass() .getResourceAsStream("/models/en-pos-maxent.bin"); POSModel posModel = new POSModel(inputStreamPOSTagger); POSTaggerME posTagger = new POSTaggerME(posModel); String tags[] = posTagger.tag(tokens); assertThat(tags).contains("NNP", "VBZ", "DT", "NN", "VBN", "NNP", "."); }

The tag() method maps the tokens into a list of POS tags. The result in the example is:

  1. “John” – NNP (proper noun)
  2. “has” – VBZ (verb)
  3. “a” – DT (determiner)
  4. “sister” – NN (noun)
  5. “named” – VBZ (verb)
  6. “Penny” –NNP (proper noun)
  7. “.” – period

7. Lemmatization

Now that we have the part-of-speech information of the tokens in a sentence, we can analyze the text even further.

Lemmatization is the process of mapping a word form that can have a tense, gender, mood or other information to the base form of the word – also called its “lemma”.

A lemmatizer takes a token and its part-of-speech tag as input and returns the word's lemma. Hence, before Lemmatization, the sentence should be passed through a tokenizer and POS tagger.

Apache OpenNLP provides two types of lemmatization:

  • Statistical – needs a lemmatizer model built using training data for finding the lemma of a given word
  • Dictionary-based – requires a dictionary which contains all valid combinations of a word, POS tags, and the corresponding lemma

For statistical lemmatization, we need to train a model, whereas for the dictionary lemmatization we just need a dictionary file like this one.

Let's look at a code example using a dictionary file:

@Test public void givenEnglishDictionary_whenLemmatize_thenLemmasAreDetected() throws Exception { SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String[] tokens = tokenizer.tokenize("John has a sister named Penny."); InputStream inputStreamPOSTagger = getClass() .getResourceAsStream("/models/en-pos-maxent.bin"); POSModel posModel = new POSModel(inputStreamPOSTagger); POSTaggerME posTagger = new POSTaggerME(posModel); String tags[] = posTagger.tag(tokens); InputStream dictLemmatizer = getClass() .getResourceAsStream("/models/en-lemmatizer.dict"); DictionaryLemmatizer lemmatizer = new DictionaryLemmatizer( dictLemmatizer); String[] lemmas = lemmatizer.lemmatize(tokens, tags); assertThat(lemmas) .contains("O", "have", "a", "sister", "name", "O", "O"); }

As we can see, we get the lemma for every token. “O” indicates that the lemma could not be determined as the word is a proper noun. So, we don't have a lemma for “John” and “Penny”.

But we have identified the lemmas for the other words of the sentence:

  • has – have
  • a – a
  • sister – sister
  • named – name

8. Chunking

Part-of-speech information is also essential in chunking – dividing sentences into grammatically meaningful word groups like noun groups or verb groups.

Similar to before, we tokenize a sentence and use part-of-speech tagging on the tokens before the calling the chunk() method:

@Test public void givenChunkerModel_whenChunk_thenChunksAreDetected() throws Exception { SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE; String[] tokens = tokenizer.tokenize("He reckons the current account deficit will narrow to only 8 billion."); InputStream inputStreamPOSTagger = getClass() .getResourceAsStream("/models/en-pos-maxent.bin"); POSModel posModel = new POSModel(inputStreamPOSTagger); POSTaggerME posTagger = new POSTaggerME(posModel); String tags[] = posTagger.tag(tokens); InputStream inputStreamChunker = getClass() .getResourceAsStream("/models/en-chunker.bin"); ChunkerModel chunkerModel = new ChunkerModel(inputStreamChunker); ChunkerME chunker = new ChunkerME(chunkerModel); String[] chunks = chunker.chunk(tokens, tags); assertThat(chunks).contains( "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"); }

As we can see, we get an output for each token from the chunker. “B” represents the start of a chunk, “I” represents the continuation of the chunk and “O” represents no chunk.

Parsing the output from our example, we get 6 chunks:

  1. “He” – noun phrase
  2. “reckons” – verb phrase
  3. “the current account deficit” – noun phrase
  4. “will narrow” – verb phrase
  5. “to” – preposition phrase
  6. “only 8 billion” – noun phrase

9. Language Detection

Additionally to the use cases already discussed, OpenNLP also provides a language detection API that allows to identify the language of a certain text.

For language detection, we need a training data file. Such a file contains lines with sentences in a certain language. Each line is tagged with the correct language to provide input to the machine learning algorithms.

A sample training data file for language detection can be downloaded here.

We can load the training data file into a LanguageDetectorSampleStream, define some training data parameters, create a model and then use the model to detect the language of a text:

@Test public void givenLanguageDictionary_whenLanguageDetect_thenLanguageIsDetected() throws FileNotFoundException, IOException { InputStreamFactory dataIn = new MarkableFileInputStreamFactory( new File("src/main/resources/models/DoccatSample.txt")); ObjectStream lineStream = new PlainTextByLineStream(dataIn, "UTF-8"); LanguageDetectorSampleStream sampleStream = new LanguageDetectorSampleStream(lineStream); TrainingParameters params = new 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, new LanguageDetectorFactory()); LanguageDetector ld = new LanguageDetectorME(model); Language[] languages = ld .predictLanguages("estava em uma marcenaria na Rua Bruno"); assertThat(Arrays.asList(languages)) .extracting("lang", "confidence") .contains( tuple("pob", 0.9999999950605625), tuple("ita", 4.939427661577956E-9), tuple("spa", 9.665954064665144E-15), tuple("fra", 8.250349924885834E-25))); }

The result is a list of the most probable languages along with a confidence score.

And, with rich models , we can achieve a very higher accuracy with this type of detection.

5. Conclusion

Wir haben hier viel über die interessanten Funktionen von OpenNLP herausgefunden. Wir haben uns auf einige interessante Funktionen konzentriert, um NLP-Aufgaben wie Lemmatisierung, POS-Tagging, Tokenisierung, Satzerkennung, Spracherkennung und mehr auszuführen.

Wie immer finden Sie die vollständige Implementierung aller oben genannten Funktionen auf GitHub.