PPO: Proximal Policy Optimization for Reinforcement Learning

PPO : Guide Complet — Optimisation Proximale par Renforcement

Proximal Policy Optimization (PPO) — Complete Guide

Summary

Proximal Policy Optimization (PPO) is today the most widely used reinforcement learning algorithm in both industry and academic research. Developed by OpenAI in 2017, it rapidly replaced more complex methods such as Trust Region Policy Optimization (TRPO) thanks to its implementation simplicity and remarkable training stability.

Whether you’re working on agents that can play video games, control physical robots, or optimize recommendation systems, PPO is an essential reference. Its central philosophy is elegant: making progressive, bounded policy updates, thus avoiding abrupt changes that could catastrophically degrade performance.

In this complete guide, we will explore the mathematical foundations of PPO, understand its deep intuition, then implement the algorithm step by step with PyTorch to solve the CartPole-v1 environment from OpenAI Gym.


Mathematical Principles

The Actor-Critic Algorithm with Clipping

PPO is fundamentally an actor-critic algorithm. This architecture combines two neural subnetworks that work in concert:

  • The actor (denoted π_θ): it defines the policy, i.e., the probability distribution over possible actions given an observed state.
  • The critic (denoted V_φ): it estimates the value function, i.e., the expected cumulative future rewards from a given state.

PPO’s major innovation lies in its objective function called the clipped surrogate objective. This function imposes an implicit constraint on the magnitude of updates, ensuring that the new policy does not drift too far from the old one. Here is the exact formulation of the clipped objective:

L^CLIP(θ) = E_t[min(r_t(θ) · A_t, clip(r_t(θ), 1 − ε, 1 + ε) · A_t)]

In this equation:

  • r_t(θ) is the probability ratio, defined as r_t(θ) = π_θ(a_t|s_t) / π_old(a_t|s_t). It measures how much the new policy favors (or disfavors) action a_t taken in state s_t, compared to the old policy.
  • A_t is the estimated advantage for time step t. A positive advantage means the action taken was better than the expected average, while a negative advantage indicates it was suboptimal.
  • ε (epsilon) is the clipping hyperparameter, typically set to 0.1, 0.2, or 0.3.

The clipping mechanism works as follows: when the ratio r_t(θ) deviates too far from 1 — meaning the new policy is significantly different from the old one — the objective function is capped. This prevents overly aggressive updates that could cause training to diverge. In practice, the gradient is removed when the two terms of the minimum differ, creating an upper and lower bound on the confidence the algorithm can place in new directions.

Generalized Advantage Advantage Estimation (GAE)

Advantage estimation is critical for PPO to function correctly. The most commonly used method is Generalized Advantage Estimation (GAE), introduced by Schulman et al. in 2015 in the same paper as TRPO.

GAE calculates the advantage as a weighted sum of temporal-difference errors (TD errors):

A_t^GAE(γ, λ) = Σ{l=0}^{T−t−1} (γ · λ)^l · δ{t+l}

where each TD error (temporal-difference error) is defined by:

δt = r_t + γ · V(s{t+1}) − V(s_t)

Here:

  • r_t is the immediate reward received at time step t.
  • γ (gamma) is the discount factor, controlling the importance given to future rewards relative to immediate rewards.
  • λ (lambda) is the GAE mixing parameter, allowing an optimal tradeoff between bias and variance of the estimate.
  • V(s_t) is the value estimated by the critic for state s_t.

When λ = 0, GAE reduces to TD(1) advantage, which has high bias but very low variance. When λ = 1, GAE uses full Monte Carlo returns, giving an unbiased but high-variance estimate. In practice, values of λ between 0.90 and 0.98 offer an excellent tradeoff.

The Total Loss Function

PPO’s complete loss function combines three essential terms:

L = L^CLIP − c_1 · L^VF + c_2 · S[π]

Each component plays a specific role:

  • L^CLIP is the clipped surrogate objective described above. It is the primary term that guides the policy update.
  • L^VF is the value function loss, typically a mean squared error between the predicted value and the observed return: L^VF = E_t[(V_φ(s_t) − V_target_t)²].
  • S[π] is the policy entropy term, which encourages exploration by penalizing overconfident distributions. This prevents the agent from getting trapped in a local optimum by prematurely exploiting a suboptimal strategy.
  • c_1 and c_2 are coefficients that weight the importance of the value function and entropy, respectively.

