Introducere în Apache OpenNLP

1. Prezentare generală

Apache OpenNLP este o bibliotecă Java de procesare a limbajului natural open source.

Dispune de un API pentru cazuri de utilizare, cum ar fi recunoașterea entității numite, detectarea sentințelor, etichetarea POS și tokenizarea.

În acest tutorial, vom arunca o privire asupra modului de utilizare a acestui API pentru diferite cazuri de utilizare.

2. Configurare Maven

În primul rând, trebuie să adăugăm dependența principală la pom.xml :

 org.apache.opennlp opennlp-tools 1.8.4 

Cea mai recentă versiune stabilă poate fi găsită pe Maven Central.

Unele cazuri de utilizare au nevoie de modele instruite. Puteți descărca modele predefinite aici și informații detaliate despre aceste modele aici.

3. Detectarea frazelor

Să începem cu înțelegerea a ceea ce este o propoziție.

Detectarea frazelor se referă la identificarea începutului și a sfârșitului unei propoziții , care depinde de obicei de limba în cauză. Aceasta este, de asemenea, numită „Dezambiguizarea limitelor frazelor” (SBD).

În unele cazuri, detectarea frazelor este destul de provocatoare din cauza naturii ambigue a caracterului de perioadă . O perioadă denotă de obicei sfârșitul unei propoziții, dar poate apărea și într-o adresă de e-mail, o abreviere, o zecimală și multe alte locuri.

În ceea ce privește majoritatea sarcinilor NLP, pentru detectarea propozițiilor, avem nevoie de un model instruit ca intrare, pe care ne așteptăm să îl avem în folderul / resources .

Pentru a implementa detectarea propozițiilor, încărcăm modelul și îl trecem într-o instanță SentenceDetectorME . Apoi, trecem pur și simplu un text în metoda sentDetect () pentru a-l împărți la limitele propoziției:

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

Notă:sufixul „ME” este utilizat în multe nume de clase în Apache OpenNLP și reprezintă un algoritm care se bazează pe „Entropia maximă”.

4. Tokenizarea

Acum că putem împărți un corpus de text în propoziții, putem începe să analizăm o propoziție mai detaliat.

Scopul tokenizării este împărțirea unei propoziții în părți mai mici numite jetoane . De obicei, aceste jetoane sunt cuvinte, numere sau semne de punctuație.

Există trei tipuri de tokenizatoare disponibile în OpenNLP.

4.1. Folosind TokenizerME

În acest caz, trebuie mai întâi să încărcăm modelul. Putem descărca fișierul model de aici, îl putem pune în folderul / resources și îl putem încărca de acolo.

Apoi, vom crea o instanță de TokenizerME folosind modelul încărcat și vom folosi metoda tokenize () pentru a efectua tokenizarea pe orice șir:

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

După cum putem vedea, tokenizer-ul a identificat toate cuvintele și caracterul de perioadă ca jetoane separate. Acest tokenizer poate fi utilizat și cu un model personalizat.

4.2. WhitespaceTokenizer

După cum sugerează și numele, acest tokenizer împarte pur și simplu propoziția în jetoane folosind caractere de spațiu alb ca delimitatori:

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

Putem vedea că propoziția a fost împărțită prin spații albe și, prin urmare, obținem „Resursă”. (cu caracterul de punct la sfârșit) ca un singur jeton în loc de două jetoane diferite pentru cuvântul „Resursă” și caracterul de punct.

4.3. SimpleTokenizer

Acest tokenizer este puțin mai sofisticat decât WhitespaceTokenizer și împarte propoziția în cuvinte, numere și semne de punctuație. Este comportamentul implicit și nu necesită niciun model:

@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. Recunoașterea entității denumite

Acum că am înțeles tokenizarea, să aruncăm o privire la un prim caz de utilizare care se bazează pe tokenizarea cu succes: recunoașterea entității denumite (NER)

Scopul NER este de a găsi entități numite, cum ar fi persoane, locații, organizații și alte lucruri numite într-un text dat.

OpenNLP utilizează modele predefinite pentru numele persoanelor, data și ora, locațiile și organizațiile. Trebuie să încărcăm modelul folosind TokenNameFinderModel șitreceți-l într-o instanță a NameFinderME. Apoi putem folosi metoda find () pentru a găsi entități numite într-un text dat:

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

După cum putem vedea în afirmație, rezultatul este o listă de obiecte Span care conțin indicii de început și de sfârșit ai jetoanelor care compun entități numite în text.

6. Etichetarea parțială a vorbirii

Un alt caz de utilizare care necesită o listă de jetoane ca intrare este etichetarea parțială a vorbirii.

O parte din vorbire (POS) identifică tipul unui cuvânt. OpenNLP utilizează următoarele etichete pentru diferitele părți ale vorbirii:

  • NN - substantiv, singular sau masă
  • 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

Am explorat multe aici, din capacitățile interesante ale OpenNLP. Ne-am concentrat pe câteva caracteristici interesante pentru a efectua sarcini NLP, cum ar fi lematizarea, etichetarea POS, Tokenizarea, detectarea frazelor, detectarea limbajului și multe altele.

Ca întotdeauna, implementarea completă a tuturor celor de mai sus poate fi găsită pe GitHub.