Многоклассовая классификация

Отчетная работа по модулю «Машинное обучение»

Автор

Авдеева Полина

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

31 мая 2026

1 Введение и постановка задачи

1.1 Актуальность и проблема

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

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

В данной работе предлагается подход к атрибуции художественных текстов на материале корпуса «A Small Collection of British Fiction» (27 произведений британской прозы конца XVIII — XIX вв. 1) с применением методов машинного обучения и моделирования данных.

1.2 Цели и задачи работы

Цель работы — на основе указанного корпуса провести стилистический анализ авторов и построить модель классификации текстов по авторам с использованием пакета {tidymodels}.

В ходе работы необходимо выполнить следующие задачи:

  1. Импортировать корпус британской прозы и подготовить его к анализу (тексты из архива british_fiction, метаданные из файла overview);
  2. Выполнить предобработку текстов, включающую токенизацию, нормализацию регистра и удаление служебных символов;
  3. Извлечь количественные лингвистические признаки для каждого текста (например, n-граммы, частоты слов, длину предложений и слов, показатели лексического разнообразия и другие характеристики текста);
  4. Провести разведывательный анализ признаков;
  5. Построить модель классификации произведений по авторам с помощью фреймворка {tidymodels};
  6. Выполнить кросс-валидацию и сравнить модели по производительности;
  7. Визуализировать результаты, а также выделить наиболее значимые признаки для классификации;
  8. Интерпретировать полученные данные.

2 Сбор и подготовка данных

2.1 Загрузка необходимых пакетов

Для выполнения анализа подключаются пакеты, используемые для обработки текстов, построения моделей машинного обучения и визуализации результатов.

Раскрыть блок с кодом
library(tidyverse)
library(textrecipes)
library(tidymodels)
library(tidytext)
library(stylo)
library(DT)
library(baguette) 
library(discrim) 
library(future)

Основной набор пакетов:

  • {tidyverse} — экосистема пакетов, обеспечивающая полный (и «чистый») цикл работы с данными: импорт, преобразование и визуализацию;
  • {tidymodels} — пакет, предназначенный для построения и оценки моделей машинного обучения: ресемплинг, настройка гиперпараметров, валидация;
  • {textrecipes} — пакет для препроцессинга текстовых данных (специализированное расширение для экосистемы {tidymodels}), благодаря которому текст преобразуется в числовые признаки для использования в моделях (токенизация, стемминг, TF‑IDF и другие методы);
  • {tidytext} — пакет для текстового анализа в стиле tidy (токенизация, удаление стоп-слов, подсчёт частот);
  • {stylo} — пакет для формирования стилометрических выборок и расчета авторского стиля.

Дополнительный:

  • {DT} — пакет для интерактивного отображения таблиц (DataTables), обеспечивающий пагинацию, поиск, фильтрацию и сортировку данных.
  • {baguette} — пакет, включающий в себя набор моделей на основе ансамблей деревьев решений, совместимый с экосистемой {tidymodels};
  • {discrim} — пакет, реализующий методы дискриминантного анализа;
  • {future} — пакет, включающий в себя инструменты для организации параллельных вычислений и ускорения ресурсоемких операций.

2.2 Обзор данных

Для исследования предоставляется корпус «А Small Collection of British Fiction», включающий 27 произведений британской художественной прозы XVIII-XIX веков на английском языке.

Для удобства восприятия и предварительного анализа дополнительно формируется обзорная Таблица 1 с полными именами авторов и названиями произведений, входящих в состав корпуса. Данная таблица используется исключительно для визуализации состава корпуса и не участвует в последующей обработке данных.

Раскрыть блок с кодом
datatable(
  corpus_list,
  colnames = c("Автор произведения", "Произведение"),
  options = list(pageLength = 5)
)
Таблица 1: Перечень произведений, входящих в состав корпуса

3 Предварительная обработка данных

3.1 Корпус текстов

Названия файлов корпуса организованы по следующему принципу:

  • Если текст принадлежит одной из сестер Бронте, в обозначении автора указаны инициал имени, фамилия и короткое название произведения (ABronte_Agnes.txt, CBronte_Villette.txt, EBronte_Wuthering);
  • Если текст принадлежит остальным авторам, указаны лишь фамилия и короткое название произведения (Austen_Emma.txt, Dickens_David.txt, Eliot_Mill.txt).

Поскольку информация о произведениях хранится одновременно в текстовых файлах и таблице метаданных, необходимо сформировать единые идентификаторы, позволяющие корректно объединить оба источника данных. В качестве таких идентификаторов используются сокращенное имя автора (author_short) и сокращенное название произведения (short_title).

Для формирования идентификаторов из имени файла извлекаются две составляющие: часть до символа подчеркивания (_), автор, а также часть после него — короткое название произведения.

