В предыдущих главах мы использовали такие алгоритмы, как регуляризованные линейные модели, машины опорных векторов и наивные байесовские модели для предсказания результатов на основе признаков, включая текстовые данные. Модели глубокого обучения решают те же задачи и преследуют те же цели, однако используются другие алгоритмы.
Глубокое обучение – это особый раздел машинного обучения. Под “глубиной” в глубоком обучении не подразумевают более глубокое понимание, достигаемое этим подходом; идея заключается в многослойном представлении. Количество слоев, которые делится модель данных, называют глубиной модели (Шолле 2023, 33).
Слои в модели глубокого обучения соединены в сеть, и такие модели называют нейронными сетями, хотя по сути они работают не так, как человеческий мозг. Слои могут быть соединены по-разному — такие конфигурации называют архитектурами сети.
В этом уроке мы познакомимся с полносвязной (dense) нейронной сетью для работы с текстом. Это одна из самых простых архитектур, и обычно такая модель не показывает наилучших результатов на текстовых данных, но с неё удобно начать, чтобы понять сам процесс построения и оценки глубоких моделей для работы с текстом. Кроме того, этот тип архитектуры может быть своеобразным мостом между методами “мешка слов”, которые мы использовали ранее, и более сложными подходами, позволяющими учитывать не только частотность слов, но также их последовательности.
На рисунке показана архитектура полносвязной прямой нейронной сети (feed-forward). Входные данные поступают в сеть сразу и полностью (в данном случае — полностью) соединены с первым скрытым слоем. Слой называется «скрытым», потому что не связан с внешним миром; этим занимаются только входной и выходной слои. Нейроны каждого слоя соединяются лишь со следующим слоем. Количество слоев и узлов в каждом из них может меняться; эти параметры называются гиперпараметрами и выбираются исследователем.
Под обучением сети подразумевается поиск набора значений весов всех слоев в сети, при котором сеть будет правильно отображать входные данные в соответствующие им результаты. Первоначально весам присваиваются случайные значения, но постепенно они корректируются в нужном направлении. “Нужным” в данном случае считается такое направление, которое минимизирует функцию потерь (среднеквадратическую ошибку, кросс-энтропию или другую).
За корректировку отвечает оптимизатор — это алгоритм, который управляет процессом обучения нейронной сети, корректируя веса модели с целью минимизации функции ошибки (или функции потерь). Проще говоря, оптимизатор помогает найти такие значения параметров, при которых сеть даёт наилучшие предсказания. Для этого он реализует алгоритм обратного распространения ошибки (backpropagation): для каждого параметра вычисляется вклад, который он вносит в значение потерь (Шолле 2023, 93).
Этот вклад определяется с помощью градиентов. Градиент – это обобщение понятия производной для функций, принимающих тензоры (многомерные массивы чисел) в качестве входных данных (Шолле 2023, 83). Градиент функции f – это вектор, который указывает направление наискорейшего роста этой функции, при этом модуль градиента равен скорости изменения функции в этом направлении.
Оптимизатор обновляет веса пропорционально этим градиентам (с учетом параметра скорости обучения), что позволяет постепенно приближаться к минимуму функции потерь. Градиентный спуск (gradient descent) — это метод оптимизации, который использует вычисленные градиенты для обновления весов сети с целью минимизации функции потерь. Он корректирует веса в направлении, противоположном градиенту (т.е. в сторону уменьшения ошибки).
28.2 Пакеты и виртуальное окружение
Пакет {reticulate} позволяет запускать Python-код прямо из R. Это обеспечивает интеграцию с Keras и Tensorflow: многие современные нейросетевые пакеты в R (в том числе {keras} и {tensorflow}) — всего лишь “обёртки” над Python-библиотеками.
library(tidyverse)library(tidymodels)library(textrecipes)conflicted::conflict_prefer("filter", winner ="dplyr")library(reticulate)library(keras3)# вспоминаем, какие создавали виртуальные окружения и гдеvirtualenv_list()
[1] "r-keras" "r-keras3-env" "r-reticulate"
virtualenv_root()
[1] "~/.virtualenvs"
# указываем путь до нужной версии питона# my_path <- "/opt/homebrew/bin/python3.11" # создаем окружение (если нет подходящего)# virtualenv_create("r-keras3-env", python = my_path)# активируем егоuse_virtualenv("r-keras3-env", required =TRUE)# устанавливаем Keras 3 и TensorFlow (если их еще нет в окружении)# virtualenv_install("r-keras3-env", c("tensorflow", "keras"))
Пакет {keras} для R предоставляет удобный интерфейс для Keras, высокоуровневого API для создания нейронных сетей. Keras отвечает за компоненты глубокого обучения высокого уровня: слои, функции потерь, оптимизатор, метрики, обучающий цикл. Keras опирается на Tensorflow (доступный в R через одноименный пакет), который отвечает за низкоуровневые манипуляции с тензорами. Специально вызывать library(tensorflow) не требуется.
# проверяемpy_config()
python: /Users/olga/.virtualenvs/r-keras3-env/bin/python
libpython: /opt/homebrew/opt/python@3.11/Frameworks/Python.framework/Versions/3.11/lib/python3.11/config-3.11-darwin/libpython3.11.dylib
pythonhome: /Users/olga/.virtualenvs/r-keras3-env:/Users/olga/.virtualenvs/r-keras3-env
version: 3.11.15 (main, Mar 3 2026, 00:52:57) [Clang 16.0.0 (clang-1600.0.26.6)]
numpy: /Users/olga/.virtualenvs/r-keras3-env/lib/python3.11/site-packages/numpy
numpy_version: 2.4.6
keras: /Users/olga/.virtualenvs/r-keras3-env/lib/python3.11/site-packages/keras
NOTE: Python version was forced by VIRTUAL_ENV
# Проверяем работу TensorFlow и Keraspy_run_string("import tensorflow as tfimport kerasprint('TensorFlow version:', tf.__version__)print('Keras version:', keras.__version__)")
Функция initial_validation_split() создает случайное разделение данных на три части: обучающую (training set), валидационную (validation set) и тестовую (testing set) выборки. Функции training(), validation() и testing() позволяют извлекать соответствующие подмножества данных после разбиения.
Мы начнем с простейшей модели типа “мешок слов”. Важно помнить, однако, что, поскольку мешок слов не сохраняет порядок следования токенов, этот метод обычно используется в поверхностных моделях обработки естественного языка и крайне редко – в моделях глубокого обучения (Шолле 2023, 414).
Число признаков уменьшаем за счет удаления цифр и стопслов. Установим максимальное значение признаков на 1000 (в реальных задачах должно быть больше). Обратите внимание, что исходная формула не задаёт зависимой переменной. Это нужно для удобства преобразования в матричный формат.
• Term frequency-inverse document frequency with: all_predictors()
Применим рецепт к обучающим данным. В функции bake() аргумент composition = "matrix" определяет формат возвращаемого результата. По умолчанию bake() возвращает tibble (или data.frame), где каждая строка — это наблюдение, а столбцы — признаки. Но мы планируем отдавать признаки нейросети, а она принимает матрицы на вход. Число элементов матрицы – 72 млн (число предикторов * число наблюдений).
One-hot кодирование меток классов — это способ представления категориальных переменных в виде бинарных векторов.
prepare_labels <-function(data) { data |>pull(class) |>as.numeric() |># даст 1,2,3,4 magrittr::subtract(1) |># преобразуем в 0,1,2,3to_categorical(num_classes =4)}class_train_onehot <-prepare_labels(data_train)class_valid_onehot <-prepare_labels(data_validate)class_test_onehot <-prepare_labels(data_test)
Функция to_categorical() из пакета {keras3} используется для преобразования вектора классов (представленного в виде целых чисел) в бинарную матрицу классов. Функция принимает вектор целочисленных меток классов, например, {0, 1, 2, 3}, и преобразует его в one-hot матрицу, где каждый класс кодируется бинарным вектором.
Пример:
[,1] [,2] [,3]
[1,] 1 0 0 # Класс 0
[2,] 0 1 0 # Класс 1
[3,] 0 0 1 # Класс 2
[4,] 0 1 0 # Класс 1
[5,] 1 0 0 # Класс 0
Здесь:
Каждая строка соответствует одному образцу.
Каждый столбец – это конкретный класс.
1 стоит в позиции индекса класса, остальное – 0.
Эта функция используется в нейронных сетях, потому что выходной слой softmax ожидает one-hot представление меток классов.
28.7 Спецификация модели BOW
Создаем пустую последовательную (sequential) модель. В последовательной модели слои идут один за другим, по порядку. Добавляем к ней два полносвязных (dense) слоя. Аргументом units = 32 указываем, что в первом и втором слое будет 32 нейрона. Число нейронов подбирается экспериментально. Наличие большей размерности (многомерное пространство представления) позволяет модели изучать более сложные представления, но делает модель более дорогостоящей в вычислительном отношении и может привести к переобучению (Шолле 2023, 149).
Аргумент activation = "relu" означает, что скрытые слои используют функцию активации relu (rectified linear unit, блок линейной ректификации). Эта функция преобразует отрицательные значения в ноль.
Без функции активации, такой как relu (также называемой фактором нелинейности) полносвязный слой layer_dense будет состоять из двух линейных операций – скалярного произведения и сложения: output <- dot(input, W) + b. Такой слой может обучаться только на линейных (аффинных) преобразованиях входных данных: пространство гипотез слоя было бы совокупностью всех возможных линейных преобразований входных данных в n-мерное пространство. Такое пространство гипотез слишком ограничено, и наложение нескольких уровней представлений друг на друга не приносило бы никакой выгоды, потому что сколь угодно длинная последовательность линейных преобразований все равно остается линейным преобразованием. – (Шолле 2023, 151)
После этого добавляем выходной слой. Здесь число нейронов соответствует числу предсказываемых классов, а активация softmax (activation = "softmax") превращает выходы нейронов в вероятности, сумма которых равна 1.
Здесь compile() — функция компиляции. Она “собирает” модель для обучения: определяет, как будут считаться ошибки (функция потерь), какой алгоритм оптимизации использовать, и по каким метрикам отслеживать качество.
Оптимизатор Adam (аргумент optimizer = "adam") - один из самых популярных оптимизаторов в глубоком обучении. Adam автоматически подбирает скорость обучения для каждого параметра. Работает быстро и надёжно на большинстве задач — особенно если нет времени или желания подбирать сложные параметры вручную.
Перекрестная энтропия (loss = "categorical_crossentropy") – функция потерь для задач многоклассовой классификации (multi-class classification). Эта функция подходит, когда на выходе модели softmax и целевая переменная — one-hot вектор.
На заметку
Перекрестная энтропия (crossentropy) – это термин из области теории информации, обозначающий меру расстояния между распределениями вероятностей или, в данном случае, между фактическими данными и предсказаниями.
Также прописываем метрику качества работы модели.
28.8 Обучение BOW-модели
Теперь проведем обучение модели в течение 10 эпох (выполним 10 итераций по всем образцам обучающих данных) пакетами по 512 образцов.
Пакет (batch) - это небольшой набор образцов, которые одновременно обрабатываются моделью. Количество часто равно степени двойки, чтобы упростить выделение памяти на процессоре. В процессе обучения пакет используется для одного обновления градиентного спуска, применяемого к весам модели.
Эпоха (epoch) — это один полный проход (прогон) по всему тренировочному датасету при обучении модели машинного обучения, например, нейронной сети. Например, если у вас есть 1000 картинок, а batch_size = 100, то за одну эпоху модель обработает все 1000 картинок по 100 за раз — всего 10 шагов (итераций). Модель обычно обучают несколько (десятков или сотен) эпох, чтобы она постепенно улучшала свои прогнозы.
Также будем следить за потерями и точностью на отложенных образцах.
Final epoch (plot to see history):
accuracy: 0.8808
loss: 0.3302
val_accuracy: 0.859
val_loss: 0.4042
После обучения в переменной bow_history сохраняется история процесса обучения: метрики, ошибки, прогресс и т.д. Взглянем на результат.
plot(bow_history) +theme_minimal()
bow_df <-as.data.frame(bow_history)bow_df
Чтобы добиться идеального обучения модели, ее сначала нужно переобучить. Если вы не знаете заранее, где лежит граница, вам придется пересечь ее, чтобы найти. Следовательно, ваша первоначальная цель, когда вы начинаете работать над задачей, заключается в том, чтобы получить модель, которая хоть в какой-то степени обобщает и способна к переобучению. Далее вы начинаете улучшать обобщение, попутно борясь с переобучением. – (Шолле 2023, 193)
28.9 Препроцессинг: Onehot-кодирование
step_sequence_onehot() превращает токены в числовой формат аналогично step_tf() и step_tfidf(), но в отличие от них учитывает порядок следования токенов.
В четвертой строке первое слово = 6, а это не “монстры”! Так произошло, потому что предложение слишком длинное и не вмещается в длину кодируемой последовательности (ее регулирует аргумент sequence_length). В таком случае текст усекается (аргумент truncating по умолчанию имеет значение "pre", но можно изменить на "post"). В коротких текстах добавляются нули, за это отвечает параметр padding. Немного изменим рецепт:
Теперь “монстры” в начале! А все нули сдвинулись вправо.
Напишем рецепт для новостного датасета.
max_words =1500max_length =50onehot_rec <-recipe( ~ description, data = data_train) |>step_mutate(description = stringr::str_remove_all(description, "\\d+")) |>step_tokenize(description) |>step_stopwords(description) |>step_tokenfilter(description, max_tokens = max_words, min_times =10) |>step_sequence_onehot(description, sequence_length = max_length,# потому что в новостях все самое важное обычно в началеtruncating ="post",prefix ="")onehot_rec
Наша вторая модель глубокого обучения преобразует тексты в эмбеддинги, затем “расплющивает” их (делает одномерными), а после этого обучает единственный полносвязный слой (dense network), чтобы предсказать класс новости.
dense_model <-keras_model_sequential() |># каждое слово превращается в плотный вектор фиксированной размерностиlayer_embedding(input_dim = max_words +1, output_dim =12) |># матрица 50 * 12 вытягивается в вектор длиной 600layer_flatten() |>layer_dense(units =32, activation ="relu") |>layer_dense(units =4, activation ="softmax")dense_model
28.11 Обучение модели на основе Onehot-кодирования
dense_history <- dense_model |>fit(x = onehot_train,y = class_train_onehot,batch_size =512,epochs =10,#validation_data = list(onehot_valid, class_valid_onehot), # заметьте еще один способ использовать часть данных для валидацииvalidation_split =0.25, verbose =FALSE)
plot(dense_history) +theme_minimal()
dense_history
Final epoch (plot to see history):
accuracy: 0.9054
loss: 0.2688
val_accuracy: 0.8536
val_loss: 0.4017
28.12 Прогнозирование и оценка
График выше показывает, что переобучение начинается после третьей эпохи, поэтому обучим модель с нуля в течение трех эпох и затем оценим ее на контрольных данных.
Простейшая модель позволила нам добиться точности около 87%. Этот результат можно улучшить, используя более сложные архитектуры. Пока используем модель для генерации предсказаний.
Поздравляем! Вы успешно построили и оценили две нейросетевые архитектуры для классификации текстов. 🎉
В этом уроке вы…
Освоили разные подходы к представлению текста: BOW (мешок слов) и последовательное one-hot кодирование
Изучили архитектурные принципы: научились создавать Embedding + Dense слои
Поняли важность борьбы с переобучением (а также то, что сначала его обязательно надо добиться!)
Научились оценивать модели, в том числе построили ROC-кривые для многоклассовой задачи и визуализировали матрицу ошибок
Познакомились с новыми инструментами: {keras3} и {tensorflow}
Интегрировали Python-библиотеки через {reticulate}
Этот урок — ваша отправная точка в мире глубокого обучения для NLP. Каждый из использованных компонентов открывает путь к более сложным и мощным моделям. Продолжайте экспериментировать, и пусть ваши нейросети становятся все умнее! 🚀
Шолле, Франсуа. 2023. Глубокое обучение с R и Keras. ДМК.