A 5×4 grid, two goals, and a reward change that reverses their preference. Build from tabular Q-learning to PPO and replay-based actor-critic: shared ideas about future reward, distinct learning objectives, and different tradeoffs in how experience is used.
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 a staircase path with four right moves and three up moves 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 optimizes the reward and termination contract you provide. Get it slightly wrong (a positive step reward under a continuing contract, say) and the robot may prefer loitering to finishing. We will make that loophole visible, 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 | continuing fixture favors loitering; finite horizon depends on masks |
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 π thereafter. The expectation is over the environment and policy randomness:
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 this controlled fixture ripple one cell outward from the goal per backup — an illustration of how evidence can propagate 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 instead of using the terminal reward alone. That is a semantic bug: depending on which terminal states are stored and updated, it can bias values near the goal upward and contaminate later backups. In this local corridor fixture, the terminal row is not updated, so the plotted starter and corrected values can look similar; the check below isolates the wrong terminal target directly. The fix is the if done branch, with a separate reset/truncation mask when a timeout is not true termination.Convergence contract. With a finite Markov decision process, sufficient visits to every relevant (s, a) pair, bounded rewards, and an appropriate decaying step size, the standard tabular Q-learning theorem gives convergence to Q*. The update is a semi-gradient stochastic approximation to a moving Bellman target; it is useful to understand the target as a noisy sample of the recursion, rather than as ordinary gradient descent on a fixed Bellman-residual objective. The contraction and step-size assumptions are doing real work.
Positive goal bonuses arrive only at goals, while each move still carries the step reward — yet after enough episodes every cell on the path can "know" it leads to reward. Here is the mechanism, step by step, with α = 0.2, γ = 0.98, rstep = −1, R1 = 10.
Controlled backup 1. Start with a zero table and isolate a transition from (4, 2) going UP into Goal 1. Transition: reward = −1 + 10 = 9, done = True. Terminal target = 9:
In this isolated fixture, only that entry has changed. During an actual episode, earlier negative step rewards would also update visited entries.
Controlled backup 2. Next isolate a transition from (3, 2) going RIGHT to (4, 2): reward = −1, not done. Bootstrap target uses the 1.8 we just learned:
Now (3, 2) has evidence that going right is on a path to reward. Episode after episode, updates can spread that evidence toward the start — a bright wave spreading like dye through water. In this controlled corridor fixture, repeated backups approach the discounted path values; stochastic visits, step costs, terminal handling, and finite episodes determine the actual table.
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 under this action ordering. With zero step reward and no exploration, the deterministic tie can make the agent walk into the left wall, never see a goal, and leave the table unchanged. A negative step cost is different: the first non-terminal update can make the attempted action negative, changing later ties, so the frozen-zero story is not universal. The cure is a non-zero ε early: random moves give the table a chance to see goals, after which rewards can propagate backward.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 can win in the continuing version of the task: the agent can collect +1 per step without ending the stream. A finite 20-step rollout collects only the discounted 20-step sum, while the continuing derivation is 50. Reaching a goal may still be optimal if the environment terminates or assigns a larger terminal return; the result depends on the full reward and termination contract.
For a representative illustrative snapshot, the numbers tell the story. In Scenario 1 the start-state Q-values are shown as (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 head toward the far goal by different staircase paths. In Scenario 2 an illustrative snapshot is (−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. These displayed values explain the policy flip; they are fixtures for intuition, not a benchmark claim.
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. It is on-policy: collect a rollout under a frozen old-policy snapshot, reuse that batch for the configured few epochs (three in the starter), then collect a fresh rollout after the policy update.
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) and a well-chosen baseline can reduce variance; an arbitrary baseline need not. 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) | depends on critic error; zero with exact V | low |
| Monte Carlo | Gt − V(st) | zero under exact returns | high (sums many random rewards) |
Generalized Advantage Estimation (Schulman et al., 2016) blends the two with a knob λ ∈ [0, 1]. This is the recurrence implemented by your compute_gae, together with its rollout masks:
Computed backwards through the trajectory. The (1 − dt) done-mask zeros the bootstrap at true episode ends; a timeout or environment reset needs the separate mask required by the rollout contract. 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 minus the baseline on a complete terminated trajectory; truncated rollouts can retain a final-value bootstrap. 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). Form returns = A + V_old before normalizing advantages. This bootstrapped return estimate is the fixed target for critic regression. 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; an unstable or drifting critic loss is a possible symptom. Preserve both the true-terminal mask and the reset/truncation mask required by the rollout contract.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, typically biased, low variance) toward 1 (Monte Carlo-like — smooth credit flowing all the way back, higher variance). Watch the credit-assignment reach further back as λ grows.
torch.no_grad()?Vanilla policy gradient can be unstable when a single big update moves the policy far from the rollout distribution. PPO uses an objective that removes incentive for some large ratio changes per batch; it is a trust-region-like heuristic, not a hard bound on every policy update or ratio.
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 batch surrogate. It can reduce the incentive to lunge past the clip, while diagnostics and the KL term still matter because the objective does not guarantee every update stays inside the interval.
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). |
Distribution caveat. The starter clamps Gaussian action samples but inherits the ordinary Normal log-density. That density is not the exact distribution of the clamped output, which has boundary mass. Its PPO ratio and KL calculation therefore follow the implementation convention; they are not exact importance ratios or KL values for the bounded executed-action distribution.
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 a useful starting policy that pure RL can drift away from. It is called "reverse" because it is the KL from the new policy to the reference; that direction has different support and mode effects from the forward KL, so describe the direction rather than promising a universal mode-seeking behavior. The off-policy problem addresses demo retention with alternating BC updates, which we meet in Chapter 9.
entropy_coef is too small or other losses dominate, the actor's action std can shrink early — the policy becomes nearly deterministic, stops exploring, and may freeze at an accidental behavior. Symptom: actor_std trends toward 0 while success plateaus below the task's achievable rate. The useful response is to inspect the entropy coefficient, KL, and rollout diagnostics rather than rely on a fixed step threshold.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 removes this sample’s incentive to increase the ratio further. For a bad action (A < 0), the upper plateau is reached as the ratio falls below 1−ε. Drag ε to inspect the surrogate region; it is not a hard safe region for every update.
Same robot, same hammer, same nail, same sparse reward. Completely different algorithm. The assignment uses on-policy and off-policy setups to study a sample-reuse tradeoff; any step counts or success rates depend on seeds, implementation, and evaluation. The curves later in this page are illustrative schematics, not measured results.
PPO is on-policy: its gradient is an expectation over (s, a) drawn from the rollout-time policy, so stale data eventually gives the wrong gradient even though one batch can be reused for a few PPO epochs. Off-policy learns a Q-function whose Bellman target is defined by the transition dynamics plus the policy used for the next action:
A transition can remain useful under a different behavior policy when the critic and actor target are defined consistently, subject to coverage and distribution-shift limits. The finite replay buffer stores recent transitions and re-samples them several times; it does not keep every transition forever. That reuse can improve sample efficiency, while stale or narrow data can bias learning.
The clean story above hides a landmine. Three properties together create a well-known risk of instability in neural Q-learning:
| 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. |
The combination can amplify errors; it does not guarantee divergence on every task or seed. 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. Forgetting it can produce stale or rapidly moving targets; inspect target and online predictions together rather than assuming one fixed symptom.
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 tends to pick an overestimated one. Watch:
You train Q toward an upward-biased target, so overestimation can compound across backups. Fix: clipped double-Q — take the min over two critics, which reduces upward bias on average at the cost of possible underestimation. 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 a reparameterized differentiable sample: the starter uses a clamped/TruncatedNormal-style action distribution with fixed Gaussian noise for the sample path. The executed action is therefore noisy during training, while the actor remains differentiable. 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, reuses it for a few epochs, then refreshes the batch. Off-policy (bottom) places recent transitions in a finite replay buffer and re-samples them many times. Press play; watch the reuse. With sparse rewards, a rare +1 transition (warm) can be sampled repeatedly while it remains in the buffer.
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 updates about 36.7% of a fixed initial gap remains; the response half-life is about 138 updates. It deliberately lags the online critic. Called once per critic update via utils.soft_update_params(net, target_net, tau).
Maintain N independently initialized critic networks. In the starter, the ensemble evaluates the same sampled batch and its members can disagree on out-of-distribution inputs; that disagreement is a diagnostic. Default N = 2; the ablation uses N = 10. With N = 10 plus a high update-to-data ratio you get a REDQ-like configuration; its stability and sample efficiency are empirical properties to measure, not guarantees.
When computing the TD target, do not use all N critics. Pick 2 at random, take the elementwise min:
The min often counters maximization bias when critics are noisy, though it can under-estimate. The randomization — vs always using critics 1 and 2 — is a REDQ-style ingredient: the loss still trains every critic, while random pairs vary the target signal and make ensemble coverage measurable.
[B, 1]. If a loader gives [B], normalize each reward and discount to [B, 1] before arithmetic; blindly unsqueezing an existing column can create [B, 1, 1]. A raw [B] + [B,1] can broadcast 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 can destabilize the critic; (2) using random.choices (with replacement) instead of random.sample can pick the same critic twice; (3) only updating the 2 sampled critics leaves the other N−2 drifting, so the loss must be summed over all N; (4) using self.critic instead of self.critic_target for the target.The equation above is the one-step case. The starter uses three-step replay: reward already contains the discounted reward sum and discount already includes the accumulated gamma factors and terminal mask. Use them directly; multiplying by gamma again is a bug.
python def batch_col(x): x = torch.as_tensor(x) if x.ndim == 1: x = x[:, None] assert x.ndim == 2 and x.shape[1] == 1 return x 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 = batch_col(reward) + batch_col(discount) * torch.min(Qi, Qj) assert y.shape == Qi.shape # 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 in this sample. Reshuffle to resample the pair and the noise: min-of-two is often conservative and max is often optimistic, but either statistic can land above or below the truth on a finite draw.
One homework, three algorithm families, and related value and policy ideas underneath them. The Bellman equation you derived in Chapter 3 is central to the tabular and off-policy critics; PPO adds a policy-gradient objective with GAE and a value baseline. Here is the arc in one picture, then the cheat sheet.
Illustrative schematic curves for the sparse-reward hammer task. PPO (on-policy) is drawn climbing slowly, while the off-policy, N=2 UTD=1 and off-policy, N=10 UTD=5 (REDQ-like) curves show how replay and update-to-data choices might change a learning trace. The shapes and step counts are fixtures for comparison, not measured benchmark results. Press play to inspect the schematic 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 | rollout batch, reused for a few epochs | finite off-policy replay buffer |
| 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 |
| Assignment budget | 5000 training episodes | at least 25% success by 1M steps | at least 90% before 100k; N=10, UTD=5 before 40k |
| 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 can walk into the wall, Q stays unchanged | A deterministic all-zero tie with no exploration, especially when the step reward is zero. Negative step costs can break this tie, so inspect the actual reward contract; use ε-greedy with a high start. |
| Q-values near the goal are biased upward | Missing if done in the target — bootstrapping off a terminal cell. The size and visibility of the symptom depend on terminal-state storage and updates. |
| Robot never finishes, loiters instead | Reward hacking — a positive step reward pays more than the goal. Check the success detector and reward contract; completion-only reward removes this particular loitering incentive. |
| actor_std crashes to 0, success plateaus low | Entropy collapse in PPO. Raise entropy_coef. |
| critic_loss becomes unstable or drifts | Possible causes include gradients through the target, shape-mismatch broadcasting, an overly fast target, or an unsuitable learning rate; inspect tensor shapes and target/online traces. |
| critic_loss stuck at exactly 0.0 | Gradients not flowing — wrong optimizer, missing backward, or loss disconnected. |
| Q-values become unstable, actor exploits nonsense | Possible maximization bias or deadly-triad interaction — inspect target construction, critic diversity, replay coverage, and UTD; min-of-2 can reduce upward bias but is not a guarantee. |
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 a semi-gradient stochastic approximation to a Bellman target. 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. Under the tabular assumptions, repeated visits and suitable step sizes recover the optimal fixed point. Two tricks: discount by γ to keep sums finite, and ε-greedy so every state-action gets sampled. Neural methods add different policy-gradient, replay, target, and critic-stabilization machinery rather than becoming one identical algorithm."
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.