Зачастую приходится работать с большими объемами документов, к примеру, исполнительными листами, заявлениями, договорами, из текстов которых нам необходимо извлечь весьма конкретную информацию: ФИО, даты рождения, наименования должности, паспортные данные, адрес, ИНН и наименование компаний, даты подписания документов и так далее. Всё это относится к задаче распознавания именованных сущностей (NER). Какие инструменты могут помочь нам в решении данной задачи для русского языка?

Пожалуй, первое что приходит в голову Data Scientist’у, когда речь идет о NLP или конкретно NER-задачах — это проекты DeepPavlov. Немного углубимся в данную тему, разберем все по порядку.

DeepPavlov — это фреймворк (open source), который помогает в разработке различных голосовых ботов, соответственно, решая различные NLP задачи.

На вход подается непредобработанный (регистры, знаки и т.д. сохранены) текст, а на выходе мы хотим увидеть, так называемые, спаны — фрагменты текста, с которыми уже можно работать (например, отнести к определенной категории).

Всю работу осуществляем в Colab. Установим Natasha (поддерживает Python версии 3.5 и выше):

!pip install natasha

Если говорить вкратце, то она умеет решать базовые (и не только базовые, но сегодня поговорим только о них) задачи NLP для русского языка, такие как: токенизация, лемматизация/нормализация фраз (или имен), определение частей речи и многое другое. 

Импортируем нужные нам модули:

from natasha import Segmenter, MorphVocab, NewsEmbedding, NewsMorphTagger, NewsSyntaxParser, NewsNERTagger,NamesExtractor, Doc

Есть мнение, что данного рода ф��еймворки хорошо работают с новостными статьями (что логично, ведь там очень часто встречаются именованные сущности). Давайте возьмем какую-нибудь хорошую новость из топа новостей Яндекса:

news = '9.12.2021 «Спартак» на выезде сыграл с польской «Легией» в матче заключительного тура группового этапа Лиги Европы.' \
'Красно-белые победили со счетом 1:0 благодаря голу Бакаева. Ранее в Москве спартаковцы уступили со счетом 0:1.' \
'На 98-й минуте матча в ворота спартаковцев был назначен пенальти. Вратарь красно-белых Селихов отразил 11-метровый.' \
'Эта победа гарантировала «Спартаку» попадание в плей-офф Лиги Европы.' 

Передадим текст с данной новостью в Doc(). Далее сегментируем наш текст (Natasha использует для этого еще одну свою разработку именуемую «Раздел», посмотрим на первые и последние 5 токенов:

segmenter = Segmenter()
footballNews.segment(segmenter)
for i in range(5): print(footballNews.tokens[i])
for i in range(5): print(footballNews.tokens[-5+i])

(Пример) out:
DocToken(stop=9, text='9.12.2021')

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

Перейдем к морфологии и разберем последнее предложение:

embedding = NewsEmbedding()
morphTagger = NewsMorphTagger(embedding)
footballNews.tag_morph(morphTagger)
footballNews.sents[0].morph.print()


(Пример) out:
Вратарь NOUN|Animacy=Anim|Case=Nom|Gender=Masc|Number=Sing
красно-белых ADJ|Case=Gen|Degree=Pos|Number=Plur
Селихов PROPN|Animacy=Anim|Case=Nom|Gender=Masc|Number=Sing

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

С лемматизацией (процессом приведения слова к нормальной форме) проблем также не обнаружено. Эта задача решается с помощью известной многим разработкой Яндекса Pymorphy:

morphVocab = MorphVocab()
for token in footballNews.tokens:
    token.lemmatize(morphVocab)
{_.text: _.lemma for _ in footballNews.tokens}
(Пример) out:
{'Бакаева': 'бакаев',
 'Вратарь': 'вратарь',
 'Европы': 'европа',
 'Красно-белые': 'красно-белый',
 'Легией': 'легия'}

Вполне неплохой результат, двигаемся дальше.

Давайте научимся добывать именованные сущности из текста:

footballNews.tag_ner(nerTagger)
footballNews.ner.print()

(Пример) out:
9.12.2021 «Спартак» на выезде сыграл с польской «Легией» в матче 
           ORG────                               ORG───          
заключительного тура группового этапа Лиги Европы.

Natasha для удобства подчеркивает именованную сущность для быстрого анализа текста глазами, для любителей работать в Jupyter Notebook есть возможность даже настроить «подсветку» сущностей. Natasha определила футбольные клубы к организациям, фамилии игроков к личностям, а город к локации. Результат положительный.

Вдогонку к лемматизации — можно нормализовать слова, которые не нормализованы в тексте:

for span in footballNews.spans:
    span.normalize(morphVocab)
{_.text: _.normal for _ in footballNews.spans if _.text != _.normal}
out:
{'Бакаева': 'Бакаев',
 'Легией': 'Легия',
 	 'Москве': 'Москва',
  'Спартаку': 'Спартак'}

На самом деле этот небольшой разбор лишь вершина айсберга возможностей, которые предоставляют нам разработчики DeepPavlov. Фреймворк покрывает значительную часть ad-hoc задач Data Scientist’а, многое отлично работает, как говорят, «из коробки». Проблемы могут возникнуть при обработке неструктурированных данных (��ДФ файлы), которые не сохраняют регистр, пунктуацию, пробелы, но это уже совсем другая история…

Еще одним интересным инструментом для решения задачи NER является библиотека SpaCy. SpaCy распознает различные типы именованных сущностей с помощью прогноза предобученных языковых моделей. Итак, импортируем библиотеку и посмотрим на что она способна.

import spacy

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

ru_model = spacy.load("ru_core_news_md")

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

Токенизация – это первое, что делает SpaCy с полученным текстом. При этом применяются правила, специфичные для языка текста. Проведем токенизацию первого предложения нашего текста.

vocab = {}
for token in doc:
    if token.text not in vocab.keys():
        vocab[token.text] = 1
    else:
        vocab[token.text] += 1
print(vocab)

(Пример) out:

Отлично, дата целостная, как и в результате работы Natasha. Также выводится количество вхождений токена в текст.

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

def lemmat(text):
    doc = rus_model(text)
    tokens = [token.lemma_ for token in doc]
    return " ".join(tokens)

(Пример) out:

Выведем из приятных новостей только те слова, которые изменились:

Получилось довольно неплохо, за исключением футбольного клуба Легии.

Именованные сущности извлекаются очень просто. Так же есть возможность подсветить их прямо в тексте с указанием типа, для этого воспользуемся модулем displacy, который входит в пакет spaCy.

from spacy import displacy
for ent in doc.ents:
    print(ent.text,ent.start_char, ent.end_char, ent.label_)  
displacy.render(doc, style = 'ent')

(Пример) out:

Отлично, результат совпадает с Natasha. Проведем морфологический анализ текста.

for ent in doc:
    print(ent.text,ent.tag_,ent.morph)

(Пример) out:

В теги выводятся характеристики слова в зависимости от части речи.

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