{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Generativne mreže\n", "\n", "Rekurentne neuronske mreže (RNN-ovi) i njihove varijante s kontroliranim ćelijama, poput ćelija s dugoročnim i kratkoročnim pamćenjem (LSTM-ovi) i jedinica s kontroliranim povratnim signalom (GRU-ovi), omogućile su modeliranje jezika, tj. mogu naučiti redoslijed riječi i pružiti predviđanja za sljedeću riječ u nizu. To nam omogućuje korištenje RNN-ova za **generativne zadatke**, poput običnog generiranja teksta, strojnog prevođenja, pa čak i opisivanja slika.\n", "\n", "U RNN arhitekturi koju smo raspravili u prethodnoj jedinici, svaka RNN jedinica proizvodila je sljedeće skriveno stanje kao izlaz. Međutim, možemo dodati i drugi izlaz svakoj rekurentnoj jedinici, što bi nam omogućilo da dobijemo **niz** (koji je jednake duljine kao i izvorni niz). Štoviše, možemo koristiti RNN jedinice koje ne primaju ulaz na svakom koraku, već samo uzimaju neki početni vektor stanja i zatim proizvode niz izlaza.\n", "\n", "U ovom ćemo bilježniku fokus staviti na jednostavne generativne modele koji nam pomažu generirati tekst. Radi jednostavnosti, izgradit ćemo **mrežu na razini znakova**, koja generira tekst slovo po slovo. Tijekom treniranja, potrebno je uzeti neki korpus teksta i podijeliti ga na nizove slova.\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": [ "## Izgradnja vokabulara znakova\n", "\n", "Za izgradnju generativne mreže na razini znakova, potrebno je podijeliti tekst na pojedinačne znakove umjesto na riječi. Ovo se može postići definiranjem drugačijeg tokenizatora:\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": [ "Pogledajmo primjer kako možemo kodirati tekst iz našeg skupa podataka:\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": [ "## Treniranje generativne RNN\n", "\n", "Način na koji ćemo trenirati RNN za generiranje teksta je sljedeći. U svakom koraku uzet ćemo niz znakova duljine `nchars` i tražiti od mreže da generira sljedeći izlazni znak za svaki ulazni znak:\n", "\n", "![Slika koja prikazuje primjer RNN generiranja riječi 'HELLO'.](../../../../../translated_images/hr/rnn-generate.56c54afb52f9781d.webp)\n", "\n", "Ovisno o stvarnom scenariju, možda ćemo htjeti uključiti i neke posebne znakove, poput *kraj-sekvence* ``. U našem slučaju, želimo trenirati mrežu za beskonačno generiranje teksta, stoga ćemo fiksirati veličinu svakog niza na `nchars` tokena. Posljedično, svaki primjer za treniranje sastojat će se od `nchars` ulaza i `nchars` izlaza (što je ulazni niz pomaknut za jedan simbol ulijevo). Minibatch će se sastojati od nekoliko takvih nizova.\n", "\n", "Način na koji ćemo generirati minibatcheve je da uzmemo svaki tekst vijesti duljine `l` i iz njega generiramo sve moguće kombinacije ulaz-izlaz (bit će ih `l-nchars` takvih kombinacija). Te kombinacije činit će jedan minibatch, a veličina minibatcheva bit će različita u svakom koraku treniranja.\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": [ "Sada ćemo definirati generator mrežu. Ona može biti bazirana na bilo kojoj rekurentnoj ćeliji o kojoj smo raspravljali u prethodnoj jedinici (jednostavna, LSTM ili GRU). U našem primjeru koristit ćemo LSTM.\n", "\n", "Budući da mreža prima znakove kao ulaz, a veličina vokabulara je prilično mala, ne trebamo sloj za ugradnju (embedding layer); ulaz kodiran u one-hot formatu može izravno ići u LSTM ćeliju. Međutim, budući da prosljeđujemo brojeve znakova kao ulaz, moramo ih kodirati u one-hot formatu prije nego ih proslijedimo u LSTM. To se postiže pozivanjem funkcije `one_hot` tijekom `forward` prolaza. Izlazni enkoder bit će linearni sloj koji će pretvoriti skriveno stanje u izlaz kodiran u one-hot formatu.\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": [ "Tijekom treniranja želimo imati mogućnost uzorkovanja generiranog teksta. Da bismo to postigli, definirat ćemo funkciju `generate` koja će proizvesti izlazni niz duljine `size`, počevši od početnog niza `start`.\n", "\n", "Način na koji to funkcionira je sljedeći. Prvo ćemo proslijediti cijeli početni niz kroz mrežu i uzeti izlazno stanje `s` i sljedeći predviđeni znak `out`. Budući da je `out` kodiran kao one-hot, uzimamo `argmax` kako bismo dobili indeks znaka `nc` u vokabularu, a zatim koristimo `itos` kako bismo odredili stvarni znak i dodali ga u rezultirajući popis znakova `chars`. Ovaj proces generiranja jednog znaka ponavlja se `size` puta kako bi se generirao potreban broj znakova.\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": [ "Sad krenimo s treningom! Petlja za trening gotovo je ista kao u svim našim prethodnim primjerima, ali umjesto točnosti ispisujemo uzorkovani generirani tekst svakih 1000 epoha.\n", "\n", "Posebnu pažnju treba obratiti na način na koji računamo gubitak. Moramo izračunati gubitak koristeći jednoznačno kodirani izlaz `out` i očekivani tekst `text_out`, koji je popis indeksa znakova. Srećom, funkcija `cross_entropy` očekuje nenormalizirani izlaz mreže kao prvi argument, a broj klase kao drugi, što je upravo ono što imamo. Također automatski provodi prosjek preko veličine minibatcha.\n", "\n", "Trening također ograničavamo na `samples_to_train` uzoraka, kako ne bismo predugo čekali. Potičemo vas da eksperimentirate i pokušate s duljim treningom, moguće kroz nekoliko epoha (u tom slučaju trebali biste stvoriti dodatnu petlju oko ovog koda).\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": [ "Ovaj primjer već generira prilično dobar tekst, ali može se dodatno poboljšati na nekoliko načina:\n", "* **Bolja generacija minibatchova**. Način na koji smo pripremili podatke za treniranje bio je generiranje jednog minibatcha iz jednog uzorka. To nije idealno, jer minibatchevi imaju različite veličine, a neki se čak ne mogu generirati jer je tekst manji od `nchars`. Osim toga, mali minibatchevi ne iskorištavaju GPU dovoljno učinkovito. Pametnije bi bilo uzeti jedan veliki dio teksta iz svih uzoraka, zatim generirati sve ulazno-izlazne parove, promiješati ih i generirati minibatcheve jednake veličine.\n", "* **Višeslojni LSTM**. Ima smisla isprobati 2 ili 3 sloja LSTM ćelija. Kao što smo spomenuli u prethodnoj jedinici, svaki sloj LSTM-a izvlači određene obrasce iz teksta, a u slučaju generatora na razini znakova možemo očekivati da će niži LSTM sloj biti odgovoran za izvlačenje slogova, a viši slojevi - za riječi i kombinacije riječi. Ovo se jednostavno može implementirati prosljeđivanjem parametra broja slojeva konstruktoru LSTM-a.\n", "* Također biste mogli eksperimentirati s **GRU jedinicama** i vidjeti koje daju bolje rezultate, kao i s **različitim veličinama skrivenih slojeva**. Prevelik skriveni sloj može dovesti do prekomjernog prilagođavanja (npr. mreža će naučiti točan tekst), dok premala veličina možda neće dati dobar rezultat.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Generiranje mekog teksta i temperatura\n", "\n", "U prethodnoj definiciji `generate`, uvijek smo uzimali znak s najvećom vjerojatnošću kao sljedeći znak u generiranom tekstu. To je često rezultiralo time da se tekst \"vrtio\" između istih sekvenci znakova iznova i iznova, kao u ovom primjeru:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "Međutim, ako pogledamo raspodjelu vjerojatnosti za sljedeći znak, može se dogoditi da razlika između nekoliko najvećih vjerojatnosti nije velika, npr. jedan znak može imati vjerojatnost 0.2, drugi 0.19, itd. Na primjer, kada tražimo sljedeći znak u sekvenci '*play*', sljedeći znak može jednako dobro biti razmak ili **e** (kao u riječi *player*).\n", "\n", "To nas dovodi do zaključka da nije uvijek \"pravedno\" odabrati znak s većom vjerojatnošću, jer odabir drugog po redu također može dovesti do smislenog teksta. Mudrije je **uzorkovati** znakove iz raspodjele vjerojatnosti koju daje izlaz mreže.\n", "\n", "Ovo uzorkovanje može se provesti pomoću funkcije `multinomial` koja implementira tzv. **multinomialnu raspodjelu**. Funkcija koja implementira ovo **meko** generiranje teksta definirana je u nastavku:\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": [ "Uveli smo još jedan parametar nazvan **temperature**, koji se koristi za označavanje koliko čvrsto trebamo slijediti najveću vjerojatnost. Ako je temperatura 1.0, radimo pošteno multinomijalno uzorkovanje, a kada temperatura ide prema beskonačnosti - sve vjerojatnosti postaju jednake i nasumično biramo sljedeći znak. U primjeru ispod možemo primijetiti da tekst postaje besmislen kada previše povećamo temperaturu, a nalikuje \"cikliranom\" teško generiranom tekstu kada se približi 0.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Odricanje od odgovornosti**: \nOvaj dokument je preveden korištenjem AI usluge za prevođenje [Co-op Translator](https://github.com/Azure/co-op-translator). Iako nastojimo osigurati točnost, imajte na umu da automatski prijevodi mogu sadržavati pogreške ili netočnosti. Izvorni dokument na izvornom jeziku treba smatrati mjerodavnim izvorom. Za ključne informacije preporučuje se profesionalni prijevod od strane stručnjaka. Ne preuzimamo odgovornost za bilo kakve nesporazume ili pogrešne interpretacije proizašle iz korištenja ovog prijevoda.\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-30T08:00:17+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "hr" } }, "nbformat": 4, "nbformat_minor": 4 }