Перейти к основному содержимому

Введение в Apache OpenNLP

· 9 мин. чтения

1. Обзор

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

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

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

2. Настройка Мавена

Во-первых, нам нужно добавить основную зависимость в наш 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 google@gmail.com.";

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

Примечание: ** ** суффикс «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("ForEach is a Spring Resource.");

assertThat(tokens).contains(
"ForEach", "is", "a", "Spring", "Resource", ".");
}

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

4.2. Токенизатор пробелов

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

@Test
public void givenWhitespaceTokenizer_whenTokenize_thenTokensAreDetected()
throws Exception {

WhitespaceTokenizer tokenizer = WhitespaceTokenizer.INSTANCE;
String[] tokens = tokenizer.tokenize("ForEach is a Spring Resource.");

assertThat(tokens)
.contains("ForEach", "is", "a", "Spring", "Resource.");
}

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

4.3. SimpleTokenizer

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

@Test
public void givenSimpleTokenizer_whenTokenize_thenTokensAreDetected()
throws Exception {

SimpleTokenizer tokenizer = SimpleTokenizer.INSTANCE;
String[] tokens = tokenizer
.tokenize("ForEach is a Spring Resource.");

assertThat(tokens)
.contains("ForEach", "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<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 – существительное в единственном числе или масса
  • ДТ – определитель
  • VB – глагол, основная форма
  • VBD – глагол в прошедшем времени
  • VBZ – глагол в третьем лице единственного числа настоящего времени
  • IN – предлог или подчинительный союз
  • NNP – имя собственное, единственное число
  • ТО – слово «к»
  • ЖЖ - прилагательное

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

Как и в примере с 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. «имеет» – ВБЗ (глагол)
  3. «а» – ДТ (определитель)
  4. «сестра» – NN (существительное)
  5. «название» – ВБЗ (глагол)
  6. «Пенни» — ** ** NNP (имя собственное)
  7. “.” - период

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

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

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

  • есть - есть
  • а - а
  • сестра - сестра
  • названный - имя

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 также предоставляет 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 .