{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Генеративні мережі\n", "\n", "Рекурентні нейронні мережі (RNN) та їх варіанти з керованими осередками, такі як осередки довготривалої короткочасної пам'яті (LSTM) і керовані рекурентні блоки (GRU), забезпечили механізм для моделювання мови, тобто вони можуть навчитися впорядковувати слова та передбачати наступне слово в послідовності. Це дозволяє використовувати RNN для **генеративних завдань**, таких як звичайна генерація тексту, машинний переклад і навіть створення підписів до зображень.\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", "![Зображення, що показує приклад генерації слова 'HELLO' за допомогою RNN.](../../../../../translated_images/uk/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` закодований у форматі one-hot, ми використовуємо `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": [ "Ми ввели ще один параметр, який називається **temperature** (температура), і він використовується для визначення того, наскільки сильно ми повинні дотримуватися найвищої ймовірності. Якщо температура дорівнює 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-30T10:13:18+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "uk" } }, "nbformat": 4, "nbformat_minor": 4 }