{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Генеративне мреже\n", "\n", "Рекурентне неуронске мреже (RNNs) и њихове варијанте са \"гейтованим\" ћелијама, као што су Long Short Term Memory ћелије (LSTMs) и Gated Recurrent Units (GRUs), пружају механизам за моделирање језика, тј. могу да науче редослед речи и дају предвиђања за следећу реч у низу. Ово нам омогућава да користимо RNNs за **генеративне задатке**, као што су обично генерисање текста, машински превод, па чак и генерисање описа слика.\n", "\n", "У RNN архитектури коју смо разматрали у претходној јединици, свака RNN јединица је производила следеће скривено стање као излаз. Међутим, можемо додати још један излаз свакој рекурентној јединици, што би нам омогућило да добијемо **низ** (који је једнаке дужине као оригинални низ). Штавише, можемо користити RNN јединице које не прихватају улаз на сваком кораку, већ само узимају неки почетни вектор стања и затим производе низ излаза.\n", "\n", "У овом нотебуку, фокусираћемо се на једноставне генеративне моделе који нам помажу да генеришемо текст. Ради једноставности, направимо **мрежу на нивоу карактера**, која генерише текст слово по слово. Током тренинга, потребно је узети неки корпус текста и поделити га на низове слова.\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", "import numpy as np\n", "from torchnlp import *\n", "train_dataset,test_dataset,classes,vocab = load_dataset()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Изградња вокабулара карактера\n", "\n", "Да бисмо изградили генеративну мрежу на нивоу карактера, потребно је да текст поделимо на појединачне карактере уместо на речи. Ово се може урадити дефинисањем другачијег токенизатора:\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Vocabulary size = 82\n", "Encoding of 'a' is 1\n", "Character with code 13 is c\n" ] } ], "source": [ "def char_tokenizer(words):\n", " return list(words) #[word for word in words]\n", "\n", "counter = collections.Counter()\n", "for (label, line) in train_dataset:\n", " counter.update(char_tokenizer(line))\n", "vocab = torchtext.vocab.vocab(counter)\n", "\n", "vocab_size = len(vocab)\n", "print(f\"Vocabulary size = {vocab_size}\")\n", "print(f\"Encoding of 'a' is {vocab.get_stoi()['a']}\")\n", "print(f\"Character with code 13 is {vocab.get_itos()[13]}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Хајде да видимо пример како можемо да кодирамо текст из нашег скупа података:\n" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "tensor([ 0, 1, 2, 2, 3, 4, 5, 6, 3, 7, 8, 1, 9, 10, 3, 11, 2, 1,\n", " 12, 3, 7, 1, 13, 14, 3, 15, 16, 5, 17, 3, 5, 18, 8, 3, 7, 2,\n", " 1, 13, 14, 3, 19, 20, 8, 21, 5, 8, 9, 10, 22, 3, 20, 8, 21, 5,\n", " 8, 9, 10, 3, 23, 3, 4, 18, 17, 9, 5, 23, 10, 8, 2, 2, 8, 9,\n", " 10, 24, 3, 0, 1, 2, 2, 3, 4, 5, 9, 8, 8, 5, 25, 10, 3, 26,\n", " 12, 27, 16, 26, 2, 27, 16, 28, 29, 30, 1, 16, 26, 3, 17, 31, 3, 21,\n", " 2, 5, 9, 1, 23, 13, 32, 16, 27, 13, 10, 24, 3, 1, 9, 8, 3, 10,\n", " 8, 8, 27, 16, 28, 3, 28, 9, 8, 8, 16, 3, 1, 28, 1, 27, 16, 6])" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def enc(x):\n", " return torch.LongTensor(encode(x,voc=vocab,tokenizer=char_tokenizer))\n", "\n", "enc(train_dataset[0][1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Тренирање генеративне RNN\n", "\n", "Начин на који ћемо тренирати RNN за генерисање текста је следећи. На сваком кораку, узимамо секвенцу карактера дужине `nchars` и тражимо од мреже да генерише следећи излазни карактер за сваки улазни карактер:\n", "\n", "![Слика која приказује пример RNN генерисања речи 'HELLO'.](../../../../../translated_images/sr/rnn-generate.56c54afb52f9781d.webp)\n", "\n", "У зависности од конкретног сценарија, можда ћемо желети да укључимо неке посебне карактере, као што је *крај секвенце* ``. У нашем случају, желимо само да обучимо мрежу за бесконачно генерисање текста, па ћемо фиксирати величину сваке секвенце да буде једнака `nchars` токенима. Сходно томе, сваки пример за тренирање ће се састојати од `nchars` улаза и `nchars` излаза (што је улазна секвенца померена за један симбол улево). Минибатч ће се састојати од неколико таквих секвенци.\n", "\n", "Начин на који ћемо генерисати минибатчеве је да узмемо сваки текст вести дужине `l` и генеришемо све могуће комбинације улаз-излаз из њега (биће `l-nchars` таквих комбинација). Оне ће чинити један минибатч, а величина минибатчева ће бити различита на сваком кораку тренирања.\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(tensor([[ 0, 1, 2, ..., 28, 29, 30],\n", " [ 1, 2, 2, ..., 29, 30, 1],\n", " [ 2, 2, 3, ..., 30, 1, 16],\n", " ...,\n", " [20, 8, 21, ..., 1, 28, 1],\n", " [ 8, 21, 5, ..., 28, 1, 27],\n", " [21, 5, 8, ..., 1, 27, 16]]),\n", " tensor([[ 1, 2, 2, ..., 29, 30, 1],\n", " [ 2, 2, 3, ..., 30, 1, 16],\n", " [ 2, 3, 4, ..., 1, 16, 26],\n", " ...,\n", " [ 8, 21, 5, ..., 28, 1, 27],\n", " [21, 5, 8, ..., 1, 27, 16],\n", " [ 5, 8, 9, ..., 27, 16, 6]]))" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "nchars = 100\n", "\n", "def get_batch(s,nchars=nchars):\n", " ins = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n", " outs = torch.zeros(len(s)-nchars,nchars,dtype=torch.long,device=device)\n", " for i in range(len(s)-nchars):\n", " ins[i] = enc(s[i:i+nchars])\n", " outs[i] = enc(s[i+1:i+nchars+1])\n", " return ins,outs\n", "\n", "get_batch(train_dataset[0][1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сада ћемо дефинисати генераторску мрежу. Она може бити заснована на било којој рекурентној ћелији коју смо разматрали у претходној јединици (једноставна, LSTM или GRU). У нашем примеру користићемо LSTM.\n", "\n", "Пошто мрежа узима карактере као улаз, а величина вокабулара је прилично мала, не треба нам слој за уграђивање; улаз кодирани у један бит може директно ићи у LSTM ћелију. Међутим, пошто прослеђујемо бројеве карактера као улаз, потребно је да их кодирујемо у један бит пре него што их проследимо LSTM-у. Ово се ради позивом функције `one_hot` током `forward` пролаза. Енкодер излаза биће линеарни слој који ће скривено стање претворити у излаз кодирани у један бит.\n" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "class LSTMGenerator(torch.nn.Module):\n", " def __init__(self, vocab_size, hidden_dim):\n", " super().__init__()\n", " self.rnn = torch.nn.LSTM(vocab_size,hidden_dim,batch_first=True)\n", " self.fc = torch.nn.Linear(hidden_dim, vocab_size)\n", "\n", " def forward(self, x, s=None):\n", " x = torch.nn.functional.one_hot(x,vocab_size).to(torch.float32)\n", " x,s = self.rnn(x,s)\n", " return self.fc(x),s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Током тренинга, желимо да будемо у могућности да узоркујемо генерисани текст. Да бисмо то постигли, дефинисаћемо функцију `generate` која ће производити излазни низ дужине `size`, почевши од почетног низа `start`.\n", "\n", "Начин на који ово функционише је следећи. Прво, проследићемо цео почетни низ кроз мрежу и добити излазно стање `s` и следећи предвиђени карактер `out`. Пошто је `out` једнократно кодирано, узимамо `argmax` да бисмо добили индекс карактера `nc` у вокабулару, и користимо `itos` да бисмо утврдили стварни карактер и додали га резултујућој листи карактера `chars`. Овај процес генерисања једног карактера се понавља `size` пута како би се генерисао потребан број карактера.\n" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def generate(net,size=100,start='today '):\n", " chars = list(start)\n", " out, s = net(enc(chars).view(1,-1).to(device))\n", " for i in range(size):\n", " nc = torch.argmax(out[0][-1])\n", " chars.append(vocab.get_itos()[nc])\n", " out, s = net(nc.view(1,-1),s)\n", " return ''.join(chars)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Хајде да започнемо са тренингом! Петља за тренинг је скоро иста као у свим нашим претходним примерима, али уместо тачности, штампамо узорковани генерисани текст сваких 1000 епоха.\n", "\n", "Посебну пажњу треба обратити на начин на који рачунамо губитак. Потребно је израчунати губитак на основу једнохот-кодованог излаза `out` и очекиваног текста `text_out`, који је листа индекса карактера. Срећом, функција `cross_entropy` очекује ненормализовани излаз мреже као први аргумент, а број класе као други, што је управо оно што имамо. Такође, она аутоматски врши просек преко величине мини-бача.\n", "\n", "Ограничавамо тренинг на `samples_to_train` узорака, како не бисмо предуго чекали. Охрабрујемо вас да експериментишете и пробате дужи тренинг, могуће током неколико епоха (у том случају би требало да направите још једну петљу око овог кода).\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Current loss = 4.398899078369141\n", "today sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr sr s\n", "Current loss = 2.161320447921753\n", "today and to the tor to to the tor to to the tor to to the tor to to the tor to to the tor to to the tor t\n", "Current loss = 1.6722588539123535\n", "today and the court to the could to the could to the could to the could to the could to the could to the c\n", "Current loss = 2.423795223236084\n", "today and a second to the conternation of the conternation of the conternation of the conternation of the \n", "Current loss = 1.702607274055481\n", "today and the company to the company to the company to the company to the company to the company to the co\n", "Current loss = 1.692358136177063\n", "today and the company to the company to the company to the company to the company to the company to the co\n", "Current loss = 1.9722288846969604\n", "today and the control the control the control the control the control the control the control the control \n", "Current loss = 1.8705692291259766\n", "today and the second to the second to the second to the second to the second to the second to the second t\n", "Current loss = 1.7626899480819702\n", "today and a security and a security and a security and a security and a security and a security and a secu\n", "Current loss = 1.5574463605880737\n", "today and the company and the company and the company and the company and the company and the company and \n", "Current loss = 1.5620026588439941\n", "today and the be that the be the be that the be the be that the be the be that the be the be that the be t\n" ] } ], "source": [ "net = LSTMGenerator(vocab_size,64).to(device)\n", "\n", "samples_to_train = 10000\n", "optimizer = torch.optim.Adam(net.parameters(),0.01)\n", "loss_fn = torch.nn.CrossEntropyLoss()\n", "net.train()\n", "for i,x in enumerate(train_dataset):\n", " # x[0] is class label, x[1] is text\n", " if len(x[1])-nchars<10:\n", " continue\n", " samples_to_train-=1\n", " if not samples_to_train: break\n", " text_in, text_out = get_batch(x[1])\n", " optimizer.zero_grad()\n", " out,s = net(text_in)\n", " loss = torch.nn.functional.cross_entropy(out.view(-1,vocab_size),text_out.flatten()) #cross_entropy(out,labels)\n", " loss.backward()\n", " optimizer.step()\n", " if i%1000==0:\n", " print(f\"Current loss = {loss.item()}\")\n", " print(generate(net))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Овај пример већ генерише прилично добар текст, али може се додатно побољшати на неколико начина:\n", "\n", "* **Боља генерација минибатчева**. Начин на који смо припремили податке за тренирање био је да генеришемо један минибач из једног узорка. Ово није идеално, јер су минибачеви различитих величина, а неки од њих чак не могу бити генерисани, јер је текст мањи од `nchars`. Такође, мали минибачеви не оптерећују довољно GPU. Паметније би било узети један велики део текста из свих узорака, затим генерисати све парове улаз-излаз, промешати их и генерисати минибачеве једнаке величине.\n", "\n", "* **Вишеслојни LSTM**. Има смисла пробати са 2 или 3 слоја LSTM ћелија. Као што смо поменули у претходној јединици, сваки слој LSTM-а извлачи одређене обрасце из текста, а у случају генератора на нивоу карактера можемо очекивати да је нижи ниво LSTM-а одговоран за извлачење слогова, а виши нивои - за речи и комбинације речи. Ово се може једноставно имплементирати прослеђивањем параметра броја слојева конструктору LSTM-а.\n", "\n", "* Такође можете експериментисати са **GRU јединицама** и видети које боље раде, као и са **различитим величинама скривених слојева**. Превелики скривени слој може довести до пренапрегнутости (нпр. мрежа ће научити тачан текст), док мања величина можда неће дати добар резултат.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Генерисање меког текста и температура\n", "\n", "У претходној дефиницији функције `generate`, увек смо узимали карактер са највећом вероватноћом као следећи карактер у генерисаном тексту. То је доводило до тога да се текст често \"врти\" између истих секвенци карактера изнова и изнова, као у овом примеру:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "Међутим, ако погледамо расподелу вероватноћа за следећи карактер, може се десити да разлика између неколико највећих вероватноћа није велика, на пример, један карактер може имати вероватноћу 0.2, а други 0.19, итд. На пример, када тражимо следећи карактер у секвенци '*play*', следећи карактер може једнако добро бити размак или **e** (као у речи *player*).\n", "\n", "Ово нас доводи до закључка да није увек \"праведно\" изабрати карактер са већом вероватноћом, јер избор другог највећег може и даље довести до смисленог текста. Паметније је **узорковати** карактере из расподеле вероватноћа коју даје излаз мреже.\n", "\n", "Ово узорковање може се обавити помоћу функције `multinomial` која имплементира такозвану **мултиномијалну расподелу**. Функција која имплементира ово **меко** генерисање текста дефинисана је испод:\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "--- Temperature = 0.3\n", "Today and a company and complete an all the land the restrational the as a security and has provers the pay to and a report and the computer in the stand has filities and working the law the stations for a company and with the company and the final the first company and refight of the state and and workin\n", "\n", "--- Temperature = 0.8\n", "Today he oniis its first to Aus bomblaties the marmation a to manan boogot that pirate assaid a relaid their that goverfin the the Cappets Ecrotional Assonia Cition targets it annight the w scyments Blamity #39;s TVeer Diercheg Reserals fran envyuil that of ster said access what succers of Dour-provelith\n", "\n", "--- Temperature = 1.0\n", "Today holy they a 11 will meda a toket subsuaties, engins for Chanos, they's has stainger past to opening orital his thempting new Nattona was al innerforder advan-than #36;s night year his religuled talitatian what the but with Wednesday to Justment will wemen of Mark CCC Camp as Timed Nae wome a leaders\n", "\n", "--- Temperature = 1.3\n", "Today gpone 2.5 fech atcusion poor cocles toparsdorM.cht Line Pamage put 43 his calt lowed to the book, that has authh-the silia rruch ailing to'ory andhes beutirsimi- Aefffive heading offil an auf eacklets is charged evis, Gunymy oy) Mony has it after-sloythyor loveId out filme, the Natabl -Najuntaxiggs \n", "\n", "--- Temperature = 1.8\n", "Today plary, P.slan chly\\401 mardregationly #39;t 8.1Mide) closes ,filtcon alfly playin roven!\\grea.-QFBEP: Iss onfarchQ/itilia CCf Zivesigntwasta orce.-Peul-aw.uicrin of fuglinfsut aftaningwo, MIEX awayew Aice Woiduar Corvagiugge oppo esig ThusBratourid canthly-RyI.co lagitems\\eexciaishes.conBabntusmor I\n", "\n" ] } ], "source": [ "def generate_soft(net,size=100,start='today ',temperature=1.0):\n", " chars = list(start)\n", " out, s = net(enc(chars).view(1,-1).to(device))\n", " for i in range(size):\n", " #nc = torch.argmax(out[0][-1])\n", " out_dist = out[0][-1].div(temperature).exp()\n", " nc = torch.multinomial(out_dist,1)[0]\n", " chars.append(vocab.get_itos()[nc])\n", " out, s = net(nc.view(1,-1),s)\n", " return ''.join(chars)\n", " \n", "for i in [0.3,0.8,1.0,1.3,1.8]:\n", " print(f\"--- Temperature = {i}\\n{generate_soft(net,size=300,start='Today ',temperature=i)}\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Увели смо још један параметар који се зове **температура**, који се користи да означи колико строго треба да се држимо највеће вероватноће. Ако је температура 1.0, радимо правично мултиномијално узорковање, а када температура иде ка бесконачности - све вероватноће постају једнаке, и насумично бирамо следећи карактер. У примеру испод можемо приметити да текст постаје бесмислен када превише повећамо температуру, и подсећа на \"циклично\" тешко генерисан текст када се приближи 0.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Одрицање од одговорности**: \nОвај документ је преведен коришћењем услуге за превођење помоћу вештачке интелигенције [Co-op Translator](https://github.com/Azure/co-op-translator). Иако се трудимо да обезбедимо тачност, молимо вас да имате у виду да аутоматски преводи могу садржати грешке или нетачности. Оригинални документ на његовом изворном језику треба сматрати ауторитативним извором. За критичне информације препоручује се професионални превод од стране људи. Не преузимамо одговорност за било каква погрешна тумачења или неспоразуме који могу настати услед коришћења овог превода.\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": "7673cd150d96c74c6d6011460094efb4", "translation_date": "2025-08-30T00:39:01+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "sr" } }, "nbformat": 4, "nbformat_minor": 4 }