Раскрыть блок с кодом
library(tidyverse)
library(textrecipes)
library(tidymodels)
library(tidytext)
library(stylo)
library(DT)
library(baguette)
library(discrim)
library(future)На протяжении последних десятилетий цифровые методы существенным образом трансформировали подходы к исследованию гуманитарных дисциплин. Несмотря на широкое распространение цифровых корпусов, задача автоматической атрибуции художественных текстов остается предметом активных исследований.
Использование методов стилометрии позволяет количественно описывать особенности авторского письма на основе формальных характеристик текста и применять методы машинного обучения для решения задач атрибуции авторства. Подобный подход дополняет традиционные литературоведческие методы анализа и позволяет исследовать большие текстовые коллекции в автоматизированном режиме.
В данной работе предлагается подход к атрибуции художественных текстов на материале корпуса «A Small Collection of British Fiction» (27 произведений британской прозы конца XVIII — XIX вв. 1) с применением методов машинного обучения и моделирования данных.
Цель работы — на основе указанного корпуса провести стилистический анализ авторов и построить модель классификации текстов по авторам с использованием пакета {tidymodels}.
В ходе работы необходимо выполнить следующие задачи:
british_fiction, метаданные из файла overview);{tidymodels};Для выполнения анализа подключаются пакеты, используемые для обработки текстов, построения моделей машинного обучения и визуализации результатов.
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} — пакет, включающий в себя инструменты для организации параллельных вычислений и ускорения ресурсоемких операций.Для исследования предоставляется корпус «А Small Collection of British Fiction», включающий 27 произведений британской художественной прозы XVIII-XIX веков на английском языке.
Для удобства восприятия и предварительного анализа дополнительно формируется обзорная Таблица 1 с полными именами авторов и названиями произведений, входящих в состав корпуса. Данная таблица используется исключительно для визуализации состава корпуса и не участвует в последующей обработке данных.
datatable(
corpus_list,
colnames = c("Автор произведения", "Произведение"),
options = list(pageLength = 5)
)Названия файлов корпуса организованы по следующему принципу:
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), содержащий полные тексты произведений, не выводится в таблицу ввиду большого объема данных. Тем не менее, именно он будет использоваться на последующих этапах анализа.
file_path — путь к файлу, содержащий название корпуса, а также название файла (шаблон: `название_корпуса/название_файла.txt`;filename — название файла с текстом, содержащий в себе краткую информацию об авторе и тексте;author_short — короткая информация об авторе;short_title — короткое название произведения.datatable(texts_corpus_md,
options = list(pageLength = 5))Наибольшее значение для дальнейшего анализа имеют столбцы author_short и short_title, используемые в качестве ключей для объединения данных. Кроме того, столбец text содержит полный текст произведения и служит источником признаков для последующего стилометрического анализа.
overview)Метаданные, представленные в Таблица 3, требуют дополнительной обработки перед объединением с текстовым корпусом.
# Шаг 1. Загрузка файла .tsv с метаданными произведений
raw_metadata <- read_tsv('overview.tsv')# Шаг 2. Создание таблицы с метаданными до предобработки
datatable(
raw_metadata,
options = list(pageLength = 5)) Исправленная Таблица 4 содержит в себе следующие изменения:
# Шаг 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))Данные исправления необходимы для успешного объединения таблицы метаданных с информацией, извлеченной из имен текстовых файлов.
Теперь необходимо объединить таблицы по ключам (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 наблюдений, что соответствует количеству произведений в исходном корпусе. Это свидетельствует о том, что ключи сформированы корректно и при объединении отсутствуют потери данных.
Для дальнейшего анализа формируются две версии текста. Исходная версия (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 = "_"
)Для увеличения количества наблюдений и последующего стилометрического анализа каждый текст разбивается на отдельные фрагменты фиксированного размера. В данной работе используются непересекающиеся выборки по 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)Поскольку авторы различаются не только тематикой произведений, но и устойчивыми особенностями авторского стиля, для каждого текста были извлечены количественные лингвистические признаки.
В настоящей работе получены следующие признаки:
word_count);sentence_count);mean_word_length);mean_sentence_length);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)
)
)Для анализа не только отдельных слов, но и характерных словосочетаний дополнительно были извлечены биграммы. Такие признаки используются преимущественно для разведывательного анализа текстов и выявления характерных словосочетаний отдельных авторов.
Распределение биграмм и наиболее характерные примеры будут рассмотрены в следующем разделе (Глава 5).
# Извлечение биграмм
bigrams <- victorian_corpus_combined |>
select(author_short, short_title, text) |>
unnest_tokens(bigram,
text,
token = "ngrams",
n = 2
)Помимо общих лингвистических характеристик, в стилометрии используются частоты наиболее употребительных слов (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 представляет полную информацию по извлеченным из произведений лингвистическим признакам.
ling_features |>
select(
author_short,
short_title,
mean_word_length,
mean_sentence_length,
ttr
) |>
datatable(options = list(pageLength = 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'
)На Рисунок 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"
)Рисунок 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"
)# Построение визуализации
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"
)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"
)Итак, среди наиболее частотных слов преобладают служебные слова (the, and, to, of, a и другие), что соответствует классическим представлениям стилометрии. Подобный результат возник, так как при исследовании намеренно избегалось удаление стоп-слов, так как они могут играть важную роль в стилометрии.
После извлечения количественных лингвистических признаков и первичного разведочного анализа можно переходить непосредственно к построению модели.
# Шаг 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)Создается базовый рецепт для обработки признаков, который обучается на тренировочных данных с помощью функции 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
)Метод Partial Least Squares (PLS) формирует латентные компоненты, максимизирующие ковариацию между предикторами и целевой переменной. В отличие от обычного PCA, который ищет компоненты, максимизирующие дисперсию самих предикторов, PLS строит компоненты, максимизирующие ковариацию между предикторами и целевой переменной.
pls_rec <- base_rec |>
step_pls(
all_numeric_predictors(),
outcome = "author",
num_comp = tune()
)При анализе текстов данные преобразуются в числовой вид, из-за чего возникает проблема высокой размерности: десятки тысяч признаков при малом числе документов, большинство признаков — нулевые. Это приводит к «проклятию размерности», из-за чего многие алгоритмы (например, k-NN) работают плохо.
Эффективным решением становятся линейные модели с регуляризацией. Регуляризация «штрафует» большие коэффициенты, снижая переобучение. Основные типы:
# Шаг 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")Метод опорных векторов (SVM) строит разделяющую гиперплоскость между двумя классами так, чтобы расстояние от нее до ближайших точек каждого класса (маржа) было максимальным. Это повышает устойчивость модели к ошибкам на новых данных.
Точки, которые определяют положение границы, называются опорными векторами. Внутренние точки на положение границы не влияют. Чем шире маржа, тем уверенее разделение классов.
svm_spec <- svm_linear(
cost = tune()
) |>
set_mode("classification") |>
set_engine("LiblineaR")Метод ближайших соседей (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")С помощью функции 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)
)В Таблица 6 приведены результаты сравнения и ранжирования прогнозных моделей, которые были получены на основе различных комбинаций методов предобработки данных и алгоритмов машинного обучения.
Качество оценивалось комплексно по трем метрикам: F-мера (F-measure), коэффициент Каппа Коэна (Kappa) и общая точность (Accuracy).
rank_results(train_res, select_best = TRUE) |>
datatable(options = list(pageLength = 5)) Рисунок 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")Далее финализируется воркфлоу.
best_results <- train_res |>
extract_workflow_set_result("base_ridge") |>
select_best(metric = "accuracy")
datatable(best_results)# Финальная оценка модели на отложенной 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()Рисунок 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"
)Для интерпретации модели извлекаются коэффициенты признаков и отбирается топ-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))Дополнительно приводится визуализация на Рисунок 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"
)В ходе данной работы были получены следующие результаты:
{tidymodels}. Сравнение моделей посредством кросс-валидации показало, что наилучшие результаты продемонстрировали линейные методы с регуляризацией, прежде всего Ridge-классификация.Таким образом, поставленная цель работы была достигнута: методы стилометрии и машинного обучения продемонстрировали высокую эффективность при классификации произведений по авторам на материале корпуса «A Small Collection of British Fiction».
В будущем можно было бы расширить исследование, дополнив корпус текстов и добавив операции обработки текстов, однако уже на данном этапе были получены значимые результаты.