Trust Regions and Proximal Policy Optimization

Dive into Deep Learning · §15.2

Trust regions and proximal policy optimization
parameter distance lies about policy distance · reuse a batch, exactly · the performance difference lemma · a clip instead of a constraint

Parameter Space versus Policy Space

One parameter, two actions (example due to Joshua Achiam), two updates of the same size \Delta\theta = 2:

\sigma'(0) = 0.25 against \sigma'(6) \approx 0.0025: no learning rate is right in both regions. One oversized step near indifference throws the policy into saturation, scores vanish, and the on-policy data is collected by the broken policy: the run is over. Capping the step in \theta caps the wrong quantity.

Importance Sampling

What can a batch from \pi_{\theta_{\text{old}}} say about \pi_\theta? Change of measure:

J(\theta) = E_{\tau \sim \theta_{\text{old}}}\!\Big[ \tfrac{P(\tau;\theta)}{P(\tau;\theta_{\text{old}})}\, R(\tau) \Big], \qquad \frac{P(\tau;\theta)}{P(\tau;\theta_{\text{old}})} = \prod_t \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_{\text{old}}}(a_t\mid s_t)}.

  • transitions cancel in the ratio: model-free, again
  • exact and unbiased, but the product compounds along the trajectory; all the bias traded for horizon-growing variance

The Per-Step Surrogate

Keep one ratio per step:

\rho_t = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_{\text{old}}}(a_t\mid s_t)}, \qquad \hat L(\theta) = \frac1n \sum_{i,t} \rho^i_t(\theta)\, \hat A^i_t.

Two corners cut: product \to per-step ratio; states still from the old policy’s visits. At \theta_{\text{old}}, \nabla \hat L is the policy gradient. \hat L is a local model: trustworthy near where it was built, but inaccurate after large policy changes.

The Performance Difference Lemma

J(\theta) - J(\theta_{\text{old}}) = E_{\tau \sim P(\cdot;\,\theta)} \Big[ \sum_t \gamma^t\, A^{\text{old}}(s_t, a_t) \Big]

The result follows by summing the TD identity along a trajectory and taking expectations :cite:Kakade.Langford.2002.

  • Improvement equals the new policy’s expected old-policy advantage.
  • The new trajectory distribution determines which states are visited.
  • The surrogate keeps the old state distribution and reweights sampled actions by \rho_t; both approximations arise from replacing the new trajectory distribution.

TRPO: a Bound, then a Constraint

J(\theta) \geq J(\theta_{\text{old}}) + \bar L(\theta) - \frac{4\gamma A_{\max}}{(1-\gamma)^2} \max_s D_{\text{KL}}\big(\pi_{\theta_{\text{old}}} \Vert \pi_\theta\big)

Here \bar L is the population surrogate under the old policy’s discounted occupancy, and \bar L(\theta_{\text{old}})=0 exactly; its sampled estimate need not be zero. Increasing this lower bound guarantees monotonic improvement in J. Practical TRPO replaces the maximum statewise divergence by a sampled mean KL constraint over visited states and solves the resulting approximation with second-order optimization. The practical constraint is a proxy rather than the bound displayed above.

For small policy changes, KL divergence induces the Fisher metric, so the corresponding steepest-ascent direction is the natural gradient. This is another instance of the geometry-dependent norms discussed in :numref:sec_muon.

The Clipped Objective

L^{\text{CLIP}} = \frac1n\sum_{i,t} \min\!\big(\rho\hat A,\ \text{clip}(\rho,1-\epsilon,1+\epsilon) \hat A\big)

When a ratio crosses the clipping boundary in the direction that would improve the sampled objective, that sample contributes zero gradient beyond the boundary. Changes that worsen the objective remain unclipped. This construction resembles a trust-region penalty but provides no monotonic-improvement guarantee.

Reusing a Batch

Before reusing a batch, freeze its advantages and the log probabilities under the behavior policy. The implementation then takes several optimization epochs and returns diagnostics for the resulting policy change:

@nnx.jit
def _ppo_step(policy, opt, obs, act, adv, logp_old, mask, epsilon,
              entropy_coef, use_clip):
    def loss_fn(policy):
        logp_all = jax.nn.log_softmax(policy(obs), axis=-1)
        logp = jnp.take_along_axis(logp_all, act[:, None], -1).squeeze(-1)
        rho = jnp.exp(logp - logp_old)
        surr = jnp.where(use_clip, jnp.minimum(
            rho * adv, jnp.clip(rho, 1 - epsilon, 1 + epsilon) * adv),
            rho * adv)
        entropy = -(jnp.exp(logp_all) * logp_all).sum(-1)
        loss = -(mask * (surr + entropy_coef * entropy)).sum() / mask.sum()
        return loss, (rho, logp, entropy)
    (_, (rho, logp, entropy)), grads = nnx.value_and_grad(
        loss_fn, has_aux=True)(policy)
    opt.update(policy, grads)
    n = mask.sum()
    return ((mask * (jnp.abs(rho - 1) > epsilon)).sum() / n,
            (mask * (logp_old - logp)).sum() / n, (mask * entropy).sum() / n)

