{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Generatywne sieci\n", "\n", "Rekurencyjne Sieci Neuronowe (RNN) oraz ich warianty z komórkami bramkowymi, takie jak Komórki Długiej Krótkoterminowej Pamięci (LSTM) i Bramkowe Jednostki Rekurencyjne (GRU), dostarczają mechanizmu modelowania języka, czyli potrafią uczyć się kolejności słów i przewidywać następne słowo w sekwencji. Dzięki temu możemy używać RNN do **zadań generatywnych**, takich jak zwykłe generowanie tekstu, tłumaczenie maszynowe, a nawet generowanie podpisów do obrazów.\n", "\n", "W architekturze RNN, którą omawialiśmy w poprzedniej jednostce, każda jednostka RNN produkowała kolejny ukryty stan jako wynik. Jednak możemy również dodać kolejny wynik do każdej rekurencyjnej jednostki, co pozwoli nam generować **sekwencję** (równą długości oryginalnej sekwencji). Co więcej, możemy używać jednostek RNN, które nie przyjmują wejścia na każdym kroku, a jedynie korzystają z początkowego wektora stanu, aby następnie generować sekwencję wyników.\n", "\n", "W tym notatniku skupimy się na prostych modelach generatywnych, które pomagają nam generować tekst. Dla uproszczenia zbudujemy **sieć na poziomie znaków**, która generuje tekst litera po literze. Podczas treningu musimy wziąć jakiś korpus tekstu i podzielić go na sekwencje liter.\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": [ "## Tworzenie słownika znaków\n", "\n", "Aby stworzyć generatywną sieć na poziomie znaków, musimy podzielić tekst na pojedyncze znaki zamiast na słowa. Można to zrobić, definiując inny tokenizer:\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": [ "Zobaczmy przykład, jak możemy zakodować tekst z naszego zestawu danych:\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": [ "## Trenowanie generatywnej RNN\n", "\n", "Sposób, w jaki będziemy trenować RNN do generowania tekstu, wygląda następująco. Na każdym kroku weźmiemy sekwencję znaków o długości `nchars` i poprosimy sieć o wygenerowanie kolejnego znaku wyjściowego dla każdego znaku wejściowego:\n", "\n", "![Obraz przedstawiający przykład generowania słowa 'HELLO' przez RNN.](../../../../../lessons/5-NLP/17-GenerativeNetworks/images/rnn-generate.png)\n", "\n", "W zależności od konkretnego scenariusza, możemy również chcieć uwzględnić specjalne znaki, takie jak *koniec sekwencji* ``. W naszym przypadku chcemy po prostu nauczyć sieć generowania niekończącego się tekstu, dlatego ustalimy rozmiar każdej sekwencji na `nchars` tokenów. W konsekwencji każdy przykład treningowy będzie składał się z `nchars` wejść i `nchars` wyjść (które są sekwencją wejściową przesuniętą o jeden symbol w lewo). Minibatch będzie składał się z kilku takich sekwencji.\n", "\n", "Sposób, w jaki będziemy generować minibatche, polega na wzięciu każdego tekstu wiadomości o długości `l` i wygenerowaniu wszystkich możliwych kombinacji wejście-wyjście z tego tekstu (będzie ich `l-nchars`). Utworzą one jeden minibatch, a rozmiar minibatchów będzie różny na każdym kroku treningowym.\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 zdefiniujmy sieć generatora. Może być oparta na dowolnej komórce rekurencyjnej, o której mówiliśmy w poprzedniej jednostce (prosta, LSTM lub GRU). W naszym przykładzie użyjemy LSTM.\n", "\n", "Ponieważ sieć przyjmuje znaki jako dane wejściowe, a rozmiar słownika jest dość mały, nie potrzebujemy warstwy osadzania – dane wejściowe zakodowane w postaci one-hot mogą bezpośrednio trafić do komórki LSTM. Jednakże, ponieważ przekazujemy numery znaków jako dane wejściowe, musimy zakodować je w postaci one-hot przed przekazaniem do LSTM. Robimy to, wywołując funkcję `one_hot` podczas przejścia `forward`. Enkoder wyjściowy będzie liniową warstwą, która przekształci stan ukryty w wyjście zakodowane w postaci one-hot.\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": [ "Podczas treningu chcemy mieć możliwość próbkowania generowanego tekstu. Aby to osiągnąć, zdefiniujemy funkcję `generate`, która wygeneruje ciąg znaków o długości `size`, zaczynając od początkowego ciągu `start`.\n", "\n", "Działa to w następujący sposób. Najpierw przekażemy cały początkowy ciąg przez sieć, aby uzyskać stan wyjściowy `s` oraz przewidywany następny znak `out`. Ponieważ `out` jest zakodowany w formacie one-hot, używamy `argmax`, aby uzyskać indeks znaku `nc` w słowniku, a następnie korzystamy z `itos`, aby znaleźć rzeczywisty znak i dodać go do wynikowej listy znaków `chars`. Ten proces generowania jednego znaku powtarzamy `size` razy, aby wygenerować wymaganą liczbę znaków.\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": [ "Teraz przejdźmy do treningu! Pętla treningowa jest prawie taka sama jak we wszystkich naszych poprzednich przykładach, ale zamiast dokładności wypisujemy próbki wygenerowanego tekstu co 1000 epok.\n", "\n", "Szczególną uwagę należy zwrócić na sposób obliczania straty. Musimy obliczyć stratę, mając jednowymiarowe zakodowane wyjście `out` oraz oczekiwany tekst `text_out`, który jest listą indeksów znaków. Na szczęście funkcja `cross_entropy` oczekuje nienormalizowanego wyjścia sieci jako pierwszego argumentu i numeru klasy jako drugiego, co dokładnie odpowiada naszym danym. Funkcja ta automatycznie wykonuje również uśrednianie po rozmiarze minibatcha.\n", "\n", "Ograniczamy również trening do liczby próbek określonej przez `samples_to_train`, aby nie czekać zbyt długo. Zachęcamy do eksperymentowania i próbowania dłuższego treningu, być może przez kilka epok (w takim przypadku należałoby stworzyć dodatkową pętlę wokół tego kodu).\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": [ "Ten przykład generuje już całkiem dobry tekst, ale można go jeszcze ulepszyć na kilka sposobów:\n", "* **Lepsze generowanie minibatchy**. Sposób, w jaki przygotowaliśmy dane do treningu, polegał na generowaniu jednego minibatcha z jednej próbki. Nie jest to idealne, ponieważ minibatche mają różne rozmiary, a niektórych z nich nawet nie da się wygenerować, ponieważ tekst jest krótszy niż `nchars`. Dodatkowo, małe minibatche nie obciążają wystarczająco GPU. Mądrzejszym podejściem byłoby pobranie dużego fragmentu tekstu ze wszystkich próbek, następnie wygenerowanie wszystkich par wejście-wyjście, ich przetasowanie i stworzenie minibatchy o równych rozmiarach.\n", "* **Wielowarstwowe LSTM**. Warto spróbować użycia 2 lub 3 warstw komórek LSTM. Jak wspomnieliśmy w poprzedniej jednostce, każda warstwa LSTM wyodrębnia pewne wzorce z tekstu, a w przypadku generatora na poziomie znaków możemy oczekiwać, że niższy poziom LSTM będzie odpowiedzialny za wyodrębnianie sylab, a wyższe poziomy - za słowa i ich kombinacje. Można to łatwo zaimplementować, przekazując parametr liczby warstw do konstruktora LSTM.\n", "* Możesz również poeksperymentować z **jednostkami GRU** i sprawdzić, które z nich działają lepiej, oraz z **różnymi rozmiarami warstw ukrytych**. Zbyt duża warstwa ukryta może prowadzić do przeuczenia (np. sieć nauczy się dokładnego tekstu), a zbyt mały rozmiar może nie dać dobrych wyników.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Generowanie miękkiego tekstu i temperatura\n", "\n", "W poprzedniej definicji `generate` zawsze wybieraliśmy znak o najwyższym prawdopodobieństwie jako kolejny znak w generowanym tekście. Skutkowało to tym, że tekst często \"cyklicznie\" powtarzał te same sekwencje znaków, jak w tym przykładzie:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "Jednakże, jeśli spojrzymy na rozkład prawdopodobieństwa dla kolejnego znaku, może się okazać, że różnica między kilkoma najwyższymi prawdopodobieństwami nie jest duża, np. jeden znak może mieć prawdopodobieństwo 0.2, a inny 0.19, itd. Na przykład, gdy szukamy kolejnego znaku w sekwencji '*play*', kolejnym znakiem może równie dobrze być spacja lub **e** (jak w słowie *player*).\n", "\n", "Prowadzi nas to do wniosku, że nie zawsze jest \"sprawiedliwe\" wybierać znak o wyższym prawdopodobieństwie, ponieważ wybór drugiego najwyższego może również prowadzić do sensownego tekstu. Rozsądniej jest **próbkować** znaki z rozkładu prawdopodobieństwa podanego przez wynik sieci.\n", "\n", "Takie próbkowanie można przeprowadzić za pomocą funkcji `multinomial`, która implementuje tzw. **rozkład wielomianowy**. Funkcja, która realizuje to **miękkie** generowanie tekstu, jest zdefiniowana poniżej:\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": [ "Wprowadziliśmy jeszcze jeden parametr o nazwie **temperatura**, który służy do wskazania, jak bardzo powinniśmy trzymać się najwyższego prawdopodobieństwa. Jeśli temperatura wynosi 1.0, przeprowadzamy uczciwe próbkowanie multinomialne, a gdy temperatura dąży do nieskończoności - wszystkie prawdopodobieństwa stają się równe i losowo wybieramy następny znak. Na poniższym przykładzie możemy zaobserwować, że tekst staje się bezsensowny, gdy zbytnio zwiększymy temperaturę, a przypomina \"cyklicznie\" generowany tekst, gdy zbliża się do 0.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Zastrzeżenie**: \nTen dokument został przetłumaczony za pomocą usługi tłumaczenia AI [Co-op Translator](https://github.com/Azure/co-op-translator). Chociaż dokładamy wszelkich starań, aby zapewnić poprawność tłumaczenia, prosimy pamiętać, że automatyczne tłumaczenia mogą zawierać błędy lub nieścisłości. Oryginalny dokument w jego języku źródłowym powinien być uznawany za wiarygodne źródło. W przypadku informacji o kluczowym znaczeniu zaleca się skorzystanie z profesjonalnego tłumaczenia przez człowieka. Nie ponosimy odpowiedzialności za jakiekolwiek nieporozumienia lub błędne interpretacje wynikające z użycia tego tłumaczenia.\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-31T13:45:17+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "pl" } }, "nbformat": 4, "nbformat_minor": 4 }