Раскрыть блок с кодом
# Шаг 1. Извлекаются ключи для объединения таблицы с метаданными
texts_corpus_md <- tibble(
  file_path = list.files('british_fiction', pattern = '\\.txt$', full.names = T)
) |> 
  mutate(
    filename = basename(file_path), 
    # Извлекается автор (до подчеркивания)
    author_short = str_extract(filename, "^[^_]+"),
    # Извлекается короткое название произведения (после подчеркивания)
    short_title = filename |> 
      str_remove("\\.txt$") |>  
      str_extract("(?<=_).+$")
  )


# Шаг 2. Извлечение полного текста произведений для дальнейшего анализа. Отдельный шаг, так как при демонстрации таблицы тексты перегружали бы ее. 
texts_corpus <- texts_corpus_md |> 
  mutate(
    text = map_chr(file_path, ~ {
      readLines(.x, warn = F, encoding = 'UTF-8') |> 
        paste(collapse = ' ') 
    })
  )

На текущем этапе Таблица 2 содержит четыре столбца. Пятый столбец (text), содержащий полные тексты произведений, не выводится в таблицу ввиду большого объема данных. Тем не менее, именно он будет использоваться на последующих этапах анализа.

  1. file_path — путь к файлу, содержащий название корпуса, а также название файла (шаблон: `название_корпуса/название_файла.txt`;
  2. filename — название файла с текстом, содержащий в себе краткую информацию об авторе и тексте;
  3. author_short — короткая информация об авторе;
  4. short_title — короткое название произведения.
Раскрыть блок с кодом
datatable(texts_corpus_md, 
          options = list(pageLength = 5))
Таблица 2: Данные, полученные после извлечения информации из файлов корпуса (без текста)

Наибольшее значение для дальнейшего анализа имеют столбцы author_short и short_title, используемые в качестве ключей для объединения данных. Кроме того, столбец text содержит полный текст произведения и служит источником признаков для последующего стилометрического анализа.

3.2 Метаданные (файл overview)

Метаданные, представленные в Таблица 3, требуют дополнительной обработки перед объединением с текстовым корпусом.

Раскрыть блок с кодом
# Шаг 1. Загрузка файла .tsv с метаданными произведений 
raw_metadata <- read_tsv('overview.tsv')
Раскрыть блок с кодом
# Шаг 2. Создание таблицы с метаданными до предобработки
datatable(
  raw_metadata, 
  options = list(pageLength = 5)) 
Таблица 3: Метаданные до предобработки

Исправленная Таблица 4 содержит в себе следующие изменения:

  1. Исправлено некорректное отображение заголовков столбцов;
  2. Сформированы ключевые переменные для последующего объединения данных;
  3. Исправлены обнаруженные опечатки в сокращенных названиях.
Раскрыть блок с кодом
# Шаг 1. Первая проблема, которую необходимо решить — битое отображение названий столбцов 
lines <- readLines("overview.tsv")
lines[1] <- gsub("textID author", "textID\tauthor", lines[1])   # Исправление первой строки: замена "textID author" на "textID\tauthor"
md_temp <- tempfile()   # Временный файл
writeLines(lines, md_temp)
metadata <- read.delim(md_temp, sep = "\t", header = TRUE, stringsAsFactors = FALSE)
unlink(md_temp)


# Шаг 2. Создание коротких имен авторов и названий произведений, чтобы далее можно было объединить данные
metadata_clear <- metadata |> 
  mutate(
    # Разделение имени в столбце author
    surname = str_extract(author, '^[^,]+'),
    name = str_extract(author, '(?<=, ).+'), 
    # Короткое название
    short_title = title |> 
      str_remove('^The ') |> 
      str_extract('^[A-Za-z]+')
  ) |> 
  select(-surname, -name)


# Шаг 3. Необходимо поправить неточности в названиях, которые есть в метаданных. Для этого нужно точечно обратиться к ошибкам.
metadata_clear_new <- metadata_clear |> 
  mutate(
    short_title = case_when(
      author == 'Bronte, Anne' & short_title == 'Tentant' ~ 'Tenant',
      author == 'Sterne, Laurence' & short_title == 'Tristam' ~ 'Tristram', 
      author == 'Thackeray, William Makepeace' & short_title == 'History' ~ 'Pendennis',
      author == 'Thackeray, William Makepeace' & short_title == 'Luck' ~ 'Barry',
      TRUE ~ short_title
    )
  ) |> 
  # А также добавляются короткие имена авторов
  mutate(
    author_short = case_when(
      authorID %in% c('AB', 'CB', 'EB') ~ paste0(authorID, 'ronte'),
      TRUE ~ str_extract(author, '^[^,]+')
    )
  )
Раскрыть блок с кодом
# Шаг 4. Вывод таблицы для просмотра результатов
datatable(metadata_clear_new, 
          options = list(pageLength = 5))
Таблица 4: Корректные метаданные

Данные исправления необходимы для успешного объединения таблицы метаданных с информацией, извлеченной из имен текстовых файлов.

3.3 Общая таблица с данными

Теперь необходимо объединить таблицы по ключам (author_short и short_title)

Раскрыть блок с кодом
victorian_corpus_combined <- inner_join(
  texts_corpus, 
  metadata_clear_new,
  by = c('author_short', 'short_title')
)
Раскрыть блок с кодом
victorian_corpus_combined |> 
  select(-text) |> # Чтобы не перегружать таблицу, убирается столбец с текстом            
  datatable(options = list(pageLength = 5))

После объединения таблиц было получено 27 наблюдений, что соответствует количеству произведений в исходном корпусе. Это свидетельствует о том, что ключи сформированы корректно и при объединении отсутствуют потери данных.

3.4 Очистка общей таблицы

Для дальнейшего анализа формируются две версии текста. Исходная версия (text_raw) сохраняется без изменений и может использоваться для расчета структурных характеристик текста, например, длины предложений или анализа пунктуации. Одновременно с этим создается очищенная версия текста (text), предназначенная для токенизации и извлечения лексических признаков.

На этапе очистки текста выполняются:

  • Приведение к нижнему регистру;
  • Удаление знаков пунктуации, чисел и лишних пробельных символов.
Раскрыть блок с кодом
# Базовая очистка текста
victorian_corpus_combined <- victorian_corpus_combined |>
  mutate(
    text_raw = text,

    text = text |>
      str_to_lower() |>
      str_remove_all("[[:punct:]]") |>
      str_remove_all("\\d+") |>
      str_squish()
  )

А также создается корпус текстов для последующего анализа и сэмплирования.

Раскрыть блок с кодом
# Шаг 1. Сбор текстов 
corpus_texts <- victorian_corpus_combined$text


# Шаг 2. Имена документов
names(corpus_texts) <- paste(
  victorian_corpus_combined$author_short,
  victorian_corpus_combined$short_title,
  seq_len(nrow(victorian_corpus_combined)),
  sep = "_"
)

3.5 Сэмплирование

Для увеличения количества наблюдений и последующего стилометрического анализа каждый текст разбивается на отдельные фрагменты фиксированного размера. В данной работе используются непересекающиеся выборки по 2000 слов, сформированные с помощью функции make.samples() из пакета {stylo}. Такой подход позволяет увеличить количество наблюдений и анализировать не только произведения целиком, но и отдельные текстовые сегменты.

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

Размер фрагмента 2000 слов был выбран как компромисс между сохранением стилометрической информации и увеличением числа наблюдений для последующего моделирования.

Раскрыть блок с кодом
# Шаг 1. Преобразование в список векторов слов 
corpus_words_list <- lapply(corpus_texts, function(txt) {
  unlist(str_split(txt, "\\s+"))
})

# Шаг 2. Сэмплирование
corpus_samples <- make.samples(corpus_words_list,
                               sample.size = 2000,
                               sampling = "normal.sampling",
                               sample.overlap = 0,
                               sampling.with.replacement = FALSE)

4 Извлечение количественных лингвистических признаков

4.1 Лингвистические признаки произведений

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

В настоящей работе получены следующие признаки:

  1. Общее количество слов (word_count);
  2. Количество предложений в тексте (sentence_count);
  3. Средняя длина слова (mean_word_length);
  4. Средняя длина предложения (mean_sentence_length);
  5. Лексическое разнообразие (Type-Token Ratio, TTR2) (ttr)

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

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

Раскрыть блок с кодом
ling_features <- victorian_corpus_combined |>
  mutate(
    # Общее количество слов
    word_count = str_count(text, "\\S+"),
    # Количество предложений
    sentence_count = str_count(text_raw, "[.!?]+"),
    # Средняя длина слова
    mean_word_length = map_dbl(
      str_split(text, "\\s+"),
      ~ mean(nchar(.x))
    ),
    # Средняя длина предложения
    mean_sentence_length =
      word_count / pmax(sentence_count, 1),
    # Лексическое разнообразие
    ttr = map_dbl(
      str_split(text, "\\s+"),
      ~ length(unique(.x)) / length(.x)
    )
  )

4.2 Биграммы

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

Распределение биграмм и наиболее характерные примеры будут рассмотрены в следующем разделе (Глава 5).

Раскрыть блок с кодом
# Извлечение биграмм
bigrams <- victorian_corpus_combined |>
  select(author_short, short_title, text) |>
  unnest_tokens(bigram,
                text,
                token = "ngrams",
                n = 2
  )

4.3 Most Frequent Words (MFW)

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

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

Раскрыть блок с кодом
# Шаг 1. Формирование списка 500 наиболее частотных слов
mfw <- make.frequency.list(corpus_samples)[1:500]


# Шаг 2. Расчет частот MFW для каждого текстового фрагмента
corpus_tf <- make.table.of.frequencies(corpus_samples, mfw) |> 
  as.data.frame.matrix() |> 
  rownames_to_column("id") |> 
  as_tibble()


# Шаг 3. Извлечение информации об авторе из идентификатора документа
corpus_tf <- corpus_tf |> 
  separate(id, into = c("author", "title", NA), sep = "_") 

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

5 Разведывательный анализ (EDA)

5.1 Анализ лингвистических признаков произведений

Таблица 5 представляет полную информацию по извлеченным из произведений лингвистическим признакам.

Раскрыть блок с кодом
ling_features |>
  select(
    author_short,
    short_title,
    mean_word_length,
    mean_sentence_length,
    ttr
  ) |>
  datatable(options = list(pageLength = 5))
Таблица 5: Лингвистические признаки произведений

На Рисунок 1 представлена средняя длина слова по авторам корпуса. Показатель рассчитывался как среднее количество символов в слове после очистки текста от знаков пунктуации и приведения к нижнему регистру.

Средняя длина слова демонстрирует низкую межавторскую вариативность. Максимальное значение наблюдается у Джейн Остен (4.39 символа) и у Генри Филдинга (4.38), минимальное — у Самюэла Ричардсона (4.11).

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

Раскрыть блок с кодом
ling_features |>
  group_by(author_short) |>
  summarise(mean_word_length = mean(mean_word_length)) |>
  ggplot(aes(reorder(author_short, mean_word_length),
             mean_word_length)) +
  geom_col(fill = '#A56644') +
  geom_text(aes(label = sprintf("%.2f", mean_word_length)),
            hjust = -0.2, 
            color = "#322A09", 
            size = 2.8, 
            family = "inter") +
  coord_flip(ylim = c(0, 5)) +
  labs(x = 'Автор', 
       y = 'Средняя длина слова') + 
  theme_minimal(base_family = 'inter') +
  theme(
    plot.background = element_rect(fill = '#E4DFD3',
                                   color = NA),
    panel.background = element_rect(fill = '#E4DFD3', 
                                    color = NA),
    panel.grid = element_blank(),
    panel.border = element_blank(),
    plot.border = element_blank(),
    axis.line = element_blank(),
    axis.text.x = element_blank(),  
    axis.ticks.x = element_blank(),  
    axis.text.y = element_text(color = '#322A09', 
                               size = 8),
    axis.title = element_text(color = '#322A09', 
                              size = 8),
    legend.position = 'none'
  )
Рисунок 1: Средняя длина слова (в символах) по авторам корпуса

На Рисунок 2 представлена средняя длина предложения по авторам корпуса. Показатель рассчитывался как отношение общего количества слов в произведении к числу предложений.

В отличие от средней длины слова, данный признак демонстрирует заметную межавторскую вариативность. Наибольшие значения наблюдаются у Генри Филдинга (29.34) и Лоренса Стерна (26.92), чьи произведения характеризуются более длинными предложениями. Наименьшие значения зафиксированы у Эмили Бронте (16.08) и Чарльза Диккенса (14.91), для которых характерны более короткие синтаксические конструкции.

Раскрыть блок с кодом
ling_features |>
  group_by(author_short) |>
  summarise(mean_sentence_length = mean(mean_sentence_length)) |>
  ggplot(aes(reorder(author_short, mean_sentence_length), mean_sentence_length)) +
  geom_col(fill = "#4E6B5C") +                     
  geom_text(aes(label = sprintf("%.2f", mean_sentence_length)),
            hjust = -0.2, 
            color = "#322A09", 
            size = 2.8, 
            family = "inter") +
  coord_flip(ylim = c(0, 32)) +                               
  labs(x = "Автор", 
       y = "Средняя длина предложения") + 
  theme_minimal(base_family = "inter") +
  theme(
    plot.background = element_rect(fill = "#E4DFD3", 
                                   color = NA),
    panel.background = element_rect(fill = "#E4DFD3", 
                                    color = NA),
    panel.grid = element_blank(),
    panel.border = element_blank(),
    plot.border = element_blank(),
    axis.line = element_blank(),
    axis.text.x = element_blank(),  
    axis.ticks.x = element_blank(),  
    axis.text.y = element_text(color = "#322A09", 
                               size = 8),
    axis.title = element_text(color = "#322A09", 
                              size = 8),
    legend.position = "none"
  )
Рисунок 2: Средняя длина предложений по авторам

Рисунок 3 показывает среднее лексическое разнообразие текстов каждого автора, измеренное через TTR.

Показатели считаются по следующей формуле: \[ TTR = \frac{\text{Число уникальных слов}}{\text{Общее число слов}} \] Низкое среднее значение TTR (как у Энтони Троллопа (0.05) и Сэмюэла Ричардсона (0.03)) указывает на большое количество повторов слов, а также менее разнообразный словарь относительно объема текста. Высокое среднее значение же указывает на то, что автор использует более разнообразный словарь и демонстрирует большее лексическое разнообразие (Лоренс Стерн (0.12), Шарлотта (0.09) и Энн Бронте (0.09)).

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

Раскрыть блок с кодом
ling_features |>
  group_by(author_short) |>
  summarise(
    mean_ttr = mean(ttr),
    .groups = "drop"
  ) |>
  ggplot(
    aes(
      reorder(author_short, mean_ttr),
      mean_ttr,
      fill = author_short
    )
  ) +
  geom_col(show.legend = FALSE) +
  geom_text(
  aes(label = sprintf("%.2f", mean_ttr)),
  hjust = 1.1,
  color = "#322A09",
  size = 2.8,
  family = "inter"
) +
  coord_flip(clip = "off") +
  labs(
    x = "Автор",
    y = "Средний TTR"
  ) +
  scale_fill_manual(values = my_colors) +
  scale_x_discrete(expand = expansion(mult = c(0.02, 0.02))) +
  scale_y_continuous(
    expand = expansion(mult = c(0, 0.1))
  ) +
  theme_minimal(base_family = "inter") +
  theme(
    plot.background = element_rect(
      fill = "#E4DFD3",
      color = NA
    ),
    panel.background = element_rect(
      fill = "#E4DFD3",
      color = NA
    ),
    panel.grid = element_blank(),
    axis.line = element_blank(),
    axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    axis.text.y = element_text(
      color = "#322A09",
      size = 8
    ),
    axis.title = element_text(
      color = "#322A09",
      size = 8
    ),
    legend.position = "none"
  )
Рисунок 3: Среднее значение показателя лексического разнообразия (TTR) по авторам корпуса

5.2 Анализ частотностей (MFW)

Рисунок 4

Раскрыть блок с кодом
# Построение визуализации
corpus_tf |> 
  count(author) |> 
  ggplot(aes(reorder(author, n), n, fill = author)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = n),
            hjust = -0.2, 
            color = "#322A09", 
            size = 2.8, 
            family = "inter") +
  coord_flip() +
  labs(x = "Автор", y = "Количество наблюдений") + 
  scale_fill_manual(values = my_colors) +
  theme_minimal(base_family = "inter") +
  theme(
    plot.background = element_rect(fill = "#E4DFD3", color = NA),
    panel.background = element_rect(fill = "#E4DFD3", color = NA),
    panel.grid = element_blank(),
    panel.border = element_blank(),
    plot.border = element_blank(),
    axis.line = element_blank(),
    axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    axis.text.y = element_text(color = "#322A09", size = 8),
    axis.title = element_text(color = "#322A09", size = 8),
    legend.position = "none"
  )
