Introdução ao Apache OpenNLP

Introdução ao Apache OpenNLP

1. Visão geral

O Apache OpenNLP é uma biblioteca Java de processamento de linguagem natural de código aberto.

Possui uma API para casos de uso, como reconhecimento de entidade nomeada, detecção de sentença, marcação de POS e tokenização.

Neste tutorial, veremos como usar esta API para diferentes casos de uso.

2. Configuração do Maven

Primeiro, precisamos adicionar a dependência principal ao nossopom.xml:


    org.apache.opennlp
    opennlp-tools
    1.8.4

A última versão estável pode ser encontrada emMaven Central.

Alguns casos de uso precisam de modelos treinados. Você pode baixar modelos predefinidosheree informações detalhadas sobre esses modeloshere.

3. Detecção de sentenças

Vamos começar entendendo o que é uma frase.

Sentence detection is about identifying the start and the end of a sentence, que geralmente depende do idioma disponível. Isso também é chamado de "Desambiguação do limite de sentença" (SBD).

Em alguns casos,sentence detection is quite challenging because of the ambiguous nature of the period character. Um ponto geralmente indica o final de uma frase, mas também pode aparecer em um endereço de email, uma abreviação, um decimal e muitos outros lugares.

Quanto à maioria das tarefas de PNL, para detecção de frases, precisamos de um modelo treinado como entrada, que esperamos residir na pasta/resources.

Para implementar a detecção de sentenças, carregamos o modelo e o passamos para uma instância deSentenceDetectorME. Em seguida, simplesmente passamos um texto para o métodosentDetect() para dividi-lo nos limites da frase:

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

Nota: o sufixo “ME” é usado em muitos nomes de classes no Apache OpenNLP e representa um algoritmo baseado em “Entropia Máxima”.

4. Tokenização

Agora que podemos dividir um corpus de texto em frases, podemos começar a analisar uma frase com mais detalhes.

The goal of tokenization is to divide a sentence into smaller parts called tokens. Geralmente, esses tokens são palavras, números ou sinais de pontuação.

Existem três tipos de tokenizers disponíveis no OpenNLP.

4.1. UsandoTokenizerME

Nesse caso, primeiro precisamos carregar o modelo. Podemos baixar o arquivo de modelo dehere, colocá-lo na pasta/resources e carregá-lo de lá.

A seguir, criaremos uma instância deTokenizerME usando o modelo carregado e usaremos o métodotokenize() para realizar a tokenização em qualquerString:

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

Como podemos ver, o tokenizer identificou todas as palavras e o caractere de ponto como tokens separados. Esse tokenizador também pode ser usado com um modelo treinado personalizado.

4.2. WhitespaceTokenizer

Como o nome sugere, esse tokenizador simplesmente divide a sentença em tokens usando caracteres de espaço em branco como delimitadores:

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

Podemos ver que a frase foi dividida por espaços em branco e, portanto, obtemos "Recurso". (com o caractere de ponto final no final) como um token único em vez de dois tokens diferentes para a palavra "Recurso" e o caractere de ponto.

4.3. SimpleTokenizer

Este tokenizer é um pouco mais sofisticado do queWhitespaceTokenizere divide a frase em palavras, números e sinais de pontuação. É o comportamento padrão e não requer nenhum modelo:

@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. Reconhecimento de entidade nomeada

Agora que entendemos a tokenização, vamos dar uma olhada em um primeiro caso de uso que é baseado na tokenização bem-sucedida: reconhecimento de entidade nomeada (NER).

O objetivo do NER é encontrar entidades nomeadas como pessoas, locais, organizações e outras coisas nomeadas em um determinado texto.

O OpenNLP usa modelos predefinidos para nomes de pessoas, data e hora, locais e organizações. Precisamos carregar o modelo usandoTokenNameFinderModele passar para uma instância deNameFinderME. Então, podemos usar o métodofind() para encontrar entidades nomeadas em um determinado texto:

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

Como podemos ver na afirmação, o resultado é uma lista de objetosSpan contendo os índices inicial e final dos tokens que compõem as entidades nomeadas no texto.

6. Marcação de parte da fala

Outro caso de uso que precisa de uma lista de tokens como entrada é a marcação de parte do discurso.

