Введение

Советский политический дискурс периода Перестройки представляет собой достаточно интересный феномен, так как, с одной стороны, он впитал в себя традиции, введенные в обиход Брежневым и Хрущевым в 1950-1980х годах, с другой же в это время была попытка “порвать с прошлым и начать все заново”, что не могло не отразиться на официальной речи. Данная практическая работа ставит перед собой цель пусть и не изучить советский политический дискурс времен Горбачева во всем его объеме, но хотя бы продемонстрировать его особенности на конкретных примерах (Отчетных докладах ЦК КПСС).

Сделано это следующим образом - по материалам отчетов на XXVII и XXVIII съездах КПСС собраны датасеты биграмм, на которых созданы, изучены и визуализированы объекты класса IGraph

Гипотеза

Давайте, чтобы знать, что искать, сформулируем гипотезы о том, какие сдвиги могли бы произойти в официальной речи в этот период. Мои предположения на данный счет таковы:

Сбор данных

В качестве исходных данных я взяла тексты Отчетных докладов ЦК КПСС на XXVII и XXVIII Съездах в формате .txt (ссылки на ресурсы: https://lib.ru/MEMUARY/GORBACHEV/doklad_xxvi.txt и http://www.uaio.ru/5/85/xxviii_1.htm) и создала на их основе датасет биграмм, собранный с помощью кода ниже:

cleaned_bigrams <- function(file_path, cnt) {
    model <- udpipe_download_model(language = "russian-syntagrus")
    ud_model <- udpipe_load_model("/content/russian-syntagrus-ud-2.5-191206.udpipe")

    text <- as.data.frame(udpipe_annotate(ud_model, x = paste(readLines(file_path, encoding = "UTF-8"), collapse = " ")))
    lemmas <- paste(text$lemma, collapse = " ")
    text_df <- as.data.frame(udpipe_annotate(ud_model, x = lemmas))

    bigrams_by_sentence <- text_df |>
    unnest_tokens(bigram, sentence, token = "ngrams", n = 2) |>
    drop_na(bigram)

    stop_words_ru <- c(stopwords("ru", "nltk"))

    my_bigrams <- bigrams_by_sentence |>
        separate(bigram, c("word1", "word2"), sep = " ") |>
        filter(!word1 %in% stop_words_ru, !word2 %in% stop_words_ru) |>
        count(word1, word2, sort = TRUE) |>
        filter (n >= cnt)
    
    return(my_bigrams)
}

Комменатрий:

Вспомогательный код

Также я решила заранее написать функцию, которая будет рисовать мои графы. Этот код я написала еще для предыдущего дз - и он мне понравился. Короткое описание - размер вершины зависит от степени, цвет (прозрачность) ребра - от веса. Таким образом, я учту на визуализации графа и атрибуты вершин (степень), и атрибуты ребер (вес). Также, только для визуализации, я ввела стрелки, показывающие направленность биграммы - я не стала добавлять эти данные в датасет, потому что иначе бы мой граф стал ориентированным, а для понимания словоупотреблений одного слова нам нужно учесть биграммы обоих порядков (например, можно говорить “наша партия” или “коммунистическая партия”, а можно - “партия Ленина”).

visualization <- function (my_graph, ttl) {
    ggraph(my_graph, layout = "nicely") +
    geom_edge_link(aes(edge_colour = n, edge_alpha = n),
                  arrow = arrow(angle = 30,
                               length = unit(0.25, "cm"),
                               ends = "last",
                               type = "closed"),
                   width = 1.5,
                   show.legend = TRUE) +
    geom_node_point(aes(size = degree), color = "steelblue") +
    geom_node_text(aes(label = name), repel = TRUE, size = 4) +
    scale_edge_colour_gradient(low = "lightblue", high = "darkblue") +
    scale_edge_alpha(range = c(0.2, 1)) +
    theme_graph() +
    labs(title = ttl,
         edge_colour = "Вес (частота)",
         size = "Степень узла")
}

XXVII Съезд - создание, анализ и визуализация

Поначалу проведем детальный анализ каждого из графов, и лишь потом сформулируем некоторый вывод (а еще будет бонус). Так как по хронологии раньше был XXVII Съезд (1986 год), то и мы начнем с него

Граф полностью

Создадим объект IGraph на основе датасета биграмм (получен с помощью функции cleaned_bigrams), добавим атрибут степени для вершины и визуализируем его с помощью написанной ранее функции visualization:

file_path <- "/content/27 Съезд.txt"
bigrams_27 <- cleaned_bigrams(file_path, 1770)

graph_27 <- graph_from_data_frame(bigrams_27)
d <- as.numeric(degree(graph_27))
V(graph_27)$degree <- d

visualization(graph_27, "Граф совместной встречаемости слов на 27 съезде КПСС")

Получим следующую картинку:

Найдем его точки сочленения, запустив функцию articulation_points(), получим следующий список: человек, советский, это, весь, сила, мир, качество, повышение, уровень, движение, наш, страна, ставить, задача, новый, редакция, программа, пленум, цк, кпсс, партия, политика, социалистический развитие, социальный, технический, хозяйство, народный, массовый, оружие, ядерный, орган, управление, хозяйственный, последний, год, й, рабочий, международный, доход, значение (41/133)

Продолжим анализ подсчетом некоторых показателей графа - числа вершин, ребер и компонент, плотности, транзитивности:

vcount(graph_27)
ecount(graph_27)
edge_density(graph_27)
components(graph_27)$no
transitivity(graph_27)
#modularity(graph_27)
Число вершин Число ребер Плотность Число компонент Транзитивность Модулярность
133 114 0.0065 27 0 0
  1. Модулярность равна нулю, так как мы не ввели никаких классовых делений на вершины (слова). Чисто теоретически можно ввести отдельный признак “вид леммы” (существительное/прилагательное/глагол и пр., зашифровав метки классов числами) или “эмоциональный тон леммы” или даже придумать свое деление типа “политическое/не политическое”, но на данном этапе это видится мне ненужным.

  2. Транзитивность графа равна нулю, а потому можно сказать, что сеть не склонна к замыканию и образованию закрытых треугольников

  3. Плотность графа также достаточно низкая, что свидетельствует о разреженности графа (малом числе ребер)

  4. По числу ребер и вершин можно понять, что граф несвязен (то есть распадается на компоненты связности, которых тут аж 27)

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

Fun fact: если мы захотим найти в этом графе клики, состоящие хотя бы из трех вершин, у нас это не получится, так как в графе нет ни одной такой группы (cliques(graph_27, min=3) выдает None)

Его подграф

Напишем следующий код, который нам извлечет и визуализирует наибольшую компоненту связности этого графа:

lgc <- largest_component(graph_27)
visualization(lgc, "Граф совместной встречаемости слов на 27 съезде КПСС (largest component)")

Заметим следующее :

  1. Наиболее тяжеловесные связи - это “научный -> технический” и “социальный -> экономический”, “весь -> это”, “советский -> союз” и “советский -> человек

  2. Наиболее тяжеловесные вершины - это “развитие”, “социалистический”, “социальный”, “наш”, “советский”, “весь”, “партия”, “программа”

  3. Выделяются несколько ветвей слов, близких по тематике: экономическая (самая левая, в которой встречаются слова “продукция”, “эффективность”, “качество”), научная (самая правая, в которой встречаются слова “научный”, “технический”, “прогресс” и “ускорение”) и политическая (самая верхняя, со словами “устав”, “партия”, “программа”, “пленум”, “цк”)

Соберем некоторые данные уже о компоненте:

Число вершин Число ребер Плотность Транзитивность Диаметр Модулярность
64 71 0.0176 0 12 0
  1. Интересно, что в наибольшую компоненту связности входит примерно половина всех вершин (64 из 133)

  2. В компоненте есть циклы - наша компонента не является деревом

  3. Диаметр компоненты равен 12, например: “вопрос” -> “ставить” -> “задача” -> “новый” -> “редакция” -> “программа” -> “партия” -> “наш” -> “весь” -> “уровень” -> “повышение” -> “качество” -> “продукция”

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

Анализ через метод Edge-betweenness:

ceb <- cluster_edge_betweenness(lgc)
par(mar = rep(0, 4))
plot(ceb, lgc)
modularity(ceb)

Заметим, что:

  • Выделилось достаточно много сообществ, некоторые из которых четко образуют словосочетания: “внешний” и “политика”, “движение” и “вперед”, “это” и “значить”.

  • Есть доминирующее сообщество (в центре), но на его долю приходится меньше половины всех вершин,

  • Модулярность равна 0.42 (ну такое…)

Анализ через метод InfoMAP:

ci <- cluster_infomap(lgc)
par(mar = rep(0, 4))
plot(ci, lgc)
modularity(ci)

Заметим, что:

  • Выделилось всего 4 сообщества, причем одно, доминирующее, покрывает больше половины вершин, а остальные - это просто цепочки из словосочетаний разной длины (есть группа, состоящая только из двух слов: “редакция” и “программ”, а есть - состоящая из пяти: “апрельский”, “пленум”, “цк”, “устав”, “кпсс”)

  • Модулярность равна 0.30 (еще хуже…)

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

XXVIII Съезд - создание, анализ и визуализация

Граф полностью

Создадим объект IGraph на основе датасета биграмм (получен с помощью функции cleaned_bigrams), добавим атрибут степени для вершины и визуализируем его с помощью написанной ранее функции visualization:

file_path <- "/content/28 Съезд.txt"
bigrams_28 <- cleaned_bigrams(file_path, 900)

graph_28 <- graph_from_data_frame(bigrams_28)
d <- as.numeric(degree(graph_28))
V(graph_28)$degree <- d

visualization(graph_28, "Граф совместной встречаемости слов на 28 съезде КПСС")

Получим следующую картинку:

Найдем его точки сочленения, запустив функцию articulation_points, получим следующий список: многонациональный, наш, преобразование, должен, хозяйствование, форма, новый, съезд, компартия, союзный, политический, социальный, энокомический, реформа, сила, общество, экономика, отношение, партия, это, весь, организация, партийный, программный, человек, свой, политика, народ, интерес, жизнь, народный, социалистический, год, вопрос, важный, перестройка, явление, коренной (38/157).

Посчитаем (как и ранее), некоторые его показатели:

Число вершин Число ребер Плотность Число компонент Транзитивность Модулярность
157 114 0.0047 45 0.0236 0

Интересно, что:

  1. Число вершин (при равном количестве ребер) здесь больше - получается, граф 28 Съезда еще более разреженный, чем раньше (о чем свидетельствует и меньшая, чем раньше, плотность графа)

  2. Внезапно транзитивность не равна нулю, а значит в графе есть клики, содержащие три и больше вершин (а именно - одна клика из трех вершин “весь” + “это” + “партия”)

  3. Модулярность равна нулю по тем же причинам, что и модулярность в графе 27 Съезда - вершины графа не поделены на классы

В связи с этим (а также с унификацией исследования), давайте вновь перейдем к подграфу этого графа - наибольшей компоненте связности.

Его подграф

Запустим следующий код:

lgc <- largest_component(graph_28)
visualization(lgc, "Граф совместной встречаемости слов на 28 съезде КПСС (largest component)")

На выходе - имеем вот такую картинку:

Заметим следующее:

  1. Cамые тяжеловесные ребра - это “весь -> это”, “межнациональный -> отношение”, “революционный -> преобразование”, “съезд -> кпсс”, “xxvii -> съезд” и “многонациональный -> государство”

  2. Вершины с наибольшей степенью - это “наш”, “партия”, “весь”, “новый”, “должен”, “союзный” и “съезд”

  3. Весь граф можно разделить на несколько ветвей, выходящих из слов “новый” и “политический”: социально-экономическую (со словами “свобода”, “сфера”, “реформа”), две политические (та, в которой есть слово “кпсс”, и та, в которой есть слово “партия”) и хозяйственную (со словом “хозяйствование”)

Соберем некоторые данные уже о компоненте:

Число вершин Число ребер Плотность Транзитивность Диаметр Модулярность
43 44 0.0243 0.0340 11 0

Из этих цифр можно понять следующее:

  1. Наша компонента содержит циклы, так как в ней больше, чем 42 ребра (всего два - “весь + партия + это” и “весь + это + партия + наш + страна”)

  2. В компоненту входит около четверти всех вершин (43 из 157)

  3. Диаметр компоненты равен 11, пример: “xvii” -> “съезд” -> “компартия” -> “союзный” -> “новый” -> “политический” -> “сила” -> “общество” -> “наш” -> “партия” -> “весь” -> “комплекс”

Перейдем к анализу (используем те же методы, что и ранее, поэтому код я не дублирую):

Анализ с помощью метода edge-betweenness:

Заметим, что:

  • Всего выделилось шесть сообществ, среди которых ярко выделяется одно доминирующее (правое, на него приходится больше половины вершин, 23 из 43)

  • Модулярность равна 0.50 (лучше, чем для XXVII Съезда, но все еще маловато)

Анализ с помощью метода InfoMAP:

Заметим, что:

  • Выделилось девять сообществ, среди которых можно выбрать доминирующее (центральное), но оно покрывает меньше половины вершин графа (в отличие от первого деления), всего лишь 13 из 43

  • Модулярность равна 0.61 (достаточно высокое, уже неплохо!),

Бонус - сравнительный анализ эго-графов слова “партия”

Напишем следующий код, который позволит нам посмотреть прицельно на одно слово, а именно - на слово “партия”. Тут я не хочу смотреть на веса (интенсивность встречаемости), а хочу просто посмотреть варианты употреблений:

Для XXVII Съезда граф будет выглядеть так:

p3 <- make_ego_graph(
  graph_27,
  order = 2,
  nodes = "партия",
  mode = "all"
)[[1]]

par(mar = rep(0,4), cex = 0.7)
layout_p3 <- layout_with_kk(p3)

plot(p3, vertex.size=6,
     edge.arrow.size = 0.5,
     vertex.label.dist = 1,
     edge.curved = 0.2,
     edge.color = "grey80",
     vertex.color = "plum",
     layout = layout_p3)

Здесь очевидно следующее:

Посмотрим, что происходило пять лет спустя (для этого надо всего лишь заменить рассматриваемый граф, то есть поменять переменную, поэтому код я вновь дублировать не буду):

Некоторые изменения виды невооруженным взглядом, а именно:

На этом у меня всем, всем спасибо за внимание!