Рисунок 4: Распределение наблюдений по авторам (MFW)
Раскрыть блок с кодом
top_words <- corpus_tf |>
  pivot_longer(
    cols = -c(author, title),
    names_to = "word",
    values_to = "freq"
  ) |>
  group_by(word) |>
  summarise(freq = sum(freq)) |>
  arrange(desc(freq)) |>
  slice_head(n = 20)
Раскрыть блок с кодом
ggplot(top_words, aes(reorder(word, freq), freq)) +
  geom_col(fill = "#CFB07A") +            
  geom_text(aes(label = sprintf("%.2f", freq)),         
            hjust = -0.2, 
            color = "#322A09", 
            size = 2.8, 
            family = "inter") +
  coord_flip(ylim = c(0, 14500)) +
  labs(x = "Слово", 
       y = "Частота") +
  theme_minimal(base_family = "inter") +
  theme(
    plot.background = element_rect(fill = "#E4DFD3", 
                                   color = NA),
    panel.background = element_rect(fill = "#E4DFD3",
                                    color = NA),
    panel.grid = element_blank(),
    panel.border = element_blank(),
    plot.border = element_blank(),
    axis.line = element_blank(),
    axis.text.x = element_blank(),
    axis.ticks.x = element_blank(),
    axis.text.y = element_text(color = "#322A09", 
                               size = 8),
    axis.title = element_text(color = "#322A09", 
                              size = 8),
    legend.position = "none"
  )
