Рассказываем, как сделать тематическое моделирование для большого объема текста, предположить его содержание и разделить по темам
Иллюстратор: Женя Родикова
Предположим, у вас большая коллекция текстов (как минимум несколько сотен, и вы хотите узнать, о чем они — и не просто узнать, а сопоставить с чем-то, что вам уже об этих текстах известно, например, с их жанром. Как же определить содержание автоматически?
Один из популярных способов решения такой проблемы — тематическое моделирование. Оно позволяет выделять из текстов темы, связанные с определенными множествами слов, и затем смотреть, с какой вероятностью тексты соотносятся с этими темами. Для нашей задачи мы будем применять тематическое моделирование, основанное на Латентном размещении Дирихле (LDA) (подробнее прочитать про это можно здесь). Так как нужные алгоритмы уже имплементированы в Питоне, математика, стоящая за ними, нас беспокоить не будет.
Для начала нам нужен датасет с текстами в табличном формате. Одна из колонок должна содержать текст, в других могут быть метаданные. Каждая строка должна соответствовать одному тексту. Установим нужные библиотеки: нам нужна библиотека pandas, которая позволяет работать с данными в табличном формате (такую структуру данных часто называют датафреймом). После того, как мы установили pandas, давайте прочитаем наш датасет с помощью метода read_csv (если таблица в формате csv) и сохраним наш датафрейм в переменную data.
import pandas as pd
data = pd.read_csv(path_to_dataset)
После того, как мы прочитали данные, посмотреть на верхние пять строк можно с помощью метода head:
data.head()
Теперь нам нужна функция для токенизации (разбиения на слова), список знаков препинания и стоп-слова. Стоп-слова — это частые слова, которые встречаются почти во всех текстах и не несут содержательной информации о нем, поэтому их вместе со знаками пунктуации лучше всего удалить. Для этого нам и нужны их списки. Загрузить функцию-токенизатор можно из библиотеки nltk. Для того, чтобы она работала, нам также понадобится пакет с токенизатором, который разбивает текст на предложения. Его можно загрузить с помощью nltk_download:
from nltk.tokenize import word_tokenize
from nltk import download as nltk_download
nltk_download("punkt")
Стоп-слова можно также загрузить из библиотеки nltk. Мы возьмем их из открытого репозитория программы по DH ВШЭ (репозиторий можно найти по этой ссылке), но так как точного списка стоп-слов нет, можно брать их из других мест. В конце все то, что мы будем выкидывать из текста (стоп-слова и знаки препинания), должно быть сохранено в переменную в виде листа.
!wget https://raw.githubusercontent.com/dhhse/dh2020/master/data/stop_ru.txt
with open ("stop_ru.txt", "r") as stop_ru:
rus_stops = [word.strip() for word in stop_ru.readlines()]
punctuation = '!\"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~—»«...–'
filter = rus_stops + list (punctuation)
После того, как мы разобрались со стоп-словами, давайте установим лемматизатор (лемматизация — приведение слов к словарной форме). Для русского языка есть прекрасная библиотека pymorphy2. Ее мы и будем использовать.
from pymorphy2 import MorphAnalyzer
parser = MorphAnalyzer()
Теперь можно написать функцию для предобработки текста. В ней мы делаем следующие операции:
def preprocess(input_text):
"""
Функция для предобработки текста. Слова приводятся к нижнему регистру,
стоп-слова удаляются, далее слова лемматизируются
:param input_text: Входной текст для очистки и лемматизации
:return: Очищенный и лемматизированный текст
"""
text = input_text.lower()
tokenized_text = word_tokenize(text)
clean_text = [word for word in tokenized_text if word not in filter]
lemmatized_text = [parser.parse(word)[0].normal_form for word in
clean_text]
return lemmatized_text
После написания функции нам нужно применить ее ко всем нашим текстам. Чтобы это сделать, можно воспользоваться функцией map. Она применит нашу функцию к каждой строке выбранной нами колонки с текстом. Результат мы запишем в новую колонку:
data["text_processed"] = data["text"].map(preprocess)
Далее нужно импортировать библиотеку gensim. Это популярная открытая библиотека для тематического моделирования, в которой есть нужная нам модель — LDA. Затем мы должны создать словарь для тематического моделирования из лемматизированного текста. После создания словаря лучше всего отфильтровать те слова, которые встречаются в слишком большом количестве текстов, и те, которые встречаются слишком редко. Для этого есть метод filter_extremes, который принимает в себя аргументы no_above (только слова, которые встречаются не более чем в указанной доле текстов) и no_below (слова, которые встречаются не менее чем в указанном количестве текстов). После удаления лишних слов словарь лучше всего ужать в размерах, убрав пропуски с помощью метода compactify.
import gensim
gensim_dictionary = gensim.corpora.Dictionary(data["text_processed"])
gensim_dictionary.filter_extremes(no_above=0.1, no_below=20)
gensim_dictionary.compactify()
Создадим корпус в виде «мешка слов» (bag of words):
corpus = [gensim_dictionary.doc2bow(text)
for text in data['text_processed']]
Наконец можно сделать само тематическое моделирование. Для этого, помимо создания корпуса и словаря, необходимо указать количество «обходов», которые будет делать алгоритм (чем больше, тем точнее и медленнее будет создаваться модель), и количество тем, которые мы хотим выделить. Вот тематическое моделирование для двадцати тем. Стоит не забыть установить параметр random_state на какое-нибудь число — это позволит восстановить результат. Так как компьютер генерирует только псевдослучайные значения, этот параметр позволяет сделать так, чтобы при каждом создании модели эти значения оставались одинаковыми, а значит, чтобы результаты тематического моделирования можно было воспроизвести.
lda_20 = gensim.models.LdaMulticore(corpus,
num_topics=20,
id2word=gensim_dictionary,
passes=10, random_state=6457)
С помощью метода print_topics можно посмотреть, какие топики в итоге получились:
lda_20.print_topics()
При создании тематического моделирования всегда возникает проблема с количеством топиков — как лучше определить их оптимальное число? Один из подходов заключается в том, чтобы сделать несколько моделей на разное количество топиков и затем посмотреть, насколько топики получаются осмысленными. Проблема этого способа в том, что его трудно формализовать. Можно поступить с данной проблемой по-другому, используя формальные метрики. Для этого можно использовать метрики, встроенные в библиотеку gensim — например, c_v или c_uci (подробнее про них, а также про другие метрики тематического моделирования можно прочитать здесь). Посмотрим, какое значение c_v есть дя модели на двадцать тем:
from gensim.models import CoherenceModel
coherence_model_lda = CoherenceModel(model=lda_20,
texts=data["text_processed"],
dictionary=gensim_dictionary,
coherence="c_v")
coherence_lda = coherence_model_lda.get_coherence()
print("\nCoherence Score: ", coherence_lda)
Значение метрики важно не только само по себе, но и как сравнение с другими возможными количествами тем. Чтобы не делать множество моделей самому, а потом сравнивать их метрики, создадим график, где на оси х будет отложено количество тем, а на оси у — соответствующее значение метрики. Для этого сначала надо сделать список значений нужной метрики для какого-нибудь промежутка тем (для экономии времени лучше не вычислять их для каждого значения количества тем, вполне можно взять шаг, например в три темы — но это зависит от того, сколько тем вы ожидаете в ваших текстах). Для этого лучше всего написать функцию, которая будет на вход принимать наш словарь, корпус, тексты, значения start (количество топиков от которых начинается подсчет) и step (шаг), max (максимальное число топиков) а также measure — метрику, которая будет использоваться (по умолчанию стоит “c_uci”)
def coherence_score(dictionary, corpus, texts, max, start=2, step=3,
measure="c_uci"):
"""
Функция вычисляет метрики для оценки тем. моделирования и выводит
график, где по оси x отложено количество топиков, а по оси y — значение
метрики
:param dictionary: словарь для тематического моделирования
:param corpus: корпус в виде мешка слов
:param texts: тексты документов
:param max: максимальное количество топиков
:param start: стартовое количество топиков
:param step: промежуток, с которым вычисляются топики
:param measure: метрика
"""
coherence_values = []
for num_topics in range(start, max, step):
model = gensim.models.LdaMulticore(corpus=corpus, id2word=dictionary,
passes=10, num_topics=num_topics,
random_state=6457)
coherencemodel = CoherenceModel(model=model, texts=texts,
dictionary=dictionary,
coherence=measure)
coherence_values.append(coherencemodel.get_coherence())
x = range(start, max, step)
plt.plot(x, coherence_values)
plt.xlabel("Number of Topics")
plt.ylabel(measure + "score")
plt.legend(("coherence_score"), loc='best')
plt.show()
Перед тем, как вызывать функцию, нужно импортировать библиотеку matplotlib:
import matplotlib.pyplot as plt
coherence_score(dictionary=gensim_dictionary, corpus=corpus, texts=data["text_processed"], start=2, max=30, step=3, measure="c_v")
Давайте для удобства предположим, что, основываясь на метриках, мы решили выбрать модель на двадцать топиков. В библиотеке gensim есть встроенная интерактивная визуализация расстояния между темами. Можно использовать ее, чтобы оценить, насколько пересекаются между собой полученные топики.
import pyLDAvis.gensim_models as gensimvis
import pyLDAvis
vis_20 = gensimvis.prepare(lda_20, corpus, gensim_dictionary)
pyLDAvis.enable_notebook()
vis_20
Как же дальше использовать результаты полученного тематического моделирования? Для начала можно назначить каждому документу наиболее подходящий (наиболее вероятный) для него топик. Для этого документ сначала нужно представить в виде «мешка слов», а затем использовать метод нашей модели get_document_topics. После этого следует выбрать топик с наибольшей вероятностью — поскольку именно он нас интересует.
def get_topic(words, lda):
"""
Функция назначает документу наиболее вероятный топик
:param words: лемматизированный текст документа
:param lda: тематическая модель
:return: список из наиболее вероятного топика
и его вероятности
"""
bag = lda.id2word.doc2bow(words)
topics = lda.get_document_topics(bag)
topic_dictionary = {}
for topic in topics:
topic_dictionary[topic[1]] = str((topic[0]))
main_probability = max(topic_dictionary)
main_topic = topic_dictionary[main_probability]
return [main_topic, main_probability]
Теперь можно применить нашу функцию к датасету. Это можно сделать с помощью функции apply:
data["lda_20"] = data["text_processed"].apply(get_topic, lda=lda_20)
Теперь у нас есть колонка, в которой сразу же записан наиболее вероятный топик и его вероятность — что не очень удобно. Давайте разнесем это по двум разным столбцам:
data["topic_20"] = data["lda_20"].str[0]
data["probability_20"] = data["lda_20"].str[1]
del data["lda_20"]
Наконец, мы можем построить график, на котором будет видно, как топики распределяются относительно интересующих нас метаданных (в нашем случае жанра). Построим график, где по горизонтали отложены жанры,а по вертикали — темы. Для этого импортируем библиотеку seaborn.
import seaborn as sns
sns.catplot(x="genre", y="topic_20", kind="swarm", data=data, height=5, aspect=3)
Важно помнить, что тексты и метаданные могут быть самые разные. Посмотреть на тематическое моделирование для фанфиков можно по ссылке (тетрадка в гугл-колабе).
Компания Google представила много новых ИИ-продуктов, а модель GPT опровергла известную математическую гипотезу Пала Эрдёша — рассказываем, что произошло в мире ИИ за последнее время
Facebook* и Instagram* будут сканировать фото и видео, чтобы находить детей, которые скрыли свой возраст
Можно ли заниматься NLP, если при словах «производная» и «матрица» хочется закрыть ноутбук? Да — если изучать математику не абстрактно, а через реальные задачи. Объясняем, какие разделы действительно нужны джуну,…