479 lines
32 KiB
Plaintext
479 lines
32 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$ передаються через мережу, вона здатна навчатися послідовним залежностям між словами. Наприклад, коли слово *не* з'являється десь у послідовності, мережа може навчитися заперечувати певні елементи вектору стану, що призводить до заперечення.\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": [
|
||
"> **Примітка:** Тут ми використовуємо необучений шар вбудовування для простоти, але для ще кращих результатів можна використовувати попередньо навчені шари вбудовування, такі як Word2Vec або GloVe, як описано в попередньому розділі. Для кращого розуміння ви можете адаптувати цей код для роботи з попередньо навченими вбудовуваннями.\n",
|
||
"\n",
|
||
"У нашому випадку ми будемо використовувати завантажувач даних із заповненням (padded data loader), тому кожен пакет (batch) міститиме кілька послідовностей однакової довжини з доданими заповнювачами. Шар RNN прийматиме послідовність тензорів вбудовування і генеруватиме два виходи:\n",
|
||
"* $x$ — це послідовність виходів RNN-комірок на кожному кроці\n",
|
||
"* $h$ — це фінальний прихований стан для останнього елемента послідовності\n",
|
||
"\n",
|
||
"Потім ми застосовуємо повнозв’язний лінійний класифікатор, щоб отримати кількість класів.\n",
|
||
"\n",
|
||
"> **Примітка:** 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$ можна уявити як певні прапорці, які можна вмикати та вимикати. Наприклад, коли ми зустрічаємо ім'я *Аліса* в послідовності, ми можемо припустити, що це стосується жіночого персонажа, і встановити прапорець у стані, що в реченні є жіночий іменник. Коли ми далі зустрічаємо фразу *і Том*, ми встановимо прапорець, що в нас є множинний іменник. Таким чином, маніпулюючи станом, ми можемо, ймовірно, відстежувати граматичні властивості частин речення.\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": [
|
||
"## Рекурентні нейронні мережі (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": {
|
||
"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-30T10:31:24+00:00",
|
||
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
|
||
"language_code": "uk"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 2
|
||
} |