AI-For-Beginners/translations/ru/lessons/5-NLP/16-RNN/RNNTF.ipynb

460 lines
33 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Рекуррентные нейронные сети\n",
"\n",
"В предыдущем модуле мы рассмотрели богатые семантические представления текста. Архитектура, которую мы использовали, захватывает агрегированное значение слов в предложении, но не учитывает **порядок** слов, поскольку операция агрегации, следующая за эмбеддингами, удаляет эту информацию из исходного текста. Поскольку эти модели не могут представлять порядок слов, они не способны решать более сложные или неоднозначные задачи, такие как генерация текста или ответы на вопросы.\n",
"\n",
"Чтобы уловить смысл последовательности текста, мы будем использовать архитектуру нейронной сети, называемую **рекуррентной нейронной сетью** или RNN. При использовании RNN мы пропускаем наше предложение через сеть по одному токену за раз, и сеть создает некоторое **состояние**, которое затем передается в сеть вместе со следующим токеном.\n",
"\n",
"![Изображение, показывающее пример генерации рекуррентной нейронной сети.](../../../../../translated_images/ru/rnn.27f5c29c53d727b5.webp)\n",
"\n",
"Учитывая входную последовательность токенов $X_0,\\dots,X_n$, RNN создает последовательность блоков нейронной сети и обучает эту последовательность от начала до конца с использованием обратного распространения ошибки. Каждый блок сети принимает пару $(X_i,S_i)$ в качестве входных данных и производит $S_{i+1}$ в качестве результата. Финальное состояние $S_n$ или выход $Y_n$ передается в линейный классификатор для получения результата. Все блоки сети имеют одинаковые веса и обучаются от начала до конца за один проход обратного распространения.\n",
"\n",
"> На рисунке выше показана рекуррентная нейронная сеть в развернутой форме (слева) и в более компактной рекуррентной форме (справа). Важно понимать, что все ячейки RNN имеют одинаковые **общие веса**.\n",
"\n",
"Поскольку векторы состояния $S_0,\\dots,S_n$ передаются через сеть, RNN способна обучаться последовательным зависимостям между словами. Например, когда слово *not* появляется где-то в последовательности, сеть может научиться отрицать определенные элементы внутри вектора состояния.\n",
"\n",
"Внутри каждой ячейки RNN содержатся две матрицы весов: $W_H$ и $W_I$, а также смещение $b$. На каждом шаге RNN, учитывая вход $X_i$ и входное состояние $S_i$, выходное состояние вычисляется как $S_{i+1} = f(W_H\\times S_i + W_I\\times X_i+b)$, где $f$ — это функция активации (часто $\\tanh$).\n",
"\n",
"> Для задач, таких как генерация текста (которую мы рассмотрим в следующем разделе) или машинный перевод, мы также хотим получать некоторое выходное значение на каждом шаге RNN. В этом случае существует еще одна матрица $W_O$, и выход вычисляется как $Y_i=f(W_O\\times S_i+b_O)$.\n",
"\n",
"Давайте посмотрим, как рекуррентные нейронные сети могут помочь нам классифицировать наш набор данных новостей.\n",
"\n",
"> Для песочницы необходимо выполнить следующую ячейку, чтобы убедиться, что нужная библиотека установлена, а данные предварительно загружены. Если вы работаете локально, этот шаг можно пропустить.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"!{sys.executable} -m pip install --quiet tensorflow_datasets==4.4.0\n",
"!cd ~ && wget -q -O - https://mslearntensorflowlp.blob.core.windows.net/data/tfds-ag-news.tgz | tar xz"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import tensorflow as tf\n",
"from tensorflow import keras\n",
"import tensorflow_datasets as tfds\n",
"import numpy as np\n",
"\n",
"# We are going to be training pretty large models. In order not to face errors, we need\n",
"# to set tensorflow option to grow GPU memory allocation when required\n",
"physical_devices = tf.config.list_physical_devices('GPU') \n",
"if len(physical_devices)>0:\n",
" tf.config.experimental.set_memory_growth(physical_devices[0], True)\n",
"\n",
"ds_train, ds_test = tfds.load('ag_news_subset').values()"
]
},
{
"cell_type": "markdown",
"metadata": {
"nteract": {
"transient": {
"deleting": false
}
}
},
"source": [
"При обучении крупных моделей распределение памяти GPU может стать проблемой. Также может понадобиться экспериментировать с различными размерами минибатчей, чтобы данные помещались в память GPU, а обучение проходило достаточно быстро. Если вы запускаете этот код на своей машине с GPU, попробуйте настроить размер минибатча для ускорения обучения.\n",
"\n",
"> **Примечание**: Известно, что некоторые версии драйверов NVidia не освобождают память после обучения модели. В этом ноутбуке мы запускаем несколько примеров, и это может привести к исчерпанию памяти в определённых конфигурациях, особенно если вы проводите собственные эксперименты в рамках того же ноутбука. Если вы столкнулись с странными ошибками при запуске обучения модели, попробуйте перезапустить ядро ноутбука.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": true,
"jupyter": {
"outputs_hidden": false,
"source_hidden": false
},
"nteract": {
"transient": {
"deleting": false
}
}
},
"outputs": [],
"source": [
"batch_size = 16\n",
"embed_size = 64"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Простой классификатор на основе RNN\n",
"\n",
"В случае простой RNN каждая рекуррентная единица представляет собой простую линейную сеть, которая принимает на вход вектор входных данных и вектор состояния, а затем выдает новый вектор состояния. В Keras это можно реализовать с помощью слоя `SimpleRNN`.\n",
"\n",
"Хотя мы можем передавать токены в формате one-hot кодирования напрямую в слой RNN, это не лучшая идея из-за их высокой размерности. Поэтому мы будем использовать слой embedding для уменьшения размерности векторов слов, затем слой RNN, а в конце — классификатор `Dense`.\n",
"\n",
"> **Note**: В случаях, когда размерность не слишком велика, например, при использовании токенизации на уровне символов, может быть целесообразно передавать токены в формате one-hot кодирования напрямую в ячейку RNN.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Model: \"sequential\"\n",
"_________________________________________________________________\n",
"Layer (type) Output Shape Param # \n",
"=================================================================\n",
"text_vectorization (TextVect (None, None) 0 \n",
"_________________________________________________________________\n",
"embedding (Embedding) (None, None, 64) 1280000 \n",
"_________________________________________________________________\n",
"simple_rnn (SimpleRNN) (None, 16) 1296 \n",
"_________________________________________________________________\n",
"dense (Dense) (None, 4) 68 \n",
"=================================================================\n",
"Total params: 1,281,364\n",
"Trainable params: 1,281,364\n",
"Non-trainable params: 0\n",
"_________________________________________________________________\n"
]
}
],
"source": [
"vocab_size = 20000\n",
"\n",
"vectorizer = keras.layers.experimental.preprocessing.TextVectorization(\n",
" max_tokens=vocab_size,\n",
" input_shape=(1,))\n",
"\n",
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, embed_size),\n",
" keras.layers.SimpleRNN(16),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.summary()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Примечание:** Здесь мы используем необученный слой встраивания для упрощения, но для достижения лучших результатов можно использовать предварительно обученный слой встраивания с помощью Word2Vec, как описано в предыдущем разделе. Это будет хорошим упражнением — адаптировать этот код для работы с предварительно обученными встраиваниями.\n",
"\n",
"Теперь давайте обучим нашу RNN. В целом, RNN довольно сложно обучать, так как после разворачивания ячеек RNN вдоль длины последовательности количество слоев, участвующих в обратном распространении ошибки, становится очень большим. Поэтому необходимо выбрать меньшую скорость обучения и обучать сеть на большем наборе данных, чтобы получить хорошие результаты. Это может занять довольно много времени, поэтому предпочтительно использовать GPU.\n",
"\n",
"Чтобы ускорить процесс, мы будем обучать модель RNN только на заголовках новостей, исключая описание. Вы можете попробовать обучить модель с использованием описания и посмотреть, удастся ли вам добиться её обучения.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Training vectorizer\n"
]
}
],
"source": [
"def extract_title(x):\n",
" return x['title']\n",
"\n",
"def tupelize_title(x):\n",
" return (extract_title(x),x['label'])\n",
"\n",
"print('Training vectorizer')\n",
"vectorizer.adapt(ds_train.take(2000).map(extract_title))"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"7500/7500 [==============================] - 82s 11ms/step - loss: 0.6629 - acc: 0.7623 - val_loss: 0.5559 - val_acc: 0.7995\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3e0030d350>"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize_title).batch(batch_size),validation_data=ds_test.map(tupelize_title).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {
"nteract": {
"transient": {
"deleting": false
}
}
},
"source": [
"> **Примечание**: точность здесь, вероятно, будет ниже, потому что мы обучаемся только на заголовках новостей.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Повторное рассмотрение последовательностей переменной длины\n",
"\n",
"Помните, что слой `TextVectorization` автоматически дополняет последовательности переменной длины в минибатче токенами-заполнителями. Оказывается, эти токены также участвуют в обучении, и это может усложнить сходимость модели.\n",
"\n",
"Существует несколько подходов, которые мы можем использовать, чтобы минимизировать количество дополнений. Один из них — это упорядочить набор данных по длине последовательностей и сгруппировать все последовательности по размеру. Это можно сделать с помощью функции `tf.data.experimental.bucket_by_sequence_length` (см. [документацию](https://www.tensorflow.org/api_docs/python/tf/data/experimental/bucket_by_sequence_length)).\n",
"\n",
"Другой подход — использовать **маскирование**. В Keras некоторые слои поддерживают дополнительный ввод, который указывает, какие токены следует учитывать при обучении. Чтобы включить маскирование в нашу модель, мы можем либо добавить отдельный слой `Masking` ([документация](https://keras.io/api/layers/core_layers/masking/)), либо указать параметр `mask_zero=True` в нашем слое `Embedding`.\n",
"\n",
"> **Примечание**: Обучение займет около 5 минут для завершения одной эпохи на всем наборе данных. Вы можете прервать обучение в любой момент, если у вас закончится терпение. Также вы можете ограничить объем данных, используемых для обучения, добавив оператор `.take(...)` после наборов данных `ds_train` и `ds_test`.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"7500/7500 [==============================] - 371s 49ms/step - loss: 0.5401 - acc: 0.8079 - val_loss: 0.3780 - val_acc: 0.8822\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3dec118850>"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def extract_text(x):\n",
" return x['title']+' '+x['description']\n",
"\n",
"def tupelize(x):\n",
" return (extract_text(x),x['label'])\n",
"\n",
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size,embed_size,mask_zero=True),\n",
" keras.layers.SimpleRNN(16),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Теперь, когда мы используем маскирование, мы можем обучить модель на всем наборе данных заголовков и описаний.\n",
"\n",
"> **Примечание**: Вы заметили, что мы использовали векторизатор, обученный на заголовках новостей, а не на полном тексте статьи? Потенциально это может привести к игнорированию некоторых токенов, поэтому лучше переобучить векторизатор. Однако эффект может быть совсем незначительным, поэтому для упрощения мы будем использовать ранее обученный векторизатор.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## LSTM: Долговременная кратковременная память\n",
"\n",
"Одна из главных проблем рекуррентных нейронных сетей (RNN) — это **затухающие градиенты**. RNN могут быть довольно длинными, и им может быть сложно передавать градиенты обратно к первому слою сети во время обратного распространения. Когда это происходит, сеть не может обучаться выявлению связей между удаленными токенами. Один из способов избежать этой проблемы — ввести **явное управление состоянием** с помощью **гейтов**. Две наиболее распространенные архитектуры, использующие гейты, — это **долговременная кратковременная память** (LSTM) и **гейтированная релейная единица** (GRU). Здесь мы рассмотрим LSTM.\n",
"\n",
"![Изображение, показывающее пример ячейки долговременной кратковременной памяти](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"Сеть LSTM организована аналогично RNN, но есть два состояния, которые передаются от слоя к слою: фактическое состояние $c$ и скрытый вектор $h$. В каждом блоке скрытый вектор $h_{t-1}$ комбинируется с входом $x_t$, и вместе они управляют тем, что происходит с состоянием $c_t$ и выходом $h_{t}$ через **гейты**. Каждый гейт имеет сигмоидную активацию (выход в диапазоне $[0,1]$), которую можно рассматривать как побитовую маску при умножении на вектор состояния. LSTM имеют следующие гейты (слева направо на изображении выше):\n",
"* **гейт забывания**, который определяет, какие компоненты вектора $c_{t-1}$ нужно забыть, а какие пропустить дальше.\n",
"* **входной гейт**, который определяет, сколько информации из входного вектора и предыдущего скрытого вектора следует включить в вектор состояния.\n",
"* **выходной гейт**, который берет новый вектор состояния и решает, какие его компоненты будут использованы для формирования нового скрытого вектора $h_t$.\n",
"\n",
"Компоненты состояния $c$ можно рассматривать как флаги, которые можно включать и выключать. Например, когда мы встречаем имя *Алиса* в последовательности, мы предполагаем, что речь идет о женщине, и поднимаем флаг в состоянии, который говорит, что в предложении есть женский существительный. Когда мы далее встречаем слова *и Том*, мы поднимаем флаг, который говорит, что у нас есть множественное число существительных. Таким образом, манипулируя состоянием, мы можем отслеживать грамматические свойства предложения.\n",
"\n",
"> **Note**: Вот отличный ресурс для понимания внутреннего устройства LSTM: [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) от Кристофера Олаха.\n",
"\n",
"Хотя внутренняя структура ячейки LSTM может выглядеть сложной, Keras скрывает эту реализацию внутри слоя `LSTM`, поэтому единственное, что нам нужно сделать в приведенном выше примере, — это заменить рекуррентный слой:\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"15000/15000 [==============================] - 188s 13ms/step - loss: 0.5692 - acc: 0.7916 - val_loss: 0.3441 - val_acc: 0.8870\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3d6af5c350>"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, embed_size),\n",
" keras.layers.LSTM(8),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(8),validation_data=ds_test.map(tupelize).batch(8))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Двунаправленные и многослойные RNN\n",
"\n",
"В наших предыдущих примерах рекуррентные сети обрабатывали последовательности от начала до конца. Это кажется естественным, так как соответствует направлению, в котором мы читаем или слушаем речь. Однако в сценариях, где требуется произвольный доступ к элементам входной последовательности, логичнее выполнять рекуррентные вычисления в обоих направлениях. RNN, которые позволяют вычисления в двух направлениях, называются **двунаправленными** (bidirectional) RNN, и их можно создать, обернув рекуррентный слой в специальный слой `Bidirectional`.\n",
"\n",
"> **Note**: Слой `Bidirectional` создает две копии слоя внутри себя и устанавливает свойство `go_backwards` одной из этих копий в значение `True`, что заставляет её обрабатывать последовательность в обратном направлении.\n",
"\n",
"Рекуррентные сети, будь то однонаправленные или двунаправленные, захватывают закономерности внутри последовательности и сохраняют их в векторах состояния или возвращают их в качестве результата. Как и в случае с сверточными сетями, мы можем добавить еще один рекуррентный слой после первого, чтобы захватывать закономерности более высокого уровня, построенные на основе закономерностей более низкого уровня, извлеченных первым слоем. Это приводит нас к понятию **многослойной RNN**, которая состоит из двух или более рекуррентных сетей, где выход предыдущего слоя передается в следующий слой в качестве входных данных.\n",
"\n",
"![Изображение, показывающее многослойную LSTM-RNN](../../../../../translated_images/ru/multi-layer-lstm.dd975e29bb2a59fe.webp)\n",
"\n",
"*Изображение из [этой замечательной статьи](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) Фернандо Лопеса.*\n",
"\n",
"Keras упрощает создание таких сетей, так как вам нужно просто добавить больше рекуррентных слоев в модель. Для всех слоев, кроме последнего, необходимо указать параметр `return_sequences=True`, чтобы слой возвращал все промежуточные состояния, а не только финальное состояние рекуррентных вычислений.\n",
"\n",
"Давайте создадим двухслойную двунаправленную LSTM для нашей задачи классификации.\n",
"\n",
"> **Note** Этот код снова выполняется довольно долго, но он дает наивысшую точность из всех, которые мы видели до сих пор. Так что, возможно, стоит подождать и посмотреть результат.\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5044/7500 [===================>..........] - ETA: 2:33 - loss: 0.3709 - acc: 0.8706\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r5045/7500 [===================>..........] - ETA: 2:33 - loss: 0.3709 - acc: 0.8706"
]
}
],
"source": [
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, 128, mask_zero=True),\n",
" keras.layers.Bidirectional(keras.layers.LSTM(64,return_sequences=True)),\n",
" keras.layers.Bidirectional(keras.layers.LSTM(64)), \n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(batch_size),\n",
" validation_data=ds_test.map(tupelize).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNN для других задач\n",
"\n",
"До этого момента мы сосредоточились на использовании RNN для классификации текстовых последовательностей. Однако они могут справляться с множеством других задач, таких как генерация текста и машинный перевод — эти задачи мы рассмотрим в следующем разделе.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Отказ от ответственности**: \nЭтот документ был переведен с помощью сервиса автоматического перевода [Co-op Translator](https://github.com/Azure/co-op-translator). Несмотря на наши усилия обеспечить точность, автоматические переводы могут содержать ошибки или неточности. Оригинальный документ на его исходном языке следует считать авторитетным источником. Для получения критически важной информации рекомендуется профессиональный перевод человеком. Мы не несем ответственности за любые недоразумения или неправильные интерпретации, возникшие в результате использования данного перевода.\n"
]
}
],
"metadata": {
"kernel_info": {
"name": "conda-env-py37_tensorflow-py"
},
"kernelspec": {
"display_name": "py37_tensorflow",
"language": "python",
"name": "conda-env-py37_tensorflow-py"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.9"
},
"nteract": {
"version": "nteract-front-end@1.0.0"
},
"coopTranslator": {
"original_hash": "81351e61f619b432ff51010a4f993194",
"translation_date": "2025-08-28T12:18:29+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNTF.ipynb",
"language_code": "ru"
}
},
"nbformat": 4,
"nbformat_minor": 4
}