← All notes

Actor-Critic Methods

Actor-critic methods build on policy gradients: alongside the policy (the “actor”), they learn a value function (the “critic”) to judge how good the actor’s actions are, giving a lower-variance learning signal than the raw Monte Carlo returns used in vanilla policy gradient.

In one line: policy gradients observe what was good or bad, then do more of the good stuff; actor-critic learns to estimate what’s good or bad, then does more of the good stuff. Same recipe, just swapping a noisy observation for a neural network’s own estimate of the value function.

Motivation: policy gradient makes inefficient use of data

Even with reward-to-go, which already stops an action from being blamed for rewards that came before it, policy gradient still only has one sampled rollout to learn from. Whatever happens after an action in that one particular trajectory still gets baked into its credit, whether or not it’s representative of what usually happens when the policy takes that action. Good and bad actions inside the same trajectory end up reinforced or punished together.1

This is sharpest under sparse rewards, where the only nonzero signal arrives once, at the very end of the episode. Two trajectories that make very different amounts of real progress toward the goal, but happen to land on the same final reward, get treated as equally good, or equally useless. The algorithm has no way to see the difference.2

All policy gradient ever knows is that a trajectory succeeded or failed as a whole; it has no notion of how good any specific state or action was on its own. That is exactly the gap a learned critic is built to fill: an estimate of a state’s or action’s expected future reward, averaged over the policy’s actual behavior, rather than one noisy, one-off realization of it.

Preliminaries

Side by side: left, running policy π from state s produces several trajectories, their expected sum of rewards labeled V^π(s). Right, taking action a from state s (highlighted), then running policy π, produces several trajectories, their expected sum of rewards labeled Q^π(s,a).
Source: Stanford CS224R.

VV and QQ are related: since Vπ(s)V^\pi(s) is the expected future reward under π\pi‘s own behavior at ss, it must equal Qπ(s,a)Q^\pi(s, a) averaged over the actions π\pi would actually choose at ss,

Vπ(s)=Eaπ(s)[Qπ(s,a)]V^\pi(s) = \mathbb{E}_{a \sim \pi(\cdot \mid s)} \left[ Q^\pi(s, a) \right]

Qπ(s,a)Q^\pi(s, a) answers “how good is action aa,” while Vπ(s)V^\pi(s) answers “how good is state ss, on average, under π\pi‘s own choices.” Averaging the first over π\pi‘s action distribution gives the second back.

This gives a natural way to measure how good a specific action is relative to the policy’s own average behavior: the advantage function,

Aπ(s,a)=Qπ(s,a)Vπ(s)A^\pi(s, a) = Q^\pi(s, a) - V^\pi(s)

how much better (or worse) it is to take action aa than to follow π\pi‘s own behavior at state ss.

From reward-to-go to Q: a lower-variance estimator

Recall the reward-to-go estimator,

θJ(θ)1Ni=1Nt=1Tθlogπθ(ai,tsi,t)Gi,t,Gi,t=t=tTr(si,t,ai,t)\nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i=1}^{N} \sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(a_{i,t} \mid s_{i,t}) \, G_{i,t}, \qquad G_{i,t} = \sum_{t'=t}^{T} r(s_{i,t'}, a_{i,t'})

This update increases the probability of actions that led to large future rewards. But Gi,tG_{i,t} is only ever computed from one sampled rollout: it’s an estimate of the future reward, not the future reward itself. The same first action can lead to wildly different futures.

A single trajectory arrives at a state (green dot), from which many different future trajectories branch out and upward, each summing to a different total reward.
From a single state, many different futures are possible, each earning a different reward; a sampled rollout only ever realizes one of them. Source: Stanford CS224R.

This is exactly the high variance from the motivation above, made concrete: Gi,tG_{i,t} swings with whichever future happened to get sampled, even though the action itself, taking ata_t at sts_t, is the same every time.3

A better estimate averages over every possible future after taking ata_t in sts_t, instead of relying on the one future that happened to get sampled:

t=tTEπ[rtst,at]\sum_{t'=t}^{T} \mathbb{E}_\pi\left[ r_{t'} \mid s_t, a_t \right]

This is exactly the QQ-function defined above: the expected future reward from taking ata_t at sts_t and then following π\pi. Substituting Qπθ(st,at)Q^{\pi_\theta}(s_t, a_t) for the sampled reward-to-go Gi,tG_{i,t} gives a new, lower-variance estimator:

θJ(θ)1Ni=1Nt=1Tθlogπθ(ai,tsi,t)Qπθ(si,t,ai,t)\nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i=1}^{N} \sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(a_{i,t} \mid s_{i,t}) \, Q^{\pi_\theta}(s_{i,t}, a_{i,t})

Instead of saying “this one rollout earned reward 8,” this says “this action is generally worth about 8”: the update is now driven by an averaged, expected value, rather than one noisy, one-off realization of it.

What the Q-function learns

Instead of trusting a single rollout, the critic accumulates experience across many episodes: every time the policy takes action aa at state ss, it records the return that followed, and averages over all of them.4 This average is exactly the Monte Carlo estimate of Qπ(s,a)Q^\pi(s, a) defined above, just computed from many rollouts instead of one.

