{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Rekurentne nevronske mreže\n", "\n", "V prejšnjem modulu smo uporabljali bogate semantične reprezentacije besedila in preprost linearni klasifikator na vrhu vgrajenih predstavitev. Ta arhitektura zajame združeni pomen besed v stavku, vendar ne upošteva **vrstnega reda** besed, saj operacija združevanja na vrhu vgrajenih predstavitev odstrani to informacijo iz izvirnega besedila. Ker ti modeli ne morejo modelirati vrstnega reda besed, ne morejo reševati bolj zapletenih ali dvoumnih nalog, kot so generiranje besedila ali odgovarjanje na vprašanja.\n", "\n", "Da bi zajeli pomen zaporedja besedila, moramo uporabiti drugo arhitekturo nevronske mreže, imenovano **rekurentna nevronska mreža** ali RNN. Pri RNN stavke pošiljamo skozi mrežo en simbol naenkrat, mreža pa ustvari neko **stanje**, ki ga nato skupaj z naslednjim simbolom ponovno pošljemo v mrežo.\n", "\n", "Glede na vhodno zaporedje tokenov $X_0,\\dots,X_n$, RNN ustvari zaporedje blokov nevronske mreže in to zaporedje trenira od začetka do konca z uporabo povratnega razširjanja napake. Vsak blok mreže sprejme par $(X_i,S_i)$ kot vhod in ustvari $S_{i+1}$ kot rezultat. Končno stanje $S_n$ ali izhod $X_n$ gre v linearni klasifikator, da ustvari rezultat. Vsi bloki mreže imajo enake uteži in se trenirajo od začetka do konca z enim prehodom povratnega razširjanja napake.\n", "\n", "Ker se vektorska stanja $S_0,\\dots,S_n$ prenašajo skozi mrežo, lahko ta model uči zaporedne odvisnosti med besedami. Na primer, ko se beseda *ne* pojavi nekje v zaporedju, se lahko nauči negirati določene elemente znotraj vektorskega stanja, kar vodi do negacije.\n", "\n", "> Ker so uteži vseh blokov RNN na sliki enake, lahko isto sliko predstavimo kot en blok (na desni) z povratno zanko, ki prenaša izhodno stanje mreže nazaj na vhod.\n", "\n", "Poglejmo, kako nam lahko rekurentne nevronske mreže pomagajo pri klasifikaciji našega nabora novic.\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": [ "## Preprost RNN klasifikator\n", "\n", "V primeru preprostega RNN je vsaka rekurentna enota preprosto linearno omrežje, ki sprejme združen vhodni vektor in vektorsko stanje ter ustvari novo vektorsko stanje. PyTorch predstavlja to enoto z razredom `RNNCell`, omrežje takšnih celic pa kot plast `RNN`.\n", "\n", "Za definiranje RNN klasifikatorja bomo najprej uporabili vgradno plast (embedding layer), da zmanjšamo dimenzionalnost vhodnega besedišča, nato pa bomo na vrhu dodali plast 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": [ "> **Opomba:** Tukaj uporabljamo neizurjeno vgrajeno plast za enostavnost, vendar lahko za še boljše rezultate uporabimo vnaprej izurjeno vgrajeno plast z Word2Vec ali GloVe vgraditvami, kot je opisano v prejšnji enoti. Za boljše razumevanje lahko ta koda prilagodite tako, da deluje z vnaprej izurjenimi vgraditvami.\n", "\n", "V našem primeru bomo uporabili podajalnik podatkov z izravnavo (padded data loader), tako da bo vsak paket vseboval več izravnanih zaporedij enake dolžine. RNN plast bo vzela zaporedje vgraditvenih tenzorjev in ustvarila dva izhoda:\n", "* $x$ je zaporedje izhodov RNN celic na vsakem koraku\n", "* $h$ je končno skrito stanje za zadnji element zaporedja\n", "\n", "Nato uporabimo popolnoma povezani linearni klasifikator, da dobimo število razredov.\n", "\n", "> **Opomba:** RNN-ji so precej zahtevni za učenje, saj je število plasti, vključenih v povratno propagacijo, precej veliko, ko so RNN celice razširjene vzdolž dolžine zaporedja. Zato moramo izbrati majhno hitrost učenja in mrežo učiti na večjem naboru podatkov, da dosežemo dobre rezultate. To lahko traja precej dolgo, zato je priporočljiva uporaba 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": [ "## Dolgoročni kratkoročni spomin (LSTM)\n", "\n", "Ena glavnih težav klasičnih RNN-jev je tako imenovana težava **izginjajočih gradientov**. Ker se RNN-ji učijo od začetka do konca v enem prehodu z vzvratnim razširjanjem, imajo težave s prenašanjem napake do prvih slojev mreže, zaradi česar mreža ne more učiti odnosov med oddaljenimi tokeni. Eden od načinov za izogibanje tej težavi je uvedba **eksplicitnega upravljanja stanja** z uporabo tako imenovanih **vrat**. Dve najbolj znani arhitekturi te vrste sta: **Dolgoročni kratkoročni spomin** (LSTM) in **Enota z vrati za posredovanje** (GRU).\n", "\n", "![Slika, ki prikazuje primer celice dolgoročnega kratkoročnega spomina](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "LSTM mreža je organizirana na način, podoben RNN-ju, vendar se iz sloja v sloj prenašata dve stanji: dejansko stanje $c$ in skriti vektor $h$. Pri vsaki enoti se skriti vektor $h_i$ združi z vhodom $x_i$, in skupaj nadzorujeta, kaj se zgodi s stanjem $c$ prek **vrat**. Vsaka vrata so nevronska mreža s sigmoidno aktivacijo (izhod v območju $[0,1]$), ki jih lahko razumemo kot bitno masko, ko jih pomnožimo z vektorskim stanjem. Obstajajo naslednja vrata (od leve proti desni na zgornji sliki):\n", "* **vrata za pozabo** vzamejo skriti vektor in določijo, katere komponente vektorja $c$ moramo pozabiti in katere prenesti naprej.\n", "* **vhodna vrata** vzamejo nekaj informacij iz vhoda in skritega vektorja ter jih vstavijo v stanje.\n", "* **izhodna vrata** transformirajo stanje prek neke linearne plasti s $\\tanh$ aktivacijo, nato pa izberejo nekatere njegove komponente z uporabo skritega vektorja $h_i$, da ustvarijo novo stanje $c_{i+1}$.\n", "\n", "Komponente stanja $c$ lahko razumemo kot neke vrste zastavice, ki jih lahko vklopimo ali izklopimo. Na primer, ko v zaporedju naletimo na ime *Alice*, lahko predpostavimo, da se nanaša na ženski lik, in dvignemo zastavico v stanju, da imamo v stavku ženski samostalnik. Ko kasneje naletimo na frazo *in Tom*, bomo dvignili zastavico, da imamo množinski samostalnik. Tako lahko z manipulacijo stanja domnevno sledimo slovničnim lastnostim delov stavka.\n", "\n", "> **Opomba**: Odličen vir za razumevanje notranje strukture LSTM je ta odlični članek [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) avtorja Christopherja Olaha.\n", "\n", "Čeprav se notranja struktura LSTM celice morda zdi zapletena, PyTorch to implementacijo skriva znotraj razreda `LSTMCell` in ponuja objekt `LSTM` za predstavitev celotnega LSTM sloja. Tako bo implementacija LSTM klasifikatorja precej podobna preprostemu RNN-ju, ki smo ga videli zgoraj:\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": [ "## Zapakirane sekvence\n", "\n", "V našem primeru smo morali vse sekvence v mini seriji zapolniti z ničelnimi vektorji. Čeprav to povzroči nekaj nepotrebne porabe pomnilnika, je pri RNN-jih še bolj kritično, da se ustvarijo dodatne RNN celice za zapolnjene vnose, ki sodelujejo pri učenju, vendar ne nosijo nobenih pomembnih vhodnih informacij. Veliko bolje bi bilo, če bi RNN trenirali le do dejanske dolžine sekvence.\n", "\n", "Za to je v PyTorch uveden poseben format za shranjevanje zapolnjenih sekvenc. Predpostavimo, da imamo vhodno zapolnjeno mini serijo, ki izgleda takole:\n", "```\n", "[[1,2,3,4,5],\n", " [6,7,8,0,0],\n", " [9,0,0,0,0]]\n", "```\n", "Tukaj 0 predstavlja zapolnjene vrednosti, dejanski vektor dolžin vhodnih sekvenc pa je `[5,3,1]`.\n", "\n", "Da bi učinkovito trenirali RNN z zapolnjenimi sekvencami, želimo začeti treniranje prve skupine RNN celic z veliko mini serijo (`[1,6,9]`), nato pa zaključiti obdelavo tretje sekvence in nadaljevati treniranje s krajšimi mini serijami (`[2,7]`, `[3,8]`) in tako naprej. Tako je zapakirana sekvenca predstavljena kot en vektor - v našem primeru `[1,6,9,2,7,3,8,4,5]`, in vektor dolžin (`[5,3,1]`), iz katerega lahko enostavno rekonstruiramo izvirno zapolnjeno mini serijo.\n", "\n", "Za ustvarjanje zapakirane sekvence lahko uporabimo funkcijo `torch.nn.utils.rnn.pack_padded_sequence`. Vse rekurentne plasti, vključno z RNN, LSTM in GRU, podpirajo zapakirane sekvence kot vhod in proizvajajo zapakiran izhod, ki ga lahko dekodiramo z uporabo `torch.nn.utils.rnn.pad_packed_sequence`.\n", "\n", "Da bi lahko ustvarili zapakirano sekvenco, moramo omrežju posredovati vektor dolžin, zato potrebujemo drugačno funkcijo za pripravo mini serij:\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": [ "Dejansko bo mreža zelo podobna `LSTMClassifier` zgoraj, vendar bo metoda `forward` prejela tako zapolnjen mini-sklop kot tudi vektor dolžin zaporedij. Po izračunu vdelave izračunamo zapakiran niz, ga posredujemo sloju LSTM in nato rezultat ponovno razpakiramo.\n", "\n", "> **Opomba**: Pravzaprav razpakiranega rezultata `x` ne uporabljamo, saj v nadaljnjih izračunih uporabljamo izhod iz skritih slojev. Zato lahko razpakiranje v tej kodi popolnoma odstranimo. Razlog, da ga tukaj vključimo, je, da vam omogočimo enostavno spreminjanje te kode, če bi morali uporabiti izhod mreže v nadaljnjih izračunih.\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": [ "> **Opomba:** Morda ste opazili parameter `use_pack_sequence`, ki ga posredujemo funkciji za učenje. Trenutno funkcija `pack_padded_sequence` zahteva, da je dolžinski zaporedni tenzor na napravi CPU, zato mora funkcija za učenje preprečiti prenos podatkov o dolžinskem zaporedju na GPU med učenjem. Implementacijo funkcije `train_emb` si lahko ogledate v datoteki [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Dvosmerne in večplastne RNN\n", "\n", "V naših primerih so vse rekurzivne mreže delovale v eni smeri, od začetka zaporedja do konca. To se zdi naravno, saj spominja na način, kako beremo in poslušamo govor. Vendar pa, ker imamo v mnogih praktičnih primerih naključni dostop do vhodnega zaporedja, bi bilo smiselno izvajati rekurzivno računanje v obeh smereh. Takšne mreže imenujemo **dvosmerne** RNN, in jih lahko ustvarimo z dodajanjem parametra `bidirectional=True` konstruktorju RNN/LSTM/GRU.\n", "\n", "Pri delu z dvosmerno mrežo potrebujemo dva vektorja skritega stanja, enega za vsako smer. PyTorch te vektorje kodira kot en vektor z dvakrat večjo velikostjo, kar je precej priročno, saj običajno posredujemo nastalo skrito stanje v popolnoma povezano linearno plast, pri čemer moramo le upoštevati to povečanje velikosti pri ustvarjanju plasti.\n", "\n", "Rekurzivna mreža, enosmerna ali dvosmerna, zajame določene vzorce znotraj zaporedja in jih lahko shrani v vektor stanja ali prenese v izhod. Tako kot pri konvolucijskih mrežah lahko na prvo plast zgradimo drugo rekurzivno plast, da zajamemo vzorce višje ravni, ki so zgrajeni iz vzorcev nižje ravni, ki jih je izločila prva plast. To nas pripelje do pojma **večplastne RNN**, ki je sestavljena iz dveh ali več rekurzivnih mrež, kjer se izhod prejšnje plasti prenese v naslednjo plast kot vhod.\n", "\n", "![Slika, ki prikazuje večplastno dolgotrajno-kratkoročno pomnilniško RNN](../../../../../translated_images/sl/multi-layer-lstm.dd975e29bb2a59fe.webp)\n", "\n", "*Slika iz [tega čudovitega prispevka](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) avtorja Fernanda Lópeza*\n", "\n", "PyTorch omogoča enostavno konstrukcijo takšnih mrež, saj morate le dodati parameter `num_layers` konstruktorju RNN/LSTM/GRU, da samodejno zgradite več plasti rekurzije. To pa tudi pomeni, da se velikost skritega/stanja vektorja sorazmerno poveča, kar morate upoštevati pri obdelavi izhoda rekurzivnih plasti.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNN-ji za druge naloge\n", "\n", "V tej enoti smo videli, da se RNN-ji lahko uporabljajo za klasifikacijo sekvenc, vendar dejansko lahko obravnavajo še veliko več nalog, kot so generiranje besedila, strojno prevajanje in druge. Te naloge bomo obravnavali v naslednji enoti.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Omejitev odgovornosti**: \nTa dokument je bil preveden z uporabo storitve za strojno prevajanje [Co-op Translator](https://github.com/Azure/co-op-translator). Čeprav si prizadevamo za natančnost, vas prosimo, da upoštevate, da lahko avtomatizirani prevodi vsebujejo napake ali netočnosti. Izvirni dokument v njegovem izvirnem jeziku je treba obravnavati kot avtoritativni vir. Za ključne informacije priporočamo strokovno človeško prevajanje. Ne prevzemamo odgovornosti za morebitna nesporazumevanja ali napačne razlage, ki izhajajo iz uporabe tega prevoda.\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-30T08:11:38+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb", "language_code": "sl" } }, "nbformat": 4, "nbformat_minor": 2 }