{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Generativa nätverk\n", "\n", "Recurrent Neural Networks (RNNs) och deras gated cell-varianter som Long Short Term Memory Cells (LSTMs) och Gated Recurrent Units (GRUs) gav en mekanism för språkmodellering, dvs. de kan lära sig ordordning och ge förutsägelser för nästa ord i en sekvens. Detta gör det möjligt att använda RNNs för **generativa uppgifter**, såsom vanlig textgenerering, maskinöversättning och till och med bildbeskrivning.\n", "\n", "I RNN-arkitekturen som vi diskuterade i föregående enhet, producerade varje RNN-enhet nästa dolda tillstånd som en utgång. Men vi kan också lägga till en annan utgång till varje återkommande enhet, vilket skulle göra det möjligt för oss att generera en **sekvens** (som är lika lång som den ursprungliga sekvensen). Dessutom kan vi använda RNN-enheter som inte tar emot en inmatning vid varje steg, utan bara tar en initial tillståndsvektor och sedan producerar en sekvens av utgångar.\n", "\n", "I denna notebook kommer vi att fokusera på enkla generativa modeller som hjälper oss att generera text. För enkelhetens skull ska vi bygga ett **teckennivånätverk**, som genererar text bokstav för bokstav. Under träningen behöver vi ta en textkorpus och dela upp den i teckensekvenser.\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": [ "## Bygga teckenordförråd\n", "\n", "För att bygga ett generativt nätverk på teckennivå behöver vi dela upp texten i enskilda tecken istället för ord. Detta kan göras genom att definiera en annan 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": [ "Låt oss se exemplet på hur vi kan koda texten från vår dataset:\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äna ett generativt RNN\n", "\n", "Så här kommer vi att träna ett RNN för att generera text. Vid varje steg tar vi en sekvens av tecken med längden `nchars` och ber nätverket att generera nästa utmatningstecken för varje inmatningstecken:\n", "\n", "![Bild som visar ett exempel på RNN-generering av ordet 'HELLO'.](../../../../../translated_images/sv/rnn-generate.56c54afb52f9781d.webp)\n", "\n", "Beroende på det faktiska scenariot kan vi också vilja inkludera några specialtecken, såsom *slut-på-sekvens* ``. I vårt fall vill vi bara träna nätverket för oändlig textgenerering, därför kommer vi att fixa storleken på varje sekvens till att vara lika med `nchars` tokens. Följaktligen kommer varje tränings-exempel att bestå av `nchars` inmatningar och `nchars` utmatningar (vilka är inmatningssekvensen förskjuten ett tecken åt vänster). Minibatchen kommer att bestå av flera sådana sekvenser.\n", "\n", "Sättet vi kommer att generera minibatcher på är att ta varje nyhetstext med längden `l` och generera alla möjliga inmatnings-utmatningskombinationer från den (det kommer att finnas `l-nchars` sådana kombinationer). Dessa kommer att bilda en minibatch, och storleken på minibatcherna kommer att vara olika vid varje träningssteg.\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": [ "Nu ska vi definiera generatornätverket. Det kan baseras på vilken som helst av de rekurrenta celler vi diskuterade i föregående enhet (enkel, LSTM eller GRU). I vårt exempel kommer vi att använda LSTM.\n", "\n", "Eftersom nätverket tar tecken som indata och vokabulärstorleken är ganska liten, behöver vi inget inbäddningslager; en one-hot-kodad indata kan direkt skickas till LSTM-cellen. Men eftersom vi skickar teckennummer som indata, måste vi one-hot-koda dem innan vi skickar dem till LSTM. Detta görs genom att anropa funktionen `one_hot` under `forward`-passet. Utgångskodaren kommer att vara ett linjärt lager som omvandlar det dolda tillståndet till en one-hot-kodad utgång.\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 vill vi kunna generera textprover. För att göra det kommer vi att definiera funktionen `generate` som producerar en utmatningssträng med längden `size`, med början från startsträngen `start`.\n", "\n", "Så här fungerar det. Först skickar vi hela startsträngen genom nätverket och tar ut tillståndet `s` och nästa förutspådda tecken `out`. Eftersom `out` är one-hot-kodad, använder vi `argmax` för att få indexet för tecknet `nc` i vokabulären, och använder `itos` för att ta reda på det faktiska tecknet och lägga till det i den resulterande listan av tecken `chars`. Denna process för att generera ett tecken upprepas `size` gånger för att generera önskat antal tecken.\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 kör vi träningen! Träningsloopen är nästan samma som i alla våra tidigare exempel, men istället för noggrannhet skriver vi ut genererad text var 1000:e epok.\n", "\n", "Särskild uppmärksamhet måste ägnas åt hur vi beräknar förlusten. Vi behöver beräkna förlusten baserat på en one-hot-kodad output `out` och förväntad text `text_out`, som är listan över teckenindex. Som tur är förväntar funktionen `cross_entropy` o-normaliserad nätverksoutput som första argument och klassnummer som det andra, vilket är precis vad vi har. Den utför också automatisk genomsnittsberäkning över minibatch-storleken.\n", "\n", "Vi begränsar också träningen till `samples_to_train` prover, för att undvika att det tar för lång tid. Vi uppmuntrar dig att experimentera och prova längre träning, eventuellt under flera epoker (i så fall skulle du behöva skapa en ytterligare loop runt denna kod).\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": [ "Det här exemplet genererar redan ganska bra text, men det kan förbättras ytterligare på flera sätt:\n", "\n", "* **Bättre minibatch-generering**. Sättet vi förberedde data för träning var att generera en minibatch från ett prov. Detta är inte optimalt, eftersom minibatcherna har olika storlekar, och vissa av dem kan inte ens genereras eftersom texten är mindre än `nchars`. Dessutom utnyttjar små minibatcher inte GPU:n tillräckligt effektivt. Det vore klokare att ta en stor textbit från alla prover, sedan generera alla in- och utgångspar, blanda dem och skapa minibatcher av samma storlek.\n", "\n", "* **Flerskikts-LSTM**. Det är vettigt att testa 2 eller 3 lager av LSTM-celler. Som vi nämnde i föregående avsnitt, extraherar varje lager av LSTM vissa mönster från texten, och i fallet med en generator på teckennivå kan vi förvänta oss att det lägre LSTM-lagret ansvarar för att extrahera stavelser, medan de högre lagren hanterar ord och ordkombinationer. Detta kan enkelt implementeras genom att skicka ett parameter för antal lager till LSTM-konstruktorn.\n", "\n", "* Du kanske också vill experimentera med **GRU-enheter** och se vilka som presterar bättre, samt med **olika storlekar på dolda lager**. Ett för stort dolt lager kan leda till överanpassning (t.ex. att nätverket lär sig exakt text), medan en mindre storlek kanske inte ger ett bra resultat.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Mjuk textgenerering och temperatur\n", "\n", "I den tidigare definitionen av `generate` valde vi alltid tecknet med högst sannolikhet som nästa tecken i den genererade texten. Detta resulterade ofta i att texten \"cirkulerade\" mellan samma teckensekvenser om och om igen, som i detta exempel:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "Men om vi tittar på sannolikhetsfördelningen för nästa tecken, kan det vara så att skillnaden mellan de högsta sannolikheterna inte är särskilt stor, t.ex. ett tecken kan ha sannolikheten 0,2, ett annat 0,19, osv. Till exempel, när vi letar efter nästa tecken i sekvensen '*play*', kan nästa tecken lika gärna vara ett mellanslag eller **e** (som i ordet *player*).\n", "\n", "Detta leder oss till slutsatsen att det inte alltid är \"rättvist\" att välja tecknet med högst sannolikhet, eftersom att välja det näst högsta fortfarande kan leda till meningsfull text. Det är klokare att **sampla** tecken från sannolikhetsfördelningen som ges av nätverkets output.\n", "\n", "Denna sampling kan göras med hjälp av funktionen `multinomial` som implementerar den så kallade **multinomialfördelningen**. En funktion som implementerar denna **mjuka** textgenerering definieras nedan:\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 introducerat en ytterligare parameter kallad **temperatur**, som används för att indikera hur strikt vi ska hålla oss till den högsta sannolikheten. Om temperaturen är 1,0 gör vi rättvis multinomial sampling, och när temperaturen går mot oändligheten - blir alla sannolikheter lika, och vi väljer nästa tecken slumpmässigt. I exemplet nedan kan vi observera att texten blir meningslös när vi ökar temperaturen för mycket, och den liknar \"cyklisk\" hårdgenererad text när den närmar sig 0.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Ansvarsfriskrivning**: \nDetta dokument har översatts med hjälp av AI-översättningstjänsten [Co-op Translator](https://github.com/Azure/co-op-translator). Även om vi strävar efter noggrannhet, bör det noteras att automatiserade översättningar kan innehålla fel eller brister. Det ursprungliga dokumentet på dess originalspråk bör betraktas som den auktoritativa källan. För kritisk information rekommenderas professionell mänsklig översättning. Vi ansvarar inte för eventuella missförstånd eller feltolkningar som uppstår vid användning av denna översättning.\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:04+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "sv" } }, "nbformat": 4, "nbformat_minor": 4 }