library(tidyverse)
library(rvest)
library(tidytext)
library(irlba)
library(widyr)
library(stopwords)
Тематическое разнообразие сказок Б. Шергина
Введение
В этой серии мы с вами продолжим наблюдать за попытками исследования самых известных сказок Б.В. Шергина. В прошлом эпизоде мы искали поморский компонет в языке сказок, а теперь попробуем посмотреть, есть ли в этих сказках общий лейт-мотив (общая идея), который привел к их популярности (== мультипликации), или они безумно уникальны сами по себе и в этом их сила. В этой серии вас ждет захватывающее приключение с косяками в мир определения тематического разнообразия с помощью LSA и векторного представления слов. Кроме того, мы попробуем определить есть ли сходство между сказками, используя косинусную близость.
Гипотезы
- Как мы помним, тема, в нашем случае, сказки может определяться контекстами, т.е. суммой слов. Мы ожидаем, что тема каждой сказки будет описана некоторым количеством слов.
- Определить общность тем, или же уникальность, нам поможет косинусное сходство между документами. В том случае, если документы (== сказки) уникальны по тематике, то углы между векторами слов будут большими, и сказки окажутся удалены друг от друга. Если же они сходны между собой, то углы будут небольшими, и сказки окажутся близко друг к другу.
Воплощение в коде. Проверка гипотез.
Подготовка данных
Для начала подгрузим все нужные библиотеки.
Мы будем использовать тот же датасет, что и в прошлый раз. Но, ввиду того, что его не получилось сохранить в формате Rdata, мы проделаем его формирование еще раз. Что тут у нас происходит: сохраняем ссылку, откуда брали данные; читаем html; забираем ссылки на главы и создаем тиббл с названиями сказок и ссылками; создаем функцию, которая заберет тексты сказок; объединяем таблицы; токенизируем текст сказок; убираем стоп-слова и упорядочиваем; переименовываем столбец для удобства.
<- "https://peskarlib.ru/b-shergin/"
Http <- read_html(Http)
html_2 <- html_2 |>
Сказки html_elements(".h2-link")
<- tibble(
lib title = Сказки |>
html_text2(),
href = Сказки |>
html_attr("href"))
<- lib |>
Ссылки_сказки mutate(link = paste0("https://peskarlib.ru", href)) |>
select(-href)
<- Ссылки_сказки |>
https pull(link)
<- function(url) {
pick_up_the_text read_html(url) |>
html_elements(".cl-sst") |>
html_text2() |>
paste(collapse= " ")
}<- map(https, pick_up_the_text)
texts <- texts |>
texts flatten_chr() |>
as_tibble()
<- Ссылки_сказки |>
Сказки_Шергина bind_cols(texts) |>
select(-link)
<- Сказки_Шергина |>
Сказки_Шергина mutate(id = paste0("doc", row_number()))
<- Сказки_Шергина |>
Сказки_Шергина_слова unnest_tokens("word", "value")
<- c(
stopwords_ru stopwords("ru", source = "snowball"),
stopwords("ru", source = "marimo"),
stopwords("ru", source = "nltk"))
<- sort(unique(stopwords_ru))
stopwords_ru
<- which(stopwords_ru == "жизнь" | stopwords_ru == "человек")
idx <- stopwords_ru[-idx]
stopwords_ru <- c(stopwords_ru, "коль", "скоро", "ко", "во", "это", "ладно", "твоего", "обо", "эко", "дак", "што")
stopwords_ru <- Сказки_Шергина_слова |>
Чистые_сказки anti_join(tibble(word = stopwords_ru))
<- Чистые_сказки |>
Clean_tales rename(token = word) # Датасет подготовлен!
Clean_tales
# A tibble: 12,654 × 3
title id token
<chr> <chr> <chr>
1 Дивный гудочек doc1 отца
2 Дивный гудочек doc1 матери
3 Дивный гудочек doc1 сынок
4 Дивный гудочек doc1 романушко
5 Дивный гудочек doc1 дочка
6 Дивный гудочек doc1 девка
7 Дивный гудочек doc1 восьмуха
8 Дивный гудочек doc1 романушко
9 Дивный гудочек doc1 желанное
10 Дивный гудочек doc1 дитятко
# ℹ 12,644 more rows
Выявление тем
Для начала, осуществим векторное представление слов на основе PMI. Для этого вычислим меру ассоциации PMI и PPMI, а затем выполним сингулярное разложение.
<- 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()
# Используем скользящие окна
}
<- Clean_tales |>
nested_tales nest(tokens = c(token))
<- nested_tales |>
tales_windows mutate(tokens = map(tokens, slide_windows, 8L)) %>%
unnest(tokens) %>%
unite(window_id, id, window_id)
#Подсчитываем меру ассоциации:
<- tales_windows |>
tales_pmi pairwise_pmi(token, window_id)
<- tales_pmi |>
tales_ppmi mutate(ppmi = case_when(pmi < 0 ~ 0,
.default = pmi))
#Делаем SVD:
<- tales_ppmi |>
word_emb widely_svd(item1, item2, ppmi,
weight_d = FALSE, nv = 50) |>
rename(word = item1)
Векторное представление слов получено, теперь мы можем посмотреть на тематику сказок Б.В.Шергина. Для этого, проделаем визуализацию 9 главных компонент (по 10 слов).
|>
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.9, show.legend = FALSE) +
facet_wrap(~dimension, scales = "free_y", ncol = 3) +
scale_x_reordered() +
coord_flip() +
labs(
x = NULL,
y = "Value",
title = "Тематическое разнообразие сказок Б.Шергина",
subtitle = "Топ-10 слов"
+
) scale_fill_viridis_c()
Определение сходства/уникальности сказок
Чтобы посчитать степень сходства сказок, мы используем, как упоминалось выше, косинусное сходство. Для этого,откатимся немного назад, и проделаем сингулярное разложение на tf-idf. Здесь мы подсчитаем, собственно tf-idf. Затем представим наши данные в разряженном формате и выполним сингулярное разложение с пакетом irlba. Почему используется именно этот способ, с тем что теперь нам нужна матрица не левых, а правых сингулярных векторов, которая хранит информацию о документах.
#Вычисляем tf-idf:
<- Clean_tales |>
tales_counts count(token, id)
<- tales_counts |>
tales_tf_idf bind_tf_idf(token, id, n) |>
arrange(tf_idf) |>
select(-n, -tf, -idf)
tales_tf_idf
# A tibble: 9,606 × 3
token id tf_idf
<chr> <chr> <dbl>
1 видит doc1 0
2 видит doc10 0
3 видит doc2 0
4 видит doc3 0
5 видит doc4 0
6 видит doc5 0
7 видит doc6 0
8 видит doc7 0
9 видит doc8 0
10 видит doc9 0
# ℹ 9,596 more rows
<- tales_tf_idf |>
matrix cast_sparse(token, id, tf_idf)
#Получаем матрицу правых сингулярных векторов:
<- irlba::irlba(matrix, 9)
SVDrownames(SVD$v) <- colnames(matrix)
colnames(SVD$v) <- paste0("dim", 1:9)
<- SVD$v |>
tales_emb as.data.frame() |>
rownames_to_column("doc") |>
as_tibble()
#Приводим данные в длинный формат:
<- tales_emb |>
tales_emb_long pivot_longer(-doc, names_to = "dimension", values_to = "value") |>
mutate(dimension = as.numeric(str_remove(dimension, "dim")))
Теперь, когда данные приведены в нужный нам формат, мы считаем косинусную близость наших сказок. Для этого применяем функцию nearest_neighbours.
<- function(df, feat, doc=F) {
nearest_neighbors <- function() {
inner_f widely(
~ {
<- .[rep(feat, nrow(.)), ]
y <- rowSums(. * y) /
res sqrt(rowSums(. ^ 2)) * sqrt(sum(.[feat, ] ^ 2)))
(
matrix(res, ncol = 1, dimnames = list(x = names(res)))
},sort = TRUE
)}if (doc) {
|> inner_f()(doc, dimension, value) }
df else {
|> inner_f()(word, dimension, value)
df |>
} select(-item2)
}#Пример работы:
nearest_neighbors(tales_emb_long, "doc4", doc = TRUE)
# A tibble: 10 × 3
item1 item2 value
<chr> <int> <dbl>
1 doc4 1 1
2 doc3 1 0.870
3 doc9 1 0.320
4 doc6 1 0.0629
5 doc8 1 0.0520
6 doc1 1 0.00362
7 doc5 1 -0.0202
8 doc10 1 -0.0641
9 doc7 1 -0.0914
10 doc2 1 -0.0941
Результаты
Тематическое разнообразие, безусловно, присутсвует в сказках Б.Шергина, однако, есть сказки, которые не очень сильно, но схожи между собой. Например, сказка “Золоченые лбы” очень близка к сказкам “Мартынко” и “Пронька Грезной”, “Волшебное кольцо”. Совершенно особняком, стоит сказка “Шиш Московский”. Это может быть вызвано тем, что она относится к отдельному циклу сказок про Шиша (Черемисинова 2017).