«1. Обзор

Apache OpenNLP — это библиотека Java для обработки естественного языка с открытым исходным кодом.

Он имеет API для таких случаев использования, как распознавание именованных объектов, определение предложений, тегирование POS и токенизация.

В этом уроке мы рассмотрим, как использовать этот API для различных вариантов использования.

2. Установка Maven

Во-первых, нам нужно добавить основную зависимость в наш pom.xml:

<dependency>
    <groupId>org.apache.opennlp</groupId>
    <artifactId>opennlp-tools</artifactId>
    <version>1.8.4</version>
</dependency>

Последнюю стабильную версию можно найти на Maven Central.

Для некоторых вариантов использования требуются обученные модели. Вы можете скачать готовые модели здесь и подробную информацию об этих моделях здесь.

3. Обнаружение предложения

Давайте начнем с понимания того, что такое предложение.

Обнаружение предложений заключается в определении начала и конца предложения, что обычно зависит от используемого языка. Это также называется «устранение неоднозначности границ предложения» (SBD).

В некоторых случаях определение предложения довольно сложно из-за неоднозначной природы символа точки. Точка обычно обозначает конец предложения, но также может появляться в адресе электронной почты, аббревиатуре, десятичной дроби и многих других местах.

Как и в большинстве задач NLP, для обнаружения предложений нам нужна обученная модель в качестве входных данных, которая, как мы ожидаем, будет находиться в папке /resources.

Чтобы реализовать обнаружение предложений, мы загружаем модель и передаем ее экземпляру SentenceDetectorME. Затем мы просто передаем текст в метод sentDetect(), чтобы разделить его по границам предложения:

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

Примечание: суффикс «ME» используется во многих именах классов в Apache OpenNLP и представляет алгоритм, который на основе «Максимальной энтропии».

4. Токенизация

Теперь, когда мы можем разделить корпус текста на предложения, мы можем приступить к более подробному анализу предложения.

Цель токенизации — разделить предложение на более мелкие части, называемые токенами. Обычно эти токены представляют собой слова, цифры или знаки препинания.

В OpenNLP доступно три типа токенизаторов.


4.1. Использование TokenizerME

В этом случае нам сначала нужно загрузить модель. Мы можем скачать файл модели отсюда, поместить его в папку /resources и загрузить оттуда.

Далее мы создадим экземпляр TokenizerME, используя загруженную модель, и используем метод tokenize() для выполнения токенизации любой строки:

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

Как мы видим, токенизатор идентифицировал все слова и символ точки как отдельные токены. Этот токенизатор также можно использовать с настраиваемой обученной моделью.

4.2. WhitespaceTokenizer

Как следует из названия, этот токенизатор просто разбивает предложение на токены, используя пробельные символы в качестве разделителей:

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

Мы видим, что предложение было разделено пробелами, и, следовательно, мы получаем «Ресурс». € (с символом точки в конце) в виде одного токена вместо двух разных токенов для слова «Ресурс» и символа точки.

4.3. SimpleTokenizer

Этот токенизатор немного сложнее, чем WhitespaceTokenizer, и разбивает предложение на слова, цифры и знаки препинания. Это поведение по умолчанию и не требует какой-либо модели:

@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. Распознавание именованных объектов

Теперь, когда мы поняли токенизацию, давайте рассмотрим первый вариант использования, основанный на успешной токенизации: named распознавание объектов (NER).

Целью NER является поиск именованных объектов, таких как люди, местоположения, организации и другие именованные объекты в заданном тексте.

OpenNLP использует предопределенные модели для имен людей, даты и времени, местоположений и организаций. Нам нужно загрузить модель с помощью TokenNameFinderModel и передать ее экземпляру NameFinderME. Затем мы можем использовать метод find() для поиска именованных сущностей в заданном тексте:

@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<Span> spans = Arrays.asList(nameFinderME.find(tokens));

    assertThat(spans.toString())
      .isEqualTo("[[0..1) person, [13..14) person, [20..21) person]");
}

