Introduction à Apache OpenNLP

Introduction à Apache OpenNLP

1. Vue d'ensemble

Apache OpenNLP est une bibliothèque Java open source de Natural Language Processing.

Il dispose d'une API pour les cas d'utilisation tels que la reconnaissance d'entités nommées, la détection de phrases, le balisage POS et la création de Token.

Dans ce tutoriel, nous allons voir comment utiliser cette API pour différents cas d'utilisation.

2. Maven Setup

Tout d'abord, nous devons ajouter la dépendance principale à nospom.xml:


    org.apache.opennlp
    opennlp-tools
    1.8.4

La dernière version stable peut être trouvée surMaven Central.

Certains cas d'utilisation nécessitent des modèles formés. Vous pouvez télécharger des modèles prédéfinishere et des informations détaillées sur ces modèleshere.

3. Détection de peine

Commençons par comprendre ce qu'est une phrase.

Sentence detection is about identifying the start and the end of a sentence, qui dépend généralement de la langue utilisée. C'est ce qu'on appelle aussi «l'homonymie de limite de phrase» (SBD).

Dans certains cas,sentence detection is quite challenging because of the ambiguous nature of the period character. Une période indique généralement la fin d'une phrase, mais peut également figurer dans une adresse électronique, une abréviation, une décimale et de nombreux autres endroits.

Comme pour la plupart des tâches PNL, pour la détection de phrases, nous avons besoin d'un modèle entraîné comme entrée, que nous prévoyons de résider dans le dossier/resources.

Pour implémenter la détection de phrases, nous chargeons le modèle et le passons dans une instance deSentenceDetectorME. Ensuite, nous passons simplement un texte dans la méthodesentDetect() pour le diviser aux limites de la phrase:

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

Remarque: tle suffixe «ME» est utilisé dans de nombreux noms de classe dans Apache OpenNLP et représente un algorithme basé sur «Entropie maximale».

4. Tokenisation

Maintenant que nous pouvons diviser un corpus de texte en phrases, nous pouvons commencer à analyser une phrase de manière plus détaillée.

The goal of tokenization is to divide a sentence into smaller parts called tokens. Généralement, ces jetons sont des mots, des chiffres ou des signes de ponctuation.

Il existe trois types de jetons disponibles dans OpenNLP.

4.1. Utilisation deTokenizerME

Dans ce cas, nous devons d’abord charger le modèle. Nous pouvons télécharger le fichier modèle depuishere, le mettre dans le dossier/resources et le charger à partir de là.

Ensuite, nous allons créer une instance deTokenizerME à l'aide du modèle chargé, et utiliser la méthodetokenize() pour effectuer la tokenisation sur toutString:

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

Comme nous pouvons le constater, le tokenizer a identifié tous les mots et le caractère de période en tant que jetons distincts. Ce tokenizer peut également être utilisé avec un modèle personnalisé.

4.2. WhitespaceTokenizer

Comme son nom l'indique, ce tokenizer divise simplement la phrase en jetons en utilisant des caractères d'espacement comme délimiteurs:

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

Nous pouvons voir que la phrase a été scindée par des espaces et nous obtenons donc «Ressource» (avec le caractère point à la fin) sous la forme d'un seul jeton au lieu de deux jetons différents pour le mot «Ressource» et le caractère point.

4.3. SimpleTokenizer

Ce tokenizer est un peu plus sophistiqué queWhitespaceTokenizer et divise la phrase en mots, nombres et signes de ponctuation. Il s'agit du comportement par défaut et ne nécessite aucun modèle:

@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. Reconnaissance d'entité nommée

Maintenant que nous avons compris la tokenisation, examinons un premier cas d'utilisation basé sur une tokenisation réussie: la reconnaissance d'entités nommées (NER).

Le but de NER est de trouver des entités nommées telles que des personnes, des lieux, des organisations et d'autres choses nommées dans un texte donné.

OpenNLP utilise des modèles prédéfinis pour les noms de personnes, la date et l'heure, les emplacements et les organisations. Nous devons charger le modèle en utilisantTokenNameFinderModel et le spass dans une instance deNameFinderME. Ensuite, nous pouvons utiliser la méthodefind() pour trouver des entités nommées dans un texte donné:

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

Comme nous pouvons le voir dans l'assertion, le résultat est une liste d'objetsSpan contenant les indices de début et de fin des jetons qui composent les entités nommées dans le texte.

6. Marquage partiel

Le balisage partiel de la parole est un autre cas d’utilisation nécessitant une liste de jetons.