Рисунок 5: Распределение по частотности слов

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

6 Моделирование

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

Раскрыть блок с кодом
# Шаг 1. Удаляются названия произведений, чтобы избежать утечку данных
corpus_tf_model <- corpus_tf |>
  select(-title)


# Шаг 2. 
corpus_tf_model <- corpus_tf_model |>
  mutate(author = as.factor(author))

Чтобы избежать переобучения модели, доступные наблюдения делятся на две группы: обучающую (data_train) и тестовую (data_test).

При перекрестной проверке обучающие данные дополнительно разбиваются на фолды (например, с помощью функции vfold_cv()). Это позволяет получить более надежную оценк производительности, поскольку каждое наблюдение по очереди участвует и в обучении, и в проверке — то есть для оценки используются все данные, но без утечки информации.

Раскрыть блок с кодом
set.seed(20012003)
data_split <- initial_split(corpus_tf_model, 
                            strata = author)


data_train <- training(data_split) 
data_test <- testing(data_split)

# Шаг 2. Разбиение на фолды 
set.seed(20012003)
folds <- vfold_cv(data_train, strata = author, v = 10)

6.1 Рецепты

Создается базовый рецепт для обработки признаков, который обучается на тренировочных данных с помощью функции prep().

Раскрыть блок с кодом
# Шаг 1. Рецепт №1: базовый набор признаков
base_rec <- recipe(author ~ ., data = data_train) |>
  step_zv(all_predictors()) |>
  step_normalize(all_numeric_predictors())


