Modul 01 · Grundlagen

Vom Perzeptron zum Transformer:
Das Substrat moderner LLMs

Dieses Kapitel erklärt „wie KI tatsächlich funktioniert" mit einem Minimum an höherer Mathematik. Ob Sie aus dem Produkt, der Entwicklung oder dem Vertrieb kommen — nach diesem Kapitel können Sie jede weitere Diskussion über Modelle und Frameworks verfolgen.

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

💡 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

EigenschaftRNN / LSTMTransformer
LangstreckenabhängigkeitenNeigt dazu, frühen Kontext zu vergessenDirekte Verbindung zwischen beliebigen Positionen
Parallele BerechnungStreng sequentiellVerarbeitet alle Positionen gleichzeitig
TrainingsgeschwindigkeitLangsamSchnell (volle GPU-Auslastung)
SkalierbarkeitSchwer über 10B Parameter zu skalierenNative 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":

  1. Jedes Wort erzeugt drei Vektoren: Query (Q), Key (K), Value (V).
  2. Für „er" wird das Skalarprodukt seines Q mit dem K jedes anderen Wortes berechnet, um einen Relevanz-Score zu erhalten.
  3. Softmax wird angewendet, um die Aufmerksamkeitsgewichte zu erhalten (z. B. 0,8 für „Xiaoming", 0,1 für „Peking").
  4. 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:

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

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:

Nichts davon passt in eine einzelne GPU (80GB), daher brauchen wir mehrere Parallelstrategien:

ParallelismusIdeeRepräsentatives Framework
DatenparallelDaten auf GPUs aufteilen; jede führt das vollständige Modell ausPyTorch DDP
ModellparallelVerschiedene Schichten auf verschiedene GPUs platzierenMegatron-LM
PipelineparallelModell in Stufen aufteilen; jede GPU verarbeitet einen anderen Micro-BatchGPipe, PipeDream
TensorparallelEine einzelne Matrixoperation auf GPUs aufteilenMegatron-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:

7. Kapitelzusammenfassung

📚 Nächstes Kapitel: Agent-Evolution — Vom LLM zum Agentic Agent

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

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

8.7 Wie geht es weiter?

📚 Vorherige: 7. Kapitelzusammenfassung · Nächstes Kapitel: Agent-Evolution — Vom LLM zum Agentic Agent