AI-For-Beginners/translations/no/lessons/5-NLP/16-RNN/RNNTF.ipynb

460 lines
23 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Rekurrente nevrale nettverk\n",
"\n",
"I det forrige modulen dekket vi rike semantiske representasjoner av tekst. Arkitekturen vi har brukt fanger opp den aggregerte meningen av ordene i en setning, men den tar ikke hensyn til **rekkefølgen** av ordene, fordi aggregeringsoperasjonen som følger etter embeddingene fjerner denne informasjonen fra den opprinnelige teksten. Siden disse modellene ikke kan representere ordrekkefølge, kan de ikke løse mer komplekse eller tvetydige oppgaver som tekstgenerering eller spørsmål-svar.\n",
"\n",
"For å fange meningen av en tekstsekvens, vil vi bruke en nevralt nettverksarkitektur kalt **rekurrente nevrale nettverk**, eller RNN. Når vi bruker en RNN, sender vi setningen vår gjennom nettverket én token om gangen, og nettverket produserer en **tilstand**, som vi deretter sender tilbake til nettverket sammen med neste token.\n",
"\n",
"![Bilde som viser et eksempel på generering med rekurrente nevrale nettverk.](../../../../../translated_images/no/rnn.27f5c29c53d727b5.webp)\n",
"\n",
"Gitt en inngangssekvens av tokenene $X_0,\\dots,X_n$, lager RNN-en 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 $Y_n$ går inn i en lineær klassifiserer for å produsere resultatet. Alle nettverksblokker deler de samme vektene og trenes ende-til-ende med én backpropagation-passering.\n",
"\n",
"> Figuren ovenfor viser et rekurrent nevralt nettverk i utrullet form (til venstre) og i en mer kompakt rekurrent representasjon (til høyre). Det er viktig å forstå at alle RNN-celler har de samme **delbare vektene**.\n",
"\n",
"Siden tilstandsvektorene $S_0,\\dots,S_n$ sendes gjennom nettverket, er RNN-en i stand til å lære sekvensielle avhengigheter mellom ord. For eksempel, når ordet *ikke* dukker opp et sted i sekvensen, kan den lære å negere visse elementer i tilstandsvektoren.\n",
"\n",
"Inni hver RNN-celle finnes det to vektmatriser: $W_H$ og $W_I$, og en bias $b$. Ved hvert RNN-trinn, gitt input $X_i$ og input-tilstand $S_i$, beregnes utgangstilstanden som $S_{i+1} = f(W_H\\times S_i + W_I\\times X_i+b)$, der $f$ er en aktiveringsfunksjon (ofte $\\tanh$).\n",
"\n",
"> For problemer som tekstgenerering (som vi skal dekke i neste enhet) eller maskinoversettelse, ønsker vi også å få en utgangsverdi ved hvert RNN-trinn. I dette tilfellet finnes det også en annen matrise $W_O$, og utgangen beregnes som $Y_i=f(W_O\\times S_i+b_O)$.\n",
"\n",
"La oss se hvordan rekurrente nevrale nettverk kan hjelpe oss med å klassifisere nyhetsdatasettet vårt.\n",
"\n",
"> For sandkassemiljøet må vi kjøre følgende celle for å sikre at det nødvendige biblioteket er installert, og at dataene er forhåndshentet. Hvis du kjører lokalt, kan du hoppe over følgende celle.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"!{sys.executable} -m pip install --quiet tensorflow_datasets==4.4.0\n",
"!cd ~ && wget -q -O - https://mslearntensorflowlp.blob.core.windows.net/data/tfds-ag-news.tgz | tar xz"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"import tensorflow as tf\n",
"from tensorflow import keras\n",
"import tensorflow_datasets as tfds\n",
"import numpy as np\n",
"\n",
"# We are going to be training pretty large models. In order not to face errors, we need\n",
"# to set tensorflow option to grow GPU memory allocation when required\n",
"physical_devices = tf.config.list_physical_devices('GPU') \n",
"if len(physical_devices)>0:\n",
" tf.config.experimental.set_memory_growth(physical_devices[0], True)\n",
"\n",
"ds_train, ds_test = tfds.load('ag_news_subset').values()"
]
},
{
"cell_type": "markdown",
"metadata": {
"nteract": {
"transient": {
"deleting": false
}
}
},
"source": [
"Når man trener store modeller, kan GPU-minneallokering bli et problem. Vi kan også trenge å eksperimentere med ulike minibatch-størrelser, slik at dataene passer inn i GPU-minnet, samtidig som treningen er rask nok. Hvis du kjører denne koden på din egen GPU-maskin, kan du eksperimentere med å justere minibatch-størrelsen for å øke treningshastigheten.\n",
"\n",
"> **Note**: Enkelte versjoner av NVidia-drivere er kjent for ikke å frigjøre minnet etter at modellen er trent. Vi kjører flere eksempler i denne notatboken, og det kan føre til at minnet blir oppbrukt i visse oppsett, spesielt hvis du gjør egne eksperimenter som en del av samme notatbok. Hvis du støter på noen rare feil når du starter treningen av modellen, kan det være lurt å starte notatbok-kjernen på nytt.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": true,
"jupyter": {
"outputs_hidden": false,
"source_hidden": false
},
"nteract": {
"transient": {
"deleting": false
}
}
},
"outputs": [],
"source": [
"batch_size = 16\n",
"embed_size = 64"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Enkel RNN-klassifiserer\n",
"\n",
"I tilfellet med en enkel RNN er hver rekurrent enhet et enkelt lineært nettverk som tar inn en input-vektor og en tilstandsvektor, og produserer en ny tilstandsvektor. I Keras kan dette representeres ved `SimpleRNN`-laget.\n",
"\n",
"Selv om vi kan sende én-hot kodede tokens direkte til RNN-laget, er dette ikke en god idé på grunn av deres høye dimensjonalitet. Derfor vil vi bruke et embedding-lag for å redusere dimensjonaliteten til ordvektorene, etterfulgt av et RNN-lag, og til slutt en `Dense`-klassifiserer.\n",
"\n",
"> **Merk**: I tilfeller der dimensjonaliteten ikke er så høy, for eksempel ved bruk av tegn-nivå tokenisering, kan det være fornuftig å sende én-hot kodede tokens direkte inn i RNN-cellen.\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Model: \"sequential\"\n",
"_________________________________________________________________\n",
"Layer (type) Output Shape Param # \n",
"=================================================================\n",
"text_vectorization (TextVect (None, None) 0 \n",
"_________________________________________________________________\n",
"embedding (Embedding) (None, None, 64) 1280000 \n",
"_________________________________________________________________\n",
"simple_rnn (SimpleRNN) (None, 16) 1296 \n",
"_________________________________________________________________\n",
"dense (Dense) (None, 4) 68 \n",
"=================================================================\n",
"Total params: 1,281,364\n",
"Trainable params: 1,281,364\n",
"Non-trainable params: 0\n",
"_________________________________________________________________\n"
]
}
],
"source": [
"vocab_size = 20000\n",
"\n",
"vectorizer = keras.layers.experimental.preprocessing.TextVectorization(\n",
" max_tokens=vocab_size,\n",
" input_shape=(1,))\n",
"\n",
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, embed_size),\n",
" keras.layers.SimpleRNN(16),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.summary()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Note:** Vi bruker et utrent embedding-lag her for enkelhets skyld, men for bedre resultater kan vi bruke et forhåndstrent embedding-lag ved hjelp av Word2Vec, som beskrevet i forrige enhet. Det kan være en god øvelse for deg å tilpasse denne koden til å fungere med forhåndstrente embeddings.\n",
"\n",
"La oss nå trene vår RNN. RNN-er generelt er ganske vanskelige å trene, fordi når RNN-cellene blir rullet ut langs sekvenslengden, blir antallet lag involvert i backpropagation ganske stort. Derfor må vi velge en mindre læringsrate og trene nettverket på et større datasett for å oppnå gode resultater. Dette kan ta ganske lang tid, så det er foretrukket å bruke en GPU.\n",
"\n",
"For å gjøre ting raskere, vil vi kun trene RNN-modellen på nyhetstitler og utelate beskrivelsen. Du kan prøve å trene med beskrivelsen og se om du klarer å få modellen til å trene.\n"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Training vectorizer\n"
]
}
],
"source": [
"def extract_title(x):\n",
" return x['title']\n",
"\n",
"def tupelize_title(x):\n",
" return (extract_title(x),x['label'])\n",
"\n",
"print('Training vectorizer')\n",
"vectorizer.adapt(ds_train.take(2000).map(extract_title))"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"7500/7500 [==============================] - 82s 11ms/step - loss: 0.6629 - acc: 0.7623 - val_loss: 0.5559 - val_acc: 0.7995\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3e0030d350>"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize_title).batch(batch_size),validation_data=ds_test.map(tupelize_title).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {
"nteract": {
"transient": {
"deleting": false
}
}
},
"source": [
"> **Merk** at nøyaktigheten sannsynligvis vil være lavere her, fordi vi kun trener på nyhetstitler.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Gjenbesøk av variabelsekvenser\n",
"\n",
"Husk at `TextVectorization`-laget automatisk vil fylle opp sekvenser med variabel lengde i en minibatch med pad-tokens. Det viser seg at disse tokens også deltar i treningen, og de kan gjøre det vanskeligere for modellen å konvergere.\n",
"\n",
"Det finnes flere tilnærminger vi kan bruke for å minimere mengden padding. En av dem er å omorganisere datasettet etter sekvenslengde og gruppere alle sekvenser etter størrelse. Dette kan gjøres ved hjelp av funksjonen `tf.data.experimental.bucket_by_sequence_length` (se [dokumentasjon](https://www.tensorflow.org/api_docs/python/tf/data/experimental/bucket_by_sequence_length)).\n",
"\n",
"En annen tilnærming er å bruke **maskering**. I Keras støtter noen lag ekstra input som viser hvilke tokens som skal tas med i betraktning under trening. For å inkludere maskering i modellen vår, kan vi enten legge til et eget `Masking`-lag ([dokumentasjon](https://keras.io/api/layers/core_layers/masking/)), eller vi kan spesifisere parameteren `mask_zero=True` i vårt `Embedding`-lag.\n",
"\n",
"> **Note**: Denne treningen vil ta omtrent 5 minutter for å fullføre én epoke på hele datasettet. Du kan avbryte treningen når som helst hvis du mister tålmodigheten. Det du også kan gjøre er å begrense mengden data som brukes til trening, ved å legge til `.take(...)`-klausul etter `ds_train`- og `ds_test`-datasett.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"7500/7500 [==============================] - 371s 49ms/step - loss: 0.5401 - acc: 0.8079 - val_loss: 0.3780 - val_acc: 0.8822\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3dec118850>"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"def extract_text(x):\n",
" return x['title']+' '+x['description']\n",
"\n",
"def tupelize(x):\n",
" return (extract_text(x),x['label'])\n",
"\n",
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size,embed_size,mask_zero=True),\n",
" keras.layers.SimpleRNN(16),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(batch_size),validation_data=ds_test.map(tupelize).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Nå som vi bruker maskering, kan vi trene modellen på hele datasettet med titler og beskrivelser.\n",
"\n",
"> **Merk**: Har du lagt merke til at vi har brukt en vektorisering som er trent på nyhetstitlene, og ikke hele artikkelens innhold? Dette kan potensielt føre til at noen av tokenene blir ignorert, så det er bedre å trene opp vektoriseringen på nytt. Likevel vil dette sannsynligvis bare ha en veldig liten effekt, så vi holder oss til den tidligere forhåndstrente vektoriseringen for enkelhets skyld.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## LSTM: Langtids korttidsminne\n",
"\n",
"Et av hovedproblemene med RNN-er er **forsvinnende gradienter**. RNN-er kan være ganske lange og kan ha vanskeligheter med å propagere gradientene helt tilbake til det første laget i nettverket under tilbakepropagering. Når dette skjer, kan ikke nettverket lære sammenhenger mellom fjerne tokens. En måte å unngå dette problemet på er å introdusere **eksplisitt tilstandshåndtering** ved å bruke **porter**. De to vanligste arkitekturene som introduserer porter er **langtids korttidsminne** (LSTM) og **gated relay unit** (GRU). Vi skal dekke LSTM-er her.\n",
"\n",
"![Bilde som viser et eksempel på en langtids korttidsminne-celle](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"Et LSTM-nettverk er organisert på en måte som ligner på et RNN, men det er to tilstander som overføres fra lag til lag: den faktiske tilstanden $c$ og den skjulte vektoren $h$. Ved hver enhet kombineres den skjulte vektoren $h_{t-1}$ med inngangen $x_t$, og sammen styrer de hva som skjer med tilstanden $c_t$ og utgangen $h_{t}$ gjennom **porter**. Hver port har sigmoid-aktivering (utgang i området $[0,1]$), som kan betraktes som en bitmaske når den multipliseres med tilstandsvektoren. LSTM-er har følgende porter (fra venstre til høyre på bildet over):\n",
"* **glemporten**, som bestemmer hvilke komponenter av vektoren $c_{t-1}$ vi trenger å glemme, og hvilke som skal passere gjennom. \n",
"* **inngangsporten**, som bestemmer hvor mye informasjon fra inngangsvektoren og den forrige skjulte vektoren som skal inkorporeres i tilstandsvektoren.\n",
"* **utgangsporten**, som tar den nye tilstandsvektoren og bestemmer hvilke av dens komponenter som skal brukes til å produsere den nye skjulte vektoren $h_t$.\n",
"\n",
"Komponentene i tilstanden $c$ kan betraktes som flagg som kan slås av og på. For eksempel, når vi støter på navnet *Alice* i sekvensen, antar vi at det refererer til en kvinne, og hever flagget i tilstanden som sier at vi har et feminint substantiv i setningen. Når vi videre støter på ordene *og Tom*, vil vi heve flagget som sier at vi har et flertallssubstantiv. Dermed kan vi ved å manipulere tilstanden holde styr på de grammatiske egenskapene til setningen.\n",
"\n",
"> **Merk**: Her er en flott ressurs for å forstå det indre av LSTM-er: [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) av Christopher Olah.\n",
"\n",
"Selv om den interne strukturen til en LSTM-celle kan se kompleks ut, skjuler Keras denne implementasjonen inne i `LSTM`-laget, så det eneste vi trenger å gjøre i eksempelet over er å erstatte det rekurrente laget:\n"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"15000/15000 [==============================] - 188s 13ms/step - loss: 0.5692 - acc: 0.7916 - val_loss: 0.3441 - val_acc: 0.8870\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\n"
]
},
{
"data": {
"text/plain": [
"<tensorflow.python.keras.callbacks.History at 0x7f3d6af5c350>"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, embed_size),\n",
" keras.layers.LSTM(8),\n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(8),validation_data=ds_test.map(tupelize).batch(8))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Toveis og flerlags RNN-er\n",
"\n",
"I eksemplene våre så langt, opererer de rekurrente nettverkene fra begynnelsen av en sekvens til slutten. Dette føles naturlig for oss fordi det følger samme retning som vi leser eller lytter til tale. Men for scenarier som krever tilfeldig tilgang til inngangssekvensen, gir det mer mening å utføre rekurrente beregninger i begge retninger. RNN-er som tillater beregninger i begge retninger kalles **toveis** RNN-er, og de kan opprettes ved å pakke den rekurrente laget inn i et spesielt `Bidirectional`-lag.\n",
"\n",
"> **Note**: `Bidirectional`-laget lager to kopier av laget inni seg, og setter `go_backwards`-egenskapen til en av disse kopiene til `True`, slik at det går i motsatt retning langs sekvensen.\n",
"\n",
"Rekurrente nettverk, enten de er enveis eller toveis, fanger opp mønstre innen en sekvens og lagrer dem i tilstandsvektorer eller returnerer dem som output. Akkurat som med konvolusjonsnettverk, kan vi bygge et annet rekurrent lag etter det første for å fange opp mønstre på et høyere nivå, bygget fra mønstre på lavere nivå 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 output fra det forrige laget sendes videre til det neste laget som input.\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",
"Keras gjør det enkelt å konstruere disse nettverkene, fordi du bare trenger å legge til flere rekurrente lag i modellen. For alle lag unntatt det siste, må vi spesifisere parameteren `return_sequences=True`, fordi vi trenger at laget returnerer alle mellomliggende tilstander, og ikke bare den endelige tilstanden av den rekurrente beregningen.\n",
"\n",
"La oss bygge et to-lags toveis LSTM for klassifiseringsproblemet vårt.\n",
"\n",
"> **Note** denne koden tar igjen ganske lang tid å fullføre, men den gir oss den høyeste nøyaktigheten vi har sett så langt. Så kanskje det er verdt å vente og se resultatet.\n"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"5044/7500 [===================>..........] - ETA: 2:33 - loss: 0.3709 - acc: 0.8706\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r5045/7500 [===================>..........] - ETA: 2:33 - loss: 0.3709 - acc: 0.8706"
]
}
],
"source": [
"model = keras.models.Sequential([\n",
" vectorizer,\n",
" keras.layers.Embedding(vocab_size, 128, mask_zero=True),\n",
" keras.layers.Bidirectional(keras.layers.LSTM(64,return_sequences=True)),\n",
" keras.layers.Bidirectional(keras.layers.LSTM(64)), \n",
" keras.layers.Dense(4,activation='softmax')\n",
"])\n",
"\n",
"model.compile(loss='sparse_categorical_crossentropy',metrics=['acc'], optimizer='adam')\n",
"model.fit(ds_train.map(tupelize).batch(batch_size),\n",
" validation_data=ds_test.map(tupelize).batch(batch_size))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNN-er for andre oppgaver\n",
"\n",
"Hittil har vi fokusert på å bruke RNN-er til å klassifisere tekstsekvenser. Men de kan håndtere mange flere oppgaver, som tekstgenerering og maskinoversettelse — vi skal 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": {
"kernel_info": {
"name": "conda-env-py37_tensorflow-py"
},
"kernelspec": {
"display_name": "py37_tensorflow",
"language": "python",
"name": "conda-env-py37_tensorflow-py"
},
"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.7.9"
},
"nteract": {
"version": "nteract-front-end@1.0.0"
},
"coopTranslator": {
"original_hash": "81351e61f619b432ff51010a4f993194",
"translation_date": "2025-08-28T17:42:35+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNTF.ipynb",
"language_code": "no"
}
},
"nbformat": 4,
"nbformat_minor": 4
}