Tokenwerk · Das LLM-Lehrbuch

Kapitel 35 · VII · Bau dein LLM · 7 Minuten

Schritt 3: Dein erstes neuronales Sprachmodell

Ein Probelauf mit dem kleinsten Sprachmodell der Welt – und ein PyTorch-Crashkurs nebenbei: Tensoren, Module, Loss und Optimizer.

Was wir bauen

Bevor wir das große GPT bauen, kommt ein Probelauf: das kleinste neuronale Sprachmodell, das es gibt. Es ist das Bigram-Modell aus Kapitel 14 – nur zählen wir diesmal nicht, sondern lassen die Tabelle per Gradientenabstieg lernen.

Warum der Umweg? Weil du dabei die vier PyTorch-Bausteine kennenlernst, die in jedem Modell stecken – vom Mini-Bigram bis zu ChatGPT:

  1. Tensoren – Zahlenfelder mit Form (Kapitel 11).
  2. nn.Module – ein Baustein mit lernbaren Gewichten.
  3. F.cross_entropy – der Fehler aus Kapitel 16.
  4. Der Optimizer – dreht die Gewichte bergab (Kapitel 8).

Wenn die zusammen funktionieren, ist der Rest „nur“ ein größeres Modell.

PyTorch in fünf Minuten

Ein Tensor ist PyTorchs Zahlenfeld, fast wie ein NumPy-Array. Der große Unterschied: PyTorch kann sich merken, wie ein Ergebnis zustande kam, und automatisch die Steigungen zurückrechnen. Probier in der Python-Konsole:

python
import torch
x = torch.tensor([[1.0, 2.0], [3.0, 4.0]])
print(x.shape)
print(x @ x)
w = torch.tensor(1.0, requires_grad=True)
loss = (w * 2 - 6) ** 2
loss.backward()
print(loss.item(), w.grad.item())
text
torch.Size([2, 2])
tensor([[ 7., 10.],
        [15., 22.]])
16.0 -16.0

Erkennst du die letzte Zeile? Das ist exakt das Beispiel aus Kapitel 8: Gewicht 1, Eingabe 2, Ziel 6, Fehler 16 und Steigung −16. Diesmal hat PyTorch die Ableitung selbst berechnet – requires_grad=True sagt „merk dir den Rechenweg“, backward() rechnet rückwärts, und in w.grad steht die Steigung. Das ist Backpropagation aus Kapitel 9, vollautomatisch.

Das Modell

python
# Das erste trainierbare Sprachmodell: eine lernbare Bigram-Tabelle (Kapitel 14–16)
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

from tokenizer import Tokenizer

tok = Tokenizer.load("data/tokenizer.json")
train = torch.from_numpy(np.load("data/train.npy")).long()
V = tok.vocab_size

Wir laden Tokenizer und Trainingsdaten. torch.from_numpy(...).long() macht aus dem NumPy-Array einen Tensor aus ganzen Zahlen – Token-IDs müssen ganze Zahlen sein.

python
class Bigram(nn.Module):
    def __init__(self):
        super().__init__()
        self.table = nn.Embedding(V, V)          # Zeile = vorheriges Token, Spalte = Score fürs nächste

    def forward(self, ids):
        return self.table(ids)                   # Logits: [B, T, V]

Das ist das ganze Modell. Jede eigene PyTorch-Klasse erbt von nn.Module und hat zwei Teile:

  • In __init__ legst du die Bausteine mit Gewichten an. Hier nur einen: nn.Embedding(V, V), eine Tabelle mit 1024 Zeilen und 1024 Spalten. Zeile = vorheriges Token, Spalte = Score für jedes mögliche nächste Token. Zusammen gut eine Million lernbare Zahlen.
  • In forward beschreibst du, wie aus der Eingabe die Ausgabe wird. Hier: Zu jeder Token-ID die passende Zeile nachschlagen. Das sind schon die Logits.

Du rufst das Modell später einfach mit model(x) auf – PyTorch ruft dann forward für dich auf.

Die Trainingsschleife im Kleinen

python
torch.manual_seed(0)
model = Bigram()
optimizer = torch.optim.AdamW(model.parameters(), lr=0.1)
for step in range(301):
    starts = torch.randint(0, len(train) - 33, (32,))
    x = torch.stack([train[s : s + 32] for s in starts])
    y = torch.stack([train[s + 1 : s + 33] for s in starts])        # Target-Shift
    logits = model(x)
    loss = F.cross_entropy(logits.view(-1, V), y.view(-1))
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if step % 50 == 0:
        print(f"Schritt {step:3d}  Loss {loss.item():.3f}")

