library(tidyverse)
library(tidytext)
13 Векторные представления слов на основе PMI. Word2Vec.
В этом уроке рассмотрим еще один способ построения эмбеддингов, когда за основу берется матрица термин-термин.
13.1 Скользящее окно
Прежде всего разделим новости на контекстные окна фиксированной величины. Чем меньше окно, тем больше синтаксической информации оно хранит.
load("../data/news_tokens_pruned.Rdata")
<- news_tokens_pruned |>
nested_news ::select(-topic) |>
dplyrnest(tokens = c(token))
nested_news
<- function(tbl, window_size) {
slide_windows <- slider::slide(
skipgrams
tbl, ~.x,
.after = window_size - 1,
.step = 1,
.complete = TRUE
)
<- safely(mutate)
safe_mutate
<- map2(skipgrams,
out 1:length(skipgrams),
~ safe_mutate(.x, window_id = .y))
%>%
out transpose() %>%
pluck("result") %>%
compact() %>%
bind_rows()
}
Деление на окна может потребовать нескольких минут. Чем больше окно, тем больше потребуется времени и тем больше будет размер таблицы.
<- nested_news |>
news_windows mutate(tokens = map(tokens, slide_windows, 10L)) %>%
unnest(tokens) %>%
unite(window_id, id, window_id)
news_windows
load("../data/news_windows.Rdata")
13.2 Что такое PMI
Обычная мера ассоциации между словами, которой пользуются лингвисты, — точечная взаимная информация, или PMI (pointwise mutual information). Она рассчитывается по формуле:
\[PMI\left(x;y\right)=\log{\frac{P\left(x,y\right)}{P\left(x\right)P\left(y\right)}}\]
В числителе — вероятность встретить два слова вместе (например, в пределах одного документа или одного «окна» длинной n слов). В знаменателе — произведение вероятностей встретить каждое из слов отдельно. Если слова чаще встречаются вместе, логарифм будет положительным; если по отдельности — отрицательным.
Посчитаем PMI на наших данных, воспользовавшись подходящей функцией из пакета widyr
.
library(widyr)
<- news_windows |>
news_pmi pairwise_pmi(token, window_id)
|>
news_pmi arrange(-abs(pmi))
13.3 Почему PPMI
В отличие от коэффициента корреляции, например, PMI может варьироваться от \(-\infty\) до \(+\infty\), но негативные значения проблематичны. Они означают, что вероятность встретить эти два слова вместе меньше, чем мы бы ожидали в результате случайного совпадения. Проверить это без огромного корпуса невозможно: если у нас есть \(w_1\) и \(w_2\), каждое из которых встречается с вероятностью \(10^{-6}\), то трудно удостовериться в том, что \(p(w_1, w_2)\) значимо отличается от \(10^{-12}\). Поэтому негативные значения PMI принято заменять нулями. В таком случае формула выглядит так:
\[ PMI\left(x;y\right)=max(\log{\frac{P\left(x,y\right)}{P\left(x\right)P\left(y\right)}},0) \] Для подобной замены подойдет векторизованное условие.
<- news_pmi |>
news_ppmi mutate(ppmi = case_when(pmi < 0 ~ 0,
.default = pmi))
|>
news_ppmi arrange(pmi)
Если мы развернем такую матрицу вширь, то она получится очень разреженной; чтобы получить плотные векторы слов, необходимо прибегнуть к SVD.
13.4 SVD на матрице с PPMI
Для этого можно передать тиббл фунции widely_svd()
для вычисления сингулярного разложения. Обратите внимание на аргумент weight_d
: если задать ему значение FALSE
, то вернутся не эмбеддинги, а матрица левых сингулярных векторов:
<- news_ppmi |>
word_emb widely_svd(item1, item2, ppmi,
weight_d = FALSE, nv = 100) |>
rename(word = item1) # иначе nearest_neighbors() будет жаловаться
word_emb
13.5 Визуализация топиков
Снова визуализируем главные компоненты нашего векторного пространства.
|>
word_emb filter(dimension < 10) |>
group_by(dimension) |>
top_n(10, abs(value)) |>
ungroup() |>
mutate(word = reorder_within(word, value, dimension)) |>
ggplot(aes(word, value, fill = dimension)) +
geom_col(alpha = 0.8, show.legend = FALSE) +
facet_wrap(~dimension, scales = "free_y", ncol = 3) +
scale_x_reordered() +
coord_flip() +
labs(
x = NULL,
y = "Value",
title = "Первые 9 главных компонент за 2019 г.",
subtitle = "Топ-10 слов"
+
) scale_fill_viridis_c()
13.6 Ближайшие соседи
Исследуем наши эмбеддинги, используя уже знакомую функцию, которая считает косинусное сходство между словами.
source("../helper_scripts/nearest_neighbors.R")
|>
word_emb nearest_neighbors("сборная")
|>
word_emb nearest_neighbors("завод")
13.7 2D-визуализации пространства слов
<- word_emb |>
word_emb_mx cast_sparse(word, dimension, value) |>
as.matrix()
Для снижения размерности мы снова используем алгоритм UMAP.
set.seed(02062024)
<- umap(word_emb_mx, n_neighbors = 15, n_threads = 2) viz
Как видно по размерности матрицы, все слова вложены теперь в двумерное пространство.
dim(viz)
[1] 6299 2
tibble(word = rownames(word_emb_mx),
V1 = viz[, 1],
V2 = viz[, 2]) |>
ggplot(aes(x = V1, y = V2, label = word)) +
geom_text(size = 2, alpha = 0.4, position = position_jitter(width = 0.5, height = 0.5)) +
annotate(geom = "rect", ymin = 2.5, ymax = 7, xmin = 1.5, xmax = 6.5, alpha = 0.2, color = "tomato")+
theme_light()
Посмотрим на выделенный фрагмент этой карты.
tibble(word = rownames(word_emb_mx),
V1 = viz[, 1],
V2 = viz[, 2]) |>
filter(V1 > 1.5 & V1 < 6.5) |>
filter(V2 > 2.5 & V2 < 7) |>
ggplot(aes(x = V1, y = V2, label = word)) +
geom_text(size = 2, alpha = 0.4, position = position_jitter(width = 0.5, height = 0.5)) +
theme_light()
Отличная работа 🏈 Теперь попробуем построить векторное пространство с использованием поверхностных нейросетей.
13.8 Word2vec
Word2vec – это полносвязаная нейросеть с одним скрытым слоем. Такое обучение называется не глубоким, а поверхностным (shallow).
library(word2vec)
<- news_tokens_pruned |>
corpus group_by(id) |>
mutate(text = str_c(token, collapse = " ")) |>
distinct(id, text)
# устанавливаем зерно, т.к. начальные веса устанавливаются произвольно
set.seed(02062024)
<- word2vec(x = corpus$text,
model type = "skip-gram",
dim = 50,
window = 5,
iter = 20,
hs = TRUE,
min_count = 5,
threads = 6)
Наша модель содержит эмбеддинги для слов; посмотрим на матрицу.
<- as.matrix(model)
emb dim(emb)
[1] 6305 50
predict(model, c("погода", "спорт"), type = "nearest", top_n = 10) |>
bind_rows()
Получившуюся модель можно визуализировать, как мы это делали выше.