{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Generativne mreže\n", "\n", "Rekurentne nevronske mreže (RNN) in njihove različice z vrati, kot so celice Long Short Term Memory (LSTM) in Gated Recurrent Units (GRU), omogočajo modeliranje jezika, tj. lahko se naučijo vrstnega reda besed in napovedujejo naslednjo besedo v zaporedju. To nam omogoča uporabo RNN za **generativne naloge**, kot so običajno generiranje besedila, strojno prevajanje in celo opisovanje slik.\n", "\n", "V arhitekturi RNN, ki smo jo obravnavali v prejšnji enoti, je vsaka enota RNN ustvarila naslednje skrito stanje kot izhod. Vendar pa lahko vsaki rekurentni enoti dodamo še en izhod, kar nam omogoča, da ustvarimo **zaporedje** (ki je enako dolžini izvirnega zaporedja). Poleg tega lahko uporabimo RNN enote, ki ne sprejemajo vhoda na vsakem koraku, temveč le začetni vektorski stanji, nato pa ustvarijo zaporedje izhodov.\n", "\n", "V tem zvezku se bomo osredotočili na preproste generativne modele, ki nam pomagajo ustvarjati besedilo. Za enostavnost bomo zgradili **mrežo na ravni znakov**, ki generira besedilo črko za črko. Med učenjem moramo vzeti neko besedilno zbirko in jo razdeliti na zaporedja črk.\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": [ "## Gradnja besedišča znakov\n", "\n", "Za gradnjo generativne mreže na ravni znakov moramo besedilo razdeliti na posamezne znake namesto besed. To lahko dosežemo z definiranjem drugačnega tokenizatorja:\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": [ "Poglejmo primer, kako lahko kodiramo besedilo iz našega nabora podatkov:\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": [ "## Učenje generativnega RNN\n", "\n", "Način, kako bomo učili RNN za generiranje besedila, je naslednji. Na vsakem koraku bomo vzeli zaporedje znakov dolžine `nchars` in mreži naročili, naj za vsak vhodni znak ustvari naslednji izhodni znak:\n", "\n", "![Slika, ki prikazuje primer generiranja besede 'HELLO' z RNN.](../../../../../translated_images/sl/rnn-generate.56c54afb52f9781d.webp)\n", "\n", "Odvisno od dejanskega scenarija bomo morda želeli vključiti tudi nekatere posebne znake, kot je *konec zaporedja* ``. V našem primeru želimo mrežo naučiti generiranja neskončnega besedila, zato bomo določili, da je velikost vsakega zaporedja enaka `nchars` znakom. Posledično bo vsak učni primer sestavljen iz `nchars` vhodov in `nchars` izhodov (kar je vhodno zaporedje, premaknjeno za en simbol v levo). Miniserija bo sestavljena iz več takšnih zaporedij.\n", "\n", "Način, kako bomo generirali miniserije, je, da vzamemo vsak novičarski tekst dolžine `l` in iz njega ustvarimo vse možne kombinacije vhod-izhod (takšnih kombinacij bo `l-nchars`). Te bodo tvorile eno miniserijo, velikost miniserij pa bo pri vsakem učnem koraku različna.\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": [ "Zdaj bomo definirali generatorno mrežo. Lahko temelji na katerikoli ponavljajoči se celici, o katerih smo govorili v prejšnji enoti (preprosta, LSTM ali GRU). V našem primeru bomo uporabili LSTM.\n", "\n", "Ker mreža sprejema znake kot vhod, velikost besedišča pa je precej majhna, ne potrebujemo vgradne plasti; enovrstni kodiran vhod lahko neposredno preide v LSTM celico. Vendar pa, ker posredujemo številke znakov kot vhod, jih moramo pred posredovanjem v LSTM enovrstno kodirati. To se izvede z uporabo funkcije `one_hot` med prehodom `forward`. Izhodni kodirnik bo linearna plast, ki bo pretvorila skrito stanje v enovrstno kodiran izhod.\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": [ "Med usposabljanjem želimo imeti možnost vzorčenja generiranega besedila. Da bi to dosegli, bomo definirali funkcijo `generate`, ki bo ustvarila izhodni niz dolžine `size`, začenši z začetnim nizom `start`.\n", "\n", "Postopek deluje na naslednji način. Najprej bomo celoten začetni niz poslali skozi mrežo, kjer bomo dobili izhodno stanje `s` in naslednji napovedani znak `out`. Ker je `out` enovročno kodiran (one-hot encoded), uporabimo `argmax`, da dobimo indeks znaka `nc` v besedišču, nato pa uporabimo `itos`, da ugotovimo dejanski znak in ga dodamo v rezultatni seznam znakov `chars`. Ta postopek generiranja enega znaka ponovimo `size`-krat, da ustvarimo zahtevano število znakov.\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": [ "Zdaj pa začnimo z usposabljanjem! Zanka za usposabljanje je skoraj enaka kot v vseh naših prejšnjih primerih, vendar namesto natančnosti vsakih 1000 epohov izpišemo vzorčno generirano besedilo.\n", "\n", "Posebno pozornost je treba nameniti načinu izračuna izgube. Izgubo moramo izračunati glede na enovrstno kodiran izhod `out` in pričakovano besedilo `text_out`, ki je seznam indeksov znakov. Na srečo funkcija `cross_entropy` pričakuje nenormaliziran izhod mreže kot prvi argument in številko razreda kot drugi argument, kar je točno tisto, kar imamo. Prav tako samodejno izvaja povprečenje glede na velikost minibatcha.\n", "\n", "Usposabljanje omejimo tudi na `samples_to_train` vzorce, da ne čakamo predolgo. Spodbujamo vas, da eksperimentirate in poskusite daljše usposabljanje, morda za več epohov (v tem primeru bi morali ustvariti dodatno zanko okoli te kode).\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": [ "Ta primer že ustvarja precej dober tekst, vendar ga je mogoče izboljšati na več načinov:\n", "\n", "* **Boljša priprava minibatch-ov**. Način, kako smo pripravili podatke za učenje, je bil, da smo iz enega vzorca ustvarili en minibatch. To ni idealno, saj so minibatch-i različnih velikosti, nekateri pa sploh ne morejo biti ustvarjeni, ker je besedilo krajše od `nchars`. Poleg tega majhni minibatch-i ne obremenijo GPU dovolj učinkovito. Bolj smiselno bi bilo vzeti en velik kos besedila iz vseh vzorcev, nato ustvariti vse pare vhod-izhod, jih premešati in ustvariti minibatch-e enake velikosti.\n", "\n", "* **Večplastni LSTM**. Smiselno je poskusiti z 2 ali 3 plastmi LSTM celic. Kot smo omenili v prejšnji enoti, vsaka plast LSTM izlušči določene vzorce iz besedila, in pri generatorju na ravni znakov lahko pričakujemo, da bo nižja raven LSTM odgovorna za izluščanje zlogov, višje ravni pa za besede in besedne zveze. To je mogoče preprosto implementirati z dodajanjem parametra za število plasti v konstruktor LSTM.\n", "\n", "* Prav tako bi morda želeli eksperimentirati z **GRU enotami** in preveriti, katere delujejo bolje, ter z **različnimi velikostmi skritih plasti**. Prevelika skrita plast lahko privede do overfittinga (npr. mreža se bo naučila točno določenega besedila), premajhna velikost pa morda ne bo dala dobrih rezultatov.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Mehko generiranje besedila in temperatura\n", "\n", "V prejšnji definiciji funkcije `generate` smo vedno izbrali znak z najvišjo verjetnostjo kot naslednji znak v generiranem besedilu. To je pogosto povzročilo, da se je besedilo \"ponavljalo\" med istimi zaporedji znakov znova in znova, kot v tem primeru:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "Če pa pogledamo porazdelitev verjetnosti za naslednji znak, lahko opazimo, da razlika med nekaj najvišjimi verjetnostmi ni velika, npr. en znak ima verjetnost 0,2, drugi pa 0,19 itd. Na primer, ko iščemo naslednji znak v zaporedju '*play*', je lahko naslednji znak prav tako presledek ali **e** (kot v besedi *player*).\n", "\n", "To nas pripelje do zaključka, da ni vedno \"pravično\" izbrati znaka z višjo verjetnostjo, saj lahko izbira drugega najverjetnejšega znaka še vedno vodi do smiselnega besedila. Bolj smiselno je **vzeti vzorec** znakov iz porazdelitve verjetnosti, ki jo poda izhod mreže.\n", "\n", "To vzorčenje lahko izvedemo z uporabo funkcije `multinomial`, ki implementira tako imenovano **multinomsko porazdelitev**. Funkcija, ki implementira to **mehko** generiranje besedila, je definirana spodaj:\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": [ "Predstavili smo še en parameter, imenovan **temperatura**, ki se uporablja za označevanje, kako strogo se moramo držati najvišje verjetnosti. Če je temperatura 1,0, izvajamo pošteno multinomialno vzorčenje, in ko temperatura gre proti neskončnosti - vse verjetnosti postanejo enake, naključno izberemo naslednji znak. V spodnjem primeru lahko opazimo, da besedilo postane nesmiselno, ko preveč povečamo temperaturo, in spominja na \"ciklirano\" težko generirano besedilo, ko se približa 0.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Omejitev odgovornosti**: \nTa dokument je bil preveden z uporabo storitve za strojno prevajanje [Co-op Translator](https://github.com/Azure/co-op-translator). Čeprav si prizadevamo za natančnost, vas prosimo, da upoštevate, da lahko avtomatizirani prevodi vsebujejo napake ali netočnosti. Izvirni dokument v njegovem izvirnem jeziku je treba obravnavati kot avtoritativni vir. Za ključne informacije priporočamo strokovni človeški prevod. Ne prevzemamo odgovornosti za morebitna nesporazumevanja ali napačne razlage, ki izhajajo iz uporabe tega prevoda.\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:59+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "sl" } }, "nbformat": 4, "nbformat_minor": 4 }