Suppose that after enough episodes, this average settles at Q(s,a)=6.8Q(s, a) = 6.8. The policy update no longer uses the reward of 22 that one sampled rollout happened to return; it uses 6.86.8, the critic’s running average over everything it has seen.

Q still depends on the state, not just the action

Even Qπ(s,a)Q^\pi(s, a) has a subtle flaw: it mixes together how good the state ss already is with how good the specific action aa is. In a position that’s already winning, every legal move can look great under QQ, not because any one move is exceptional, but because the state itself already guarantees a good outcome.5

The fix is to stop asking “is this action good?” in absolute terms, and instead ask “is this action better than what I usually do from this state?” That’s exactly what the advantage function Aπ(s,a)=Qπ(s,a)Vπ(s)A^\pi(s, a) = Q^\pi(s, a) - V^\pi(s) measures: subtracting off the state’s own average value, Vπ(s)=Eaπ[Qπ(s,a)]V^\pi(s) = \mathbb{E}_{a \sim \pi}[Q^\pi(s, a)], removes whatever part of Qπ(s,a)Q^\pi(s, a) was just “the state is good,” leaving only the part attributable to the action itself.6

Plugging AπθA^{\pi_\theta} into the gradient in place of QπθQ^{\pi_\theta} gives the actor-critic estimator:

θJ(θ)1Ni=1Nt=1Tθlogπθ(ai,tsi,t)Aπθ(si,t,ai,t)\nabla_\theta J(\theta) \approx \frac{1}{N} \sum_{i=1}^{N} \sum_{t=1}^{T} \nabla_\theta \log \pi_\theta(a_{i,t} \mid s_{i,t}) \, A^{\pi_\theta}(s_{i,t}, a_{i,t})

Better estimates of AπA^\pi lead to less noisy gradients. The actor no longer chases large absolute rewards caused by simply being in a good state; it updates based on whether an action was better or worse than expected from that specific state, a more stable and efficient learning signal.

The actor-critic loop

Fitting VπV^\pi, QπQ^\pi, or AπA^\pi turns actor-critic into a small extension of the online RL loop: collect a batch of data with the current policy, fit a model to estimate the expected return, then use that fitted estimate to improve the policy.

Actor-critic loop diagram: run the policy to collect a batch of data, fit a model to estimate expected return by estimating V^π, Q^π, or A^π, improve the policy via θ ← θ + ∇_θ J(θ), then repeat.
Source: Stanford CS224R.

You estimate value metrics like the State-Value VπV^\pi, Action-Value QπQ^\pi, or the Advantage AπA^\pi (which is seen in the top equation). This gives you the ground truth weight needed for your gradient update.

In practice, this fitted model is a neural network: it takes a state ss as input and outputs an estimate V^π(s)\hat{V}^\pi(s), with its own parameters ϕ\phi, separate from the policy’s parameters θ\theta.

A feedforward neural network taking state s as input and outputting an estimate V-hat^π(s), with its own parameters φ.
Source: Stanford CS224R.

Training the value network

Fitting V^ϕπ(s)\hat{V}^\pi_\phi(s) turns into an ordinary supervised learning problem: for every state si,ts_{i,t} visited in a batch, use the observed return yi,t=Gi,ty_{i,t} = G_{i,t}, the actual reward-to-go collected from that state onward, as the regression label.

The loss is standard mean squared error between the network’s prediction and this target,

L(ϕ)=12i(V^ϕπ(si)yi)2L(\phi) = \frac{1}{2} \sum_i \left( \hat{V}^\pi_\phi(s_i) - y_i \right)^2

training ϕ\phi by gradient descent on LL, exactly like any other regression problem.7

Every episode that passes through a given state contributes another noisy sample of the return from there. Training on many of these samples pushes the network’s prediction toward their average, which is exactly Vπ(s)V^\pi(s): the expected return under π\pi, not any single episode’s realization of it.8

Bootstrapping: expressing Q with just V

Fitting a network for VπV^\pi alone, without a separate QQ-network, is enough. Start from Qπ(st,at)Q^\pi(s_t, a_t) written as a sum of expected future rewards, and pull out the very first one, the reward that arrives immediately after taking ata_t:

Qπ(st,at)=t=tTEπθ[r(st,at)st,at]=r(st,at)+t=t+1TEπθ[r(st,at)st,at]=r(st,at)+Est+1p(st,at)[Vπ(st+1)]r(st,at)+Vπ(st+1)(use the sampled st+1)\begin{aligned} Q^\pi(s_t, a_t) &= \sum_{t'=t}^{T} \mathbb{E}_{\pi_\theta} \left[ r(s_{t'}, a_{t'}) \mid s_t, a_t \right] \\ &= r(s_t, a_t) + \sum_{t'=t+1}^{T} \mathbb{E}_{\pi_\theta} \left[ r(s_{t'}, a_{t'}) \mid s_t, a_t \right] \\ &= r(s_t, a_t) + \mathbb{E}_{s_{t+1} \sim p(\cdot \mid s_t, a_t)} \left[ V^\pi(s_{t+1}) \right] \\ &\approx r(s_t, a_t) + V^\pi(s_{t+1}) \quad \text{(use the sampled } s_{t+1}\text{)} \end{aligned}

