PPO ist seit 2017 der De-facto-Standard für on-policy Reinforcement Learning. Der Trick: Statt eine harte KL-Beschränkung wie TRPO zu lösen, deckelt PPO das Update durch einfaches Clipping. Wir entwickeln das Clipped Surrogate Objective Schritt für Schritt, zeigen die PPO-Update-Schleife als Mermaid-Diagramm, implementieren ein lauffähiges Mini-PPO auf der numpy-GridWorld und ergänzen einen realistischen PyTorch-Skeleton.
1. Das Problem: Policy-Gradient-Schritte können tödlich sein
Vanilla Policy-Gradient hat ein Sicherheitsproblem: Ein einzelner zu großer Update-Schritt kann die Policy in einen Bereich werfen, aus dem sie nicht zurückfindet. Das Lernen kollabiert – auch dann, wenn der Schritt mathematisch in die richtige Richtung zeigt.
| Lernrate | Verhalten |
|---|---|
| Zu klein | Konvergiert, aber unerträglich langsam |
| Mittel | Funktioniert in einfachen Umgebungen |
| Zu groß | Catastrophic collapse: Policy wird zufällig, Recovery selten |
Die mathematisch saubere Antwort: Trust Region Policy Optimization (TRPO, Schulman et al. 2015). TRPO formuliert die Update-Regel als constrained optimization:
$$\max_\theta \mathbb{E}\left[\frac{\pi_\theta(a \mid s)}{\pi_{\theta_{\text{old}}}(a \mid s)} \hat{A}t\right] \quad \text{u.B.} \quad \mathbb{E}[\text{KL}(\pi{\theta_{\text{old}}} \,|\, \pi_\theta)] \le \delta$$
In Worten: Maximiere den erwarteten Vorteil, aber bleibe in einer KL-Kugel um die alte Policy. Theoretisch elegant – praktisch braucht TRPO konjugierte Gradienten und Hessian-Vector-Products. Wenige Frameworks implementieren es robust.
2. Die PPO-Idee: Clipping statt KL-Constraint
Schulman et al. (2017) stellten die pragmatische Frage: Was, wenn wir die Trust-Region-Garantie durch eine einfache, harte Deckelung des Ratios ersetzen? Das Ergebnis ist das Clipped Surrogate Objective:
$$L^{\text{CLIP}}(\theta) = \mathbb{E}\left[\min\left(r_t(\theta) \hat{A}_t,\; \text{clip}(r_t(\theta), 1 - \epsilon, 1 + \epsilon) \hat{A}_t\right)\right]$$
mit dem Importance-Sampling-Ratio
$$r_t(\theta) = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{\text{old}}}(a_t \mid s_t)}.$$
Die Logik in Klartext:
- Ist $\hat{A}t > 0$ (Aktion war besser als der Durchschnitt), möchten wir $\pi\theta(a)$ erhöhen. Aber nur bis $r_t = 1 + \epsilon$. Danach: Update auf 0 gedeckelt – keine weiteren Anreize, das Ratio explodieren zu lassen.
- Ist $\hat{A}t < 0$, möchten wir $\pi\theta(a)$ senken. Aber nur bis $r_t = 1 - \epsilon$.
- Das
minsorgt dafür, dass nur „die schlechtere" der beiden Optionen (geklippt vs. nicht geklippt) zählt – PPO ist konservativ.
Typische Werte: $\epsilon = 0.2$.
Quick-Check: Was bewirkt das Clipping im PPO-Objective?
3. Mehrere Epochen pro Batch: PPOs Sample-Effizienz
Klassisches REINFORCE wirft jeden Batch nach genau einem Gradient-Step weg. PPO ist deutlich großzügiger: Auf einem einmal gesammelten Batch von typischerweise 2048–8192 Schritten laufen $K$ Epochen mit Minibatches ($K = 4 \dots 10$).
Das funktioniert, weil das Clipping nach den ersten paar Updates greift: Sobald die neue Policy weit genug von $\pi_{\theta_\text{old}}$ entfernt ist, liefert der geklippte Pfad einen 0-Gradienten – die weiteren Epochen ändern dann nichts mehr. PPO ist damit halb-on-policy: nicht vollständig off-policy wie SAC, aber deutlich effizienter als reines REINFORCE.
Quick-Check: Warum darf PPO mehrere Epochen über denselben Batch laufen lassen?
4. Lauffähig: Mini-PPO auf der GridWorld
Wir bauen ein vollständiges PPO – ohne PyTorch, mit reiner numpy-Policy-Tabelle. Die Schritte folgen dem klassischen PPO-Rezept:
- Rollout unter $\pi_{\theta_\text{old}}$ sammeln.
- GAE-Advantage berechnen (vereinfacht hier: einfacher TD-Fehler).
- $K$ Epochen mit Clipped Loss updaten.
import numpy as np
rng = np.random.default_rng(0)
LENGTH = 7
GAMMA = 0.95
LR_A, LR_C = 0.05, 0.1
EPS = 0.2 # Clip-Range
K_EPOCHS = 4
ITERATIONS = 200
STEPS_PER_ITER = 256
theta = np.zeros((LENGTH, 2))
V = np.zeros(LENGTH)
def softmax(x):
z = x - x.max(); e = np.exp(z); return e / e.sum()
def policy_probs_from(theta_arr, s): return softmax(theta_arr[s])
def step(s, a):
s_next = max(0, min(LENGTH - 1, s + (1 if a == 1 else -1)))
done = s_next in (0, LENGTH - 1)
if done:
r = 1.0 if s_next == LENGTH - 1 else -1.0
else:
r = 0.0
return s_next, r, done
def collect_rollout(theta_old, n_steps):
"""Sammelt n_steps Transitionen, startet Episoden in der Mitte."""
data = []
s = LENGTH // 2
for _ in range(n_steps):
probs = policy_probs_from(theta_old, s)
a = int(rng.choice(2, p=probs))
s_next, r, done = step(s, a)
data.append((s, a, r, s_next, done, probs[a]))
s = LENGTH // 2 if done else s_next
return data
def compute_advantages(data):
advs = []
for (s, a, r, s_next, done, _) in data:
target = r + (0.0 if done else GAMMA * V[s_next])
advs.append(target - V[s])
return np.array(advs)
ep_returns_per_iter = []
for it in range(ITERATIONS):
theta_old = theta.copy()
data = collect_rollout(theta_old, STEPS_PER_ITER)
advs = compute_advantages(data)
# Normalisieren: stabilisiert den Loss.
advs = (advs - advs.mean()) / (advs.std() + 1e-8)
# K Epochen Clipped Update.
for _ in range(K_EPOCHS):
for (s, a, r, s_next, done, p_old_a), adv in zip(data, advs):
probs_new = policy_probs_from(theta, s)
p_new_a = probs_new[a]
ratio = p_new_a / max(p_old_a, 1e-8)
clipped = min(max(ratio, 1 - EPS), 1 + EPS)
# Surrogate-Beitrag: das min(...) bestimmt, welcher Pfad aktiv ist.
unclipped_obj = ratio * adv
clipped_obj = clipped * adv
# Wenn der unclipped Pfad das min liefert: regulärer PG-Gradient.
# Wenn der clipped Pfad das min liefert: Gradient = 0.
if unclipped_obj <= clipped_obj:
grad_log = -probs_new.copy(); grad_log[a] += 1.0
# Gradient von ratio*adv bzgl theta entspricht ratio * grad_log * adv.
theta[s] += LR_A * ratio * grad_log * adv
# Critic-Update (TD).
for (s, a, r, s_next, done, _) in data:
target = r + (0.0 if done else GAMMA * V[s_next])
V[s] += LR_C * (target - V[s])
# Episoden-Returns aus den done-Markern extrahieren (Approximation).
done_returns = [r for (_, _, r, _, done, _) in data if done]
if done_returns:
ep_returns_per_iter.append(np.mean(done_returns))
print(f"Mittlerer Terminal-Reward erste 20 Iter : {np.mean(ep_returns_per_iter[:20]):+.3f}")
print(f"Mittlerer Terminal-Reward letzte 20 Iter: {np.mean(ep_returns_per_iter[-20:]):+.3f}")
print("Gelernte P(rechts):")
for s in range(LENGTH):
print(f" s={s}: {policy_probs_from(theta, s)[1]:.2f} V={V[s]:+.2f}")
Der Clip-Mechanismus ist in der if unclipped_obj <= clipped_obj-Verzweigung sichtbar: Sobald das Ratio aus dem $[1-\epsilon, 1+\epsilon]$-Korridor läuft und dieser Pfad das ungünstigere Surrogate liefert, fällt der Gradient auf 0. Genau das ist PPOs Sicherheitsnetz.
Quick-Check zum Code: Wofür steht `ratio` im Snippet?
5. PyTorch-Skeleton (Konzept, nicht im Browser lauffähig)
Für einen realistischen Eindruck, wie PPO in PyTorch aussieht – diese Variante setzt gymnasium voraus und läuft nur lokal:
import torch
import torch.nn as nn
from torch.distributions import Categorical
class ActorCritic(nn.Module):
def __init__(self, obs_dim, n_actions, hidden=64):
super().__init__()
self.shared = nn.Sequential(
nn.Linear(obs_dim, hidden), nn.Tanh(),
nn.Linear(hidden, hidden), nn.Tanh(),
)
self.actor_head = nn.Linear(hidden, n_actions)
self.critic_head = nn.Linear(hidden, 1)
def forward(self, x):
h = self.shared(x)
return Categorical(logits=self.actor_head(h)), self.critic_head(h).squeeze(-1)
def ppo_update(model, optimizer, batch, eps=0.2, k_epochs=4):
"""batch = (obs, actions, old_log_probs, advantages, returns)."""
obs, acts, old_log_probs, advs, returns = batch
advs = (advs - advs.mean()) / (advs.std() + 1e-8)
for _ in range(k_epochs):
dist, values = model(obs)
new_log_probs = dist.log_prob(acts)
ratio = (new_log_probs - old_log_probs).exp()
unclipped = ratio * advs
clipped = torch.clamp(ratio, 1 - eps, 1 + eps) * advs
policy_loss = -torch.min(unclipped, clipped).mean()
value_loss = ((returns - values) ** 2).mean()
entropy = dist.entropy().mean()
loss = policy_loss + 0.5 * value_loss - 0.01 * entropy
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 0.5)
optimizer.step()
Drei produktive Details, die im numpy-Toy fehlen:
- Gradient-Norm-Clipping (
clip_grad_norm_(0.5)) – verhindert Spikes durch Outlier-Batches. - Entropie-Bonus (
-0.01 * entropy) – belohnt Exploration und verhindert vorzeitige Determinisierung. - Wertfunktion und Policy in einem Loss – mit fester Gewichtung 0.5 (PPO-Standard).
6. Hyperparameter-Cheat-Sheet
| Parameter | Typischer Wert | Wirkung |
|---|---|---|
| Clip-Range $\epsilon$ | 0.1 – 0.3 | Größer → schnellere Updates, instabiler |
| K Epochen | 4 – 10 | Mehr → höhere Sample-Effizienz, mehr Off-Policy-Bias |
| Batch-Size | 2048 – 8192 Schritte | Mehr → stabilere Schätzer, weniger Updates pro Stunde |
| Minibatch-Size | 64 – 256 | Klassisch SGD-Tuning |
| GAE-$\lambda$ | 0.9 – 0.97 | Bias-Varianz-Hebel (Artikel 03) |
| Entropie-Koeff. | 0.0 – 0.01 | Höher → mehr Exploration |
| LR (Adam) | 3e-4 (Standard) | Mit linearer Decay zu 0 hin am Ende |
7. Diskussion: warum PPO so dominant wurde
PPO ist nicht der theoretisch eleganteste Algorithmus. Er gewinnt durch Engineering-Eigenschaften:
- Wenige Hyperparameter, die alle robust sind.
- Standard-Implementierung passt in 200 Zeilen PyTorch.
- Skaliert auf Distributed-Setups (parallele Worker, IMPALA-artige Architekturen).
- Funktioniert sowohl in diskreten als auch kontinuierlichen Aktionsräumen (mit Gauß-Policy).
Genau diese Eigenschaften haben PPO zur Wahl für RLHF in modernen LLMs (ChatGPT, Llama, Claude) gemacht – allerdings mit eigenen Anpassungen (Reference-KL-Penalty, Reward-Model-basierter Reward).
8. Abschluss-Quiz
Was ist der Hauptvorteil von PPO gegenüber TRPO?
Welche Aussage zum Clipping ist korrekt?
Wozu dient der Entropie-Bonus im PPO-Loss?
PPO wird oft als *halb-on-policy* beschrieben. Warum?
Warum normalisiert man die Advantages in jedem PPO-Update typischerweise auf Mittelwert 0 und Standardabweichung 1?
9. Weiterführend
- Nächster Artikel: Soft Actor-Critic (SAC) (Artikel 05 dieser Serie).
- Querverweis: self-learning-agents/02-rl-fuer-code-agenten – PPO als RLHF-Backbone.
- Knowledgebase:
masteringpytorch-secondedition.md– Kapitel zu Modern Policy Gradients.
Quellen
- Schulman, J. et al. (2017) – Proximal Policy Optimization Algorithms, arXiv:1707.06347.
- Schulman, J. et al. (2015) – Trust Region Policy Optimization, ICML.
- Engstrom, L. et al. (2020) – Implementation Matters in Deep RL: A Case Study on PPO and TRPO – die berühmte Reproducibility-Studie über PPO-Tricks.