PPO Clipped Surrogate Objective
Problem Statement
Compute PPO's clipped surrogate objective over a minibatch, along with the fraction of samples the clip range actually touched.
Background
Vanilla policy gradients are only valid for data collected by the current policy, so a step that is too large destroys the policy and there is no gradient signal left to recover with. TRPO solved this with a hard KL trust region and a conjugate-gradient solve. PPO gets most of the benefit with a first-order objective you can write in four lines.
Start from the importance-sampling ratio between the new and old policy on the same action:
Computing it as exp of a log-difference rather than a quotient of probabilities is what keeps it numerically sane. The objective is then
The min makes this a pessimistic lower bound, and its asymmetry is the whole trick:
- When the objective stops rewarding you once — no more gain from cranking a good action's probability up.
- When the clip binds on the other side, at .
- Crucially, when and is large, the min picks the unclipped term (it is more negative). The objective deliberately leaves the gradient un-truncated so the policy can pull itself back — clipping there would strand it.
The commonly logged diagnostic clipfrac is the fraction of samples with ; a value that climbs toward 1 means the policy is moving too far per update.
Your Task
Implement:
def ppo_clip_objective(logp_old, logp_new, advantages, clip_eps):
...
- logp_old[i], logp_new[i] — log-probabilities of the same action under the old and new policy.
- advantages[i] — the advantage estimate.
- clip_eps — the clip range, e.g. 0.2.
Return a **tuple **(objective, clip_fraction)****:
- objective — the mean over the batch of min(ratio*A, clip(ratio, 1-eps, 1+eps)*A).
- clip_fraction — the fraction of samples with abs(ratio - 1) > clip_eps.
Both are floats.
Input / Output Format
Three lists of floats and a float in; a tuple of two floats out, rounded to 4 decimals by the grader.
Sample
obj, frac = ppo_clip_objective([0.0, 0.0], [0.0, 0.0], [2.0, -3.0], 0.2)
print(round(obj, 4), round(frac, 4))
Output:
-0.5 0.0
Both ratios are exp(0) = 1, nothing is clipped, and the objective is the mean advantage (2.0 - 3.0)/2 = -0.5.
Example
ppo_clip_objective([0.0, 0.0], [0.0, 0.0], [2.0, -3.0], 0.2)
-0.5 0.0
Identical log-probabilities give ratio = exp(0) = 1 for both samples, so nothing falls outside [0.8, 1.2] and clip_fraction is 0. With ratio 1 the clipped and unclipped terms are both just the advantage, so the objective is the mean advantage (2.0 + (-3.0))/2 = -0.5.
Constraints
1 <= batch_size <= 1000000.0 < clip_eps < 1.0- Compute the ratio as
exp(logp_new - logp_old), not as a quotient of probabilities. objectiveis a mean, not a sum.clip_fractioncounts samples withabs(ratio - 1) > clip_eps, regardless of the sign of the advantage.- Do not round inside the function; return
(objective, clip_fraction)in that order.