CS224R · HOMEWORK AS FORGE · ONLINE REINFORCEMENT LEARNING

HW2 — Online RL

A 5×4 grid, two goals, and one small number that flips which goal is optimal. Then the same Bellman recursion, scaled up to neural nets twice: PPO on the good side, off-policy actor-critic on the sample-efficient side. Q-learning is the seed of all of it.

Prerequisites: basic algebra + comfort with a neural network as a function. No prior RL assumed. NumPy & PyTorch ideas introduced as needed — the code you run here is numpy only.
12
Chapters
12
Live Sims
4
Code Labs
1
Forge Studio

A companion & practice forge for Stanford's CS 224R Homework 2 (Online RL). It credits the public course materials and teaches you to implement the core math yourself — it is not a copy-paste answer bank. The real starter-code contracts you meet here (choose_action, the Q-update, compute_gae, the clipped surrogate, update_critic) are the ones from the homework, shrunk to a scale you can run in your browser with numpy alone — no torch, no gym, no cluster.

Chapter 0: The Flip

You have a tiny robot on a 5×4 grid. It starts bottom-left. There are two goal squares: a far, valuable one up in the top-right corner, and a near, cheaper one straight to the right. The robot knows nothing — no map, no rules, just the ability to try moves and feel a reward.

Train it with a mild cost of −1 per step. It learns to march up the diagonal to the far, valuable goal. Sensible: the extra reward there is worth the extra walking.

Now change one number. Make each step cost −2 instead of −1. Retrain from scratch. The same algorithm, same grid, same goals — and the robot now walks straight to the near, cheaper goal and ignores the valuable one entirely. A single knob, and the optimal behavior flipped.

One number flips the policy

The 5×4 grid. G1 (far, +10) sits top-right; G2 (near, +5) sits to the right of the start. Drag the step reward. Watch the arrows — the learned policy — and the highlighted path re-solve to a different goal. This is the crux of the whole first problem.

step reward −1
The big reveal. The optimal policy is not set by which goal is bigger. It is set by the whole reward landscape — and the per-step cost reshapes that landscape. At −1 the 3 extra steps to the far goal cost 3, less than its 5-point bonus edge, so the far goal wins. At −2 those 3 extra steps cost 6, wiping the bonus out, so the near goal wins. Reward design is that sharp.

This is reward shaping — and it is one of the hardest problems in real RL. The robot always does exactly what the reward tells it to. Your only lever is what you reward. Get it slightly wrong (a positive step reward, say) and the robot happily learns to never finish the task at all, because loitering pays better than any goal. We will make that happen too, on purpose, in Chapter 6.

You double the per-step penalty (from −1 to −2) and the goal rewards stay the same. Before any math — what is the most likely reason the robot switches from the far, valuable goal to the near, cheap one?
Where we are headed. Twelve chapters. We build tabular Q-learning from the Bellman equation, watch the reward flip (Problem 1), then scale the exact same recursion to neural nets two ways: PPO — on-policy, advantage + GAE + a clipped surrogate (Problem 2) — and an off-policy actor-critic with an ensemble of critics and a min-of-two-random target (Problem 3). There is a Forge Studio (the ⚒ button) where you build all three real HW2 kernels on live instruments: the gridworld with its flipping arrows, a PPO advantage strip, and a critic-ensemble bar chart. Numpy only.

Chapter 1: The Gridworld

Before we touch a robot arm or a neural network, we solve a simpler version of the same problem in a world you can hold entirely in your head. That is the point of Problem 1 — and it is the right warm-up because tabular Q-learning has every property of "real" RL except one: function approximation.

The world

A grid 5 wide, 4 tall. The agent always starts at (0, 0) — bottom-left. There are two goal cells:

GoalPositionDistance from startReward
Goal 1 (far)(4, 3) top-right7 steps (4 right, 3 up)the bigger bonus
Goal 2 (near)(4, 0) right edge4 steps (4 right)the smaller bonus

Four actions: left, right, up, down, integer-coded {0, 1, 2, 3}. Moving off the edge keeps you in place but still costs the per-step reward — walls are not free. Reaching either goal ends the episode.

The reward function — three numbers

The reward is parameterized by three scalars. Every step the agent gets r_step; landing on a goal adds a one-time bonus:

r(s, a, s′) = rstep + R1 · 𝟙[s′ = Goal 1] + R2 · 𝟙[s′ = Goal 2]

where 𝟙[·] is the indicator — 1 if the condition holds, 0 otherwise. Three scenarios use the same algorithm and produce three different optimal policies:

ScenariorstepR1R2Optimal goal
1−1105Goal 1 (far)
2−2105Goal 2 (near)
3+111neither — stall forever
The hidden hard part is already here. The optimal policy is not determined by goal magnitude alone — the per-step reward reshapes the value landscape. Scenario 3 is worse still: a positive step reward makes loitering more profitable than finishing. That is reward hacking in miniature, and it is the whole lesson of Problem 1.

Why tabular first

