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

460 lines
23 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Rekurentní neuronové sítě\n",
"\n",
"V předchozím modulu jsme se zabývali bohatými sémantickými reprezentacemi textu. Architektura, kterou jsme používali, zachycuje agregovaný význam slov ve větě, ale nezohledňuje **pořadí** slov, protože operace agregace, která následuje po vnoření, tuto informaci z původního textu odstraňuje. Protože tyto modely nejsou schopny reprezentovat pořadí slov, nemohou řešit složitější nebo nejednoznačné úkoly, jako je generování textu nebo odpovídání na otázky.\n",
"\n",
"Abychom zachytili význam textové sekvence, použijeme architekturu neuronové sítě nazývanou **rekurentní neuronová síť** (RNN). Při použití RNN procházíme větou sítí po jednom tokenu a síť produkuje určitý **stav**, který poté předáváme síti spolu s dalším tokenem.\n",
"\n",
"![Obrázek ukazující příklad generování rekurentní neuronové sítě.](../../../../../translated_images/cs/rnn.27f5c29c53d727b5.webp)\n",
"\n",
"Při dané vstupní sekvenci tokenů $X_0,\\dots,X_n$ RNN vytváří sekvenci bloků neuronové sítě a trénuje tuto sekvenci end-to-end pomocí zpětné propagace. Každý blok sítě přijímá dvojici $(X_i,S_i)$ jako vstup a produkuje $S_{i+1}$ jako výsledek. Konečný stav $S_n$ nebo výstup $Y_n$ se předává do lineárního klasifikátoru, aby se vytvořil výsledek. Všechny bloky sítě sdílejí stejné váhy a jsou trénovány end-to-end jedním průchodem zpětné propagace.\n",
"\n",
"> Obrázek výše ukazuje rekurentní neuronovou síť v rozvinuté podobě (vlevo) a v kompaktnější rekurentní reprezentaci (vpravo). Je důležité si uvědomit, že všechny RNN buňky mají stejné **sdílené váhy**.\n",
"\n",
"Protože stavové vektory $S_0,\\dots,S_n$ procházejí sítí, RNN je schopna se naučit sekvenční závislosti mezi slovy. Například když se někde v sekvenci objeví slovo *not*, může se naučit negovat určité prvky ve stavovém vektoru.\n",
"\n",
"Uvnitř každé RNN buňky jsou dvě matice vah: $W_H$ a $W_I$, a bias $b$. Při každém kroku RNN, při daném vstupu $X_i$ a vstupním stavu $S_i$, se výstupní stav vypočítá jako $S_{i+1} = f(W_H\\times S_i + W_I\\times X_i+b)$, kde $f$ je aktivační funkce (často $\\tanh$).\n",
"\n",
"> U problémů, jako je generování textu (které pokryjeme v další jednotce) nebo strojový překlad, chceme také získat nějakou výstupní hodnotu při každém kroku RNN. V tomto případě existuje další matice $W_O$ a výstup se vypočítá jako $Y_i=f(W_O\\times S_i+b_O)$.\n",
"\n",
"Podívejme se, jak nám rekurentní neuronové sítě mohou pomoci klasifikovat naši datovou sadu zpráv.\n",
"\n",
"> Pro sandboxové prostředí je třeba spustit následující buňku, abychom zajistili, že požadovaná knihovna je nainstalována a data jsou předem načtena. Pokud pracujete lokálně, můžete tuto buňku přeskočit.\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": [
"Při trénování velkých modelů může být problémem alokace paměti GPU. Také může být potřeba experimentovat s různými velikostmi minibatchů, aby se data vešla do paměti GPU a zároveň bylo trénování dostatečně rychlé. Pokud tento kód spouštíte na svém vlastním GPU stroji, můžete experimentovat s úpravou velikosti minibatchů pro zrychlení trénování.\n",
"\n",
"> **Note**: Je známo, že některé verze ovladačů NVidia neuvolňují paměť po trénování modelu. V tomto notebooku spouštíme několik příkladů, což může v určitých konfiguracích způsobit vyčerpání paměti, zejména pokud provádíte vlastní experimenty v rámci stejného notebooku. Pokud narazíte na podivné chyby při zahájení trénování modelu, může být vhodné restartovat kernel notebooku.\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": [
"## Jednoduchý RNN klasifikátor\n",
"\n",
"V případě jednoduchého RNN je každá rekurentní jednotka jednoduchou lineární sítí, která přijímá vstupní vektor a stavový vektor a vytváří nový stavový vektor. V Kerasu to lze reprezentovat pomocí vrstvy `SimpleRNN`.\n",
"\n",
"I když můžeme předávat tokeny zakódované metodou one-hot přímo do vrstvy RNN, není to dobrý nápad kvůli jejich vysoké dimenzionalitě. Proto použijeme vrstvu embedding ke snížení dimenzionality slovních vektorů, následovanou vrstvou RNN a nakonec klasifikátorem `Dense`.\n",
"\n",
"> **Note**: V případech, kdy dimenzionalita není tak vysoká, například při použití tokenizace na úrovni znaků, může být smysluplné předávat tokeny zakódované metodou one-hot přímo do buňky RNN.\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": [
"> **Poznámka:** Zde používáme netrénovanou vrstvu vkládání pro zjednodušení, ale pro lepší výsledky můžeme použít předtrénovanou vrstvu vkládání pomocí Word2Vec, jak bylo popsáno v předchozí jednotce. Bylo by dobrým cvičením upravit tento kód tak, aby fungoval s předtrénovanými vkládáními.\n",
"\n",
"Nyní pojďme natrénovat naši RNN. RNN jsou obecně poměrně obtížné trénovat, protože jakmile jsou buňky RNN rozvinuty podél délky sekvence, výsledný počet vrstev zapojených do zpětné propagace je velmi vysoký. Proto je potřeba zvolit menší rychlost učení a trénovat síť na větším datasetu, aby se dosáhlo dobrých výsledků. To může trvat poměrně dlouho, takže je preferováno použití GPU.\n",
"\n",
"Abychom proces urychlili, budeme model RNN trénovat pouze na titulcích zpráv a vynecháme popis. Můžete zkusit trénovat i s popisem a zjistit, zda se vám podaří model natrénovat.\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": [
"> **Poznámka** že přesnost bude pravděpodobně nižší, protože trénujeme pouze na titulcích zpráv.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Znovu se podívejme na sekvence proměnných\n",
"\n",
"Pamatujte, že vrstva `TextVectorization` automaticky doplní sekvence proměnné délky v minibatchi pomocí padovacích tokenů. Ukazuje se, že tyto tokeny se také účastní trénování a mohou komplikovat konvergenci modelu.\n",
"\n",
"Existuje několik přístupů, které můžeme použít ke snížení množství paddingu. Jedním z nich je přeřazení datasetu podle délky sekvence a seskupení všech sekvencí podle velikosti. To lze provést pomocí funkce `tf.data.experimental.bucket_by_sequence_length` (viz [dokumentace](https://www.tensorflow.org/api_docs/python/tf/data/experimental/bucket_by_sequence_length)).\n",
"\n",
"Dalším přístupem je použití **maskování**. V Keras některé vrstvy podporují dodatečný vstup, který ukazuje, které tokeny by měly být zohledněny při trénování. Abychom do našeho modelu začlenili maskování, můžeme buď přidat samostatnou vrstvu `Masking` ([dokumentace](https://keras.io/api/layers/core_layers/masking/)), nebo můžeme specifikovat parametr `mask_zero=True` u naší vrstvy `Embedding`.\n",
"\n",
"> **Note**: Toto trénování zabere přibližně 5 minut na dokončení jedné epochy na celém datasetu. Pokud vám dojde trpělivost, můžete trénování kdykoli přerušit. Další možností je omezit množství dat použitých pro trénování přidáním klauzule `.take(...)` po datasetech `ds_train` a `ds_test`.\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": [
"Nyní, když používáme maskování, můžeme model trénovat na celém datasetu titulků a popisů.\n",
"\n",
"> **Note**: Všimli jste si, že jsme používali vektorizér natrénovaný na titulcích zpráv, a ne na celém textu článku? To může potenciálně způsobit, že některé tokeny budou ignorovány, takže je lepší vektorizér přeškolit. Nicméně, efekt by mohl být velmi malý, takže pro zjednodušení zůstaneme u předchozího předtrénovaného vektorizéru.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## LSTM: Dlouhá krátkodobá paměť\n",
"\n",
"Jedním z hlavních problémů RNN je **mizení gradientů**. RNN mohou být poměrně dlouhé a mohou mít potíže s propagací gradientů zpět až k první vrstvě sítě během zpětného šíření. Když k tomu dojde, síť se nedokáže naučit vztahy mezi vzdálenými tokeny. Jedním ze způsobů, jak tento problém obejít, je zavedení **explicitního řízení stavu** pomocí **bran**. Dvě nejběžnější architektury, které zavádějí brány, jsou **dlouhá krátkodobá paměť** (LSTM) a **gated relay unit** (GRU). Zde se budeme věnovat LSTM.\n",
"\n",
"![Obrázek ukazující příklad buňky dlouhé krátkodobé paměti](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"LSTM síť je organizována podobně jako RNN, ale existují dva stavy, které se předávají z vrstvy do vrstvy: aktuální stav $c$ a skrytý vektor $h$. V každé jednotce se skrytý vektor $h_{t-1}$ kombinuje se vstupem $x_t$ a společně určují, co se stane se stavem $c_t$ a výstupem $h_{t}$ prostřednictvím **bran**. Každá brána má sigmoidní aktivaci (výstup v rozmezí $[0,1]$), kterou si můžeme představit jako bitovou masku při násobení stavovým vektorem. LSTM mají následující brány (zleva doprava na obrázku výše):\n",
"* **zapomínací brána**, která určuje, které složky vektoru $c_{t-1}$ je třeba zapomenout a které propustit dál.\n",
"* **vstupní brána**, která určuje, kolik informací ze vstupního vektoru a předchozího skrytého vektoru by mělo být začleněno do stavového vektoru.\n",
"* **výstupní brána**, která vezme nový stavový vektor a rozhodne, které jeho složky budou použity k vytvoření nového skrytého vektoru $h_t$.\n",
"\n",
"Složky stavu $c$ si můžeme představit jako příznaky, které lze zapínat a vypínat. Například když v sekvenci narazíme na jméno *Alice*, odhadneme, že se jedná o ženu, a nastavíme příznak ve stavu, který říká, že máme v větě ženské podstatné jméno. Když dále narazíme na slova *a Tom*, nastavíme příznak, který říká, že máme množné číslo podstatného jména. Manipulací se stavem tak můžeme sledovat gramatické vlastnosti věty.\n",
"\n",
"> **Note**: Zde je skvělý zdroj pro pochopení vnitřní struktury LSTM: [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) od Christophera Olaha.\n",
"\n",
"I když může vnitřní struktura LSTM buňky vypadat složitě, Keras tuto implementaci skrývá uvnitř vrstvy `LSTM`, takže jediná věc, kterou musíme udělat v příkladu výše, je nahradit rekurentní vrstvu:\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": [
"## Obousměrné a vícevrstvé RNN\n",
"\n",
"V našich dosavadních příkladech pracují rekurentní sítě od začátku sekvence až do jejího konce. To nám připadá přirozené, protože to odpovídá směru, kterým čteme nebo posloucháme řeč. Nicméně v situacích, které vyžadují náhodný přístup k vstupní sekvenci, dává větší smysl provádět rekurentní výpočty v obou směrech. RNN, které umožňují výpočty v obou směrech, se nazývají **obousměrné** RNN a lze je vytvořit obalením rekurentní vrstvy speciální vrstvou `Bidirectional`.\n",
"\n",
"> **Note**: Vrstva `Bidirectional` vytvoří dvě kopie vrstvy uvnitř sebe a nastaví vlastnost `go_backwards` jedné z těchto kopií na hodnotu `True`, což způsobí, že bude procházet sekvenci opačným směrem.\n",
"\n",
"Rekurentní sítě, ať už jednosměrné nebo obousměrné, zachycují vzory v rámci sekvence a ukládají je do stavových vektorů nebo je vracejí jako výstup. Stejně jako u konvolučních sítí můžeme vytvořit další rekurentní vrstvu, která následuje po první, aby zachytila vzory na vyšší úrovni, vytvořené z nižších úrovní vzorů extrahovaných první vrstvou. To nás přivádí k pojmu **vícevrstvé RNN**, které se skládají ze dvou nebo více rekurentních sítí, kde výstup předchozí vrstvy je předán jako vstup další vrstvě.\n",
"\n",
"![Obrázek znázorňující vícevrstvou dlouhodobou krátkodobou paměťovou RNN](../../../../../translated_images/cs/multi-layer-lstm.dd975e29bb2a59fe.webp)\n",
"\n",
"*Obrázek z [tohoto skvělého příspěvku](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) od Fernanda Lópeze.*\n",
"\n",
"Keras usnadňuje konstrukci těchto sítí, protože stačí přidat více rekurentních vrstev do modelu. U všech vrstev kromě poslední je třeba specifikovat parametr `return_sequences=True`, protože potřebujeme, aby vrstva vracela všechny mezistavy, a nejen konečný stav rekurentního výpočtu.\n",
"\n",
"Postavme dvouvrstvou obousměrnou LSTM pro náš klasifikační problém.\n",
"\n",
"> **Note** tento kód opět trvá poměrně dlouho, než se dokončí, ale poskytuje nám nejvyšší přesnost, jakou jsme dosud viděli. Možná tedy stojí za to počkat a podívat se na výsledek.\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 pro jiné úkoly\n",
"\n",
"Doposud jsme se zaměřovali na používání RNN k klasifikaci sekvencí textu. Ale zvládnou mnohem více úkolů, jako je generování textu a strojový překlad — těmto úkolům se budeme věnovat v další jednotce.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Upozornění**: \nTento dokument byl přeložen pomocí služby pro automatický překlad [Co-op Translator](https://github.com/Azure/co-op-translator). I když se snažíme o co největší přesnost, mějte prosím na paměti, že automatické překlady mohou obsahovat chyby nebo nepřesnosti. Za autoritativní zdroj by měl být považován původní dokument v jeho původním jazyce. Pro důležité informace doporučujeme profesionální lidský překlad. Neodpovídáme za žádná nedorozumění nebo nesprávné výklady vyplývající z použití tohoto překladu.\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-29T16:16:12+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNTF.ipynb",
"language_code": "cs"
}
},
"nbformat": 4,
"nbformat_minor": 4
}