{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Rekurrente nevrale nettverk\n", "\n", "I det forrige modulen har vi brukt rike semantiske representasjoner av tekst, og en enkel lineær klassifikator på toppen av embeddingene. Det denne arkitekturen gjør, er å fange opp den aggregerte meningen av ordene i en setning, men den tar ikke hensyn til **rekkefølgen** av ordene, fordi aggregeringsoperasjonen på embeddingene fjerner denne informasjonen fra den opprinnelige teksten. Siden disse modellene ikke kan modellere ordrekkefølge, kan de ikke løse mer komplekse eller tvetydige oppgaver som tekstgenerering eller spørsmål-svar.\n", "\n", "For å fange opp meningen i en tekstsekvens, må vi bruke en annen nevralt nettverksarkitektur, som kalles et **rekurrent nevralt nettverk**, eller RNN. I et RNN sender vi setningen vår gjennom nettverket ett symbol om gangen, og nettverket produserer en **tilstand**, som vi deretter sender tilbake til nettverket sammen med neste symbol.\n", "\n", "Gitt inndatasekvensen av token $X_0,\\dots,X_n$, lager RNN en sekvens av nevrale nettverksblokker og trener denne sekvensen ende-til-ende ved hjelp av backpropagation. Hver nettverksblokk tar et par $(X_i,S_i)$ som input og produserer $S_{i+1}$ som resultat. Den endelige tilstanden $S_n$ eller utgangen $X_n$ går inn i en lineær klassifikator for å produsere resultatet. Alle nettverksblokker deler de samme vektene og trenes ende-til-ende ved hjelp av én backpropagation-passering.\n", "\n", "Siden tilstandsvektorene $S_0,\\dots,S_n$ sendes gjennom nettverket, er det i stand til å lære de sekvensielle avhengighetene mellom ordene. For eksempel, når ordet *ikke* dukker opp et sted i sekvensen, kan det lære å negere visse elementer i tilstandsvektoren, noe som resulterer i negasjon.\n", "\n", "> Siden vektene til alle RNN-blokkene på bildet er delte, kan det samme bildet representeres som én blokk (til høyre) med en rekurrent tilbakemeldingssløyfe, som sender utgangstilstanden til nettverket tilbake til inngangen.\n", "\n", "La oss se hvordan rekurrente nevrale nettverk kan hjelpe oss med å klassifisere nyhetsdatasettet vårt.\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-klassifiserer\n", "\n", "Når det gjelder enkel RNN, er hver rekurrent enhet et enkelt lineært nettverk som tar en sammenkoblet inputvektor og tilstandsvektor, og produserer en ny tilstandsvektor. PyTorch representerer denne enheten med klassen `RNNCell`, og et nettverk av slike celler - som laget `RNN`.\n", "\n", "For å definere en RNN-klassifiserer, vil vi først bruke et innebyggingslag for å redusere dimensjonaliteten til input-ordforrådet, og deretter ha et RNN-lag på toppen av dette:\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:** Vi bruker et utrent embedding-lag her for enkelhets skyld, men for enda bedre resultater kan vi bruke et forhåndstrent embedding-lag med Word2Vec eller GloVe embeddings, som beskrevet i den forrige enheten. For bedre forståelse kan det være lurt å tilpasse denne koden til å fungere med forhåndstrente embeddings.\n", "\n", "I vårt tilfelle vil vi bruke en data loader med padding, slik at hver batch vil ha et antall sekvenser med samme lengde. RNN-laget vil ta sekvensen av embedding-tensore og produsere to utganger:\n", "* $x$ er en sekvens av RNN-celleutganger ved hvert steg\n", "* $h$ er en endelig skjult tilstand for det siste elementet i sekvensen\n", "\n", "Deretter bruker vi en fullt tilkoblet lineær klassifiserer for å få antall klasser.\n", "\n", "> **Note:** RNN-er er ganske vanskelige å trene, fordi når RNN-cellene rulles ut langs sekvenslengden, blir antallet lag involvert i tilbakepropagering ganske stort. Derfor må vi velge en liten læringsrate og trene nettverket på et større datasett for å oppnå gode resultater. Det kan ta ganske lang tid, så det er foretrukket å bruke 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": [ "## Lang Korttidsminne (LSTM)\n", "\n", "Et av hovedproblemene med klassiske RNN-er er det såkalte **problemet med forsvinnende gradienter**. Fordi RNN-er trenes ende-til-ende i én bakoverpropageringspassering, har de vanskeligheter med å propagere feil til de første lagene i nettverket, og dermed kan ikke nettverket lære relasjoner mellom fjerne tokens. En av måtene å unngå dette problemet på er å introdusere **eksplisitt tilstandshåndtering** ved å bruke såkalte **porter**. Det finnes to mest kjente arkitekturer av denne typen: **Lang Korttidsminne** (LSTM) og **Gated Relay Unit** (GRU).\n", "\n", "![Bilde som viser et eksempel på en lang korttidsminnecelle](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n", "\n", "LSTM-nettverket er organisert på en måte som ligner på RNN, men det er to tilstander som blir sendt fra lag til lag: faktisk tilstand $c$, og skjult vektor $h$. Ved hver enhet blir skjult vektor $h_i$ sammenkoblet med input $x_i$, og de kontrollerer hva som skjer med tilstanden $c$ via **porter**. Hver port er et nevralt nettverk med sigmoid-aktivering (output i området $[0,1]$), som kan betraktes som en bitmaske når den multipliseres med tilstandsvektoren. Det finnes følgende porter (fra venstre til høyre på bildet ovenfor):\n", "* **glemporten** tar skjult vektor og bestemmer hvilke komponenter av vektoren $c$ vi må glemme, og hvilke vi skal sende videre.\n", "* **inputporten** tar noe informasjon fra input og skjult vektor, og setter det inn i tilstanden.\n", "* **outputporten** transformerer tilstanden via et lineært lag med $\\tanh$-aktivering, og velger deretter noen av komponentene ved hjelp av skjult vektor $h_i$ for å produsere ny tilstand $c_{i+1}$.\n", "\n", "Komponentene i tilstanden $c$ kan betraktes som noen flagg som kan slås av og på. For eksempel, når vi møter et navn *Alice* i sekvensen, kan vi anta at det refererer til en kvinnelig karakter, og heve flagget i tilstanden som indikerer at vi har et kvinnelig substantiv i setningen. Når vi senere møter frasen *og Tom*, vil vi heve flagget som indikerer at vi har et flertallssubstantiv. Dermed kan vi ved å manipulere tilstanden antagelig holde styr på grammatiske egenskaper ved setningsdeler.\n", "\n", "> **Note**: En flott ressurs for å forstå det indre av LSTM er denne utmerkede artikkelen [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) av Christopher Olah.\n", "\n", "Selv om den interne strukturen til LSTM-cellen kan virke kompleks, skjuler PyTorch denne implementasjonen inne i `LSTMCell`-klassen, og tilbyr `LSTM`-objektet for å representere hele LSTM-laget. Dermed vil implementeringen av en LSTM-klassifiserer være ganske lik den enkle RNN-en vi har sett 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 vårt eksempel måtte vi fylle opp alle sekvensene i minibatchen med nullvektorer. Selv om dette fører til noe sløsing med minne, er det mer kritisk med RNN-er at ekstra RNN-celler opprettes for de utfylte input-elementene. Disse deltar i treningen, men inneholder ingen viktig inputinformasjon. Det ville vært mye bedre å trene RNN kun til den faktiske sekvensstørrelsen.\n", "\n", "For å oppnå dette, er et spesielt format for lagring av utfylte sekvenser introdusert i PyTorch. Anta at vi har en utfylt minibatch som ser slik ut:\n", "```\n", "[[1,2,3,4,5],\n", " [6,7,8,0,0],\n", " [9,0,0,0,0]]\n", "```\n", "Her representerer 0 utfylte verdier, og den faktiske lengdevektoren for inputsekvensene er `[5,3,1]`.\n", "\n", "For å effektivt trene RNN med utfylte sekvenser, ønsker vi å starte treningen med den første gruppen av RNN-celler med en stor minibatch (`[1,6,9]`), men deretter avslutte behandlingen av den tredje sekvensen og fortsette treningen med mindre minibatcher (`[2,7]`, `[3,8]`), og så videre. Dermed representeres den pakkede sekvensen som én vektor – i vårt tilfelle `[1,6,9,2,7,3,8,4,5]`, og en lengdevektor (`[5,3,1]`), som vi enkelt kan bruke til å rekonstruere den opprinnelige utfylte minibatchen.\n", "\n", "For å produsere en pakket sekvens, kan vi bruke funksjonen `torch.nn.utils.rnn.pack_padded_sequence`. Alle rekurrente lag, inkludert RNN, LSTM og GRU, støtter pakkede sekvenser som input og produserer pakket output, som kan dekodes ved hjelp av `torch.nn.utils.rnn.pad_packed_sequence`.\n", "\n", "For å kunne produsere en pakket sekvens, må vi sende lengdevektoren til nettverket, og derfor trenger vi en annen funksjon for å forberede minibatcher:\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 nettverket vil være veldig likt `LSTMClassifier` ovenfor, men `forward`-passet vil motta både polstret minibatch og vektoren av sekvenslengder. Etter å ha beregnet embedding, beregner vi en pakket sekvens, sender den til LSTM-laget, og pakker deretter resultatet ut igjen.\n", "\n", "> **Merk**: Vi bruker faktisk ikke det utpakkede resultatet `x`, fordi vi bruker output fra de skjulte lagene i de følgende beregningene. Derfor kan vi fjerne utpakkingen helt fra denne koden. Grunnen til at vi plasserer det her er for at du enkelt skal kunne modifisere denne koden, i tilfelle du skulle trenge å bruke nettverksoutput i videre 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": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Toveis og flerlags RNN-er\n", "\n", "I våre eksempler har alle rekurrente nettverk operert i én retning, fra begynnelsen av en sekvens til slutten. Det virker naturlig, fordi det ligner på måten vi leser og lytter til tale. Men siden vi i mange praktiske tilfeller har tilfeldig tilgang til inngangssekvensen, kan det være fornuftig å utføre rekurrent beregning i begge retninger. Slike nettverk kalles **toveis** RNN-er, og de kan opprettes ved å sende parameteren `bidirectional=True` til RNN/LSTM/GRU-konstruktøren.\n", "\n", "Når vi arbeider med et toveis nettverk, trenger vi to skjulte tilstandsvektorer, én for hver retning. PyTorch koder disse vektorene som én vektor med dobbelt så stor størrelse, noe som er ganske praktisk, fordi du vanligvis sender den resulterende skjulte tilstanden til et fullt tilkoblet lineært lag, og du trenger bare å ta denne økningen i størrelse med i betraktning når du oppretter laget.\n", "\n", "Et rekurrent nettverk, enten det er énveis eller toveis, fanger visse mønstre innen en sekvens og kan lagre dem i tilstandsvektoren eller sende dem til utgangen. Som med konvolusjonsnettverk kan vi bygge et annet rekurrent lag oppå det første for å fange mønstre på høyere nivå, bygget fra lavnivåmønstre som det første laget har hentet ut. Dette leder oss til begrepet **flerlags RNN**, som består av to eller flere rekurrente nettverk, der utgangen fra det forrige laget sendes til det neste laget som inngang.\n", "\n", "![Bilde som viser et flerlags lang-korttidsminne-RNN](../../../../../translated_images/no/multi-layer-lstm.dd975e29bb2a59fe.webp)\n", "\n", "*Bilde fra [denne fantastiske artikkelen](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) av Fernando López*\n", "\n", "PyTorch gjør det enkelt å konstruere slike nettverk, fordi du bare trenger å sende parameteren `num_layers` til RNN/LSTM/GRU-konstruktøren for automatisk å bygge flere lag med rekurrens. Dette vil også bety at størrelsen på den skjulte/tilstandsvektoren vil øke proporsjonalt, og du må ta dette med i betraktning når du håndterer utgangen fra de rekurrente lagene.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## RNN-er for andre oppgaver\n", "\n", "I denne enheten har vi sett at RNN-er kan brukes til sekvensklassifisering, men faktisk kan de håndtere mange flere oppgaver, som tekstgenerering, maskinoversettelse og mer. Vi vil se nærmere på disse oppgavene i neste enhet.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Ansvarsfraskrivelse**: \nDette dokumentet er oversatt ved hjelp av AI-oversettelsestjenesten [Co-op Translator](https://github.com/Azure/co-op-translator). Selv om vi streber etter nøyaktighet, vær oppmerksom på at automatiserte oversettelser kan inneholde feil eller unøyaktigheter. Det originale dokumentet på sitt opprinnelige språk bør anses som den autoritative kilden. For kritisk informasjon anbefales profesjonell menneskelig oversettelse. Vi er ikke ansvarlige for misforståelser eller feiltolkninger som oppstår ved bruk av denne oversettelsen.\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:45:06+00:00", "source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb", "language_code": "no" } }, "nbformat": 4, "nbformat_minor": 2 }