Einführung in Apache OpenNLP

Einführung in Apache OpenNLP

1. Überblick

Apache OpenNLP ist eine Open-Source-Java-Bibliothek für die Verarbeitung natürlicher Sprachen.

Es bietet eine API für Anwendungsfälle wie die Erkennung benannter Entitäten, Satzerkennung, POS-Kennzeichnung und Tokenisierung.

In diesem Tutorial erfahren Sie, wie Sie diese API für verschiedene Anwendungsfälle verwenden.

2. Maven Setup

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


    org.apache.opennlp
    opennlp-tools
    1.8.4

Die neueste stabile Version befindet sich überMaven Central.

Einige Anwendungsfälle erfordern geschulte Modelle. Sie können vordefinierte Modellehere und detaillierte Informationen zu diesen Modellenhere herunterladen.

3. Satzerkennung

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

Sentence detection is about identifying the start and the end of a sentence, was normalerweise von der jeweiligen Sprache abhängt. Dies wird auch als "Satzgrenzen-Begriffsklärung" (SBD) bezeichnet.

In einigen Fällensentence detection is quite challenging because of the ambiguous nature of the period character. Ein Punkt kennzeichnet normalerweise das Ende eines Satzes, kann aber auch in einer E-Mail-Adresse, einer Abkürzung, einer Dezimalzahl und vielen anderen Stellen vorkommen.

Wie bei den meisten NLP-Aufgaben benötigen wir zur Satzerkennung ein trainiertes Modell als Eingabe, das sich voraussichtlich im Ordner/resourcesbefindet.

Um die Satzerkennung zu implementieren, laden wir das Modell und übergeben es an eine Instanz vonSentenceDetectorME. Dann übergeben wir einfach einen Text an die MethodesentDetect(), 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

Jetzt, da wir einen Textkörper in Sätze unterteilen können, können wir beginnen, einen Satz detaillierter zu analysieren.

The goal of tokenization is to divide a sentence into smaller parts called tokens. Normalerweise sind diese Zeichen Wörter, Zahlen oder Satzzeichen.

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

4.1. Verwenden vonTokenizerME

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

Als Nächstes erstellen wir eine Instanz vonTokenizerME mit dem geladenen Modell und verwenden die Methodetokenize(), um die Tokenisierung für alleString: 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("example is a Spring Resource.");

    assertThat(tokens).contains(
      "example", "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 geschulten Modell verwendet werden.

4.2. WhitespaceTokenizer

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

@Test
public void givenWhitespaceTokenizer_whenTokenize_thenTokensAreDetected()
  throws Exception {

    WhitespaceTokenizer tokenizer = WhitespaceTokenizer.INSTANCE;
    String[] tokens = tokenizer.tokenize("example is a Spring Resource.");

    assertThat(tokens)
      .contains("example", "is", "a", "Spring", "Resource.");
  }

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

4.3. SimpleTokenizer

Dieser Tokenizer ist etwas ausgefeilter alsWhitespaceTokenizer 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("example is a Spring Resource.");

    assertThat(tokens)
      .contains("example", "is", "a", "Spring", "Resource", ".");
  }

5. Named Entity Recognition

Nachdem wir die Tokenisierung verstanden haben, werfen wir einen Blick auf einen ersten Anwendungsfall, der auf einer erfolgreichen Tokenisierung basiert: die 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, Standorte und Organisationen. Wir müssen das Modell mitTokenNameFinderModel laden und in eine Instanz vonNameFinderME. aufteilen. Dann können wir die Methodefind() 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("[[person, [13..14) person, [20..21) person]");
}

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

6. Part-of-Speech-Tagging

Ein weiterer Anwendungsfall, für den eine Liste von Tokens als Eingabe erforderlich ist, ist die Spracherkennung.

A part-of-speech (POS) identifies the type of a word. OpenNLP verwendet die folgenden Tags für die verschiedenen Wortarten:

  • NN – Substantiv, Singular oder Masse

  • DT – Bestimmer

  • VB – Verb, Grundform

  • VBD – Verb, Vergangenheitsform

  • VBZ – Verb, dritte Person Singular vorhanden

  • IN – Präposition oder untergeordnete Konjunktion

  • NNP – Eigenname, Singular

  • TO –ist das Wort "bis"

  • JJ – Adjektiv

Dies sind die gleichen Tags wie in der Penn Tree Bank definiert. Eine vollständige Liste finden Sie unterthis list.

Ähnlich wie im NER-Beispiel laden wir das entsprechende Modell und verwenden dannPOSTaggerME und seine Methodetag() für eine Reihe von Token, um den Satz zu kennzeichnen:

@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", ".");
}