Optimization is typically performed with an Adam optimizer, chosen for its robustness and ability to handle gradients of varying magnitudes. The policy is updated for multiple epochs (epochs) on the same batch of collected experiences, making PPO particularly efficient in terms of data utilization.


Intuition — Understanding PPO Simply

To truly grasp the essence of PPO, imagine the situation of a teacher correcting a student. This is not merely a decorative metaphor: it is exactly the mechanism at work in the algorithm.

When the student (our agent) makes a small mistake, the teacher (the PPO algorithm) gently advises. They slightly adjust the student’s understanding in the right direction. It is a fine, gradual correction, respectful of the student’s learning pace.

Now imagine the student makes a big mistake. A too-severe teacher might scold harshly, at the risk of completely discouraging the student and destroying their confidence. PPO does not act this way. Thanks to the fundamental clipping mechanism, it limits the magnitude of its correction. It essentially says: “I noticed your mistake, I’m going to correct it, but I’m not going to overturn everything either.”

That is exactly the core idea of PPO: progressive policy updates, neither too timid nor too aggressive. The perfect balance between cautious exploration and decisive learning.

Compare this with older methods like classical REINFORCE or even TRPO. REINFORCE can make extremely volatile updates: a single unfortunate batch of experiences can explode the policy and ruin hours of training. TRPO, on the other hand, is mathematically elegant but algorithmically very complex — it requires computing Hessian matrix inverses, which is prohibitively expensive for large neural networks containing millions of parameters.

PPO finds the sweet spot, the optimal equilibrium point: it is as simple to implement as REINFORCE, but offers stability comparable to TRPO. It is precisely this winning combination that propelled it to the rank of de facto standard for modern reinforcement learning. Companies like OpenAI, DeepMind, and numerous academic research laboratories use it daily.


Python Implementation with PyTorch

Now let’s move to concrete practice. We will implement PPO from scratch with PyTorch to solve the CartPole-v1 environment, a reinforcement learning classic where an agent must balance a vertical pole on a mobile cart by applying left or right forces.

Step 1: Actor-Critic Network Architecture

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import gymnasium as gym


class ActorCritic(nn.Module):
    """
    Shared Actor-Critic network:
    - A shared hidden layer extracts state features.
    - The actor produces a categorical distribution over actions.
    - The critic estimates the value of the current state.
    """
    def __init__(self, state_dim, action_dim, hidden_dim=256):
        super(ActorCritic, self).__init__()

        # Shared feature extraction layer
        self.feature_extractor = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.Tanh(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.Tanh(),
        )

        # Actor head — action distribution
        self.actor = nn.Sequential(
            nn.Linear(hidden_dim, action_dim),
        )

        # Critic head — scalar value estimation
        self.critic = nn.Sequential(
            nn.Linear(hidden_dim, 1),
        )

    def forward(self, state):
        # Feature extraction
        features = self.feature_extractor(state)

        # Action probability distribution
        action_logits = self.actor(features)
        action_probs = torch.softmax(action_logits, dim=-1)
        action_dist = torch.distributions.Categorical(probs=action_probs)

        # Critic value estimation
        state_value = self.critic(features)

        return action_dist, state_value

Step 2: GAE Computation (Generalized Advantage Estimation)

def compute_gae(rewards, values, dones, gamma=0.99, lam=0.95):
    """
    Computes GAE advantages and returns for each time step.

    Parameters:
        rewards: list of rewards
        values:  list of critic-estimated values
        dones:   list of episode end indicators
        gamma:   discount factor
        lam:     GAE mixing parameter

    Returns:
        advantages: normalized GAE advantages
        returns:    computed returns (advantages + values)
    """
    advantages = []
    gae = 0.0

    # Reverse traversal (from last step to first)
    for t in reversed(range(len(rewards))):
        if t == len(rewards) - 1:
            next_non_terminal = 1.0 - dones[t]
            next_value = 0.0
        else:
            next_non_terminal = 1.0 - dones[t]
            next_value = values[t + 1]

        # TD error (temporal-difference error)
        delta = rewards[t] + gamma * next_value * next_non_terminal - values[t]

        # GAE: weighted sum of TD errors
        gae = delta + gamma * lam * next_non_terminal * gae
        advantages.insert(0, gae)

    advantages_tensor = torch.tensor(advantages, dtype=torch.float32)
    returns_tensor = advantages_tensor + torch.tensor(values, dtype=torch.float32)

    # Advantage normalization (considerably stabilizes training)
    advantages_tensor = (
        (advantages_tensor - advantages_tensor.mean())
        / (advantages_tensor.std() + 1e-8)
    )

    return advantages_tensor, returns_tensor

