Введение в Apache OpenNLP

Введение в Apache OpenNLP

1. обзор

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

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

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

2. Maven Setup

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


    org.apache.opennlp
    opennlp-tools
    1.8.4

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

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

3. Обнаружение приговора

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

Sentence detection is about identifying the start and the end of a sentence, который обычно зависит от используемого языка. Это также называется «устранение неоднозначности границ» (SBD).

В некоторых случаяхsentence detection is quite challenging because of the ambiguous nature of the period character. Точка обычно обозначает конец предложения, но также может появляться в адресе электронной почты, сокращении, десятичной дроби и многих других местах.

Что касается большинства задач НЛП, для обнаружения предложений нам нужна обученная модель в качестве входных данных, которая, как мы ожидаем, будет находиться в папке/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. Tokenizing

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

The goal of tokenization is to divide a sentence into smaller parts called tokens. Обычно это токены, слова, цифры или знаки препинания.

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

4.1. ИспользуяTokenizerME

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

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

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

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

4.2. WhitespaceTokenizerс

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

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

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

4.3. SimpleTokenizerс

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

@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. Признание названного лица

Теперь, когда мы разобрались с токенизацией, давайте рассмотрим первый вариант использования, основанный на успешной токенизации: распознавание именованных сущностей (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 spans = Arrays.asList(nameFinderME.find(tokens));

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

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

6. Частичная речь

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

A part-of-speech (POS) identifies the type of a word. OpenNLP использует следующие теги для различных частей речи:

  • NN – существительное, единственное число или масса

  • ОпределительDT –

  • VB – глагол, основная форма

  • VBD – глагол, прошедшее время

  • VBZ – глагол, третье лицо единственного числа присутствует

  • IN – предлог или подчинительный союз

  • NNP – имя собственное, единственное число

  • TO – слово «к»

  • JJ – прилагательное

Это те же теги, что и в банке Penn Tree. Полный список см. Вthis list.

Как и в примере с 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. «Джон» - NNP (имя существительное)

  2. «Имеет» - VBZ (глагол)

  3. «А» - DT (определитель)

  4. «Сестра» - NN (существительное)

  5. «По имени» - VBZ (глагол)

  6. «Пенни» - NNP (имя собственное)

  7. «.» - период

7. лемматизации

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

Lemmatization is the process of mapping a word form, которые могут иметь напряжение, пол, настроение или другую информациюto the base form of the word – also called its “lemma”.

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

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

  • Statistical – нужна модель лемматизатора, построенная с использованием обучающих данных для поиска леммы данного слова.

  • Dictionary-based – требует словарь, который содержит все допустимые комбинации слова, теги POS и соответствующую лемму.

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

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

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

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

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

  • есть - есть

  • а - а

  • сестра - сестра

  • по имени

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. «Он» - существительное

  2. «Рассчитывает» - глагольная фраза

  3. «Дефицит текущего счета» - существительное

  4. «Сузит» - глагольная фраза

  5. «К» - предлог

  6. «Всего 8 миллиардов» - именная группа

9. Обнаружение языка

В дополнение к уже обсужденным вариантам использованияOpenNLP also provides a language detection API that allows to identify the language of a certain text. 

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

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

Мы можем загрузить файл обучающих данных в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. Мы сосредоточились на некоторых интересных функциях для выполнения задач НЛП, таких как лемматизация, POS-теги, токенизация, обнаружение предложений, определение языка и многое другое.

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