Обработка естественного языка

Все курсы > Вводный курс > Занятие 19

Сегодня мы рассмотрим тему обработки естественного языка (Natural Language Processing, NLP), области на стыке математики, информатики и лингвистики, занимающейся пониманием (анализом) и созданием (синтезом) текстов с помощью компьютера.

В частности мы разберем тему (1) предварительной обработки языка (language pre-processing), (2) а также изучим два несложных способа анализа содержания текста (topic identification).

Постановка задачи

В школьных учебниках можно встретить такое задание: «Прочитайте текст. Определите его тему». У человека, как правило, это не вызывает никаких сложностей. Теперь представьте, что вы компьютер. Как понять, о чем говорится в тексте?

В качестве примера, возьмем следующий текст на английском языке:

When we were in Paris we visited a lot of museums. We first went to the Louvre, the largest art museum in the world. I have always been interested in art so I spent many hours there. The museum is enourmous, so a week there would not be enough.

Если вы попросите меня описать содержание тремя или четырьмя словами, я бы сказал: «музей» (museum), «Лувр» (Louvre), «Париж» (Paris), «искусство» (art). Посмотрим, что скажет компьютер.

По традиции вначале откроем ноутбук к этому занятию

В лингвистике совокупность рассматриваемых текстов принято называть корпусом (corpus, мн.ч. кóрпусы, corpora).

Импортируем пакет библиотек для обработки естественного языка (Natural Language Toolkit или NLTK) и другие уже известные нам библиотеки.

Предварительная обработка текста

Шаг 1. Разделение на предложения

Вначале текст логично разделить на предложения или токенизировать. Хотя задача кажется тривиальной (достаточно разделить текст по точкам, восклицательным и вопросительным знакам), есть несколько сложностей. Например, фраза «Мороженое стоит 100 руб. 20 коп.» может быть ошибочно разбита на два предложения. Помимо сокращений есть и другие сложности. Например, из-за опечатки предложение может заканчиваться пробелом, а не знаком препинания.

Для решения этой задачи можно использовать стандартный метод библиотеки NLTK sent_tokenize.

Метод использует уже обученную модель, в нашем случае, для английского языка: nltk_data/tokenizers/punkt/english.pickle. Также можно использовать модели для других языков или обучить собственную модель.

Шаг 2. Разделение на слова

Разделение на слова или токенизация слов — следующий шаг в обработке текста. В целом используется аналогичный подход и метод word_tokenize. Для примера, разобьем на слова первое предложение.

Сделаем то же самое для всего текста.

Шаг 3. Перевод в нижний регистр, удаление стоп-слов и знаков пунктуации

Выясняется, что для анализа текста далеко не все слова являются релевантными. Если мы попробуем применить к тексту статистические методы, то многие важные с точки зрения человеческой грамматики слова будут нам только мешать. В частности, это предлоги, союзы и артикли. Назовем их «стоп-словами» и попробуем опустить.

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

Как вы вероятно заметили, мы дополнительно удалили пунктуацию с помощью метода isalpha().

Шаг 4. Лемматизация

Кроме того, с точки зрения анализа содержания слова «музеи» и «музей» означают одно и то же, но компьютер посчитает их разными словами. Значит, слова нужно привести к начальной (словарной) форме или лемме.

WordNet Lemmatizer библиотеки NLTK использует базу данных WordNet⧉ для поиска словарной формы слова.

Шаг 5. Стемминг

Стемминг (stemming), в отличие от лемматизации, ориентирован на поиск основы слова (stem). Попробуем применить и его.

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

Как вы видите, list comprehension позволяет в одну строчку создавать список. Подробнее этот прием мы рассмотрим на следующем курсе.

Теперь применим более агрессивный стеммер Ланкастера.

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

Мешок слов

Принцип метода мешка слов (Bag of Words, BoW) чрезвычайно прост. Мы считаем как часто встречается каждое слово в тексте.

Несмотря на простоту, при правильной предобработке текста (в первую очередь удалении стоп-слов, которые и будут наиболее частотными) этот метод показывает неплохие результаты. Применим его к нашему тексту с помощью класса Counter модуля Collections.

