Reinforcement-learning objectives

What PPO Clipping Actually Limits

The clipped surrogate removes incentives in one direction at a time. It does not enforce a hard probability-ratio or KL bound.

If PPO uses a clip range of 0.2, can an action's probability move by more than 20%? Yes. The clippingA training-objective rule that stops rewarding some overly large policy changes. modifies a training objective; it does not project every updated policy back into a guaranteed probability-ratio interval.

PPO's clipped surrogate compares the ordinary probability-ratio-weighted advantage with a clipped version and takes the smaller value. The paper uses this construction while performing multiple minibatch optimization epochs on collected data. The sign of the advantage determines which direction loses additional incentive beyond the clipping threshold. PPO paper, Section 3

Positive and negative advantages flatten on opposite sides
Two signed-advantage clipped-surrogate curves plus a policy-distribution inset. Ratios .6,.8,1,1.2,1.4; advantage +2 gives 1.2,1.6,2,2.4,2.4; advantage −2 gives −1.6,−1.6,−2,−2.4,−2.8. Figure data.

Read one term carefully

Let r be new_probability / old_probability for a sampled action, A its estimated advantage, and epsilon = 0.2. The objective term is min(rA, clip(r, 0.8, 1.2)A). The objective is maximized.

For A = 2 and r = 1.4, the unclipped term is 2.8 while the clipped term is 2.4, so the result is 2.4. Increasing that action's probability further provides no extra benefit through this individual term.

But if r = 0.6, the terms are 1.2 and 1.6, so the objective keeps 1.2. Reducing the probability of a positively advantaged action remains penalized. Clipping does not flatten both sides symmetrically.

Now use A = −2. At r = 0.6, the terms are −1.2 and −1.6, and the result is −1.6. The incentive to keep decreasing the action's probability has flattened. At r = 1.4, the result is −2.8: increasing the probability of a negatively advantaged action still makes the objective worse.

The minimum is essential. Clipping r first and multiplying by A without retaining the unclipped alternative would create a different objective.

A counterexample to the hard-bound interpretation

Take an old two-action policy [0.5, 0.5] and a candidate policy [0.9, 0.1]. For the first action, r = 1.8, clearly outside [0.8, 1.2]. With advantage 1, its clipped objective term is still perfectly well-defined: 1.2.

The code calculates the full old-to-new KL divergence for these two distributions, approximately 0.510826 nats. This candidate is not claimed to be the outcome of a particular optimizer run. It demonstrates that the clipping formula itself does not reject or make an out-of-range policy impossible.

Actual updates involve many samples, shared parameters, optimizer state, and potentially additional loss terms. A change encouraged by one sample can move another sample's probability after its own term has flattened. Unseen states have no directly evaluated clipping term at all.

For a training experiment, log an explicit policy-change statistic alongside return, clip fraction, learning rate, and update epochs. A small-looking clip parameter cannot replace those observations. The CPU example evaluates objective values only; it provides no control-task performance result.

Run the example

The Python 3 example uses only the standard library. The notebook, source, figure data and recorded output are included in the complete bundle.

Inspect the code and recorded output
from math import log

epsilon = 0.2

def clipped_objective(ratio, advantage):
    clipped_ratio = min(1 + epsilon, max(1 - epsilon, ratio))
    return min(ratio * advantage, clipped_ratio * advantage)

for ratio in (0.6, 0.8, 1.0, 1.2, 1.4):
    positive = clipped_objective(ratio, 2.0)
    negative = clipped_objective(ratio, -2.0)
    print(f"r={ratio:.1f}: A=+2 -> {positive:.1f}, A=-2 -> {negative:.1f}")

old = [0.5, 0.5]
new = [0.9, 0.1]
assert abs(sum(old) - 1) < 1e-12
assert abs(sum(new) - 1) < 1e-12
ratio = new[0] / old[0]
kl = sum(p * log(p / q) for p, q in zip(old, new))
assert ratio > 1 + epsilon
assert abs(clipped_objective(ratio, 1.0) - 1.2) < 1e-12
assert kl > 0.5
print(f"candidate_ratio={ratio:.2f}, objective={clipped_objective(ratio, 1):.2f}")
print(f"KL(old||new)={kl:.6f} nats")

Recorded CPU output

r=0.6: A=+2 -> 1.2, A=-2 -> -1.6
r=0.8: A=+2 -> 1.6, A=-2 -> -1.6
r=1.0: A=+2 -> 2.0, A=-2 -> -2.0
r=1.2: A=+2 -> 2.4, A=-2 -> -2.4
r=1.4: A=+2 -> 2.4, A=-2 -> -2.8
candidate_ratio=1.80, objective=1.20
KL(old||new)=0.510826 nats

Sources

  1. Proximal Policy Optimization Algorithms