Kontext und RNNs — warum Reihenfolge zählt
Artikel 5 von 8 · Serie: Wie LLMs funktionieren
Wenn wir einen Satz lesen, bauen wir seine Bedeutung Wort für Wort auf. Nach dem dritten Wort wissen wir mehr als nach dem ersten, nach dem zehnten nochmal mehr. Jedes neue Wort bekommt seine Bedeutung erst durch die, die vorher kamen. Genau das ist das fundamentale Merkmal von Sprache: Reihenfolge und Kontext sind die Bedeutung.
Ein kleines Experiment: das Wort „Bank". Für sich allein ein Rätsel, es könnte eine Sitzgelegenheit sein oder ein Finanzinstitut. Wir stellen den Satz drumherum:
Er brauchte Bargeld und ging zur Bank.
Er setzte sich auf die Bank im Park.
Dasselbe Wort, derselbe Embedding-Vektor aus Artikel 2. Aber zwei komplett verschiedene Bedeutungen. Und der Unterschied entsteht ausschließlich aus dem was vorher stand.
Das Modell das wir in Artikel 4 gebaut haben, kann das nicht leisten. Es sieht immer nur ein einziges Token auf einmal. Input ist genau ein Embedding, Output eine Vorhersage. Dem Netz ist völlig unbekannt was zwei Sätze vorher stand, und sogar was ein Wort vorher stand.
Wir brauchen also ein Netz mit Gedächtnis. Die erste Idee die in der Deep-Learning-Welt dafür funktionierte, war das Rekurrente Neuronale Netz, kurz RNN. Dieser Artikel zeigt wie RNNs arbeiten, wo sie glänzten, wo sie scheiterten, und warum sie heute weitgehend durch die nächste Idee, den Transformer, ersetzt sind.
Die Grundidee: ein Notizblock fürs Netz
Wenn man Sprache Token für Token verarbeiten will, braucht das Netz irgendeinen Ort an dem es notieren kann, was es bisher gesehen hat. Eine Art Kurzzeitgedächtnis.
In einem RNN heißt dieses Gedächtnis Hidden State. Wir können ihn uns als einen Notizblock vorstellen, den das Netz bei sich trägt. Beim ersten Token ist der Block leer. Nach jedem neuen Token schreibt das Netz darauf, und es liest auch wieder davon ab, bevor es seine nächste Vorhersage trifft.
Die Rechnung die dabei passiert, ist konzeptionell denkbar einfach. Bei jedem Zeitschritt:
- Nimm das aktuelle Token als Input.
- Nimm den alten Notizblock.
- Kombiniere beides zu einem neuen Notizblock.
- Leite aus dem neuen Notizblock die Vorhersage ab.
Fertig. Das ist das ganze Prinzip. Die Komplexität steckt in Schritt 3, wo aus Input und altem Zustand ein neuer Zustand wird. Aber die Mechanik ist im Kern die eines einzelnen Schritts aus Artikel 3: gewichtete Summe plus Aktivierungsfunktion. Nur dass jetzt zwei Eingaben reinkommen statt einer.
Wie das in Code aussieht
Bevor wir in Formeln abtauchen, hier der Code für einen einzigen RNN-Schritt. Drei Gewichtsmatrizen, ein bisschen numpy, mehr nicht.
import numpy as np
# Drei Gewichtsmatrizen, die das Netz lernen wird:
# W_xh: verarbeitet den aktuellen Input
# W_hh: verarbeitet den Notizblock
# W_hy: leitet aus dem Notizblock die Vorhersage ab
#
# Plus zwei Biases, wie aus Artikel 3 bekannt.
def rnn_step(x, h):
"""Ein einzelner RNN-Schritt."""
h_new = np.tanh(x @ W_xh + h @ W_hh + b_h) # neuer Notizblock
y = h_new @ W_hy + b_y # Vorhersage
return h_new, y
Drei Zeilen Rechnung, das ist die RNN-Zelle. Die erste Zeile ist die Kernidee:
x @ W_xh: das aktuelle Token wird durch eine erste Gewichtsmatrix geschickth @ W_hh: der alte Notizblock wird durch eine zweite Matrix geschickt- Beide Ergebnisse werden addiert, plus Bias, plus Aktivierung
Die Addition ist der Clou. Sie fusioniert den aktuellen Input mit dem, was vorher war. W_hh, die Matrix die den Notizblock transformiert, ist das eigentliche Gedächtnis-Organ des Netzes. Sie entscheidet wie Information von einem Schritt zum nächsten wandert, was erhalten bleibt und was verloren geht.
Jetzt eine kleine Sequenz durchlaufen lassen:
np.random.seed(42)
d_in, d_hidden, d_out = 4, 8, 5
W_xh = np.random.randn(d_in, d_hidden) * 0.3
W_hh = np.random.randn(d_hidden, d_hidden) * 0.3
W_hy = np.random.randn(d_hidden, d_out) * 0.3
b_h, b_y = np.zeros(d_hidden), np.zeros(d_out)
# Eine Sequenz aus 4 Tokens (zufällige Embeddings)
sequence = np.random.randn(4, d_in)
# Notizblock beginnt leer
h = np.zeros(d_hidden)
# Token für Token durchgehen
for t, x in enumerate(sequence):
h, y = rnn_step(x, h)
print(f"Schritt {t}: Notizblock[:3] = {h[:3].round(3)}")
Schritt 0: Notizblock[:3] = [ 0.082 -0.217 0.435]
Schritt 1: Notizblock[:3] = [-0.168 0.091 0.312]
Schritt 2: Notizblock[:3] = [ 0.273 -0.425 0.602]
Schritt 3: Notizblock[:3] = [-0.038 0.315 0.148]
Der Notizblock verändert sich mit jedem Schritt. Und, das ist das Entscheidende, sein Inhalt bei Schritt 3 hängt nicht nur vom vierten Token ab, sondern auch vom dritten, zweiten und ersten. Die Information fließt durch die Zeit.
Dieselbe Zelle, immer wieder
Eine Sache die gerade passiert, sieht man dem Code nicht sofort an: In allen vier Schritten benutzen wir dieselben Gewichtsmatrizen. Kein frisches W_xh für Schritt 2, kein neues W_hh für Schritt 3. Immer das gleiche Set.
Das ist ein fundamentaler Unterschied zu einem tiefen MLP aus Artikel 3, wo jede Schicht ihre eigenen Gewichte hätte. Ein RNN recycelt seine Gewichte über die Zeit. Konzeptuell ist es eine Zelle, die immer wieder neu auf dasselbe Problem angewandt wird, Token für Token, bis die Sequenz durch ist.
Genau deshalb kann ein RNN mit Sequenzen beliebiger Länge umgehen. Ob die Eingabe drei Tokens lang ist oder dreihundert, das Netz braucht keine neuen Parameter. Es läuft einfach länger im Kreis.
In Grafiken zeichnet man ein RNN oft auf zwei Weisen. Links als eine Zelle mit einem Rückkopplungs-Pfeil zu sich selbst. Rechts „ausgefaltet" (englisch: unrolled), als Sequenz von Kopien der gleichen Zelle, eine pro Zeitschritt, verbunden durch den Notizblock-Pfeil. Beide Darstellungen meinen dasselbe Netz, die ausgefaltete Variante ist nur hilfreicher, um sich das Training vorzustellen.
Training: Fehler reist zurück in die Zeit
Nehmen wir an, das Netz soll für einen Satz bei jedem Schritt das nächste Wort vorhersagen. Nach dem zehnten Wort vergleicht es seine Vorhersage mit dem tatsächlichen elften Wort, berechnet den Fehler, und das Spiel läuft wie in Artikel 4: Fehler messen, Gradienten berechnen, Gewichte updaten.
Der Unterschied ist: Das „Netz" durch das der Fehler zurückpropagiert wird, ist jetzt die ausgefaltete Version. Zehn Kopien der Zelle hintereinander. Der Algorithmus heißt Backpropagation Through Time, BPTT. Klingt nach Science Fiction, ist aber im Kern derselbe Algorithmus wie in Artikel 4, nur durch eine Sequenz statt durch gestapelte Layer.
Eine Besonderheit gibt es: Weil alle Zeitschritte dieselben Gewichte benutzen, sammeln sich beim Backward Pass die Gradient-Beiträge aller Zeitschritte auf, und zwar für dieselben Parameter. Das ist kein Bug, sondern Feature. Das Netz lernt aus jeder Position in der Sequenz gleichzeitig, und genau dadurch wird seine einzige RNN-Zelle zu etwas das über die Zeit generalisieren kann.
Die Gradienten durch die Zeit, formaler
Der Hidden State h_t geht an zwei Stellen in den Gesamt-Loss ein: direkt, über den Output y_t bei Schritt t, und indirekt, weil er in die Berechnung von h_{t+1} einfließt und damit auch alle späteren Schritte beeinflusst.
Beim Rückwärtspropagieren summieren sich diese Beiträge:
dL/dh_t = dL_t/dh_t + (dL/dh_{t+1}) · dh_{t+1}/dh_t
Das ist eine rekursive Formel, die man vom Ende der Sequenz zurück zum Anfang abrollt.
Für die Gewichtsmatrix W_hh gilt entsprechend: Der Gradient ist die Summe der Beiträge an jedem Zeitschritt. Das Netz lernt aus jeder Position gleichzeitig, und die Updates konsolidieren sich in eine einzige, geteilte Matrix.
Jetzt kommt die Stelle, an der RNNs in der Praxis jahrelang scheiterten.
Das Stille-Post-Problem
Bei jedem Schritt wird der Notizblock mit derselben Matrix W_hh transformiert. Bei einer Sequenz von 30 Tokens passiert diese Transformation also 30-mal hintereinander. Beim Training, wo der Gradient rückwärts durch die Zeit fließt, wird der Gradient jedes Mal ebenfalls mit (einer Variante von) W_hh multipliziert.
Das ist das Problem. Stellen wir uns Stille Post vor: eine Nachricht wird 30-mal weitergeflüstert. Am Ende kommt meist nichts mehr vom Original an. Genau das passiert auch mit Gradienten durch die Zeit.
Wenn W_hh Werte im Schnitt kleiner als 1 produziert, schrumpft der Gradient mit jedem Schritt. Nach 20 Schritten ist er praktisch Null. Das Netz bekommt keinen Trainings-Signal mehr für Inputs die weit in der Vergangenheit liegen. Das ist der berühmte Vanishing Gradient.
Werden die Werte im Schnitt größer als 1, explodiert der Gradient stattdessen. Die Updates werden astronomisch, das Training wird instabil, NaN-Werte tauchen auf. Exploding Gradient.
Das praktische Ergebnis: Ein einfaches RNN lernt kurze Zusammenhänge gut. „Welches Wort folgt direkt auf ein Verb?", solche Fragen sind kein Problem. Lange Zusammenhänge scheitern aber. „Welches Subjekt wurde fünfzehn Worte zuvor eingeführt?", dafür reicht das Gedächtnis nicht. Für Sprache ist das eine schwere Einschränkung. Sätze, Absätze, Dokumente haben ständig Bezüge über lange Distanzen.
Exploding Gradients lassen sich mit einem pragmatischen Trick in den Griff bekommen (man begrenzt einfach die Gradient-Größe vor jedem Update, genannt Gradient Clipping). Vanishing Gradients waren das härtere Problem. Das verlangte einen architektonischen Umbau.
LSTMs: ein Notizblock mit Markierungen
Die Lösung kam 1997 von Sepp Hochreiter und Jürgen Schmidhuber. Das Papier wurde damals kaum beachtet und gilt heute als einer der wichtigsten Beiträge zum Machine Learning überhaupt. Die Architektur heißt Long Short-Term Memory, LSTM.
Die Idee klingt mit unserer Notizblock-Metapher ziemlich natürlich. Das Problem beim einfachen RNN ist, dass der ganze Notizblock bei jedem Schritt komplett neu geschrieben wird. Wenn dabei aus Versehen wichtige alte Information übermalt wird, ist sie weg. Und weil das bei jedem Schritt passiert, hält sich nichts lange.
Ein LSTM ergänzt den Notizblock um eine zweite Ebene: einen Cell State, eine Art Langzeitspeicher neben dem Notizblock. Und drei kleine Entscheider, genannt Gates:
- Das Forget-Gate entscheidet für jede Stelle im Langzeitspeicher: „löschen oder behalten?"
- Das Input-Gate entscheidet: „gibt es neues, was reingeschrieben werden soll?"
- Das Output-Gate entscheidet wie der aktuelle Langzeitspeicher in den Notizblock überführt wird
Jedes dieser Gates ist selbst ein kleines neuronales Netz, und sie lernen ihre Entscheidungen aus den Daten. Der Clou: Der Langzeitspeicher fließt durch die Zeit, ohne bei jedem Schritt komplett neu gemischt zu werden. Information kann über hunderte Schritte erhalten bleiben, solange das Forget-Gate sie nicht aktiv löscht. Das Stille-Post-Problem ist damit nicht ganz verschwunden, aber deutlich abgemildert.
In Code sieht ein LSTM-Schritt so aus (konzeptionell, leicht vereinfacht):
def lstm_step(x, h, c):
"""Vereinfachter LSTM-Schritt."""
z = np.concatenate([x, h])
f = sigmoid(z @ W_f + b_f) # Forget-Gate
i = sigmoid(z @ W_i + b_i) # Input-Gate
o = sigmoid(z @ W_o + b_o) # Output-Gate
g = np.tanh( z @ W_g + b_g) # Kandidat fürs Schreiben
c_new = f * c + i * g # Langzeitspeicher updaten
h_new = o * np.tanh(c_new) # Notizblock ableiten
return h_new, c_new
Das sieht nach viel aus, und ist es auch. Ein LSTM hat etwa viermal so viele Parameter pro Zelle wie ein einfaches RNN. Die Belohnung: Sequenzen mit hundert und mehr Tokens werden trainierbar.
Eine verwandte Vereinfachung ist die GRU (Gated Recurrent Unit), 2014 von Kyunghyun Cho et al. vorgestellt. Sie hat nur zwei Gates und keinen separaten Langzeitspeicher, erreicht in vielen Aufgaben aber vergleichbare Ergebnisse wie LSTM bei weniger Parametern. In der Praxis war die Wahl zwischen beiden oft Geschmackssache.
Die goldene Ära: 2014 bis 2017
Mit LSTMs (und etwas später GRUs) eroberten RNNs zwischen 2014 und 2017 praktisch jede NLP-Aufgabe. Das dominante Muster hieß Sequence-to-Sequence (Ilya Sutskever et al): Zwei RNNs, ein Encoder liest die Eingabesequenz und produziert einen finalen Zustand, ein Decoder startet mit diesem Zustand und generiert die Ausgabesequenz. Auf diesem Muster basierten Übersetzer (Google Neural Machine Translation, 2016), Bildbeschreiber, Sprachsynthese und erste Sprachmodelle.
Andrej Karpathys Blog-Post „The Unreasonable Effectiveness of Recurrent Neural Networks" von 2015 war für viele der Moment an dem RNNs vom Forschungsthema zum praktisch spielbaren Werkzeug wurden. Er zeigte Character-Level-RNNs die Shakespeare-artigen Text generierten, LaTeX-Pseudocode, oder Linux-Kernel-artigen C-Code. Albern? Klar. Aber sie machten sichtbar: Diese Netze lernen tatsächlich Struktur, nicht bloß Wortfrequenzen.
Gleichzeitig wurden die Grenzen immer deutlicher. Zwei Probleme bekam man architektonisch nicht weg:
1. Lange Abhängigkeiten blieben schwierig. LSTMs waren eine große Verbesserung gegenüber einfachen RNNs. Aber auch sie bekamen Mühe mit Bezügen über hunderte Tokens. Ein Satz mit 20 Wörtern war kein Problem. Ein ganzes Dokument mit 2000 Wörtern schon.
2. RNNs passen schlecht zu GPUs. Die sequentielle Struktur eines RNNs zwingt dazu, Token 1 fertig zu verarbeiten, bevor Token 2 starten kann. Bevor Token 3 starten kann. Und so weiter. Moderne GPUs sind darauf ausgelegt, tausende Operationen gleichzeitig durchzuführen. Eine Architektur die das gar nicht zulässt, nutzt diese Hardware kaum aus. RNN-Training auf langen Sequenzen dauerte Tage bis Wochen.
Beide Probleme hatten dieselbe Wurzel: Das Netz muss Information durch einen schmalen und sequentiellen Kanal schicken. Der Hidden State (oder Cell State bei LSTMs) ist eine einzige Zustandsgröße fester Größe, durch die aller Kontext gepresst wird, Token für Token in Reihe.
Der Ausweg: einfach überall hinschauen
2015 führten Bahdanau, Cho und Bengio einen neuen Mechanismus ein, zunächst als Erweiterung für RNN-Übersetzer: Attention. Die Idee war, dass der Decoder nicht nur auf seinen eigenen Notizblock schaut, sondern zu jedem Zeitpunkt der Ausgabe aktiv auf alle Positionen der Eingabe zurückblickt und gewichtet diejenigen auswählt, die gerade relevant sind. Statt Information durch einen engen Kanal zu pressen, darf der Decoder sich die Zutaten einfach aus dem ganzen Satz holen.
Zwei Jahre später, 2017, veröffentlichten Vaswani und Kollegen das Paper mit dem kühnsten Titel der NLP-Geschichte: „Attention Is All You Need". Ihre zentrale These: Attention alleine, ganz ohne den rekurrenten Unterbau, funktioniert nicht nur, sondern besser. Die Architektur die sie vorstellten heißt Transformer und verzichtet komplett auf RNNs. Ein Transformer verarbeitet alle Tokens einer Sequenz parallel. Beziehungen zwischen Positionen werden durch Attention modelliert. Damit erledigen sich beide Probleme der RNN-Ära: lange Abhängigkeiten werden strukturell zugänglich, die Parallelisierung ergibt sich natürlich.
Was Attention genau ist und wie sie funktioniert, ist das Thema von Artikel 6. Wie daraus ein vollständiger Transformer wird, folgt in Artikel 7.
Was von den RNNs bleibt
Auch wenn RNNs im NLP-Mainstream heute durch Transformer ersetzt sind, lassen sie dem Feld ein paar wichtige Lektionen zurück:
- Gewichte über die Zeit zu teilen ist ein mächtiges Prinzip das in moderner Form bis heute gebraucht wird.
- Vanishing Gradients sind kein Spezialproblem von RNNs. Sie treten in jedem tiefen Netz auf. Die spätere Lösung per skip connections (bekannt aus ResNets) ist konzeptuell eng verwandt mit den Gates der LSTMs.
- Rekurrente Architekturen sind nicht tot. Moderne State Space Models wie Mamba (2023) experimentieren wieder mit rekurrenten Strukturen, in sparsamerer Form als klassische RNNs, aber mit derselben Grundidee. Für bestimmte Problemklassen, insbesondere sehr lange Sequenzen, sind sie effizienter als Transformer.
Was die RNNs am Ende wirklich aus dem Rennen genommen hat, war nicht ein einzelner Konzeptfehler, sondern der fundamentale Zielkonflikt zwischen sequenzieller Struktur und paralleler Hardware. Der Transformer löste ihn, indem er die sequentielle Abhängigkeit einfach aufgab. Wie das geht, sehen wir im nächsten Artikel.
Alle Artikel der Serie
- Das nächste Wort — wie Sprachmodelle funktionieren
- Wörter als Punkte im Raum — was Embeddings wirklich sind
- Neuronale Netze von Grund auf
- Backpropagation — wie ein Modell lernt
- Kontext und RNNs — warum Reihenfolge zählt ← dieser Artikel
- Attention — der Mechanismus der alles veränderte (erscheint demnächst)
- Der Transformer — die vollständige Architektur (erscheint demnächst)
- Fine-Tuning — vom Basismodell zum Assistenten (erscheint demnächst)
Serie: Wie LLMs funktionieren · rotecodefraktion.de