AI-For-Beginners/translations/sk/lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb

411 lines
21 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Generatívne siete\n",
"\n",
"Rekurentné neurónové siete (RNN) a ich varianty s bránami, ako sú bunky Long Short Term Memory (LSTM) a Gated Recurrent Units (GRU), poskytujú mechanizmus na modelovanie jazyka, t.j. dokážu sa naučiť poradie slov a poskytovať predpovede pre nasledujúce slovo v sekvencii. To nám umožňuje používať RNN na **generatívne úlohy**, ako je bežná generácia textu, strojový preklad a dokonca aj popisovanie obrázkov.\n",
"\n",
"V architektúre RNN, ktorú sme diskutovali v predchádzajúcej jednotke, každá jednotka RNN produkovala ako výstup nasledujúci skrytý stav. Avšak, môžeme tiež pridať ďalší výstup ku každej rekurentnej jednotke, čo nám umožní generovať **sekvenciu** (ktorá má rovnakú dĺžku ako pôvodná sekvencia). Navyše, môžeme použiť jednotky RNN, ktoré neprijímajú vstup na každom kroku, ale len berú nejaký počiatočný stavový vektor a následne produkujú sekvenciu výstupov.\n",
"\n",
"V tomto notebooku sa zameriame na jednoduché generatívne modely, ktoré nám pomáhajú generovať text. Pre jednoduchosť si vytvoríme **sieť na úrovni znakov**, ktorá generuje text písmeno po písmene. Počas tréningu musíme vziať nejaký textový korpus a rozdeliť ho na sekvencie písmen.\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": [
"## Vytváranie slovníka znakov\n",
"\n",
"Na vytvorenie generatívnej siete na úrovni znakov je potrebné rozdeliť text na jednotlivé znaky namiesto slov. To sa dá dosiahnuť definovaním iného tokenizéra:\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": [
"Pozrime sa na príklad, ako môžeme zakódovať text z nášho datasetu:\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": [
"## Trénovanie generatívnej RNN\n",
"\n",
"Spôsob, akým budeme trénovať RNN na generovanie textu, je nasledovný. V každom kroku vezmeme sekvenciu znakov s dĺžkou `nchars` a požiadame sieť, aby pre každý vstupný znak vygenerovala nasledujúci výstupný znak:\n",
"\n",
"![Obrázok zobrazujúci príklad generovania slova 'HELLO' pomocou RNN.](../../../../../translated_images/sk/rnn-generate.56c54afb52f9781d.webp)\n",
"\n",
"V závislosti od konkrétneho scenára môžeme chcieť zahrnúť aj špeciálne znaky, ako napríklad *koniec sekvencie* `<eos>`. V našom prípade chceme sieť trénovať na nekonečné generovanie textu, preto nastavíme veľkosť každej sekvencie na `nchars` tokenov. Každý tréningový príklad tak bude pozostávať z `nchars` vstupov a `nchars` výstupov (čo je vstupná sekvencia posunutá o jeden symbol doľava). Minibatch bude obsahovať niekoľko takýchto sekvencií.\n",
"\n",
"Spôsob, akým budeme generovať minibatch, je nasledovný: vezmeme každý text správy s dĺžkou `l` a z neho vygenerujeme všetky možné kombinácie vstup-výstup (bude ich `l-nchars`). Tieto kombinácie vytvoria jeden minibatch, pričom veľkosť minibatchov bude pri každom tréningovom kroku odlišná.\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": [
"Teraz si definujme generátorovú sieť. Môže byť založená na akejkoľvek rekurentnej bunke, o ktorej sme hovorili v predchádzajúcej jednotke (jednoduchá, LSTM alebo GRU). V našom príklade použijeme LSTM.\n",
"\n",
"Keďže sieť prijíma znaky ako vstup a veľkosť slovníka je pomerne malá, nepotrebujeme vrstvu na vkladanie (embedding layer), jednohotovo zakódovaný vstup môže ísť priamo do LSTM bunky. Avšak, pretože ako vstup odovzdávame čísla znakov, musíme ich pred odovzdaním do LSTM jednohotovo zakódovať. Toto sa vykonáva volaním funkcie `one_hot` počas prechodu `forward`. Výstupný enkodér bude lineárna vrstva, ktorá prevedie skrytý stav na jednohotovo zakódovaný výstup.\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": [
"Počas tréningu chceme byť schopní vzorkovať generovaný text. Na tento účel definujeme funkciu `generate`, ktorá vytvorí výstupný reťazec dĺžky `size`, začínajúc od počiatočného reťazca `start`.\n",
"\n",
"Funguje to nasledovne. Najprv prejdeme celý počiatočný reťazec cez sieť a získame výstupný stav `s` a ďalší predpovedaný znak `out`. Keďže `out` je zakódovaný v one-hot formáte, použijeme `argmax`, aby sme získali index znaku `nc` vo slovníku, a pomocou `itos` zistíme skutočný znak, ktorý pridáme do výsledného zoznamu znakov `chars`. Tento proces generovania jedného znaku sa opakuje `size` krát, aby sme vygenerovali požadovaný počet 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": [
"Poďme na tréning! Tréningová slučka je takmer rovnaká ako vo všetkých našich predchádzajúcich príkladoch, ale namiesto presnosti vypisujeme každých 1000 epoch vygenerovaný text.\n",
"\n",
"Osobitnú pozornosť treba venovať spôsobu, akým počítame stratu. Stratu musíme vypočítať na základe výstupu zakódovaného v one-hot formáte `out` a očakávaného textu `text_out`, čo je zoznam indexov znakov. Našťastie, funkcia `cross_entropy` očakáva ako prvý argument nenormalizovaný výstup siete a ako druhý číslo triedy, čo je presne to, čo máme. Táto funkcia tiež automaticky vykonáva priemerovanie podľa veľkosti minibatchu.\n",
"\n",
"Tréning tiež obmedzujeme na počet vzoriek `samples_to_train`, aby sme nemuseli čakať príliš dlho. Odporúčame vám experimentovať a skúsiť dlhší tréning, prípadne na niekoľko epoch (v takom prípade by ste museli vytvoriť ďalšiu slučku okolo tohto kódu).\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": [
"Tento príklad už generuje pomerne dobrý text, ale dá sa ešte vylepšiť niekoľkými spôsobmi: \n",
"* **Lepšia generácia minibatchov**. Spôsob, akým sme pripravili dáta na trénovanie, bol taký, že sme generovali jeden minibatch z jednej vzorky. To nie je ideálne, pretože minibatche majú rôzne veľkosti a niektoré z nich sa ani nedajú vygenerovať, pretože text je menší ako `nchars`. Navyše, malé minibatche nedostatočne zaťažujú GPU. Múdrejšie by bolo získať jeden veľký blok textu zo všetkých vzoriek, potom vygenerovať všetky páry vstup-výstup, zamiešať ich a vytvoriť minibatche rovnakej veľkosti. \n",
"* **Viacvrstvové LSTM**. Má zmysel vyskúšať 2 alebo 3 vrstvy LSTM buniek. Ako sme spomenuli v predchádzajúcej jednotke, každá vrstva LSTM extrahuje určité vzory z textu, a v prípade generátora na úrovni znakov môžeme očakávať, že nižšia úroveň LSTM bude zodpovedná za extrakciu slabík, a vyššie úrovne - za slová a kombinácie slov. Toto sa dá jednoducho implementovať odovzdaním parametra počtu vrstiev do konštruktora LSTM. \n",
"* Môžete tiež experimentovať s **GRU jednotkami** a zistiť, ktoré z nich fungujú lepšie, a s **rôznymi veľkosťami skrytých vrstiev**. Príliš veľká skrytá vrstva môže viesť k overfittingu (napr. sieť sa naučí presný text), a menšia veľkosť nemusí priniesť dobrý výsledok. \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Generovanie mäkkého textu a teplota\n",
"\n",
"V predchádzajúcej definícii `generate` sme vždy vyberali znak s najvyššou pravdepodobnosťou ako ďalší znak v generovanom texte. To malo za následok, že text často \"cykloval\" medzi rovnakými sekvenciami znakov znova a znova, ako v tomto príklade:\n",
"```\n",
"today of the second the company and a second the company ...\n",
"```\n",
"\n",
"Ak sa však pozrieme na rozdelenie pravdepodobností pre ďalší znak, môže sa stať, že rozdiel medzi niekoľkými najvyššími pravdepodobnosťami nie je veľký, napríklad jeden znak môže mať pravdepodobnosť 0.2, iný 0.19, atď. Napríklad, pri hľadaní ďalšieho znaku v sekvencii '*play*' môže byť ďalší znak rovnako dobre medzera alebo **e** (ako v slove *player*).\n",
"\n",
"To nás vedie k záveru, že nie je vždy \"spravodlivé\" vybrať znak s vyššou pravdepodobnosťou, pretože výber druhého najvyššieho môže stále viesť k zmysluplnému textu. Je rozumnejšie **vzorkovať** znaky z rozdelenia pravdepodobností, ktoré poskytuje výstup siete.\n",
"\n",
"Toto vzorkovanie sa dá vykonať pomocou funkcie `multinomial`, ktorá implementuje tzv. **multinomiálne rozdelenie**. Funkcia, ktorá implementuje toto **mäkké** generovanie textu, je definovaná nižšie:\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": [
"Zaviedli sme ďalší parameter nazývaný **teplota**, ktorý sa používa na určenie, ako striktne by sme sa mali držať najvyššej pravdepodobnosti. Ak je teplota 1.0, vykonávame spravodlivé multinomiálne vzorkovanie, a keď teplota dosiahne nekonečno - všetky pravdepodobnosti sa stanú rovnakými a náhodne vyberáme ďalší znak. V príklade nižšie môžeme pozorovať, že text sa stáva nezmyselným, keď príliš zvýšime teplotu, a pripomína „cyklický“ ťažko generovaný text, keď sa teplota blíži k 0.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Upozornenie**: \nTento dokument bol preložený pomocou služby na automatický preklad [Co-op Translator](https://github.com/Azure/co-op-translator). Hoci sa snažíme o presnosť, upozorňujeme, že automatické preklady môžu obsahovať chyby alebo nepresnosti. Pôvodný dokument v jeho pôvodnom jazyku by mal byť považovaný za autoritatívny zdroj. Pre dôležité informácie odporúčame profesionálny ľudský preklad. Nezodpovedáme za žiadne nedorozumenia alebo nesprávne interpretácie vyplývajúce z použitia tohto prekladu.\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:36:56+00:00",
"source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb",
"language_code": "sk"
}
},
"nbformat": 4,
"nbformat_minor": 4
}