{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Generative netværk\n", "\n", "Recurrent Neural Networks (RNNs) og deres gatede cellevarianter såsom Long Short Term Memory Cells (LSTMs) og Gated Recurrent Units (GRUs) gav en mekanisme til sproglig modellering, dvs. de kan lære ords rækkefølge og give forudsigelser for det næste ord i en sekvens. Dette gør det muligt for os at bruge RNNs til **generative opgaver**, såsom almindelig tekstgenerering, maskinoversættelse og endda billedbeskrivelse.\n", "\n", "I RNN-arkitekturen, som vi diskuterede i den forrige enhed, producerede hver RNN-enhed den næste skjulte tilstand som output. Men vi kan også tilføje et andet output til hver rekurrent enhed, hvilket giver os mulighed for at generere en **sekvens** (som har samme længde som den oprindelige sekvens). Desuden kan vi bruge RNN-enheder, der ikke modtager input ved hvert trin, men blot tager en initial tilstandsvektor og derefter producerer en sekvens af outputs.\n", "\n", "I denne notebook vil vi fokusere på simple generative modeller, der hjælper os med at generere tekst. For enkelhedens skyld lad os bygge et **karakter-niveau netværk**, som genererer tekst bogstav for bogstav. Under træning skal vi tage en tekstkorpus og opdele den i bogstavsekvenser.\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": [ "## Opbygning af tegnordforråd\n", "\n", "For at opbygge et generativt netværk på tegnniveau, skal vi opdele teksten i individuelle tegn i stedet for ord. Dette kan gøres ved at definere en anden 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": [ "Lad os se eksemplet på, hvordan vi kan kode teksten fra vores datasæt:\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æning af en generativ RNN\n", "\n", "Måden, vi vil træne en RNN til at generere tekst, er som følger. Ved hvert trin tager vi en sekvens af tegn med længden `nchars` og beder netværket om at generere det næste outputtegn for hvert inputtegn:\n", "\n", "![Billede, der viser et eksempel på RNN-generering af ordet 'HELLO'.](../../../../../translated_images/da/rnn-generate.56c54afb52f9781d.webp)\n", "\n", "Afhængigt af det konkrete scenarie kan vi også ønske at inkludere nogle specialtegn, såsom *end-of-sequence* ``. I vores tilfælde ønsker vi blot at træne netværket til uendelig tekstgenerering, så vi vil fastsætte størrelsen af hver sekvens til at være lig med `nchars` tokens. Derfor vil hvert træningseksempel bestå af `nchars` input og `nchars` output (hvilket er inputsekvensen forskudt med ét symbol til venstre). En minibatch vil bestå af flere sådanne sekvenser.\n", "\n", "Måden, vi vil generere minibatches på, er ved at tage hver nyhedstekst med længden `l` og generere alle mulige input-output-kombinationer fra den (der vil være `l-nchars` sådanne kombinationer). Disse vil udgøre én minibatch, og størrelsen af minibatches vil variere ved hvert træningstrin.\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": [ "Lad os nu definere generatornetværket. Det kan baseres på enhver rekurrent celle, som vi diskuterede i den forrige enhed (simpel, LSTM eller GRU). I vores eksempel vil vi bruge LSTM.\n", "\n", "Da netværket tager tegn som input, og ordforrådet er ret lille, behøver vi ikke en embedding-lag; one-hot-kodet input kan direkte sendes til LSTM-cellen. Dog, fordi vi sender tegnnumre som input, skal vi one-hot-kode dem, før de sendes til LSTM. Dette gøres ved at kalde funktionen `one_hot` under `forward`-passet. Output-encoderen vil være et lineært lag, der konverterer den skjulte tilstand til one-hot-kodet output.\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": [ "Under træning ønsker vi at kunne sample genereret tekst. For at gøre dette vil vi definere funktionen `generate`, som vil producere en output-streng af længden `size`, startende fra den indledende streng `start`.\n", "\n", "Sådan fungerer det. Først vil vi sende hele start-strengen gennem netværket og tage output-tilstanden `s` og næste forudsagte tegn `out`. Da `out` er one-hot kodet, tager vi `argmax` for at få indekset for tegnet `nc` i ordforrådet og bruger `itos` til at finde det faktiske tegn og tilføje det til den resulterende liste af tegn `chars`. Denne proces med at generere ét tegn gentages `size` gange for at generere det ønskede antal tegn.\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": [ "Nu skal vi i gang med træningen! Træningsløkken er næsten den samme som i alle vores tidligere eksempler, men i stedet for nøjagtighed udskriver vi genereret tekst hver 1000. epoch.\n", "\n", "Der skal lægges særlig vægt på, hvordan vi beregner tab. Vi skal beregne tabet ud fra den one-hot-kodede output `out` og den forventede tekst `text_out`, som er listen over tegnindekser. Heldigvis forventer funktionen `cross_entropy` et unormeret netværksoutput som første argument og klassens nummer som det andet, hvilket er præcis, hvad vi har. Den udfører også automatisk gennemsnit over minibatch-størrelsen.\n", "\n", "Vi begrænser også træningen til `samples_to_train` prøver, for ikke at skulle vente for længe. Vi opfordrer dig til at eksperimentere og prøve længere træning, eventuelt over flere epochs (i så fald skal du oprette en ekstra løkke omkring denne kode).\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": [ "Dette eksempel genererer allerede ret god tekst, men det kan forbedres yderligere på flere måder:\n", "\n", "* **Bedre minibatch-generering**. Den måde, vi forberedte data til træning på, var at generere én minibatch fra én prøve. Dette er ikke ideelt, fordi minibatches har forskellige størrelser, og nogle af dem kan endda ikke genereres, fordi teksten er mindre end `nchars`. Derudover udnytter små minibatches ikke GPU'en tilstrækkeligt. Det ville være klogere at tage en stor tekstblok fra alle prøver, derefter generere alle input-output-par, blande dem og generere minibatches af ens størrelse.\n", "\n", "* **Multilags LSTM**. Det giver mening at prøve 2 eller 3 lag af LSTM-celler. Som vi nævnte i den forrige enhed, udtrækker hvert lag af LSTM visse mønstre fra teksten, og i tilfælde af en generator på tegnniveau kan vi forvente, at det lavere LSTM-lag er ansvarligt for at udtrække stavelser, og de højere lag - for ord og ordkombinationer. Dette kan nemt implementeres ved at sende parameteren for antal lag til LSTM-konstruktøren.\n", "\n", "* Du kan også eksperimentere med **GRU-enheder** og se, hvilke der fungerer bedre, samt med **forskellige størrelser på skjulte lag**. Et for stort skjult lag kan resultere i overtilpasning (f.eks. at netværket lærer den præcise tekst), og en mindre størrelse kan muligvis ikke producere gode resultater.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Blød tekstgenerering og temperatur\n", "\n", "I den tidligere definition af `generate` valgte vi altid det tegn med den højeste sandsynlighed som det næste tegn i den genererede tekst. Dette resulterede ofte i, at teksten \"cyklede\" mellem de samme tegnsekvenser igen og igen, som i dette eksempel:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "Men hvis vi ser på sandsynlighedsfordelingen for det næste tegn, kan det være, at forskellen mellem de højeste sandsynligheder ikke er særlig stor, f.eks. kan ét tegn have en sandsynlighed på 0,2, mens et andet har 0,19 osv. For eksempel, når vi leder efter det næste tegn i sekvensen '*play*', kan det næste tegn lige så godt være et mellemrum eller **e** (som i ordet *player*).\n", "\n", "Dette fører os til konklusionen, at det ikke altid er \"retfærdigt\" at vælge tegnet med den højeste sandsynlighed, fordi det at vælge det næsthøjeste stadig kan føre til meningsfuld tekst. Det er mere fornuftigt at **udvælge** tegn fra sandsynlighedsfordelingen givet af netværkets output.\n", "\n", "Denne udvælgelse kan udføres ved hjælp af funktionen `multinomial`, som implementerer den såkaldte **multinomialfordeling**. En funktion, der implementerer denne **bløde** tekstgenerering, er defineret nedenfor:\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": [ "Vi har introduceret en ekstra parameter kaldet **temperatur**, som bruges til at angive, hvor strengt vi skal holde os til den højeste sandsynlighed. Hvis temperaturen er 1,0, udfører vi retfærdig multinomial sampling, og når temperaturen går mod uendelig - bliver alle sandsynligheder lige, og vi vælger tilfældigt den næste karakter. I eksemplet nedenfor kan vi observere, at teksten bliver meningsløs, når vi øger temperaturen for meget, og den minder om \"cyklet\" hårdt-genereret tekst, når den nærmer sig 0.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Ansvarsfraskrivelse**: \nDette dokument er blevet oversat ved hjælp af AI-oversættelsestjenesten [Co-op Translator](https://github.com/Azure/co-op-translator). Selvom vi bestræber os på nøjagtighed, skal du være opmærksom på, at automatiserede oversættelser kan indeholde fejl eller unøjagtigheder. Det originale dokument på dets oprindelige sprog bør betragtes som den autoritative kilde. For kritisk information anbefales professionel menneskelig oversættelse. Vi er ikke ansvarlige for eventuelle misforståelser eller fejltolkninger, der måtte opstå som følge af brugen af denne oversættelse.\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-28T17:27:48+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "da" } }, "nbformat": 4, "nbformat_minor": 4 }