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.
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.
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.
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.
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.
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.
A grid 5 wide, 4 tall. The agent always starts at (0, 0) — bottom-left. There are two goal cells:
| Goal | Position | Distance from start | Reward |
|---|---|---|---|
| Goal 1 (far) | (4, 3) top-right | 7 steps (4 right, 3 up) | the bigger bonus |
| Goal 2 (near) | (4, 0) right edge | 4 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 is parameterized by three scalars. Every step the agent gets r_step; landing on a goal adds a one-time bonus:
where 𝟙[·] is the indicator — 1 if the condition holds, 0 otherwise. Three scenarios use the same algorithm and produce three different optimal policies:
| Scenario | rstep | R1 | R2 | Optimal goal |
|---|---|---|---|---|
| 1 | −1 | 10 | 5 | Goal 1 (far) |
| 2 | −2 | 10 | 5 | Goal 2 (near) |
| 3 | +1 | 1 | 1 | neither — stall forever |
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:
(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.(height, width, 4) and not (width, height, 4)?Before deriving anything, we nail the language. Five words. Each is a precise object; sloppiness here costs you the whole homework.
| Term | Symbol | What it is |
|---|---|---|
| State | s | A 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. |
| Action | a | What the agent does — one of {left, right, down, up}. The agent's only way to influence the world. |
| Reward | r(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. |
| Return | Gt | The total discounted reward from time t onward (below). |
The return is the sum of all future rewards, each future one shrunk by a factor of the discount γ:
Here γ = 0.98. Rewards far in the future are worth less than rewards now. Three reasons this matters:
All of RL boils down to one line: find the policy π that maximizes the expected return.
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.
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.
All of Q-learning rests on one recursion, and it falls out of the definitions with a single algebraic trick. Watch it happen.
Define the Q-value as the expected return if you take action a in state s, then follow π forever:
The trick. Pull the first reward out of the sum, and factor a γ out of everything that remains:
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:
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.
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":
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).
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.
Q-learning is the Bellman optimality equation, rewritten as an update rule. Here is the whole derivation.
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.
This is the most important equation in the homework. Read it as three pieces:
| Piece | Name | Meaning |
|---|---|---|
| r + γ maxa′ Q(s′, a′) | TD target | What we now think Q(s, a) should be, after one more step of evidence. |
| Q(s, a) | current estimate | What we thought before. |
| target − Q(s, a) | TD error | How 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:
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.
If s′ is terminal (the episode just ended), there is no future. The target is just the reward:
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.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.
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:
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:
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.
Q ← Q + α(target − Q) mean intuitively, and what is target − Q called?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.
You need both. Pure greedy gets stuck in bad habits; pure random never acts on what it learned.
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)
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.The homework's choose_action is four lines, and two mistakes lurk:
int(np.argmax(q_values)) — the index of the best action (an integer the env can execute), not np.max (the value, a float). Returning the value makes the env assert 1.8 in (0,1,2,3), which is False, and crash.rng.random() and rng.integers(4), and only sample the random action inside the explore branch. Pre-sampling on every call (even when going greedy) advances the RNG state differently and makes two logically-equivalent runs diverge from the same seed. Using global np.random.* bypasses the seed entirely and destroys reproducibility.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.
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.
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 wins by 2. The far, valuable goal is worth the walk.
Scenario 2 — rstep = −2, R1 = 10, R2 = 5: goals unchanged, penalty doubled:
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:
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."
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 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.
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.
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:
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.
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:
"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:
We do not know V, so we train a critic Vφ(s) alongside the policy. With a critic, the two extreme advantage estimators are:
| Estimator | Formula | Bias | Variance |
|---|---|---|---|
| 1-step TD | rt + γ V(st+1) − V(st) | high (leans on imperfect V) | low |
| Monte Carlo | Gt − V(st) | zero | high (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:
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:
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."
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+.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.
torch.no_grad()?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.
Define the ratio between the new and the rollout-time (old) policy for the same action:
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.
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).
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.
The full PPO loss adds three terms to the clipped surrogate:
| Term | What 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 reference | Penalizes drift from the BC-pretrained policy (below). |
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:
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.
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 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.
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.
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:
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.
The clean story above hides a landmine. Three properties, together, make neural Q-learning diverge:
| Ingredient | Why it's dangerous |
|---|---|
| Function approximation | A neural Q leaks: updating one (s, a) changes many others. |
| Bootstrapping | The target r + γ Q(s′, a′) uses the same network we're optimizing — we regress toward a moving label. |
| Off-policy data | The 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:
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.
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:
"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.
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.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.
The fix for maximization bias is the technical heart of Problem 3's update_critic. Three stabilization tricks stack into four lines.
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).
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.
When computing the TD target, do not use all N critics. Pick 2 at random, take the elementwise min:
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.
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.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))
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.
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.
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.
| Tabular Q (P1) | PPO (P2) | Off-policy AC (P3) | |
|---|---|---|---|
| Class | value, tabular | policy gradient | Q-learning + actor |
| Data | online samples | on-policy, discard | off-policy, replay forever |
| Core update | Q += α(target − Q) | clipped surrogate on advantage | TD regression to min-of-2 target |
| Stability trick | ε-greedy + γ | clip + reverse-KL + entropy | target net + ensemble + min-of-2 |
| Steps to plateau | 5000 episodes | ~1M | ~100k |
| Don't-forget-demos | — | reverse-KL to frozen BC ref | BC steps mixed in during RL |
| Name | Equation |
|---|---|
| Bellman optimality | Q*(s, a) = 𝔼[ r + γ maxa′ Q*(s′, a′) ] |
| Q-learning update | Q ← Q + α[ r + γ maxa′ Q(s′, a′) − Q ] |
| ε-greedy | uniform 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 target | y = 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−τ)φ̄ + τφ |
| Symptom | Cause & cure |
|---|---|
| Agent walks into the wall forever, Q stays 0 | No exploration (ε = 0 early). Use ε-greedy with a high start. |
| Q-values near the goal inflate without bound | Missing if done in the target — bootstrapping off a terminal cell. |
| Robot never finishes, loiters instead | Reward hacking — a positive step reward pays more than the goal. Use sparse rewards. |
| actor_std crashes to 0, success plateaus low | Entropy 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.0 | Gradients not flowing — wrong optimizer, missing backward, or loss disconnected. |
| Q-values explode, actor exploits nonsense | Maximization bias — using max not min-of-2, or too few critics for the UTD. |
if done: break go in the inner loop, and what breaks if it's misplaced?exp of a difference, not a direct division?critic and critic_target, and when does each update?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."
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.