Die Methodetag() ordnet die Token einer Liste von POS-Tags zu. Das Ergebnis im Beispiel ist:

  1. "John" - NNP (Eigenname)

  2. "Hat" - VBZ (Verb)

  3. "A" - DT (Bestimmer)

  4. "Schwester" - NN

  5. “Named” - VBZ (Verb)

  6. „Penny“ - NNP (Eigenname)

  7. "." - Zeitraum

7. Lemmatisierung

Jetzt, da wir die Wortartinformationen der Token in einem Satz haben, können wir den Text noch weiter analysieren.

Lemmatization is the process of mapping a word form, die eine Zeitform, ein Geschlecht, eine Stimmung oder andere Informationen haben könnento the base form of the word – also called its “lemma”.

Ein Lemmatizer nimmt ein Token und sein Teil der Sprache als Eingabe und gibt das Lemma des Wortes zurück. Daher sollte der Satz vor der Lemmatisierung durch einen Tokenizer und einen POS-Tagger geleitet werden.

Apache OpenNLP bietet zwei Arten der Lemmatisierung:

  • Statistical – benötigt ein Lemmatisierermodell, das unter Verwendung von Trainingsdaten erstellt wurde, um das Lemma eines bestimmten Wortes zu finden

  • Dictionary-based – erfordert ein Wörterbuch, das alle gültigen Kombinationen eines Wortes, POS-Tags und das entsprechende Lemma enthält

Für die statistische Lemmatisierung müssen wir ein Modell trainieren, während wir für die Wörterbuch-Lemmatisierung nur eine Wörterbuchdatei wiethis one. benötigen

Schauen wir uns ein Codebeispiel mit einer Wörterbuchdatei an:

@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");
}

Wie wir sehen können, erhalten wir das Lemma für jedes Token. "O" bedeutet, dass das Lemma nicht bestimmt werden konnte, da das Wort ein Eigenname ist. Wir haben also kein Lemma für "John" und "Penny".

Aber wir haben die Deckspelzen für die anderen Wörter des Satzes identifiziert:

  • hat haben

  • a - a

  • Schwester Schwester

  • benannt - Name

8. Chunking

Teil-der-Sprache-Informationen sind auch wichtig beim Teilen - Teilen von Sätzen in grammatisch bedeutsame Wortgruppen wie Nomengruppen oder Verbgruppen.

Ähnlich wie zuvor markieren wir einen Satz und verwenden eine Teil-der-Sprache-Kennzeichnung für die Token, bevor wir diechunk()-Methode aufrufen:

@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");
}

Wie wir sehen können, erhalten wir eine Ausgabe für jedes Token vom Chunker. "B" steht für den Beginn eines Chunks, "I" für die Fortsetzung des Chunks und "O" für kein Chunks.

Wenn wir die Ausgabe aus unserem Beispiel analysieren, erhalten wir 6 Chunks:

  1. "Er" - Nominalphrase

  2. "Rechnet" - Verbalphrase

  3. "Das Leistungsbilanzdefizit" - Nominalphrase

  4. "Will narrow" - Verbalphrase

  5. "To" - Präposition

  6. "Nur 8 Milliarden" - Nominalphrase

9. Spracherkennung

Zusätzlich zu den bereits diskutierten Anwendungsfällen sindOpenNLP also provides a language detection API that allows to identify the language of a certain text. 

Für die Spracherkennung benötigen wir eine Trainingsdatei. Eine solche Datei enthält Zeilen mit Sätzen in einer bestimmten Sprache. Jede Zeile ist mit der richtigen Sprache markiert, um Eingaben für die Algorithmen für maschinelles Lernen zu liefern.

Eine Beispiel-Trainingsdatendatei zur Spracherkennung kannhere heruntergeladen werden.

Wir können die Trainingsdatendatei inLanguageDetectorSampleStream,laden und einige Trainingsdatenparameter definieren, ein Modell erstellen und dann das Modell verwenden, um die Sprache eines Textes zu erkennen:

@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)));
}

Das Ergebnis ist eine Liste der wahrscheinlichsten Sprachen mit einem Konfidenz-Score.

Und mit umfangreichen Modellen können wir mit dieser Art der Erkennung eine sehr viel höhere Genauigkeit erzielen.

5. Fazit

Wir haben hier viel von den interessanten Möglichkeiten von OpenNLP erforscht. Wir haben uns auf einige interessante Funktionen konzentriert, um NLP-Aufgaben wie Lemmatisierung, POS-Tagging, Tokenization, Satzerkennung, Spracherkennung und mehr durchzuführen.

Wie immer kann die vollständige Implementierung aller oben genanntenover on GitHub gefunden werden.