Этот же метод можно реализовать с помощью класса CountVectorizer библиотеки Scikit-learn.

Преобразуем матрицу csr в привычный формат массива Numpy.

Строки предсталяют собой предложения (документы), столбцы — слова (токены).

Есть два способа посмотреть на используемые токены. С помощью атрибута vocabulary_.

Здесь числа это не частотность слов, а просто их порядковый номер или индекс. Также токены можно вывести с помощью метода .get_feature_names_out().

Результат для удобства также можно преобразовать в датафрейм.

мешок слов через CountVectorizer

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

Это очень важный результат, о котором мы поговорим в конце занятия.

Метод TF-IDF

Чуть более сложный и продвинутый метод определения значимости слов в тексте называется TF-IDF (term frequency — inverse document frequency).

Основная идея

Если слово часто встречается во всех документах (это в первую очередь касается предлогов, союзов и других стоп-слов), то вряд ли эти слова имеют большое значение. И наоборот, если слово встречаться только в одном документе, вероятно оно в большей степени определяет его содержание.

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

Простой пример и формула

Предположим, что у нас есть два текста и мы посчитали частотность слов в каждом из них (т.е. создали мешок слов).

пример метода TF-IDF
Пример взят из Википедии
Расчет tf-idf для слова this

Поставим себе задачу рассчитать TF-IDF для слова this в каждом из документов. На первом этапе вычисляем частоту слова (term frequency или TF) относительно всех слов в документе.

$$ tf(this, d_1) = \frac{частотность \space слова}{всего \space слов} = \frac{1}{5} = 0.2 $$

$$ tf(this, d_2) = \frac{частотность \space слова}{всего \space слов} = \frac{1}{7} \approx 0.14 $$

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

На втором этапе рассчитаем IDF слова this. То есть мы делим общее количество документов в корпусе на количество документов, в которых встречается искомый токен, и берем логарифм (в данном случае, десятичный) частного.

$$ idf(this, D) = \log_{10} \left(\frac{всего\spaceдокументов}{документов\spaceс\spaceтокеном}\right) = \log_{10} \left( \frac{2}{2} \right) = 0 $$

Остаётся перемножить TF и IDF.

$$ tf-idf (this, d_1, D) = 0.2 \times 0 = 0 $$

$$ tf-idf (this, d_2, D) = 0.14 \times 0 = 0 $$

Этот показатель равен нулю, что отражает низкую значимость слова this для обоих документов. Теперь сделаем аналогичный расчет для слова example.

Расчет tf-idf для слова example

$$ tf(example, d_1) = \frac{0}{5} = 0 $$

$$ tf(example, d_2) = \frac{3}{7} \approx 0.429 $$

$$ idf(example, D) = \log_{10} \left( \frac{2}{1} \right) \approx 0.301 $$

$$ tf-idf (example, d_1, D) = 0 \times 0.301 = 0 $$

$$ tf-idf (example, d_2, D) = 0.429 \times 0.301 \approx 0.129 $$

В данном случае слово example имеет заметно большее значение для второго документа. Для первого документа его значимость по-прежнему равна нулю (такого слова там нет).

Логика формулы следующая, чем выше частота слова в документе (tf) и чем реже оно встречается в целом в документах (idf), тем выше общий показатель (tf-idf).

Также замечу, что на практике эти формулы часто модифицируют. Например, к знаменателю формулы расчета idf добавляют единицу, чтобы избежать деления на ноль, если документов с таким токеном не нашлось.

$$ idf = \log \left(\frac{всего\spaceдокументов}{документов\spaceс\spaceтокеном + 1}\right) $$

TF-IDF с помощью библиотеки Scikit-learn

Применим этот метод к нашему исходному тексту про Париж и музеи. Вначале последовательно используем классы CountVectorizer и TfidfTransformer.

Способ 1. CountVectorizer + TfidfTransformer

TF или частоту слов мы можем взять из предыдущего раздела.

Теперь нужно рассчитать IDF.

Остается TF x IDF.

Теперь мы можем посмотреть на показатель TF-IDF для конкретного слова в конкретном документе.

Всего после обработки метод оставил 15 слов.

Такого же результата можно добиться применив метод Tfidfvectorizer.