Because the state is enumerable and actions are discrete, you can store every value in a table — a cube of shape (height, width, 4). No neural network. No PyTorch. Just numpy. That means three superpowers:

The (x, y) vs (y, x) trap. States are stored as (x, y) tuples (column, row), but the Q-table is indexed q[y, x, action] (row, column, action) — because numpy is row-major, so the first axis is the slowest-varying and q[y] gives you a whole grid row. Get the flip wrong and you train the wrong cells with no crash. A silent bug. The code's action_values(q, state) helper keeps the flip in one place.
Why is the Q-table shaped (height, width, 4) and not (width, height, 4)?

Chapter 2: RL Foundations

Before deriving anything, we nail the language. Five words. Each is a precise object; sloppiness here costs you the whole homework.

TermSymbolWhat it is
StatesA complete description of the world right now. Here, just (x, y). It has the Markov property: it contains everything needed to predict the next state and reward, no history required.
ActionaWhat the agent does — one of {left, right, down, up}. The agent's only way to influence the world.
Rewardr(s, a, s′)A scalar: how good was this transition? The agent's only goal is to maximize total reward. We design r to encode what we want.
Policyπ(a | s)A rule for choosing actions. In Q-learning we derive it from the table by argmax: pick the action with the highest Q-value.
ReturnGtThe total discounted reward from time t onward (below).

The return, and why we discount

The return is the sum of all future rewards, each future one shrunk by a factor of the discount γ:

Gt = rt + γ rt+1 + γ2 rt+2 + γ3 rt+3 + …

Here γ = 0.98. Rewards far in the future are worth less than rewards now. Three reasons this matters:

The bounded-sum number, worked. In Scenario 3 the agent can earn +1 forever. The return of "+1 every step, discounted at γ = 0.98" is the geometric series 1 + 0.98 + 0.98² + … = 1 / (1 − 0.98) = 50. When you train Scenario 3, the Q-values at the start converge to exactly 50.0 — the optimal value of "stay alive forever." That is not a coincidence; it is the closed form of the geometric sum.

The objective

All of RL boils down to one line: find the policy π that maximizes the expected return.

maximize   𝔼[Gt] = 𝔼[ rt + γ rt+1 + γ2 rt+2 + … ]

The expectation accounts for randomness in the policy and the environment. We want a policy that performs well on average — not one that got lucky once. The Q-function is the tool that lets us find it. That is the next chapter.

The discount, visualized

Each bar is one future reward's present value — a reward of 1 that arrives k steps from now is worth γk today. Drag γ and watch the horizon shrink or stretch. At γ = 1 nothing decays (and infinite sums blow up); at γ = 0.9 the future fades fast.

discount γ 0.980
In Scenario 3 (rstep = +1), what is the exact discounted return of "collect +1 forever" at γ = 0.98, and why does the math need γ < 1?

Chapter 3: The Bellman Equation

All of Q-learning rests on one recursion, and it falls out of the definitions with a single algebraic trick. Watch it happen.

From return to Q-value

Define the Q-value as the expected return if you take action a in state s, then follow π forever:

Qπ(s, a) = 𝔼[ Gt | st = s, at = a ] = 𝔼[ rt + γ rt+1 + γ2 rt+2 + … ]

The trick. Pull the first reward out of the sum, and factor a γ out of everything that remains:

Qπ(s, a) = 𝔼[ rt + γ(rt+1 + γ rt+2 + γ2 rt+3 + …) ] = 𝔼[ rt + γ Gt+1 ]

But the bracketed thing is just the return from time t+1 — which is itself a Q-value, at the next state and next action. So the Q-value refers to itself one step ahead:

Qπ(s, a) = 𝔼[ r(s, a, s′) + γ Qπ(s′, a′) ]

This is the Bellman expectation equation. The value of (s, a) decomposes into the immediate reward plus the discounted value of where you land. That recursion is the entire engine of temporal-difference learning — tabular, neural, on-policy, off-policy, all of them.

The optimality version

If we want the optimal Q-function — the value of always acting greedily from here on — we replace "follow π" with "always take the best next action":

Q*(s, a) = 𝔼s′[ r(s, a, s′) + γ maxa′ Q*(s′, a′) ]

The maxa′ is what makes this an optimality condition: from the next state, take whichever action has the highest Q-value. This is a fixed-point equation — Q* is its unique solution. Solve it, even approximately, and you have solved the RL problem, because the optimal policy is just π*(s) = argmaxa Q*(s, a).

Two ways to solve a fixed point. Value iteration sweeps every state and sets Q to the expected right-hand side — but it needs the environment's transition probabilities, which we usually do not know. Q-learning instead samples transitions by acting, and nudges Q toward the observed right-hand side — no transition model required. We use Q-learning. Next chapter turns this equation into an update rule.
Bellman backup: the value flows one step

A single Bellman backup at a cell. The reward on the edge plus the discounted best value at the next cell becomes the new value here. Press step to watch value ripple one cell outward from the goal per backup — exactly how reward propagates during training.

What single algebraic step turns the definition of the return into the Bellman recursion?

Chapter 4: Q-Learning, Derived

