# Загружаем данные
<- load.corpus.and.parse(corpus.dir = "../Documents/corpus")
corpus
# Разделим тексты на отрывки длиной 2000 слов
<- make.samples(corpus,
corpus_samples sample.size = 2000,
sampling = "normal.sampling",
sample.overlap = 0,
sampling.with.replacement = FALSE)
<- tibble(
samples_df sample_id = names(corpus_samples),
text = sapply(corpus_samples, function(x) paste(x, collapse = " "))
)<- samples_df |>
samples_df separate(sample_id, into = c("author", "title", NA), sep = "_")
Британская проза конца XVIII-XIX вв. Определение авторства.
Введение
Для определения авторов по текстам произведений будем использовать многоклассовую классификацию. Для начала немного о данных. Перед нами 11 авторов (Jane Austen, Anne Bronte, Charlotte Bronte, Emile Bronte, Charles Dickens, George Eliot, Henry Fielding,Samuel Richardson, Laurence Sterne, Antony Trollope, William Thackeray) и 27 произведений британской прозы конца XVIII-XIX вв.
Цель - обучение модели многоклассовой классификации, чтобы определить автора.
Задачи: - Провести разведывательный анализ. - Подготовить данные. Разделить на более мелкие части. Лемматизировать. - Отсечь имена собственные из лемматизированных отрывков. Первая идея - извлечь служебные слова, так как они употребляются авторами неосознанно и могут отражать стиль. - Посчитать частотные слова. Выделить 500 самых частотных. - Разделить данные на тренировочную и тестовую выборки. - Снизить размерность данных. - Подготовить рецепты для обучения. - Подготовить модели и настроить параметры. - Сравнить модели по производительности и выбрать наиболее точную для определения автора.
Разведывательный анализ. Часть 1.
Чтобы понимать, что представляют из себя наши данные и выбрать методы дальнейшей их обработки, проведем небольшой разведывательный анализ. Для начала посмотрим, как распределяются, сделанные отрывки по авторам.
# A tibble: 11 × 2
author n
<chr> <int>
1 EBronte 59
2 Sterne 115
3 ABronte 118
4 Austen 201
5 CBronte 236
6 Fielding 244
7 Trollope 377
8 Eliot 379
9 Thackeray 401
10 Dickens 414
11 Richardson 709
Как мы можем увидеть. У E. Bronte совсем немного отрывков - 59. Это может вызывать сложности при обучении и тестировании, так как очень мало данных. Модель, скорее всего, не сможет корректно распознавать этого автора. Чего не скажешь о Richardson. Однако, принято решение не исключать из дальнейшей работы авторов, имеющих мало отрывков. Данные распределены неравномерно. У одних авторов меньше произведений или их объем невелик, у других наоборот.
Подготовка признаков
Для того, чтобы выделить и подготовить признаки, которые могла бы понять модель, проведем лемматизацию отрывков, уберем имена собственные (поскольку могут попасться имена героев и географических названий и др.), так как это может повлиять на определение уникальности стиля авторов. Посчитаем частоты слов и выберем 500 самых часто встречающихся для каждого автора.
# 2. Загружаем модель UDPipe для английского языка
#ud_model <- udpipe_download_model(language = "english-ewt")
#ud_model <- udpipe_load_model(ud_model$file_model)
# 3. Аннотируем тексты с помощью UDPipe
#annotated <- udpipe_annotate(ud_model, x = samples_df$text, doc_id = samples_df$author)
#annotated <- as_tibble(annotated)
load("~/annotated.Rdata")
annotated
# A tibble: 6,520,015 × 14
doc_id paragraph_id sentence_id sentence token_id token lemma upos xpos
<chr> <int> <int> <chr> <chr> <chr> <chr> <chr> <chr>
1 ABronte_A… 1 1 agnes g… 1 agnes agne ADV RB
2 ABronte_A… 1 1 agnes g… 2 grey grey ADJ JJ
3 ABronte_A… 1 1 agnes g… 3 chap… chap… NOUN NN
4 ABronte_A… 1 1 agnes g… 4 i I PRON PRP
5 ABronte_A… 1 1 agnes g… 5 the the DET DT
6 ABronte_A… 1 1 agnes g… 6 pars… pars… NOUN NN
7 ABronte_A… 1 1 agnes g… 7 all all DET DT
8 ABronte_A… 1 1 agnes g… 8 true true ADJ JJ
9 ABronte_A… 1 1 agnes g… 9 hist… hist… NOUN NNS
10 ABronte_A… 1 1 agnes g… 10 cont… cont… VERB VBP
# ℹ 6,520,005 more rows
# ℹ 5 more variables: feats <chr>, head_token_id <chr>, dep_rel <chr>,
# deps <chr>, misc <chr>
# Уберем имена собственные из лемматизированных отрывков.
<- annotated[annotated$upos != "PROPN", ]
filtered_df # Выберем doc_id и лемму.
<- filtered_df |>
filtered_df select(doc_id, lemma)
Так как процесс лемматизации очень долгий (длился 4 часа), размеченный датасет можно найти здесь: лемматизация в формате Rdata.
Посчитаем частоты.
# Группируем леммы по авторам и подсчитываем их частоту
<- filtered_df %>%
frequency_df group_by(doc_id, lemma) %>%
summarise(freq = n(), .groups = 'drop')
# Отбираем 500 самых частотных лемм
<- frequency_df %>%
top_lemmas arrange(desc(freq)) %>%
distinct(lemma) %>%
slice_head(n = 500) %>%
pull(lemma)
# Создаем матрицу частот
<- frequency_df %>%
frequency_matrix filter(lemma %in% top_lemmas) %>%
pivot_wider(names_from = lemma, values_from = freq, values_fill = list(freq = 0))
# Теперь frequency_matrix содержит матрицу частот
# Преобразуем в tibble для удобства
<- as_tibble(frequency_matrix)
frequency_matrix <- frequency_matrix |>
frequency_matrix separate(doc_id, into = c("author", "title", NA), sep = "_")
<- frequency_matrix |>
frequency_matrix select(-title)
frequency_matrix
# A tibble: 4,343 × 500
author `'s` I a agne all and any as at aunt be
<chr> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int>
1 ABronte 13 22 31 1 15 71 2 10 10 1 56
2 ABronte 6 50 36 0 8 52 3 10 7 0 51
3 ABronte 8 57 26 0 5 50 4 11 10 0 41
4 ABronte 2 71 21 0 15 60 4 19 9 1 59
5 ABronte 4 61 31 1 9 68 3 18 10 0 55
6 ABronte 1 56 65 0 9 69 4 15 8 0 32
7 ABronte 5 42 28 0 7 73 2 18 9 0 63
8 ABronte 5 45 31 0 8 56 5 20 13 0 77
9 ABronte 2 48 27 0 9 59 3 15 15 0 71
10 ABronte 3 76 42 0 5 61 1 8 5 0 71
# ℹ 4,333 more rows
# ℹ 488 more variables: but <int>, by <int>, chapter <int>, child <int>,
# come <int>, could <int>, crack <int>, day <int>, dear <int>, do <int>,
# family <int>, father <int>, `for` <int>, friend <int>, from <int>,
# gentleman <int>, get <int>, girl <int>, give <int>, go <int>, good <int>,
# hand <int>, have <int>, he <int>, hear <int>, herself <int>, himself <int>,
# house <int>, how <int>, idea <int>, `if` <int>, `in` <int>, into <int>, …
Данные прошли предобработку и готовы к обучению.
Выборки
Разделим наш подготовленный корпус на обучающую и тестовую выборки.
# 5-fold cross-validation using stratification
# A tibble: 5 × 2
splits id
<list> <chr>
1 <split [2603/652]> Fold1
2 <split [2603/652]> Fold2
3 <split [2603/652]> Fold3
4 <split [2605/650]> Fold4
5 <split [2606/649]> Fold5
Подготовка рецептов. Снижение размерности. Разведывательный анализ. Часть 2.
# Подготовим первый рецепт
<- recipe(author ~ ., data = data_train) |>
base_rec step_zv(all_predictors()) |>
step_normalize(all_numeric_predictors())
base_rec# Обучим рецепт на основе обучающей выборки
<- base_rec |>
base_trained prep(data_train)
base_trained
# Применим рецепт к обучающим данным
|>
base_trained bake(new_data = NULL)
# A tibble: 3,255 × 498
`'s` I a agne all and any as at aunt
<dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 -0.969 1.02 0.800 -0.104 -0.433 1.02 -0.951 -1.24 -1.07 -0.256
2 -0.969 0.0873 -0.614 3.25 -0.122 1.51 -1.40 -0.875 -1.31 -0.256
3 -1.19 1.96 0.582 -0.104 4.24 -0.112 -0.0567 2.40 0.421 -0.256
4 -0.745 0.677 2.65 -0.104 0.190 0.212 -0.951 1.31 1.66 -0.256
5 -0.0729 0.296 -1.16 -0.104 -0.122 0.780 -0.504 0.580 0.173 -0.256
6 -0.0729 0.886 -0.831 -0.104 -0.122 0.374 -0.504 -0.147 1.16 -0.256
7 -0.745 0.608 0.147 1.01 -0.122 -0.355 -0.951 1.49 -0.818 -0.256
8 -0.521 0.434 1.89 -0.104 -1.37 0.212 -0.504 -0.875 0.916 -0.256
9 -0.745 0.608 -0.940 -0.104 -0.744 0.942 0.390 -0.875 0.173 0.730
10 -0.745 0.330 0.800 -0.104 1.12 1.02 -1.40 -1.24 0.173 0.730
# ℹ 3,245 more rows
# ℹ 488 more variables: be <dbl>, but <dbl>, by <dbl>, chapter <dbl>,
# child <dbl>, come <dbl>, could <dbl>, crack <dbl>, day <dbl>, dear <dbl>,
# do <dbl>, family <dbl>, father <dbl>, `for` <dbl>, friend <dbl>,
# from <dbl>, gentleman <dbl>, get <dbl>, girl <dbl>, give <dbl>, go <dbl>,
# good <dbl>, hand <dbl>, have <dbl>, he <dbl>, hear <dbl>, herself <dbl>,
# himself <dbl>, house <dbl>, how <dbl>, idea <dbl>, `if` <dbl>, …
# Также подготовим рецепт с главными компонентами
<- base_rec |>
pca_rec step_pca(all_numeric_predictors(), num_comp = 7)
pca_rec
# Обучим для снижения размерности
<- pca_rec |>
pca_trained prep(data_train)
# Визуализируем результаты снижения размерности.
|>
pca_trained juice() |>
ggplot(aes(PC1, PC2, color = author)) +
geom_point() +
theme_light() +
ggtitle("Снижение размерности методом главных компонент")
# Визуализируем по словам.
library(learntidymodels)
|>
pca_trained plot_top_loadings(component_number <= 4, n = 10) +
scale_fill_brewer(palette = "Paired") +
theme_light()
По первой компоненте хорош отличим Trollope (“not”, “to”, “will”), по второй компоненте Richardson (“which”) и Sterne (“of”).
# Создадим рецепт с PLS и посмотрим на результаты снижения размерности.
<- base_trained |>
pls_trained step_pls(all_numeric_predictors(), outcome = "author", num_comp = 7) |>
prep()
|>
pls_trained juice()
# A tibble: 3,255 × 8
author PLS1 PLS2 PLS3 PLS4 PLS5 PLS6 PLS7
<fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 ABronte -0.688 -2.40 -2.41 0.493 1.78 0.920 0.149
2 ABronte -0.598 -0.583 -1.67 -1.07 0.325 1.03 0.713
3 ABronte -1.37 -1.98 -2.49 0.255 -0.583 0.436 1.08
4 ABronte 0.166 -0.313 -1.29 -0.724 -0.0430 0.275 0.560
5 ABronte -0.539 -1.80 -1.04 -0.419 0.707 0.240 0.970
6 ABronte -2.07 0.381 -0.791 -0.962 0.253 -0.159 0.785
7 ABronte -2.27 1.81 1.27 -0.979 -1.69 0.642 0.364
8 ABronte 2.70 1.33 -2.97 -0.153 -1.04 1.01 0.845
9 ABronte -1.63 -1.44 0.312 -0.977 0.358 0.993 0.616
10 ABronte 0.328 0.927 -1.43 -1.15 -0.862 1.33 1.22
# ℹ 3,245 more rows
# Визуализируем
|>
pls_trained juice() |>
ggplot(aes(PLS1, PLS2, color = author)) +
geom_point() +
theme_light() +
ggtitle("Снижение размерности методом частичных наименьших квадратов")
# Визуализируем нагрузки компонент по словам.
|>
pls_trained plot_top_loadings(component_number <= 4, n = 10, type = "pls") +
scale_fill_brewer(palette = "Paired") +
theme_light()
Здесь мы можем увидеть более четкие классы. Хотя, по прежнему по первым двум компонентам хорошо различимы Richardson, Trollope, Sterne, чуть лучше стала ситуация с Austin.
# Создадим рецепты с PLS и UMAP.
<- base_rec |>
pls_rec step_pls(all_numeric_predictors(), outcome = "author", num_comp = tune())
<- base_rec |>
umap_rec step_umap(all_numeric_predictors(),
outcome = "author",
num_comp = tune(),
neighbors = tune(),
min_dist = tune()
)# Визуализируем с umap.
|>
base_trained step_umap(all_numeric_predictors(), outcome = "author", num_comp = 4) |>
prep() |>
juice() |>
ggplot(aes(UMAP1, UMAP2, color = author)) +
geom_point(alpha = 0.5) +
theme_light()
Метод снижения размерности UMAP позволяет нам более четко воспринимать классы и границы между ними. По первым двум компонентам, ситуация та же.
Спецификации моделей
Для обучения многоклассовой классификации мы попробуем использовать три спецификации моделей: регуляризационные lasso и ridge, а также опорные векторы.
<- multinom_reg(penalty = tune(), mixture = 1) |>
lasso_spec set_mode("classification") |>
set_engine("glmnet")
<- multinom_reg(penalty = tune(), mixture = 0) |>
ridge_spec set_mode("classification") |>
set_engine("glmnet")
<- svm_linear(cost = tune()) |>
svm_spec set_mode("classification") |>
set_engine("LiblineaR")
Обучение и выбор лучшей модели
# A workflow set/tibble: 12 × 4
wflow_id info option result
<chr> <list> <list> <list>
1 base_svm <tibble [1 × 4]> <opts[0]> <list [0]>
2 base_lasso <tibble [1 × 4]> <opts[0]> <list [0]>
3 base_ridge <tibble [1 × 4]> <opts[0]> <list [0]>
4 pca_svm <tibble [1 × 4]> <opts[0]> <list [0]>
5 pca_lasso <tibble [1 × 4]> <opts[0]> <list [0]>
6 pca_ridge <tibble [1 × 4]> <opts[0]> <list [0]>
7 pls_svm <tibble [1 × 4]> <opts[0]> <list [0]>
8 pls_lasso <tibble [1 × 4]> <opts[0]> <list [0]>
9 pls_ridge <tibble [1 × 4]> <opts[0]> <list [0]>
10 umap_svm <tibble [1 × 4]> <opts[0]> <list [0]>
11 umap_lasso <tibble [1 × 4]> <opts[0]> <list [0]>
12 umap_ridge <tibble [1 × 4]> <opts[0]> <list [0]>
Методы снижения размерности, как мы видим в результате, не показали хороших результатов. Базовый рецепт оказался наиболее эффективным при обучении. Наиболее хорошие результаты (точность) характерны для методов гребневой регрессии (штрафные коэффициенты) и опорных векторов.
# A tibble: 24 × 9
wflow_id .config .metric mean std_err n preprocessor model rank
<chr> <chr> <chr> <dbl> <dbl> <int> <chr> <chr> <int>
1 base_svm Preprocessor… accura… 0.991 6.09e-4 5 recipe svm_… 1
2 base_svm Preprocessor… f_meas 0.988 8.09e-4 5 recipe svm_… 1
3 base_ridge Preprocessor… accura… 0.991 1.85e-3 5 recipe mult… 2
4 base_ridge Preprocessor… f_meas 0.987 3.15e-3 5 recipe mult… 2
5 base_lasso Preprocessor… accura… 0.990 1.03e-3 5 recipe mult… 3
6 base_lasso Preprocessor… f_meas 0.984 2.87e-3 5 recipe mult… 3
7 umap_lasso Preprocessor… accura… 0.945 3.52e-3 5 recipe mult… 4
8 umap_lasso Preprocessor… f_meas 0.926 4.08e-3 5 recipe mult… 4
9 umap_svm Preprocessor… accura… 0.941 5.05e-3 5 recipe svm_… 5
10 umap_svm Preprocessor… f_meas 0.913 7.53e-3 5 recipe svm_… 5
# ℹ 14 more rows
Посмотрим на параметры лучшей модели. В данном случае это гребневая регрессия.
Первые две модели справляются одинаково хорошо, на третьей происходит переобучение.
Финал. Тестирование.
# A tibble: 1 × 2
penalty .config
<dbl> <chr>
1 1.06e-10 Preprocessor1_Model1
После выбора наилучшей модели, предстоит оценить ее на всей обучающей выборке.
# A tibble: 3 × 4
.metric .estimator .estimate .config
<chr> <chr> <dbl> <chr>
1 f_meas macro 0.984 Preprocessor1_Model1
2 accuracy multiclass 0.994 Preprocessor1_Model1
3 roc_auc hand_till 1.00 Preprocessor1_Model1
Как мы можем увидеть, модель работает довольно неплохо, все показатели довольно близки к единице. Осталось только на них взглянуть)
Мы можем увидеть, что моделб ошибается в определении Emile Bronte, она путает ее с Charlotte Bronte (сестры ведь). Учитывая, что для E. Bronte было довольно мало данных в самом начале, такой результат не является удивительным. Laurence Sterne также определяется с ошибками, модель путает его с C. Bronte и A. Bronte. Очень хорошо определяется Richardson, Dickens, Thackeray, Trollope, Austen и Fildeing. Это может объясняться достаточным количеством данных. Выше мы наблюдали, что для них характерно большое количество отрывков. С одной ошибкой - Eliot и C. Bronte.
collect_predictions(ridge_res) |>
roc_curve(truth = author, .pred_ABronte:.pred_Trollope) |>
ggplot(aes(1 - specificity, sensitivity, color = .level)) +
geom_abline(slope = 1, color = "gray50", lty = 2, alpha = 0.8) +
geom_path(linewidth = 1.5, alpha = 0.7) +
labs(color = NULL) +
theme_light()
Судя по расположению кривой, модель работает достаточно эффективно.
Теперь давайте посмотрим, на характерные для каждого автора слова-признаки.
Как мы видим, помимо служебных частей речи и существительных, в качестве весомых признаков встречаются имена героев. Значит при лемматизации имели место ошибки. Например, это характерно для произведений C. Bronte (“roshester”, “jane”) и для J. Austen (“elizabeth”, “darcy”). По частому употреблению служебных частей речи (союзы) среди других очень выделяется A. Bronte (“but”, “or”, “and”). Это делает ее произведения довольно противоречивыми.Частое употребление противительных союзов может говорить о наличии контраста и сопоставления в произведении.
Выводы
- Качество модели очень сильно зависит от количества и репрезентативности данных.
- Важно! Необходимо проверять результаты лемматизации. Возможно, было бы лучше довести до ума первоначальную идею об извлечении служебных частей речи после лемматизации, и последующего подсчета относительных частот (жаль, что не получилось).
- В процессе определения авторства с помощью многоклассовой классификации, можно выделить неожиданные стилистические особенности автора, как в случае с Anne Bronte.