# Шаг 2. Обучение на тренировочных данных
base_trained <- prep(base_rec)


# Шаг 3. Применение преобразований
base_features <- bake(
  base_trained,
  new_data = NULL
)
Раскрыть блок с кодом
# Шаг 1. Рецепт №2: метод главных компонент (PCA)
pca_rec <- recipe(author ~ ., data = data_train) |>
  step_zv(all_predictors()) |>
  step_normalize(all_numeric_predictors()) |>
  step_pca(all_numeric_predictors(),
           num_comp = 7
  )


# Шаг 2. Обучение на тренировочных данных
pca_trained <- prep(pca_rec)


# Шаг 3. Применение преобразований
pca_features <- bake(
  pca_trained,
  new_data = NULL
)

6.2 PLS (Partial Least Squares)

Метод Partial Least Squares (PLS) формирует латентные компоненты, максимизирующие ковариацию между предикторами и целевой переменной. В отличие от обычного PCA, который ищет компоненты, максимизирующие дисперсию самих предикторов, PLS строит компоненты, максимизирующие ковариацию между предикторами и целевой переменной.

Раскрыть блок с кодом
pls_rec <- base_rec |>
  step_pls(
    all_numeric_predictors(),
    outcome = "author",
    num_comp = tune()
  )

6.3 Регуляризация: Lasso и Ridge