Q-learning is the Bellman optimality equation, rewritten as an update rule. Here is the whole derivation.

The naive idea, and why it fails

If we knew the right-hand side r + γ maxa′ Q*(s′, a′) exactly, we would just set Q[s, a] equal to it. Done. But we do not know it — we only have one noisy sample of it, computed from a single observed transition. So instead of overwriting, we nudge: move Q a fraction α of the way toward the observed target.

Q(s, a) ← Q(s, a) + α [ r + γ maxa′ Q(s′, a′) − Q(s, a) ]

This is the most important equation in the homework. Read it as three pieces:

PieceNameMeaning
r + γ maxa′ Q(s′, a′)TD targetWhat we now think Q(s, a) should be, after one more step of evidence.
Q(s, a)current estimateWhat we thought before.
target − Q(s, a)TD errorHow wrong we were. Positive → push Q up; negative → push Q down.

So the update reads: "new Q = old Q + α · (how wrong we were)." Equivalently, it is a smoothed running average:

Q(s, a) ← (1 − α) Q(s, a) + α · target

With α = 0.2 (this homework's default), each update moves us 20% of the way toward the latest evidence, keeping 80% of the old belief. A gentle running mean over one-sample targets.

The terminal edge case — a real bug, not a nicety

If s′ is terminal (the episode just ended), there is no future. The target is just the reward:

target = r   if done,    else    target = r + γ maxa′ Q(s′, a′)
Named failure mode: the missing if done. If you always use the bootstrap formula even on terminal steps, you add γ max Q(goal, ·) to the target. At first Q(goal, ·) = 0 so you get lucky — but the goal is terminal, so its Q-values were never legitimately learned. Once spurious updates make them non-zero, every target that bootstraps off the goal is biased upward, and training slowly poisons itself. Symptom: Q-values near the goal creep upward without bound and the greedy policy becomes erratic near the goal. The fix is the if done branch.

Why it converges

Theorem (Watkins, 1989): if every (s, a) pair is visited infinitely often and α is decayed appropriately, tabular Q-learning provably converges to Q* — the unique fixed point of the Bellman optimality equation. Why? At convergence the expected TD error is zero, which happens exactly when Q satisfies Bellman. The algorithm is stochastic gradient descent on the Bellman residual, and it converges because the Bellman operator is a contraction.

The intuition that survives all the math. You are never told the right answer. You only ever see one-step samples of reality. Each sample lets you nudge one table entry a little, toward less error. Repeat 100,000 times. The noise averages out, and your table converges to the truth.

A worked propagation, by hand

Reward only ever happens at goals — yet after enough episodes every cell on the path "knows" it leads to reward. Here is the mechanism, step by step, with α = 0.2, γ = 0.98, rstep = −1, R1 = 10.

Episode 1. By luck, the agent reaches (4, 2) and goes UP into Goal 1. Transition: reward = −1 + 10 = 9, done = True. Terminal target = 9:

Q[(4,2), UP] ← 0 + 0.2 · (9 − 0) = 1.8

One cell is now non-zero. Everything else is still zero.

Episode 2. The agent reaches (3, 2) and goes RIGHT to (4, 2): reward = −1, not done. Bootstrap target uses the 1.8 we just learned:

target = −1 + 0.98 · 1.8 = 0.764  →  Q[(3,2), RIGHT] ← 0 + 0.2 · 0.764 = 0.153

Now (3, 2) knows that going right is on a path to reward. Episode after episode, this boundary of "cells with non-zero Q" expands one cell toward the start — a bright wave spreading like dye through water. After enough episodes every cell on the optimal path points at the goal, its value geometrically decayed by γ per step.

What does the update Q ← Q + α(target − Q) mean intuitively, and what is target − Q called?

Chapter 5: Explore vs Exploit

The Q-update is one half of the algorithm. The other half is: which (s, a) do we update? If we always take the action our current Q says is best, we will never try alternatives — and we might miss a better path forever.

The dilemma

You need both. Pure greedy gets stuck in bad habits; pure random never acts on what it learned.

ε-greedy: the simplest balance

π(a | s) = uniform over the 4 actions  with prob ε  (explore)    else   argmaxa′ Q(s, a′)  with prob 1 − ε  (exploit)

Set ε high early (explore a lot) and decay it low later (mostly exploit). This homework decays linearly from 0.4 to 0.02 across 5000 episodes:

python
def epsilon_for_episode(scenario, episode_idx):
    fraction = episode_idx / max(1, scenario.episodes - 1)
    return scenario.epsilon_start + fraction * (scenario.epsilon_end - scenario.epsilon_start)
Named failure mode: no exploration at all. At episode 0 the Q-table is all zeros. argmax([0,0,0,0]) returns 0 — LEFT. Without exploration the agent picks LEFT every step, hits the left wall, never sees a goal, and the Q-table stays zero forever. Symptom: evaluation shows "agent walks left into the wall the whole episode," total reward is just horizon × rstep, and training never converges. The cure is a non-zero ε early: 40% random moves is plenty of stumbling to eventually trip into a goal, at which point the reward propagates one cell and the cascade begins.

Two subtle implementation bugs

The homework's choose_action is four lines, and two mistakes lurk:

The ε schedule and its effect

Top: ε decaying linearly 0.4 → 0.02 across training. Drag the episode marker. The dots below show a sample of the agent's move choices at that point — mostly random early (blue = explore), mostly greedy late (teal = exploit). Learning needs the early scatter.

episode 200
The connection forward. Neural RL has the same problem on continuous actions. PPO (Chapter 7–8) explores via an entropy bonus on a stochastic policy; the off-policy actor-critic (Chapters 9–10) explores via fixed-std Gaussian noise + replay-buffer diversity. All three are the same idea: keep the policy random enough to keep discovering reward.
Why is ε set high at the start of training and decayed low by the end?

Chapter 6: The Reward Flip

Now the payoff of Problem 1. Same algorithm, same grid, same two goals. We change only rstep and watch three completely different optimal policies emerge — including one that refuses to do the task at all. This is the heart of the homework: the optimal policy is not determined by goal magnitude — the per-step reward reshapes the value landscape.

The arithmetic, done by hand

Both goals are reachable by an optimal-length path. Compute the total return of each path directly:

Scenario 1 — rstep = −1, R1 = 10, R2 = 5:

Goal 1 (far, 7 steps): 7 × (−1) + 10 = 3     Goal 2 (near, 4 steps): 4 × (−1) + 5 = 1

Goal 1 wins by 2. The far, valuable goal is worth the walk.

Scenario 2 — rstep = −2, R1 = 10, R2 = 5: goals unchanged, penalty doubled:

Goal 1 (far): 7 × (−2) + 10 = −4     Goal 2 (near): 4 × (−2) + 5 = −3

The flip. Goal 2 now wins by 1. The 3 extra steps to Goal 1 cost 6 with the doubled penalty — more than the 5-point reward gap. A single number moved, and the optimal goal switched from far to near.

Scenario 3 — rstep = +1, R1 = R2 = 1: the step reward is now positive:

Reach the near goal (4 steps): 4 × (+1) + 1 = 5     Stall against a wall for the whole horizon: 20 × (+1) + 0 = 20

Stalling wins by a landslide. The agent learns to walk LEFT into the wall and soak up +1 per step for all 20 steps. Reaching a goal would end that profitable stream after a single bonus. The start Q-values converge to exactly 50.0 — the geometric-sum value of "+1 forever."

Reward hacking, named. Scenario 3 is the canonical example: the agent maximizes the reward you specified, which is not the behavior you intended. In real RL, reward design is one of the hardest engineering problems. Sparse rewards — reward only at task completion, which is exactly what Problems 2 and 3 use — are chosen precisely to dodge this trap: if the only way to get reward is to finish, you cannot hack it. The cost is that learning is slower because reward is rare.

Reading the actual Q-values

After training, the numbers tell the story. In Scenario 1 the start-state Q-values come out (LEFT, RIGHT, DOWN, UP) = (1.219, 2.265, 1.219, 2.265). LEFT and DOWN tie low (both walk into a wall, wasting the step); RIGHT and UP tie high (both head toward Goal 1's diagonal). In Scenario 2 they become (−4.996, −3.057, −4.996, −4.329) — now RIGHT is distinctly best and UP is worse, because the optimal path is straight right to Goal 2. The table figured out that the policy should change.

The reward flip — the whole grid re-solves

The trained 5×4 gridworld. Each cell's shade = its state value (warm = high). Each arrow = the greedy action there. The highlighted path runs from the start to whichever goal the current policy prefers. Flip rstep and watch the arrows and the path re-solve — far goal, near goal, or stall in place.

step reward rstep −1
Carry this. Three small changes to one number produced three optimal policies — far goal, near goal, and "never finish." That sensitivity is reward shaping. It is the same lever that, scaled up, makes real robot-reward design so hard — and it is why the neural problems ahead use sparse, completion-only rewards you cannot game.
Doubling the step penalty flips the optimal goal from far to near. What is the precise reason, in return arithmetic?

Chapter 7: PPO — Advantage & GAE

Leave the grid behind. Now the state is 39 numbers describing a robot arm, the action is a 4-D continuous vector, and the reward is sparse — 0 every step, +1 the instant a nail is driven, then done. You cannot enumerate a Q-table over a continuous action. So we switch to a policy-gradient method: PPO, the most widely used one in modern RL. It is on-policy — collect rollouts under the current policy, update, discard.

The objective and the policy gradient

We want to maximize expected discounted return, J(θ) = 𝔼τ~πθ[ Σt γt rt ]. The policy gradient theorem gives its gradient in a form we can estimate from samples:

θ J(θ) = 𝔼τ[ Σtθ log πθ(at | st) · Gt ]

In English: take each action you sampled and increase its log-probability in proportion to how good the future turned out after it. Good actions — push up. Bad actions — push down. The miracle of the log-derivative trick (∇ log p = (1/p) ∇ p) is that the unknown environment dynamics cancel out of the gradient. Implement just this and you have REINFORCE — correct, but painfully slow. Two problems make it slow, and PPO fixes both.

Problem 1: variance → use a baseline (advantage)

The return Gt swings wildly across rollouts. We can subtract any state-dependent baseline b(st) from Gt without changing the gradient's expectation (the cross-term provably has zero mean) but with a big drop in variance. The natural baseline is the state-value V(s) = 𝔼[Gt | st = s] — how good is this state on average? Subtracting it gives the advantage:

A(s, a) = Q(s, a) − V(s)

"How much better was action a than the policy's average action in this state?" Positive → above average, push toward it; negative → below average, push away. The clean policy gradient becomes:

θ J = 𝔼[ Σtθ log πθ(at | st) · A(st, at) ]

Problem 2: how do we estimate A? → GAE

We do not know V, so we train a critic Vφ(s) alongside the policy. With a critic, the two extreme advantage estimators are:

EstimatorFormulaBiasVariance
1-step TDrt + γ V(st+1) − V(st)high (leans on imperfect V)low
Monte CarloGt − V(st)zerohigh (sums many random rewards)

Generalized Advantage Estimation (Schulman et al., 2016) blends the two with a knob λ ∈ [0, 1]. This is the exact content of your compute_gae:

δt = rt + γ(1 − dt) V(st+1) − V(st)   (the 1-step TD error)
At = δt + γ λ (1 − dt) At+1   (the backward recursion)

Computed backwards through the trajectory. The (1 − dt) done-mask is the same terminal trick from Problem 1 — it zeros the bootstrap at episode ends. Unroll the recursion and it is a geometrically-weighted sum of future TD errors:

At = δt + (γλ) δt+1 + (γλ)2 δt+2 + (γλ)3 δt+3 + …

Each future TD error contributes less by a factor of γλ. The backward pass is just the O(T) way to compute this sum. At λ = 0 you get pure 1-step TD; at λ = 1, Monte Carlo. This homework uses λ = 0.99 — "trust the real rewards mostly, blend in the critic to dampen variance."

The returns output, and why grads are off. compute_gae returns both advantages and returns, where returnst = At + V(st). Since A = Q − V, returns = A + V is our low-variance estimate of Q — used as the regression target for the critic. Critically, the whole GAE computation runs inside torch.no_grad(): these are targets. If gradients flowed through them, the critic would be trained against its own moving prediction and chase its own tail. Symptom of forgetting no_grad: the critic loss explodes to 1e6+.
GAE: the bias–variance dial

A toy 8-step trajectory with a sparse reward at the end. Each bar is the advantage At at that step. Drag λ from 0 (pure 1-step TD — blocky, biased, low variance) toward 1 (Monte Carlo — smooth credit flowing all the way back, higher variance). Watch the credit-assignment reach further back as λ grows.

GAE λ 0.95
Why does GAE iterate backwards through time, and why is the whole computation wrapped in torch.no_grad()?

Chapter 8: PPO — The Clip

Vanilla policy gradient is unstable for one reason: a single big update can move the policy so far that the rollouts you collected no longer represent it, and learning collapses. PPO's whole stability mechanism is one clever objective that caps how far the policy can move per update.

The importance ratio

Define the ratio between the new and the rollout-time (old) policy for the same action:

ρt = πθ(at | st) / πθold(at | st) = exp( log πθ(at | st) − log πθold(at | st) )

If the policy has not changed, ρ = 1. Doubled the action's probability → ρ = 2; halved → ρ = 0.5. We compute it as exp(new_log_prob − old_log_prob), not a direct division, because densities for a 4-D continuous action can be tiny (1e-30); subtracting log-probs and exponentiating is numerically safe.

The clipped surrogate

LCLIP = − meant [ min( ρt · At, clip(ρt, 1−ε, 1+ε) · At ) ]

Two terms inside the min. ρ · A is the unclipped surrogate (what vanilla PG uses). clip(ρ, 1−ε, 1+ε) · A caps ρ to [0.9, 1.1] for this homework's ε = 0.1. Then: take the elementwise min, average, negate (PyTorch minimizes; the objective is a max).

Why min — the pessimistic cap

Walk both signs of the advantage:

The min always picks the worse outcome from the optimizer's view — a pessimistic bound. The optimizer cannot game the surrogate by lunging too far in either direction. That is the entire PPO stability story.

Why min and not average? Because the optimizer is adversarial. Averaging the two surrogates would let it blow past the clip wherever the unclipped term is favorable, defeating the point. The min is a pessimistic bound: the optimizer always sees the worse of the two, so it cannot exploit the clip.

The three extra loss terms

The full PPO loss adds three terms to the clipped surrogate:

L = LCLIP + cv · Lvalue − cH · H[π] + cKL · DKLθ ‖ πref)
TermWhat it does
Value loss — MSE(Vφ(s), returns)Trains the critic toward the GAE returns. Coefficient ~0.5.
Entropy bonus −cH H[π]Rewards a spread-out action distribution — keeps exploration alive so the policy doesn't collapse to nearly deterministic. Subtracted from the loss (added to the objective). Coefficient ~0.001–0.01.
Reverse KL to a frozen referencePenalizes drift from the BC-pretrained policy (below).

Reverse KL to a frozen BC reference

Before RL, the policy is warm-started by behavior cloning on 20 expert demos. A frozen snapshot of that BC policy becomes the reference_actor. The reverse-KL term keeps the RL policy anchored near it:

LKL = 𝔼old[ ρ · (log πθ − log πref) ]   (an estimate of DKLθ ‖ πref))

