479 lines
34 KiB
Plaintext
479 lines
34 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Рекуррентные нейронные сети\n",
|
||
"\n",
|
||
"В предыдущем модуле мы использовали богатые семантические представления текста и простой линейный классификатор поверх эмбеддингов. Такая архитектура позволяет захватывать агрегированное значение слов в предложении, но не учитывает **порядок** слов, так как операция агрегации поверх эмбеддингов удаляет эту информацию из исходного текста. Поскольку эти модели не могут моделировать порядок слов, они не способны решать более сложные или неоднозначные задачи, такие как генерация текста или ответы на вопросы.\n",
|
||
"\n",
|
||
"Чтобы уловить смысл последовательности текста, необходимо использовать другую архитектуру нейронной сети, которая называется **рекуррентной нейронной сетью** (RNN). В RNN мы пропускаем предложение через сеть по одному символу за раз, и сеть формирует некоторое **состояние**, которое затем передается обратно в сеть вместе со следующим символом.\n",
|
||
"\n",
|
||
"Дано входная последовательность токенов $X_0,\\dots,X_n$, RNN создает последовательность блоков нейронной сети и обучает эту последовательность от начала до конца с использованием обратного распространения. Каждый блок сети принимает пару $(X_i,S_i)$ в качестве входных данных и выдает $S_{i+1}$ в качестве результата. Финальное состояние $S_n$ или выход $X_n$ передается в линейный классификатор для получения результата. Все блоки сети используют одни и те же веса и обучаются от начала до конца за один проход обратного распространения.\n",
|
||
"\n",
|
||
"Благодаря тому, что вектор состояния $S_0,\\dots,S_n$ передается через сеть, она способна обучаться последовательным зависимостям между словами. Например, если слово *not* появляется где-то в последовательности, сеть может научиться отрицать определенные элементы внутри вектора состояния, что приводит к отрицанию.\n",
|
||
"\n",
|
||
"> Поскольку веса всех блоков RNN на картинке одинаковы, ту же картинку можно представить как один блок (справа) с рекуррентной обратной связью, которая передает выходное состояние сети обратно на вход.\n",
|
||
"\n",
|
||
"Давайте посмотрим, как рекуррентные нейронные сети могут помочь нам классифицировать наш набор данных новостей.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Loading dataset...\n",
|
||
"Building vocab...\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"import torch\n",
|
||
"import torchtext\n",
|
||
"from torchnlp import *\n",
|
||
"train_dataset, test_dataset, classes, vocab = load_dataset()\n",
|
||
"vocab_size = len(vocab)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Простой классификатор на основе RNN\n",
|
||
"\n",
|
||
"В случае простой RNN каждый рекуррентный блок представляет собой простую линейную сеть, которая принимает объединённый входной вектор и вектор состояния и производит новый вектор состояния. PyTorch представляет этот блок с помощью класса `RNNCell`, а сеть из таких блоков — как слой `RNN`.\n",
|
||
"\n",
|
||
"Чтобы определить классификатор на основе RNN, мы сначала применим слой эмбеддинга для уменьшения размерности входного словаря, а затем добавим слой RNN поверх него:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class RNNClassifier(torch.nn.Module):\n",
|
||
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
|
||
" super().__init__()\n",
|
||
" self.hidden_dim = hidden_dim\n",
|
||
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
|
||
" self.rnn = torch.nn.RNN(embed_dim,hidden_dim,batch_first=True)\n",
|
||
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
|
||
"\n",
|
||
" def forward(self, x):\n",
|
||
" batch_size = x.size(0)\n",
|
||
" x = self.embedding(x)\n",
|
||
" x,h = self.rnn(x)\n",
|
||
" return self.fc(x.mean(dim=1))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"> **Note:** Здесь мы используем необученный слой встраивания для упрощения, но для достижения еще лучших результатов можно использовать предварительно обученный слой встраивания с Word2Vec или GloVe, как описано в предыдущем разделе. Для лучшего понимания вы можете адаптировать этот код для работы с предварительно обученными встраиваниями.\n",
|
||
"\n",
|
||
"В нашем случае мы будем использовать загрузчик данных с дополнением, чтобы каждый пакет содержал несколько дополненных последовательностей одинаковой длины. Слой RNN будет принимать последовательность тензоров встраивания и выдавать два результата:\n",
|
||
"* $x$ — это последовательность выходов ячеек RNN на каждом шаге\n",
|
||
"* $h$ — это финальное скрытое состояние для последнего элемента последовательности\n",
|
||
"\n",
|
||
"Затем мы применяем полностью связанный линейный классификатор, чтобы получить количество классов.\n",
|
||
"\n",
|
||
"> **Note:** RNN сложно обучать, потому что, когда ячейки RNN разворачиваются вдоль длины последовательности, количество слоев, участвующих в обратном распространении, становится довольно большим. Поэтому необходимо выбрать небольшой коэффициент обучения и обучать сеть на большом наборе данных для получения хороших результатов. Это может занять довольно много времени, поэтому предпочтительно использовать GPU.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"metadata": {
|
||
"scrolled": true
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"3200: acc=0.3090625\n",
|
||
"6400: acc=0.38921875\n",
|
||
"9600: acc=0.4590625\n",
|
||
"12800: acc=0.511953125\n",
|
||
"16000: acc=0.5506875\n",
|
||
"19200: acc=0.57921875\n",
|
||
"22400: acc=0.6070089285714285\n",
|
||
"25600: acc=0.6304296875\n",
|
||
"28800: acc=0.6484027777777778\n",
|
||
"32000: acc=0.66509375\n",
|
||
"35200: acc=0.6790056818181818\n",
|
||
"38400: acc=0.6929166666666666\n",
|
||
"41600: acc=0.7035817307692308\n",
|
||
"44800: acc=0.7137276785714286\n",
|
||
"48000: acc=0.72225\n",
|
||
"51200: acc=0.73001953125\n",
|
||
"54400: acc=0.7372794117647059\n",
|
||
"57600: acc=0.7436631944444444\n",
|
||
"60800: acc=0.7503947368421052\n",
|
||
"64000: acc=0.75634375\n",
|
||
"67200: acc=0.7615773809523809\n",
|
||
"70400: acc=0.7662642045454545\n",
|
||
"73600: acc=0.7708423913043478\n",
|
||
"76800: acc=0.7751822916666666\n",
|
||
"80000: acc=0.7790625\n",
|
||
"83200: acc=0.7825\n",
|
||
"86400: acc=0.7858564814814815\n",
|
||
"89600: acc=0.7890513392857142\n",
|
||
"92800: acc=0.7920474137931034\n",
|
||
"96000: acc=0.7952708333333334\n",
|
||
"99200: acc=0.7982258064516129\n",
|
||
"102400: acc=0.80099609375\n",
|
||
"105600: acc=0.8037594696969697\n",
|
||
"108800: acc=0.8060569852941176\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)\n",
|
||
"net = RNNClassifier(vocab_size,64,32,len(classes)).to(device)\n",
|
||
"train_epoch(net,train_loader, lr=0.001)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Долгая краткосрочная память (LSTM)\n",
|
||
"\n",
|
||
"Одна из главных проблем классических RNN — это так называемая проблема **затухающих градиентов**. Поскольку RNN обучаются от начала до конца за один проход обратного распространения, им сложно передавать ошибку к первым слоям сети, и, следовательно, сеть не может выучить связи между удаленными токенами. Один из способов избежать этой проблемы — ввести **явное управление состоянием** с помощью так называемых **врат**. Существует две наиболее известные архитектуры такого типа: **Долгая краткосрочная память** (LSTM) и **Управляемая релейная единица** (GRU).\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"Сеть LSTM организована аналогично RNN, но в ней передаются два состояния от слоя к слою: текущее состояние $c$ и скрытый вектор $h$. В каждой ячейке скрытый вектор $h_i$ объединяется с входом $x_i$, и они управляют тем, что происходит с состоянием $c$ через **врата**. Каждые врата представляют собой нейронную сеть с сигмоидной активацией (выход в диапазоне $[0,1]$), которую можно рассматривать как побитовую маску при умножении на вектор состояния. Существуют следующие врата (слева направо на изображении выше):\n",
|
||
"* **врата забывания** принимают скрытый вектор и определяют, какие компоненты вектора $c$ нужно забыть, а какие пропустить дальше.\n",
|
||
"* **входные врата** берут некоторую информацию из входа и скрытого вектора и добавляют её в состояние.\n",
|
||
"* **выходные врата** преобразуют состояние через линейный слой с активацией $\\tanh$, затем выбирают некоторые из его компонентов, используя скрытый вектор $h_i$, чтобы сформировать новое состояние $c_{i+1}$.\n",
|
||
"\n",
|
||
"Компоненты состояния $c$ можно рассматривать как своего рода флаги, которые можно включать и выключать. Например, когда мы встречаем имя *Alice* в последовательности, мы можем предположить, что оно относится к женскому персонажу, и установить флаг в состоянии, указывающий на наличие женского существительного в предложении. Когда мы далее встречаем фразу *and Tom*, мы поднимаем флаг, указывающий на наличие множественного числа. Таким образом, манипулируя состоянием, мы можем, предположительно, отслеживать грамматические свойства частей предложения.\n",
|
||
"\n",
|
||
"> **Note**: Отличный ресурс для понимания внутреннего устройства LSTM — это замечательная статья [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) Кристофера Олаха.\n",
|
||
"\n",
|
||
"Хотя внутренняя структура ячейки LSTM может показаться сложной, PyTorch скрывает эту реализацию внутри класса `LSTMCell` и предоставляет объект `LSTM` для представления всего слоя LSTM. Таким образом, реализация классификатора на основе LSTM будет очень похожа на простую RNN, которую мы рассматривали выше:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class LSTMClassifier(torch.nn.Module):\n",
|
||
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
|
||
" super().__init__()\n",
|
||
" self.hidden_dim = hidden_dim\n",
|
||
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
|
||
" self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n",
|
||
" self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n",
|
||
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
|
||
"\n",
|
||
" def forward(self, x):\n",
|
||
" batch_size = x.size(0)\n",
|
||
" x = self.embedding(x)\n",
|
||
" x,(h,c) = self.rnn(x)\n",
|
||
" return self.fc(h[-1])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": []
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"3200: acc=0.259375\n",
|
||
"6400: acc=0.25859375\n",
|
||
"9600: acc=0.26177083333333334\n",
|
||
"12800: acc=0.2784375\n",
|
||
"16000: acc=0.313\n",
|
||
"19200: acc=0.3528645833333333\n",
|
||
"22400: acc=0.3965625\n",
|
||
"25600: acc=0.4385546875\n",
|
||
"28800: acc=0.4752777777777778\n",
|
||
"32000: acc=0.505375\n",
|
||
"35200: acc=0.5326704545454546\n",
|
||
"38400: acc=0.5557552083333334\n",
|
||
"41600: acc=0.5760817307692307\n",
|
||
"44800: acc=0.5954910714285714\n",
|
||
"48000: acc=0.6118333333333333\n",
|
||
"51200: acc=0.62681640625\n",
|
||
"54400: acc=0.6404779411764706\n",
|
||
"57600: acc=0.6520138888888889\n",
|
||
"60800: acc=0.662828947368421\n",
|
||
"64000: acc=0.673546875\n",
|
||
"67200: acc=0.6831547619047619\n",
|
||
"70400: acc=0.6917897727272727\n",
|
||
"73600: acc=0.6997146739130434\n",
|
||
"76800: acc=0.707109375\n",
|
||
"80000: acc=0.714075\n",
|
||
"83200: acc=0.7209134615384616\n",
|
||
"86400: acc=0.727037037037037\n",
|
||
"89600: acc=0.7326674107142858\n",
|
||
"92800: acc=0.7379633620689655\n",
|
||
"96000: acc=0.7433645833333333\n",
|
||
"99200: acc=0.7479032258064516\n",
|
||
"102400: acc=0.752119140625\n",
|
||
"105600: acc=0.7562405303030303\n",
|
||
"108800: acc=0.76015625\n",
|
||
"112000: acc=0.7641339285714286\n",
|
||
"115200: acc=0.7677777777777778\n",
|
||
"118400: acc=0.7711233108108108\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"(0.03487814127604167, 0.7728)"
|
||
]
|
||
},
|
||
"execution_count": 5,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"net = LSTMClassifier(vocab_size,64,32,len(classes)).to(device)\n",
|
||
"train_epoch(net,train_loader, lr=0.001)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Упакованные последовательности\n",
|
||
"\n",
|
||
"В нашем примере нам пришлось дополнить все последовательности в минибатче нулевыми векторами. Это приводит к некоторой потере памяти, но в случае с RNN более критично то, что создаются дополнительные ячейки RNN для дополненных элементов ввода. Эти ячейки участвуют в обучении, но не несут никакой важной входной информации. Было бы гораздо лучше обучать RNN только на фактической длине последовательности.\n",
|
||
"\n",
|
||
"Для этого в PyTorch введен специальный формат хранения дополненных последовательностей. Предположим, у нас есть дополненный входной минибатч, который выглядит следующим образом:\n",
|
||
"```\n",
|
||
"[[1,2,3,4,5],\n",
|
||
" [6,7,8,0,0],\n",
|
||
" [9,0,0,0,0]]\n",
|
||
"```\n",
|
||
"Здесь 0 обозначает дополненные значения, а вектор фактической длины входных последовательностей равен `[5,3,1]`.\n",
|
||
"\n",
|
||
"Чтобы эффективно обучать RNN с дополненными последовательностями, мы хотим начать обучение первой группы ячеек RNN с большим минибатчем (`[1,6,9]`), затем завершить обработку третьей последовательности и продолжить обучение с укороченными минибатчами (`[2,7]`, `[3,8]`) и так далее. Таким образом, упакованная последовательность представляется в виде одного вектора — в нашем случае `[1,6,9,2,7,3,8,4,5]` — и вектора длины (`[5,3,1]`), из которых мы легко можем восстановить исходный дополненный минибатч.\n",
|
||
"\n",
|
||
"Чтобы создать упакованную последовательность, мы можем использовать функцию `torch.nn.utils.rnn.pack_padded_sequence`. Все рекуррентные слои, включая RNN, LSTM и GRU, поддерживают упакованные последовательности в качестве входных данных и выдают упакованный выход, который можно декодировать с помощью `torch.nn.utils.rnn.pad_packed_sequence`.\n",
|
||
"\n",
|
||
"Чтобы иметь возможность создать упакованную последовательность, нам нужно передать вектор длины в сеть, и, следовательно, нам потребуется другая функция для подготовки минибатчей:\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def pad_length(b):\n",
|
||
" # build vectorized sequence\n",
|
||
" v = [encode(x[1]) for x in b]\n",
|
||
" # compute max length of a sequence in this minibatch and length sequence itself\n",
|
||
" len_seq = list(map(len,v))\n",
|
||
" l = max(len_seq)\n",
|
||
" return ( # tuple of three tensors - labels, padded features, length sequence\n",
|
||
" torch.LongTensor([t[0]-1 for t in b]),\n",
|
||
" torch.stack([torch.nn.functional.pad(torch.tensor(t),(0,l-len(t)),mode='constant',value=0) for t in v]),\n",
|
||
" torch.tensor(len_seq)\n",
|
||
" )\n",
|
||
"\n",
|
||
"train_loader_len = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=pad_length, shuffle=True)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"Фактическая сеть будет очень похожа на `LSTMClassifier`, описанный выше, но в методе `forward` будут передаваться как дополненный минибатч, так и вектор длин последовательностей. После вычисления эмбеддинга мы создаем упакованную последовательность, передаем ее в слой LSTM, а затем распаковываем результат обратно.\n",
|
||
"\n",
|
||
"> **Примечание**: На самом деле мы не используем распакованный результат `x`, так как в последующих вычислениях используется выход из скрытых слоев. Таким образом, распаковку можно полностью убрать из этого кода. Причина, по которой мы оставили ее здесь, заключается в том, чтобы вы могли легко модифицировать этот код, если вам потребуется использовать выходные данные сети в дальнейших вычислениях.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class LSTMPackClassifier(torch.nn.Module):\n",
|
||
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
|
||
" super().__init__()\n",
|
||
" self.hidden_dim = hidden_dim\n",
|
||
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
|
||
" self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n",
|
||
" self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n",
|
||
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
|
||
"\n",
|
||
" def forward(self, x, lengths):\n",
|
||
" batch_size = x.size(0)\n",
|
||
" x = self.embedding(x)\n",
|
||
" pad_x = torch.nn.utils.rnn.pack_padded_sequence(x,lengths,batch_first=True,enforce_sorted=False)\n",
|
||
" pad_x,(h,c) = self.rnn(pad_x)\n",
|
||
" x, _ = torch.nn.utils.rnn.pad_packed_sequence(pad_x,batch_first=True)\n",
|
||
" return self.fc(h[-1])"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": []
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"metadata": {
|
||
"scrolled": true
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"3200: acc=0.285625\n",
|
||
"6400: acc=0.33359375\n",
|
||
"9600: acc=0.3876041666666667\n",
|
||
"12800: acc=0.44078125\n",
|
||
"16000: acc=0.4825\n",
|
||
"19200: acc=0.5235416666666667\n",
|
||
"22400: acc=0.5559821428571429\n",
|
||
"25600: acc=0.58609375\n",
|
||
"28800: acc=0.6116666666666667\n",
|
||
"32000: acc=0.63340625\n",
|
||
"35200: acc=0.6525284090909091\n",
|
||
"38400: acc=0.668515625\n",
|
||
"41600: acc=0.6822596153846154\n",
|
||
"44800: acc=0.6948214285714286\n",
|
||
"48000: acc=0.7052708333333333\n",
|
||
"51200: acc=0.71521484375\n",
|
||
"54400: acc=0.7239889705882353\n",
|
||
"57600: acc=0.7315277777777778\n",
|
||
"60800: acc=0.7388486842105263\n",
|
||
"64000: acc=0.74571875\n",
|
||
"67200: acc=0.7518303571428572\n",
|
||
"70400: acc=0.7576988636363636\n",
|
||
"73600: acc=0.7628940217391305\n",
|
||
"76800: acc=0.7681510416666667\n",
|
||
"80000: acc=0.7728125\n",
|
||
"83200: acc=0.7772235576923077\n",
|
||
"86400: acc=0.7815393518518519\n",
|
||
"89600: acc=0.7857700892857142\n",
|
||
"92800: acc=0.7895043103448276\n",
|
||
"96000: acc=0.7930520833333333\n",
|
||
"99200: acc=0.7959072580645161\n",
|
||
"102400: acc=0.798994140625\n",
|
||
"105600: acc=0.802064393939394\n",
|
||
"108800: acc=0.8051378676470589\n",
|
||
"112000: acc=0.8077857142857143\n",
|
||
"115200: acc=0.8104600694444445\n",
|
||
"118400: acc=0.8128293918918919\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/plain": [
|
||
"(0.029785829671223958, 0.8138166666666666)"
|
||
]
|
||
},
|
||
"execution_count": 8,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"net = LSTMPackClassifier(vocab_size,64,32,len(classes)).to(device)\n",
|
||
"train_epoch_emb(net,train_loader_len, lr=0.001,use_pack_sequence=True)\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"> **Примечание:** Вы могли заметить параметр `use_pack_sequence`, который мы передаем в функцию обучения. В настоящее время функция `pack_padded_sequence` требует, чтобы тензор длины последовательности находился на устройстве CPU, и поэтому функция обучения должна избегать перемещения данных длины последовательности на GPU во время обучения. Вы можете ознакомиться с реализацией функции `train_emb` в файле [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Двунаправленные и многослойные RNN\n",
|
||
"\n",
|
||
"В наших примерах все рекуррентные сети работали в одном направлении — от начала последовательности к её концу. Это кажется естественным, так как напоминает способ, которым мы читаем или слушаем речь. Однако, поскольку во многих практических случаях у нас есть произвольный доступ к входной последовательности, имеет смысл выполнять рекуррентные вычисления в обоих направлениях. Такие сети называются **двунаправленными** RNN, и их можно создать, передав параметр `bidirectional=True` в конструктор RNN/LSTM/GRU.\n",
|
||
"\n",
|
||
"При работе с двунаправленной сетью нам понадобятся два вектора скрытых состояний — по одному для каждого направления. PyTorch кодирует эти векторы как один вектор в два раза большего размера, что довольно удобно, так как обычно вы передаёте полученное скрытое состояние в полносвязный линейный слой, и вам просто нужно учесть это увеличение размера при создании слоя.\n",
|
||
"\n",
|
||
"Рекуррентная сеть, будь то однонаправленная или двунаправленная, захватывает определённые паттерны внутри последовательности и может сохранять их во векторе состояния или передавать на выход. Как и в случае с сверточными сетями, мы можем построить ещё один рекуррентный слой поверх первого, чтобы захватывать паттерны более высокого уровня, сформированные из паттернов низкого уровня, извлечённых первым слоем. Это приводит нас к понятию **многослойной RNN**, которая состоит из двух или более рекуррентных сетей, где выход предыдущего слоя передаётся в следующий слой в качестве входа.\n",
|
||
"\n",
|
||
"\n",
|
||
"\n",
|
||
"*Изображение из [этой замечательной статьи](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) Фернандо Лопеса*\n",
|
||
"\n",
|
||
"PyTorch упрощает создание таких сетей, так как вам нужно всего лишь передать параметр `num_layers` в конструктор RNN/LSTM/GRU, чтобы автоматически построить несколько слоёв рекуррентности. Это также означает, что размер вектора скрытого состояния увеличится пропорционально, и вам нужно будет учитывать это при обработке выхода рекуррентных слоёв.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Рекуррентные нейронные сети для других задач\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": {
|
||
"interpreter": {
|
||
"hash": "16af2a8bbb083ea23e5e41c7f5787656b2ce26968575d8763f2c4b17f9cd711f"
|
||
},
|
||
"kernelspec": {
|
||
"display_name": "Python 3.8.12 ('py38')",
|
||
"language": "python",
|
||
"name": "python3"
|
||
},
|
||
"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.8.12"
|
||
},
|
||
"coopTranslator": {
|
||
"original_hash": "522ee52ae3d5ae933e283286254e9a55",
|
||
"translation_date": "2025-08-28T12:21:39+00:00",
|
||
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
|
||
"language_code": "ru"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
} |