Способ 2. Tfidfvectorizer

Мы можем посмотреть какие слова остались после фильтрации.

В частности мы видим, что метод Tfidfvectorizer оставил теже слова, что и CountVectorizer и TfidfTransformer. Посмотрим IDF слов.

Посмотрим на количество документов и количество токенов (слов).

Рассчитаем значение TF-IDF для каждого слова по каждому тексту.

В целом упражнение можно завершить. Однако напомню, что нашей целью было определить тему текста. Для этого мы также можем рассчитать среднее значение TF-IDF для каждого слова по всем текстам.

Вычислим среднее арифметическое по строкам матрицы, приведенной выше (подробное объяснение кода вы найдете в ноутбуке⧉).

И создадим датафрейм, отсортировав слова по убыванию средних весов.

веса модели TF-IDF через Tfidfvectorizer

Результаты

Как мы видим, ни один из алгоритмов не справился со своей задачей на отлично. Отчасти это связано с ограничениями самих методов, отчасти с тем, что мы взяли очень небольшой текст для анализа.

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

векторизация текста

Дополнительные примеры

Прежде чем завершить, я покажу два примера применения текстовых векторов.

Косинусное расстояние между текстовыми векторами

Возьмем два предложения и объединим их в корпус.

Создадим объект класса TfidfVectorizer и рассчитаем веса tf-idf.

Для удобства, мы можем преобразовать данные в датафрейм.

текстовые вектора для расчета косинусного расстояния

Вектора готовы. Напомню формулу расчета косинусного расстояния.

$$ \cos \theta ={\mathbf {a} \cdot \mathbf {b} \over \|\mathbf {a} \|\|\mathbf {b} \|} $$

Теперь поместим каждый вектор в отдельную переменную.

Выполним операции в числителе формулы.

Займемся знаменателем.

Остается рассчитать косинус угла.

И после этого перевести его в радианы, а затем в градусы.

Кластерный анализ текста

Теперь давайте посмотрим, как можно разделить предложения в тексте на темы (кластеры). Возьмем текст с предложениями из Википедии, посвященных науке о данных и Большому театру.

Разобьем текст на преложения и переведем в нижний регистр.

Создадим векторы каждого из предложений.

Применим алгоритм k-средних и разделим предложения на кластеры.

Как результат, мы создали две модели:

  • Модель векторизации через tfIdfVectorizer
  • Модель кластеризации

Возьмем новые предложения и с помощью обученных моделей разобьем их на кластеры.

Как мы видим, первое предложение отнесено к одному кластеру, второе и третье — к другому.

Подведем итог

Сегодня мы впервые поработали с текстами. В частности, научились предварительно обрабатывать их и анализировать содержание. Для этого мы использовали метод мешка слов и метод TF-IDF. Второй метод позволил превратить предложения в числа или векторизовать их.

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

Вопросы для закрепления

Перечислите способы предварительной обработки текста

Посмотреть правильный ответ

В чем отличие мешка слов от метода TF-IDF?

Посмотреть правильный ответ

Что позволяет нам выполнять математические операции над текстом?

Посмотреть правильный ответ

На следующем занятии мы поговорим про анализ временных рядов.


Ответы на вопросы

Вопрос. Имеет ли значение какое основание логарифма использовать в формуле TF-IDF?

Ответ. Нет, не имеет, основание может быть любым. В формуле выше используется десятичный логарифм, sklearn использует натуральный.


Вопрос. Во втором дополнительном примере, откуда мы знаем, что кластеров должно быть два (в алгоритме k-means)?

Ответ. В данном случае, мы либо заранее знаем, что темы две, и нам нужно научить алгоритм разделять тексты на эти два кластера, либо попробовать метод локтя, как мы это делали на занятии по кластеризации.


Вопрос. Зачем мы применили .ravel() к массиву при работе с TfidfVectorizer (способ 2)? То есть зачем убирали второе измерение?

Ответ. Мы это сделали, чтобы затем корректно сработала функция tolist(). Если не использовать .ravel(), то применив только tolist() мы получим вложенные списки [[some_list]], а нам нужен обычный список [some_list].

Попробуйте в качестве эксперимента обойтись без .ravel().