Введение
Мы в компании создаем сервис, который позволяет автоматически создавать, управлять и безопасно хранить лицензионные соглашения и прочие договоры между фрилансерами и их клиентами.
Для решения это задачи я опробовал десятки решений в области обработки естественного языка, в том числе решения с открытым кодом и хотел бы поделиться опытом работы с open source Python — библиотеками для распознавания именованных сущностей.
Распознавание именованных сущностей
Несколько слов о самой проблеме. Named Entity Recognition (NER) — это направление технологии обработки человеческого языка, программная реализация которой позволяет находить в речи и тексте опредмеченные категории слов и словосочетаний. Сначала это были географические наименования, имена людей, организаций, адреса, однако в настоящее время это понятие сильной расширилось и с помощью NER мы ищем в тексте относительные и абсолютные даты, числа, номера и т.д.
Выявление именованных сущностей — это «ворота» в человеческий язык, оно позволяет выявлять и обрабатывать намерения человека, устанавливать связи слов в его речи и реальным миром.
Языковое неравенство
Для начала, хотел бы обратить внимание на очевидное неравенство в программных решениях для разных языков. Так, большинство разработок (в том числе созданные российскими программистами) работают с английским языком. Найти готовые модели для бахаса, хинди или арабского — задача неблагодарная.
Европейские языки худо-бедно представлены в наиболее популярных библиотеках, языки африки не существуют в современном Natural Language Processing в принципе. Между тем, по своему опыту знаю, что африканский континент — это огромный и богатый рынок, и такое отношение это, скорее всего, инерция рынка.
Для русского языка есть несколько вызывающих удивление своим качеством решений, однако, за ними не чувствуется такой коммерческой мощи и академического потенциала как для развитых библиотек «построенных» на обработке английского языка.
Текст для обработки
Я взял несколько предложений из разных источников, и соединил их в несколько гипнотический текст, чтобы протестировать насколько хорошо справятся со своей задачей выбранные библиотеки.
english_text = ''' I want a person available 7 days and with prompt response all most every time. Only Indian freelancer need I need PHP developer who have strong experience in Laravel and Codeigniter framework for daily 4 hours. I need this work by Monday 27th Jan. should be free from plagiarism .
Need SAP FICO consultant for support project needs to be work on 6 months on FI AREAWe. Want a same site to be created as the same as this https://www.facebook.com/?ref=logo, please check the site before contacting to me and i want this site to be ready in 10 days. They will be ready at noon tomorrow .'''
russian_text = '''Власти Москвы выделили 110 млрд рублей на поддержку населения, системы здравоохранения и городского хозяйства. Об этом сообщается на сайте мэра столицы https://www.sobyanin.ru/ в пятницу, 1 мая. По адресу Алтуфьевское шоссе д.51 (основной вид разрешенного использования: производственная деятельность, склады) размещен МПЗ? Подпоручик Киже управляя автомобилем ВАЗ2107 перевозил автомат АК47 с целью ограбления банка ВТБ24, как следует из записей.
Взыскать c индивидуального предпринимателя Иванова Костантипа Петровича дата рождения 10 января 1970 года, проживающего по адресу город Санкт-Петербург, ул. Крузенштерна, дом 5/1А 8 000 (восемь тысяч) рублей 00 копеек гос. пошлины в пользу бюджета РФ Жители требуют незамедлительной остановки МПЗ и его вывода из района. Решение было принято по поручению мэра города Сергея Собянина в связи с ограничениями из-за коронавируса.'''
Библиотека NLTK
NLTK это классическая библиотека для обработки естественного языка, она проста в использовании, не требует длительного изучения и выполняет 99% задач, которые могут возникнуть при решении студенческих задач.
import nltk
nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
nltk.download('maxent_ne_chunker')
nltk.download('words')
for sent in nltk.sent_tokenize(english_text):
for chunk in nltk.ne_chunk(nltk.pos_tag(nltk.word_tokenize(sent))):
if hasattr(chunk, 'label'):
print(chunk)
Output:
(GPE Indian/JJ)
(ORGANIZATION PHP/NNP)
(GPE Laravel/NNP)
(PERSON Need/NNP)
(ORGANIZATION SAP/NNP)
(ORGANIZATION FI/NNP)
Как мы видим, NLTK неплохо справился со своей задачей, однако для того, чтобы сделать результат более «богатым» нам придется обучать свой собственный tagger (или выбрать другой из достаточно широкого списка). Но, стоит ли это делать в 2020 году если есть более простые решения?
Stanford CoreNLP
Один из способов расширить возможности NLTK это использовать вместе с классической Python библиотекой классическую Java библиотеку от Stanford CoreNLP. Качество улучшается значительно, при сравнительно низких требованиях.
from nltk.tag.stanford import StanfordNERTagger
jar = "stanford-ner-2015-04-20/stanford-ner-3.5.2.jar"
model = "stanford-ner-2015-04-20/classifiers/"
st_3class = StanfordNERTagger(model + "english.all.3class.distsim.crf.ser.gz", jar, encoding='utf8')
st_4class = StanfordNERTagger(model + "english.conll.4class.distsim.crf.ser.gz", jar, encoding='utf8')
st_7class = StanfordNERTagger(model + "english.muc.7class.distsim.crf.ser.gz", jar, encoding='utf8')
for i in [st_3class.tag(english_text.split()), st_4class.tag(english_text.split()), st_7class.tag(english_text.split())]:
for b in i:
if b[1] != 'O':
print(b)
Output:
('PHP', 'ORGANIZATION')
('Laravel', 'LOCATION')
('Indian', 'MISC')
('PHP', 'ORGANIZATION')
('Laravel', 'LOCATION')
('Codeigniter', 'PERSON')
('SAP', 'ORGANIZATION')
('FICO', 'ORGANIZATION')
('PHP', 'ORGANIZATION')
('Laravel', 'LOCATION')
('Monday', 'DATE')
('27th', 'DATE')
('Jan.', 'DATE')
('SAP', 'ORGANIZATION')
Как мы видим, качество выдачи улучшилось значительно, и теперь, с учетом скорости работы и простоты использования очевидно, что для несложных задач NLTK вполне пригодна и в промышленной разработке.
Spacy
Spacy это Python-библиотека с открытым исходным кодом для обработки естественного языка, она издается под лицензией MIT (!), ее создали и развивают Мэтью Ганнибал и Инес Монтани, основатели компании-разработчика Explosion.
Как правило, каждый, кто сталкивается с необходимостью решения каких-то задач для обработки естественного языка, рано или поздно узнает об этой библиотеке. Большинство функций доступно «из коробки», разработчики заботятся о том, чтобы библиотека была удобна в использовании.
Space предлагает 18 меток (tags) которыми отмечаются именованные сущности, равно как и простой способ дообучить свою собственную модель. Добавьте сюда отличную документацию, огромное сообщество и хорошую поддержку — и станет понятно, почему это решение стало таким популярным в последние пару лет.
import spacy
model_sp = en_core_web_lg.load()
for ent in model_sp(english_text).ents:
print(ent.text.strip(), ent.label_)
Output:
7 days DATE
New York GPE
Indian NORP
Laravel LOC
Codeigniter NORP
4 hours TIME
Monday 27th Jan. DATE
FICO ORG
6 months DATE
10 days DATE
noon TIME
tomorrow DATE
Iceland GPE
Как видим, результат намного лучше, а код существенно проще и понятней. Минусы работы — большой вес моделей, медленная работа, относительно нелогичные «метки», отсутствие моделей для многих языков, в том числе русского (хотя есть мультиязычные модели).
Flair
Flair предлагает гораздо более глубокое погружение в предметную область, библиотека создана, фактически, для решения исследовательских задач, документация неплохая, но с некоторыми провалами, есть интеграция с большим количеством других библиотек, понятный, логичный и читаемый код.
У библиотеки развитое сообщество, причем не только ориентированное на английский язык, благодаря большому количеству доступных моделей Flair существенно более демократичен в выборе языков чем Spacy.
from flair.models import SequenceTagger
tagger = SequenceTagger.load('ner')
from flair.data import Sentence
s = Sentence(english_text)
tagger.predict(s)
for entity in s.get_spans('ner'):
print(entity)
Output:
Span [6,7]: "7 days" [− Labels: DATE (0.9329)]
Span [17]: "Indian" [− Labels: NORP (0.9994)]
Span [35,36]: "4 hours." [− Labels: TIME (0.7594)]
Span [42,43,44]: "Monday 27th Jan." [− Labels: DATE (0.9109)]
Span [53]: "FICO" [− Labels: ORG (0.6987)]
Span [63,64]: "6 months" [− Labels: DATE (0.9412)]
Span [98,99]: "10 days." [− Labels: DATE (0.9320)]
Span [105,106]: "noon tomorrow" [− Labels: TIME (0.8667)]
Как видите, претренированная модель отработала не лучшим образом. Однако, мало кто использует Flair «из коробки» — это прежде всего библиотека для создания своих инструментов.
То же самое, с оговорками, можно сказать и о следующей библиотеке.
DeepPavlov
DeepPavlov это библиотека с открытым исходным кодом, построенная TensorFlow и Keras.
Разработчики предполагают использование системы преимущественно для «диалоговых» систем, чат-ботов и т.д., но библиотека превосходно подходит и для решения исследовательских задач. Использовать ее «в продакшн» без серьезной работы по «кастомизации» и «допиливанию» решения — задача, похоже, даже не предполагаемая создателями из МФТИ.
Странный и нелогичный подход к архитектуре кода, противоречащий дзен Python, тем не менее приносит хорошие результаты, если потратить достаточно времени, чтобы с ним разобраться.
from deeppavlov import configs, build_model
from deeppavlov import build_model, configs
ner_model = build_model(configs.ner.ner_ontonotes_bert, download=True)
result = ner_model([english_text])
for i in range(len(result[0][0])):
if result [1][0][i] != 'O':
print(result[0][0][i], result[1][0][i])
Output:
7 B-DATE
days I-DATE
Indian B-NORP
Laravel B-PRODUCT
Codeigniter B-PRODUCT
daily B-DATE
4 B-TIME
hours I-TIME
Monday B-DATE
27th I-DATE
Jan I-DATE
6 B-DATE
months I-DATE
FI B-PRODUCT
AREAWe I-PRODUCT
10 B-DATE
days I-DATE
noon B-TIME
tomorrow B-DATE
Результат предсказуемый, понятный, подробный и один из лучших. Сама модель также может использоваться напрямую в Hugging Face Transformers, что снимает, во многом, претензии к архитектуре кода.
deepmipt/ner
Это, по сути, библиотека с которой начинался Deep Pavlov. Она может использоваться, чтобы понять направление мысли разработчиков и прогресс, который был ими достигнут.
import ner
example = russian_text
def deepmint_ner(text):
extractor = ner.Extractor()
for m in extractor(text):
print(m)
deepmint_ner(example)
Output:
Match(tokens=[Token(span=(7, 13), text='Москвы')], span=Span(start=7, end=13), type='LOC')
Match(tokens=[Token(span=(492, 499), text='Иванова')], span=Span(start=492, end=499), type='PER')
Match(tokens=[Token(span=(511, 520), text='Петровича'), Token(span=(521, 525), text='дата')], span=Span(start=511, end=525), type='PER')
Match(tokens=[Token(span=(591, 600), text='Петербург')], span=Span(start=591, end=600), type='LOC')
Match(tokens=[Token(span=(814, 820), text='Сергея'), Token(span=(821, 829), text='Собянина')], span=Span(start=814, end=829), type='PER')
Polyglot
Одна из старейших библиотек, быстрая работа и большое количество поддерживаемых языков делают ее по прежнему популярной. С другой стороны вирусная GPLv3 license не позволяют ее использовать в коммерческой разработке в полную силу.
from polyglot.text import Text
for ent in Text(english_text).entities:
print(ent[0],ent.tag)
Output:
Laravel I-LOC
SAP I-ORG
FI I-ORG
И для русского языка:
!polyglot download embeddings2.ru ner2.ru
for ent in Text(russian_text).entities:
print(ent[0],ent.tag)
Output:
ВТБ24 I-ORG
Иванова I-PER
Санкт I-LOC
Крузенштерна I-PER
РФ I-ORG
Сергея I-PER
Результат не самый хороший, но скорость работы и хорошая поддержка позволяют улучшить его, если приложить усилия.
AdaptNLP
Еще одна новая библиотека с крайне низким порогом входа для исследователя.
AdaptNLP позволяет пользователям, начиная от студентов и заканчивая опытными дата-инженерами, использовать современные модели NLP и методики обучения.
Библиотека построена поверх популярных библиотек Flair и Hugging Face Transformers.
from adaptnlp import EasyTokenTagger
tagger = EasyTokenTagger()
sentences = tagger.tag_text(
text = english_text, model_name_or_path = "ner-ontonotes"
)
spans = sentences[0].get_spans("ner")
for sen in sentences:
for entity in sen.get_spans("ner"):
print(entity)
Output:
DATE-span [6,7]: "7 days"
NORP-span [18]: "Indian"
PRODUCT-span [30]: "Laravel"
TIME-span [35,36,37]: "daily 4 hours"
DATE-span [44,45,46]: "Monday 27th Jan."
ORG-span [55]: "FICO"
DATE-span [65,66]: "6 months"
DATE-span [108,109]: "10 days"
TIME-span [116,117]: "noon tomorrow"
Результат приемлемый, однако библиотека позволяет использовать самые разные модели для выполнения задачи, и он может быть многократно улучшен если приложить силы (но зачем, если есть непосредственно Flair и Hugging Face Transformers).
Тем не менее, простота, большой список выполняемых задач и хорошая архитектура, а также планомерные усилия разработчиков позволяют надеяться, что у библиотеки есть будущее.
Stanza
Stanza от StanfordNlp — это подарок разработчикам в 2020 году от университета Стенфорда. То, что не хватало Spacy — мультиязычность, глубокое погружение в язык вместе с простотой использования.
Если сообщество поддержит эту библиотеку — у нее есть все шансы стать одной из самых популярных.
import stanza
stanza.download('en')
def stanza_nlp(text):
nlp = stanza.Pipeline(lang='en', processors='tokenize,ner')
doc = nlp(text)
print(*[f'entity: {ent.text}ttype: {ent.type}' for sent in doc.sentences for ent in sent.ents], sep='n')
stanza_nlp(english_text)
Output:
entity: 7 days type: DATE
entity: Indian type: NORP
entity: Laravel type: ORG
entity: Codeigniter type: PRODUCT
entity: daily 4 hours type: TIME
entity: Monday 27th Jan. type: DATE
entity: SAP type: ORG
entity: FICO type: ORG
entity: 6 months type: DATE
entity: FI AREAWe type: ORG
entity: 10 days type: DATE
entity: noon tomorrow type: TIME
И для русского языка:
import stanza
stanza.download('ru')
def stanza_nlp_ru(text):
nlp = stanza.Pipeline(lang='ru', processors='tokenize,ner')
doc = nlp(text)
print(*[f'entity: {ent.text}ttype: {ent.type}' for sent in doc.sentences for ent in sent.ents], sep='n')
stanza_nlp_ru(russian_text)
Output:
2020-05-15 08:01:18 INFO: Use device: cpu
2020-05-15 08:01:18 INFO: Loading: tokenize
2020-05-15 08:01:18 INFO: Loading: ner
2020-05-15 08:01:19 INFO: Done loading processors!
entity: Москвы type: LOC
entity: Алтуфьевское шоссе type: LOC
entity: Киже type: PER
entity: ВАЗ2107 type: MISC
entity: АК47 type: MISC
entity: ВТБ24 type: MISC
entity: Иванова Костантипа Петровича type: PER
entity: Санкт-Петербург type: LOC
entity: ул. Крузенштерна type: LOC
entity: РФ type: LOC
entity: МПЗ type: LOC
entity: Сергея Собянина type: PER
Быстрая работа, красивый код, хороший результат.
AllenNLP
Библиотека для исследовательской работы, построенная на PyTorch/
С одной стороны — простая архитектура и быстрая скорость работы, с другой стороны, разработчики постоянно изменяют что-то в архитектуре, что сказывается на работе бибилиотеки в целом.
from allennlp.predictors.predictor import Predictor
import allennlp_models.ner.crf_tagger
predictor = Predictor.from_path("https://storage.googleapis.com/allennlp-public-models/ner-model-2020.02.10.tar.gz")
allen_result = predictor.predict(
sentence=english_text
)
for i in zip(allen_result['tags'], allen_result['words']):
if (i[0]) != 'O':
print(i)
Output:
('U-MISC', 'Indian')
('U-ORG', 'PHP')
('U-MISC', 'Laravel')
('U-MISC', 'Codeigniter')
('B-ORG', 'SAP')
('L-ORG', 'FICO')
Модуль работает быстро, но результат неприемлемо скудный.
HanLP
HanLP — это одна из открытых библиотек от разработчиков из Китая. Умный, проработанный, активный проект, который, как мне кажется, найдет свою нишу и за пределами «Поднебесной».
Библиотека NLP для исследователей и компаний создана на TensorFlow 2.0.
HanLP поставляется с заранее подготовленными моделями для разных языков, включая английский, китайский и многие другие.
Единственная проблема — качество выдачи «скачет» после каждого обновления библиотеки.
recognizer = hanlp.load(hanlp.pretrained.ner.MSRA_NER_BERT_BASE_ZH)
recognizer([list('上海华安工业(集团)公司董事长谭旭光和秘书张晚霞来到美国纽约现代艺术博物馆参观。'),
list('萨哈夫说,伊拉克将同联合国销毁伊拉克大规模杀伤性武器特别委员会继续保持合作。')])
Output:
[[('上海华安工业(集团)公司', 'NT', 0, 12), ('谭旭光', 'NR', 15, 18),
('张晚霞', 'NR', 21, 24),
('美国', 'NS', 26, 28),
('纽约现代艺术博物馆', 'NS', 28, 37)],
[('萨哈夫', 'NR', 0, 3),
('伊拉克', 'NS', 5, 8),
('联合国销毁伊拉克大规模杀伤性武器特别委员会', 'NT', 10, 31)]]
import hanlp
tokenizer = hanlp.utils.rules.tokenize_english
testing = tokenizer('Need SAP FICO consultant for support project needs to be work on 6 months on FI AREAWe')
recognizer = hanlp.load(hanlp.pretrained.ner.CONLL03_NER_BERT_BASE_UNCASED_EN)
recognizer(testing)
Output:
[('SAP FICO', 'ORG', 1, 3)]
Для английского языка результат нестабилен, но, это решается с использованием токинайзера от NLTK.
PullEnti
Библиотека C# для NER на русском языке. В 2016 году она заняла первое место на конкурсе factRuEval-2016. В 2018 году автор портировал код на Java и Python.
Наверно, самое симпатичное решение для русского языка.
Быстро, глубоко, со вниманием к деталям. Решение rule based, что естественным образом ограничивает его развитие, однако его автономность, скорость и результаты позволяют надеяться на развитие проекта.
Есть python-wrapper для библиотеки, хотя он и выглядит «заброшенным».
from pullenti_wrapper.processor import (
Processor,
MONEY,
URI,
PHONE,
DATE,
KEYWORD,
DEFINITION,
DENOMINATION,
MEASURE,
BANK,
GEO,
ADDRESS,
ORGANIZATION,
PERSON,
MAIL,
TRANSPORT,
DECREE,
INSTRUMENT,
TITLEPAGE,
BOOKLINK,
BUSINESS,
NAMEDENTITY,
WEAPON,
)
processor = Processor([PERSON, ORGANIZATION, GEO, DATE, MONEY])
text = russian_text
result = processor(text)
result.graph
Output:
Natasha
«Наташа», это, похоже, один из главных проектов NLP для русского языка. Он имеет долгую историю, и начинался с rule based решения, которое развивалось через популярный Yargy Parser, и сейчас решает основные задачи NLP для русского языка: токенизацию, сегментация предложения, лемматизация, нормализация фразы, синтаксический разбор, NER-тегирование, извлечение фактов.
from natasha import (
Segmenter,
MorphVocab,
NewsEmbedding,
NewsMorphTagger,
NewsSyntaxParser,
NewsNERTagger,
PER,
NamesExtractor,
Doc
)
segmenter = Segmenter()
morph_vocab = MorphVocab()
emb = NewsEmbedding()
morph_tagger = NewsMorphTagger(emb)
syntax_parser = NewsSyntaxParser(emb)
ner_tagger = NewsNERTagger(emb)
names_extractor = NamesExtractor(morph_vocab)
doc = Doc(russian_text)
Output:
Власти Москвы выделили 110 млрд рублей на поддержку населения, системы
LOC───
здравоохранения и городского хозяйства. Об этом сообщается на сайте
мэра столицы https://www.sobyanin.ru/ в пятницу, 1 мая. По адресу
Алтуфьевское шоссе д.51 (основной вид разрешенного использования:
LOC───────────────
производственная деятельность, склады) размещен МПЗ? Подпоручик Киже
ORG PER────────────
управляя автомобилем ВАЗ2107 перевозил автомат АК47 с целью ограбления
банка ВТБ24, как следует из записей.
ORG──
Взыскать c индивидуального предпринимателя Иванова Костантипа
PER────────────────
Петровича дата рождения 10 января 1970 года, проживающего по адресу
─────────
город Санкт-Петербург, ул. Крузенштерна, дом 5/1А 8 000 (восемь тысяч)
LOC──────────── PER─────────
рублей 00 копеей госпошлины в пользу бюджета РФ Жители требуют
LO
незамедлительной остановки МПЗ и его вывода из района. Решение было
принято по поручению мэра города Сергея Собянина в связи с
PER────────────
ограничениями из-за коронавируса.
Результат, к сожалению, не стабилен, в отличии от настраиваемых правил Yargy Parser от того же разработчика, тем не менее, проект активно развивается, и показывает достойный результат при коммерческом использовании.
ner-d
Последний модуль — это частный проект, не имеющий особой популярности, построенный поверх Spacy и Thinc и, тем не менее, заслуживающий внимания подходом выбранным к архитектуре (акцент на простоте использования).
from nerd import ner
doc_nerd_d = ner.name(english_text)
text_label = [(X.text, X.label_) for X in doc_nerd_d]
print(text_label)
Output:
[('7 days', 'DATE'), ('Indian', 'NORP'), ('PHP', 'ORG'), ('Laravel', 'GPE'), ('daily 4 hours', 'DATE'), ('Monday 27th Jan.', 'DATE'), ('Need SAP FICO', 'PERSON'), ('6 months', 'DATE'), ('10 days', 'DATE'), ('noon', 'TIME'), ('tomorrow', 'DATE')]
Из всех проектов самым «сбалансированным» и удобным, с приемлемыми результатами и удобством использования мне кажется является Stanza от StanfordNlp — работа большинства языков «из коробки», качественная академическая проработка и поддержка научного сообщества самого университета делает проект самым перспективным, на мой взгляд.
В следующий раз я поделюсь опытом о работе с «закрытыми» решениями и предлагаемым API для обработки естественного языка.
Весь код доступен Google Colab
Автор: Александр Мазалов