def ppo_epochs(ac, batch, adv, logp_old, epsilon, num_epochs,
               entropy_coef=0.01, use_clip=True):
    """num_epochs clipped-surrogate passes on one frozen batch; returns
    [num_epochs, 3] numpy diagnostics: fraction of ratios outside the
    band, approximate KL, mean policy entropy."""
    size = 1 << max(6, (len(adv) - 1).bit_length())
    mask = jnp.asarray((np.arange(size) < len(adv)).astype(np.float32))
    obs, act, adv, logp_old = (_pad(np.asarray(x), size) for x in
                               (batch.obs, batch.act, adv, logp_old))
    step = nnx.cached_partial(_ppo_step, ac.policy, ac.opt_pi)
    return np.array([step(obs, act, adv, logp_old, mask, epsilon,
                          entropy_coef, use_clip)
                     for _ in range(num_epochs)])

train_ppo: GAE(0.95) by Default

The implementation uses generalized advantage estimation from :numref:sec_actorcritic with the commonly used setting \lambda=0.95:

def train_ppo(seed, ac, use_clip=True, trace=None):
    """Freeze the advantages and the collecting policy's log-probs, then
    spend num_epochs surrogate passes; GAE(0.95) is the default."""
    rng, env = np.random.default_rng(seed), gym.make('CartPole-v1')
    env.reset(seed=seed)
    for _ in range(num_updates):
        batch = d2l.rollout(env, ac.act, batch_episodes, rng)
        for _ in range(critic_steps):   # fresh lambda-return target, per pass
            fit_value(ac, batch.obs, batch.gae(ac.value_np, gamma, lam)
                      + ac.value_np(batch.obs))
        adv = d2l.normalize(batch.gae(ac.value_np, gamma, lam))
        logp_old = ac.log_prob_np(batch.obs, batch.act)
        d = ppo_epochs(ac, batch, adv, logp_old, epsilon_clip, num_epochs,
                       entropy_coef, use_clip)
        if trace is not None:
            trace.append(d)
        yield (float(batch.episode_returns().mean()), *d.mean(0), *d[-1])

The Ablation: Eight Seeds, Clip On and Off

Both variants use the same batches and 20 optimization passes. At least half of the unclipped seeds collapse to returns near 9, whereas every clipped seed reaches the task ceiling. Roughly 5% of ratio evaluations activate the clip. The individual failing seeds vary between runs, but the failure frequency is consistent in this experiment.

Training Diagnostics

entropy: 0.64 over the first five updates, 0.26 over the last five
  • In these runs, KL divergence and clipping events are concentrated in the first optimization epochs for each batch; clipping limits subsequent drift.
  • During training, entropy decreases from about 0.65 to 0.25 nats. The entropy bonus slows this decrease, as discussed in :numref:sec_regularized.

Policy Drift within a Batch

Because the likelihood ratios are importance weights, their effective sample size provides a diagnostic of weight concentration:

after 20 epochs the batch is worth 95% (clipped (PPO)) vs 42% (no clip) of its 136 steps

With clipping, the likelihood ratios remain nearly uniform over the chosen number of epochs. Without clipping, their effective sample size falls to half the batch or less. This weight-only diagnostic does not account for advantages, sample dependence, or mismatch in the state distribution.

Vectorized Collection and Minibatches

  • Vectorized environments collect an N\times T array of transitions. At the collection boundary, V supplies the bootstrap value, just as for other truncations discussed in :numref:sec_mdp.
  • Several minibatch epochs, such as four epochs with batches of 32, distribute the update over smaller stochastic-gradient steps.
  • Common implementation details include learning-rate annealing, normalization, value clipping, KL-based early stopping, and orthogonal initialization :cite:Huang.Dossa.Raffin.ea.2022.

When implementation details are matched, TRPO and PPO achieve similar performance :cite:Engstrom.Ilyas.Santurkar.ea.2020. Thus empirical differences cannot be attributed to the nominal objective alone. The reference implementation cleanrl/ppo.py provides a useful comparison with the shorter version in this section.

Recap

  • Parameter distance does not determine policy distance, so updates should be constrained in policy space.
  • Importance sampling permits reuse, but trajectory-ratio variance grows rapidly; the per-step surrogate is accurate only near the old policy.
  • The performance difference lemma evaluates the old advantage under the new policy’s trajectory distribution.
  • TRPO optimizes a constrained bound; PPO uses clipping without a monotonic improvement guarantee.
  • Common implementations use GAE near 0.95 and an entropy bonus.