Why: with sparse rewards, BC is the only thing that roughly knows how to do the task. Pure RL would forget it. It is called "reverse" because it is the KL from the new policy to the reference — mode-seeking, pushing the new policy to cover the reference. The off-policy problem solves the same "don't forget the demos" disease with a different antibiotic (alternating BC updates), which we meet in Chapter 9.

Named failure mode: entropy collapse. If entropy_coef is too small, the actor's action std shrinks toward 0 in the first ~50k steps — the policy becomes nearly deterministic, stops exploring, and freezes at whatever it happened to learn early. Symptom: actor_std crashes toward 0 on the training plot and eval/episode_success plateaus far below the achievable rate. The cure is a large-enough entropy bonus.
The clip: the pessimistic cap in action

The PPO objective per transition as a function of the ratio ρ. Toggle the advantage sign. For a good action (A > 0) the objective climbs then plateaus at ρ = 1+ε — the flat region is the clip, where the gradient vanishes. For a bad action (A < 0) it bottoms out at 1−ε. Drag ε to widen or narrow the safe region.

clip ε 0.10
For a good action (A > 0), what happens to the gradient once the ratio ρ exceeds 1 + ε?

Chapter 9: Off-Policy & the Deadly Triad

Same robot, same hammer, same nail, same sparse reward. Completely different algorithm. PPO needed ~1,000,000 environment steps. This off-policy actor-critic reaches >90% success in ~100,000 — a 10× sample-efficiency gap. Understanding where that gap comes from is the whole lesson.

