Ch. 19Signature chapter
RL + RLHF
MDPs, PPO, RLHF pipeline, DPO derived, GRPO + RLVR. Reward hacking taxonomy from the inside.
A reinforcement learning agent trained to grab an object learned to position its hand between the object and the camera. The reward function checked whether the camera could see the object behind the hand. The hand was in front of the camera. The reward was high. The agent had not grabbed the object. This is reward hacking, and it is the central problem of reinforcement learning. The math of RL is beautiful: a Bellman equation, a policy A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary →, a clipped objective, an advantage estimate. You can derive every algorithm in this chapter on a napkin. You cannot derive a reward function on a napkin. RLHF, DPO, GRPO, RLVR are the four most consequential algorithms shipped by AI labs in the last five years and they all run on a reward signal that someone had to specify. By the end of this chapter you will know how each one works, in code, and you will know which way each one breaks. The breaks are not edge cases. The breaks are the alignment problem.
FIG 19.1 · Learning outcomes
By the end of this chapter you will be able to:
- Write down the Bellman equation from memory and use value iteration to solve a 4x4 gridworld optimally in 30 lines of NumPy.
- Implement tabular Q-learning, observe it converge on Frozen Lake, and explain in two sentences why $\epsilon$-greedy exploration is necessary even when the environment is fully observable.
- Implement Deep Q-Learning (DQN) with a target network and experience replay, train it on CartPole, and diagnose what fails when you remove either component.
- Derive the REINFORCE policy gradient theorem from the log-derivative trick in five lines, and explain why the baseline term reduces variance without biasing the gradient.
- Implement vanilla policy gradient (VPG) and Actor-Critic in PyTorch, and explain why an Actor-Critic estimator has lower variance than REINFORCE.
- Implement PPO with the clipped surrogate objective and GAE, train it on CartPole in 30 seconds, and explain what each of the four terms in the loss is doing.
- Implement the full RLHF pipeline: train a reward model from pairwise preference data, then fine-tune a GPT-2-small with PPO against that reward model, then observe mode collapse.
- Derive the DPO loss from the Bradley-Terry preference model + KL-regularized RL, in 12 lines of algebra, and explain why DPO trains on the same data as RLHF but skips the reward model.
- Implement GRPO (group-relative policy optimization), the DeepSeek variant, by replacing PPO's critic with a per-prompt group baseline.
- Articulate three distinct reward-hacking failure modes from the Lilian Weng taxonomy, each with a concrete RL or RLHF example, and propose a specific detection method for each.
- Read a paper that claims a new RL alignment method, look at its KL term, and tell whether the method is robust or just gaming a held-out reward proxy.
FIG 19.2 · What you need first
- Ch 10 — PyTorch — you need
nn.Module, optimizer-step semantics, and the difference between.detachandwith torch.no_grad. RL code is full of both. - Ch 11 — Training Deep Neural Networks — AdamW, gradient clipping, learning-rate schedules. We use all of them.
- Ch 15 — Transformers from Scratch — RLHF runs on top of a pretrained transformer. We assume you understand the autoregressive forward pass and can sample with temperature.
- Ch 17 — Efficient Inference — PPO and DPO require sample generation at every training step. Inference cost dominates RLHF wall-time. The KV-cache and batching tricks are what make RLHF feasible at scale.
- Ch 8 — Unsupervised Learning §EM — useful framing. Policy iteration uses an EM-like alternation. Knowing one helps with the other.
- Ch 18 — Generative Models §reparameterization — useful but not required. The trick reappears here as the policy-gradient log-derivative trick.
- Optional but rewarding: read Karpathy's Pong from Pixels (
24-founder-blogs/karpathy-rl) before starting. It is the cleanest single-document derivation of policy gradients in 130 lines of NumPy.
FIG 19.3.1
MDPs and the Bellman equation
A Markov Decision Process is the formal object an RL agent acts on. It is a tuple where is the state space, is the action space, is the transition A number between 0 and 1 saying how confident the model is, where 1 means totally sure and 0.5 means a coin-flip.Full glossary →, is the reward function, and is the discount factor. A policy is a probability distribution over actions conditioned on state. The agent's goal is to find a policy that maximizes expected discounted return .
Two derived quantities make the math tractable. The state-value function under policy :
The action-value function:
The Bellman equation relates the value at a state to the values at successor states:
The optimal policy satisfies the Bellman optimality equation:
Read that twice. The Bellman optimality equation is a fixed-point equation. The unique solution is the optimal value function, and from it you can recover the optimal policy by acting greedily. Every algorithm in this chapter is some way of approximating that fixed point.
From-scratch path (value iteration on a known MDP, the simplest possible RL solver):
import numpy as np
def value_iteration(P: np.ndarray, R: np.ndarray, gamma: float = 0.99,
tol: float = 1e-6) -> tuple[np.ndarray, np.ndarray]:
"""
P: (S, A, S) transition probabilities
R: (S, A, S) rewards
Returns: V (S,), pi (S,) greedy policy.
"""
S, A, _ = P.shape
V = np.zeros(S)
while True:
Q = np.einsum("sat,sat->sa", P, R + gamma * V[None, None, :])
V_new = Q.max(axis=1)
if np.abs(V_new - V).max() < tol: break
V = V_new
pi = Q.argmax(axis=1)
return V, piThat is the entire planning side of classical RL. The hard part is what happens when and are unknown and you have to estimate them by interacting with the environment.
FIG 19.3.2
Q-learning and the SARSA-vs-Q-learning split
When the MDP is unknown, the agent has to learn the value function by interacting with the environment. The temporal-difference (TD) family is the workhorse. The simplest member is Q-learning. Maintain a table initialized to zero. At each step, take an action, observe the reward and next state, and update:
The term in brackets is the TD error: how much the new estimate (one-step bootstrap) disagrees with the old. is the How big an adjustment the model makes to its numbers each time, after the gradient tells it which way to go.Full glossary →. The crucial thing is that the update uses regardless of what action the agent actually took next. This makes Q-learning off-policy: it learns the value of the greedy policy while behaving according to some other (usually -greedy) policy.
SARSA is the on-policy cousin. The update is identical except the next-action is sampled from the behaviour policy:
The difference matters in cliff-walking environments. SARSA learns a "safe" policy that stays away from the cliff because the -greedy exploration occasionally falls off. Q-learning learns the "optimal" policy that walks right along the edge because it ignores the exploration in its update. Both converge, eventually, in the tabular case under standard step-size conditions.
The exploration policy matters. -greedy is the dumbest thing that works: with A number between 0 and 1 saying how confident the model is, where 1 means totally sure and 0.5 means a coin-flip.Full glossary → act random, otherwise act greedy. Decay from 1.0 toward 0.05 over training. Boltzmann (A function that turns a list of raw scores into confidences for several options that add up to exactly 1 (100%).Full glossary → over Q) is sometimes nicer. UCB and Thompson sampling are nicer still. For most of what we will do here, -greedy is fine.
From here on the library paths use gymnasium (pip install gymnasium), the maintained fork of OpenAI's gym that gives every RL environment a uniform interface: gym.make("FrozenLake-v1") builds an env, env.reset returns the start observation, and env.step(a) returns the (observation, reward, terminated, truncated, info) five-tuple where done = terminated or truncated.
Library path (Frozen Lake with tabular Q-learning):
import gymnasium as gym
import numpy as np
env = gym.make("FrozenLake-v1", is_slippery=True)
Q = np.zeros((env.observation_space.n, env.action_space.n))
alpha, gamma, eps = 0.1, 0.99, 1.0
for ep in range(20_000):
s, _ = env.reset()
done = False
while not done:
a = env.action_space.sample() if np.random.rand() < eps else int(Q[s].argmax())
s_next, r, term, trunc, _ = env.step(a)
Q[s, a] += alpha * (r + gamma * Q[s_next].max() - Q[s, a])
s, done = s_next, term or trunc
eps = max(0.05, eps * 0.9995)FIG 19.3.3
Deep Q-Networks: experience replay and target networks
Tabular Q-learning fails when the state space is too large to enumerate. Replace the table with a neural network , where are the network parameters, and you have Deep Q-Networks (Mnih et al., 2013, 2015). Two engineering tricks make the training stable enough to actually work on Atari (the suite of arcade games that became the standard deep-RL benchmark).
Experience replay. Store every transition in a replay buffer. At each A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → step, sample a minibatch uniformly from the buffer and update on that A small group of examples the model looks at together before making one adjustment to its numbers.Full glossary →. This breaks the temporal correlation between consecutive samples (which violates the i.i.d. assumption of SGD) and lets you reuse each transition for multiple updates.
Target networks. The Q-learning target uses the same parameters being updated. This means the target moves while you are trying to fit to it, which causes divergence. Fix: maintain a second set of "target" parameters that are a slow-moving copy of (either hard-updated every steps or Polyak-averaged). Use for the target. The loss becomes:
Library path (the minimum viable DQN):
import torch
import torch.nn as nn
import torch.nn.functional as F
from collections import deque
import random
class QNet(nn.Module):
def __init__(self, obs_dim: int, n_actions: int):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, 128), nn.ReLU(),
nn.Linear(128, 128), nn.ReLU(),
nn.Linear(128, n_actions),
)
def forward(self, x): return self.net(x)
def dqn_step(q, q_target, batch, opt, gamma=0.99):
s, a, r, s_next, done = batch
q_sa = q(s).gather(1, a.unsqueeze(1)).squeeze(1)
with torch.no_grad():
q_next_max = q_target(s_next).max(dim=1).values
target = r + gamma * q_next_max * (1 - done.float())
loss = F.mse_loss(q_sa, target)
opt.zero_grad(); loss.backward()
nn.utils.clip_grad_norm_(q.parameters(), max_norm=10.0)
opt.step()
return loss.item()Remove the replay buffer and DQN diverges. Remove the target network and DQN diverges. Both are necessary. The 2013 paper makes a big deal of this in the experiments section, and ARENA's part 2.1 walks you through the failure modes one at a time. Worth reproducing.
Two refinements that became standard. Double DQN (van Hasselt et al., 2016) uses to select the next action and to evaluate it, reducing the overestimation A single number that gets added to every guess, the same amount no matter what the inputs are.Full glossary → of the max operator. Dueling DQN factorizes , which separates the state value from the action advantage and learns faster in states where the action does not matter.
FIG 19.3.4
Policy gradients: REINFORCE
DQN learns a value function and acts greedily. The policy-A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → family learns the policy directly. Parameterize the policy as a neural network , sample actions from it, observe returns, and adjust in the direction that increases the A number between 0 and 1 saying how confident the model is, where 1 means totally sure and 0.5 means a coin-flip.Full glossary → of high-return actions.
The math is the policy gradient theorem. Define where is a trajectory and . The gradient is
where is the return from step onward. The derivation is the log-derivative trick: , applied to the trajectory distribution. It is five lines once you see it, and Karpathy's Pong from Pixels writes it out in NumPy with full annotations.
This is REINFORCE (Williams, 1992). Sample a full episode, compute the return at each step, take the log-probability gradient, scale by the return, sum, step. The estimator is unbiased. It is also very high variance, because can vary enormously across episodes.
Library path:
import torch
import torch.nn as nn
from torch.distributions import Categorical
class PolicyNet(nn.Module):
def __init__(self, obs_dim: int, n_actions: int):
super().__init__()
self.net = nn.Sequential(
nn.Linear(obs_dim, 64), nn.Tanh(),
nn.Linear(64, n_actions),
)
def forward(self, x): return self.net(x)
def reinforce_loss(policy: PolicyNet, observations, actions, returns):
logits = policy(observations)
log_probs = Categorical(logits=logits).log_prob(actions)
return -(log_probs * returns).mean()From-scratch path (the log-derivative trick by hand, on a binary action space):
import numpy as np
def reinforce_policy_gradient_numpy(W1, W2, obs, action, advantage):
# 2-layer NN policy with sigmoid output (prob of action=1)
h = np.maximum(0, obs @ W1) # ReLU
logits = h @ W2 # scalar
p = 1.0 / (1.0 + np.exp(-logits)) # probability of action=1
# log pi(a|s) = log(p) if a=1 else log(1-p)
# d/d_logits log pi = (a - p)
d_logits = (action - p) * advantage
dW2 = np.outer(h, d_logits)
dh = d_logits * W2
dh[h <= 0] = 0
dW1 = np.outer(obs, dh)
return dW1, dW2This is exactly the Karpathy Pong-from-pixels architecture, simplified for one observation. The whole Pong from Pixels post is this gradient applied for 6,000,000 frames.
FIG 19.3.5
Baselines, advantages, and Actor-Critic
The REINFORCE A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → is unbiased but high-variance. The trick that fixes this is the A simple reference method you compare against to see whether a fancier approach is actually worth it.Full glossary →: subtract a state-dependent function from in the gradient:
The estimator remains unbiased for any that does not depend on the action (because ). The variance is minimized when . The difference is the advantage: how much better is action than the average action from state .
This gives Actor-Critic. Two networks: an actor that picks actions and a critic that estimates value. The critic is trained on TD targets () and the actor is trained on advantage estimates ( for one-step, or richer estimators for multi-step). A2C is the synchronous version. A3C runs multiple agents in parallel and updates a shared model asynchronously (DeepMind, 2016).
The big practical question is how many steps to bootstrap. One-step TD () is biased but low-variance. Full Monte Carlo return () is unbiased but high-variance. GAE (next sub-section) splits the difference.
Library path:
class ActorCritic(nn.Module):
def __init__(self, obs_dim: int, n_actions: int):
super().__init__()
self.body = nn.Sequential(
nn.Linear(obs_dim, 64), nn.Tanh(),
nn.Linear(64, 64), nn.Tanh(),
)
self.actor = nn.Linear(64, n_actions)
self.critic = nn.Linear(64, 1)
def forward(self, x):
h = self.body(x)
return self.actor(h), self.critic(h).squeeze(-1)FIG 19.3.6
Generalized Advantage Estimation (GAE)
GAE (Schulman et al., 2015) interpolates between one-step TD and Monte Carlo return. Define the one-step TD residual . The GAE estimator is the exponentially-weighted sum of these residuals:
The One of the model's internal numbers that gets adjusted as it learns.Full glossary → is the A single number that gets added to every guess, the same amount no matter what the inputs are.Full glossary →-variance knob. recovers one-step TD (high bias, low variance). recovers Monte Carlo (low bias, high variance). Schulman's empirical sweet spot is , which has been the de-facto PPO default ever since.
In practice GAE is computed backwards through a trajectory in a single pass:
def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95):
advantages = []
gae = 0
for t in reversed(range(len(rewards))):
next_value = values[t + 1] if t + 1 < len(values) else 0
delta = rewards[t] + gamma * next_value * (1 - dones[t]) - values[t]
gae = delta + gamma * lam * (1 - dones[t]) * gae
advantages.insert(0, gae)
return torch.tensor(advantages)FIG 19.3.7
PPO: the clipped objective
Proximal Policy Optimization (Schulman et al., 2017) is the workhorse of modern policy-A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → RL. It is the algorithm OpenAI used for the Dota 5 agents, the algorithm InstructGPT/ChatGPT use for RLHF, and the algorithm ARENA uses as the centerpiece of its RL chapter.
The framing: vanilla policy gradient takes one gradient step per A small group of examples the model looks at together before making one adjustment to its numbers.Full glossary → of experience and throws the data away. This is sample-inefficient. You would like to take many gradient steps per batch. But each gradient step changes the policy, which means subsequent gradient steps are using off-policy data, and the gradient estimator is biased.
PPO's fix is the clipped surrogate objective. Define the A number between 0 and 1 saying how confident the model is, where 1 means totally sure and 0.5 means a coin-flip.Full glossary → ratio between the new policy and the old policy:
The objective is:
Read this carefully. When (action was better than average), increasing is good, but clipping at stops you from increasing it too much in one step. When (action was worse than average), decreasing is good, clipping at stops you. The takes the more pessimistic of the clipped and unclipped objectives. The net effect is a trust region (the idea, from TRPO, that each update should keep the new policy close to the old one): the policy cannot move too far from in a single update.
The full PPO loss has three terms:
The first is the clipped policy loss. The second is the value function loss (MSE against the GAE returns). The third is an entropy bonus: maximize the entropy of to encourage exploration. Typical , , .
Library path (PPO update, the central function):
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Categorical
def ppo_update(actor_critic, optimizer, observations, actions, old_log_probs,
returns, advantages, clip_eps=0.2, vf_coef=0.5, ent_coef=0.01,
n_epochs=4, batch_size=64):
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
for _ in range(n_epochs):
idx = torch.randperm(len(observations))
for start in range(0, len(observations), batch_size):
b = idx[start:start + batch_size]
logits, values = actor_critic(observations[b])
dist = Categorical(logits=logits)
new_log_probs = dist.log_prob(actions[b])
entropy = dist.entropy().mean()
ratio = (new_log_probs - old_log_probs[b]).exp()
surr1 = ratio * advantages[b]
surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * advantages[b]
policy_loss = -torch.min(surr1, surr2).mean()
value_loss = F.mse_loss(values, returns[b])
loss = policy_loss + vf_coef * value_loss - ent_coef * entropy
optimizer.zero_grad()
loss.backward()
nn.utils.clip_grad_norm_(actor_critic.parameters(), 0.5)
optimizer.step()A vanilla PPO agent trains CartPole to 500 reward (the cap) in about 30 seconds on a CPU. ARENA's part 2.3 walks you through every A setting you pick yourself before training starts, like the learning rate, batch size, or number of layers, which the model does not learn on its own.Full glossary → individually.
FIG 19.3.8
Off-policy continuous control: SAC and TD3
PPO is on-policy and works on discrete action spaces. For continuous control (robotics, MuJoCo) and for sample-efficient learning, the modern picks are SAC and TD3. We will be brief because this chapter is mostly about discrete and language RL.
SAC (Soft Actor-Critic, Haarnoja et al., 2018) is the entropy-regularized variant of off-policy actor-critic. The reward is augmented with , encouraging high-entropy policies. Two Q-networks (clipped double-Q trick), a target network, a replay buffer, automatic A dial that controls how much a model gambles on unlikely words versus sticking to the most likely one.Full glossary → tuning. Robust to hyperparameters in a way that PPO is not.
TD3 (Twin Delayed DDPG, Fujimoto et al., 2018) is the deterministic-policy alternative. Two Q-networks (take the min for the target, reducing overestimation), delayed policy updates, target policy smoothing. Less of a robust default than SAC, but simpler to implement.
The lesson for a reader of this chapter: off-policy methods (DQN family, SAC, TD3) are more sample-efficient because they reuse data from a replay buffer. On-policy methods (PPO, A2C) are more stable in practice because the policy A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → estimator is unbiased on freshly-collected data. For language model RLHF (next), the choice is PPO. The data is small and the off-policy When the data a model meets in the real world differs from the data it trained on, so it stumbles.Full glossary → is the problem you want to minimize.
FIG 19.3.9
RLHF: the pipeline
Reinforcement Learning from Human Feedback is the procedure that turned GPT-3 into ChatGPT. The pipeline has three phases.
Phase 1: Supervised Taking a model that already learned a lot of general skills and training it a bit more on your own specific data.Full glossary → (SFT). Start with a pretrained language model. Fine-tune it on a small set of demonstration data: pairs of (prompt, ideal completion) hand-written by contractors. The output is a model that mimics the style of the demonstrations. The SFT model is the policy initialization for the RL phase.
Phase 2: Reward model training. Sample pairs of completions for the same prompt from the SFT model. Have a human (or stronger model) rank them: or . Train a separate model to predict these preferences. The loss is the Bradley-Terry likelihood:
where is the winner and is the loser. The reward model is usually initialized from the SFT model with the final unembedding replaced by a scalar head. After training, gives a real-valued score that approximates "how much would a human prefer this response."
Phase 3: PPO fine-tuning against the reward model. Treat the language model as a policy (sequentially sampling tokens), the prompt as the initial state, the action space as the The fixed set of all chunks a model is allowed to read or produce.Full glossary →, and the reward as paid at the end of the sequence. Run PPO. Critically, add a KL penalty to the reward to prevent the policy from drifting too far from the SFT model:
The KL penalty is the most important knob. Too small and the policy collapses onto reward-model failure modes (next section). Too large and the policy ignores the reward signal. Typical .
The PPO step inside RLHF — sample completions, fold a per-A single chunk of a sequence the model reads or produces, often a word or a character.Full glossary → KL penalty into the reward stream, run GAE, then take the clipped update — is trl.PPOTrainer, unrolled:
trl.PPOTrainer vs. from-scratch rollout + GAE + clipped surrogate
DL gluetrainer = PPOTrainer(
args=PPOConfig(kl_coef=0.05, cliprange=0.2, vf_coef=0.5,
gamma=1.0, lam=0.95, num_ppo_epochs=4),
model=actor_with_value_head, ref_model=ref, reward_model=rm,
processing_class=tokenizer, train_dataset=prompt_ds,
)
trainer.train() # generate -> score -> KL-penalize -> GAE -> clipped update@torch.no_grad()
def rollout(actor, ref_model, prompts, max_new_tokens=8, temperature=1.0):
full = prompts.clone(); log_probs=[]; ref_log_probs=[]; values=[]
for _ in range(max_new_tokens):
logits, vals = actor(full)
dist = Categorical(F.softmax(logits[:, -1, :] / temperature, dim=-1))
nxt = dist.sample()
log_probs.append(dist.log_prob(nxt))
ref_logits = ref_model(full)[:, -1, :] / temperature
ref_log_probs.append(Categorical(logits=ref_logits).log_prob(nxt))
values.append(vals[:, -1])
full = torch.cat([full, nxt.unsqueeze(-1)], dim=-1)
_, vf = actor(full); values.append(vf[:, -1])
return full, torch.stack(log_probs,-1), torch.stack(ref_log_probs,-1), torch.stack(values,-1)
def compute_gae(rewards, values, gamma=1.0, lam=0.95):
B, T = rewards.shape; adv = torch.zeros_like(rewards); last = torch.zeros(B)
for t in reversed(range(T)):
delta = rewards[:, t] + gamma*values[:, t+1] - values[:, t]
last = delta + gamma*lam*last; adv[:, t] = last
return adv
def ppo_update(actor, full_tokens, prompt_len, old_log_probs, advantages, returns, opt,
clip_eps=0.2, vf_coef=0.5, ent_coef=0.01, n_epochs=2):
for _ in range(n_epochs):
logits, values = actor(full_tokens)
comp_logits = logits[:, prompt_len-1:-1, :]
dist = Categorical(logits=comp_logits)
new_log_probs = dist.log_prob(full_tokens[:, prompt_len:])
ratio = (new_log_probs - old_log_probs).exp()
surr1 = ratio * advantages
surr2 = ratio.clamp(1-clip_eps, 1+clip_eps) * advantages
policy_loss = -torch.min(surr1, surr2).mean()
value_loss = F.mse_loss(values[:, prompt_len-1:-1], returns)
loss = policy_loss + vf_coef*value_loss - ent_coef*dist.entropy().mean()
opt.zero_grad(); loss.backward()
torch.nn.utils.clip_grad_norm_(actor.parameters(), 0.5); opt.step()from scratch: lab/solution.py: rollout + compute_gae + ppo_update
- 1
trainer.train() generation phaserollout(): autoregressive sampling with Categorical, recording actor + ref log-probs and value-head outputs - 2
PPOConfig.kl_coef penalty against ref_modelper-token KL = log_probs_old - ref_log_probs folded into the per-token reward stream - 3
internal advantage estimator (gamma, lam)compute_gae(): backward delta = r_t + gamma*V_{t+1} - V_t, A_t = delta + gamma*lam*A_{t+1} - 4
cliprange in the surrogate objectivesurr2 = ratio.clamp(1-clip_eps, 1+clip_eps)*adv; policy_loss = -min(surr1, surr2).mean() - 5
vf_coef value-function lossvalue_loss = F.mse_loss(comp_values, returns) - 6
num_ppo_epochs reusing the same batchthe for _ in range(n_epochs) loop re-evaluating new_log_probs against cached old_log_probs - 7
automatic grad clipping / optimizer stepclip_grad_norm_(actor.parameters(), 0.5); opt.step()
What the one call hides
- Placing the scalar reward only at the final token while spreading the KL penalty across every token (the reward-shaping bookkeeping)
- Advantage normalization (subtract mean, divide by std) before the clipped update -- the scratch lab leaves this as a TODO
- The log-prob index shift: logits[:, t] predicts token t+1, so completions read logits at prompt_len-1 onward
- Adaptive KL controller, value clipping, whitening, and pad/EOS masking that TRL adds
- Maintaining a frozen reference model + a value head the same size as the policy (the 2-3x memory cost)
- Gotcha: Forget advantage normalization and PPO is wildly unstable; TRL does it for you so beginners never learn it matters
- Gotcha: kl_coef too low -> mode collapse (the whole point of the lab); too high -> the reward is ignored
- Gotcha: Off-by-one in the logits/targets slice silently trains on the wrong tokens with no error
- Gotcha: TRL's PPOTrainer arg names churn across releases (ppo_epochs vs num_ppo_epochs, model wrapping); pin the version
Use trl.PPOTrainer for any real RLHF run; build rollout+GAE+clip yourself once so you can debug mode collapse by watching the KL term and recognize PPO as clipped policy-gradient on (reward - per-token KL).
On the job: At work you write the reward shaping (where the scalar reward and KL land per token), the value head, and the training schedule; rollout, GAE, and the clipped update come from the trainer.
The reference implementation in hf-alignment-handbook has more bells (adaptive KL controller, value clipping, advantage normalization), all of which earn their place experimentally.
FIG 19.3.10
Mode collapse and reward overoptimization in RLHF
RLHF with PPO has a characteristic failure mode. Train long enough against a fixed reward model and the policy collapses onto a small set of high-reward strings. ARENA's part 2.4 walks the reader through training a transformer to "maximize output of periods" and observing the policy go from coherent English to spam like "...........................". This is mode collapse, and it is the toy version of the failure mode that matters in production.
The mechanism is reward hacking (Lilian Weng's 2024 post enumerates the taxonomy). The reward model is a finite-capacity approximation to "human preferences." It has failure modes. The policy is being optimized against the proxy, not the true objective. This is Goodhart's law ("when a measure becomes a target, it ceases to be a good measure"): the bigger the gap between proxy and true reward, the more the policy exploits.
Three concrete RLHF reward-hacking patterns, all documented in the literature:
- Sycophancy. The reward model rewards "responses the rater agreed with." The policy learns to mirror the user's stated views regardless of correctness. (Sharma et al., 2023, Towards Understanding Sycophancy in LMs.) Detection: ask the model the same factual question with different framings that imply different "right answers" and see if the answer changes.
- Length A single number that gets added to every guess, the same amount no matter what the inputs are.Full glossary →. Human raters slightly prefer longer responses. The reward model learns this. The policy learns to pad responses with bullet points and disclaimers. (Singhal et al., 2023, A Long Way to Go.) Detection: regression of reward against length on the held-out set. If the slope is significantly positive, your RM has a length confound.
- Hallucinated confidence. The reward model rewards "confident-sounding" responses. The policy learns to be confidently wrong rather than appropriately uncertain. Detection: How well a model's stated confidence matches how often it's actually right.Full glossary → measurement on held-out fact questions.
The KL term in the RLHF objective is the principled mitigation: it pulls the policy back toward the SFT model and bounds the exploitation. But the KL term cannot save you from a bad reward model. If your reward model is systematically wrong, KL just makes the wrong-ness milder. The deeper fix is to keep iterating: collect new preference data on the RLHF-fine-tuned model's outputs, train a fresh reward model, redo PPO. This is "iterated RLHF" and it is what production labs actually do.
FIG 19.3.11
Direct Preference Optimization (DPO)
DPO (Rafailov et al., 2023) is the most elegant idea in alignment since RLHF. The observation: the KL-regularized RL objective with a reward model has a closed-form optimal policy, and you can use that closed form to fold the reward model out of the algorithm entirely. You train the policy directly on the preference data.
Start with the RLHF objective (KL-regularized expected reward):
The optimal policy satisfies . Rearrange to solve for the implicit reward:
The partition function cancels out of any preference comparison. Substitute this expression into the Bradley-Terry likelihood and you get the DPO loss:
That is it. One supervised classification loss on preference triples . No reward model. No PPO. No KL penalty as a separate term: the KL is implicit in the ratio.
trl.DPOTrainer vs. from-scratch DPO log-ratio loss
DL gluetrainer = DPOTrainer(
model=policy, ref_model=ref,
args=DPOConfig(beta=0.1, output_dir="dpo"),
train_dataset=ds, # columns: prompt, chosen, rejected
processing_class=tokenizer,
)
trainer.train() # -logsigmoid(beta*((logpi_w-logref_w)-(logpi_l-logref_l)))def dpo_loss(policy, ref_policy, prompts, chosen, rejected, beta=0.1):
pi_chosen = policy.log_probs(prompts, chosen).sum(-1)
pi_rejected = policy.log_probs(prompts, rejected).sum(-1)
with torch.no_grad():
ref_chosen = ref_policy.log_probs(prompts, chosen).sum(-1)
ref_rejected = ref_policy.log_probs(prompts, rejected).sum(-1)
log_ratio_chosen = pi_chosen - ref_chosen
log_ratio_rejected = pi_rejected - ref_rejected
margin = beta * (log_ratio_chosen - log_ratio_rejected)
return -F.logsigmoid(margin).mean()from scratch: draft.md §11 (dpo_loss); assessment Q5 (dpo_loss) -- not present in solution.py
- 1
DPOConfig(beta=0.1)the beta multiplier on the log-ratio margin - 2
model vs ref_model (frozen reference)policy.log_probs vs ref_policy.log_probs (the latter under torch.no_grad()) - 3
chosen / rejected dataset columnsthe chosen and rejected token tensors passed to log_probs - 4
the DPO sigmoid loss inside trainer.train()-F.logsigmoid(beta*((logpi_w-logref_w)-(logpi_l-logref_l))).mean() - 5
sequence log-prob of each completionpolicy.log_probs(prompts, chosen).sum(-1) -- summing per-token log-probs over the completion
What the one call hides
- Computing per-token log-probs of completions while masking out prompt and pad positions before summing
- Default sigmoid loss type; DPOConfig can switch to ipo/hinge/kto variants via one arg
- The reference-model forward pass is run and cached (or precomputed) under no_grad
- label_smoothing and the implicit-reward / chosen-vs-rejected reward logging TRL adds
- That the KL-to-reference constraint is baked into the loss via the log-ratio, with no explicit KL term
- Gotcha: beta is exactly as delicate as the RLHF KL coefficient; the one-liner makes it look like a free default
- Gotcha: DPO can push down the likelihood of chosen responses too if it drifts from ref; TRL won't warn you
- Gotcha: Wrong prompt/completion masking inflates log-probs and silently corrupts the margin
Reach for trl.DPOTrainer in production when you have pairwise preferences and want to skip the reward model and PPO loop; write the loss by hand once to see DPO is one logistic-regression loss on the policy-vs-reference log-ratio margin with the KL folded in implicitly.
On the job: At work you write the per-token completion log-prob / masking helper and curate the (prompt, chosen, rejected) triples; the loss and reference handling come from DPOTrainer.
DPO has practical advantages over RLHF. Implementation is simpler (no rollout, no reward model, no value head). Training is more stable (one supervised loss, no on-policy adversarial dynamics). Compute is lower (no two-model PPO loop). On many benchmarks DPO matches or exceeds RLHF.
Caveats. DPO assumes the preference data is well-modelled by Bradley-Terry, which is not always true (humans are intransitive). DPO can over-reduce the likelihood of all responses, including the chosen ones, if the A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → pushes the policy too far from the reference. The tradeoff is just as delicate as the RLHF tradeoff.
FIG 19.3.12
The post-DPO landscape: IPO, KTO, ORPO, SimPO
DPO is now the A simple reference method you compare against to see whether a fancier approach is actually worth it.Full glossary → that newer alignment algorithms compete with. A non-exhaustive tour.
IPO (Identity Preference Optimization, Azar et al., 2023) replaces the A function that takes any number and squeezes it into a single confidence between 0 and 1.Full glossary → in DPO with a squared-error loss on the implicit reward margin. The motivation: DPO's logistic loss is unbounded, so deterministic preferences ( with certainty) push the model's log-ratio to infinity, which is pathological. IPO bounds the margin and is more robust to label noise.
KTO (Kahneman-Tversky Optimization, Ethayarajh et al., 2024) replaces pairwise preferences with single-response binary feedback ("good" or "bad"). This unlocks data sources that do not come as pairs (thumbs up/down from production logs). The loss is built on a prospect-theory utility function, which is one of the rare cases where behavioral economics shows up in ML training.
ORPO (Odds Ratio Preference Optimization, Hong et al., 2024) does away with the reference model. The KL-to-reference term in DPO is replaced with an odds-ratio penalty inside the loss. The result: a single training stage that combines SFT and preference optimization. Practical for shops that do not want to manage two copies of the model.
SimPO (Simple Preference Optimization, Meng et al., 2024) drops the reference model and length-normalizes the log-probabilities. The argument: DPO's length-A single number that gets added to every guess, the same amount no matter what the inputs are.Full glossary → and ref-model dependency are both artifacts of the formulation. SimPO removes them. On AlpacaEval 2, SimPO outperforms DPO with the same data.
The pattern across all five (DPO + IPO + KTO + ORPO + SimPO): each variation tweaks one term of the DPO loss or removes one assumption. Empirically, the differences are smaller than the marketing implies. The right framing: this is a family of preference-optimization losses, each with a different bias-variance-robustness tradeoff, and the choice depends on your data shape and how much you trust your labels.
FIG 19.3.13
GRPO and RLVR: DeepSeek's contribution
GRPO (Group Relative Policy Optimization, Shao et al., 2024) is the variant of PPO that DeepSeek used to train DeepSeek-Math and DeepSeek-R1. The observation: PPO's value function is expensive to maintain and noisy on long-form text. Replace it with a group A simple reference method you compare against to see whether a fancier approach is actually worth it.Full glossary →. For each prompt, sample completions from the current policy. Use the mean reward across the group as the baseline; the per-completion advantage is . Plug this into the PPO clipped objective. No value head needed.
The savings are real. The value network in RLHF is the same size as the policy, doubling GPU memory. GRPO removes it.
RLVR (Reinforcement Learning with Verifiable Rewards) is the framing DeepSeek used for math/code. The reward is not a learned reward model. It is a hard verifier: did the proof check, did the test pass, did the answer match the The correct, human-given answer for a piece of data, used to judge the model's guess.Full glossary →. Verifiable rewards eliminate the reward-hacking surface area that comes from preference modeling. You cannot game a Python interpreter into accepting a wrong answer the way you can game a Bradley-Terry classifier. (You can still game the verifier, e.g. by modifying test files. See the coding model learns to change the unit tests example in Weng's reward-hacking taxonomy. The surface area is smaller, not zero.)
GRPO + RLVR is the recipe behind the surprising reasoning capabilities of DeepSeek-R1. The o1-style reasoning models from OpenAI use a closely related procedure. Both make the same bet: if you can get the reward signal right, RL can do a lot more than RLHF gave it credit for.
trl.GRPOTrainer vs. from-scratch group-relative advantage
DL gluetrainer = GRPOTrainer(
model=policy,
reward_funcs=my_reward_fn, # or a reward model; verifiable reward for RLVR
args=GRPOConfig(num_generations=8, beta=0.04), # G completions per prompt
train_dataset=prompt_ds, processing_class=tokenizer,
)
trainer.train() # advantage = (r - group_mean) / group_std, no criticdef grpo_advantages(rewards: torch.Tensor) -> torch.Tensor:
"""rewards: (n_prompts, group_size). Returns same shape, group-normalized."""
mean = rewards.mean(dim=1, keepdim=True)
std = rewards.std(dim=1, keepdim=True) + 1e-8
return (rewards - mean) / stdfrom scratch: draft.md §13 (grpo_advantages); assessment Q6 -- not present in solution.py
- 1
GRPOConfig(num_generations=G)the group_size dimension of the rewards tensor (G completions per prompt) - 2
GRPO's critic-free advantage(rewards - mean) / std -- the per-prompt group baseline replaces a value head - 3
reward_funcs / reward_modelthe rewards tensor scored per completion (verifier or RM) - 4
per-prompt baseline (group mean)rewards.mean(dim=1, keepdim=True) - 5
advantage scalingdivision by rewards.std(dim=1, keepdim=True) + 1e-8
What the one call hides
- Still runs a PPO-style clipped surrogate + KL-to-reference around this advantage; only the value head is removed
- Sampling G completions per prompt every step (the dominant compute cost)
- Masking and per-token broadcast of the per-completion scalar advantage onto each token
- The std default: torch.std is UNBIASED (ddof=1), so the normalization divides by sqrt(G-1), not sqrt(G)
- The 1e-8 numerical guard and how degenerate (all-equal-reward) groups give zero advantage
- Gotcha: If every completion in a group gets the same reward, the advantage is ~0 and the step does nothing -- you need reward variance within the group
- Gotcha: num_generations too small makes the group baseline a noisy estimate of the mean
- Gotcha: It is NOT critic-free PPO end-to-end on its own; the one-liner hides the surrounding clip+KL machinery
Use trl.GRPOTrainer for RLVR / DeepSeek-style training where a value network is too expensive or noisy; write the one-liner yourself to see the whole DeepSeek innovation is replacing PPO's critic with a normalized per-prompt group baseline.
On the job: At work you write the reward function(s) (verifier or RM) and choose G; the group normalization plus the surrounding clip+KL loop come from GRPOTrainer.
FIG 19.3.14
Reward hacking: the technical view
Reward hacking is the phenomenon that ties this chapter together. Every algorithm above optimizes a policy against a scalar reward signal , and reward hacking is the gap between what you wrote down as and what you actually wanted.
Here, the focus is what reward hacking looks like in the training curves and what an RL practitioner can do inside the algorithm to detect and mitigate it.
Three technical signals you should monitor during training:
- KL divergence to reference policy. RLHF imposes a KL penalty for this reason: it bounds how far the optimized policy can drift from the SFT model. Track per step. If it spikes mid-training while reward keeps climbing, the policy is exploiting a reward-model loophole.
- Reward-model overoptimization curves. Train two reward models on the same preference data with different seeds. Track the gap between (a) reward as scored by the model you are optimizing against, and (b) reward as scored by the held-out model. The gap grows as overoptimization kicks in. The held-out model is a noisy oracle but a useful one.
- Pan et al. (2022) Rewriting your numbers so that very large measurements and very small ones are put on a comparable footing.Full glossary → pattern. Across model sizes, the gap between proxy reward and true reward grew with scale on the same training run. The implication: bigger models reward-hack harder. Hold-out-reward-vs-proxy-reward should be in your training dashboard from day one.
Three categories of mitigation that operate at the algorithm level:
- Algorithmic. KL penalties (RLHF, DPO), iterated RLHF, conservative algorithms (CPO, KTO), ensemble reward models that disagree, RLAIF (use a strong LLM as the rater) with the same A single number that gets added to every guess, the same amount no matter what the inputs are.Full glossary → controls as human raters.
- Reward-model-level. More diverse raters, How well a model's stated confidence matches how often it's actually right.Full glossary → questions, structured rubrics, pairwise consistency checks, reward-model uncertainty (Bayesian or A training trick where the model randomly switches off some of its own pieces each pass, so it can't lean too hard on any one of them.Full glossary →-based), pessimistic estimation.
- Verifiable rewards (RLVR). Where possible, replace the learned reward with a checkable one: unit tests, mathematical proof checkers, formal verifiers. The reward becomes a measurement, not a model. DeepSeek's GRPO + RLVR (§13 above) is the production case study.
FIG 19.3.15
Practical RL training discipline
You do not need to wait for deceptive-alignment debates to land before adopting the discipline that catches reward-hacking failures early. Three habits to write into the training scaffolding of any RL run.
- Never trust an eval score on a model you trained on the same eval distribution. Even mild contamination (eval prompts in training, eval-similar prompts in training, eval-style preferences in the reward model) breaks the score's meaning. See Ch 23 §10 for the eval-side discipline.
- Add adversarial probes to your eval suite. Questions designed to elicit the failure mode, not the behaviour. The Apollo Research evals and the METR evals are public examples worth borrowing from.
- Separate the people optimizing capability metrics from the people validating safety. Anthropic and OpenAI both run separate red-team groups for this reason. The full red-team operational discipline, plus the deceptive-alignment debate it grew out of, lives in Ch 24 §13.
The algorithms in sections 1–13 are good at optimizing what you tell them to optimize. The what you tell them part is the A spot in a model where its width is squeezed small on purpose, forcing the information to compress before it continues.Full glossary →, and the discipline of telling them the right thing is half of what another chapter is about.
FIG 19.3.16
What we left out (and where to read it)
For length, this chapter skipped: model-based RL (PlaNet, Dreamer, MuZero), inverse RL, imitation learning beyond plain SFT, hierarchical RL, meta-RL, offline RL (CQL, IQL), exploration bonuses (RND, ICM), and the entire multi-agent RL subfield. Three pointers:
- Sutton & Barto, chapters 8 and 17, for model-based RL.
- Lilian Weng's
2018-04-08-policy-gradientfor the full algorithmic zoo (SAC, TD3, IMPALA, MPO). - Berkeley CS285 for the lecture-quality treatment of everything else.
FIG 19.4 · Safety lens · this chapter
Reward hacking is the safety lens of this chapter, and it has been the spine of the chapter from section 7 onward. We will not repeat what is already in sections 10, 14, and 15. The brief synthesis:
What can go wrong. Every RL algorithm in this chapter optimizes a scalar reward. The scalar is a proxy. The proxy has bugs. The optimizer finds the bugs. In tabular RL with a clean reward function (Frozen Lake, CartPole), this is invisible. In RLHF on a 7-billion-One of the model's internal numbers that gets adjusted as it learns.Full glossary → LM with a learned reward model, it is the central operational concern. Lilian Weng's 2024 taxonomy enumerates the documented patterns (sycophancy, length A single number that gets added to every guess, the same amount no matter what the inputs are.Full glossary →, gaming the rater, in-context reward hacking, RLVR test-suite tampering). The list grows monotonically.
How researchers detect and guard. Three layered defenses, ordered by trustworthiness. (1) Verifiable rewards (RLVR): wherever the desired behavior can be checked programmatically (proofs, tests, regex matches), use a checker, not a learned reward. (2) KL constraints (RLHF and DPO): bound how far the policy can drift from the reference. This is a coarse, A penalty added to the wrongness score that discourages the model from leaning on big, fussy numbers, nudging it to stay simple.Full glossary →-style mitigation; it does not fix a wrong reward, it slows the exploitation. (3) Interpretability and red-teaming (another chapter, another chapter): probe the trained model's internals (e.g., did the refusal direction in the The main running tally of information that flows through a deep model, with each layer reading from it and adding its bit back in.Full glossary → get suppressed during RLHF?) and probe its behavior on adversarial prompts (e.g., do sycophancy probes still trigger the gaming response after Taking a model that already learned a lot of general skills and training it a bit more on your own specific data.Full glossary →?). Apollo's evals, METR's evals, and Anthropic's red-team work are the public references.
Habits for the code you wrote in this chapter. Three.
First, log everything. Save the prompt, the SFT-model completion, the RL-fine-tuned completion, the reward-model score, the KL to reference, the A signal showing which direction to change each of the model's numbers to make its mistakes BIGGER, so you go the opposite way to make them smaller.Full glossary → norm. Reward hacking is detected by patterns across many examples, not single examples. Make the data exist.
Second, never trust a reward curve. Train against a held-out reward model that was not used during training, and report both. If the training-time reward keeps going up while the held-out reward plateaus or declines, you are reward-hacking the training-time RM. This is the cleanest single signal that you have a problem.
Third, never trust a single eval. Generative model evals are dense with contamination, surface-pattern shortcuts, and rater-style When a model memorizes the quirks and flukes of its study examples instead of the real pattern, so it flops on anything new.Full glossary →. Run at least three independent evals from different sources (e.g., MT-Bench, AlpacaEval, an internal red-team set). Disagreements between them are signal. Agreement is also signal, but only after you have ruled out shared contamination across the eval sets.
The deeper claim of this chapter: the reward signal is often the dominant A spot in a model where its width is squeezed small on purpose, forcing the information to compress before it continues.Full glossary →, but the optimization algorithms are not solved. PPO, DPO, and GRPO each retain stability, bias, data-quality, and A setting you pick yourself before training starts, like the learning rate, batch size, or number of layers, which the model does not learn on its own.Full glossary → failure modes. Treat reward design and optimization as coupled research-and-engineering problems; a better objective cannot rescue a misleading reward, and a good reward cannot rescue a broken training loop.
FIG 19.5 · Under the hood
The library call, and the lines it hides
You don't have to choose between “use the library” and “build it from scratch.” Here is the one library call, the exact lines it stands in for, and when to reach for which on the job.
trl.RewardTrainer vs. from-scratch Bradley-Terry loss
DL gluetrainer = RewardTrainer(
model=reward_model, # base LM with a 1-logit value head
args=RewardConfig(output_dir="rm", per_device_train_batch_size=8),
train_dataset=ds, # columns: chosen, rejected (tokenized text)
processing_class=tokenizer,
)
trainer.train() # minimizes -logsigmoid(r(chosen) - r(rejected))class RewardModel(nn.Module):
def __init__(self, base: TinyGPT):
super().__init__()
self.base = base
self.score = nn.Linear(base.d_model, 1)
def forward(self, tokens: torch.Tensor) -> torch.Tensor:
h = self.base.trunk(tokens)
return self.score(h[:, -1, :]).squeeze(-1) # score at last token -> (B,)
def train_reward_model(rm, preference_pairs, opt, epochs=1):
losses = []
for _ in range(epochs):
for chosen, rejected in preference_pairs:
opt.zero_grad()
loss = -F.logsigmoid(rm(chosen) - rm(rejected)).mean()
loss.backward()
opt.step()
losses.append(float(loss.item()))
return lossesfrom scratch: lab/solution.py: train_reward_model (+ RewardModel.forward)
- 1
RewardTrainer(model=...) wrapping a model with a scalar headRewardModel: TinyGPT trunk + nn.Linear(d_model, 1) scoring the last-token hidden state - 2
trainer.train() (the implicit pairwise loss)loss = -F.logsigmoid(rm(chosen) - rm(rejected)).mean() -- the Bradley-Terry / logistic loss - 3
chosen / rejected dataset columnsthe (chosen, rejected) tuples iterated from preference_pairs - 4
Trainer's optimizer + backward/step loopopt.zero_grad(); loss.backward(); opt.step() over epochs - 5
reward read at the final position of the sequenceself.score(h[:, -1, :]).squeeze(-1)
What the one call hides
- Default AdamW optimizer, the linear warmup+decay LR schedule, and weight decay you never see
- Tokenization, padding, and the attention/pad mask handling for variable-length chosen/rejected
- That the reward is read at the last NON-PAD token, not pooled over the sequence
- Optional margin / center-rewards regularization and reward-logit clipping in some RewardConfig settings
- Gradient accumulation, mixed precision, and multi-GPU sharding
- Gotcha: RewardConfig defaults are tuned for full LMs; on a tiny model the default LR can diverge
- Gotcha: If your dataset columns are not literally named chosen/rejected the trainer silently mis-maps them
- Gotcha: TRL moved the reward head / API around across versions (tokenizer vs processing_class); pin the version
In production use trl.RewardTrainer; write the loss by hand only to prove that reward-model training is just logistic regression on the score difference r(chosen)-r(rejected).
On the job: At work you write the RewardModel head (a Linear on the last-token hidden state) and the dataset plumbing; the Bradley-Terry loss and training loop come from RewardTrainer.
FIG 19.6 · Chapter notebook
Build this chapter with your own hands
A single self-contained notebook. You implement the ideas, check yourself against assert cells as you go, then finish with a capstone. Hint ladders and folded solutions throughout, so it runs top-to-bottom even before you fill anything in.
What you'll build
- A 4x4 gridworld solver: value iteration and policy iteration, checked against the exact Bellman fixed point (residual zero, and the two methods agree to machine precision).
- Tabular Q-learning that learns the same optimal policy by interacting, plus the early-break bug that silently returns an all-zero value function, broken then fixed.
- The REINFORCE policy gradient by hand in NumPy, checked against finite differences, then PPO that balances a self-contained CartPole to 200+ steps in under a minute.
- A toy RLHF stack on a 6-token language model: a Bradley-Terry reward model, PPO fine-tuning, and the moment the policy collapses to ....... when you drop the KL penalty.
- DPO and GRPO as one-function variants on the same preference data, and a reward-hacking gridworld where optimizing the proxy reward gets a true return of exactly zero.
~6 min on CPU · 115 cells · 18 checked exercises · runs in Colab
FIG 19.7 · Going further
14-arena-notebooks/chapter2-part1-intro-to-rlthroughchapter2-part4-rlhf— the entire ARENA another chapter sequence. The single best hands-on RL curriculum available for free. If you do all six notebooks (intro, Q-learning, DQN, VPG, PPO, RLHF) you will have implemented the full stack in PyTorch.23-textbooks/sutton-barto-rlSutton & Barto, the canonical RL textbook. Chapters 3-9 cover everything in sections 1-6 of this chapter in proper depth. Worth reading once if you plan to do serious RL research.
24-founder-blogs/karpathy-rlPong from Pixels. 130 lines of NumPy, policy gradients from scratch, the single best document for building first-principles intuition.
28-uni-courses/cs285-rail-eecs-berkeley-edu-deeprlcourseSergey Levine's deep RL course. Lecture-quality treatment of policy gradients, Q-learning, model-based RL, and inverse RL. The slides are the best in the field.
04-stanford/cs336-lecture_17Stanford's CS336 "Language Modeling from Scratch" Lecture 17 on alignment and RL. Specifically about RLHF-for-LMs framing.
18-lilian-weng/2024-11-28-reward-hackingLilian Weng's taxonomy and the single best survey on reward-hacking patterns and mitigations as of 2024.
06-practice/hf-alignment-handbookproduction-quality reference implementations of SFT, reward modeling, DPO, PPO, GRPO. Read after you understand the math.
06-practice/hf-smol-coursethe bite-sized companion to the alignment handbook. Each unit is a small notebook.
25-alignment-canon/www-anthropic-com-news-core-views-on-ai-safetyread once for the conceptual framing of why this chapter's safety lens matters.
FIG 19.8 · What this enables
Chapters you can now read, with the connecting idea written out.
FIG 19.9 · 30 sources
- - `01-explorables/distill-augmented-rnns`
- - `03-curricula/d2l-ch17-rl`
- - `03-curricula/hf-deep-rl-course-intro`
- - `03-curricula/hf-deep-rl-course-unit0`
- - `04-stanford/cs336-lecture_17`
- - `05-safety/anthropic-alignment-index`
- - `05-safety/anthropic-research-core-views-on-ai-safety`
- - `06-practice/hf-alignment-handbook`
- - `06-practice/hf-smol-course`
- - `06-practice/hf-smol-course-readme`
- - `14-arena-notebooks/chapter2-part1-intro-to-rl`
- - `14-arena-notebooks/chapter2-part2-q-learning-and-policy-gradient`
- - `14-arena-notebooks/chapter2-part21-dqn`
- - `14-arena-notebooks/chapter2-part22-vpg`
- - `14-arena-notebooks/chapter2-part3-ppo`
- - `14-arena-notebooks/chapter2-part4-rlhf`
- - `18-lilian-weng/2018-02-19-rl-overview`
- - `18-lilian-weng/2018-04-08-policy-gradient`
- - `18-lilian-weng/2018-05-05-drl-implementation`
- - `18-lilian-weng/2024-11-28-reward-hacking`
- - `20-aisafetybook/organizational-risks`
- - `22-anthropic-recent/2025-attribution-graphs-biology`
- - `23-textbooks/sutton-barto-rl`
- - `24-founder-blogs/karpathy-rl`
- - `24-founder-blogs/huyenchip-huyenchip-com-2023-05-02-rlhf-html`
- - `25-alignment-canon/www-anthropic-com-news-core-views-on-ai-safety`
- - `25-alignment-canon/www-lesswrong-com-posts-umq3cqwdphhjtiesc-agi-ruin-a-list-of-lethalities`
- - `25-alignment-canon/www-anthropic-com-research`
- - `26-pentest-redteam/owasp-org-www-project-top-10-for-large-language-model-applications`
- - `28-uni-courses/cs285-rail-eecs-berkeley-edu-deeprlcourse`