При анализе текстов данные преобразуются в числовой вид, из-за чего возникает проблема высокой размерности: десятки тысяч признаков при малом числе документов, большинство признаков — нулевые. Это приводит к «проклятию размерности», из-за чего многие алгоритмы (например, k-NN) работают плохо.

Эффективным решением становятся линейные модели с регуляризацией. Регуляризация «штрафует» большие коэффициенты, снижая переобучение. Основные типы:

  • L2 (Ridge) — штраф за сумму квадратов весов (уменьшает веса, но не обнуляет их);
  • L1 (Lasso) — штраф за сумму моделей весов (обнуляет неважные признаки, выполняя отбор);
  • Elastic Net — комбинация L1 и L2.
Раскрыть блок с кодом
# Шаг 1. Регуляризация: Lasso 
lasso_spec <- multinom_reg(
  penalty = tune(),
  mixture = 1
) |> 
  set_mode("classification") |> 
  set_engine("glmnet")


# Шаг 2. Регуляризация: Ridge
ridge_spec <- multinom_reg(
  penalty = tune(),
  mixture = 0
) |> 
  set_mode("classification") |> 
  set_engine("glmnet")

6.4 Опорные векторы (SVM)

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

Точки, которые определяют положение границы, называются опорными векторами. Внутренние точки на положение границы не влияют. Чем шире маржа, тем уверенее разделение классов.

Раскрыть блок с кодом
svm_spec <- svm_linear(
  cost = tune()
) |> 
  set_mode("classification") |> 
  set_engine("LiblineaR")

6.5 Дополнительные модели

Метод ближайших соседей (k-NN) — непараметрический алгоритм классификации и регрессии. Классификация объекта выполняется на основе «голосования»: объект относится к тому классу, который наиболее часто встречается среди k ближайших к нему объектов из обучающей выборки. Алгоритм не требует обучения в классическом смысле, однако его производительность сильно зависит от выбора k, метрики расстояния и масштаба признаков.

Раскрыть блок с кодом
knn_spec <- nearest_neighbor(
  neighbors = tune()
) |>
  set_engine("kknn") |>
  set_mode("classification")

Regularized Discriminant Analysis (RDA) занимает промежуточное положение между линейным и квадратичным дискриминантным анализом, дополненное регуляризацией. Устойчива при малом объеме выборки или большом количестве признаке, что снижает риск переобучения.

Раскрыть блок с кодом
rda_spec <- discrim_regularized(
  frac_common_cov = tune(),
  frac_identity = tune()
) |>
  set_engine("klaR")

6.6 Workflow_set

С помощью функции workflow_set() тестируются различные комбинации моделей и рецептов на одном наборе данных.

Раскрыть блок с кодом
wflow_set <- workflow_set(  
  preproc = list(base = base_rec,
                 pca = pca_rec,
                 pls = pls_rec),  
  models = list(svm = svm_spec,
                lasso = lasso_spec,
                ridge = ridge_spec,
                knn = knn_spec),  
  cross = TRUE
)
Раскрыть блок с кодом
plan(multisession, workers = 6) # Так как 8 процессоров

train_res <- wflow_set |> 
  workflow_map(
    seed = 20012003,
    resamples = folds,
    grid = 5,
    metrics = metric_set(f_meas, 
                         kap, 
                         accuracy),
    control = control_resamples(save_pred = TRUE)
  )

7 Оценка и визуализация результатов

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

