AI-For-Beginners/translations/de/lessons/5-NLP/16-RNN/RNNPyTorch.ipynb

479 lines
25 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": [
"# Rekurrente neuronale Netze\n",
"\n",
"Im vorherigen Modul haben wir reichhaltige semantische Repräsentationen von Text verwendet und einen einfachen linearen Klassifikator auf den Einbettungen aufgebaut. Diese Architektur erfasst die aggregierte Bedeutung der Wörter in einem Satz, berücksichtigt jedoch nicht die **Reihenfolge** der Wörter, da die Aggregationsoperation auf den Einbettungen diese Information aus dem ursprünglichen Text entfernt hat. Da diese Modelle die Wortreihenfolge nicht modellieren können, sind sie nicht in der Lage, komplexere oder mehrdeutige Aufgaben wie Textgenerierung oder Fragebeantwortung zu lösen.\n",
"\n",
"Um die Bedeutung einer Textsequenz zu erfassen, müssen wir eine andere Architektur neuronaler Netze verwenden, die als **rekurrentes neuronales Netz** oder RNN bezeichnet wird. In einem RNN führen wir unseren Satz nacheinander Symbol für Symbol durch das Netzwerk, und das Netzwerk erzeugt einen **Zustand**, den wir dann zusammen mit dem nächsten Symbol erneut in das Netzwerk einspeisen.\n",
"\n",
"Gegeben die Eingabesequenz von Tokens $X_0,\\dots,X_n$, erstellt das RNN eine Sequenz von neuronalen Netzwerkblöcken und trainiert diese Sequenz end-to-end mittels Backpropagation. Jeder Netzwerkblock nimmt ein Paar $(X_i,S_i)$ als Eingabe und erzeugt $S_{i+1}$ als Ergebnis. Der finale Zustand $S_n$ oder die Ausgabe $X_n$ wird in einen linearen Klassifikator eingespeist, um das Ergebnis zu erzeugen. Alle Netzwerkblöcke teilen sich die gleichen Gewichte und werden in einem einzigen Backpropagation-Durchlauf end-to-end trainiert.\n",
"\n",
"Da die Zustandsvektoren $S_0,\\dots,S_n$ durch das Netzwerk weitergegeben werden, kann es die sequentiellen Abhängigkeiten zwischen Wörtern lernen. Zum Beispiel, wenn das Wort *nicht* irgendwo in der Sequenz erscheint, kann es lernen, bestimmte Elemente innerhalb des Zustandsvektors zu negieren, was zu einer Verneinung führt.\n",
"\n",
"> Da die Gewichte aller RNN-Blöcke im Bild geteilt werden, kann dasselbe Bild als ein Block (rechts) mit einer rekurrenten Rückkopplungsschleife dargestellt werden, die den Ausgabestatus des Netzwerks zurück an den Eingang weitergibt.\n",
"\n",
"Schauen wir uns an, wie rekurrente neuronale Netze uns dabei helfen können, unser Nachrichten-Dataset zu klassifizieren.\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",
"from torchnlp import *\n",
"train_dataset, test_dataset, classes, vocab = load_dataset()\n",
"vocab_size = len(vocab)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Einfacher RNN-Klassifikator\n",
"\n",
"Im Fall eines einfachen RNN ist jede rekurrente Einheit ein einfaches lineares Netzwerk, das einen zusammengefügten Eingabevektor und Zustandsvektor aufnimmt und einen neuen Zustandsvektor erzeugt. PyTorch repräsentiert diese Einheit mit der Klasse `RNNCell`, und ein Netzwerk solcher Zellen als `RNN`-Schicht.\n",
"\n",
"Um einen RNN-Klassifikator zu definieren, wenden wir zunächst eine Embedding-Schicht an, um die Dimensionalität des Eingabevokabulars zu reduzieren, und fügen dann eine RNN-Schicht darüber hinzu:\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"class RNNClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.rnn = torch.nn.RNN(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" x,h = self.rnn(x)\n",
" return self.fc(x.mean(dim=1))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Hinweis:** Wir verwenden hier eine untrainierte Einbettungsschicht zur Vereinfachung, aber für noch bessere Ergebnisse können wir eine vortrainierte Einbettungsschicht mit Word2Vec- oder GloVe-Einbettungen verwenden, wie in der vorherigen Einheit beschrieben. Für ein besseres Verständnis könnten Sie den Code anpassen, um mit vortrainierten Einbettungen zu arbeiten.\n",
"\n",
"In unserem Fall verwenden wir einen gepolsterten Datenlader, sodass jede Charge eine Anzahl gepolsterter Sequenzen gleicher Länge enthält. Die RNN-Schicht nimmt die Sequenz von Einbettungstensoren und erzeugt zwei Ausgaben:\n",
"* $x$ ist eine Sequenz von RNN-Zellenausgaben bei jedem Schritt\n",
"* $h$ ist der finale versteckte Zustand für das letzte Element der Sequenz\n",
"\n",
"Anschließend wenden wir einen vollständig verbundenen linearen Klassifikator an, um die Anzahl der Klassen zu bestimmen.\n",
"\n",
"> **Hinweis:** RNNs sind recht schwierig zu trainieren, da die Anzahl der Schichten, die bei der Rückwärtsausbreitung beteiligt sind, ziemlich groß wird, sobald die RNN-Zellen entlang der Sequenzlänge entrollt werden. Daher müssen wir eine kleine Lernrate wählen und das Netzwerk auf einem größeren Datensatz trainieren, um gute Ergebnisse zu erzielen. Dies kann ziemlich lange dauern, daher wird die Verwendung einer GPU bevorzugt.\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.3090625\n",
"6400: acc=0.38921875\n",
"9600: acc=0.4590625\n",
"12800: acc=0.511953125\n",
"16000: acc=0.5506875\n",
"19200: acc=0.57921875\n",
"22400: acc=0.6070089285714285\n",
"25600: acc=0.6304296875\n",
"28800: acc=0.6484027777777778\n",
"32000: acc=0.66509375\n",
"35200: acc=0.6790056818181818\n",
"38400: acc=0.6929166666666666\n",
"41600: acc=0.7035817307692308\n",
"44800: acc=0.7137276785714286\n",
"48000: acc=0.72225\n",
"51200: acc=0.73001953125\n",
"54400: acc=0.7372794117647059\n",
"57600: acc=0.7436631944444444\n",
"60800: acc=0.7503947368421052\n",
"64000: acc=0.75634375\n",
"67200: acc=0.7615773809523809\n",
"70400: acc=0.7662642045454545\n",
"73600: acc=0.7708423913043478\n",
"76800: acc=0.7751822916666666\n",
"80000: acc=0.7790625\n",
"83200: acc=0.7825\n",
"86400: acc=0.7858564814814815\n",
"89600: acc=0.7890513392857142\n",
"92800: acc=0.7920474137931034\n",
"96000: acc=0.7952708333333334\n",
"99200: acc=0.7982258064516129\n",
"102400: acc=0.80099609375\n",
"105600: acc=0.8037594696969697\n",
"108800: acc=0.8060569852941176\n"
]
}
],
"source": [
"train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)\n",
"net = RNNClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch(net,train_loader, lr=0.001)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Langzeit-Kurzzeitgedächtnis (LSTM)\n",
"\n",
"Eines der Hauptprobleme klassischer RNNs ist das sogenannte **Problem der verschwindenden Gradienten**. Da RNNs in einem einzigen Backpropagation-Durchlauf Ende-zu-Ende trainiert werden, fällt es ihnen schwer, den Fehler bis zu den ersten Schichten des Netzwerks weiterzuleiten. Dadurch kann das Netzwerk keine Beziehungen zwischen weit entfernten Tokens lernen. Eine Möglichkeit, dieses Problem zu umgehen, besteht darin, eine **explizite Zustandsverwaltung** durch den Einsatz sogenannter **Gates** einzuführen. Zwei der bekanntesten Architekturen dieser Art sind: **Langzeit-Kurzzeitgedächtnis** (LSTM) und **Gated Relay Unit** (GRU).\n",
"\n",
"![Bild, das eine Beispielzelle eines Langzeit-Kurzzeitgedächtnisses zeigt](../../../../../lessons/5-NLP/16-RNN/images/long-short-term-memory-cell.svg)\n",
"\n",
"Ein LSTM-Netzwerk ist ähnlich wie ein RNN organisiert, aber es gibt zwei Zustände, die von Schicht zu Schicht weitergegeben werden: der aktuelle Zustand $c$ und der versteckte Vektor $h$. In jeder Einheit wird der versteckte Vektor $h_i$ mit der Eingabe $x_i$ verknüpft, und sie steuern über **Gates**, was mit dem Zustand $c$ geschieht. Jedes Gate ist ein neuronales Netzwerk mit einer Sigmoid-Aktivierung (Ausgabe im Bereich $[0,1]$), das als bitweises Maskieren betrachtet werden kann, wenn es mit dem Zustandsvektor multipliziert wird. Es gibt folgende Gates (von links nach rechts im obigen Bild):\n",
"* **Vergessens-Gate**: Es nimmt den versteckten Vektor und bestimmt, welche Komponenten des Vektors $c$ wir vergessen und welche wir durchlassen müssen.\n",
"* **Eingabe-Gate**: Es nimmt Informationen aus der Eingabe und dem versteckten Vektor und fügt sie dem Zustand hinzu.\n",
"* **Ausgabe-Gate**: Es transformiert den Zustand über eine lineare Schicht mit $\\tanh$-Aktivierung und wählt dann einige seiner Komponenten mithilfe des versteckten Vektors $h_i$ aus, um den neuen Zustand $c_{i+1}$ zu erzeugen.\n",
"\n",
"Die Komponenten des Zustands $c$ können als Flags betrachtet werden, die ein- und ausgeschaltet werden können. Zum Beispiel, wenn wir im Sequenzkontext den Namen *Alice* begegnen, könnten wir annehmen, dass es sich um eine weibliche Figur handelt, und das Flag im Zustand setzen, dass wir ein weibliches Substantiv im Satz haben. Wenn wir später auf die Phrase *und Tom* stoßen, setzen wir das Flag, dass wir ein Plural-Substantiv haben. Durch die Manipulation des Zustands können wir also theoretisch grammatikalische Eigenschaften von Satzteilen verfolgen.\n",
"\n",
"> **Hinweis**: Eine großartige Ressource, um die Interna von LSTMs zu verstehen, ist der Artikel [Understanding LSTM Networks](https://colah.github.io/posts/2015-08-Understanding-LSTMs/) von Christopher Olah.\n",
"\n",
"Obwohl die interne Struktur einer LSTM-Zelle komplex erscheinen mag, verbirgt PyTorch diese Implementierung in der `LSTMCell`-Klasse und stellt das `LSTM`-Objekt bereit, um die gesamte LSTM-Schicht darzustellen. Daher wird die Implementierung eines LSTM-Klassifikators der eines einfachen RNNs, das wir oben gesehen haben, ziemlich ähnlich sein:\n"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"class LSTMClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n",
" self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" x,(h,c) = self.rnn(x)\n",
" return self.fc(h[-1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.259375\n",
"6400: acc=0.25859375\n",
"9600: acc=0.26177083333333334\n",
"12800: acc=0.2784375\n",
"16000: acc=0.313\n",
"19200: acc=0.3528645833333333\n",
"22400: acc=0.3965625\n",
"25600: acc=0.4385546875\n",
"28800: acc=0.4752777777777778\n",
"32000: acc=0.505375\n",
"35200: acc=0.5326704545454546\n",
"38400: acc=0.5557552083333334\n",
"41600: acc=0.5760817307692307\n",
"44800: acc=0.5954910714285714\n",
"48000: acc=0.6118333333333333\n",
"51200: acc=0.62681640625\n",
"54400: acc=0.6404779411764706\n",
"57600: acc=0.6520138888888889\n",
"60800: acc=0.662828947368421\n",
"64000: acc=0.673546875\n",
"67200: acc=0.6831547619047619\n",
"70400: acc=0.6917897727272727\n",
"73600: acc=0.6997146739130434\n",
"76800: acc=0.707109375\n",
"80000: acc=0.714075\n",
"83200: acc=0.7209134615384616\n",
"86400: acc=0.727037037037037\n",
"89600: acc=0.7326674107142858\n",
"92800: acc=0.7379633620689655\n",
"96000: acc=0.7433645833333333\n",
"99200: acc=0.7479032258064516\n",
"102400: acc=0.752119140625\n",
"105600: acc=0.7562405303030303\n",
"108800: acc=0.76015625\n",
"112000: acc=0.7641339285714286\n",
"115200: acc=0.7677777777777778\n",
"118400: acc=0.7711233108108108\n"
]
},
{
"data": {
"text/plain": [
"(0.03487814127604167, 0.7728)"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"net = LSTMClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch(net,train_loader, lr=0.001)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Gepackte Sequenzen\n",
"\n",
"In unserem Beispiel mussten wir alle Sequenzen im Minibatch mit Nullvektoren auffüllen. Dies führt zwar zu einem gewissen Speicherverbrauch, aber bei RNNs ist es noch kritischer, dass zusätzliche RNN-Zellen für die aufgefüllten Eingabeelemente erstellt werden. Diese nehmen am Training teil, tragen jedoch keine wichtigen Eingabeinformationen. Es wäre viel besser, das RNN nur bis zur tatsächlichen Sequenzlänge zu trainieren.\n",
"\n",
"Um dies zu erreichen, wurde in PyTorch ein spezielles Format zur Speicherung gepolsterter Sequenzen eingeführt. Angenommen, wir haben ein gepolstertes Eingabe-Minigruppenbatch, das so aussieht:\n",
"```\n",
"[[1,2,3,4,5],\n",
" [6,7,8,0,0],\n",
" [9,0,0,0,0]]\n",
"```\n",
"Hierbei repräsentiert 0 die gepolsterten Werte, und der tatsächliche Längenvektor der Eingabesequenzen ist `[5,3,1]`.\n",
"\n",
"Um ein RNN effektiv mit gepolsterten Sequenzen zu trainieren, möchten wir die erste Gruppe von RNN-Zellen mit einem großen Minibatch (`[1,6,9]`) starten, dann jedoch die Verarbeitung der dritten Sequenz beenden und mit verkleinerten Minibatches (`[2,7]`, `[3,8]`) weitermachen, und so weiter. Eine gepackte Sequenz wird daher als ein einziger Vektor dargestellt in unserem Fall `[1,6,9,2,7,3,8,4,5]` und einem Längenvektor (`[5,3,1]`), aus dem wir das ursprüngliche gepolsterte Minibatch leicht rekonstruieren können.\n",
"\n",
"Um eine gepackte Sequenz zu erzeugen, können wir die Funktion `torch.nn.utils.rnn.pack_padded_sequence` verwenden. Alle rekurrenten Schichten, einschließlich RNN, LSTM und GRU, unterstützen gepackte Sequenzen als Eingabe und erzeugen gepackte Ausgaben, die mit `torch.nn.utils.rnn.pad_packed_sequence` dekodiert werden können.\n",
"\n",
"Um eine gepackte Sequenz erzeugen zu können, müssen wir den Längenvektor an das Netzwerk übergeben. Daher benötigen wir eine andere Funktion, um Minibatches vorzubereiten:\n"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"def pad_length(b):\n",
" # build vectorized sequence\n",
" v = [encode(x[1]) for x in b]\n",
" # compute max length of a sequence in this minibatch and length sequence itself\n",
" len_seq = list(map(len,v))\n",
" l = max(len_seq)\n",
" return ( # tuple of three tensors - labels, padded features, length sequence\n",
" torch.LongTensor([t[0]-1 for t in b]),\n",
" torch.stack([torch.nn.functional.pad(torch.tensor(t),(0,l-len(t)),mode='constant',value=0) for t in v]),\n",
" torch.tensor(len_seq)\n",
" )\n",
"\n",
"train_loader_len = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=pad_length, shuffle=True)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Das tatsächliche Netzwerk wäre dem oben genannten `LSTMClassifier` sehr ähnlich, aber der `forward`-Durchlauf erhält sowohl das gepolsterte Minibatch als auch den Vektor der Sequenzlängen. Nach der Berechnung des Embeddings erstellen wir eine gepackte Sequenz, geben sie an die LSTM-Schicht weiter und entpacken anschließend das Ergebnis.\n",
"\n",
"> **Hinweis**: Tatsächlich verwenden wir das entpackte Ergebnis `x` nicht, da wir die Ausgabe aus den versteckten Schichten für die folgenden Berechnungen nutzen. Daher könnten wir das Entpacken in diesem Code vollständig entfernen. Der Grund, warum wir es hier belassen, ist, dass Sie diesen Code bei Bedarf leicht anpassen können, falls Sie die Netzwerkausgabe in weiteren Berechnungen verwenden möchten.\n"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [],
"source": [
"class LSTMPackClassifier(torch.nn.Module):\n",
" def __init__(self, vocab_size, embed_dim, hidden_dim, num_class):\n",
" super().__init__()\n",
" self.hidden_dim = hidden_dim\n",
" self.embedding = torch.nn.Embedding(vocab_size, embed_dim)\n",
" self.embedding.weight.data = torch.randn_like(self.embedding.weight.data)-0.5\n",
" self.rnn = torch.nn.LSTM(embed_dim,hidden_dim,batch_first=True)\n",
" self.fc = torch.nn.Linear(hidden_dim, num_class)\n",
"\n",
" def forward(self, x, lengths):\n",
" batch_size = x.size(0)\n",
" x = self.embedding(x)\n",
" pad_x = torch.nn.utils.rnn.pack_padded_sequence(x,lengths,batch_first=True,enforce_sorted=False)\n",
" pad_x,(h,c) = self.rnn(pad_x)\n",
" x, _ = torch.nn.utils.rnn.pad_packed_sequence(pad_x,batch_first=True)\n",
" return self.fc(h[-1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": []
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3200: acc=0.285625\n",
"6400: acc=0.33359375\n",
"9600: acc=0.3876041666666667\n",
"12800: acc=0.44078125\n",
"16000: acc=0.4825\n",
"19200: acc=0.5235416666666667\n",
"22400: acc=0.5559821428571429\n",
"25600: acc=0.58609375\n",
"28800: acc=0.6116666666666667\n",
"32000: acc=0.63340625\n",
"35200: acc=0.6525284090909091\n",
"38400: acc=0.668515625\n",
"41600: acc=0.6822596153846154\n",
"44800: acc=0.6948214285714286\n",
"48000: acc=0.7052708333333333\n",
"51200: acc=0.71521484375\n",
"54400: acc=0.7239889705882353\n",
"57600: acc=0.7315277777777778\n",
"60800: acc=0.7388486842105263\n",
"64000: acc=0.74571875\n",
"67200: acc=0.7518303571428572\n",
"70400: acc=0.7576988636363636\n",
"73600: acc=0.7628940217391305\n",
"76800: acc=0.7681510416666667\n",
"80000: acc=0.7728125\n",
"83200: acc=0.7772235576923077\n",
"86400: acc=0.7815393518518519\n",
"89600: acc=0.7857700892857142\n",
"92800: acc=0.7895043103448276\n",
"96000: acc=0.7930520833333333\n",
"99200: acc=0.7959072580645161\n",
"102400: acc=0.798994140625\n",
"105600: acc=0.802064393939394\n",
"108800: acc=0.8051378676470589\n",
"112000: acc=0.8077857142857143\n",
"115200: acc=0.8104600694444445\n",
"118400: acc=0.8128293918918919\n"
]
},
{
"data": {
"text/plain": [
"(0.029785829671223958, 0.8138166666666666)"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"net = LSTMPackClassifier(vocab_size,64,32,len(classes)).to(device)\n",
"train_epoch_emb(net,train_loader_len, lr=0.001,use_pack_sequence=True)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"> **Hinweis:** Sie haben möglicherweise den Parameter `use_pack_sequence` bemerkt, den wir an die Trainingsfunktion übergeben. Derzeit erfordert die Funktion `pack_padded_sequence`, dass der Längensequenz-Tensor auf dem CPU-Gerät ist, und daher muss die Trainingsfunktion vermeiden, die Längensequenz-Daten während des Trainings auf die GPU zu verschieben. Sie können die Implementierung der Funktion `train_emb` in der Datei [`torchnlp.py`](../../../../../lessons/5-NLP/16-RNN/torchnlp.py) einsehen.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Bidirektionale und mehrschichtige RNNs\n",
"\n",
"In unseren Beispielen haben alle rekurrenten Netzwerke in eine Richtung gearbeitet, von Anfang bis Ende einer Sequenz. Das erscheint natürlich, da es der Art und Weise ähnelt, wie wir lesen und Sprache hören. Allerdings haben wir in vielen praktischen Fällen zufälligen Zugriff auf die Eingabesequenz, weshalb es sinnvoll sein könnte, die rekurrente Berechnung in beide Richtungen auszuführen. Solche Netzwerke werden als **bidirektionale** RNNs bezeichnet, und sie können erstellt werden, indem man den Parameter `bidirectional=True` an den Konstruktor von RNN/LSTM/GRU übergibt.\n",
"\n",
"Bei der Arbeit mit einem bidirektionalen Netzwerk benötigen wir zwei Zustandsvektoren, einen für jede Richtung. PyTorch kodiert diese Vektoren als einen Vektor mit doppelter Größe, was sehr praktisch ist, da man den resultierenden Zustandsvektor normalerweise an eine vollständig verbundene lineare Schicht übergibt. Man muss lediglich diese Größenänderung berücksichtigen, wenn man die Schicht erstellt.\n",
"\n",
"Ein rekurrentes Netzwerk, sei es eindirektional oder bidirektional, erfasst bestimmte Muster innerhalb einer Sequenz und kann diese entweder im Zustandsvektor speichern oder in die Ausgabe weitergeben. Ähnlich wie bei konvolutionalen Netzwerken können wir eine weitere rekurrente Schicht auf die erste aufbauen, um Muster höherer Ordnung zu erfassen, die aus den von der ersten Schicht extrahierten Mustern niedriger Ordnung bestehen. Dies führt uns zum Konzept des **mehrschichtigen RNN**, das aus zwei oder mehr rekurrenten Netzwerken besteht, wobei die Ausgabe der vorherigen Schicht als Eingabe an die nächste Schicht weitergegeben wird.\n",
"\n",
"![Bild, das ein mehrschichtiges Long-Short-Term-Memory-RNN zeigt](../../../../../lessons/5-NLP/16-RNN/images/multi-layer-lstm.jpg)\n",
"\n",
"*Bild aus [diesem großartigen Beitrag](https://towardsdatascience.com/from-a-lstm-cell-to-a-multilayer-lstm-network-with-pytorch-2899eb5696f3) von Fernando López*\n",
"\n",
"PyTorch macht die Konstruktion solcher Netzwerke einfach, da man lediglich den Parameter `num_layers` an den RNN/LSTM/GRU-Konstruktor übergeben muss, um mehrere rekurrente Schichten automatisch zu erstellen. Das bedeutet auch, dass die Größe des Zustandsvektors proportional zunimmt, und man muss dies berücksichtigen, wenn man die Ausgabe der rekurrenten Schichten verarbeitet.\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## RNNs für andere Aufgaben\n",
"\n",
"In dieser Einheit haben wir gesehen, dass RNNs für die Sequenzklassifikation verwendet werden können. Tatsächlich können sie jedoch viele weitere Aufgaben bewältigen, wie Textgenerierung, maschinelle Übersetzung und mehr. Diese Aufgaben werden wir in der nächsten Einheit betrachten.\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": "522ee52ae3d5ae933e283286254e9a55",
"translation_date": "2025-08-31T17:07:58+00:00",
"source_file": "lessons/5-NLP/16-RNN/RNNPyTorch.ipynb",
"language_code": "de"
}
},
"nbformat": 4,
"nbformat_minor": 2
}