AI-For-Beginners/translations/bg/lessons/5-NLP/16-RNN/RNNPyTorch.ipynb

479 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",
"Дадена входна последователност от токени $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": [
"> **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",
"![Изображение, показващо пример за клетка с дългосрочна краткосрочна памет](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\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",
"![Изображение, показващо многослойна дългосрочно-краткосрочна памет RNN](../../../../../translated_images/bg/multi-layer-lstm.dd975e29bb2a59fe.webp)\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": [
"## RNNs за други задачи\n",
"\n",
"В този раздел видяхме, че RNNs могат да се използват за класификация на последователности, но всъщност те могат да се справят с много повече задачи, като генериране на текст, машинен превод и други. Ще разгледаме тези задачи в следващия раздел.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Отказ от отговорност**: \nТози документ е преведен с помощта на AI услуга за превод [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-30T01:00:20+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
"language_code": "bg"
}
},
"nbformat": 4,
"nbformat_minor": 2
}