Why off-policy can reuse old data

PPO is on-policy: its gradient is an expectation over (s, a) drawn from the current policy, so stale data gives the wrong gradient — it must collect fresh rollouts and throw them away. Off-policy learns a Q-function instead, and the Bellman equation it enforces is a property of the environment, not of any policy:

Q(s, a) = r(s, a) + γ 𝔼[ Q(s′, a′) ]

As long as you have a tuple (s, a, r, s′), you can use it to enforce this constraint — regardless of which policy produced it. So you keep a replay buffer of every transition ever seen and replay it thousands of times. That is the source of the sample-efficiency win. The chef analogy: on-policy tastes a dish, writes the rating, throws the recipe away. Off-policy stores every entry in a giant cookbook and re-reads it forever.

Why Q, not V, here. PPO used V(s) as a baseline. Off-policy uses Q(s, a) because actions matter directly: to improve the actor we push it toward actions the critic scores high, which needs an action-level signal. With V(s) we would only know whether a state is good on average and would have to recover action-level signal through the noisy policy gradient. Q-learning skips that middleman.

The deadly triad

The clean story above hides a landmine. Three properties, together, make neural Q-learning diverge:

IngredientWhy it's dangerous
Function approximationA neural Q leaks: updating one (s, a) changes many others.
BootstrappingThe target r + γ Q(s′, a′) uses the same network we're optimizing — we regress toward a moving label.
Off-policy dataThe sampled (s, a) distribution doesn't match the policy being evaluated, so errors can amplify.