Качество оценивалось комплексно по трем метрикам: F-мера (F-measure), коэффициент Каппа Коэна (Kappa) и общая точность (Accuracy).

Раскрыть блок с кодом
rank_results(train_res, select_best = TRUE) |> 
  datatable(options = list(pageLength = 5)) 
Таблица 6: Рейтинг лучших конфигураций моделей

Рисунок 6 дополнительно визуализирует сравнение точности классификации для различных комбинаций методов. Каждая точка соответствует среднему значению метрики accuracy, полученному в результате кросс-валидации, а вертикальные отрезки отображают стандартную ошибку оценки.

Наиболее высокие значения точности продемонстрировали базовые модели без дополнительного преобразования признаков, в частности Ridge, SVM и Lasso, для которых accuracy близка к 1.0. Это свидетельствует о высокой способности данных моделей корректно классифицировать наблюдения на исходном наборе признаков.

Раскрыть блок с кодом
autoplot(train_res, metric = "accuracy") +
  scale_color_manual(values = my_colors) +
  theme_minimal(base_family = "inter") +
  theme(
    plot.background = element_rect(fill = "#E4DFD3", color = NA),
    panel.background = element_rect(fill = "#E4DFD3", color = NA),
    panel.grid = element_blank(),
    panel.border = element_blank(),   
    axis.line = element_blank(),
    axis.text = element_text(color = "#322A09", size = 8),
    axis.title = element_text(color = "#322A09", size = 8),
    legend.position = "none"
  ) +
  geom_text(aes(y = (mean - 2 * std_err), label = wflow_id),
            angle = 90, hjust = 1.5,
            color = "#322A09", family = "inter", size = 2.8) +
  coord_cartesian(xlim = c(NA, 60), ylim = c(0, 1)) +
  labs(x = "Конфигурация модели", y = "Accuracy")
Рисунок 6: Сравнение точности классификации различных моделей по результатам кросс-валидации

Далее финализируется воркфлоу.

Раскрыть блок с кодом
best_results <- train_res |> 
  extract_workflow_set_result("base_ridge") |> 
  select_best(metric = "accuracy")

datatable(best_results)
Таблица 7: Финальный воркфлоу
Раскрыть блок с кодом
# Финальная оценка модели на отложенной test-выборке
ridge_res <- train_res |> 
  extract_workflow("base_ridge") |> 
  finalize_workflow(best_results) |> 
  last_fit(
    split = data_split,
    metrics = metric_set(f_meas, kap, accuracy)
  )

В Таблица 8 приведены значения метрик качества, полученные в ходе имитации реального цикла разработки модели. Все метрики близки к единице, что свидетельствует о высокой предсказательной способности модели.

Раскрыть блок с кодом
collect_metrics(ridge_res) |> 
  datatable()
Таблица 8: Метрики качества модели на тестовой выборке

7.1 Confusion Matrix

Рисунок 7 представляет матрицу ошибок (confusion matrix) для модели Ridge-классификации. По горизонтальной оси отложены истинные классы (авторы произведений), по вертикальной — классы, предсказанные моделью. Числа в ячейках отражают количество наблюдений, отнесённых к соответствующему классу, а интенсивность окраски показывает частоту таких случаев.

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

Единственная ошибка классификации наблюдается для Чарльза Диккенса: из 103 текстов данного автора 102 было классифицировано верно, тогда как один текст был ошибочно отнесён к классу Эмили Бронте. Таким образом, модель продемонстрировала практически безошибочную работу, допустив лишь одну ошибку на всём тестируемом наборе данных.

Раскрыть блок с кодом
collect_predictions(ridge_res) |> 
  conf_mat(truth = author, estimate = .pred_class) |> 
  autoplot(type = "heatmap", 
           text_color = "#322A09",    
           show_legend = FALSE,
           size = 2.8) +
  scale_fill_gradient(low = "white",
                      high = "#9E851B",
                      guide = "none") +
  labs(
    x = "Истинный автор",         
    y = "Предсказанный автор"
  ) +
  theme_minimal(base_family = "inter") +
  theme(
    plot.background = element_rect(fill = "#E4DFD3", 
                                   color = NA),
    panel.background = element_rect(fill = "#E4DFD3", 
                                    color = NA),
    panel.border = element_blank(),
    plot.border = element_blank(),
    axis.line = element_blank(),
    panel.grid = element_blank(),    
    axis.text.x = element_text(color = "#322A09", 
                               size = 8, 
                               angle = 90, 
                               vjust = 0.5, 
                               hjust = 1),
    axis.text.y = element_text(color = "#322A09", 
                               size = 8),
    axis.title = element_text(color = "#322A09", 
                              size = 8),
    legend.position = "none"
  )
Рисунок 7: Матрица ошибок модели

7.2 Интерпретация модели

Для интерпретации модели извлекаются коэффициенты признаков и отбирается топ-10 наиболее значимых слов для каждого автора. Результаты отображены в Таблица 9.