A part-of-speech (POS) identifies the type of a word. OpenNLP utilise les balises suivantes pour les différentes parties du discours:

  • NN – nom, singulier ou masse

  • déterminantDT –

  • VerbeVB –, forme de base

  • VerbeVBD –, passé

  • VerbeVBZ –, troisième personne du singulier présente

  • Préposition ou conjonction subordonnéeIN –

  • NNP – nom propre, singulier

  • TO – le mot «à»

  • JJ – adjectif

Ce sont les mêmes balises que définies dans la Penn Tree Bank. Pour une liste complète, veuillez vous référer àthis list.

Semblable à l'exemple NER, nous chargeons le modèle approprié puis utilisonsPOSTaggerME et sa méthodetag() sur un ensemble de jetons pour baliser la phrase:

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

La méthodetag() mappe les jetons dans une liste de balises POS. Le résultat dans l'exemple est:

  1. “John” - NNP (nom propre)

  2. “A” - VBZ (verbe)

  3. “A” - DT (déterminant)

  4. “Soeur” - NN (nom)

  5. “Nommé” - VBZ (verbe)

  6. «Penny» - NNP (nom propre)

  7. "." - période

7. Lemmatisation

Maintenant que nous avons les informations de partie du discours des jetons dans une phrase, nous pouvons analyser le texte encore plus loin.

Lemmatization is the process of mapping a word form qui peuvent avoir un temps, un sexe, une humeur ou d'autres informationsto the base form of the word – also called its “lemma”.

Un lemmatiseur prend un jeton et sa balise de partie de discours comme entrée et renvoie le lemme du mot. Par conséquent, avant la lemmatisation, la phrase devrait être transmise par un tokenizer et un étiqueteur POS.

Apache OpenNLP fournit deux types de lemmatisation:

  • Statistical – a besoin d'un modèle de lemmatiseur construit à l'aide de données d'apprentissage pour trouver le lemme d'un mot donné

  • Dictionary-based – nécessite un dictionnaire contenant toutes les combinaisons valides d'un mot, les balises POS et le lemme correspondant

Pour la lemmatisation statistique, nous devons former un modèle, alors que pour la lemmatisation du dictionnaire, nous avons juste besoin d'un fichier dictionnaire commethis one.

Examinons un exemple de code utilisant un fichier de dictionnaire:

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

Comme nous pouvons le voir, nous obtenons le lemme pour chaque jeton. “O” indique que le lemme n'a pas pu être déterminé car le mot est un nom propre. Donc, nous n’avons pas de lemme pour «John» et «Penny».

Mais nous avons identifié les lemmes pour les autres mots de la phrase:

  • a - a

  • un - un

  • soeur - soeur

  • nommé - nom

8. Chunking

Les informations sur une partie du discours sont également essentielles pour diviser des phrases en groupes de mots ayant une signification grammaticale, tels que des groupes de noms ou des groupes de verbes.

Comme auparavant, nous tokenisons une phrase et utilisons le balisage d'une partie du discours sur les jetons avant d'appeler la méthodechunk():

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

Comme nous pouvons le constater, nous obtenons une sortie pour chaque jeton à partir du chunker. "B" représente le début d'un morceau, "I" représente la continuation du morceau et "O" ne représente aucun morceau.

En analysant la sortie de notre exemple, nous obtenons 6 morceaux:

  1. “Il” - phrase nominale

  2. «Compte» - phrase du verbe

  3. “Le déficit du compte courant” - phrase nominale

  4. “Va rétrécir” - phrase verbale

  5. “To” - phrase de préposition

  6. “Seulement 8 milliards” - phrase nominale

9. Détection de la langue

En plus des cas d'utilisation déjà discutés,OpenNLP also provides a language detection API that allows to identify the language of a certain text. 

Pour la détection de la langue, nous avons besoin d’un fichier de données de formation. Un tel fichier contient des lignes avec des phrases dans une langue donnée. Chaque ligne est étiquetée avec le langage correct pour fournir une entrée aux algorithmes d’apprentissage automatique.

Un exemple de fichier de données d'apprentissage pour la détection de la langue peut être téléchargéhere.

Nous pouvons charger le fichier de données d'entraînement dans unLanguageDetectorSampleStream, définir certains paramètres de données d'entraînement, créer un modèle puis utiliser le modèle pour détecter la langue d'un texte:

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

Le résultat est une liste des langues les plus probables avec un score de confiance.

Et, avec les modèles riches, nous pouvons atteindre une précision très supérieure avec ce type de détection.

5. Conclusion

Nous avons beaucoup exploré ici, parmi les fonctionnalités intéressantes d’OpenNLP. Nous nous sommes concentrés sur certaines fonctionnalités intéressantes pour effectuer des tâches PNL telles que la lemmatisation, le marquage POS, la tokénisation, la détection de phrases, la détection de langue, etc.

Comme toujours, l'implémentation complète de tout ce qui précède peut être trouvéeover on GitHub.