Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
1. Введение
Чисто теоретически, конечной задачей всей деятельности по созданию алгоритмов для обработки естественного языка (Natural Language Processing, NLP) является создание искусственного интеллекта (ИИ), который бы понимал человеческий язык, причем “понимал” в значении “осознавал смысл” (анализ текста) и “делал осмысленные высказывания” (синтез текста). Пока до этой цели ещё очень далеко - для распознавания живого языка потребуется дать агенту ИИ все огромные знания об окружающем мире, а также возможность взаимодействовать с ним, т.е. создать «действительно мыслящего» агента. Так что сейчас, в практической плоскости, под обработкой естественного языка понимаются различные алгоритмические методы для извлечения какой-либо полезной информации из текстовых данных.
Существует достаточно широкий круг практических задач где нужна обработка текстов на естественном языке:
машинный перевод текстов с иностранных языков,
автоматическое аннотирование текста (краткое содержание),
классификация текстов по категориям (спам/не-спам, рубрикация новостей, анализ тональности текста и пр.),
диалоговые системы (чат-боты, системы вопрос-ответ),
распознавание именованных сущностей (найти в тексте имена собственные людей, компаний, местоположений и т.п.).
Применительно к системам мониторинга ИТ-сервисов и инфраструктуры, да и бизнес-процессов, NLP-алгоритмы могут использоваться для решения задач по классификации текстовых данных и в создании различных диалоговых систем. В данной статье будут кратко описаны методы обработки естественного языка, которые используются в микросервисах AIOps платформы Monq для зонтичного ИТ-мониторинга, в частности для анализа поступающих в систему событий и логов.
2. Преобразование текста в числовые векторные представления
Математические алгоритмы работают с числами, поэтому, чтобы использовать математический аппарат для обработки и анализа текстовых данных, необходимо сначала преобразовать слова и предложения в числовые вектора, причём желательно с сохранением семантической связи и порядка слов, т.е.:
числовой вектор должен отображать содержание и структуру текста,
схожие по смыслу слова и предложения должны иметь близкие значения векторных представлений.
В настоящее время существует два основных подхода для перевода текста, точнее набора текстов (или, в NLP-терминологии - корпуса документов), в векторные представления:
тематическое моделирование текста - несколько видов статистических моделей для нахождения скрытых (латентных) в корпусе документов тем: латентно-семантический анализ (PLSA), латентное размещение Дирихле (LDA),
различные модели контекстного представления слов, основанные на дистрибутивной гипотезе: нейросетевые модели Word2Vec, GloVe, Doc2Vec, fastText и некоторые другие.
Векторные представления, получаемые на выходе этих алгоритмов, позволяют достаточно легко сравнивать тексты, искать между ними похожие, проводить категоризацию и кластеризацию текстов и т.п..
Лет 10-15 тому назад только эксперты могли участвовать в проектах по обработке естественного языка, поскольку это требовало серьезных знаний в области математики, машинного обучения и лингвистики. Теперь же для решения NLP-задач разработчики могут использовать множество готовых инструментов и библиотек. В частности, в языке Python довольно широкий спектр NLP-возможностей для обучения различных моделей предоставляет комбинация двух модулей: nltk и gensim - именно на них (но не только) основан NLP-функционал платформы Monq.
3. Предобработка текстовых данных для обучения моделей
Предварительная обработка текстовых данных является важным шагом в процессе построения различных NLP-моделей - здесь принцип GIGO (“garbage in, garbage out”) справедлив как нигде. Основные стадии предобработки текста включают в себя методы токенизации, методы нормализации (стемминг или лемматизация) и удаление стоп-слов. Сюда также часто относят методы выделения словосочетаний (в NLP-терминологии - n-грамм или коллокаций) и составление словаря токенов, но мы их выделяем в отдельный этап.
Токенизация - это разбиение текста на текстовые единицы, токены, которыми могут быть как отдельные слова, так и словосочетания и целые предложения. Документ, в этом контексте - это совокупность токенов, принадлежащих одной смысловой единице (например, предложение, абзац или параграф), а корпус - это общая совокупность (коллекция) всех документов. В процессе токенизации текст:
разбивается на предложения,
очищается от пунктуации,
приводится к нижнему регистру,
разбивается на токены (чаще всего слова, но иногда буквосочетания или слоги).
Нормализация - это приведение слов к единой морфологической форме, для чего используется либо стемминг - приведение слова к основе (“книжкой” - “книжк”, “читаю” - “чита”), либо лемматизация - приведение слова к начальной форме (“книжкой” - “книжка”, “читаю” - “читать”). Для русского языка лемматизация является более предпочтительной и, как правило, приходится использовать два разных алгоритма для лемматизации слов - отдельно для русского (в Python для этого можно использовать модуль pymorphy2) и английского языков.
Удаление из исходного текста стоп-слов - это выбрасывание из текста слов, которые не несут информативной нагрузки. К таким чаще всего относятся общеупотребительные слова, местоимения и служебные слова (предлоги, частицы, союзы и т.п.). В Python списки стоп-слов для разных языков есть в самом модуле nltk, а более полные наборы стоп-слов содержит специальный модуль stop-words - для полноты картины разные списки стоп-слов можно объединить. Довольно часто в списки стоп-слов также добавляют имена и отчества.
Вот относительно полный пример кода на Python для предварительной обработки коллекции текстов на русском и английском (на выходе получается список токенизированных документов):
import re
from nltk.tokenize.punkt import PunktSentenceTokenizer
sentTokenizer = PunktSentenceTokenizer()
from nltk.tokenize import RegexpTokenizer
tokenizer = RegexpTokenizer(r'\w+')
from nltk.stem.wordnet import WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
import pymorphy2
morph = pymorphy2.MorphAnalyzer()
import langid
from nltk.corpus import stopwords
from stop_words import get_stop_words
langid.set_languages(['en','ru'])
stopWordsEn=set().union(get_stop_words('en'), stopwords.words('english'))
stopWordsRu=set().union(get_stop_words('ru'), stopwords.words('russian'))
stopWords=list(set().union(stopWordsEn, stopWordsRu))
stopWords.sort()
textCollection=['Основные стадии предобработки текста включают в себя методы токенизации, методы нормализации и удаление стоп-слов.', 'Токенизация текста - это разбиение текста на текстовые единицы. В процессе токенизации текст сначала разбивается на предложения.', 'Tokenization in Python is the most primary step in any natural language processing program. ', 'We have imported re library and used "\w+" for picking up words from the expression.']
textCollTokens = []
for text in textCollection: ## loop over the collection of texts
sentList = [sent for sent in sentTokenizer.tokenize(text)]
tokens = [word for sent in sentList for word in tokenizer.tokenize(sent.lower())]
lemmedTokens=[]
for token in tokens:
if langid.classify(token)[0]=='en':
lemmedTokens.append(lemmatizer.lemmatize(token))
elif langid.classify(token)[0]=='ru':
lemmedTokens.append(morph.parse(token)[0].normal_form)
goodTokens = [token for token in lemmedTokens if not token in stopWords]
textCollTokens.append(goodTokens)
4. Выделение n-грамм и составление словаря токенов
Выделение в корпусе текстов устойчивых словосочетаний (n-грамм или коллокаций, например, “New York”, “Центральный Банк”, “обменный курс” и т.п.) и их использование как единичного токена в NLP-моделях является достаточно стандартным способом повысить качество таких моделей. Существует несколько алгоритмов для выделения коллокаций в коллекции текстов, основанных на подсчёте различных статистик совместной встречаемости слов в данной коллекции. Но мы не будем углубляться в анализ плюсов и минусов этих алгоритмов, а просто будем использовать методы выделения биграмм и триграмм, которые предоставляет модуль gensim:
from gensim.models.phrases import Phrases, Phraser
bigrams = Phrases(textCollTokens, min_count=1, threshold=5) ## finding bigrams in the collection
trigrams = Phrases(bigrams[textCollTokens], min_count=2, threshold=5) ## finding trigrams
bigramPhraser = Phraser(bigrams) ## setting up parser for bigrams
trigramPhraser = Phraser(trigrams) ## parser for trigrams
docCollTexts=[]
for doc in textCollTokens:
docCollTexts.append(trigramPhraser[bigramPhraser[doc]])
Финальной стадией предварительной обработки текстовых данных является составление словаря токенов (с учётом всех найденных n-грамм) для данной коллекции текстов. Как правило, чтобы попасть в словарь токен должен удовлетворять некоторым дополнительным критериям - фильтрация токенов для уменьшения “шума” и “фона” - в нашем случае (пример кода следует ниже):
токен должен встречаться во всей коллекции не менее определённого числа раз (параметр no_below),
токен должен встречаться не чаще чем в половине текстов из коллекции (параметр no_above).
from gensim import corpora
textCollDictionary = corpora.Dictionary(docCollTexts)
textCollDictionary.filter_extremes(no_below=1, no_above=0.5, keep_n=None)
Таким образом, после предварительной обработки коллекции текстов на выходе мы имеем список токенизированных (с учётом n-грамм) документов, словарь токенов для данного корпуса текстов, а также “обученные” парсеры биграмм и триграмм. Три последних объекта необходимо сохранить на диске для последующего использования, так как они являются важной составной частью создаваемой NLP-модели (словарь токенов для удобства сохраняется ещё и как текстовый файл):
bigramPhraser.save('bigramPhraser.pkl')
trigramPhraser.save('trigramPhraser.pkl')
textCollDictionary.save('textCollDictionary.pkl')
textCollDictionary.save_as_text('textCollDictionary.txt')
5. Тематическое моделирование методом латентного размещения Дирихле (LDA)
Как было сказано выше, один из подходов для перевода коллекции текстов в векторные представления - это тематическое моделирование. В своей платформе мы используем для этого метод латентного размещения Дирихле (LDA, Latent Dirichlet allocation). Мы не будем описывать в деталях механизмы обработки текста внутри LDA (оригинальная статья с описанием здесь), а попытаемся кратко изложить основные принципы работы метода:
каждый документ в коллекции описывается множеством латентных (скрытых) тем,
тема - совокупность слов с определёнными весами, или вероятностями (мультиноминальное вероятностное распределение на множестве слов заданного словаря),
документ - случайная независимая выборка слов (мешок слов), порождаемая латентным множеством тем,
строятся матрицы частоты использования слов внутри конкретного документа и среди документов во всей коллекции,
используя сэмплирование по Гиббсу - алгоритм для генерации выборки совместного распределения (по Дирихле) множества случайных величин, подбираются такие матрицы распределений слов по темам и тем по документам, которые наиболее "правильно" отображают заданный корпус текстов,
на выходе алгоритм LDA каждому документу из коллекции сопоставляет тематический вектор с относительным весом каждой из выделенных тем в его содержании.
Суть метода хорошо иллюстрируется нижеследующей картинкой, которую, условно говоря, можно было бы получить после прогона метода LDA на корпусе текстов русских сказок. На выходе алгоритм LDA сопоставил бы “Сказке о рыбаке и рыбке” тематический вектор T=(0.35, 0.5, 0.15), где 0.35 - вес темы-1, 0.5 - вес темы-2, а 0.15 - вес темы-3. Следует отметить, что метод LDA никак не интерпретирует темы и не предлагает какого-то обобщённого названия для каждой из них - темы просто пронумерованные по индексу наборы слов. Иногда в качестве простого “осмысленного” идентификатора темы берут одно или несколько слов с максимальными весам внутри данной темы.
Используя модуль gensim, в Python очень легко обучить LDA-модель:
from gensim import models
import numpy as np
textCorpus = [textCollDictionary.doc2bow(doc) for doc in docCollTexts]
nTopics=4
ldamodel=models.ldamodel.LdaModel(textCorpus, id2word=textCollDictionary, num_topics=nTopics, passes=10)
ldamodel.save('ldaModel')
textTopicsMtx=np.zeros(shape=(len(textCorpus),nTopics),dtype=float)
for k in range(len(textCorpus)): ## make the matrix of docs to topic vectors
for tpcId,tpcProb in ldamodel.get_document_topics(textCorpus[k]):
textTopicsMtx[k,tpcId]=tpcProb
В этом примере мы обучаем LDA-модель, сохраняем её на диск, а также создаём матрицу из тематических векторов всех документов в корпусе (матрицу тематических представлений коллекции текстов), которую можно использовать в дальнейшем для решения задач категоризации и кластеризации текстов. Вот так выглядит матрица тематических представлений для нашего простого примера:
Для визуализации самих тем, их словарного состава, в Python существует несколько модулей. Приведём пример кода для визуализации результатов LDA с использованием модулей wordcloud и matplotlib:
import matplotlib.pyplot as plt
from wordcloud import WordCloud
cloud = WordCloud(background_color='white', width=2500, height=1800, max_words=5, colormap='tab10',prefer_horizontal=1.0)
topics = ldamodel.show_topics(num_topics=nTopics, num_words=5, formatted=False)
fig, ax = plt.subplots(1, 4, figsize=(8, 3), sharex=True, sharey=True)
for i, ax in enumerate(ax.flatten()):
fig.add_subplot(ax)
topicWords = dict(topics[i][1])
cloud.generate_from_frequencies(topicWords, max_font_size=300)
plt.gca().imshow(cloud)
plt.gca().set_title('Topic ' + str(i), fontdict=dict(size=16))
plt.gca().axis('off')
plt.subplots_adjust(wspace=0, hspace=0)
plt.axis('off')
plt.show()
В результате для нашего примера получаем такую картинку:
Поскольку в приведённом примере коллекция текстов всего лишь набор отдельных предложений, тематический анализ, фактически, выделил отдельную тему под каждое предложение (документ), хотя и отнёс предложения на английском к одной теме.
В принципе, у алгоритма LDA не так много параметров настройки, главный из которых - число латентных тем и его надо определять заранее, причём выбор оптимального значения этого параметра весьма нетривиальная задача. В своей практике мы руководствуемся следующим простым правилом: берём размер словаря токенов построенного на заданном корпусе текстов и делим на число от 10 до 20 (среднее число слов в отдельной теме) - полученное значение передаётся на вход алгоритма LDA.
6. Контекстное представление слов в моделях Word2Vec и Doc2Vec
Матрица из тематических векторов коллекции текстов на выходе процедуры LDA составляет первую часть полного векторного представления корпуса текстов, вторая часть образуется из семантических векторов, или контекстных представлений.
Концепция семантических векторов построена на дистрибутивной гипотезе: смысл слова заключается не в том, из каких звуков и букв оно состоит, а в том, среди каких слов оно чаще всего встречается, т.е. смысл слова не хранится где-то внутри него, а сосредоточен в его возможных контекстах. По сути, семантический вектор слова показывает, как часто оно встречается рядом с другими словами.
Простейший вариант семантических векторов - матрица частоты употреблений слов в одном контексте, т.е. на расстоянии не больше n слов друг от друга (часто берут n=10), Если взять, к примеру, журнал “Домашний очаг” и посчитать, как часто разные пары слов находятся внутри одного контекстного окна определённого размера, то получится таблица, часть которой может выглядеть так:
Каждая строка чисел в этой таблице и есть семантический вектор (контекстное представление) слов из первого столбца, определённый на корпусе текстов журнала “Домашний очаг”.
Если провести такую процедуру на корпусе текстов всего языка (например, этом), то сразу становятся видны два существенных недостатка такого подхода:
таблица слишком большая - её размер определяется размером словаря, а это десятки тысяч,
таблица будет заполнена в основном нулями.
С 60-тых годов прошлого века предлагались разные методики для уменьшения размерности матрицы совместной встречаемости слов (сингулярное разложение, метод главных компонент, различные типы фильтрации), но какого-то существенного прорыва не наблюдалось. Прорыв случился в 2013 году, когда группа исследователей из Google предложила для получения семантических векторов использовать нейросетевую архитектуру Word2Vec. Опять же, мы не будем описывать в деталях, как работает нейросеть Word2Vec (оригинал статьи можно взять здесь, более доступным языком здесь), а ограничимся основными аспектами:
нейросеть обучается на большой коллекции документов предсказывать какие слова вероятнее всего встретить рядом друг с другом (причём нейросеть отнюдь не глубокая - всего два слоя),
два режима обучения, CBOW (continuous bag-of-words, более быстрый) и skip-gram (более точный для малоупотребляемых слов),
размерность выходных векторов несколько сотен (типично, n=300),
после обучения матрица весов от входного слоя к скрытому слою нейронов автоматически даёт искомые семантические вектора для всех слов.
Дальнейшим развитием метода Word2Vec является нейросетевая архитектура Doc2Vec, которая определяет семантические вектора для целых предложений и параграфов. По сути, в начало последовательности токенов каждого документа произвольным образом вставляется дополнительный абстрактный токен, который используется нейросетью в обучении. На выходе соответствующий этому токену семантический вектор и содержит в себе некий обобщенный смысл всего документа. Хотя выглядит эта процедура как “финт ушами”, на практике семантические вектора из Doc2Vec улучшают характеристики NLP-моделей (но, конечно, не всегда).
Мы используем реализацию алгоритма Doc2Vec из все того же модуля gensim:
from gensim.models import Doc2Vec
d2vSize=5
d2vCorpus= [models.doc2vec.TaggedDocument(text,[k]) for k,text in enumerate(docCollTexts)]
d2vModel=Doc2Vec(vector_size=d2vSize, min_count=1, epochs=10, dm=1)
d2vModel.build_vocab(d2vCorpus)
d2vModel.train(d2vCorpus, total_examples=d2vModel.corpus_count, epochs=d2vModel.epochs)
d2vModel.save('doc2vecModel')
textD2vMtx=np.zeros(shape=(len(textCorpus), d2vSize),dtype=float)
for docId in range(len(d2vCorpus)):
doc2vector=d2vModel.infer_vector(d2vCorpus[docId].words)
textD2vMtx[docId,:]=doc2vector
Здесь мы обучаем Doc2Vec-модель, сохраняем её на диск, а потом, используя обученную модель, создаём матрицу из семантических векторов всех документов. Вот так выглядит эта матрица для нашего простого примера:
Глядя на эту матрицу, достаточно сложно интерпретировать её содержание, особенно по сравнению с тематической матрицей, где всё более или менее понятно. Но сложность интерпретации - это характерная особенность нейросетевых моделей, главное - чтобы они работали.
Объединяя матрицы, рассчитанные в результате отработки алгоритмов LDA и Doc2Vec, получаем матрицу полных векторных представлений коллекции документов (в нашем простом примере размер матрицы - 4х9). На этом задачу перевода текстовых данных в числовые векторы можно считать завершенной, а полученную матрицу - готовой к дальнейшему использованию для построения NLP-моделей категоризации и кластеризации текстов.
7. Заключение
В данной статье мы разобрали примеры использования нескольких библиотек Python для обработки текстовых данных и их перевода в числовые векторы. В следующей статье мы опишем конкретный пример использования методов LDA и Doc2Vec для решения задачи автокластеризации первичных событий в системе ИТ-мониторинга.