Раскрыть блок с кодом
# Шаг 1. Финальная модель
final_model <- extract_fit_parsnip(ridge_res)


# Шаг 2. Получение топа-10 слов авторов
top_terms <- tidy(final_model) |>
  filter(term != "(Intercept)") |>
  group_by(class) |>                           
  slice_max(abs(estimate), n = 10)  |>             
  ungroup()  |> 
  mutate(term = fct_reorder(term, abs(estimate)))
Раскрыть блок с кодом
# Шаг 3. Таблица
top_terms |> 
  datatable(options = list(pageLength = 5))
Таблица 9: Наиболее информативные признаки классификации

Дополнительно приводится визуализация на Рисунок 8, на котором представлены наиболее значимые признаки (термины) для каждого класса авторов, выявленные моделью. Для каждого автора показаны слова с наибольшими по модулю коэффициентами модели.

Результаты показывают, что модель выделяет для каждого автора собственный набор характерных лексических маркеров. Например, для произведений Джейн Остен наиболее информативными оказались слова «every», «said», «could» и «herself», тогда как для текстов Чарльза Диккенса значимый вклад в классификацию вносят слова «returned», «continued» и «manner». Для автора Джорджа Элиота среди наиболее важных признаков выделяются слова «maggie», «towards», «everything» и «sat», а для Лоренса Стерна — «upon», «church», «world» и «into».

Раскрыть блок с кодом
top_terms |> 
  ggplot(aes(x = estimate, y = term, fill = class)) +
  geom_col(show.legend = FALSE, alpha = 0.85) +
  facet_wrap(~ class, scales = "free_y", nrow = 4) +
  scale_fill_manual(values = my_colors) +
  labs(
    x = "Коэффициент",
    y = "Признак"
  ) +
  theme_minimal(base_family = "inter") +
  theme(
    plot.background = element_rect(fill = "#E4DFD3", color = NA),
    panel.background = element_rect(fill = "#E4DFD3", color = NA),
    panel.grid = element_blank(),
    panel.border = element_blank(),   
    axis.line = element_blank(),
    axis.text = element_text(color = "#322A09", size = 8),
    axis.title = element_text(color = "#322A09", size = 8),
    strip.background = element_blank(),
    strip.text = element_text(color = "#322A09", size = 9, family = "inter", face = "bold"),
    plot.title = element_text(color = "#322A09", size = 10, family = "inter", hjust = 0.5),
    legend.position = "none"
  )
Рисунок 8: Наиболее важные признаки для каждого автора

8 Выводы

В ходе данной работы были получены следующие результаты:

  1. Перед исследованием был подготовлен корпус британской художественной прозы XVIII-XIX веков: выполнена очистка текстов, создана единая структура данных.
  2. Для характеристики авторского стиля были извлечены количественные признаки различного типа: средняя длина слова, средняя длина предложения, показатель лексического разнообразия (TTR), а также частоты наиболее употребительных слов (Most Frequent Words);
  3. Разведывательный анализ показал, что отдельные стилометрические характеристики действительно различаются между авторами. Наиболее заметные различия наблюдаются в средней длине предложений и показателях лексического разнообразия. Средняя длина слова оказалась сравнительно стабильной и менее информативной.
  4. Анализ частотных характеристик подтвердил классическое положение стилометрии о высокой информативности служебных слов. Среди наиболее частотных единиц корпуса преобладают артикли, предлоги, союзы и местоимения, использование которых в меньшей степени зависит от тематики произведения и отражает индивидуальные особенности авторского письма.
  5. Для решения задачи атрибуции авторства были протестированы различные варианты комбинации методов предобработки и алгоритмов машинного обучения в рамках фреймворка {tidymodels}. Сравнение моделей посредством кросс-валидации показало, что наилучшие результаты продемонстрировали линейные методы с регуляризацией, прежде всего Ridge-классификация.
  6. Финальная модель достигла практически идеальных значений (приближенность к единице) Accuracy, F-меры и коэффициента Каппа на тестовой выборке. Матрица ошибок показала лишь единичный случай неправильной классификации в рамках используемой схемы валидации, что свидетельствует о высокой разделимости авторских стилей в рассматриваемом корпусе.
  7. Интерпретация коэффициентов модели позволила выделить характерные слова-маркеры для стиля каждого автора. Полученные результаты подтверждают, что даже наиболее частотные слова могут служить надежными индикаторами авторского стиля и успешно использоваться для автоматической атрибуции текстов.

Таким образом, поставленная цель работы была достигнута: методы стилометрии и машинного обучения продемонстрировали высокую эффективность при классификации произведений по авторам на материале корпуса «A Small Collection of British Fiction».

В будущем можно было бы расширить исследование, дополнив корпус текстов и добавив операции обработки текстов, однако уже на данном этапе были получены значимые результаты.

Сноски

  1. Здесь была отмечена неточность: в описании корпуса указано, что произведений в нем 28, однако на самом деле их 27.↩︎

  2. Отношение числа уникальных слов к общему количеству слов.↩︎