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

479 lines
24 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Rekurrente neurale netværk\n",
"\n",
"I det forrige modul har vi brugt rige semantiske repræsentationer af tekst og en simpel lineær klassifikator oven på embeddings. Denne arkitektur fanger den samlede betydning af ordene i en sætning, men den tager ikke højde for **rækkefølgen** af ordene, fordi aggregeringsoperationen oven på embeddings fjerner denne information fra den oprindelige tekst. Da disse modeller ikke kan modellere ords rækkefølge, kan de ikke løse mere komplekse eller tvetydige opgaver som tekstgenerering eller besvarelse af spørgsmål.\n",
"\n",
"For at fange betydningen af tekstsekvenser skal vi bruge en anden neural netværksarkitektur, som kaldes et **rekurrent neuralt netværk**, eller RNN. I RNN sender vi vores sætning gennem netværket én symbol ad gangen, og netværket producerer en **tilstand**, som vi derefter sender til netværket igen sammen med det næste symbol.\n",
"\n",
"Givet inputsekvensen af tokens $X_0,\\dots,X_n$, skaber RNN en sekvens af neurale netværksblokke og træner denne sekvens end-to-end ved hjælp af backpropagation. Hver netværksblok tager et par $(X_i,S_i)$ som input og producerer $S_{i+1}$ som resultat. Den endelige tilstand $S_n$ eller output $X_n$ sendes til en lineær klassifikator for at producere resultatet. Alle netværksblokke deler de samme vægte og trænes end-to-end ved hjælp af én backpropagation-pass.\n",
"\n",
"Fordi tilstandsvektorerne $S_0,\\dots,S_n$ sendes gennem netværket, er det i stand til at lære de sekventielle afhængigheder mellem ord. For eksempel, når ordet *ikke* optræder et sted i sekvensen, kan det lære at negere visse elementer inden for tilstandsvektoren, hvilket resulterer i negation.\n",
"\n",
"> Da vægtene for alle RNN-blokke på billedet er delt, kan det samme billede repræsenteres som én blok (til højre) med en rekurrent feedback-loop, som sender netværkets outputtilstand tilbage til input.\n",
"\n",
"Lad os se, hvordan rekurrente neurale netværk kan hjælpe os med at klassificere vores nyheds-datasæt.\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": [
"## Enkel RNN-klassifikator\n",
"\n",
"I tilfælde af en simpel RNN er hver rekurrent enhed et simpelt lineært netværk, som tager en sammenkædet inputvektor og tilstandsvektor og producerer en ny tilstandsvektor. PyTorch repræsenterer denne enhed med klassen `RNNCell`, og et netværk af sådanne celler - som laget `RNN`.\n",
"\n",
"For at definere en RNN-klassifikator vil vi først anvende et indlejringslag for at reducere dimensionaliteten af inputvokabularet, og derefter placere et RNN-lag ovenpå:\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": [
"> **Bemærk:** Vi bruger her et utrænet embedding-lag for enkelhedens skyld, men for endnu bedre resultater kan vi bruge et forudtrænet embedding-lag med Word2Vec- eller GloVe-embeddings, som beskrevet i den foregående enhed. For bedre forståelse kan du overveje at tilpasse denne kode til at arbejde med forudtrænede embeddings.\n",
"\n",
"I vores tilfælde vil vi bruge en polstret data-loader, så hver batch vil indeholde et antal polstrede sekvenser af samme længde. RNN-laget vil tage sekvensen af embedding-tensorer og producere to outputs:\n",
"* $x$ er en sekvens af RNN-cellens outputs ved hvert trin\n",
"* $h$ er den endelige skjulte tilstand for det sidste element i sekvensen\n",
"\n",
"Vi anvender derefter en fuldt forbundet lineær klassifikator for at få antallet af klasser.\n",
"\n",
"> **Bemærk:** RNN'er er ret svære at træne, fordi når RNN-cellerne udfoldes langs sekvensens længde, bliver antallet af lag, der er involveret i backpropagation, ret stort. Derfor skal vi vælge en lille læringsrate og træne netværket på et større datasæt for at opnå gode resultater. Det kan tage ret lang tid, så det anbefales at bruge 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": [
"## Long Short Term Memory (LSTM)\n",
"\n",
"Et af de største problemer med klassiske RNN'er er det såkaldte **vanishing gradients**-problem. Fordi RNN'er trænes end-to-end i én backpropagation-pass, har de svært ved at propagere fejl til de første lag i netværket, og derfor kan netværket ikke lære relationer mellem fjerne tokens. En af måderne at undgå dette problem på er at introducere **eksplicit tilstandsadministration** ved at bruge såkaldte **gates**. Der er to mest kendte arkitekturer af denne type: **Long Short Term Memory** (LSTM) og **Gated Relay Unit** (GRU).\n",
"\n",
"![Billede, der viser et eksempel på en long short term memory-celle](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"LSTM-netværket er organiseret på en måde, der ligner RNN, men der er to tilstande, der bliver sendt fra lag til lag: den faktiske tilstand $c$ og den skjulte vektor $h$. Ved hver enhed bliver den skjulte vektor $h_i$ sammenkædet med input $x_i$, og de styrer, hvad der sker med tilstanden $c$ via **gates**. Hver gate er et neuralt netværk med sigmoid-aktivering (output i intervallet $[0,1]$), som kan betragtes som en bitmaske, når den multipliceres med tilstandsvektoren. Der er følgende gates (fra venstre til højre på billedet ovenfor):\n",
"* **forget gate** tager den skjulte vektor og bestemmer, hvilke komponenter af vektoren $c$ vi skal glemme, og hvilke vi skal lade passere.\n",
"* **input gate** tager noget information fra input og den skjulte vektor og indsætter det i tilstanden.\n",
"* **output gate** transformerer tilstanden via et lineært lag med $\\tanh$-aktivering og vælger derefter nogle af dens komponenter ved hjælp af den skjulte vektor $h_i$ for at producere den nye tilstand $c_{i+1}$.\n",
"\n",
"Komponenterne i tilstanden $c$ kan betragtes som nogle flag, der kan tændes og slukkes. For eksempel, når vi støder på et navn som *Alice* i sekvensen, vil vi måske antage, at det refererer til en kvindelig karakter og hæve flaget i tilstanden, der indikerer, at vi har et kvindeligt substantiv i sætningen. Når vi senere støder på frasen *and Tom*, vil vi hæve flaget, der indikerer, at vi har et flertal substantiv. Ved at manipulere tilstanden kan vi således formodentlig holde styr på grammatiske egenskaber ved sætningsdele.\n",
"\n",
"> **Note**: En fremragende ressource til at forstå LSTM's interne funktioner er denne fantastiske artikel [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) af Christopher Olah.\n",
"\n",
"Selvom den interne struktur af en LSTM-celle kan virke kompleks, skjuler PyTorch denne implementering inde i `LSTMCell`-klassen og tilbyder `LSTM`-objektet til at repræsentere hele LSTM-laget. Implementeringen af en LSTM-klassifikator vil derfor være ret lig den simple RNN, som vi har set ovenfor:\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": [
"## Pakkede sekvenser\n",
"\n",
"I vores eksempel var vi nødt til at udfylde alle sekvenser i minibatchen med nulvektorer. Selvom det medfører noget spild af hukommelse, er det mere kritisk med RNN'er, at der oprettes ekstra RNN-celler for de udfyldte inputelementer, som deltager i træningen, men ikke indeholder nogen vigtig inputinformation. Det ville være meget bedre kun at træne RNN til den faktiske sekvensstørrelse.\n",
"\n",
"For at gøre dette introduceres et specielt format til opbevaring af udfyldte sekvenser i PyTorch. Antag, at vi har en udfyldt minibatch, der ser sådan ud:\n",
"```\n",
"[[1,2,3,4,5],\n",
" [6,7,8,0,0],\n",
" [9,0,0,0,0]]\n",
"```\n",
"Her repræsenterer 0 udfyldte værdier, og den faktiske længdevektor for inputsekvenserne er `[5,3,1]`.\n",
"\n",
"For effektivt at træne RNN med udfyldte sekvenser ønsker vi at starte træningen af den første gruppe af RNN-celler med en stor minibatch (`[1,6,9]`), men derefter afslutte behandlingen af den tredje sekvens og fortsætte træningen med kortere minibatches (`[2,7]`, `[3,8]`) osv. Således repræsenteres den pakkede sekvens som én vektor - i vores tilfælde `[1,6,9,2,7,3,8,4,5]`, og længdevektoren (`[5,3,1]`), hvorfra vi nemt kan rekonstruere den oprindelige udfyldte minibatch.\n",
"\n",
"For at producere en pakket sekvens kan vi bruge funktionen `torch.nn.utils.rnn.pack_padded_sequence`. Alle rekurrente lag, inklusive RNN, LSTM og GRU, understøtter pakkede sekvenser som input og producerer pakket output, som kan dekodes ved hjælp af `torch.nn.utils.rnn.pad_packed_sequence`.\n",
"\n",
"For at kunne producere en pakket sekvens skal vi give længdevektoren til netværket, og derfor har vi brug for en anden funktion til at forberede minibatches:\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": [
"Den faktiske netværk vil være meget lig `LSTMClassifier` ovenfor, men `forward`-passet vil modtage både den polstrede minibatch og vektoren af sekvenslængder. Efter at have beregnet embedding, beregner vi den pakkede sekvens, sender den til LSTM-laget og pakker derefter resultatet ud igen.\n",
"\n",
"> **Note**: Vi bruger faktisk ikke det udpakkede resultat `x`, fordi vi bruger output fra de skjulte lag i de følgende beregninger. Derfor kan vi fjerne udpakkingen helt fra denne kode. Grunden til, at vi placerer det her, er for at gøre det nemt for dig at ændre denne kode, hvis du skulle få brug for at bruge netværkets output i yderligere beregninger.\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": [
"> **Bemærk:** Du har måske bemærket parameteren `use_pack_sequence`, som vi sender til træningsfunktionen. I øjeblikket kræver funktionen `pack_padded_sequence`, at længdesekvenstensoren er på CPU-enheden, og derfor skal træningsfunktionen undgå at flytte længdesekvensdataene til GPU under træning. Du kan se implementeringen af funktionen `train_emb` i filen [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py).\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Bidirektionelle og flerlags RNN'er\n",
"\n",
"I vores eksempler har alle rekurrente netværk opereret i én retning, fra starten af en sekvens til slutningen. Det virker naturligt, fordi det minder om den måde, vi læser og lytter til tale. Men i mange praktiske tilfælde har vi tilfældig adgang til inputsekvensen, og det kan derfor give mening at udføre rekurrent beregning i begge retninger. Sådanne netværk kaldes **bidirektionelle** RNN'er, og de kan oprettes ved at angive parameteren `bidirectional=True` til RNN/LSTM/GRU-konstruktøren.\n",
"\n",
"Når vi arbejder med et bidirektionelt netværk, har vi brug for to skjulte tilstandsvektorer, én for hver retning. PyTorch koder disse vektorer som én vektor med dobbelt så stor størrelse, hvilket er ret praktisk, da man normalt vil sende den resulterende skjulte tilstand til et fuldt forbundet lineært lag. Man skal blot tage denne forøgelse i størrelse i betragtning, når man opretter laget.\n",
"\n",
"Et rekurrent netværk, enten én-retnings eller bidirektionelt, fanger visse mønstre inden for en sekvens og kan gemme dem i en tilstandsvektor eller sende dem videre til output. Ligesom med konvolutionelle netværk kan vi bygge et andet rekurrent lag oven på det første for at fange mønstre på et højere niveau, bygget fra lav-niveau mønstre, som det første lag har udtrukket. Dette fører os til begrebet **flerlags RNN**, som består af to eller flere rekurrente netværk, hvor output fra det foregående lag sendes til det næste lag som input.\n",
"\n",
"![Billede der viser en flerlags lang-kort-tids-hukommelses-RNN](../../../../../translated_images/multi-layer-lstm.dd975e29bb2a59fe58b429db833932d734c81f211cad2783797a9608984acb8c.da.jpg)\n",
"\n",
"*Billede fra [denne fantastiske artikel](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) af Fernando López*\n",
"\n",
"PyTorch gør det nemt at konstruere sådanne netværk, da man blot skal angive parameteren `num_layers` til RNN/LSTM/GRU-konstruktøren for automatisk at bygge flere lag af rekurrence. Dette betyder også, at størrelsen på den skjulte/tilstandsvektor vil stige proportionalt, og man skal tage dette i betragtning, når man håndterer output fra de rekurrente lag.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNN'er til andre opgaver\n",
"\n",
"I denne enhed har vi set, at RNN'er kan bruges til sekvensklassifikation, men faktisk kan de håndtere mange flere opgaver, såsom tekstgenerering, maskinoversættelse og mere. Vi vil se nærmere på disse opgaver i den næste enhed.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Ansvarsfraskrivelse**: \nDette dokument er blevet oversat ved hjælp af AI-oversættelsestjenesten [Co-op Translator](https://github.com/Azure/co-op-translator). Selvom vi bestræber os på nøjagtighed, skal du være opmærksom på, at automatiserede oversættelser kan indeholde fejl eller unøjagtigheder. Det originale dokument på dets oprindelige sprog bør betragtes som den autoritative kilde. For kritisk information anbefales professionel menneskelig oversættelse. Vi er ikke ansvarlige for eventuelle misforståelser eller fejltolkninger, der opstår som følge af brugen af denne oversættelse.\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-28T17:44:18+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
"language_code": "da"
}
},
"nbformat": 4,
"nbformat_minor": 2
}