{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Generative Netzwerke\n", "\n", "Rekurrente neuronale Netzwerke (RNNs) und ihre Varianten mit gated Zellen wie Long Short Term Memory Cells (LSTMs) und Gated Recurrent Units (GRUs) bieten einen Mechanismus für Sprachmodellierung, d.h. sie können die Reihenfolge von Wörtern lernen und Vorhersagen für das nächste Wort in einer Sequenz treffen. Dies ermöglicht es uns, RNNs für **generative Aufgaben** zu nutzen, wie z.B. gewöhnliche Textgenerierung, maschinelle Übersetzung und sogar Bildbeschriftung.\n", "\n", "In der RNN-Architektur, die wir in der vorherigen Einheit besprochen haben, produzierte jede RNN-Einheit den nächsten versteckten Zustand als Ausgabe. Wir können jedoch auch eine weitere Ausgabe zu jeder rekurrenten Einheit hinzufügen, die es uns ermöglicht, eine **Sequenz** auszugeben (die genauso lang ist wie die ursprüngliche Sequenz). Darüber hinaus können wir RNN-Einheiten verwenden, die bei jedem Schritt keine Eingabe akzeptieren, sondern lediglich einen Anfangszustandsvektor nehmen und dann eine Sequenz von Ausgaben erzeugen.\n", "\n", "In diesem Notebook konzentrieren wir uns auf einfache generative Modelle, die uns helfen, Text zu generieren. Der Einfachheit halber bauen wir ein **zeichenbasiertes Netzwerk**, das Text Buchstabe für Buchstabe generiert. Während des Trainings müssen wir einen Textkorpus nehmen und ihn in Buchstabenfolgen aufteilen.\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": [ "## Aufbau eines Zeichen-Vokabulars\n", "\n", "Um ein generatives Netzwerk auf Zeichenebene zu erstellen, müssen wir den Text in einzelne Zeichen statt in Wörter aufteilen. Dies kann durch die Definition eines anderen Tokenizers erfolgen:\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": [ "Lassen Sie uns das Beispiel sehen, wie wir den Text aus unserem Datensatz codieren können:\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": [ "## Training eines generativen RNN\n", "\n", "So werden wir das RNN trainieren, um Text zu generieren: Bei jedem Schritt nehmen wir eine Zeichenfolge der Länge `nchars` und lassen das Netzwerk für jedes Eingabezeichen das nächste Ausgabesymbol vorhersagen:\n", "\n", "![Bild, das ein Beispiel für die RNN-Generierung des Wortes 'HELLO' zeigt.](../../../../../lessons/5-NLP/17-GenerativeNetworks/images/rnn-generate.png)\n", "\n", "Je nach Szenario möchten wir möglicherweise auch einige Sonderzeichen einbeziehen, wie zum Beispiel *Ende-der-Sequenz* ``. In unserem Fall möchten wir das Netzwerk jedoch für die endlose Textgenerierung trainieren. Daher legen wir die Größe jeder Sequenz auf `nchars` Tokens fest. Folglich besteht jedes Trainingsexemplar aus `nchars` Eingaben und `nchars` Ausgaben (die Eingabesequenz, um ein Symbol nach links verschoben). Ein Minibatch wird aus mehreren solcher Sequenzen bestehen.\n", "\n", "Die Minibatches werden wir folgendermaßen generieren: Wir nehmen jeden Nachrichtentext der Länge `l` und erzeugen daraus alle möglichen Eingabe-Ausgabe-Kombinationen (es wird `l-nchars` solcher Kombinationen geben). Diese bilden ein Minibatch, und die Größe der Minibatches wird bei jedem Trainingsschritt unterschiedlich sein.\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": [ "Nun definieren wir das Generatornetzwerk. Es kann auf jeder rekurrenten Zelle basieren, die wir in der vorherigen Einheit besprochen haben (einfach, LSTM oder GRU). In unserem Beispiel verwenden wir LSTM.\n", "\n", "Da das Netzwerk Zeichen als Eingabe erhält und die Vokabulärgröße relativ klein ist, benötigen wir keine Embedding-Schicht. Die one-hot-codierte Eingabe kann direkt an die LSTM-Zelle übergeben werden. Da wir jedoch Zeichen als Zahlen übergeben, müssen wir diese vor der Übergabe an die LSTM-Zelle one-hot-codieren. Dies geschieht, indem die Funktion `one_hot` während des `forward`-Durchlaufs aufgerufen wird. Der Ausgabe-Encoder wäre eine lineare Schicht, die den versteckten Zustand in eine one-hot-codierte Ausgabe umwandelt.\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": [ "Während des Trainings möchten wir in der Lage sein, generierten Text zu sampeln. Dazu definieren wir die Funktion `generate`, die eine Ausgabestring der Länge `size` erzeugt, beginnend mit dem Anfangsstring `start`.\n", "\n", "So funktioniert es: Zuerst geben wir den gesamten Startstring durch das Netzwerk und erhalten den Ausgabestatus `s` und das nächste vorhergesagte Zeichen `out`. Da `out` one-hot codiert ist, verwenden wir `argmax`, um den Index des Zeichens `nc` im Vokabular zu erhalten, und nutzen `itos`, um das tatsächliche Zeichen zu ermitteln und es der resultierenden Zeichenliste `chars` hinzuzufügen. Dieser Prozess der Generierung eines Zeichens wird `size`-mal wiederholt, um die gewünschte Anzahl von Zeichen zu erzeugen.\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": [ "Jetzt geht's ans Training! Die Trainingsschleife ist fast dieselbe wie in all unseren vorherigen Beispielen, aber anstelle der Genauigkeit geben wir alle 1000 Epochen einen Ausschnitt des generierten Textes aus.\n", "\n", "Besondere Aufmerksamkeit sollte der Art und Weise gewidmet werden, wie wir den Verlust berechnen. Wir müssen den Verlust basierend auf der One-Hot-codierten Ausgabe `out` und dem erwarteten Text `text_out` berechnen, welcher die Liste der Zeichenindizes darstellt. Glücklicherweise erwartet die Funktion `cross_entropy` als erstes Argument die unnormalisierte Netzwerkausgabe und als zweites die Klassennummer – genau das, was wir haben. Sie führt außerdem eine automatische Mittelung über die Minibatch-Größe durch.\n", "\n", "Wir begrenzen das Training außerdem auf `samples_to_train` Samples, um nicht zu lange warten zu müssen. Wir ermutigen euch, zu experimentieren und längeres Training auszuprobieren, möglicherweise über mehrere Epochen (in diesem Fall müsstet ihr eine weitere Schleife um diesen Code erstellen).\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": [ "Dieses Beispiel erzeugt bereits ziemlich guten Text, aber es gibt mehrere Möglichkeiten, ihn weiter zu verbessern:\n", "\n", "* **Bessere Minibatch-Erstellung**. Die Art und Weise, wie wir die Daten für das Training vorbereitet haben, bestand darin, ein Minibatch aus einer einzelnen Probe zu generieren. Das ist nicht ideal, da die Minibatches alle unterschiedliche Größen haben und einige von ihnen nicht einmal generiert werden können, weil der Text kleiner als `nchars` ist. Außerdem nutzen kleine Minibatches die GPU nicht ausreichend aus. Es wäre klüger, einen großen Textabschnitt aus allen Proben zu nehmen, dann alle Eingabe-Ausgabe-Paare zu generieren, sie zu mischen und Minibatches gleicher Größe zu erstellen.\n", "\n", "* **Mehrschichtige LSTM**. Es macht Sinn, 2 oder 3 Schichten von LSTM-Zellen auszuprobieren. Wie wir in der vorherigen Einheit erwähnt haben, extrahiert jede Schicht eines LSTM bestimmte Muster aus dem Text. Im Fall eines zeichenbasierten Generators können wir erwarten, dass die untere LSTM-Schicht für die Extraktion von Silben verantwortlich ist, während die höheren Schichten für Wörter und Wortkombinationen zuständig sind. Dies kann einfach implementiert werden, indem man den Parameter für die Anzahl der Schichten an den LSTM-Konstruktor übergibt.\n", "\n", "* Es könnte auch interessant sein, mit **GRU-Einheiten** zu experimentieren, um zu sehen, welche besser funktionieren, sowie mit **unterschiedlichen Größen der versteckten Schichten**. Eine zu große versteckte Schicht kann zu Overfitting führen (z. B. lernt das Netzwerk den exakten Text), während eine kleinere Größe möglicherweise keine guten Ergebnisse liefert.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Weiche Textgenerierung und Temperatur\n", "\n", "In der vorherigen Definition von `generate` haben wir immer das Zeichen mit der höchsten Wahrscheinlichkeit als nächstes Zeichen im generierten Text ausgewählt. Dies führte oft dazu, dass sich der Text zwischen denselben Zeichenfolgen immer wieder \"wiederholte\", wie in diesem Beispiel:\n", "```\n", "today of the second the company and a second the company ...\n", "```\n", "\n", "Wenn wir uns jedoch die Wahrscheinlichkeitsverteilung für das nächste Zeichen ansehen, könnte es sein, dass der Unterschied zwischen den höchsten Wahrscheinlichkeiten nicht groß ist, z. B. kann ein Zeichen eine Wahrscheinlichkeit von 0,2 haben, ein anderes - 0,19 usw. Wenn wir beispielsweise das nächste Zeichen in der Sequenz '*play*' suchen, könnte das nächste Zeichen genauso gut ein Leerzeichen oder **e** sein (wie im Wort *player*).\n", "\n", "Das führt uns zu der Erkenntnis, dass es nicht immer \"fair\" ist, das Zeichen mit der höchsten Wahrscheinlichkeit auszuwählen, da die Wahl des zweitwahrscheinlichsten Zeichens dennoch zu sinnvollem Text führen könnte. Es ist klüger, Zeichen aus der Wahrscheinlichkeitsverteilung zu **samplen**, die durch die Netzwerkausgabe vorgegeben wird.\n", "\n", "Dieses Sampling kann mit der Funktion `multinomial` durchgeführt werden, die die sogenannte **multinomiale Verteilung** implementiert. Eine Funktion, die diese **weiche** Textgenerierung umsetzt, ist unten definiert:\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": [ "Wir haben einen weiteren Parameter namens **Temperatur** eingeführt, der angibt, wie strikt wir uns an die höchste Wahrscheinlichkeit halten sollten. Wenn die Temperatur 1,0 beträgt, führen wir eine faire multinomiale Stichprobe durch, und wenn die Temperatur gegen unendlich geht, werden alle Wahrscheinlichkeiten gleich, und wir wählen zufällig das nächste Zeichen aus. Im untenstehenden Beispiel können wir beobachten, dass der Text bedeutungslos wird, wenn wir die Temperatur zu stark erhöhen, und er ähnelt einem \"zyklischen\" hart generierten Text, wenn er sich näher an 0 bewegt.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n---\n\n**Haftungsausschluss**: \nDieses Dokument wurde mit dem KI-Übersetzungsdienst [Co-op Translator](https://github.com/Azure/co-op-translator) übersetzt. Obwohl wir uns um Genauigkeit bemühen, beachten Sie bitte, dass automatisierte Übersetzungen Fehler oder Ungenauigkeiten enthalten können. Das Originaldokument in seiner ursprünglichen Sprache sollte als maßgebliche Quelle betrachtet werden. Für kritische Informationen wird eine professionelle menschliche Übersetzung empfohlen. Wir übernehmen keine Haftung für Missverständnisse oder Fehlinterpretationen, die sich aus der Nutzung dieser Übersetzung ergeben.\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-31T16:53:08+00:00", "source_file": "lessons/5-NLP/17-GenerativeNetworks/GenerativePyTorch.ipynb", "language_code": "de" } }, "nbformat": 4, "nbformat_minor": 4 }