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

414 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Генеративни мрежи\n",
"\n",
"Рекурентните невронни мрежи (RNNs) и техните варианти с управлявани клетки, като клетки с дългосрочна памет (LSTMs) и управлявани рекурентни единици (GRUs), предоставят механизъм за моделиране на езика, т.е. те могат да научат подредбата на думите и да предсказват следващата дума в последователността. Това ни позволява да използваме RNNs за **генеративни задачи**, като обикновено генериране на текст, машинен превод и дори създаване на описания за изображения.\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/bg/rnn-generate.56c54afb52f9781d.webp)\n",
"\n",
"В зависимост от конкретния сценарий, може да искаме да включим и някои специални символи, като например *край на последователността* `<eos>`. В нашия случай просто искаме да обучим мрежата за безкрайно генериране на текст, затова ще фиксираме размера на всяка последователност да бъде равен на `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",
"Тъй като мрежата приема символи като вход, а размерът на речника е сравнително малък, нямаме нужда от embedding слой входът, кодиран като one-hot, може директно да бъде подаден към LSTM клетката. Въпреки това, понеже подаваме номера на символите като вход, трябва да ги кодираме като one-hot, преди да ги предадем на LSTM. Това се извършва чрез извикване на функцията `one_hot` по време на `forward` преминаването. Енкодерът на изхода ще бъде линеен слой, който ще преобразува скритото състояние в изход, кодиран като 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": [
"По време на обучението искаме да можем да извадим примерен генериран текст. За да направим това, ще дефинираме функцията `generate`, която ще произвежда изходен низ с дължина `size`, започвайки от началния низ `start`.\n",
"\n",
"Ето как работи. Първо, ще предадем целия начален низ през мрежата и ще вземем изходното състояние `s` и следващия предсказан символ `out`. Тъй като `out` е едно-горещо кодиран, използваме `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": [
"Въведохме още един параметър, наречен **температура**, който се използва, за да покаже колко строго трябва да се придържаме към най-високата вероятност. Ако температурата е 1.0, правим честно мултиномиално извадково вземане, а когато температурата достигне до безкрайност - всички вероятности стават равни и случайно избираме следващия символ. В примера по-долу можем да наблюдаваме, че текстът става безсмислен, когато увеличим температурата твърде много, и прилича на \"циклично\" твърдо генериран текст, когато се доближи до 0.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n---\n\n**Отказ от отговорност**: \nТози документ е преведен с помощта на AI услуга за превод [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-30T00:38:19+00:00",
"source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb",
"language_code": "bg"
}
},
"nbformat": 4,
"nbformat_minor": 4
}