Інструменти для Natural Language Understanding: поради, особливості роботи, та українська мова в NLP

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Привіт! Мене звати Ян Бутельський, я NLP-фахівець в сфері розробки діалогових систем. Вже сьомий рік допомагаю штучному інтелекту зрозуміти людський.

Як і обіцяв, продовжую серію публікацій про NLP в контексті розробки діалогової системи. Головна мета — описати свій власний досвід роботи з NLU-модулем та детально проаналізувати наявні Python бібліотеки (SPACY, STANZA, FLAIR) для якісної та швидкої розробки NLU-модуля. З першою частиною цієї теми ви можете ознайомитись в цій публікації.

Особливості та переваги Spacy

Почнемо з бібліотеки Spacy, яка зарекомендувала себе як незамінний інструмент в арсеналі багатьох інженерів та науковців. Одразу скажу, що використовував її як основний пайплайн на декількох NLP-проєктах. Системи, які були побудовані на її базі обробляють сотні тисяч текстових документів в день, без memory leaks та ексепшенів :) З власного досвіду можу сказати, що її основними перевагами є швидкодія, оптимізація роботи під CPU, а не тільки GPU, надзвичайно велике ком’юніті, ну і звісно простий та зручний інтерфейс, про який я розповім вам в цій статті з прикладами.

На малюнку зображені основні елементи пайплайну Spacy.

Першим елементом в пайплайні є неструктурований текст. Для початку роботи треба лише встановити Spacy та завантажити необхідну мовну модель. Перелік доступних моделей можна знайти за цим посиланням.

!pip install spacy
!python -m spacy download en_core_web_lg

import spacy

nlp = spacy.load("en_core_web_lg")

text = ("When Sebastian Thrun started working on self-driving cars at Google in 2007, few people outside of the company took him seriously.I can tell you very senior CEOs of major American car companies would shake my hand and turn away because I wasn't worth talking to, said Thrun, in an interview with Recode earlier this week.")
doc = nlp(text)

Закодовуючи текст на одній з мовних моделей англійської мови, ми одразу отримаємо доступ до всіх доступних NLU-інструментів Spacy, за допомогою яких можна почати розробку власного NLU-модуля для діалогової системи. Одним з найбільш показових є модуль NER, який допоможе знайти сутності, щоб розпізнати і заповнити необхідні слоти для розпізнавання інтенту.

На прикладі нижче показно, які сутності витягнуло Spacy з прикладу тексту вище. Як на мене, досить непоганий результат.

Стрічка коду нижче дає перелік всіх доступних сутностей для цієї мовної моделі.

nlp.pipe_labels['ner']
['CARDINAL', 'DATE', 'EVENT', 'FAC', 'GPE', 'LANGUAGE', 'LAW', 'LOC', 'MONEY', 'NORP', 'ORDINAL', 'ORG', 'PERCENT', 'PERSON', 'PRODUCT', 'QUANTITY', 'TIME', 'WORK_OF_ART']

Також хочеться звернути увагу на функціонал, який допоможе визначити подібність речень, або слів за допомогою векторного простору, який надає Spacy з коробки.

doc1 = nlp("I like salty fries and hamburgers.")
doc2 = nlp("Fast food tastes very good.")

# Similarity of two documents
print(doc1, "<->", doc2, doc1.similarity(doc2))
# Similarity of tokens and spans
french_fries = doc1[2:4]
burgers = doc1[5]
print(french_fries, "<->", burgers, french_fries.similarity(burgers))

Точність кожного з модулей Spacy — дивіться тут.

Варто додати, що Spacy дозволяє глибоко кастомізувати кожний зі своїх елементів пайплайну, зокрема модуль NER, тобто це дозволяє тренувати свої кастомні сутності.

Загалом раджу всім спробувати Spacy для роботи з текстами та уважно дослідити всі властивості Spacy.doc, Spacy.span, та Spacy.token об’єктів.

Властивості Stanza

Наступна NLU-бібліотека, на яку хотілося б звернути увагу — це Stanza. Як на мене, Stanza та Spacy досить подібні за своєю філософією та NLU можливостями для використання однієї з них у розробці діалогової системи.

На малюнку нижче — зображення основні елементи пайплайну Stanza.

Однією з переваг Stanza над Spacy є набагато більша кількість мовних моделей для різних мов світу — понад 60 мов. Зокрема серед них є українська мова, що мене дуже тішить :) На прикладі української мови спробуємо розібратись, яку лінгвістичну інформацію надає нам Stanza.

На прикладі слова «народився» видно, наскільки повною є лінгвістична інформація про кожне вибране слово з прикладу. Також можна дуже просто визначити сутності, які розпізнала українська мовна модель.

text = "Ян Бутельський народився у місті Львів"
import stanza
stanza.download('uk')
nlp = stanza.Pipeline('uk')
doc = nlp(text)
print(doc)
print(doc.entities)




