{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Rețele neuronale recurente\n", "\n", "În modulul anterior, am utilizat reprezentări semantice bogate ale textului și un clasificator liniar simplu deasupra acestor încorporări. Ceea ce face această arhitectură este să capteze semnificația agregată a cuvintelor dintr-o propoziție, dar nu ia în considerare **ordinea** cuvintelor, deoarece operația de agregare aplicată încorporărilor elimină această informație din textul original. Deoarece aceste modele nu pot modela ordonarea cuvintelor, ele nu pot rezolva sarcini mai complexe sau ambigue, cum ar fi generarea de text sau răspunsul la întrebări.\n", "\n", "Pentru a capta semnificația unei secvențe de text, trebuie să utilizăm o altă arhitectură de rețea neuronală, numită **rețea neuronală recurentă**, sau RNN. În RNN, trecem propoziția prin rețea, simbol cu simbol, iar rețeaua produce un anumit **stat**, pe care îl trecem din nou prin rețea împreună cu următorul simbol.\n", "\n", "Dată fiind secvența de tokeni $X_0,\\dots,X_n$, RNN creează o secvență de blocuri de rețea neuronală și antrenează această secvență cap-coadă folosind propagarea înapoi. Fiecare bloc de rețea primește o pereche $(X_i,S_i)$ ca intrare și produce $S_{i+1}$ ca rezultat. Starea finală $S_n$ sau ieșirea $X_n$ este transmisă unui clasificator liniar pentru a produce rezultatul. Toate blocurile de rețea împărtășesc aceleași greutăți și sunt antrenate cap-coadă folosind o singură trecere de propagare înapoi.\n", "\n", "Deoarece vectorii de stare $S_0,\\dots,S_n$ sunt trecuți prin rețea, aceasta este capabilă să învețe dependențele secvențiale dintre cuvinte. De exemplu, atunci când cuvântul *not* apare undeva în secvență, rețeaua poate învăța să nege anumite elemente din vectorul de stare, rezultând o negare.\n", "\n", "> Deoarece greutățile tuturor blocurilor RNN din imagine sunt împărtășite, aceeași imagine poate fi reprezentată ca un singur bloc (în dreapta) cu o buclă de feedback recurent, care transmite starea de ieșire a rețelei înapoi la intrare.\n", "\n", "Să vedem cum rețelele neuronale recurente ne pot ajuta să clasificăm setul nostru de date de știri.\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": [ "## Clasificator RNN simplu\n", "\n", "În cazul unui RNN simplu, fiecare unitate recurentă este o rețea liniară simplă, care primește un vector de intrare concatenat și un vector de stare, și produce un nou vector de stare. PyTorch reprezintă această unitate prin clasa `RNNCell`, iar o rețea formată din astfel de celule - prin stratul `RNN`.\n", "\n", "Pentru a defini un clasificator RNN, vom aplica mai întâi un strat de încorporare pentru a reduce dimensiunea vocabularului de intrare, iar apoi vom adăuga un strat RNN deasupra acestuia:\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:** Folosim un strat de încorporare neantrenat aici pentru simplitate, dar pentru rezultate și mai bune putem folosi un strat de încorporare pre-antrenat cu încorporări Word2Vec sau GloVe, așa cum este descris în unitatea anterioară. Pentru o înțelegere mai bună, s-ar putea să doriți să adaptați acest cod pentru a funcționa cu încorporări pre-antrenate.\n", "\n", "În cazul nostru, vom folosi un loader de date cu secvențe completate, astfel încât fiecare lot va avea un număr de secvențe completate de aceeași lungime. Stratul RNN va lua secvența de tensori de încorporare și va produce două ieșiri:\n", "* $x$ este o secvență de ieșiri ale celulelor RNN la fiecare pas\n", "* $h$ este starea ascunsă finală pentru ultimul element al secvenței\n", "\n", "Aplicăm apoi un clasificator liniar complet conectat pentru a obține numărul de clase.\n", "\n", "> **Note:** RNN-urile sunt destul de dificile de antrenat, deoarece odată ce celulele RNN sunt desfășurate de-a lungul lungimii secvenței, numărul rezultat de straturi implicate în propagarea înapoi este destul de mare. Astfel, trebuie să selectăm o rată de învățare mică și să antrenăm rețeaua pe un set de date mai mare pentru a obține rezultate bune. Poate dura destul de mult timp, așa că utilizarea GPU-ului este preferată.\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": [ "## Memorie pe Termen Lung și Scurt (LSTM)\n", "\n", "Una dintre principalele probleme ale RNN-urilor clasice este așa-numita problemă a **gradientului care dispare**. Deoarece RNN-urile sunt antrenate cap-coadă într-o singură trecere de back-propagation, acestea întâmpină dificultăți în propagarea erorii către primele straturi ale rețelei, iar astfel rețeaua nu poate învăța relațiile dintre tokenii distanți. Una dintre modalitățile de a evita această problemă este introducerea **gestionării explicite a stării** prin utilizarea așa-numitelor **porți**. Există două arhitecturi bine cunoscute de acest tip: **Memorie pe Termen Lung și Scurt** (LSTM) și **Unitatea de Releu cu Porți** (GRU).\n", "\n", "![Imagine care arată un exemplu de celulă LSTM](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "Rețeaua LSTM este organizată într-un mod similar cu RNN, dar există două stări care sunt transmise de la un strat la altul: starea actuală $c$ și vectorul ascuns $h$. La fiecare unitate, vectorul ascuns $h_i$ este concatenat cu intrarea $x_i$, iar acestea controlează ce se întâmplă cu starea $c$ prin intermediul **porților**. Fiecare poartă este o rețea neuronală cu activare sigmoidă (ieșire în intervalul $[0,1]$), care poate fi considerată ca o mască bit cu bit atunci când este înmulțită cu vectorul de stare. Există următoarele porți (de la stânga la dreapta în imaginea de mai sus):\n", "* **Poarta de uitare** preia vectorul ascuns și determină ce componente ale vectorului $c$ trebuie să uităm și care să fie transmise mai departe.\n", "* **Poarta de intrare** preia informații din intrare și din vectorul ascuns și le inserează în stare.\n", "* **Poarta de ieșire** transformă starea printr-un strat liniar cu activare $\\tanh$, apoi selectează unele dintre componentele sale folosind vectorul ascuns $h_i$ pentru a produce noua stare $c_{i+1}$.\n", "\n", "Componentele stării $c$ pot fi considerate ca niște semnale care pot fi activate sau dezactivate. De exemplu, când întâlnim un nume precum *Alice* într-o secvență, putem presupune că se referă la un personaj feminin și putem activa semnalul în stare care indică faptul că avem un substantiv feminin în propoziție. Când întâlnim ulterior expresia *și Tom*, vom activa semnalul care indică faptul că avem un substantiv la plural. Astfel, prin manipularea stării, putem, teoretic, să urmărim proprietățile gramaticale ale părților propoziției.\n", "\n", "> **Note**: O resursă excelentă pentru a înțelege detaliile interne ale LSTM este acest articol minunat [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) de Christopher Olah.\n", "\n", "Deși structura internă a celulei LSTM poate părea complexă, PyTorch ascunde această implementare în clasa `LSTMCell` și oferă obiectul `LSTM` pentru a reprezenta întregul strat LSTM. Astfel, implementarea unui clasificator LSTM va fi destul de similară cu cea a unui RNN simplu pe care l-am văzut mai sus:\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": [ "## Secvențe împachetate\n", "\n", "În exemplul nostru, a trebuit să completăm toate secvențele din minibatch cu vectori de zero. Deși acest lucru duce la o risipă de memorie, în cazul RNN-urilor este mai critic faptul că se creează celule RNN suplimentare pentru elementele de intrare completate, care participă la antrenament, dar nu conțin informații importante. Ar fi mult mai bine să antrenăm RNN doar pentru dimensiunea reală a secvenței.\n", "\n", "Pentru a face acest lucru, PyTorch introduce un format special de stocare a secvențelor completate. Să presupunem că avem un minibatch de intrare completat care arată astfel:\n", "```\n", "[[1,2,3,4,5],\n", " [6,7,8,0,0],\n", " [9,0,0,0,0]]\n", "```\n", "Aici, 0 reprezintă valorile completate, iar vectorul de lungime reală al secvențelor de intrare este `[5,3,1]`.\n", "\n", "Pentru a antrena eficient RNN cu secvențe completate, dorim să începem antrenamentul primului grup de celule RNN cu un minibatch mare (`[1,6,9]`), dar apoi să terminăm procesarea celei de-a treia secvențe și să continuăm antrenamentul cu minibatch-uri mai mici (`[2,7]`, `[3,8]`) și așa mai departe. Astfel, secvența împachetată este reprezentată ca un singur vector - în cazul nostru `[1,6,9,2,7,3,8,4,5]`, și un vector de lungime (`[5,3,1]`), din care putem reconstrui cu ușurință minibatch-ul completat original.\n", "\n", "Pentru a produce o secvență împachetată, putem folosi funcția `torch.nn.utils.rnn.pack_padded_sequence`. Toate straturile recurente, inclusiv RNN, LSTM și GRU, acceptă secvențe împachetate ca intrare și produc o ieșire împachetată, care poate fi decodificată folosind `torch.nn.utils.rnn.pad_packed_sequence`.\n", "\n", "Pentru a putea produce o secvență împachetată, trebuie să transmitem vectorul de lungime rețelei, și astfel avem nevoie de o funcție diferită pentru a pregăti minibatch-urile:\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": [ "Rețeaua efectivă ar fi foarte similară cu `LSTMClassifier` de mai sus, dar trecerea `forward` va primi atât minibatch-ul umplut, cât și vectorul lungimilor secvențelor. După ce calculăm embedding-ul, calculăm secvența împachetată, o trecem prin stratul LSTM și apoi despachetăm rezultatul înapoi.\n", "\n", "> **Notă**: De fapt, nu folosim rezultatul despachetat `x`, deoarece utilizăm ieșirea din straturile ascunse în calculele următoare. Astfel, putem elimina complet despachetarea din acest cod. Motivul pentru care o plasăm aici este pentru a vă permite să modificați acest cod cu ușurință, în cazul în care aveți nevoie să utilizați ieșirea rețelei în calcule ulterioare.\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": [ "> **Notă:** Este posibil să fi observat parametrul `use_pack_sequence` pe care îl transmitem funcției de antrenare. În prezent, funcția `pack_padded_sequence` necesită ca tensorul de lungime a secvenței să fie pe dispozitivul CPU, și astfel funcția de antrenare trebuie să evite mutarea datelor de lungime a secvenței pe GPU în timpul antrenării. Puteți analiza implementarea funcției `train_emb` în fișierul [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNN-uri bidirecționale și multilayer\n", "\n", "În exemplele noastre, toate rețelele recurente au funcționat într-o singură direcție, de la începutul unei secvențe până la sfârșit. Pare natural, deoarece seamănă cu modul în care citim și ascultăm vorbirea. Totuși, în multe cazuri practice, avem acces aleatoriu la secvența de intrare, așa că ar putea avea sens să efectuăm calculul recurent în ambele direcții. Astfel de rețele se numesc **RNN-uri bidirecționale**, și pot fi create prin transmiterea parametrului `bidirectional=True` către constructorul RNN/LSTM/GRU.\n", "\n", "Când lucrăm cu o rețea bidirecțională, vom avea nevoie de două vectori de stare ascunsă, câte unul pentru fiecare direcție. PyTorch codifică acești vectori ca un singur vector de dimensiune dublă, ceea ce este destul de convenabil, deoarece, de obicei, vectorul de stare rezultat este transmis unui strat liniar complet conectat, și trebuie doar să ținem cont de această creștere în dimensiune atunci când creăm stratul.\n", "\n", "O rețea recurentă, fie unidirecțională, fie bidirecțională, captează anumite modele dintr-o secvență și le poate stoca în vectorul de stare sau le poate transmite în ieșire. La fel ca în cazul rețelelor convoluționale, putem construi un alt strat recurent deasupra primului pentru a capta modele de nivel superior, construite din modelele de nivel inferior extrase de primul strat. Acest lucru ne conduce la conceptul de **RNN multilayer**, care constă din două sau mai multe rețele recurente, unde ieșirea stratului anterior este transmisă stratului următor ca intrare.\n", "\n", "![Imagine care arată un RNN multilayer cu memorie pe termen lung și scurt](../../../../../translated_images/ro/multi-layer-lstm.dd975e29bb2a59fe.webp)\n", "\n", "*Imagine din [această postare minunată](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) de Fernando López*\n", "\n", "PyTorch face construirea unor astfel de rețele o sarcină ușoară, deoarece trebuie doar să transmiteți parametrul `num_layers` către constructorul RNN/LSTM/GRU pentru a construi automat mai multe straturi de recurență. Acest lucru înseamnă, de asemenea, că dimensiunea vectorului de stare ascunsă va crește proporțional, și va trebui să țineți cont de acest aspect atunci când gestionați ieșirea straturilor recurente.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNN-uri pentru alte sarcini\n", "\n", "În această unitate, am văzut că RNN-urile pot fi utilizate pentru clasificarea secvențelor, dar, de fapt, ele pot gestiona multe alte sarcini, cum ar fi generarea de text, traducerea automată și altele. Vom analiza aceste sarcini în unitatea următoare.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Declinare de responsabilitate**: \nAcest document a fost tradus folosind serviciul de traducere AI [Co-op Translator](https://github.com/Azure/co-op-translator). Deși ne străduim să asigurăm acuratețea, vă rugăm să rețineți că traducerile automate pot conține erori sau inexactități. Documentul original în limba sa maternă ar trebui considerat sursa autoritară. Pentru informații critice, se recomandă traducerea profesională realizată de un specialist uman. Nu ne asumăm responsabilitatea pentru eventualele neînțelegeri sau interpretări greșite care pot apărea din utilizarea acestei traduceri.\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-30T00:59:23+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb", "language_code": "ro" } }, "nbformat": 4, "nbformat_minor": 2 }