{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Generatieve netwerken\n", "\n", "Recurrent Neural Networks (RNNs) en hun varianten met gated cellen, zoals Long Short Term Memory Cells (LSTMs) en Gated Recurrent Units (GRUs), bieden een mechanisme voor taalmodellering, oftewel ze kunnen de volgorde van woorden leren en voorspellingen doen voor het volgende woord in een reeks. Dit stelt ons in staat om RNNs te gebruiken voor **generatieve taken**, zoals gewone tekstgeneratie, machinevertaling en zelfs beeldbeschrijving.\n", "\n", "In de RNN-architectuur die we in de vorige eenheid hebben besproken, produceerde elke RNN-eenheid de volgende verborgen toestand als output. We kunnen echter ook een extra output toevoegen aan elke recurrente eenheid, waardoor we een **reeks** kunnen genereren (die even lang is als de oorspronkelijke reeks). Bovendien kunnen we RNN-eenheden gebruiken die bij elke stap geen invoer accepteren, maar alleen een initiële toestandsvector nemen en vervolgens een reeks outputs produceren.\n", "\n", "In deze notebook richten we ons op eenvoudige generatieve modellen die ons helpen tekst te genereren. Voor de eenvoud bouwen we een **karakter-niveau netwerk**, dat tekst letter voor letter genereert. Tijdens de training moeten we een tekstcorpus nemen en deze splitsen in letterreeksen.\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": [ "## Het opbouwen van een karaktervocabulaire\n", "\n", "Om een generatief netwerk op karakterniveau te bouwen, moeten we tekst opsplitsen in afzonderlijke karakters in plaats van woorden. Dit kan worden gedaan door een andere tokenizer te definiëren:\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": [ "Laten we het voorbeeld bekijken van hoe we de tekst uit onze dataset kunnen coderen:\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": [ "## Een generatieve RNN trainen\n", "\n", "De manier waarop we een RNN zullen trainen om tekst te genereren, is als volgt. Bij elke stap nemen we een reeks karakters van lengte `nchars` en vragen we het netwerk om het volgende uitvoerkarakter te genereren voor elk invoerkarakter:\n", "\n", "![Afbeelding die een voorbeeld toont van een RNN die het woord 'HELLO' genereert.](../../../../../translated_images/nl/rnn-generate.56c54afb52f9781d.webp)\n", "\n", "Afhankelijk van het specifieke scenario willen we mogelijk ook enkele speciale karakters opnemen, zoals *einde-van-sequentie* ``. In ons geval willen we het netwerk gewoon trainen voor eindeloze tekstgeneratie, dus stellen we de grootte van elke sequentie vast op `nchars` tokens. Bijgevolg zal elk trainingsvoorbeeld bestaan uit `nchars` invoer en `nchars` uitvoer (wat de invoersequentie is, verschoven met één symbool naar links). Een minibatch zal bestaan uit meerdere van dergelijke sequenties.\n", "\n", "De manier waarop we minibatches zullen genereren, is door elke nieuwsberichttekst van lengte `l` te nemen en alle mogelijke invoer-uitvoercombinaties daaruit te genereren (er zullen `l-nchars` van dergelijke combinaties zijn). Deze vormen één minibatch, en de grootte van de minibatches zal bij elke trainingsstap verschillen.\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": [ "Laten we het generatornetwerk definiëren. Het kan gebaseerd zijn op elke recurrente cel die we in de vorige eenheid hebben besproken (eenvoudig, LSTM of GRU). In ons voorbeeld zullen we LSTM gebruiken.\n", "\n", "Omdat het netwerk tekens als invoer neemt en de vocabulairegrootte vrij klein is, hebben we geen embeddinglaag nodig; een one-hot-gecodeerde invoer kan direct naar de LSTM-cel gaan. Echter, omdat we tekennummers als invoer doorgeven, moeten we ze one-hot-encoderen voordat we ze naar de LSTM sturen. Dit wordt gedaan door de functie `one_hot` aan te roepen tijdens de `forward`-pass. De uitvoerencoder zal een lineaire laag zijn die de verborgen toestand omzet in een one-hot-gecodeerde uitvoer.\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": [ "Tijdens de training willen we in staat zijn om gegenereerde tekst te bemonsteren. Om dit te doen, definiëren we de functie `generate`, die een uitvoerstring van lengte `size` zal produceren, beginnend met de initiële string `start`.\n", "\n", "De werkwijze is als volgt. Eerst geven we de volledige startstring door het netwerk en nemen we de uitvoerstatus `s` en het volgende voorspelde teken `out`. Aangezien `out` one-hot gecodeerd is, nemen we `argmax` om de index van het teken `nc` in de woordenschat te verkrijgen, en gebruiken we `itos` om het daadwerkelijke teken te achterhalen en voegen dit toe aan de resulterende lijst van tekens `chars`. Dit proces van het genereren van één teken wordt `size` keer herhaald om het vereiste aantal tekens te genereren.\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": [ "Laten we beginnen met de training! De trainingslus is bijna hetzelfde als in al onze eerdere voorbeelden, maar in plaats van nauwkeurigheid printen we elke 1000 epochs een willekeurig gegenereerde tekst.\n", "\n", "Er moet speciale aandacht worden besteed aan de manier waarop we verlies berekenen. We moeten het verlies berekenen op basis van de één-hot-gecodeerde output `out` en de verwachte tekst `text_out`, wat de lijst van karakterindices is. Gelukkig verwacht de functie `cross_entropy` niet-genormaliseerde netwerkoutput als eerste argument en het klasnummer als tweede, wat precies is wat we hebben. Het voert ook automatische averaging uit over de minibatch-grootte.\n", "\n", "We beperken de training ook tot `samples_to_train` samples, zodat we niet te lang hoeven te wachten. We moedigen je aan om te experimenteren en langere trainingen te proberen, mogelijk voor meerdere epochs (in dat geval zou je een extra lus rond deze code moeten maken).\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": [ "Dit voorbeeld genereert al behoorlijk goede tekst, maar er zijn verschillende manieren om het verder te verbeteren:\n", "\n", "* **Betere minibatch-generatie**. De manier waarop we data voor training hebben voorbereid, was door één minibatch uit één sample te genereren. Dit is niet ideaal, omdat minibatches allemaal verschillende groottes hebben, en sommige zelfs niet gegenereerd kunnen worden omdat de tekst kleiner is dan `nchars`. Bovendien belasten kleine minibatches de GPU niet voldoende. Het zou verstandiger zijn om één groot tekstblok uit alle samples te halen, vervolgens alle input-outputparen te genereren, deze te schudden en minibatches van gelijke grootte te maken.\n", "\n", "* **Meerdere lagen LSTM**. Het is zinvol om 2 of 3 lagen van LSTM-cellen te proberen. Zoals we in de vorige eenheid hebben besproken, haalt elke laag van een LSTM bepaalde patronen uit de tekst, en in het geval van een generator op tekenniveau kunnen we verwachten dat een lagere LSTM-laag verantwoordelijk is voor het herkennen van lettergrepen, terwijl hogere lagen zich richten op woorden en woordcombinaties. Dit kan eenvoudig worden geïmplementeerd door een parameter voor het aantal lagen door te geven aan de LSTM-constructor.\n", "\n", "* Je kunt ook experimenteren met **GRU-eenheden** om te zien welke beter presteren, en met **verschillende groottes van verborgen lagen**. Een te grote verborgen laag kan resulteren in overfitting (bijvoorbeeld dat het netwerk de exacte tekst leert), terwijl een kleinere grootte mogelijk geen goed resultaat oplevert.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Zachte tekstgeneratie en temperatuur\n", "\n", "In de vorige definitie van `generate` kozen we altijd het karakter met de hoogste waarschijnlijkheid als het volgende karakter in de gegenereerde tekst. Dit resulteerde er vaak in dat de tekst \"cyclisch\" werd en steeds dezelfde karakterreeksen herhaalde, zoals in dit voorbeeld:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "Als we echter naar de waarschijnlijkheidsverdeling voor het volgende karakter kijken, kan het zijn dat het verschil tussen een paar hoogste waarschijnlijkheden niet groot is, bijvoorbeeld: één karakter kan een waarschijnlijkheid van 0,2 hebben, en een ander 0,19, enzovoort. Bijvoorbeeld, bij het zoeken naar het volgende karakter in de reeks '*play*', kan het volgende karakter net zo goed een spatie zijn, of **e** (zoals in het woord *player*).\n", "\n", "Dit leidt ons tot de conclusie dat het niet altijd \"eerlijk\" is om het karakter met de hoogste waarschijnlijkheid te kiezen, omdat het kiezen van het op één na hoogste karakter ook tot betekenisvolle tekst kan leiden. Het is verstandiger om **karakters te bemonsteren** uit de waarschijnlijkheidsverdeling die door de netwerkoutput wordt gegeven.\n", "\n", "Dit bemonsteren kan worden gedaan met behulp van de functie `multinomial`, die de zogenaamde **multinomiale verdeling** implementeert. Een functie die deze **zachte** tekstgeneratie implementeert, is hieronder gedefinieerd:\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": [ "We hebben een extra parameter geïntroduceerd genaamd **temperatuur**, die wordt gebruikt om aan te geven hoe strikt we ons moeten houden aan de hoogste waarschijnlijkheid. Als de temperatuur 1.0 is, doen we eerlijke multinomiale steekproeven, en wanneer de temperatuur naar oneindig gaat - worden alle waarschijnlijkheden gelijk, en selecteren we willekeurig het volgende karakter. In het onderstaande voorbeeld kunnen we zien dat de tekst betekenisloos wordt wanneer we de temperatuur te veel verhogen, en het lijkt op \"gecyclet\" hard gegenereerde tekst wanneer het dichter bij 0 komt.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Disclaimer**: \nDit document is vertaald met behulp van de AI-vertalingsservice [Co-op Translator](https://github.com/Azure/co-op-translator). Hoewel we streven naar nauwkeurigheid, dient u zich ervan bewust te zijn dat geautomatiseerde vertalingen fouten of onnauwkeurigheden kunnen bevatten. Het originele document in de oorspronkelijke taal moet worden beschouwd als de gezaghebbende bron. Voor kritieke informatie wordt professionele menselijke vertaling aanbevolen. Wij zijn niet aansprakelijk voor misverstanden of verkeerde interpretaties die voortvloeien uit het gebruik van deze vertaling.\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-28T21:34:08+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "nl" } }, "nbformat": 4, "nbformat_minor": 4 }