{
      "id": 3,
      "text": "народився",
      "lemma": "народитися",
      "upos": "VERB",
      "xpos": "Vmeis-sm",
      "feats": "Aspect=Perf|Gender=Masc|Mood=Ind|Number=Sing|Tense=Past|VerbForm=Fin",
      "head": 0,
      "deprel": "root",
      "start_char": 15,
      "end_char": 24,
      "ner": "O"
    },

[{
  "text": "Бутельський",
  "type": "PERS",
  "start_char": 3,
  "end_char": 14
}, {
  "text": "Львів",
  "type": "LOC",
  "start_char": 33,
  "end_char": 38
}]

Приклади вище свідчать про те, що Stanza може стати досить непоганою альтернативою Spacy для розробки власно NLU-модуля. Відповідь на запитання «Що вибрати?» досить неочевидна, тому раджу всім спробувати обидві та детально ознайомитись з документацією в посиланнях, що наведені вище. З власного досвіду хочу сказати, що активніше використовую Spacy, напевно більше сподобався вебсайт :)

Приклад використання Flair

Останнє, про що хотілося б поговорити в цій статті, це можливість особисто натренувати модель зі специфічними сутностями, які не є наявними в пайплайнах бібліотек Spacy та Stanza. Звичайно, кожна з цих бібліотек має функціонал для цього, проте він не до кінця оптимальний та гнучкий. Водночас, якщо маєте бажання, можна спробувати різні рішення, адже, можливо, саме та чи інша бібліотека якраз підійде для вирішення ваших завдань!

Щоб ще більше розширити кругозір в різномаїтті NLU-бібліотек, потрібно згадати про Flair. Особисто для мене вона стала одним з найкращих інструментів для розробки класифікаторів на базі Pytorch. Щоправда, Flair — це надзвичайно зручна та гнучка обгортка навколо Pytorch. Вона дозволяє з коробки використовувати широку кількість мовних моделей, які можна легко та швидко дотренувати своїми власними даними. Вам на вибір дають можливість роботи як і рекурентними мережами, так і троформерами, зокрема з BERT.

Приклад коду нижче показує, що інтерфейс Flair також дуже простий та зрозумілий в роботі. Для прикладу використаємо той самий приклад тексту, що й і для Spacy.

from flair.data import Sentence
from flair.models import SequenceTagger

# make a sentence
sentence = Sentence(text)

# load the NER tagger
tagger = SequenceTagger.load('ner')

# run NER over sentence
tagger.predict(sentence)

print(sentence)
print('The following NER tags are found:')

# iterate over entities and print
for entity in sentence.get_spans('ner'):
   print(entity)
 
The following NER tags are found:
Span [2,3]: "Sebastian Thrun"   [− Labels: PER (0.9996)]
Span [10]: "Google"   [− Labels: ORG (0.9988)]
Span [31]: "American"   [− Labels: MISC (0.9965)]
Span [50]: "Thrun"   [− Labels: PER (0.9999)]
Span [56]: "Recode"   [− Labels: ORG (0.9848)]

Отже, результат трошки гірший, ніж у Spacy, проте цікавою властивістю є впевненість моделі стосовно кожної сутності, в той же час, однією з беззаперечних переваг саме Spacy перед Flair, швидкодія — Spacy від 3 до 5 разів швидша, коли робить tagger.predict. Це досить значна перевага.

Тренувальний процес Flair

Наступне, на чому хотілося б зосередити увагу, це пайплайн тренування Flair моделі. Давайте спочатку виберемо середовища для тренування. Це можна робити локально на CPU і GPU. З власного досвіду, Pytorch більше любить GPU :) Як каже мій власний досвід, оптимальним вибором є якийсь клауд-сервіс на кшталт Colab. Це дуже зручний застосунок — швидко, зручно, все зберігається в хмарі, можна тренувати ледь не з браузера телефону — СПРОБУЙТЕ.

Приклад нижче зображує основні кроки тренувального процесу.

Перший крок — це, звісно, так званий лейблінг данних — дуже довгий та клопіткий процес, оскільки від нього найбільше залежить якість роботи майбутньої моделі. Опис процесу лейблінгу заслуговує на окрему публікацію, тому детально на ньому зупинятись не буду. Лише скажу, що класичний підхід — це BIO-анотація, трохи більше деталей описано в моїй попередній публікації. У прикладі використовуються вже заздалегідь промарковані дані, раджу розібратись з форматом без поспіху. Головна його особливість — те, що кожне слово-токін промарковане необхідною лейбою, вказаною з нового рядка, а кожне нове речення розділене \n.

Отже, наша майбутня модель буде намагатись класифікувати кожний токін відповідною лейбою, визначити, де початок та кінець сутності та речення. З прикладу нижче легко розібратись з наступними процесами пайплайну, такими як використання вже готових імбедінгів слів, які, до речі, Flair дозволяє комбінувати, але не забувайте, що це може добряче збільшити час тренування. Для прикладу беремо LSTM-модель з досить стандартними праметрами, тут вже хто хоче може поекспериментувати..... Почекавши хвилин 30-40, якщо це Colab, отримуємо нашу модель.

