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

460 lines
33 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Рекурентни невронни мрежи\n",
"\n",
"В предишния модул разгледахме богати семантични представяния на текст. Архитектурата, която използвахме, улавя обобщеното значение на думите в изречение, но не отчита **реда** на думите, тъй като операцията по обобщаване, която следва вгражданията, премахва тази информация от оригиналния текст. Тъй като тези модели не могат да представят реда на думите, те не могат да решават по-сложни или двусмислени задачи като генериране на текст или отговаряне на въпроси.\n",
"\n",
"За да уловим значението на текстова последователност, ще използваме архитектура на невронна мрежа, наречена **рекурентна невронна мрежа** или RNN. Когато използваме RNN, подаваме изречението си през мрежата, една по една дума (токен), и мрежата произвежда някакво **състояние**, което след това подаваме отново на мрежата заедно със следващия токен.\n",
"\n",
"![Изображение, показващо пример за генериране с рекурентна невронна мрежа.](../../../../../translated_images/bg/rnn.27f5c29c53d727b5.webp)\n",
"\n",
"Дадена входна последователност от токени $X_0,\\dots,X_n$, RNN създава последователност от блокове на невронната мрежа и обучава тази последователност от край до край, използвайки обратно разпространение. Всеки блок на мрежата приема двойка $(X_i,S_i)$ като вход и произвежда $S_{i+1}$ като резултат. Финалното състояние $S_n$ или изходът $Y_n$ се подава към линеен класификатор, за да се произведе резултатът. Всички блокове на мрежата споделят едни и същи тегла и се обучават от край до край с един цикъл на обратно разпространение.\n",
"\n",
"> Фигурата по-горе показва рекурентна невронна мрежа в разгъната форма (вляво) и в по-компактна рекурентна форма (вдясно). Важно е да се разбере, че всички клетки на RNN имат едни и същи **споделени тегла**.\n",
"\n",
"Тъй като векторите на състоянието $S_0,\\dots,S_n$ преминават през мрежата, RNN може да научи последователни зависимости между думите. Например, когато думата *не* се появи някъде в последователността, мрежата може да научи да отрича определени елементи в рамките на вектора на състоянието.\n",
"\n",
"Вътре всяка клетка на RNN съдържа две матрици на тегла: $W_H$ и $W_I$, и отклонение $b$. На всяка стъпка на RNN, при дадени вход $X_i$ и входно състояние $S_i$, изходното състояние се изчислява като $S_{i+1} = f(W_H\\times S_i + W_I\\times X_i+b)$, където $f$ е функция на активация (често $\\tanh$).\n",
"\n",
"> За задачи като генериране на текст (които ще разгледаме в следващия модул) или машинен превод, също искаме да получим някаква изходна стойност на всяка стъпка на RNN. В този случай има и друга матрица $W_O$, а изходът се изчислява като $Y_i=f(W_O\\times S_i+b_O)$.\n",
"\n",
"Нека видим как рекурентните невронни мрежи могат да ни помогнат да класифицираме нашия набор от новини.\n",
"\n",
"> За пясъчната среда трябва да изпълним следващата клетка, за да се уверим, че необходимата библиотека е инсталирана и данните са предварително заредени. Ако работите локално, можете да пропуснете следващата клетка.\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": [
"Когато обучавате големи модели, разпределението на паметта на GPU може да се превърне в проблем. Също така може да се наложи да експериментирате с различни размери на минипартидите, така че данните да се поберат в паметта на GPU, но обучението да бъде достатъчно бързо. Ако изпълнявате този код на собствена машина с GPU, можете да експериментирате с настройването на размера на минипартидите, за да ускорите обучението.\n",
"\n",
"> **Note**: Известно е, че определени версии на драйверите на NVidia не освобождават паметта след обучението на модела. В този тетрадка изпълняваме няколко примера, което може да доведе до изчерпване на паметта в определени конфигурации, особено ако правите собствени експерименти в рамките на същата тетрадка. Ако срещнете странни грешки при стартиране на обучението на модела, може да се наложи да рестартирате ядрото на тетрадката.\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": [
"## Прост класификатор с RNN\n",
"\n",
"При използването на прост RNN, всяка рекурентна единица представлява проста линейна мрежа, която приема входен вектор и вектор на състоянието и произвежда нов вектор на състоянието. В Keras това може да бъде представено чрез слоя `SimpleRNN`.\n",
"\n",
"Въпреки че можем директно да подаваме токени, кодирани с one-hot, към слоя RNN, това не е добра идея заради тяхната висока размерност. Затова ще използваме слой за вграждане (embedding layer), за да намалим размерността на векторите на думите, следван от слой RNN и накрая класификатор `Dense`.\n",
"\n",
"> **Note**: В случаи, когато размерността не е толкова висока, например при използване на токенизация на ниво символи, може да има смисъл да се подават токени, кодирани с one-hot, директно в клетката на 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": [
"> **Забележка:** Тук използваме нетрениран слой за вграждане за простота, но за по-добри резултати можем да използваме предварително обучен слой за вграждане с Word2Vec, както беше описано в предишния модул. Би било добра практика за вас да адаптирате този код, за да работи с предварително обучени вграждания.\n",
"\n",
"Сега нека обучим нашата RNN. RNN моделите като цяло са доста трудни за обучение, защото когато клетките на RNN се разгънат по дължината на последователността, броят на слоевете, участващи в обратното разпространение, става доста голям. Затова трябва да изберем по-малка скорост на обучение и да тренираме мрежата върху по-голям набор от данни, за да постигнем добри резултати. Това може да отнеме доста време, така че използването на GPU е за предпочитане.\n",
"\n",
"За да ускорим процеса, ще обучим RNN модела само върху заглавия на новини, като пропуснем описанията. Можете да опитате да тренирате с описания и да видите дали ще успеете да накарате модела да се обучи.\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": [
"> **Забележка** че точността вероятно ще бъде по-ниска тук, защото обучаваме само върху заглавия на новини.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Преразглеждане на последователности с променлива дължина\n",
"\n",
"Не забравяйте, че слоят `TextVectorization` автоматично ще допълва последователностите с променлива дължина в минипартида с помощта на специални символи за запълване. Оказва се, че тези символи също участват в обучението и могат да усложнят сближаването на модела.\n",
"\n",
"Има няколко подхода, които можем да използваме, за да минимизираме количеството запълване. Един от тях е да пренаредим набора от данни по дължина на последователността и да групираме всички последователности по размер. Това може да се направи с помощта на функцията `tf.data.experimental.bucket_by_sequence_length` (вижте [документацията](https://www.tensorflow.org/api_docs/python/tf/data/experimental/bucket_by_sequence_length)).\n",
"\n",
"Друг подход е използването на **маскиране**. В Keras някои слоеве поддържат допълнителен вход, който показва кои символи трябва да се вземат предвид по време на обучението. За да включим маскирането в нашия модел, можем или да добавим отделен слой `Masking` ([документация](https://keras.io/api/layers/core_layers/masking/)), или да зададем параметъра `mask_zero=True` на нашия слой `Embedding`.\n",
"\n",
"> **Note**: Това обучение ще отнеме около 5 минути за завършване на една епоха върху целия набор от данни. Можете да прекъснете обучението по всяко време, ако загубите търпение. Също така можете да ограничите количеството данни, използвани за обучение, като добавите `.take(...)` след наборите от данни `ds_train` и `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": [
"Сега, когато използваме маскиране, можем да обучим модела върху целия набор от заглавия и описания.\n",
"\n",
"> **Note**: Забелязахте ли, че използваме векторизатор, обучен върху заглавията на новините, а не върху цялото съдържание на статията? Потенциално това може да доведе до игнориране на някои от токените, така че е по-добре да се преобучи векторизаторът. Въпреки това, ефектът може да е много малък, затова ще се придържаме към предварително обучения векторизатор за по-голяма простота.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## LSTM: Дългосрочна краткосрочна памет\n",
"\n",
"Един от основните проблеми на RNN е **изчезващите градиенти**. RNN могат да бъдат доста дълги и може да им е трудно да пренасят градиентите обратно до първия слой на мрежата по време на обратното разпространение. Когато това се случи, мрежата не може да научи връзките между отдалечени токени. Един от начините за избягване на този проблем е въвеждането на **явно управление на състоянието** чрез използване на **гейтове**. Двете най-често срещани архитектури, които използват гейтове, са **дългосрочна краткосрочна памет** (LSTM) и **гейтирана релейна единица** (GRU). Тук ще разгледаме LSTM.\n",
"\n",
"![Изображение, показващо пример за клетка с дългосрочна краткосрочна памет](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"LSTM мрежата е организирана по начин, подобен на RNN, но има две състояния, които се предават от слой на слой: действителното състояние $c$ и скритият вектор $h$. Във всяка единица скритият вектор $h_{t-1}$ се комбинира с входа $x_t$, и заедно те контролират какво се случва със състоянието $c_t$ и изхода $h_{t}$ чрез **гейтове**. Всеки гейт има сигмоидна активация (изход в диапазона $[0,1]$), която може да се разглежда като побитова маска, когато се умножи със състоянието. LSTM имат следните гейтове (отляво надясно на изображението по-горе):\n",
"* **гейт за забравяне**, който определя кои компоненти на вектора $c_{t-1}$ трябва да забравим и кои да предадем нататък.\n",
"* **входен гейт**, който определя колко информация от входния вектор и предишния скрит вектор трябва да бъде включена в състоянието.\n",
"* **изходен гейт**, който взема новото състояние и решава кои от неговите компоненти ще бъдат използвани за създаване на новия скрит вектор $h_t$.\n",
"\n",
"Компонентите на състоянието $c$ могат да се разглеждат като флагове, които могат да бъдат включвани и изключвани. Например, когато срещнем името *Алис* в последователността, предполагаме, че става въпрос за жена, и вдигаме флаг в състоянието, който казва, че имаме женско съществително в изречението. Когато по-нататък срещнем думите *и Том*, ще вдигнем флаг, който казва, че имаме множествено число. Така чрез манипулиране на състоянието можем да следим граматическите свойства на изречението.\n",
"\n",
"> **Note**: Ето един чудесен ресурс за разбиране на вътрешната структура на LSTM: [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) от Кристофър Ола.\n",
"\n",
"Въпреки че вътрешната структура на LSTM клетката може да изглежда сложна, Keras скрива тази имплементация вътре в слоя `LSTM`, така че единственото, което трябва да направим в примера по-горе, е да заменим рекурентния слой:\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": [
"## Двунасочни и многослойни RNN\n",
"\n",
"В досегашните ни примери рекурентните мрежи работят от началото на последователността до края. Това ни се струва естествено, защото следва същата посока, в която четем или слушаме реч. Въпреки това, за сценарии, които изискват произволен достъп до входната последователност, е по-логично рекурентните изчисления да се извършват и в двете посоки. RNN, които позволяват изчисления в двете посоки, се наричат **двунасочни** RNN, и могат да бъдат създадени чрез обвиване на рекурентния слой със специален слой `Bidirectional`.\n",
"\n",
"> **Note**: Слоят `Bidirectional` прави две копия на слоя в него и задава свойството `go_backwards` на едно от тези копия на `True`, което го кара да се движи в обратната посока по последователността.\n",
"\n",
"Рекурентните мрежи, еднопосочни или двунасочни, улавят модели в рамките на последователността и ги съхраняват в състояния или ги връщат като изход. Както при конволюционните мрежи, можем да изградим друг рекурентен слой след първия, за да уловим модели на по-високо ниво, изградени от модели на по-ниско ниво, извлечени от първия слой. Това ни води до понятието за **многослойна RNN**, която се състои от две или повече рекурентни мрежи, където изходът на предишния слой се предава на следващия слой като вход.\n",
"\n",
"![Изображение, показващо многослойна дългосрочно-краткосрочна памет RNN](../../../../../translated_images/bg/multi-layer-lstm.dd975e29bb2a59fe.webp)\n",
"\n",
"*Снимка от [този чудесен пост](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) от Фернандо Лопес.*\n",
"\n",
"Keras прави изграждането на тези мрежи лесна задача, защото просто трябва да добавите повече рекурентни слоеве към модела. За всички слоеве, освен последния, трябва да зададем параметъра `return_sequences=True`, защото ни е необходимо слоят да връща всички междинни състояния, а не само крайното състояние на рекурентното изчисление.\n",
"\n",
"Нека изградим двуслоен двунасочен LSTM за нашия проблем с класификацията.\n",
"\n",
"> **Note** този код отново отнема доста време за изпълнение, но ни дава най-високата точност, която сме виждали досега. Така че може би си струва да изчакаме и да видим резултата.\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": [
"## RNNs за други задачи\n",
"\n",
"Досега се концентрирахме върху използването на RNNs за класифициране на текстови последователности. Но те могат да се справят с много повече задачи, като генериране на текст и машинен превод — ще разгледаме тези задачи в следващата единица.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Отказ от отговорност**: \nТози документ е преведен с помощта на AI услуга за превод [Co-op Translator](https://github.com/Azure/co-op-translator). Въпреки че се стремим към точност, моля, имайте предвид, че автоматизираните преводи може да съдържат грешки или неточности. Оригиналният документ на неговия роден език трябва да се счита за авторитетен източник. За критична информация се препоръчва професионален човешки превод. Не носим отговорност за недоразумения или погрешни интерпретации, произтичащи от използването на този превод.\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-30T00:56:44+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNTF.ipynb",
"language_code": "bg"
}
},
"nbformat": 4,
"nbformat_minor": 4
}