library(tidyverse)
load("../data/news_tokens_pruned.Rdata")
news_tokens_pruned
14 Тематическое моделирование c LDA
14.1 Что такое LDA
Тематическое моделирование — семейство методов обработки больших коллекций текстовых документов. Эти методы позволяют определить, к каким темам относится каждый документ и какие слова образуют каждую тему.
Одним из таких методов является Латентное размещение Дирихле (Latent Dirichlet Allocation, LDA). Это вероятностная модель, которая позволяет выявить заданное количество тем в корпусе. В основе метода лежит предположение о том, что каждый документ представляет собой комбинацию ограниченного числа топиков (тем), а каждый топик — это распределение вероятностей для слов. При этом, как и в естественном языке, документы могут перекрывать друг друга по темам, а темы — по словам. Например, слово «мяч» может быть связано не только со спортивным топиком, но и, например, с политическим («клятва в зале для игры в мяч»).
Создатели метода поясняют это на примере публикации из журнала Science.
На картинке голубым выделена тема «анализ данных»; розовым — «эволюционная биология», а желтым — «генетика». Если разметить все слова в тексте (за исключением «шумовых», таких как союзы, артикли и т.п.), то можно увидеть, что документ представляет собой сочетание нескольких тем. Цветные «окошки» слева — это распределение вероятностей для слов в теме. Гистограмма справа — это распределение вероятностей для тем в документе. Все документы в коллекции представляют собой сочетание одних и тех же тем — но в разной пропорции. Например, в этом примере почти нет зеленого «текстовыделителя», что хорошо видно на гистограмме.
Ассоциацию тем с документами, с одной стороны, и слов с темами, с другой, и рассчитывает алгоритм. При этом LDA относится к числу методов обучения без учителя (unsupervised), то есть не требует предварительной разметки корпуса: машина сама «находит» скрытые в корпусе темы и аннотирует каждый документ. Это делает метод востребованным в тех случаях, когда мы сами точно не знаем, что ищем — например, в исследованиях электронных архивов.
Сложность при построении модели обычно заключается в том, чтобы установить оптимальное число тем: для этого предлагались различные количественные метрики, но важнейшим условием является также интерпретируемость результата. Единственно правильного решения здесь нет: например, моделируя архив газетных публикаций, мы можем подобрать темы так, чтобы они примерно соответствовали рубрикам («спорт», «политика», «культура» и т.п.), но в некоторых случаях бывает полезно сделать zoom in, чтобы разглядеть отдельные сюжеты (например, «фигурное катание» и «баскетбол» в спортивной рубрике…)
14.2 Распределение Дирихле
Математические и статистические основания LDA достаточно хитроумны; общие принципы на русском языке хорошо изложены в статье “Как понять, о чем текст, не читая его” на сайте “Системный блок”, а лучшее объяснение на английском языке можно найти здесь и здесь.
Альфа и бета на этой схеме - гиперпараметры распределения Дирихле. Гиперпараметры регулируют распределения документов по темам и тем по словам. Наглядно это можно представить так (при числе тем > 3 треугольник превращается в n-мерный тетраэдр):
При α = 1 получается равномерное распределение: темы распределены равномерно (поэтому α называют “параметром концентрации”). При значениях α > 1 выборки начинают концентрироваться в центре треугольника, представляя собой равномерную смесь всех тем. При низких значениях альфа α < 1 большинство наблюдений находится в углах – скорее всего, в в этом случае в документах будет меньше смешения тем.
Распределение документов по топикам θ зависит от значения α;из θ выбирается конкретная тема Z. Аналогичным образом гиперпараметр β определяет связь тем со словами. Чем выше бета, тем с большим числом слов связаны темы. При меньших значениях беты темы меньше похожи друг на друга. Конкретное слово W “выбирается” из распределения слов φ в теме Z.
14.3 Подтоговка данных
Чтобы понять возможности алгоритма, мы попробуем передать ему тот же новостной архив: на новостях сразу видно адекватность модели; но это не значит, что применение LDA ограничено подобными сюжетами. Этот метод с успехом применяется, например, в историко-научных или литературоведческих исследованиях. Он хорошо подходит, если необходимо на основе журнального архива описать развитие некоторой области знания. Но сейчас нам подойдет пример попроще 👶
Поскольку LDA – вероятностная модель, то на входе она принимает целые числа. В самом деле, не имеет смысла говорить о том, что некое распределение породило 0.5 слов или того меньше. Поэтому мы считаем абсолютную, а не относительную встречаемость – и не tf_idf.
<- news_tokens_pruned |>
news_counts count(token, id)
news_counts
14.4 Матрица встречаемости
Для работы с LDA в R устанавливаем пакет topicmodels
. На входе нужная нам функция этого пакета принимает такую структуру данных, как document-term matrix (dtm), которая используется для хранения сильно разреженных данных и происходит из популярного пакета для текст-майнинга tm
.
Поэтому “тайдифицированный” текст придется для моделирования преобразовать в этот формат, а полученный результат вернуть в опрятный формат для визуализаций.
Для преобразования подготовленного корпуса в формат dtm воспользуемся возможностями пакета tidytext
:
library(tidytext)
<- news_counts |>
news_dtm cast_dtm(id, term = token, value = n)
news_dtm
<<DocumentTermMatrix (documents: 3407, terms: 6299)>>
Non-/sparse entries: 196774/21263919
Sparsity : 99%
Maximal term length: 20
Weighting : term frequency (tf)
Убеждаемся, что почти все ячейки в нашей матрице – нули (99-процентная разреженность).
14.5 Оценка perplexity
Количество тем для модели LDA задается вручную. Здесь на помощь приходит функция perplexity()
из topicmodels
. Она показывает, насколько подогнанная модель не соответствует данным – поэтому чем значение меньше, тем лучше.
Подгоним сразу несколько моделей с разным количеством тем и посмотрим, какая из них покажет себя лучше. Чтобы ускорить дело, попробуем запараллелить вычисления.
library(topicmodels)
library(furrr)
plan(multisession, workers = 6)
<- c(2, 4, 8, 16, 32, 64)
n_topics <- n_topics |>
news_lda_models future_map(topicmodels::LDA, x = news_dtm,
control = list(seed = 0211), .progress = TRUE)
data_frame(k = n_topics,
perplex = map_dbl(news_lda_models, perplexity)) |>
ggplot(aes(k, perplex)) +
geom_point() +
geom_line() +
labs(title = "Оценка LDA модели",
x = "Число топиков",
y = "Perplexity")
Если верить графику, предпочтительны 32 темы или больше. Посмотрим, сколько тем задано редакторами вручную.
|>
news_tokens_pruned count(topic) |>
arrange(-n)
14.6 Выбор числа тем с ldatuning
Еще одну возможность подобрать оптимальное число тем предлагает пакет ldatuning. Снова придется подождать.
library(ldatuning)
<- FindTopicsNumber(
result
news_dtm,topics = n_topics,
metrics = c("Griffiths2004", "CaoJuan2009", "Arun2010", "Deveaud2014"),
method = "Gibbs",
control = list(seed = 05092024),
verbose = TRUE
)
result
FindTopicsNumber_plot(result)
Этот график тоже говорит о том, что модель требует не меньше 32 тем.
14.7 Модель LDA
<- topicmodels::LDA(news_dtm, k = 32, control = list(seed = 05092024)) news_lda
Теперь наша тематическая модель готова. Осталось понять, что с ней делать.
14.8 Слова и темы
Пакет tidytext
дает возможность “тайдифицировать” объект lda с использованием разных методов. Метод β (“бета”) показывает связь топиков с отдельными словами.
<- tidy(news_lda, matrix = "beta")
news_topics
|>
news_topics filter(term == "чай") |>
arrange(-beta)
Например, слово “чай” с большей вероятностью порождено темой 22, чем остальными темами 🍵
Посмотрим на главные термины в первых девяти топиках.
<- news_topics |>
news_top_terms filter(topic < 10) |>
group_by(topic) |>
arrange(-beta) |>
slice_head(n = 12) |>
ungroup()
news_top_terms
|>
news_top_terms mutate(term = reorder(term, beta)) |>
ggplot(aes(term, beta, fill = factor(topic))) +
geom_col(show.legend = FALSE) +
facet_wrap(~ topic, scales = "free", ncol=3) +
coord_flip()
14.9 Сравнение топиков
Сравним два топика по формуле: \(log_2\left(\frac{β_2}{β_1}\right)\). Если \(β_2\) в 2 раза больше \(β_1\), то логарифм будет равен 1; если наоборот, то -1. На всякий случай напомним: \(\frac{1}{2} = 2^{-1}\).
Для подсчетов снова придется трансформировать данные.
<- news_topics |>
beta_wide filter(topic %in% c(5, 7)) |>
mutate(topic = paste0("topic_", topic)) |>
pivot_wider(names_from = topic, values_from = beta) |>
filter(topic_5 > 0.001 | topic_7 > 0.001) |>
mutate(log_ratio = log2(topic_7 / topic_5))
beta_wide
На графике выглядит понятнее:
<- beta_wide |>
beta_log_ratio filter(!log_ratio %in% c(-Inf, Inf, 0)) |>
mutate(sign = case_when(log_ratio > 0 ~ "pos",
< 0 ~ "neg")) |>
log_ratio select(-topic_5, -topic_7) |>
group_by(sign) |>
arrange(desc(abs(log_ratio))) |>
slice_head(n = 12)
|>
beta_log_ratio ggplot(aes(reorder(term, log_ratio), log_ratio, fill = sign)) +
geom_col(show.legend = FALSE) +
xlab("термин") +
ylab("log2 (beta_7 / beta_5)") +
coord_flip()
14.10 Темы и документы
Распределение тем по документам хранит матрица gamma.
<- tidy(news_lda, matrix = "gamma")
news_documents
|>
news_documents filter(topic == 1) |>
arrange(-gamma)
Значение gamma можно понимать как долю слов в документе, происходящую из данного топика, при этом каждый документ в рамках LDA рассматривается как собрание всех тем. Значит, сумма всех гамм для текста должна быть равна единице. Проверим.
|>
news_documents group_by(document) |>
summarise(sum = sum(gamma))
Все верно!
Теперь отберем несколько новостей и посмотрим, какие топики в них представлены.
<- news_tokens_pruned |>
long_news_id group_by(id) |>
summarise(nwords = n()) |>
arrange(-nwords) |>
slice_head(n = 4) |>
pull(id)
long_news_id
[1] "doc608" "doc1670" "doc389" "doc2200"
|>
news_documents filter(document %in% long_news_id) |>
arrange(-gamma) |>
ggplot(aes(as.factor(topic), gamma, color = document)) +
geom_boxplot(show.legend = F) +
facet_wrap(~document, nrow = 2) +
xlab(NULL)
14.11 Распределения вероятности для топиков
|>
news_documents filter(topic < 10) |>
ggplot(aes(gamma, fill = as.factor(topic))) +
geom_histogram(show.legend = F) +
facet_wrap(~ topic, ncol = 3) +
scale_y_log10() +
labs(title = "Распределение вероятностей для каждого топика",
y = "Число документов")
Почти ни одна тема не распределена равномерно: гамма чаще всего принимает значения либо около нуля, либо в районе единицы.
14.12 Интерактивные визуализации
Более подробно изучить полученную модель можно при помощи интерактивной визуализации. Пакет LDAvis установим из репозитория.
::install_github("cpsievert/LDAvis") devtools
Эта функция поможет преобразовать объект lda в файл json.
<- function(x, ...){
topicmodels2LDAvis <- function(x) tsne(svd(x)$u)
svd_tsne <- topicmodels::posterior(x)
post if (ncol(post[["topics"]]) < 3) stop("The model must contain > 2 topics")
<- x@wordassignments
mat
::createJSON(
LDAvisphi = post[["terms"]],
theta = post[["topics"]],
vocab = colnames(post[["terms"]]),
doc.length = slam::row_sums(mat, na.rm = TRUE),
term.frequency = slam::col_sums(mat, na.rm = TRUE),
mds.method = svd_tsne,
reorder.topics = FALSE
) }
Интерактивная визуализация сохранится в отдельной папке.
library(LDAvis)
::serVis(topicmodels2LDAvis(news_lda), out.dir = "ldavis") LDAvis
Это приложение можно опубликовать на GitHub Pages.
Об этом приложении см. здесь.
Значения лямбды, очень близкие к нулю, показывают термины, наиболее специфичные для выбранной темы. Это означает, что вы увидите термины, которые “важны” для данной конкретной темы, но не обязательно “важны” для всего корпуса.
Значения лямбды, близкие к единице, показывают те термины, которые имеют наибольшее соотношение между частотой терминов по данной теме и общей частотой терминов из корпуса.
Сами разработчики советуют выставлять значение лямбды в районе 0.6.