from flair.datasets import ColumnCorpus
from flair.embeddings import WordEmbeddings, FlairEmbeddings, StackedEmbeddings
from flair.models import SequenceTagger
from flair.trainers import ModelTrainer


# 1. get the corpus
columns = {0: "text", 1: "pos", 2: "np", 3: "ner"}
data_folder = '/content/drive/MyDrive/conll2003'
corpus = ColumnCorpus(data_folder, 
columns,                 train_file='/content/drive/MyDrive/conll2003/train.txt',                   test_file='/content/drive/MyDrive/conll2003/test.txt')
print(corpus)

# 2. What label do we want to predict?
label_type = 'ner'
# 3. make the label dictionary from the corpus




label_dict = corpus.make_label_dictionary(label_type=label_type)
print(label_dict)

# 4. initialize embedding stack with Flair and GloVe
embedding_types = [
   WordEmbeddings('glove'),
   FlairEmbeddings('news-forward'),
   FlairEmbeddings('news-backward'),
]

embeddings = StackedEmbeddings(embeddings=embedding_types)

# 5. initialize sequence tagger
tagger = SequenceTagger(hidden_size=256,
                       embeddings=embeddings,
                       tag_dictionary=label_dict,
                       tag_type=label_type,
                       use_crf=True)

# 6. initialize trainer
trainer = ModelTrainer(tagger, corpus)

# 7. start training
trainer.train('resources/taggers/sota-ner-flair',
             learning_rate=0.1,
             mini_batch_size=32,
             max_epochs=5,
             embeddings_storage_mode="gpu")

Як на мене, результат досить непоганий, якщо ви все правильно зробили, F1 вийде більше 96%.

I am Yan <B-PER> Buteslkyy <I-PER> I work for New <B-ORG> Fire <I-ORG> Partners <I-ORG> company .

Заключне слово

Ну що ж, ми познайомились з одним з елементів пайплайну діалогової системи, який відповідає за розуміння природної мови. Вибрані приклади бібліотек я особисто використовував, тому можу гарантувати їхню надійність та ефективність. Кожна з них пройшла бойове хрещення в продакшені. Також сподіваюсь, що виконав побажання читачів в коментарях, які просили більше прикладів з реальної імплементації. Обіцяю продовжити розповідати про наступні елементи пайплайну діалогової системи. Сподіваюсь, що в кінці наша діалогова система зможе відповідати на фразу «Слава Україні!».

Сподобалась стаття? Натискай «Подобається» внизу. Це допоможе автору виграти подарунок у програмі #ПишуНаDOU

👍ПодобаєтьсяСподобалось4
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Проблема в тому, що люди не є лінгвістами, тому використовують мову забувши спитати лінгвістів, як правильно.

Ще більша проблема — що люди використовують мову маючи дууууже обмежену оперативну пам′ять, яка і пам′яттю не є у комп′ютерному розумінні цього терміну. За моделлю доступу це ближче до кешу.

Сумарно можна сказати, що так, інструментарій аналізу натуральних мов досить розвинений, навряд чи там щось революційно нове з′явиться. Проблема в тому, що результат аналізу носієм мови і результат аналізу фреймворком є абсолютно різним. Тобто, модель аналізу не здатна отримати повідомлення, лише виокремити те, що добре знає. Приблизно на рівні 3-річної дитини. І вище вона ніколи не сягне, якщо не застосовувати радикально більш складних механізмів.

Власне, нейромережі стикнулися із тим самим бар′єром: розуміючи деталі, вони не можуть зрозуміти що саме за інформацію вони отримали.

Можу сказати простими словами: левова доля інформації є сміттям. Допоки модель не здатна розуміти потоку сміття, вона не здатна отримати інформацію з мови.

Завдання NLP перетворити неструктуровані дані якими є текст в структуровані і нічого більше. За останні роки відбувся знайчний прогрес завдяки трансформерам.

В тому-то й справа, що структури обрані ті, в які легше структурувати, а не ті, які дають розуміння інформації. Це нагадує Grammarly, які абсолютно не розуміючи сказаного, дають поради як це сформулювати «правильно». Тим не менше, обравши цільовою аудиторією невпевнених у власних знаннях людей, вони «чесно заробили» цією ілюзією чимало грошей.

Дякую за статтю.
1. чи не оптималніше робити trainer.fine_tune базової моделі замість тренування з 0?
2. з вашого досвіду тренування типів сутностей, яка мінімальна кількість маркованих даних на клас?

Привіт, радий, що було цікаво, звісно trainer.fine_tune робити оптимальніше, але хотів показати процес from scratch. Стосовно мінімальної кількості маркованих даних, з мого досвіду 500-600 достатньо для всіх пайпланйнів, що я описав, за умови, якщо викоритовуєш вже готові імбедінги для слів.

500-600 на клас? гігантська робота для лейбйлінг тім.

так на клас, це в ідеальному світі, але так роботи з анотації дуже багато, зі свого боку можу порадити: prodi.gy або labelstud.io

ліпше людей порадьте хто акуратно лейбелить :)

Підписатись на коментарі