Any one is fine. Any two, usually fine. All three together — exploding Q-values and training collapse. Two concrete failure modes and their fixes:

Failure 1: the moving target. If the same critic computes both the prediction and the target, one gradient step moves both. You reach for the flag and it runs away at your own speed. Fix: a target network — a slow copy of the critic used only for targets, updated by a soft Polyak average φ̄ ← (1−τ)φ̄ + τφ with τ = 0.005. On the timescale of one gradient step, the target looks frozen. Symptom of forgetting it: the target stays at random init, Q never propagates beyond γ per step, learning crawls, the critic loss looks fine but the actor never improves.

Failure 2: maximization bias. Vanilla Q-learning's target uses maxa′ Q(s′, a′). If Q is noisy — some actions overestimated, some under, on average correct — the max systematically picks the overestimated ones. Watch:

true Q = [1.0, 1.0, 1.0];   noisy estimate = [1.1, 0.9, 1.0];   max(estimate) = 1.1  (biased UP by 0.1);   true max = 1.0

You train Q toward an upward-biased target. Q grows. Next iteration, even more bias. Q-values explode. Fix: clipped double-Q — take the min over two critics, which counters the upward bias. That is Chapter 10.

The actor-critic fix for continuous actions

You cannot enumerate argmaxa Q(s, a) over a continuous 4-D action. So we train an actor πθ(a | s) whose job is to output the maximizer. Its loss is simply:

