1. Künstliche neuronale Netze: vom biologischen Neuron zur mathematischen Funktion
1.1 Die kleinste Einheit eines neuronalen Netzes
Ein künstliches neuronales Netz ist von der Funktionsweise biologischer Neurone inspiriert. Ein einzelnes Neuron ist im Wesentlichen eine mathematische Funktion:
output = activation(w · x + b) Hier ist x der Eingabevektor, w das Gewicht, b der Bias, und activation eine nichtlineare Funktion (gewöhnlich Sigmoid, ReLU oder GELU). Stapeln Sie Tausende dieser „Neurone" zu Schichten und verbinden Sie sie — und Sie haben die Grundlage des modernen Deep Learning.
1.2 Die drei Elemente des Trainings
- Vorwärtspropagation (Forward propagation): Daten fließen von der Eingabeschicht zur Ausgabeschicht und erzeugen eine Vorhersage.
- Verlustfunktion (Loss function): misst die Lücke zwischen Vorhersage und Grundwahrheit. Klassifikation verwendet Kreuzentropie; Regression verwendet den mittleren quadratischen Fehler.
- Rückwärtspropagation + Gradientenabstieg (Backpropagation + gradient descent): ausgehend von der Ausgabe wird der „Beitrag" (Gradient) jedes Parameters zum Verlust berechnet, dann wird das Gewicht in die entgegengesetzte Richtung verschoben, um die nächste Vorhersage genauer zu machen.
💡 Schlüsselintuition: das Training eines neuronalen Netzes besteht im Wesentlichen darin, „in einem milliarden-dimensionalen Parameterraum nach einer Gewichtskonfiguration zu suchen, die den Verlust minimiert". Deshalb benötigen LLMs enorme Datenmengen und Rechenleistung — der Parameterraum ist riesig.
2. Transformer: der Eckpfeiler moderner LLMs
2017 veröffentlichte Google Attention Is All You Need, stellte die Transformer-Architektur vor und veränderte die KI-Trajektorie. Die zentrale Innovation: die Rekurrenz vollständig aufgeben und sich rein auf den Aufmerksamkeitsmechanismus zur Sequenzverarbeitung verlassen.
2.1 Warum Transformer das NLP beherrschte
| Eigenschaft | RNN / LSTM | Transformer |
|---|---|---|
| Langstreckenabhängigkeiten | Neigt dazu, frühen Kontext zu vergessen | Direkte Verbindung zwischen beliebigen Positionen |
| Parallele Berechnung | Streng sequentiell | Verarbeitet alle Positionen gleichzeitig |
| Trainingsgeschwindigkeit | Langsam | Schnell (volle GPU-Auslastung) |
| Skalierbarkeit | Schwer über 10B Parameter zu skalieren | Native Unterstützung für 100B+ Parameter |
2.2 Self-Attention
Die zentrale Idee: für jedes Wort in einer Sequenz „schau" jedes andere Wort an und entscheide, wie wichtig jedes einzelne für es ist.
Nehmen Sie diesen Beispielsatz — „Xiaoming reiste gestern geschäftlich nach Peking, und er kam heute zurück":
- Jedes Wort erzeugt drei Vektoren: Query (Q), Key (K), Value (V).
- Für „er" wird das Skalarprodukt seines Q mit dem K jedes anderen Wortes berechnet, um einen Relevanz-Score zu erhalten.
- Softmax wird angewendet, um die Aufmerksamkeitsgewichte zu erhalten (z. B. 0,8 für „Xiaoming", 0,1 für „Peking").
- Diese Gewichte werden verwendet, um eine gewichtete Summe des V jedes Wortes zu bilden, was eine neue, kontextreiche Repräsentation von „er" ergibt.
In mathematischer Form:
Attention(Q, K, V) = softmax(Q · K^T / √d_k) · V wobei d_k die Dimensionalität des Key-Vektors ist; die Division durch seine Quadratwurzel verhindert, dass das Skalarprodukt den Softmax-Gradienten sprengt.
2.3 Multi-Head Attention
Multi-Head Attention ist Self-Attention auf Steroiden. Sie führt mehrere Self-Attention-Operationen parallel über verschiedene „Repräsentationsunterräume" aus und verkettet die Ergebnisse. Dadurch kann das Modell mehrere Arten von Beziehungen gleichzeitig erfassen:
- Ein Kopf kann sich auf syntaktische Beziehungen (Subjekt-Verb-Objekt) konzentrieren.
- Ein anderer auf semantische Beziehungen (Synonyme, Antonyme).
- Ein dritter auf Langstrecken-Koreferenz (auf welches Nomen sich ein Pronomen bezieht).
2.4 Positionskodierung (Positional encoding)
Der Transformer verarbeitet jedes Wort parallel und verliert daher standardmäßig die „Reihenfolge"-Information. „Der Hund beißt den Mann" und „Der Mann beißt den Hund" haben identische Tokens — nur die Reihenfolge unterscheidet sich. Die Positionskodierung fügt dem Embedding jedes Wortes einen eindeutigen „Positionsvektor" hinzu, damit das Modell weiß, wo es in der Sequenz steht.
Der ursprüngliche Transformer verwendete feste sinusförmige/cosinusförmige Positionskodierungen. Spätere Modelle (LLaMA, Qwen) verwenden RoPE (Rotary Position Embeddings), das viel längere Sequenzen handhabt.
3. Die vollständige Transformer-Architektur
Ein Standard-Transformer-Block hat zwei Hälften:
input → [LayerNorm → Multi-Head Attention → Residual]
→ [LayerNorm → Feed-Forward Network (FFN) → Residual]
→ output - Multi-Head Attention: findet „Beziehungen" — lässt jedes Wort jedes andere Wort sehen.
- Feed-Forward Network (FFN): übernimmt die „Verarbeitung" — wendet eine nichtlineare Transformation auf jedes Wort einzeln an, um Merkmale höherer Ebene zu extrahieren.
- Residualverbindung: addiert die Eingabe direkt zur Ausgabe, vermeidet verschwindende Gradienten und ermöglicht tiefe Netze.
- LayerNorm: stabilisiert das Training und verhindert numerische Explosionen.
Die GPT-Familie (ChatGPT, DeepSeek, Qwen) sind einfach Dutzende bis Hunderte solcher Blöcke, gestapelt zu einem Decoder-only-Transformer.
4. Vortraining, SFT, RLHF: die dreistufige Trainingsrezeptur
4.1 Vortraining
Training auf einem riesigen Korpus von Internet-Text (Billionen von Tokens), damit das Modell lernt, „das nächste Token vorherzusagen". Dies ist der längste und teuerste Schritt — DeepSeek-V3 verwendete 14,8T Tokens Trainingsdaten und kostete ungefähr 5,57M USD (GPU-Miete).
4.2 Überwachtes Feintuning (SFT)
Training auf einem kleinen Satz qualitativ hochwertiger, von Menschen kuratierter „Anweisung-Antwort"-Paare (Zehntausende), damit das Modell lernt, „die Anfrage zu verstehen und entsprechend zu antworten". Das Volumen ist weit kleiner als beim Vortraining, aber die Qualitätsanforderungen sind extrem.
4.3 Verstärkungslernen aus menschlichem Feedback (RLHF)
Menschen bewerten die verschiedenen Antworten des Modells; ein Belohnungsmodell wird auf diesen Bewertungen trainiert, dann wird RL (typischerweise PPO oder DPO) verwendet, um das Modell in Richtung höher bewerteter Antworten zu lenken.
5. Warum brauchen wir GPU-Cluster?
Das Training eines 100B-Parameter-Modells — ein einzelner Vorwärts- + Rückwärtsdurchlauf erfordert:
- Eingabe-Token-Embeddings (params × Sequenzlänge)
- Aufmerksamkeitsmatrizen (Sequenzlänge² × Anzahl Köpfe)
- Gradienten (params)
- Optimierer-Zustand (Momentum, Varianz — 3-4× params)
Nichts davon passt in eine einzelne GPU (80GB), daher brauchen wir mehrere Parallelstrategien:
| Parallelismus | Idee | Repräsentatives Framework |
|---|---|---|
| Datenparallel | Daten auf GPUs aufteilen; jede führt das vollständige Modell aus | PyTorch DDP |
| Modellparallel | Verschiedene Schichten auf verschiedene GPUs platzieren | Megatron-LM |
| Pipelineparallel | Modell in Stufen aufteilen; jede GPU verarbeitet einen anderen Micro-Batch | GPipe, PipeDream |
| Tensorparallel | Eine einzelne Matrixoperation auf GPUs aufteilen | Megatron-LM |
6. Von LLMs zu Agents: der Fähigkeitssprung
Sobald LLMs stark genug wurden, begannen die Leute, „Extras" hinzuzufügen: das Modell kann nicht nur „sprechen", sondern auch „tun". Dies ist der Ausgangspunkt des AI Agent:
- ReAct (reason + act): das Modell überlegt zuerst, was zu tun ist, und entscheidet dann, welches Werkzeug aufgerufen wird.
- Function calling: das Modell gibt strukturierte „Werkzeugaufruf-Anfragen" aus, die von einem externen System ausgeführt werden.
- Multi-Agent-Zusammenarbeit: mehrere Agents spielen verschiedene Rollen und koordinieren sich, um eine Aufgabe zu erledigen.
7. Kapitelzusammenfassung
- ✅ Der Transformer ersetzte die Rekurrenz durch den Aufmerksamkeitsmechanismus und ermöglichte paralleles Training und Langstreckenmodellierung.
- ✅ Self-Attention lässt jedes Wort jedes andere „sehen"; Multi-Head Attention erfasst mehrere Beziehungen gleichzeitig.
- ✅ Vortraining + SFT + RLHF ist die dreistufige Rezeptur zum Training eines LLM.
- ✅ 100B-Parameter-Modelle erfordern GPU-Cluster und eine Kombination von Parallelstrategien.
8. Praxis: einen Mini-Transformer von Grund auf bauen
Nachdem die Theorie behandelt ist, führt Sie dieser Abschnitt durch den Aufbau eines 6-Schichten-Decoder-only-Transformers in PyTorch von Grund auf, trainiert auf einem kleinen Textkorpus. Der vollständige Code umfasst etwa 250 Zeilen und läuft auf CPU oder GPU — das Ziel ist, jedes Zahnrad des Transformers für Sie sichtbar zu machen.
8.1 Ziele und Umgebung
- Implementieren Sie einen 6-Schichten-Decoder-only-Transformer mit ~1,2M Parametern.
- Trainieren Sie ein Zeichen-Sprachmodell auf einem kleinen ~500-Zeilen-Korpus und beobachten Sie, wie der Verlust sinkt.
- Stellen Sie zwei lauffähige Skripte bereit:
train.pyundsample.py.
Umgebungseinrichtung (2 Zeilen):
pip install torch numpy
python -c "import torch; print('torch', torch.__version__)" 8.2 Datenvorbereitung
Wir verwenden ein kleines Stück Tang-Gedicht-Stil-Pseudodaten (Sie können es durch Ihre eigene input.txt ersetzen):
cat > data/input.txt << 'EOF'
Quiet night thoughts
Before my bed a pool of light,
Can it be frost upon the ground?
Eyes up, I find the moon so bright,
Head down, I think of home I've found.
Spring dawn
Spring slumber knows no dawn so soon,
The birds' song fills the air with tune.
Last night the wind and rain did blow,
How many flowers have let go?
EOF
wc -c data/input.txt Ein Zeichen-Ebenen-Modell liest einfach die gesamte Datei als eine lange Zeichenkette — kein Tokenizer nötig.
8.3 Vollständiger Code: train.py
"""Mini-Transformer: 6-Schichten-Decoder-only, Zeichen-Sprachmodell.
Verwendung:
python train.py
"""
import math
import time
from pathlib import Path
import torch
import torch.nn as nn
import torch.nn.functional as F
# ---------- 1. Data loader ----------
class CharDataset:
"""Schneidet eine Textdatei in (block_size)-lange Trainingsbeispiele."""
def __init__(self, path: str, block_size: int = 64):
self.text = Path(path).read_text(encoding="utf-8")
self.chars = sorted(set(self.text))
self.vocab_size = len(self.chars)
self.stoi = {c: i for i, c in enumerate(self.chars)}
self.itos = {i: c for i, c in enumerate(self.chars)}
self.block_size = block_size
self.data = torch.tensor([self.stoi[c] for c in self.text], dtype=torch.long)
print(f"[data] chars={self.vocab_size} len={len(self.data)}")
def get_batch(self, batch_size: int = 32):
ix = torch.randint(0, len(self.data) - self.block_size - 1, (batch_size,))
x = torch.stack([self.data[i : i + self.block_size] for i in ix])
y = torch.stack([self.data[i + 1 : i + 1 + self.block_size] for i in ix])
return x, y
# ---------- 2. Modellkomponenten ----------
class PositionalEncoding(nn.Module):
"""Sinusförmige Positionskodierung, eingefroren."""
def __init__(self, d_model: int, max_len: int = 512):
super().__init__()
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(position * div_term)
pe[:, 1::2] = torch.cos(position * div_term)
self.register_buffer("pe", pe.unsqueeze(0)) # (1, max_len, d_model)
def forward(self, x):
return x + self.pe[:, : x.size(1)]
class CausalSelfAttention(nn.Module):
"""Multi-Head kausale Self-Attention (im Decoder verwendet)."""
def __init__(self, d_model: int, n_heads: int, max_len: int = 512, dropout: float = 0.1):
super().__init__()
assert d_model % n_heads == 0
self.n_heads = n_heads
self.head_dim = d_model // n_heads
self.qkv = nn.Linear(d_model, 3 * d_model)
self.proj = nn.Linear(d_model, d_model)
self.dropout = nn.Dropout(dropout)
# kausale Maske: oberes Dreieck (ohne Diagonale) ist -inf
mask = torch.triu(torch.ones(max_len, max_len), diagonal=1).bool()
self.register_buffer("mask", mask)
def forward(self, x):
B, T, C = x.shape
qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim).permute(2, 0, 3, 1, 4)
q, k, v = qkv[0], qkv[1], qkv[2] # (B, h, T, head_dim)
att = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
att = att.masked_fill(self.mask[:T, :T], float("-inf"))
att = F.softmax(att, dim=-1)
att = self.dropout(att)
out = (att @ v).transpose(1, 2).reshape(B, T, C)
return self.proj(out)
class FeedForward(nn.Module):
"""Transformer-FFN: GELU + Linear + Dropout."""
def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Linear(d_ff, d_model),
nn.Dropout(dropout),
)
def forward(self, x):
return self.net(x)
class Block(nn.Module):
"""Pre-LN Decoder-Block: Attention -> FFN, jeweils mit Residual."""
def __init__(self, d_model: int, n_heads: int, d_ff: int, max_len: int, dropout: float):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = CausalSelfAttention(d_model, n_heads, max_len, dropout)
self.ln2 = nn.LayerNorm(d_model)
self.ffn = FeedForward(d_model, d_ff, dropout)
def forward(self, x):
x = x + self.attn(self.ln1(x))
x = x + self.ffn(self.ln2(x))
return x
class MiniTransformer(nn.Module):
"""6-Schichten-Decoder-only-Transformer, Zeichen-Sprachmodell."""
def __init__(
self,
vocab_size: int,
d_model: int = 128,
n_heads: int = 4,
d_ff: int = 512,
n_layers: int = 6,
max_len: int = 256,
dropout: float = 0.1,
):
super().__init__()
self.tok_emb = nn.Embedding(vocab_size, d_model)
self.pos_enc = PositionalEncoding(d_model, max_len)
self.blocks = nn.ModuleList(
[Block(d_model, n_heads, d_ff, max_len, dropout) for _ in range(n_layers)]
)
self.ln_f = nn.LayerNorm(d_model)
self.head = nn.Linear(d_model, vocab_size, bias=False)
# Weight tying: Eingabe-Embedding teilt Gewichte mit dem Ausgabe-Kopf
self.head.weight = self.tok_emb.weight
self.apply(self._init_weights)
def _init_weights(self, m):
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, std=0.02)
if m.bias is not None:
nn.init.zeros_(m.bias)
elif isinstance(m, nn.Embedding):
nn.init.normal_(m.weight, std=0.02)
def forward(self, idx, targets=None):
x = self.tok_emb(idx)
x = self.pos_enc(x)
for blk in self.blocks:
x = blk(x)
x = self.ln_f(x)
logits = self.head(x)
loss = None
if targets is not None:
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1))
return logits, loss
@torch.no_grad()
def generate(self, idx, max_new_tokens: int = 100, temperature: float = 1.0, top_k: int = 20):
"""Autoregressive Generierung mit Temperatur + Top-k-Sampling."""
for _ in range(max_new_tokens):
idx_cond = idx[:, -self.pos_enc.pe.size(1) :]
logits, _ = self(idx_cond)
logits = logits[:, -1, :] / temperature
if top_k:
v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
logits[logits < v[:, [-1]]] = float("-inf")
probs = F.softmax(logits, dim=-1)
nxt = torch.multinomial(probs, num_samples=1)
idx = torch.cat([idx, nxt], dim=1)
return idx
# ---------- 3. Trainingsschleife ----------
def main():
torch.manual_seed(0)
ds = CharDataset("data/input.txt", block_size=64)
device = "cuda" if torch.cuda.is_available() else "cpu"
model = MiniTransformer(vocab_size=ds.vocab_size).to(device)
print(f"[model] params={sum(p.numel() for p in model.parameters()) / 1e6:.2f}M device={device}")
opt = torch.optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.1)
steps = 1000
batch_size = 32
t0 = time.time()
for step in range(steps):
x, y = ds.get_batch(batch_size)
x, y = x.to(device), y.to(device)
_, loss = model(x, y)
opt.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
opt.step()
if step % 100 == 0 or step == steps - 1:
dt = time.time() - t0
print(f"step {step:4d} loss={loss.item():.4f} elapsed={dt:.1f}s")
torch.save({"model": model.state_dict(), "stoi": ds.stoi, "itos": ds.itos}, "ckpt.pt")
print("[done] saved ckpt.pt")
if __name__ == "__main__":
main() 8.4 Inferenz-Skript: sample.py
"""Generiert Text aus einem trainierten ckpt.pt."""
import torch
from train import MiniTransformer
ckpt = torch.load("ckpt.pt", map_location="cpu")
stoi, itos = ckpt["stoi"], ckpt["itos"]
model = MiniTransformer(vocab_size=len(itos))
model.load_state_dict(ckpt["model"])
model.eval()
prompt = "Quiet night thoughts\n"
idx = torch.tensor([[stoi[c] for c in prompt]], dtype=torch.long)
out = model.generate(idx, max_new_tokens=120, temperature=0.8, top_k=10)[0].tolist()
print("=== generated ===")
print("".join(itos[i] for i in out)) 8.5 Ausführen und erwartete Ausgabe
mkdir -p data
# (schreiben Sie die input.txt aus 8.2)
python train.py Erwartete Ausgabe (CPU ~30-60s, MPS/GPU wenige Sekunden):
[data] chars=28 len=87
[model] params=1.21M device=cpu
step 0 loss=3.3321 elapsed=0.0s
step 100 loss=2.7102 elapsed=2.1s
step 200 loss=2.3504 elapsed=4.0s
...
step 900 loss=1.8421 elapsed=32.5s
step 999 loss=1.7903 elapsed=36.0s
[done] saved ckpt.pt
python sample.py
=== generated ===
Quiet night thoughts
Before my bed a pool of light,
Can it be frost upon the ground?
Eyes up, I find the moon so bright,
Head down, I think of home I've found. 8.6 Tuning-Tipps
- Verlust sinkt nicht: Daten zu kurz → verwenden Sie eine längere
input.txt(z. B. einen vollständigen Tang-Gedicht-Korpus); LR auf 1e-4 / 5e-4 anpassen. - Generierung kollabiert auf ein Zeichen: Temperatur zu niedrig — auf 0,8-1,2 erhöhen; top_k auf 20-50 anheben.
- Speicher knapp:
batch_sizeauf 8 senken, oder in Halbpräzision mittorch.amp.autocastlaufen lassen. - Mehr Geschwindigkeit nötig:
d_model=96,n_layers=4setzen, um auf ~400K Parameter zu kommen.
8.7 Wie geht es weiter?
- Ein vollständiges GPT:
top-p (nucleus)-Sampling, KV-Cache für schnellere Inferenz hinzufügen. - LLaMA-Architektur: LayerNorm durch
RMSNormersetzen, sinusförmige PE durchRoPE, GELU-FFN durchSwiGLU, undGQAzur Speicherreduzierung hinzufügen. - Multi-GPU-Training:
torchrun --nproc_per_node=2 train.pymitDistributedDataParallel. - Echten Tokenizer verwenden:
CharDatasetdurchtiktoken(GPT-2 BPE) odersentencepieceersetzen.