Das ist der Lernkreislauf aus Kapitel 8, jetzt mit echtem Text:

  • Batch ziehen: 32 zufällige Startstellen, ab jeder 32 Tokens als Eingabe x. Das Ziel y ist dieselbe Stelle, um eins verschoben – der Target-Shift aus Kapitel 16. Bei x[i] soll das Modell y[i] vorhersagen, also das jeweils nächste Token.
  • Vorwärts: model(x) liefert Logits der Form [32, 32, 1024]: 32 Texte, 32 Positionen, 1024 Scores.
  • Fehler: F.cross_entropy will zwei flache Listen: alle Vorhersagen untereinander (view(-1, V) macht daraus [1024, 1024]) und alle Ziele (view(-1)). Softmax und Logarithmus rechnet die Funktion selbst.
  • Rückwärts und Schritt: zero_grad() löscht die alten Steigungen, backward() berechnet die neuen, step() dreht alle Gewichte ein Stück bergab.

Text erzeugen

python
ids = [tok.special["<bos>"]]
for _ in range(40):
    probs = F.softmax(model(torch.tensor([ids]))[0, -1], dim=-1)
    ids.append(torch.multinomial(probs, 1).item())
print(tok.decode(ids))

Wir starten mit <bos> und fragen 40-mal: Welche Wahrscheinlichkeiten hat das nächste Token? Dann würfeln wir mit torch.multinomial eines aus und hängen es an. Genau wie das Ratespiel aus Kapitel 1 – nur dass das Modell die Prozente selbst gelernt hat.

Die ganze Datei

python
# Das erste trainierbare Sprachmodell: eine lernbare Bigram-Tabelle (Kapitel 14–16)
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

from tokenizer import Tokenizer

tok = Tokenizer.load("data/tokenizer.json")
train = torch.from_numpy(np.load("data/train.npy")).long()
V = tok.vocab_size


class Bigram(nn.Module):
    def __init__(self):
        super().__init__()
        self.table = nn.Embedding(V, V)          # Zeile = vorheriges Token, Spalte = Score fürs nächste

    def forward(self, ids):
        return self.table(ids)                   # Logits: [B, T, V]


torch.manual_seed(0)
model = Bigram()
optimizer = torch.optim.AdamW(model.parameters(), lr=0.1)
for step in range(301):
    starts = torch.randint(0, len(train) - 33, (32,))
    x = torch.stack([train[s : s + 32] for s in starts])
    y = torch.stack([train[s + 1 : s + 33] for s in starts])        # Target-Shift
    logits = model(x)
    loss = F.cross_entropy(logits.view(-1, V), y.view(-1))
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if step % 50 == 0:
        print(f"Schritt {step:3d}  Loss {loss.item():.3f}")

ids = [tok.special["<bos>"]]
for _ in range(40):
    probs = F.softmax(model(torch.tensor([ids]))[0, -1], dim=-1)
    ids.append(torch.multinomial(probs, 1).item())
print(tok.decode(ids))

Ausführen und prüfen

bash
python bigram.py
text
Schritt   0  Loss 7.404
Schritt  50  Loss 4.861
Schritt 100  Loss 4.197
Schritt 150  Loss 3.907
Schritt 200  Loss 3.849
Schritt 250  Loss 3.763
Schritt 300  Loss 3.741
Nein, und wurzeit auf einmal ein wenigster zwei Rosen ist, es stre. Och der mich nur zu holbier, gehalten.« Die schickte Augen

So liest du das:

  • Der Start liegt bei etwa 7,4. Ein Modell, das blind rät, hätte ln(1024) ≈ 6,9. Wir starten etwas schlechter, weil PyTorch die Tabelle mit ziemlich großen Zufallszahlen füllt – das Modell rät also nicht gleichmäßig, sondern zufällig daneben. Beim GPT beheben wir das gleich.
  • Nach 300 Schritten sind wir bei etwa 3,7. Das Modell hat gelernt, welche Tokens typischerweise aufeinander folgen. Der Text sieht schon nach Deutsch aus: echte Wörter, Satzzeichen, sogar »wörtliche Rede«.
  • Aber Sinn ergibt nichts. Klar – das Modell sieht immer nur das allerletzte Token. Es hat ein Gedächtnis von genau einem Wort, wie in Kapitel 14 beschrieben.

Merk dir die 3,7. Das ist deine Baseline: Das GPT im nächsten Schritt muss deutlich darunter kommen, sonst stimmt etwas nicht.

Wenn etwas schiefgeht

  • FileNotFoundError: data/tokenizer.json – Erst python prepare.py ausführen.
  • RuntimeError: shape '[-1, 1024]' is invalid – Die Vokabulargröße passt nicht. Prüfe, dass V = tok.vocab_size steht und nicht eine feste Zahl.
  • Der Loss sinkt nicht – Prüfe die Verschiebung: y muss bei s + 1 beginnen, nicht bei s. Und optimizer.zero_grad() muss vor backward() stehen.

Kurz gemerkt

  • Ein PyTorch-Modell ist eine Klasse mit __init__ (Bausteine) und forward (Rechenweg).
  • Jede Trainingsrunde: Batch ziehen, vorwärts, cross_entropy, zero_grad, backward, step.
  • Das Bigram-Modell kommt auf einen Loss von etwa 3,7. Das ist die Messlatte für das GPT.