Lactor(θ) = − 𝔼s~buffer[ Qφ(s, πθ(s)) ]

"Plug the policy's chosen action into the critic; that scalar is what the actor wants to maximize" (minus to minimize). Gradient flows from Q back into θ via the reparameterization trick: sample a = tanh(μθ(s)) + 0.1·ε with ε ~ N(0,1) fixed — a is a differentiable function of θ. The critic tells you which actions are good; the actor learns to produce them; they bootstrap each other up. That ping-pong is actor-critic.

Behavior cloning during RL. With sparse reward, random exploration almost never sees a +1. So we warm-start the actor by BC on 20 demos (Lbc = −mean[log πθ(a|s)]), then keep mixing in BC steps during RL. Why during? Early on the critic is random junk; a pure actor step would walk the policy toward the critic's random preferences and forget the demos. The BC anchor holds it near the expert distribution until the critic becomes trustworthy. Same disease as PPO's reverse-KL, different antibiotic.
On-policy discards; off-policy replays

Both learn from transitions (dots). PPO (top) collects a rollout, updates a few times, and discards it — each transition is used once. Off-policy (bottom) drops every transition into a replay buffer and re-samples it many times. Press play; watch the reuse. With sparse rewards, the rare +1 transitions (warm) get replayed dozens of times on the bottom track.

Why can an off-policy method reuse a transition from any past policy, while on-policy PPO cannot?

Chapter 10: Ensemble Critics & the Min-of-Two Target

The fix for maximization bias is the technical heart of Problem 3's update_critic. Three stabilization tricks stack into four lines.

Trick 1 — target network (Polyak/EMA)

φ̄ ← (1 − τ) φ̄ + τ φ,  τ = 0.005

Each step the target critic moves 0.5% toward the online critic. After ~200 steps it catches up, but on any single update it looks frozen — a stable regression target. Called once per critic update via utils.soft_update_params(net, target_net, tau).

Trick 2 — an ensemble of N critics

Maintain N independent critic networks, each initialized differently and fed different minibatch orderings. They disagree on out-of-distribution inputs, and that disagreement is a signal. Default N = 2; the ablation uses N = 10. With N = 10 + a high update-to-data ratio you approach REDQ ("Randomized Ensembled Double Q-learning"), a state-of-the-art sample-efficiency recipe.

Trick 3 — random pair + min for the target

When computing the TD target, do not use all N critics. Pick 2 at random, take the elementwise min:

i, j ~ sample(1..N, 2 distinct);   y = r + γ(1 − done) · min( Q̄i(s′, a′), Q̄j(s′, a′) )