Step 3: Main PPO Training Loop

def train_ppo(
    env_id="CartPole-v1",
    total_timesteps=500_000,
    steps_per_rollout=2048,
    ppo_epochs=10,
    batch_size=64,
    gamma=0.99,
    lam=0.95,
    clip_epsilon=0.2,
    c1=0.5,      # Value loss coefficient
    c2=0.01,     # Entropy coefficient
    lr=3e-4,
    max_grad_norm=0.5,
):
    """
    Trains a PPO agent on the specified environment.
    """

    # Environment creation
    env = gym.make(env_id)
    state_dim = env.observation_space.shape[0]
    action_dim = env.action_space.n

    # Network and optimizer initialization
    model = ActorCritic(state_dim, action_dim, hidden_dim=256)
    optimizer = optim.Adam(model.parameters(), lr=lr, eps=1e-5)

    # Reward tracking
    episode_rewards = []
    current_episode_reward = 0.0

    print(f"Starting PPO training on {env_id}")
    print(f"   Parameters: lr={lr}, gamma={gamma}, lambda={lam}, epsilon={clip_epsilon}")
    print(f"   Rollouts of {steps_per_rollout} steps, {ppo_epochs} PPO epochs")
    print(f"   Target: {total_timesteps} time steps\n")

    global_step = 0
    while global_step < total_timesteps:

        # ─────────────────────────────────────────────
        # Phase 1: Experience collection (rollout)
        # ─────────────────────────────────────────────
        states, actions, rewards, log_probs, values, dones = [], [], [], [], [], []

        state, _ = env.reset()
        current_episode_reward = 0.0

        for step in range(steps_per_rollout):
            state_tensor = torch.tensor(state, dtype=torch.float32).unsqueeze(0)

            with torch.no_grad():
                action_dist, state_value = model(state_tensor)

            action = action_dist.sample().item()
            log_prob = action_dist.log_prob(torch.tensor(action))

            next_state, reward, terminated, truncated, _ = env.step(action)
            done = terminated or truncated

            states.append(state)
            actions.append(action)
            rewards.append(reward)
            dones.append(done)
            log_probs.append(log_prob.item())
            values.append(state_value.item())

            state = next_state
            current_episode_reward += reward
            global_step += 1

            if done:
                episode_rewards.append(current_episode_reward)
                if len(episode_rewards) % 50 == 0:
                    avg = np.mean(episode_rewards[-50:])
                    print(f"   Step {global_step} | Avg reward (last 50): {avg:.1f}")
                state, _ = env.reset()
                current_episode_reward = 0.0

        # ─────────────────────────────────────────────
        # Phase 2: GAE advantage computation
        # ─────────────────────────────────────────────
        advantages, returns = compute_gae(rewards, values, dones, gamma, lam)

        # ─────────────────────────────────────────────
        # Phase 3: PPO updates (multiple epochs)
        # ─────────────────────────────────────────────
        tensors_states = torch.tensor(np.array(states), dtype=torch.float32)
        tensors_actions = torch.tensor(actions, dtype=torch.int64)

        for epoch in range(ppo_epochs):
            # Data shuffling
            indices = torch.randperm(len(states))
            for start in range(0, len(states), batch_size):
                end = start + batch_size
                batch_idx = indices[start:end]

                batch_states = tensors_states[batch_idx]
                batch_actions = tensors_actions[batch_idx]
                batch_advantages = advantages[batch_idx]
                batch_returns = returns[batch_idx]
                batch_old_log_probs = torch.tensor(log_probs, dtype=torch.float32)[batch_idx]

                # Forward pass
                action_dist, state_values = model(batch_states)

                # Probability ratio
                new_log_probs = action_dist.log_prob(batch_actions)
                ratio = torch.exp(new_log_probs - batch_old_log_probs)

                # Clipped surrogate objective
                surr1 = ratio * batch_advantages
                surr2 = torch.clamp(
                    ratio, 1.0 - clip_epsilon, 1.0 + clip_epsilon
                ) * batch_advantages
                actor_loss = -torch.min(surr1, surr2).mean()

                # Value loss
                critic_loss = nn.functional.mse_loss(
                    state_values.squeeze(), batch_returns
                )

                # Entropy (encourages exploration)
                entropy = action_dist.entropy().mean()

                # Combined total loss
                total_loss = actor_loss + c1 * critic_loss - c2 * entropy

                # Backpropagation and weight update
                optimizer.zero_grad()
                total_loss.backward()
                nn.utils.clip_grad_norm_(model.parameters(), max_grad_norm)
                optimizer.step()

    env.close()
    print(f"\nTraining completed after {global_step} time steps.")
    print(f"   Last average over 50 episodes: {np.mean(episode_rewards[-50:]):.1f}")

    return model, episode_rewards

