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

460 lines
31 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/sr/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",
"> **Напомена**: Познато је да одређене верзије 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) како бисмо смањили димензионалност векторa речи, затим RNN слој, и на крају `Dense` класификатор.\n",
"\n",
"> **Напомена**: У случајевима када димензионалност није толико висока, на пример када се користи токенизација на нивоу карактера, може имати смисла директно проследити једно-вруће кодиране токене у 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` слој аутоматски додаје токене за попуњавање (padding) секвенцама променљиве дужине у мини серијама. Испоставља се да ти токени такође учествују у тренингу, што може закомпликовати конвергенцију модела.\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",
"> **Напомена**: Овај тренинг ће трајати око 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",
"> **Напомена**: Да ли сте приметили да користимо векторизатор који је трениран на насловима вести, а не на целокупном тексту чланка? Потенцијално, ово може довести до тога да неки токени буду игнорисани, па је боље поново тренирати векторизатор. Ипак, то може имати веома мали утицај, тако да ћемо се ради једноставности држати претходно тренираног векторизатора.\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/) од Christopher Olah.\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": [
"## Двосмерни и вишеслојни РНН-ови\n",
"\n",
"У нашим досадашњим примерима, рекурентне мреже раде од почетка секвенце до краја. Ово нам делује природно јер прати исти смер у којем читамо или слушамо говор. Међутим, за сценарије који захтевају насумичан приступ улазној секвенци, има више смисла покренути рекурентне прорачуне у оба смера. РНН-ови који омогућавају прорачуне у оба смера називају се **двосмерни** РНН-ови, и могу се креирати тако што се рекурентни слој обмота посебним `Bidirectional` слојем.\n",
"\n",
"> **Note**: `Bidirectional` слој прави две копије слоја унутар себе и поставља својство `go_backwards` једне од тих копија на `True`, чиме се омогућава да иде у супротном смеру дуж секвенце.\n",
"\n",
"Рекурентне мреже, било једносмерне или двосмерне, хватају обрасце унутар секвенце и чувају их у векторима стања или их враћају као излаз. Као и код конволуционих мрежа, можемо изградити још један рекурентни слој након првог како бисмо ухватили обрасце вишег нивоа, изграђене од образаца нижег нивоа које је извукао први слој. Ово нас доводи до појма **вишеслојног РНН-а**, који се састоји од два или више рекурентних мрежа, где се излаз претходног слоја прослеђује следећем слоју као улаз.\n",
"\n",
"![Слика која приказује вишеслојни дугорочно-краткорочно-меморијски РНН](../../../../../translated_images/sr/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": [
"## РНМ за друге задатке\n",
"\n",
"До сада смо се фокусирали на коришћење РНМ-а за класификацију текстуалних секвенци. Али они могу обрађивати много више задатака, као што су генерисање текста и машински превод — те задатке ћемо размотрити у наредној јединици.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Одрицање од одговорности**: \nОвај документ је преведен коришћењем услуге за превођење помоћу вештачке интелигенције [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:57:42+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNTF.ipynb",
"language_code": "sr"
}
},
"nbformat": 4,
"nbformat_minor": 4
}