REINFORCE and variance reduction
The policy gradient theorem of Chapter 5.4 gave me an exact expression for , but it is an expectation, and I cannot evaluate it in closed form for anything real. I have to estimate it by sampling. The moment you estimate a gradient by sampling, you inherit a new adversary that has nothing to do with bias and everything to do with noise: variance. REINFORCE is the most direct Monte Carlo estimator of the policy gradient, and left raw it is so high-variance as to be nearly useless. This chapter derives REINFORCE, then derives the three standard variance-reduction moves, reward-to-go, baselines, and the advantage, proving that each one leaves the gradient unbiased while shrinking its variance. The baseline unbiasedness proof is the theoretical seed of GRPO, so I give it in full. The lab is the payoff of the whole part: a REINFORCE agent on a toy task with a matplotlib figure showing the gradient-variance curve collapse the instant a baseline is switched on.
Theory
REINFORCE: Monte Carlo policy gradient
Start from the trajectory form of the theorem, Chapter 5.4 Equation (5). Sample trajectories from the current policy and replace the expectation with its sample mean:
This is REINFORCE (Williams, 1992). The update rule is gradient ascent, . In words: for each sampled trajectory, push up the log-probability of every action it took, scaled by the trajectory's total return. Good trajectories make all their actions more likely; bad trajectories make all their actions less likely. It is beautifully simple and, as stated, badly behaved, for a reason I want to make precise before fixing it.
The trouble is that the single scalar multiplies the log-prob gradient of every action in the trajectory, including actions that had nothing to do with the outcome. If one lucky reward inflates , REINFORCE dutifully reinforces every token that happened to be in that sequence, noise and signal alike. The estimator is unbiased, but its variance grows with trajectory length and with the scale of the returns, and for a language model with hundreds of tokens per episode that variance is enormous.
Reward-to-go: the first free variance cut
The first fix follows from causality: an action cannot influence rewards that were already collected before it was taken. So weighting by the full return , which includes rewards from steps , is adding pure noise. Replace the full return with the reward-to-go, the return accumulated from step onward.
Let be the reward-to-go from step . I claim the past rewards contribute zero to the gradient, so dropping them changes nothing in expectation. Consider a single past reward with and the action term at step . Its contribution to the gradient is
Condition on everything up to and including step (which fixes and the state ). Inside that conditioning, is a constant, and is still drawn fresh from . The expectation of the score function under its own distribution is zero, a fact I prove in the next box:
By the tower property, the whole term vanishes. Since this holds for every past reward with , replacing by the reward-to-go at each step leaves the gradient unbiased:
We removed variance (the noisy past rewards) at zero cost in bias. That is the ideal kind of trade, and it is worth internalizing that for a terminal-reward LLM task with , the reward-to-go equals the single final reward for every , so reward-to-go and full-return coincide and this particular cut buys nothing. It buys a great deal in dense-reward or long-horizon tasks, which is why I keep it in the general form.
The score function integrates to zero
The identity I just used is the linchpin of every variance-reduction result in RL, so I prove it standalone.
For any state and any ,
Apply the log-derivative trick (Chapter 5.4, Eq. 4) to each term:
The gradient and the sum swap because the action set is finite and fixed. The last step is the punchline: probabilities sum to one, a constant, whose gradient is zero. This single fact, that the policy is normalized, is why you can subtract things from the return without biasing the gradient. Hold onto it for the baseline proof.
Baselines: subtracting a number for free
Reward-to-go still leaves the returns on an arbitrary scale. If every completion for a prompt scores between and , REINFORCE reinforces all of them strongly (every is large and positive), even though relative to each other some are worse. What matters for learning is not the absolute return but how a return compares to what you expected. So subtract a baseline , a number that can depend on the state but not on the action taken:
The remarkable fact is that any action-independent baseline leaves the gradient exactly unbiased, so you may choose purely to minimize variance.
I need to show the baseline contributes zero to the gradient in expectation, i.e.
Look at a single step and condition on the state (which fixes , since the baseline depends only on the state). Pull the constant out of the inner expectation over the action:
By Equation (3), the inner expectation is zero. So the whole term is , for every . Summing over , the baseline's total contribution is zero, hence (4) is unbiased for any function that does not depend on the action.
This is the theorem GRPO stands on. GRPO samples a group of completions for one prompt, computes each completion's reward , and uses the group mean as the baseline. Because depends only on the prompt state, not on which specific completion you are updating, the proof above says subtracting it is unbiased. The group-relative advantage (usually also divided by the group's standard deviation) is precisely a baseline-subtracted return, and no learned value network is anywhere in sight. Everything special about GRPO is contained in this box plus the choice "let the baseline be the group mean."
Why a baseline reduces variance, and the optimal choice
Unbiasedness says the baseline does not hurt; now I show a good baseline helps. Write the per-step estimator as . Its variance, for a scalar parameter to keep the algebra clean, is , and the second term is fixed (it is the true gradient squared, baseline-independent by the proof above). So minimizing variance means minimizing .
Let . Minimize over :
This is a quadratic in . Differentiate and set to zero:
The optimal baseline is a -weighted average of the return, which is close to the expected return but weighted by the squared score. In practice people use the plainer choice , the value function, because it is intuitive ("compare the return to what you expected from this state") and nearly optimal. That single substitution, , is what turns REINFORCE into an actor-critic method (Chapter 5.6): the "critic" is just a learned estimate of the variance-reducing baseline.
Where the variance actually comes from
It helps to name the two sources of noise in the REINFORCE estimator separately, because the baseline attacks one of them and reward-to-go attacks the other. The law of total variance gives the decomposition cleanly.
Consider the single-step estimator with , and decompose its variance by conditioning on the action . The law of total variance states, for any random variables,
The first term is the variance within a fixed action: for a fixed , is fixed, so this is , the spread of the return caused by the stochastic environment and future actions. Reward-to-go attacks this term by stripping out the irrelevant past rewards that inflate . The second term is the variance across actions: how much the mean estimate swings as the sampled action changes. This is the term the baseline attacks. By centering the return on , the factor becomes the advantage , which is small in magnitude and averages to zero, so its spread across actions shrinks. The two variance-reduction tricks are not redundant, they target the two orthogonal terms of (6), which is why real systems use both.
Advantage as the general form
Put reward-to-go and a value baseline together. With and recalling that , the weight on the log-prob gradient becomes, in expectation,
the advantage from Chapter 5.2. The advantage is the general form of the policy gradient weight, and every method in the rest of Part V is a different way of estimating :
- REINFORCE with baseline estimates by with a running average or the value function.
- Actor-critic and GAE (5.6) estimate with a learned critic and bootstrapping.
- PPO (5.7) uses the same but clips the update to stay in a trust region.
- GRPO (5.8) estimates by across a group, no critic at all.
Seen this way, the whole progression from REINFORCE to GRPO is one question asked repeatedly: what is the cheapest low-variance unbiased estimate of the advantage that fits in 16 GB?
[S&B] Section 13.3 is REINFORCE, my (1) and (2); Section 13.4 adds the baseline and states the unbiasedness result, my (4) and the proof box; Section 13.5 is REINFORCE-with-baseline as a stepping stone to actor-critic, my (7). Their Figure 13.2 shows the variance reduction empirically, which is exactly what the lab below reproduces from scratch. For the LLM translation, [BRM] Chapter 6 sidebars connect the advantage form (8) to how reasoning-model training frames the reward-minus-baseline signal per completion; read those sidebars after the lab, when the group-mean-baseline idea from the GRPO seed box is fresh, and the connection to will click.
Tooling
The tooling is matplotlib for the artifact plus NumPy for the agent, both already in the uv project from Chapter 5.1. I deliberately avoid a deep-learning framework here so that every line of the gradient estimator is visible and the variance measurement is unambiguous. The one methodological tool worth naming is measuring gradient variance directly: draw many independent gradient estimates at a fixed and take the variance across them, rather than trusting the loss curve to tell you whether your estimator is noisy. That measurement is the figure.
Lab
The task is a contextual bandit that stands in for one-step LLM generation: a "prompt" (context) is one of a few categories, the "action" is one of several tokens, and a verifier rewards the correct token for each context. I train a softmax policy with REINFORCE, once with no baseline and once with a running-mean baseline, and at each step I estimate the variance of the gradient across a fresh minibatch. The artifact is a matplotlib figure with two panels: reward-over-time (both variants learn) and gradient-variance-over-time (the baseline variant is dramatically lower). That variance gap, at equal final reward, is the entire lesson of the chapter made visual.
"""REINFORCE on a contextual bandit (a stand-in for one-step LLM
generation), with and without a baseline. Measure the variance of the
gradient estimator at each step and plot it. Artifact: reinforce_variance.png
Context = "prompt" category. Action = "token". Reward = 1 if the action
is the correct token for that context, else 0 (a toy verifier).
Run: uv run python labs/reinforce_variance.py
"""
from __future__ import annotations
import numpy as np
import matplotlib
matplotlib.use("Agg") # headless: write a file, no display
import matplotlib.pyplot as plt
N_CONTEXTS = 4
N_ACTIONS = 5
CORRECT = np.array([0, 2, 4, 1]) # the "right token" per context
STEPS = 400
BATCH = 32 # completions sampled per step
LR = 0.5
SEED = 0
def softmax_rows(logits):
z = logits - logits.max(axis=1, keepdims=True)
e = np.exp(z)
return e / e.sum(axis=1, keepdims=True)
def reward(context, action):
return 1.0 if action == CORRECT[context] else 0.0
def grad_log_pi(probs_row, action):
"""d/dlogits log pi(action) for a softmax = onehot(action) - probs."""
g = -probs_row.copy()
g[action] += 1.0
return g
def train(use_baseline: bool, rng):
# Policy parameters: one logit vector per context.
theta = np.zeros((N_CONTEXTS, N_ACTIONS))
baseline = np.zeros(N_CONTEXTS) # running mean reward per context
rewards_hist, gradvar_hist = [], []
for step in range(STEPS):
probs = softmax_rows(theta)
# Sample a batch of (context, action, reward).
contexts = rng.integers(N_CONTEXTS, size=BATCH)
batch_reward = 0.0
# Accumulate per-sample gradients so we can measure their spread.
per_sample_grads = np.zeros((BATCH, N_CONTEXTS * N_ACTIONS))
grad_sum = np.zeros_like(theta)
for i, c in enumerate(contexts):
a = rng.choice(N_ACTIONS, p=probs[c])
r = reward(c, a)
batch_reward += r
advantage = r - (baseline[c] if use_baseline else 0.0)
g = np.zeros_like(theta)
g[c] = advantage * grad_log_pi(probs[c], a)
grad_sum += g
per_sample_grads[i] = g.ravel()
if use_baseline:
# Update running-mean baseline for this context (eq: b ~ E[G]).
baseline[c] += 0.05 * (r - baseline[c])
rewards_hist.append(batch_reward / BATCH)
# Variance of the gradient estimator: trace of the covariance of
# the per-sample gradients (total variance across all components).
gradvar_hist.append(float(per_sample_grads.var(axis=0).sum()))
# Gradient ASCENT on J.
theta += LR * grad_sum / BATCH
return np.array(rewards_hist), np.array(gradvar_hist)
def smooth(x, k=15):
kernel = np.ones(k) / k
return np.convolve(x, kernel, mode="valid")
if __name__ == "__main__":
rng = np.random.default_rng(SEED)
r_no, v_no = train(use_baseline=False, rng=rng)
rng = np.random.default_rng(SEED) # same seed: fair comparison
r_yes, v_yes = train(use_baseline=True, rng=rng)
print(f"final reward (no baseline): {r_no[-50:].mean():.3f}")
print(f"final reward (baseline) : {r_yes[-50:].mean():.3f}")
print(f"mean grad-var (no baseline): {v_no.mean():.4f}")
print(f"mean grad-var (baseline) : {v_yes.mean():.4f}")
print(f"variance reduction factor : {v_no.mean() / max(v_yes.mean(), 1e-9):.2f}x")
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4.2))
ax1.plot(r_no, alpha=0.35, color="tab:red")
ax1.plot(r_yes, alpha=0.35, color="tab:blue")
ax1.plot(range(len(smooth(r_no))), smooth(r_no), color="tab:red",
label="no baseline")
ax1.plot(range(len(smooth(r_yes))), smooth(r_yes), color="tab:blue",
label="with baseline")
ax1.set_title("Reward over training (both learn)")
ax1.set_xlabel("step"); ax1.set_ylabel("mean batch reward"); ax1.legend()
ax2.plot(v_no, color="tab:red", alpha=0.8, label="no baseline")
ax2.plot(v_yes, color="tab:blue", alpha=0.8, label="with baseline")
ax2.set_title("Gradient-estimator variance (lower is better)")
ax2.set_xlabel("step"); ax2.set_ylabel("trace of grad covariance")
ax2.set_yscale("log"); ax2.legend()
fig.tight_layout()
fig.savefig("reinforce_variance.png", dpi=130)
print("\nwrote reinforce_variance.png")
What you should see. The printed summary reports that both variants reach essentially the same final reward (both solve the toy task, climbing toward a mean batch reward near ), while the mean gradient variance with a baseline is several times lower than without, a variance-reduction factor typically in the range of roughly 2x to 5x on this problem with these seeds. The saved figure reinforce_variance.png makes it unmistakable: the left panel shows two reward curves rising to the same ceiling, so the baseline costs nothing in final performance, and the right panel, on a log scale, shows the blue (baseline) variance curve sitting well below the red (no-baseline) one for the whole run. That separation at equal reward is the empirical face of the unbiasedness proof: the baseline moved variance, not bias. This is the exact mechanism GRPO scales up, swapping my per-context running-mean baseline for a per-prompt group-mean baseline, and it is why a critic-free method can train a reasoning model on a single RTX 5080 16GB (when you run the real GRPO loop in Part VII, log this same gradient-variance and entropy pair: measured on the baseline machine — record value, date, driver).
The unbiasedness proof hinges on the baseline being independent of the action taken. It is tempting, and wrong, to peek at the sampled action when computing the baseline, for instance by using that completion's own reward as its own baseline, which makes the advantage identically zero and kills learning. GRPO's group mean is safe precisely because it averages over the whole group, so from any single completion's perspective the baseline is an action-independent constant (to leading order). If you ever find your advantages mysteriously collapsing to zero or your gradient going biased, check first whether your baseline has secretly become a function of the action you are updating.
Post angle: "Why grading on a curve is the trick that makes AI training work." REINFORCE, the foundational recipe for learning from trial and error, has a crippling flaw: it is so noisy that it barely learns, because it can't tell a genuinely good attempt from a lucky one. The fix is almost philosophical: don't reward an attempt for its raw score, reward it for beating expectations, for how much better it did than the average attempt at the same problem. That's grading on a curve, and there's a clean proof that it speeds up learning without ever distorting what the model learns, only how quickly. The essay lands on the fact that this exact "compare each answer to the group average" move is the beating heart of GRPO, the algorithm behind today's open reasoning models, and that you can watch the noise drop in a chart you can generate on a laptop in under a second.