Everything after the first reward is, by definition, exactly Vπ(st+1)V^\pi(s_{t+1}): the expected future reward from following π\pi starting at the next state. There’s an expectation, E[Vπ(st+1)]\mathbb{E}[V^\pi(s_{t+1})], because after your action, many next states are possible. But in reinforcement learning we only ever experience one next state per rollout: current state, action, observed next state, no branching. So the last line approximates that expectation with the one sampled next state: it plugs in VπV^\pi evaluated at the st+1s_{t+1} that was actually observed, instead of averaging over all of them.

This is called bootstrapping: using the current value estimate itself to stand in for future rewards, instead of needing a whole separate rollout (or a model of the environment’s dynamics) to know what happens next.

Substituting this into the advantage function, Aπ(st,at)=Qπ(st,at)Vπ(st)A^\pi(s_t, a_t) = Q^\pi(s_t, a_t) - V^\pi(s_t):

Aπ(st,at)r(st,at)+Vπ(st+1)Vπ(st)Let’s just fit Vπ!A^\pi(s_t, a_t) \approx r(s_t, a_t) + V^\pi(s_{t+1}) - V^\pi(s_t) \quad \text{Let's just fit } V^\pi!

No QQ-network required: the advantage, which needed QπQ^\pi a moment ago, can now be computed from VπV^\pi alone, plus the one reward that was actually observed.

Temporal difference learning: bootstrapped targets

The Monte Carlo target used above, yi,t=Gi,t=t=tTri,ty_{i,t} = G_{i,t} = \sum_{t'=t}^{T} r_{i,t'}, has a practical problem: you can’t compute it until the episode is over. If an episode runs for 10,00010{,}000 steps, the network can’t take a single gradient step until step 10,00010{,}000.

The fix is the same bootstrapped estimate derived above, now used as the training target itself, instead of just for the advantage:

y=r+V^π(s)y = r + \hat{V}^\pi(s')

Instead of waiting for the rest of the episode, the label is available immediately: one observed reward, plus the network’s own current guess at everything after it.9

At first, that guess can be quite wrong. But every gradient step improves V^π\hat{V}^\pi a little, which means every subsequent target y=r+V^π(s)y = r + \hat{V}^\pi(s') gets a little more accurate too: the network is teaching itself, bootstrapping better and better labels out of its own improving predictions.

The loss function doesn’t change at all, only the target does:

L(ϕ)=12(V^π(s)y)2,y=r+V^π(s)L(\phi) = \frac{1}{2} \left( \hat{V}^\pi(s) - y \right)^2, \qquad y = r + \hat{V}^\pi(s')

Learning is now much faster, since it no longer waits for an episode to end.

This is called temporal difference (TD) learning because of what’s being compared: the current prediction V^π(s)\hat{V}^\pi(s), against a target built from the next time step, r+V^π(s)r + \hat{V}^\pi(s'). Their difference is the TD error,

δ=r+V^π(s)V^π(s)\delta = r + \hat{V}^\pi(s') - \hat{V}^\pi(s)

If δ>0\delta > 0, the state turned out better than expected, so V^π(s)\hat{V}^\pi(s) should increase. If δ<0\delta < 0, it turned out worse than expected, so V^π(s)\hat{V}^\pi(s) should decrease.

Bias vs. variance: Monte Carlo vs. TD

The Monte Carlo target from training the value network and the bootstrapped TD target above sit on opposite ends of a classic tradeoff.

The Monte Carlo target, y=Gi,ty = G_{i,t}, uses the actual future return realized in that one sampled episode. It’s unbiased: averaged over enough episodes, it converges to the true Vπ(s)V^\pi(s). But it’s noisy, since a single episode can be lucky or unlucky, so the label attached to any one state can swing widely from episode to episode.

The TD target, y=r+V^π(s)y = r + \hat{V}^\pi(s'), uses the current, imperfect estimate V^π(s)\hat{V}^\pi(s') in place of the true value of the next state. Since V^π\hat{V}^\pi can be wrong, especially early in training, this introduces bias: the target itself may be systematically off. But it only depends on one sampled reward, not an entire trajectory’s worth of randomness, so it has much lower variance.

Monte Carlo trusts the data completely and pays for it in noise; TD trades some of that noise away by trusting its own, imperfect predictions instead.10

N-step returns: a middle ground

Monte Carlo and 1-step TD are two ends of a spectrum: sum every real reward all the way to TT, or bootstrap after just one. Nothing forces that choice to be so extreme.

Diagram comparing Monte Carlo (sum rewards over the whole trajectory), bootstrapped (one reward plus V), and an n-step return in between (sum rewards over n steps, then plus V) along the same set of trajectories.
Source: Stanford CS224R.

An n-step return sums the actual, observed rewards for the first nn steps, then bootstraps off V^π\hat{V}^\pi for everything after that:

yi,t=t=tt+n1r(si,t,ai,t)+V^π(si,t+n)y_{i,t} = \sum_{t'=t}^{t+n-1} r(s_{i,t'}, a_{i,t'}) + \hat{V}^\pi(s_{i,t+n})

n=1n=1 recovers the 1-step TD target from above; n=Tn=T recovers the Monte Carlo target, with no bootstrapping at all.

In practice, some nn strictly in between, 1<n<T1 < n < T, often works best: using nn real rewards before bootstrapping means less of the estimate leans on V^π\hat{V}^\pi being correct, so it has less variance than Monte Carlo; but it also depends on fewer of the single sampled trajectory’s rewards than the full return does, so it has lower bias than the 1-step bootstrap.

A full algorithm walkthrough

Putting every piece together needs one last bit of notation: the discount factor γ[0,1]\gamma \in [0, 1], which weights rewards further in the future slightly less than immediate ones (everywhere above has implicitly used γ=1\gamma = 1). With it, the actor-critic algorithm is:

  1. Sample a batch of trajectories {(si,1,ai,1,,si,T,ai,T)}\{(s_{i,1}, a_{i,1}, \dots, s_{i,T}, a_{i,T})\} by running the current policy πθ\pi_\theta.
  2. Fit V^ϕπθ\hat{V}_\phi^{\pi_\theta} to the summed rewards in the batch.
  3. Evaluate the advantage at every timestep, A^πθ(st,i,at,i)=r(st,i,at,i)+γV^ϕπθ(st+1,i)V^ϕπθ(st,i),t,i\hat{A}^{\pi_\theta}(s_{t,i}, a_{t,i}) = r(s_{t,i}, a_{t,i}) + \gamma \hat{V}_\phi^{\pi_\theta}(s_{t+1,i}) - \hat{V}_\phi^{\pi_\theta}(s_{t,i}), \qquad \forall t, i
  4. Evaluate the policy gradient, θJ(θ)t,iθlogπθ(at,ist,i)A^πθ(st,i,at,i)\nabla_\theta J(\theta) \approx \sum_{t,i} \nabla_\theta \log \pi_\theta(a_{t,i} \mid s_{t,i}) \, \hat{A}^{\pi_\theta}(s_{t,i}, a_{t,i})
  5. Update the policy, θθ+αθJ(θ)\theta \leftarrow \theta + \alpha \nabla_\theta J(\theta).

Then repeat from step 1 with the newly updated policy.

Step 2’s target and loss are exactly the n-step return from above, now with discounting folded in:

yi,t=t=tt+n1γttr(si,t,ai,t)+γnV^ϕπ(si,t+n)y_{i,t} = \sum_{t'=t}^{t+n-1} \gamma^{t'-t} r(s_{i,t'}, a_{i,t'}) + \gamma^n \hat{V}_\phi^\pi(s_{i,t+n}) L(ϕ)=12iV^ϕπ(si)yi2\mathcal{L}(\phi) = \frac{1}{2} \sum_{i} \left\Vert \hat{V}_\phi^\pi(s_i) - y_{i} \right\Vert^2

Steps 1 and 4 are the actor: the policy network πθ(as)\pi_\theta(a \mid s), updated by the policy gradient. Steps 2 and 3 are the critic: the value network V^ϕπ(s)\hat{V}^\pi_\phi(s), whose bootstrapped estimate turns raw rewards into the advantage the actor learns from. Two separate networks, trained together, each taking the same state ss as input, giving the method its name.

Two neural networks side by side, both taking state s as input: one outputting π_θ(a|s), the other outputting V-hat^π(s), labeled 'actor-critic algorithm'.
Source: Stanford CS224R.

Off-policy actor-critic

The algorithm above is on-policy: every gradient step needs a fresh batch of trajectories collected under the current policy πθ\pi_\theta, including the batch the critic V^ϕπθ\hat{V}_\phi^{\pi_\theta} is trained on. Turning this into an off-policy method means reusing data collected under an older policy for more than one gradient step, instead of collecting a batch, taking one gradient step, and throwing it away.11 Each transition gets reused many times instead of once, which is far more sample-efficient. Importance sampling is what makes that reuse valid, at the cost of a slightly stale advantage estimate.

Version 1: Multiple Gradient Steps

  1. Sample a batch of trajectories {(si,1,ai,1,,si,T,ai,T)}\{(s_{i,1}, a_{i,1}, \dots, s_{i,T}, a_{i,T})\} by running the current policy πθ\pi_\theta.
  2. Fit V^ϕπθ\hat{V}_\phi^{\pi_\theta} to the summed rewards in the batch.
  3. Evaluate the advantage at every timestep, A^πθ(st,i,at,i)=r(st,i,at,i)+γV^ϕπθ(st+1,i)V^ϕπθ(st,i),t,i\hat{A}^{\pi_\theta}(s_{t,i}, a_{t,i}) = r(s_{t,i}, a_{t,i}) + \gamma \hat{V}_\phi^{\pi_\theta}(s_{t+1,i}) - \hat{V}_\phi^{\pi_\theta}(s_{t,i}), \qquad \forall t, i
  4. Evaluate the importance-weighted policy gradient, θJ(θ)t,iπθ(ai,tsi,t)πθ(ai,tsi,t)θlogπθ(at,ist,i)A^πθ(st,i,at,i)\nabla_{\theta'} J(\theta') \approx \sum_{t,i} \frac{\pi_{\theta'}(a_{i,t} \mid s_{i,t})}{\pi_\theta(a_{i,t} \mid s_{i,t})} \, \nabla_{\theta'} \log \pi_{\theta'}(a_{t,i} \mid s_{t,i}) \, \hat{A}^{\pi_\theta}(s_{t,i}, a_{t,i})
  5. Update θθ+αθJ(θ)\theta' \leftarrow \theta' + \alpha \nabla_{\theta'} J(\theta'), then repeat from step 4 for another gradient step on the same batch.

This is what gives the version its name: instead of one gradient step per batch, steps 4 and 5 loop several times on θ\theta' before the outer loop ever touches step 1 again. But the advantage A^πθ\hat{A}^{\pi_\theta} being reused in every one of those inner steps is still exactly what step 3 computed, from the old policy πθ\pi_\theta; it never gets recomputed as θ\theta' moves. The more inner steps you take, the more outdated that advantage becomes, and the less it reflects what θ\theta' would actually experience now. θ\theta' can’t wander too far from θ\theta before this stops being reliable: once the two policies disagree enough, the importance weights also blow up, exactly the failure mode trust regions exist to prevent. In practice, “multiple” here means a handful of steps, not many.

After a handful of these inner steps, set θθ\theta \leftarrow \theta' and repeat from step 1 with a freshly collected batch.

Version 2: Replay Buffers

[Update this]

Proximal Policy Optimization

Recall the surrogate objective trick from policy gradients: instead of writing out a full gradient by hand, define a scalar objective whose gradient recovers it, so a single backward() call does the work. Applying that here, to the importance-weighted update from off-policy actor-critic, gives the surrogate objective

J~(θ)t,iπθ(ai,tsi,t)πθ(ai,tsi,t)A^πθ(st,i,at,i)\tilde{J}(\theta') \approx \sum_{t,i} \frac{\pi_{\theta'}(a_{i,t} \mid s_{i,t})}{\pi_\theta(a_{i,t} \mid s_{i,t})} \, \hat{A}^{\pi_\theta}(s_{t,i}, a_{t,i})

Differentiating J~\tilde{J} with respect to θ\theta', treating A^πθ\hat{A}^{\pi_\theta} as a constant, gives back exactly that importance-weighted policy gradient. As always, climbing J~\tilde{J} increases the probability of high-advantage actions and decreases the probability of low-advantage ones.

J~\tilde{J} has two pieces. The importance ratio,

r(θ)=πθ(as)πθ(as)r(\theta') = \frac{\pi_{\theta'}(a \mid s)}{\pi_\theta(a \mid s)}

compares the new policy to the old one. Unpacking each symbol:

So r=1r=1 means θ\theta' behaves exactly like θ\theta; r>1r>1 means θ\theta' is more likely to take this action than θ\theta was; r<1r<1 means less likely.12 The advantage, A^πθ(s,a)\hat{A}^{\pi_\theta}(s,a), was computed once from the old policy’s data: positive means the action was good, negative means it was bad. Multiplying them together says: increase the probability of good actions, decrease the probability of bad ones.

Why multiple gradient steps can go wrong

Version 1 already flagged the risk of repeating steps 4 and 5 too many times: nothing in J~\tilde{J} itself penalizes θ\theta' for drifting far from θ\theta, so an optimizer that’s free to keep climbing will happily push r(θ)r(\theta') far past 11, long after the advantage estimate it’s multiplying has stopped being trustworthy.13

TRPO fixes this by directly constraining how far θ\theta' is allowed to move, measured by KL divergence: DKL(πθπθ)δD_{\mathrm{KL}}(\pi_{\theta'} \,\|\, \pi_\theta) \leq \delta. It works well, but solving a constrained optimization problem at every gradient step is mathematically involved.

PPO takes a simpler route: instead of constraining the policy directly, just stop the importance ratio itself from getting too large.

Clipping the importance ratio

Pick a small ϵ\epsilon (commonly ϵ=0.2\epsilon = 0.2), and clip r(θ)r(\theta') to the interval [1ϵ,1+ϵ][1-\epsilon, 1+\epsilon] before it’s allowed to multiply the advantage: anything outside that interval gets pulled back to the nearest edge.14

clip(r(θ),1ϵ,1+ϵ)\mathrm{clip}\big(r(\theta'),\, 1-\epsilon,\, 1+\epsilon\big)

Clipping matters because, left alone, r(θ)A^πθr(\theta') \, \hat{A}^{\pi_\theta} grows without bound: for a fixed positive advantage, a bigger ratio always means a bigger objective, so the optimizer is rewarded for pushing θ\theta' arbitrarily far from θ\theta. Once the clipped ratio hits its boundary, it stops growing, so there’s no more reward for pushing it any further.15

The PPO objective

Taking the smaller of the clipped and unclipped versions, then summing back over the sampled timesteps the way J~\tilde{J} did, gives the full PPO objective:

J~CLIP(θ)t,imin(rt,i(θ)A^πθ(st,i,at,i),  clip(rt,i(θ),1ϵ,1+ϵ)A^πθ(st,i,at,i))\tilde{J}^{\mathrm{CLIP}}(\theta') \approx \sum_{t,i} \min\Big( r_{t,i}(\theta') \, \hat{A}^{\pi_\theta}(s_{t,i}, a_{t,i}), \ \ \mathrm{clip}\big(r_{t,i}(\theta'), 1-\epsilon, 1+\epsilon\big) \, \hat{A}^{\pi_\theta}(s_{t,i}, a_{t,i}) \Big)

where rt,i(θ)r_{t,i}(\theta') is the importance ratio at a single sampled state-action pair. The min\min and the clip both apply per sample, inside the sum: every term is capped on its own before anything is added up. The PPO paper writes this same objective as LCLIP(θ)L^{\mathrm{CLIP}}(\theta'), with an expectation over samples in place of the sum.

Why take the minimum, instead of just always optimizing clip(r(θ))A^πθ\mathrm{clip}(r(\theta')) \, \hat{A}^{\pi_\theta} on its own? Because clipping alone can accidentally reward exactly the policy changes it’s supposed to discourage: whenever r(θ)r(\theta') has moved outside the trust region in a bad direction, the clipped term can score better than the true, unclipped one. Taking the minimum always keeps whichever number is worse, so a bad update can never be made to look better than it actually is.16

Either way, the policy can’t change too much in a single update while still improving the objective. This clipping mechanism, simple to implement and no constrained optimization required, is the core idea that makes PPO both stable and simple compared with earlier trust-region methods like TRPO.

What “the old policy” means in practice

The denominator of the ratio is not the model from the previous gradient step; it’s a frozen snapshot. Call it P0P_0. That frozen copy does two jobs: it generates the rollouts, and it supplies πθ(as)\pi_\theta(a \mid s) for every ratio computed off them.

One pass of the loop looks like this:

  1. Freeze the old policy. Take a copy of the current model and call it P0P_0. Nothing trains this copy; it only generates rollouts and supplies the denominator πP0(as)\pi_{P_0}(a \mid s).
  2. Generate a batch with P0P_0. Prompt 1 → response A, prompt 2 → response B, prompt 3 → response C, and so on.
  3. Score the batch. Compute rewards for every response, then advantages for every sampled state-action pair.
  4. Take a gradient step. The trainable model moves P0P1P_0 \to P_1, so the ratio is πP1πP0\dfrac{\pi_{P_1}}{\pi_{P_0}}.
  5. Take another. P1P2P_1 \to P_2, ratio πP2πP0\dfrac{\pi_{P_2}}{\pi_{P_0}}. And another: P2P3P_2 \to P_3, ratio πP3πP0\dfrac{\pi_{P_3}}{\pi_{P_0}}. The numerator moves every step; the denominator does not move at all.
  6. Reset. After enough passes over the batch (typically 2 to 10 epochs), discard the data, set the old policy to the latest one (P3P_3 here, which now plays both roles), and repeat from step 2 with a completely fresh batch.

That final reset is what keeps the batch honest: the actions and rewards in it came from P0P_0, so training on them long after the policy has moved to, say, P20P_{20} means optimizing against behavior the current model no longer exhibits, and the gradient estimates get steadily less accurate.

The reason to squeeze several steps out of one batch at all is cost. For LLM RL, generating rollouts dominates: a single batch might mean generating 16,000 completions, scoring each one with a reward model or verifier, and computing advantages, which can take minutes. A gradient step on data already sitting in GPU memory is cheap by comparison. Taking exactly one update per batch would mean paying the expensive part over and over for one cheap step each time.

Despite the importance ratio, PPO is usually called an on-policy algorithm, because the data it trains on always comes from the current policy or one that’s only a few optimization steps old.

Footnotes

  1. Consider a robot walking, where a good start is undone by one bad step:

    StepActionOutcome
    1forward
    2forward
    3backward✗, robot falls

    The trajectory’s total reward is bad, because of the fall. Vanilla policy gradient only sees that one bad number for the whole trajectory, so it decreases the probability of every action in it, forward included: it pushes down the likelihood of stepping forward. But the first two forward steps were good; only the backward step at step 3 caused the fall. Policy gradient can’t tell the two apart, because both actions get scored by the same trajectory-wide outcome.

  2. Consider three attempts by a robot to fold a jacket, with reward given only at the very end of the episode:

    TrajectoryOutcomeReward
    τ2\tau_2Folds only the sleeves00
    τ3\tau_3Flattens the jacket but doesn’t fold it00
    τ4\tau_4Folds it completely11

    Policy gradient only sees the reward column: τ4\tau_4 gets reinforced, while τ2\tau_2 and τ3\tau_3 are both treated as equally useless failures. But τ2\tau_2 and τ3\tau_3 each made real progress toward the goal; the algorithm has no way to see that, because the only feedback it ever gets is the final number.

  3. Suppose taking action ata_t at state sts_t could lead to four possible future paths:

    PathReward
    A1010
    B22
    C00
    D77

    Vanilla policy gradient only ever samples one of these paths per rollout. If it happens to observe path B, it sees a reward of 22 and credits ata_t accordingly. But that doesn’t mean ata_t is only worth 22.

  4. Suppose the critic has recorded the return that followed taking action aa at state ss, across several episodes:

    EpisodeReturn
    122
    21010
    377
    400
    588
    699
    755
    \dots\dots

    Averaging over all of these episodes,

    Q(s,a)2+10+7+0+8+9+5+number of episodes=6.8Q(s, a) \approx \frac{2 + 10 + 7 + 0 + 8 + 9 + 5 + \cdots}{\text{number of episodes}} = 6.8

    a single noisy sample (episode 1’s return of 22) is far from this average; the critic’s job is to converge toward 6.86.8, not toward whatever any one episode happened to return.

  5. Suppose you’re playing chess in an already-winning position, with three legal moves, every one of which still leads to a win:

    MoveQQ-value
    Queen100100
    Bishop9898
    Pawn9696

    All three numbers are large mostly because the state is already fantastic, close to a guaranteed win, not because any individual move is exceptional. Using QQ directly would credit all three moves with a huge update, even though none of them is meaningfully better than the others.

  6. Continuing the chess example above, the state’s value is the average QQ-value over the three moves, V(s)=100+98+963=98V(s) = \frac{100 + 98 + 96}{3} = 98. Subtracting it out gives each move’s advantage:

    MoveQQ-valueA(s,a)=Q(s,a)V(s)A(s, a) = Q(s, a) - V(s)
    Queen100100+2+2
    Bishop989800
    Pawn96962-2

    Instead of three huge, nearly identical rewards, the advantage shows Queen is mildly better than average, Bishop is exactly average, and Pawn is mildly worse, a much more informative signal than 100100, 9898, and 9696.

  7. Suppose the network currently predicts V^π(s)=5\hat{V}^\pi(s) = 5 for some state, but the observed return from that state was 66. The loss is (56)2=1(5 - 6)^2 = 1. After enough gradient steps on examples like this one, the prediction gradually climbs toward the target:

    StepPrediction
    Before training55
    After some training5.75.7
    After more training5.955.95
    Eventually66
  8. Suppose state SS shows up across five different episodes, with observed returns 55, 88, 66, 99, and 77. The network sees the training pairs (S,5)(S, 5), (S,8)(S, 8), (S,6)(S, 6), (S,9)(S, 9), (S,7)(S, 7), one per episode. Minimizing squared error across all of them pulls the prediction toward their average, 5+8+6+9+75=7\frac{5+8+6+9+7}{5} = 7, so the network learns V^π(S)7\hat{V}^\pi(S) \approx 7, close to the state’s true expected return.

  9. Suppose the immediate reward is r=2r = 2, and the network’s current prediction for the next state is V^π(s)=5\hat{V}^\pi(s') = 5. The bootstrapped target is y=2+5=7y = 2 + 5 = 7, available right away, without waiting to see how the rest of the episode plays out.

  10. Consider two episodes running through the same two states, Pink and Blue, before reaching a terminal:

    A trajectory running through a pink state, then a blue state, branching to either a green terminal (reward +1) or a red terminal (reward -1), with reward 0 elsewhere along the path.
    Source: Stanford CS224R.
    • Episode 1: Blue \to Green (+1+1)
    • Episode 2: Pink \to Blue \to Red (1-1)
    EstimateValueWhy
    MC(Blue)MC(\text{Blue})00Blue was visited twice, with returns +1+1 and 1-1. Average =1+(1)2=0= \frac{1 + (-1)}{2} = 0.
    MC(Pink)MC(\text{Pink})1-1Pink was visited only once, and that episode ended at the red terminal. Return =1= -1.
    TD(Blue)TD(\text{Blue})00Blue is followed by a different state in each episode, Green in episode 1, Red in episode 2, and both are terminal, so V(terminal)=0V(\text{terminal}) = 0. Its TD target in episode 1 is r(Blue,Green)+V(Green)=1+0=1r(\text{Blue}, \text{Green}) + V(\text{Green}) = 1 + 0 = 1; in episode 2 it’s r(Blue,Red)+V(Red)=1+0=1r(\text{Blue}, \text{Red}) + V(\text{Red}) = -1 + 0 = -1. Blue sees both targets over training, and gradient descent on the two squared errors pulls its estimate toward their average, 1+(1)2=0\frac{1 + (-1)}{2} = 0.
    TD(Pink)TD(\text{Pink})00The TD target for Pink is r+V(Blue)=0+0=0r + V(\text{Blue}) = 0 + 0 = 0, since Blue has already learned a value of 00 after the two episodes.

    The key difference: Monte Carlo uses the actual return from the episode, while TD uses the immediate reward plus the estimated value of the next state. That’s why MC(Pink)=1MC(\text{Pink}) = -1: it saw the whole bad outcome. But TD(Pink)=0TD(\text{Pink}) = 0: it only looks one step ahead to Blue, whose estimated value is already 00.

  11. Without off-policy learning: collect data, take one gradient step, discard all the data, collect again. Every transition is used exactly once, no matter how expensive it was to collect. With off-policy learning: collect data once, then take a gradient step, another, another, another, and only then collect more. Each transition gets reused across every one of those steps instead of being thrown away after a single update.

  12. Suppose the state is the prompt “The capital of France is”, the action taken during rollout was the token Paris, and the old policy assigned it 0.600.60:

    Next tokenOld policy
    Paris0.600.60
    London0.250.25
    Rome0.150.15

    After a training step, the new policy predicts:

    Next tokenNew policy
    Paris0.750.75
    London0.180.18
    Rome0.070.07

    The ratio for the action actually taken is r=0.750.60=1.25r = \frac{0.75}{0.60} = 1.25: the new policy makes Paris 25% more likely than before. Suppose instead the new policy had predicted Paris 0.300.30, London 0.500.50, Rome 0.200.20. Then r=0.300.60=0.5r = \frac{0.30}{0.60} = 0.5, and the chosen action is only half as likely as it was under the old policy.

    Which direction is desirable depends on the sign of the advantage. With A=+2A = +2, a larger ratio increases the objective, so the optimizer is encouraged to raise this action’s probability. With A=2A = -2, raising the probability hurts the objective, and the optimizer pushes the ratio below 11 instead.

  13. Suppose the old policy is Left 0.50.5, Right 0.50.5, and the advantage says A(Left)=10A(\text{Left}) = 10. The optimizer keeps increasing the probability of Left. After several gradient steps: Left 0.990.99, Right 0.010.01. The importance ratio for Left is now r=0.990.5=1.98r = \frac{0.99}{0.5} = 1.98. Left unchecked, further optimization pushes rr to 55, 1010, 2020, 5050, and beyond: θ\theta' becomes very different from the θ\theta that generated the advantage estimate, so the optimizer ends up trusting information that’s no longer accurate.

  14. With ϵ=0.2\epsilon = 0.2, the clipping interval is [0.8,1.2][0.8, 1.2]:

    Raw ratioClipped
    0.50.50.80.8
    0.90.90.90.9
    1.11.11.11.1
    1.51.51.21.2
    4.04.01.21.2

    Anything outside [0.8,1.2][0.8, 1.2] gets pulled back to the nearest edge; anything already inside passes through unchanged.

  15. Suppose the advantage is A=5A = 5. Without clipping, the objective grows without bound as the ratio grows:

    RatioObjective
    1155
    221010
    552525
    2020100100

    With ϵ=0.2\epsilon = 0.2 clipping applied first:

    RatioClippedObjective
    111155
    1.11.11.11.15.55.5
    1.51.51.21.266
    441.21.266

    Once the ratio passes 1.21.2, the clipped objective stops growing: there’s no more reward for increasing the probability any further.

  16. Compare min(rA^,clip(r)A^)\min\big(r\hat{A}, \, \mathrm{clip}(r)\hat{A}\big) against always optimizing just clip(r)A^\mathrm{clip}(r)\hat{A} on its own, with ϵ=0.2\epsilon = 0.2 (clip interval [0.8,1.2][0.8, 1.2]):

    • Good action, ratio too high: A=+10A=+10, r=1.5r=1.5. Unclipped rA=1.5(10)=15rA = 1.5(10) = 15; clipped =1.2(10)=12= 1.2(10) = 12. min(15,12)=12\min(15, 12) = 12, exactly what clipping alone would give. Here the min\min changes nothing.
    • Bad action, ratio too high: A=10A=-10, r=1.5r=1.5 (a bad action made much more likely). Unclipped =1.5(10)=15= 1.5(-10) = -15; clipped =1.2(10)=12= 1.2(-10) = -12. Since 12>15-12 > -15, always using the clipped objective would score this update 12-12, better than it deserves, effectively rewarding the bad update for landing outside the clip region. PPO’s min(15,12)=15\min(-15, -12) = -15 refuses that, keeping the worse, true score.
    • Good action, ratio too low: A=+10A=+10, r=0.6r=0.6 (a good action made much less likely). Unclipped =0.6(10)=6= 0.6(10) = 6; clipped =0.8(10)=8= 0.8(10) = 8. Always using the clipped objective would score this 88, again better than it deserves. PPO’s min(6,8)=6\min(6, 8) = 6 keeps the worse score instead.
    • Bad action, ratio correctly reduced: A=10A=-10, r=0.2r=0.2 (a bad action correctly made much less likely). Unclipped =0.2(10)=2= 0.2(-10) = -2; clipped =0.8(10)=8= 0.8(-10) = -8. min(2,8)=8\min(-2, -8) = -8: PPO still applies the more conservative, clipped score here, even though this particular change was in the right direction. Once rr moves outside the trust region, the objective saturates, whether or not that move happened to be beneficial.

    In every case, the min\min either matches the clipped objective, when clipping alone already handles the update correctly, or falls back to the true, unclipped score, when clipping alone would have flattered a bad update. That’s what stops the clipped objective from ever making an undesirable policy change look better than it is.