Step 4: Evaluating the Trained Model

def evaluate_model(model, env_id="CartPole-v1", n_episodes=10, render=False):
    """
    Evaluates the performance of the trained model over several episodes.
    """
    if render:
        env = gym.make(env_id, render_mode="human")
    else:
        env = gym.make(env_id)

    rewards = []
    model.eval()

    for ep in range(n_episodes):
        state, _ = env.reset()
        episode_reward = 0.0
        done = False

        while not done:
            state_tensor = torch.tensor(state, dtype=torch.float32).unsqueeze(0)
            with torch.no_grad():
                action_dist, _ = model(state_tensor)
            action = action_dist.sample().item()

            state, reward, terminated, truncated, _ = env.step(action)
            episode_reward += reward
            done = terminated or truncated

        rewards.append(episode_reward)
        print(f"   Episode {ep+1}/{n_episodes} — Reward: {episode_reward:.1f}")

    env.close()
    print(f"\nAverage over {n_episodes} episodes: {np.mean(rewards):.1f} (±{np.std(rewards):.1f})")
    return rewards

Key Hyperparameters

The choice of hyperparameters is absolutely critical for PPO’s final performance. Here are the most important ones, with their typical values and their specific influence on training:

Hyperparameter Description Typical Value Influence
epsilon (clip range) Clipping range for the probability ratio 0.1 to 0.3 An epsilon that is too small makes learning excessively slow; too large, it completely loses its stabilizing role
gamma (discount factor) Discount factor for future rewards 0.99 to 0.999 Determines the temporal horizon: the closer gamma is to 1, the more the agent considers the long-term consequences of its actions
lam (GAE lambda) GAE bias-variance tradeoff parameter 0.90 to 0.98 Low lambda = biased but stable estimate; high lambda = faithful but noisy and volatile estimate
epochs_per_update Number of passes over rollout data 3 to 15 Too many epochs inevitably leads to overfitting on the data collected during the rollout
batch_size Size of each update batch 32 to 256 Directly influences the variance of the estimated gradient and overall training speed
lr (learning rate) Learning rate for the Adam optimizer 1e-4 to 5e-4 A learning rate that is too high can cause instability; too low, learning is unnecessarily long and inefficient
c1 Value loss weight in the total function 0.5 to 1.0 Controls the importance given to the accuracy of critic estimates
c2 Entropy weight in the total function 0.0 to 0.05 Encourages state space exploration; a value that is too high systematically prevents convergence
max_grad_norm Maximum gradient norm (gradient clipping) 0.5 to 1.0 Prevents catastrophic gradient explosions that could cause the entire training to diverge

Advantages and Limitations

Advantages

  1. Exceptional stability: The clipping mechanism rigorously ensures that updates stay within a reasonable zone, completely avoiding catastrophic policy collapse. This is the most appreciated characteristic of PPO among practitioners.
  2. Remarkable implementation simplicity: Unlike TRPO, which requires extremely complex matrix computations (conjugate gradient, Hessian matrices), PPO can be comfortably coded in a few hundred lines of clean Python.
  3. Superior sample efficiency: Thanks to multiple optimization epochs on the same rollout data set, PPO extracts maximum valuable information from each individually collected experience.
  4. Exceptional adaptability to varied environments: PPO works equally well with discrete action spaces (CartPole, Atari games) and continuous action spaces (RoboSchool environments, MuJoCo simulators for robotics).
  5. Easy and effective parallelization: Experience collection can be distributed across multiple environments operating simultaneously and independently, considerably accelerating overall learning.