Как видно из утверждения, результатом является список объектов Span, содержащих начальный и конечный индексы токенов, которые составить в тексте названные сущности.

6. Маркировка частями речи

Еще один вариант использования, для которого в качестве входных данных требуется список токенов, — это теги частей речи.

«Часть речи (POS) определяет тип слова. OpenNLP использует следующие теги для различных частей речи:

    NN — существительное, единственное число или масса DT — определитель VB — глагол, основная форма VBD — глагол, прошедшее время VBZ — «глагол, настоящее третьего лица единственного числа IN — предлог или подчинительный союз NNP — имя собственное, единственное число TO — слово «to» JJ — прилагательное

Это те же теги, что и в Penn Берега деревьев. Полный список см. в этом списке.

Как и в примере с NER, мы загружаем соответствующую модель, а затем используем POSTaggerME и его метод tag() для набора токенов, чтобы пометить предложение:

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

Метод tag() отображает токены в список POS-тегов. Результат в примере:

  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. Лемматизация

Теперь, когда у нас есть информация о частях речи токенов в предложении, мы можем еще больше проанализировать текст.

Лемматизация — это процесс сопоставления словоформы, которая может иметь время, род, наклонение или другую информацию, с базовой формой слова, также называемой «леммой».

Лемматизатор принимает токен и его часть речи в качестве входных данных и возвращает лемму слова. Следовательно, перед лемматизацией предложение должно пройти через токенизатор и POS-теггер.

Apache OpenNLP обеспечивает два типа лемматизации:

    Статистическая — требуется модель лемматизатора, построенная с использованием обучающих данных для поиска леммы заданного слова. На основе словаря — требуется словарь, который содержит все допустимые комбинации слово, теги POS и соответствующая лемма

Для статистической лемматизации нам нужно обучить модель, тогда как для лемматизации словаря нам просто нужен файл словаря, подобный этому.

Давайте посмотрим на пример кода, использующий файл словаря:

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

Как мы видим, мы получаем лемму для каждой лексемы. «О» указывает на то, что лемма не может быть определена, поскольку слово является именем собственным. Итак, у нас нет леммы для «Джон» и «Пенни».

Но мы идентифицировали леммы для других слов предложения:

    has – have a – a сестра – сестра по имени – name

8. Разделение на части

Часть Речевая информация также важна для фрагментации — разделения предложений на грамматически значимые группы слов, такие как группы существительных или группы глаголов.

Как и раньше, мы токенизируем предложение и используем теги частей речи для токенов перед вызовом метода chunk():

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

Как мы видим, мы получаем вывод для каждого токена из чанкер. «B» представляет собой начало фрагмента, «I» представляет собой продолжение фрагмента, а «O» представляет отсутствие фрагмента.

Анализируя вывод нашего примера, мы получаем 6 фрагментов:

  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. Определение языка

В дополнение к уже рассмотренным вариантам использования OpenNLP также предоставляет API для определения языка, который позволяет идентифицировать язык определенного текста. .

Для определения языка нам нужен файл обучающих данных. Такой файл содержит строки с предложениями на определенном языке. Каждая строка помечена правильным языком, чтобы обеспечить входные данные для алгоритмов машинного обучения.

Образец файла обучающих данных для определения языка можно скачать здесь.

Мы можем загрузить файл обучающих данных в LanguageDetectorSampleStream, определить некоторые параметры обучающих данных, создать модель, а затем использовать модель для определения языка текста:

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

Результатом является список наиболее вероятные языки вместе с оценкой достоверности.

И с богатыми моделями мы можем достичь очень высокой точности с этим типом обнаружения.

5. Заключение

Мы многое изучили здесь из интересных возможностей OpenNLP. Мы сосредоточились на некоторых интересных функциях для выполнения задач NLP, таких как лемматизация, тегирование POS, токенизация, определение предложений, определение языка и многое другое.

Как всегда, полную реализацию всего вышеперечисленного можно найти на GitHub.