The min counters maximization bias (two overestimates min'd together is less of an overestimate). The randomization — vs always using critics 1 and 2 — is the REDQ improvement: it forces every critic to stay reliable, otherwise critics 3..N drift because they are never used in targets.

Named failure mode from the Common Bugs list — the shape-mismatch silent killer. reward and discount are shape [B]; the critic outputs are [B, 1]. If you don't .unsqueeze(-1) to align them, [B] + [B,1] broadcasts to [B, B] — the loss is a number, training runs, and it is gibberish. Other entries on the list: (1) forgetting torch.no_grad() on the target → critic loss explodes to 1e6+; (2) using random.choices (with replacement) instead of random.sample → can pick the same critic twice; (3) only updating the 2 sampled critics → the other N−2 drift; the loss must be summed over all N; (4) using self.critic instead of self.critic_target for the target — the single most common bug.

The full target, four lines

python
with torch.no_grad():
    a_next = actor(s_next).sample(clip=stddev_clip)     # next action from current policy
    target_q_list = critic_target(s_next, a_next)        # list of N tensors
    Qi, Qj = random.sample(target_q_list, 2)              # 2 DISTINCT critics
    y = reward.unsqueeze(-1) + discount.unsqueeze(-1) * torch.min(Qi, Qj)
# loss over ALL N online critics:
critic_loss = sum(F.mse_loss(q, y) for q in critic(s, a))
Why the ensemble saves aggressive replay (UTD). The update-to-data ratio (UTD) is how many critic updates you do per environment step. UTD > 1 exploits each transition more — but with only 2 critics it overfits recent buffer entries, the actor exploits the resulting Q-errors, and training collapses. A larger ensemble has lower variance in its target estimates, so min-of-2-random is more conservative when the underlying ensemble is more diverse — it tolerates high UTD. That pairing (UTD=5 + N=10) is the REDQ recipe.
Maximization bias vs the min-of-two

N critic estimates of Q(s′, a′) as bars (true value = the dashed line). Two are highlighted — the randomly-sampled pair — and their min is the teal marker used as the target. The red ghost bar is what max over the ensemble would give: an overestimate. Reshuffle to resample the pair and the noise; the min stays at or below the truth, the max stays above.

When computing the TD target we sample only 2 critics and take their min — but the critic loss is computed over all N. Why train all N if only 2 are used in the target?

Chapter 11: Showcase & Field Guide

One homework, three algorithms, one recursion underneath all of them. The Bellman equation you derived in Chapter 3 is the seed of every method in HW2. Here is the whole arc in one picture, then the cheat sheet.

Showcase: three learners, one task

Three sample-efficiency curves on the sparse-reward hammer task. PPO (on-policy) climbs slowly — ~1M steps to plateau, discarding every rollout. Off-policy, N=2 UTD=1 reaches high success in ~100k by replaying the buffer. Off-policy, N=10 UTD=5 (REDQ-like) is the most sample-efficient. Press play to watch them race; drag to compare where each is at a given step budget.

env steps 1000k

The three-algorithm comparison

Tabular Q (P1)PPO (P2)Off-policy AC (P3)
Classvalue, tabularpolicy gradientQ-learning + actor
Dataonline sampleson-policy, discardoff-policy, replay forever
Core updateQ += α(target − Q)clipped surrogate on advantageTD regression to min-of-2 target
Stability trickε-greedy + γclip + reverse-KL + entropytarget net + ensemble + min-of-2
Steps to plateau5000 episodes~1M~100k
Don't-forget-demosreverse-KL to frozen BC refBC steps mixed in during RL
The one idea underneath all three. Every method enforces the Bellman recursion — value = immediate reward + discounted future value. Tabular writes it directly into a cell. PPO estimates it as a GAE advantage and takes a clipped gradient step. Off-policy regresses a neural Q toward a stabilized bootstrap target. Same equation, three engineering realizations, chosen by your constraint: enumerable states → table; stable-but-slow → PPO; sample-efficient-but-fragile → off-policy.

Cheat sheet — the equations to carry

NameEquation
Bellman optimalityQ*(s, a) = 𝔼[ r + γ maxa′ Q*(s′, a′) ]
Q-learning updateQ ← Q + α[ r + γ maxa′ Q(s′, a′) − Q ]
ε-greedyuniform w.p. ε, else argmaxa Q(s, a)
GAEδt = r + γ(1−d)V(s′) − V(s);   At = δt + γλ(1−d)At+1
PPO clip−mean[ min( ρA, clip(ρ, 1±ε)A ) ],   ρ = exp(logπnew − logπold)
Off-policy targety = r + γ(1−done) min( Q̄i(s′,a′), Q̄j(s′,a′) )
Critic lossΣk=1..N ( Qk(s,a) − y )2  (all N)
Actor loss−(1/N) Σk Qk(s, πθ(s))
Polyak targetφ̄ ← (1−τ)φ̄ + τφ

Named failure modes — symptom → cause

SymptomCause & cure
Agent walks into the wall forever, Q stays 0No exploration (ε = 0 early). Use ε-greedy with a high start.
Q-values near the goal inflate without boundMissing if done in the target — bootstrapping off a terminal cell.
Robot never finishes, loiters insteadReward hacking — a positive step reward pays more than the goal. Use sparse rewards.
actor_std crashes to 0, success plateaus lowEntropy collapse in PPO. Raise entropy_coef.
critic_loss explodes to 1e6+Forgot no_grad on the target, OR a shape-mismatch broadcast, OR no soft update.
critic_loss stuck at exactly 0.0Gradients not flowing — wrong optimizer, missing backward, or loss disconnected.
Q-values explode, actor exploits nonsenseMaximization bias — using max not min-of-2, or too few critics for the UTD.

Self-quiz — answer these without re-reading and you're ready

  1. Why does Q-learning track actions in the table, not just states?
  2. What would happen with γ = 1 in Scenario 3, and why?
  3. Where does the if done: break go in the inner loop, and what breaks if it's misplaced?
  4. What does λ control in GAE, and what are λ = 0 and λ = 1 called?
  5. Why is the PPO ratio computed as exp of a difference, not a direct division?
  6. Why does the min in the PPO surrogate stabilize training?
  7. Why can off-policy reuse old data but on-policy cannot?
  8. Why sample 2 critics for the target but train all N?
  9. What's the difference between critic and critic_target, and when does each update?
  10. Why does PPO need ~1M steps but the off-policy method ~100k?
  11. What does the BC step during RL accomplish that pretraining alone can't?

Take it back to class. If a friend asks "How does Q-learning work?" — you say:

"It's stochastic gradient descent on the Bellman residual. You never know the right Q-values, but every transition gives a noisy sample of the right-hand side — immediate reward plus your best guess of the discounted future. Move toward that sample by α each time. Repeat and the table converges to the optimal Q-function. Two tricks: discount by γ to keep sums finite, and ε-greedy so every state-action gets sampled. Scale the table to a neural net and you get PPO and SAC — same recursion, different stabilization."

Where this connects

This forge is the second stop in the CS224R arc (HW1 was imitation learning). Related on the site:

"What I cannot create, I do not understand." — Feynman. You have now created all three: the table that nudges toward the Bellman target, the clipped policy gradient that dares not move too far, and the ensemble critic that refuses to fool itself. Build them in the Studio and the understanding is yours.

Companion & practice forge for Stanford CS 224R Homework 2 (Online RL). Credits the public course materials. Implement the core math yourself; this is not a solution answer bank.