Limitations

  1. Significant sample consumption: PPO remains fundamentally an on-policy algorithm, which imperatively means it requires collecting fresh data after each update. It is therefore very greedy in environment interactions, making it poorly suited to real-world situations where interactions are costly.
  2. Hyperparameter sensitivity: Although more robust than many alternative methods, PPO remains sensitive to the choice of learning rate and clip range. Poor tuning can lead to painfully slow convergence or trapping in a suboptimal local optimum.
  3. Limited exploration in difficult environments: Entropy alone as an exploration mechanism may prove insufficient in environments with extremely rare rewards or extended frustrating performance plateaus.
  4. No strong theoretical monotonicity guarantee: Unlike TRPO, which offers a rigorous mathematical guarantee of strict improvement monotonicity, PPO provides only an approximate guarantee through the clipping mechanism.
  5. Memory footprint for long rollouts: Storing all transitions during a complete rollout can become prohibitively expensive for environments with high-dimensional state spaces or particularly long rollouts.

4 Concrete Use Cases

1. Video games and AI competition

PPO is the algorithm behind many spectacular and widely publicized achievements in competitive video gaming. The iconic OpenAI Five agent that beat professional human Dota 2 players used a massively parallelized PPO architecture across thousands of machines. Similarly, agents developed for AI competitions regularly rely on sophisticated variants of PPO for their tactical refinement phase. In complex video games, the agent must learn nuanced long-term strategies — exactly the area where GAE estimation excels remarkably, propagating delayed rewards across hundreds of time steps.

2. Physical robot control

Fine control of physical robots is a domain where PPO’s intrinsic stability is particularly valuable and even critical. Prestigious companies like Boston Dynamics, Agility Robotics, and numerous world-renowned academic laboratories use PPO to train sophisticated bipedal or quadrupedal locomotion policies, delicate object manipulation, or autonomous navigation in complex environments. The progressive and bounded nature of PPO updates prevents the robot from adopting physically dangerous behaviors during training — an absolutely critical consideration when the policy directly controls a real and expensive mechanical system.

3. Language model optimization (RLHF)

One of the most publicized and revolutionary applications of PPO is Reinforcement Learning from Human Feedback (RLHF), widely used to fine-tune large language models like OpenAI’s ChatGPT. In this sophisticated context, PPO optimizes the text generation policy to maximize rewards assigned by a reward model previously trained on carefully annotated human preferences. The clipping mechanism is absolutely essential here: it prevents the model from dangerously straying too far from its initial linguistic knowledge, thus avoiding catastrophic degradation of generated text quality (catastrophic forgetting), a well-documented phenomenon in the literature.

4. Algorithmic trading and quantitative finance

In the demanding field of quantitative finance, PPO is successfully applied to algorithmic portfolio management, optimal stock order execution, and risk hedging. The agent progressively learns to make informed buy, sell, or hold decisions by maximizing risk-adjusted financial return. PPO’s remarkable ability to handle continuous action spaces (such as precise portfolio proportions) and incorporate complex reward functions combining return and variance penalty makes it a natural choice for these critical applications where each individual decision has direct and sometimes irreversible financial consequences.


Conclusion

Proximal Policy Optimization undeniably represents a masterpiece of algorithmic engineering in the field of reinforcement learning. By finding the perfect compromise between elegant algorithmic simplicity and remarkable practical performance, it has established itself as the absolute reference algorithm for stable intelligent agent training across the entire world.

Its fundamental clipping mechanism — that elegant and deeply intuitive idea of limiting the magnitude of updates — is truly the key to its legendary robustness. GAE advantage estimation, for its part, provides the most informative and precise learning signals possible. Together, these two major components form a learning system that is both remarkably powerful and reliable, capable of solving problems of once-unimaginable complexity.

For any serious machine learning practitioner, mastering PPO in depth is absolutely not optional: it is a fundamental and essential skill. Whether you are building agents for exciting video games, autonomous physical robots, sophisticated financial systems, or conversational language models, PPO will very likely be your tool of choice for the crucial reinforcement optimization phase.


See Also


About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →