Тематическое моделирование воспоминаний интеллигенции Новосибирского Академгородка с помощью метода LDA

Автор

Екатерина Кулятина

Дата публикации

20.12.2025

Аннотация

Статья-туториал посвящена описанию тематического устройства текстов речевого жанра “Воспоминания” с помощью метода Latent Dirichlet Allocation (LDA).

Почему воспоминания?

В течение последних нескольких лет я работаю с корпусом воспоминаний учёных и преподавателей Новосибирского государственного университета. Изначально моей задачей было описание организации этих текстов с точки структурно-семантического синтаксиса, но, как это часто случается, в какой-то момент тексты перестали для меня быть исключительно языковым материалом. Я начала изучать биографии моих респондентов и размышлять, почему они вспоминают одни события, а не другие. Есть ли какие-то темы, которые человек обязательно затронет в рассказе о себе? От чего зависят эти темы?

Информация о тематическом устройстве текстов может пригодиться для развиия теории речевых жанров – уточнения характеристики жанра “Воспоминание”.

Цель и задачи исследования

Целью проекта является описание тематической структуры текстов-воспоминаний. Для этого мной была вручную составлена предварительная тематическая модель корпуса, затем, уже с помощью LDA-алгоритма, в текстах выделены топики и главные термины для них; полученные данные я сопоставила с гипотетической моделью.

Описание данных

Материалом для исследования послужили 9 записей монологической речи интеллигенции Новосибирского Академгородка. Записи собраны в 2008, 2012 и 2025 годах. Респондентами выступили исследователи и преподаватели Новосибирского государственного университета в возрасте 70–80 лет, имеющие ученую степень и /или ученое звание, прожившие бо́льшую часть жизни в Академгородке.

Расшифрованные записи хранятся в формат XML-файлов, их можно скачать по ссылке. В целях защиты персональных данных, воспоминания обезличины: в поле author указан индивидуальный номер.

Гипотетическая модель

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

Номер текста Количество топиков в тексте Ключевые слова для топиков
1 1 1) школа, оценки, учителя
2 3 1) поступление, универистет, Академгородок, Омск 2) муж, жилье, университет 3) мама, дача, деньги
3 3 1) Ташкент, родственники, детство 2) Москва, Питер, Рига, студенчество 3) горы, поход, голландцы
4 4 1) интеграл, Академгородок 2) конкурс, мисс, интеграл 3) костюмы, дочь 4) Узбекистан
5 2 1) Лондон 2) Италия
6 2 1) приезд, Аврорин, академик, филология 2) Марр, Сталин, ссора
7 2 1) больница, университет, журналистика, муж 2) деньги, филология, медсестра, вечерняя школа
8 1 1) университет, студенты
9 4 1) жилье, родители 2) работа, сталинское учение 3) болезнь 4) Марр, статьи, студенты

Моя предварительная модель содержит 22 топика. Так как количество тем для модели LDA задается вручную, это число мы проверим в качестве оптимального значения аргумента k (об этом в разделе “Выбор количества топиков”).

Переходим к практике

Подготовка данных

Загрузим библиотеки, которые нам понадобятся для проведения исследования.

library(purrr)
library(xml2)
library(dplyr)
library(tidyverse)
library(tidytext)
library(stopwords)
library(topicmodels)
library(furrr)
library(reshape2)
library(udpipe)

Напишем функцию, которая достает из XML-файла автора и текст воспоминаний и передает эту информацию в тиббл.

read_memoir <- function(filepath) {
  text <- read_xml(filepath)
  ns <- xml_ns_rename(xml_ns(text), d1 = "tei")
  rootnode <- xml_root(text)
  
  author <- xml_find_first(rootnode, "//tei:teiHeader//tei:author", ns)|> 
    xml_text()|> 
    trimws()
  text <- xml_find_first(rootnode, "//tei:text", ns)|> 
    xml_text()|> 
    trimws()
  
  tibble(
    author = author,
    text = text
  )
}

Загрузим данные из папки с помощью функции map_dfr()

path_to_folder <- "C:/Users/TurboPC/Desktop/katya/memories"
files <- list.files(path_to_folder,  pattern = "\\.xml$", full.names = TRUE)
all_memoirs_tbl <- files |>
  map_dfr(read_memoir)

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

#очищаем от чисел с использованием регулярок
all_memoirs_tbl <- all_memoirs_tbl |> 
  mutate(text = str_remove_all(text, '\\d+'))

#делим тексты на токены-слова
memoirs_tokens <- all_memoirs_tbl |> 
  unnest_tokens("word", "text")

#очищаем от стоп-слов
sw <- stopwords(language = "ru")
memoirs_words_tidy <- memoirs_tokens  |>  
  filter(!word %in% sw)

Теперь отберем только существительные. Для этого лемматизируем токены с помощью пакета SynTagRus.

str(udpipe_download_model(language='russian-syntagrus'))
russian_syntagrus <- udpipe_load_model(file = "russian-syntagrus-ud-2.5-191206.udpipe")
memoirs_annotate <- udpipe_annotate(russian_syntagrus, memoirs_words_tidy$word, doc_id =memoirs_words_tidy$author)

memoirs_pos <- memoirs_annotate |> 
  as_tibble() |> 
  select(-paragraph_id)

memoirs_nouns <- memoirs_pos |> 
  filter(upos == 'NOUN') |> 
  select(doc_id, lemma)

Тематическое моделирование

Считаем абсолютную встречаемость слов.

memoirs_counts <- memoirs_nouns |> 
  count(lemma, doc_id)

Преобразуем в формат dtm

memoirs_dtm <- memoirs_counts |> 
  cast_dtm(doc_id, term = lemma, value = n)

Выбор количества топиков

Как уже было отмечено ранее, предполагается, что в текстах содержится 22 топика. Найдем количество топиков, рекомендуемое пакетом LDA Tuning, и сравним это число с гипотетическим.

library(devtools)
devtools::install_github("nikita-moor/ldatuning")
library(ldatuning)

n_topics <- c(2, 4, 8, 16, 22, 32, 64)
result <- FindTopicsNumber(
  memoirs_dtm,
  topics = n_topics,
  metrics = c("Griffiths2004", "CaoJuan2009", "Arun2010", "Deveaud2014"),
  method = "Gibbs",
  control = list(seed = 122025),
  verbose = TRUE
)
fit models... done.
calculate metrics:
  Griffiths2004... done.
  CaoJuan2009... done.
  Arun2010... done.
  Deveaud2014... done.
FindTopicsNumber_plot(result)

На графике видно, что математические модели предлагают построить модель для 16 топиков. Доверимся :)

Я делаю выбор с торону меньшего числа топиков, так как ожидаю, что модель не сможет выделить те темы, которые выделила я, и породит множесто смежных тем, различающихся 1 ключевым словом. Итак, подставляем k = 4.

memoirs_lda <- topicmodels::LDA(memoirs_dtm, k = 16, control = list(seed = 122025))

Главные слова для топиков

Выделим первые 7 терминов для топиков.

memoirs_topics <- tidy(memoirs_lda, matrix = "beta")
memoirs_top_terms <- memoirs_topics |> 
  filter(topic < 23) |> 
  group_by(topic) |> 
  arrange(-beta) |> 
  slice_head(n = 7) |> 
  ungroup()

#здесь на графике мы видим основные термины в топиках
memoirs_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=4) +
  coord_flip()

Некоторые топики пересекаются. Особенно ярко это заметно на примере Т5, Т9 и Т14. У них общие ключевые слова время, год, человек, вопрос, которые содержатся практически во всех текстах. Предполагаю, что эти темы не будут доминирующими ни в одном документе.

Забавно, что в топике №1 мы можем заметить глагол “интеграть”. Это неправильно лемматизировалось существительное “интеграл”. Похожая ситуация случилась с “Лидией Ивановной” в Т8 (получились лидий и ивановно) :) Моей работе это не помешает, поэтому оставлю для поднятия настроения внимательного читателя.

Распределение топиков по документам

memoirs_documents <- tidy(memoirs_lda, matrix = "gamma")

memoirs_documents |> 
  filter(topic == 1) |> 
  arrange(-gamma)

memoirs_documents |> 
  group_by(document) |> 
  summarise(sum = sum(gamma))

memoirs_id <- memoirs_words_tidy  |> 
  group_by(author) |> 
  summarise(nwords = n()) |> 
  arrange(-nwords) |> 
  slice_head(n = 9) |> 
  pull(author)
memoirs_documents |> 
  filter(document  %in%  memoirs_id) |> 
  arrange(-gamma) |> 
  ggplot(aes(as.factor(topic), gamma, color = document)) + 
  geom_boxplot(show.legend = F) +
  facet_wrap(~document, nrow = 3, ncol=3) +
  xlab(NULL) 

Из этого графика мы можем заглючить, что каждый информант говорит на какую-то свою тему (темы). Значит, мы можем идентифицировать текст по топику.

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

memoirs_documents |>
  arrange(-gamma) |>
  filter(gamma > 0.1) |>
  ggplot(aes(document, gamma, fill = as.factor(topic))) +
  geom_col(position = 'stack') +
  geom_text(
    aes(label = paste("T", topic)),
    position = position_stack(vjust = 0.5),
    color = "white",
    fontface = "bold",
    size = 3,
    check_overlap = TRUE 
  ) +
  labs(
    title = "Распределение топиков по документам",
    x = "Документ",
    y = "Гамма",
    fill = "Топик"
  ) +
  theme_minimal()

Обратим внимание, что топики 5, 9 и 14, как предполагалось ранее, почти не присутствуют в текстах.

Сравнение полученных данных с предварительной моделью

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

Номер текста Количество топиков, выделенное мной Количество топиков, выделенное с помощью LDA Совпадение количества
1 1 1 да
2 3 1 нет
3 3 1 нет
4 4 2 нет
5 2 1 нет
6 2 2 да
7 2 2 да
8 1 1 да
9 4 1 нет

Интерпретация результатов

  • Меньшее количество топиков, выделенных моделью, по сравнению с количеством топиков, выделенных мной, свидетельствует о высокой степени связности исследуемых текстов., которую можно объяснить двумя факторами:

    • наличие в тексте рематически наполненных интерферированных высказываний (Земская 1973) (говорящий, начиная высказывание, не планирует его до конца и после заполнения всех валентностей предиката использует последний член первоначальной конструкции для начала второй, содержащей новую информацию, не прерывая высказывания), которые, относясь к синтаксическому устройству высказывания, влияют и на лексическое наполнение, что в конечном итоге отражается на абсолютной частотности слов в тексте, которая используется в алгоритме LDA;

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

  • Для каждого текста количество топиков <= 2, значит, информанты в своих монологах последовательны, не перескакивают между совсем разными темами. Предполагаю, что это связано с высокой культурой речи, образованием и профессиональной деятельностью говорящих: даже спонтанную речь они могут построить логично, подобно научному стилю.

  • Топики оказались распределены по документам таким образом, что каждый респондент имеет свой уникальный сет тем. Это позволяет нам однозначно определить по топику, какому тексту он принадлежит, и подтвердить прописную истину, что “Воспоминание” – жанр, которому свойственна уникальность содержания.

Перспективы исследования

Конечно, сделать какие-то выводы о корпусе или о целом жанре на результатах анализа всего 9 текстов невозможно. Эта работа – лишь попытка применить к сложной для обработки устной речи статистические методы и найти новые вопросы, которми можно себя увлечь.

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

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


использованная литература

Земская, Е. 1973. Русская разговорная речь. Москва: Наука.