A part-of-speech (POS) identifies the type of a word. OpenNLP usa as seguintes tags para as diferentes classes gramaticais:

  • NN – substantivo, singular ou massa

  • DT – determinante

  • VB – verbo, forma de base

  • VBD – verbo, pretérito

  • VBZ – verbo, terceira pessoa do singular presente

  • IN – preposição ou conjunção subordinada

  • NNP – substantivo próprio, singular

  • TO –a palavra “para”

  • JJ – adjetivo

Essas são as mesmas tags definidas no Penn Tree Bank. Para uma lista completa, consultethis list.

Semelhante ao exemplo NER, carregamos o modelo apropriado e, em seguida, usamosPOSTaggerMEe seu métodotag() em um conjunto de tokens para marcar a frase:

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

O métodotag() mapeia os tokens em uma lista de tags POS. O resultado no exemplo é:

  1. "John" - NNP (nome próprio)

  2. “Has” - VBZ (verbo)

  3. “A” - DT (determinante)

  4. "Sister" - NN (substantivo)

  5. “Named” - VBZ (verbo)

  6. “Penny” - NNP (nome próprio)

  7. "." - período

7. Lematização

Agora que temos as informações de parte do discurso dos tokens em uma frase, podemos analisar o texto ainda mais.

Lemmatization is the process of mapping a word form que pode ter tempo, gênero, humor ou outras informaçõesto the base form of the word – also called its “lemma”.

Um lematizador pega um token e sua tag de classe gramatical como entrada e retorna o lema da palavra. Portanto, antes da Lematização, a sentença deve ser passada através de um tokenizador e etiquetador POS.

O Apache OpenNLP fornece dois tipos de lematização:

  • Statistical – precisa de um modelo lematizador construído usando dados de treinamento para encontrar o lema de uma determinada palavra

  • Dictionary-based – requer um dicionário que contém todas as combinações válidas de uma palavra, tags POS e o lema correspondente

Para a lematização estatística, precisamos treinar um modelo, enquanto para a lematização de dicionário precisamos apenas de um arquivo de dicionário comothis one.

Vejamos um exemplo de código usando um arquivo de dicionário:

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

Como podemos ver, obtemos o lema de cada token. "O" indica que o lema não pôde ser determinado, pois a palavra é um nome próprio. Portanto, não temos um lema para "John" e "Penny".

Mas identificamos os lemas para as outras palavras da frase:

  • tem - tem

  • a - a

  • irmã - irmã

  • named - name

8. Chunking

Informações de parte do discurso também são essenciais para dividir as frases em grupos de palavras gramaticalmente significativas, como grupos de substantivos ou grupos de verbos.

Semelhante ao anterior, nós tokenizamos uma frase e usamos a marcação de classes gramaticais nos tokens antes de chamar o métodochunk():

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

Como podemos ver, obtemos uma saída para cada token do chunker. "B" representa o início de um pedaço, "I" representa a continuação do pedaço e "O" não representa nenhum pedaço.

Analisando a saída do nosso exemplo, obtemos 6 pedaços:

  1. "He" - frase substantivo

  2. “Conta” - frase verbal

  3. “Déficit em conta corrente” - frase substantivo

  4. "Vai estreitar" - frase verbal

  5. "To" - frase de preposição

  6. “Only 8 billion” - frase substantivo

9. Detecção de idioma

Além dos casos de uso já discutidos,OpenNLP also provides a language detection API that allows to identify the language of a certain text. 

Para detecção de idioma, precisamos de um arquivo de dados de treinamento. Esse arquivo contém linhas com frases em um determinado idioma. Cada linha é marcada com o idioma correto para fornecer entrada para os algoritmos de aprendizado de máquina.

Um arquivo de dados de treinamento de amostra para detecção de idioma pode ser baixadohere.

Podemos carregar o arquivo de dados de treinamento em umLanguageDetectorSampleStream, definir alguns parâmetros de dados de treinamento, criar um modelo e, em seguida, usar o modelo para detectar o idioma de um texto:

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

O resultado é uma lista dos idiomas mais prováveis, juntamente com uma pontuação de confiança.

E, com modelos sofisticados, podemos obter uma precisão muito maior com esse tipo de detecção.

5. Conclusão

Nós exploramos muito aqui, a partir dos recursos interessantes do OpenNLP. Focamos em alguns recursos interessantes para executar tarefas de PNL, como lematização, marcação de PDV, tokenização, detecção de sentenças, detecção de idioma e muito mais.

Como sempre, a implementação completa de tudo acima pode ser encontradaover on GitHub.