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:
- Tensoren – Zahlenfelder mit Form (Kapitel 11).
nn.Module– ein Baustein mit lernbaren Gewichten.F.cross_entropy– der Fehler aus Kapitel 16.- 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:
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())torch.Size([2, 2])
tensor([[ 7., 10.],
[15., 22.]])
16.0 -16.0Erkennst 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
# 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_sizeWir 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.
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
forwardbeschreibst 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
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 Zielyist dieselbe Stelle, um eins verschoben – der Target-Shift aus Kapitel 16. Beix[i]soll das Modelly[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_entropywill 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
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
# 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
python bigram.pySchritt 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 AugenSo 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– Erstpython prepare.pyausführen.RuntimeError: shape '[-1, 1024]' is invalid– Die Vokabulargröße passt nicht. Prüfe, dassV = tok.vocab_sizesteht und nicht eine feste Zahl.- Der Loss sinkt nicht – Prüfe die Verschiebung:
ymuss beis + 1beginnen, nicht beis. Undoptimizer.zero_grad()muss vorbackward()stehen.
Kurz gemerkt
- Ein PyTorch-Modell ist eine Klasse mit
__init__(Bausteine) undforward(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.