Многоклассовая классификация произведений британских авторов

Автор

Линев Никита

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

30 мая 2026 г.

Сегодня наша задача – на основе корпуса A Small Collection of British Fiction (28 произведений британской прозы конца XVIII — XIX веков) проанализировать стилистические особенности авторов и классифицировать тексты по авторам.


Сначала подключим необходимые библиотеки и загрузим непосредственно корпус текстов.

library(tidyverse)
library(textrecipes)
library(tidymodels)
library(tidytext)
library(glmnet)
library(stylo)
corpus <- load.corpus.and.parse(
  corpus.dir = "A_Small_Collection_of_British_Fiction-master/corpus"
)
slicing input text into tokens...

turning words into features, e.g. char n-grams (if applicable)...

Далее разделяем тексты на фрагменты по 2000 слов и извлекаем необходимые метаданные.

samples <- make.samples(
  corpus,
  sample.size = 2000,
  sampling = "normal.sampling",
  sample.overlap = 0,
  sampling.with.replacement = FALSE
)

samples_tbl <- tibble(
  id = names(samples),
  text = purrr::map_chr(samples, paste, collapse = " ")
)
samples_tbl <- samples_tbl |>
  separate(id,
           into = c("author", "title", "sample"),
           sep = "_",
           extra = "merge") |>
  filter(sample != "1") |>
  mutate(
    author = as.factor(author),
    work = paste(author, title, sep = "_")
  )

samples_tbl <- samples_tbl |>
  filter(author != "EBronte")

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


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

works_tbl <- samples_tbl |>
  distinct(author, title, work)

set.seed(20260530)

test_works <- works_tbl |>
  group_by(author) |>
  slice_sample(n = 1) |>
  ungroup()

train_works <- works_tbl |>
  anti_join(test_works, by = "work")

data_train <- samples_tbl |>
  semi_join(train_works, by = "work")

data_test <- samples_tbl |>
  semi_join(test_works, by = "work")
data_train <- data_train |> mutate(author = factor(author))
data_test  <- data_test  |> mutate(author = factor(author))

data_split <- initial_split(
  bind_rows(data_train, data_test),
  strata = author
)

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

В непосредственно рецепт мы отправим токенизацию, удаление стоп-слов, отбор наиболее информативных токенов в размере 1000 единиц, преобразование текстов в матрицу признаков с помощью TF-IDF и стандартизацию признаков.

folds <- vfold_cv(
  data_train,
  v = 5,
  strata = author
)

base_rec <- recipe(author ~ text, data = data_train) |>
  step_tokenize(text) |>
  step_stopwords(text) |>
  step_tokenfilter(text, max_tokens = 1000) |>
  step_tfidf(text) |>
  step_zv(all_predictors()) |>
  step_normalize(all_predictors())

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

ridge_spec <- multinom_reg(
  penalty = tune(),
  mixture = 0
) |>
  set_engine("glmnet") |>
  set_mode("classification")

ridge_wflow <- workflow() |>
  add_recipe(base_rec) |>
  add_model(ridge_spec)
ridge_res <- ridge_wflow |>
  tune_grid(
    resamples = folds,
    grid = 4,
    metrics = metric_set(accuracy, f_meas)
  )

Затем переобучим модель с учетом выбранных ЛУЧШИХ параметрах и протестируем ее на отложенных данных.

collect_metrics(ridge_res)
# A tibble: 8 × 7
   penalty .metric  .estimator  mean     n    std_err .config        
     <dbl> <chr>    <chr>      <dbl> <int>      <dbl> <chr>          
1 1.29e-10 accuracy multiclass 0.998     5 0.00000421 pre0_mod1_post0
2 1.29e-10 f_meas   macro      0.996     5 0.000532   pre0_mod1_post0
3 2.33e- 7 accuracy multiclass 0.998     5 0.00000421 pre0_mod2_post0
4 2.33e- 7 f_meas   macro      0.996     5 0.000532   pre0_mod2_post0
5 4.70e- 4 accuracy multiclass 0.998     5 0.00000421 pre0_mod3_post0
6 4.70e- 4 f_meas   macro      0.996     5 0.000532   pre0_mod3_post0
7 9.11e- 1 accuracy multiclass 0.984     5 0.00204    pre0_mod4_post0
8 9.11e- 1 f_meas   macro      0.972     5 0.00254    pre0_mod4_post0
autoplot(ridge_res) + theme_light()

best_params <- select_best(ridge_res, metric = "accuracy")

final_ridge <- finalize_workflow(
  ridge_wflow,
  best_params
)

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

final_fit <- final_ridge |>
  last_fit(split = data_split)

collect_metrics(final_fit)
# A tibble: 3 × 4
  .metric     .estimator .estimate .config        
  <chr>       <chr>          <dbl> <chr>          
1 accuracy    multiclass   0.999   pre0_mod0_post0
2 roc_auc     hand_till    1       pre0_mod0_post0
3 brier_class multiclass   0.00562 pre0_mod0_post0
collect_predictions(final_fit) |>
  conf_mat(truth = author, estimate = .pred_class) |>
  autoplot(type = "heatmap")

final_model <- extract_fit_parsnip(final_fit)

top_terms <- tidy(final_model) |>
  filter(term != "(Intercept)") |>
  group_by(class) |>
  slice_max(abs(estimate), n = 10) |>
  ungroup()

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

top_terms |>
  ggplot(aes(
    x = estimate,
    y = reorder(term, estimate),
    fill = class
  )) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~class, scales = "free") +
  theme_light()

Что мы можем сказать о полученных результатах?

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

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