PPO : Guide Complet — Proximal Policy Optimization

PPO : Guide Complet — Proximal Policy Optimization

PPO : Proximal Policy Optimization — Guide Complet

Résumé

Le PPO (Proximal Policy Optimization) est l’algorithme de reinforcement learning le plus largement adopté en 2026, aussi bien dans l’industrie que dans la recherche académique. Développé par John Schulman, Filip Wolski, Prafulla Dhariwal, Alec Radford et Oleg Klimov chez OpenAI en 2017, le PPO a rapidement supplanté les méthodes précédentes grâce à sa combinaison élégante de simplicité d’implémentation et de performances remarquables.

Contrairement aux méthodes de politique gradient classiques comme TRPO (Trust Region Policy Optimization), le PPO n’impose pas de contraintes d’optimisation complexes nécessitant des calculs de matrices de Fisher coûteuses. À la place, il utilise une fonction objective tronquée (clipped surrogate objective) qui limite implicitement les mises à jour de la politique. Cette approche est à la fois plus facile à programmer et plus efficace en pratique.

Le PPO sert aujourd’hui de colonne vertébrale à de nombreux systèmes d’IA avancés, y compris des agents conversationnels, des robots autonomes et des systèmes de contrôle industriel. Sa popularité s’explique par un équilibre rare entre robustesse, facilité de réglage et efficacité computationnelle.

Principe mathématique du PPO

Le ratio de politiques

Au cœur du PPO se trouve le ratio de politiques, qui mesure comment la nouvelle politique se compare à l’ancienne politique pour une action donnée :

$$r_t(\theta) = \frac{\pi_\theta(a_t|s_t)}{\pi_{\text{old}}(a_t|s_t)}$$

Ce ratio quantifie la différence relative de probabilité entre la politique actuelle $\pi_\theta$ et la politique ancienne $\pi_{\text{old}}$ qui a généré les données. Si $r_t(\theta) > 1$, la nouvelle politique accorde plus de probabilité à cette action que l’ancienne. Si $r_t(\theta) < 1$, c’est l’inverse. Lorsque $r_t(\theta) = 1$, les deux politiques sont identiques pour cette action.

L’avantage de ce ratio est qu’il est invariant à l’échelle : il ne dépend pas de la magnitude absolue des probabilités, seulement de leur rapport relatif. Cela le rend particulièrement robuste aux variations numériques lors de l’entraînement.

La fonction de surrogaît avec clipping

La contribution principale du PPO est sa fonction objective tronquée (clipped surrogate objective). Au lieu de maximiser directement l’espérance des avantages pondérés par le ratio — ce qui peut entraîner des mises à jour excessivement grandes — le PPO tronque le ratio dans un intervalle $[1-\varepsilon, 1+\varepsilon]$ :

$$L^{\text{CLIP}}(\theta) = \mathbb{E}_t \left[ \min\left(r_t(\theta) \cdot \hat{A}_t,\; \text{clip}(r_t(\theta), 1-\varepsilon, 1+\varepsilon) \cdot \hat{A}_t\right) \right]$$

Dans cette formule, $\hat{A}_t$ représente l’avantage estimé à l’instant $t$, et $\varepsilon$ est un hyperparamètre (typiquement $0,2$) qui détermine l’ampleur maximale autorisée pour le ratio de politiques.

Le mécanisme de clipping fonctionne de la manière suivante : le minimum entre deux termes est sélectionné. Le premier terme est l’objectif de politique gradient standard, tandis que le second terme est ce même objectif mais avec le ratio tronqué. En prenant le minimum, on s’assure que le ratio effectif ne peut jamais s’éloigner trop de 1.

Concrètement :

  • Si l’avantage est positif (l’action était bonne), le clipping empêche $r_t(\theta)$ d’augmenter au-delà de $1+\varepsilon$. On encourage donc la politique à choisir cette action plus souvent, mais sans excès.
  • Si l’avantage est négatif (l’action était mauvaise), le clipping empêche $r_t(\theta)$ de descendre en dessous de $1-\varepsilon$. On décourage la politique de choisir cette action, mais sans la supprimer complètement.

Cette approche est remarquablement élégante : elle n’impose aucune contrainte dure sur l’optimisation, mais pénalise implicitement les mises à jour trop agressives.

Generalized Advantage Estimation (GAE)

Pour estimer les avantages $\hat{A}_t$, le PPO utilise généralement la Generalized Advantage Estimation (GAE), introduite par Schulman et al. en 2015. La GAE combine plusieurs estimations d’avantages à différents horizons temporels selon un paramètre $\lambda$ :

$$\hat{A}t^{\text{GAE}(\gamma,\lambda)} = \sum$$}^{\infty} (\gamma\lambda)^l \cdot \delta_{t+l

où $\delta_t = r_t + \gamma \cdot V(s_{t+1}) – V(s_t)$ est l’erreur de différence temporelle (TD error).

Le paramètre $\lambda \in [0,1]$ contrôle le compromis biais-variance de l’estimation :

  • $\lambda$ proche de 0 : estimation biaisée mais de faible variance. On ne regarde que l’erreur TD immédiate, ce qui est stable mais peut manquer des récompenses à long terme.
  • $\lambda$ proche de 1 : estimation peu biaisée mais de forte variance. On considère presque toute la trajectoire, ce qui est précis mais instable.
  • En pratique, $\lambda = 0,95$ offre un excellent compromis.

Le paramètre $\gamma \in [0,1]$ est le facteur d’actualisation standard en reinforcement learning, contrôlant l’importance des récompenses futures par rapport aux récompenses immédiates.

La fonction de coût totale

L’objectif complet du PPO combine trois composantes :

$$L(\theta) = L^{\text{CLIP}}(\theta) + c_1 \cdot L^V(\theta) – c_2 \cdot S[\pi_\theta]$$

Où :

  • $L^{\text{CLIP}}(\theta)$ est la fonction de surrogaît tronquée décrite ci-dessus.
  • $L^V(\theta)$ est la perte de la fonction de valeur, généralement une erreur quadratique moyenne entre la valeur prédite $V(s_t)$ et la cible $r_t + \gamma \cdot V(s_{t+1})$. Le coefficient $c_1$ (généralement $0,5$) contrôle l’importance relative de cette composante.
  • $S[\pi_\theta]$ est le terme d’entropie de la politique. Maximiser l’entropie encourage l’exploration en maintenant la distribution d’actions suffisamment diverse. Le coefficient $c_2$ (généralement $0,01$) ajuste la force de cette régularisation.

Intuition : pourquoi le PPO fonctionne si bien

Imaginez un élève qui apprend une technique sportive auprès d’un professeur. Si l’élève essaie de modifier radicalement son geste après une seule leçon, il risque de détruire tout ce qu’il avait appris précédemment. À l’inverse, s’il ne modifie jamais son geste, il ne progressera pas.

Le PPO trouve exactement ce juste milieu. Le mécanisme de clipping agit comme un garde-fou : l’élève peut améliorer sa technique, mais pas trop brusquement. Chaque mise à jour de la politique reste dans une « zone de confiance » autour de la politique précédente.

Par comparaison, les méthodes de policy gradient classiques (comme REINFORCE) n’ont aucune protection contre les mises à jour excessives. Un avantage fortement positif peut faire augmenter la probabilité d’une action de façon exponentielle, conduisant à une politique déterministe prématurée qui ne peut plus explorer efficacement. C’est comme si l’élève, après avoir réussi un seul mouvement, décidait de ne plus jamais faire autrement — au risque de passer à côté de techniques supérieures.

Le PPO évite cet écueil grâce à son clipping. Même si une action reçoit un avantage très élevé, la probabilité de cette action ne peut augmenter que d’un facteur $(1+\varepsilon)$ au maximum par itération. Cela garantit une évolution douce et stable de la politique, ce qui est particulièrement important dans des environnements complexes où les surfaces de récompense sont irrégulières et bruitées.

Implémentation Python avec PyTorch

Voici une implémentation complète d’un agent PPO pour l’environnement CartPole-v1 de Gymnasium :

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import gymnasium as gym
from torch.distributions import Categorical
import os

# ──────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
GAMMA = 0.99
GAE_LAMBDA = 0.95
CLIP_EPSILON = 0.2
VALUE_COEFF = 0.5
ENTROPY_COEFF = 0.01
LEARNING_RATE = 3e-4
N_STEPS = 2048
N_EPOCHS = 10
BATCH_SIZE = 64
MAX_EPISODES = 500


# ──────────────────────────────────────────────
# Architecture Actor-Critic
# ──────────────────────────────────────────────
class ActorCritic(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        self.shared = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.Tanh(),
            nn.Linear(128, 128),
            nn.Tanh(),
        )
        self.actor_head = nn.Linear(128, action_dim)
        self.critic_head = nn.Linear(128, 1)

    def forward(self, state):
        features = self.shared(state)
        action_logits = self.actor_head(features)
        state_value = self.critic_head(features)
        return action_logits, state_value

    def act(self, state):
        logits, value = self.forward(state)
        distribution = Categorical(logits=logits)
        action = distribution.sample()
        log_prob = distribution.log_prob(action)
        return action.item(), log_prob, value.squeeze(-1)

    def evaluate(self, state, action):
        logits, value = self.forward(state)
        distribution = Categorical(logits=logits)
        log_prob = distribution.log_prob(action)
        entropy = distribution.entropy()
        return log_prob, value.squeeze(-1), entropy


# ──────────────────────────────────────────────
# Calcul du GAE (Generalized Advantage Estimation)
# ──────────────────────────────────────────────
def compute_gae(rewards, values, dones, gamma=GAMMA, lam=GAE_LAMBDA):
    advantages = []
    gae = 0.0
    values = values + [0.0]  # Valeur de l'état terminal

    for t in reversed(range(len(rewards))):
        delta = (rewards[t] + gamma * values[t + 1] * (1 - dones[t]) - values[t])
        gae = delta + gamma * lam * (1 - dones[t]) * gae
        advantages.insert(0, gae)

    returns = [adv + val for adv, val in zip(advantages, values[:-1])]
    return advantages, returns


# ──────────────────────────────────────────────
# Entraînement PPO
# ──────────────────────────────────────────────
def train_ppo():
    env = gym.make("CartPole-v1")
    state_dim = env.observation_space.shape[0]
    action_dim = env.action_space.n
    env.close()

    policy = ActorCritic(state_dim, action_dim).to(DEVICE)
    optimizer = optim.Adam(policy.parameters(), lr=LEARNING_RATE)

    rewards_history = []
    best_mean_reward = -float("inf")

    global_step = 0

    for episode in range(MAX_EPISODES):
        env = gym.make("CartPole-v1")
        state, _ = env.reset()
        ep_reward = 0

        # Collecte des données
        roll_states, roll_actions, roll_log_probs = [], [], []
        roll_rewards, roll_dones, roll_values = [], [], []

        for step in range(N_STEPS):
            state_t = torch.tensor(state, dtype=torch.float32).to(DEVICE).unsqueeze(0)
            action, log_prob, value = policy.act(state_t)

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

            roll_states.append(state)
            roll_actions.append(action)
            roll_log_probs.append(log_prob.item())
            roll_rewards.append(reward)
            roll_dones.append(float(done))
            roll_values.append(value.item())

            state = next_state
            ep_reward += reward
            global_step += 1

            if done:
                state, _ = env.reset()

        env.close()

        # Calcul GAE
        advantages, returns = compute_gae(
            roll_rewards, roll_values, roll_dones
        )

        advantages = torch.tensor(advantages, dtype=torch.float32, device=DEVICE)
        returns = torch.tensor(returns, dtype=torch.float32, device=DEVICE)
        states = torch.tensor(np.array(roll_states), dtype=torch.float32, device=DEVICE)
        actions = torch.tensor(np.array(roll_actions), device=DEVICE)
        old_log_probs = torch.tensor(roll_log_probs, dtype=torch.float32, device=DEVICE)

        # Normalisation des avantages
        advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)

        # Mise à jour de la politique sur N epochs
        dataset_size = len(states)
        indices = np.arange(dataset_size)

        for epoch in range(N_EPOCHS):
            np.random.shuffle(indices)

            for start in range(0, dataset_size, BATCH_SIZE):
                end = start + BATCH_SIZE
                batch_idx = indices[start:end]

                b_states = states[batch_idx]
                b_actions = actions[batch_idx]
                b_old_log_probs = old_log_probs[batch_idx]
                b_advantages = advantages[batch_idx]
                b_returns = returns[batch_idx]

                # Évaluation avec la politique courante
                new_log_probs, values, entropies = policy.evaluate(b_states, b_actions)

                # Ratio de politiques
                ratios = torch.exp(new_log_probs - b_old_log_probs)

                # Fonction de surrogaît avec clipping
                surr1 = ratios * b_advantages
                clipped_ratios = torch.clamp(ratios, 1 - CLIP_EPSILON, 1 + CLIP_EPSILON)
                surr2 = clipped_ratios * b_advantages
                policy_loss = -torch.min(surr1, surr2).mean()

                # Perte de la fonction de valeur
                value_loss = nn.MSELoss()(values, b_returns)

                # Terme d'entropie
                entropy_bonus = entropies.mean()

                # Perte totale
                loss = policy_loss + VALUE_COEFF * value_loss - ENTROPY_COEFF * entropy_bonus

                optimizer.zero_grad()
                loss.backward()
                nn.utils.clip_grad_norm_(policy.parameters(), max_norm=0.5)
                optimizer.step()

        rewards_history.append(ep_reward)
        mean_reward = np.mean(rewards_history[-100:])

        if episode % 50 == 0:
            print(f"Épisode {episode} | Récompense : {ep_reward:.0f} | "
                  f"Moyenne 100 épisodes : {mean_reward:.1f}")

        # Sauvegarde du meilleur modèle
        if mean_reward > best_mean_reward:
            best_mean_reward = mean_reward
            save_path = os.path.join(
                os.path.dirname(os.path.abspath(__file__)), "best_ppo_cartpole.pt"
            )
            torch.save(policy.state_dict(), save_path)

        if mean_reward >= 490:
            print(f"\nConvergence atteinte ! Moyenne : {mean_reward:.1f}")
            break

    print(f"\nEntraînement terminé. Meilleure moyenne : {best_mean_reward:.1f}")


if __name__ == "__main__":
    train_ppo()

Cette implémentation illustre les composants essentiels du PPO :

  1. Réseau Actor-Critic partagé : les couches cachées sont communes à l’acteur et au critique, ce qui permet un apprentissage de représentations plus riche et efficace.
  2. Calcul du GAE : la fonction compute_gae implémente la formule récursive avec le paramètre $\lambda$, fournissant des estimations d’avantages stables.
  3. Clipping du ratio : le cœur du PPO — torch.clamp(ratios, 1 - CLIP_EPSILON, 1 + CLIP_EPSILON) — garantit que les ratios restent dans l’intervalle $[0,8\ ; 1,2]$.
  4. Normalisation des avantages : une pratique standard qui stabilise considérablement l’entraînement en centrant et réduisant les avantages.
  5. Troncature du gradient : clip_grad_norm_ protège contre les gradients explosifs, un problème fréquent en reinforcement learning.

Hyperparamètres clés du PPO

Le réglage des hyperparamètres est crucial pour obtenir de bonnes performances avec le PPO. Voici les paramètres les plus importants :

Hyperparamètre Valeur typique Rôle
clip_epsilon 0,1 – 0,3 (défaut 0,2) Amplitude maximale de la mise à jour de la politique. Plus la valeur est petite, plus les mises à jour sont conservatives et stables.
gae_lambda 0,90 – 0,98 (défaut 0,95) Compromis biais-variance dans l’estimation des avantages. $\lambda=1$ équivaut aux avantages Monte Carlo purs ; $\lambda=0$ équivaut au TD(0).
value_coeff ($c_1$) 0,5 – 1,0 (défaut 0,5) Poids de la perte de la fonction de valeur. Si le critique est de mauvaise qualité, augmenter ce coefficient peut aider.
entropy_coeff ($c_2$) 0,005 – 0,02 (défaut 0,01) Force du bonus d’entropie. Augmenter cette valeur favorise l’exploration ; la diminuer accélère la convergence mais risque une exploration prématurément insuffisante.
n_steps 128 – 4 096 (défaut 2 048) Nombre d’étapes de collecte de données par itération. Plus de données entraînent des gradients plus stables mais un coût computationnel plus élevé.
n_epochs 3 – 10 (défaut 10) Nombre de passes sur les données collectées. Attention : trop d’epochs peuvent faire diverger car les données deviennent obsolètes par rapport à la politique courante.
learning_rate 1e-4 – 5e-4 (défaut 3e-4) Taux d’apprentissage standard. Un learning rate decay linéaire est souvent bénéfique.
batch_size 32 – 256 (défaut 64) Taille des mini-batches pour la mise à jour des gradients.

Conseil pratique : commencez avec les valeurs par défaut. Le PPO est remarquablement robuste aux réglages par rapport à d’autres algorithmes de reinforcement learning. Si les performances stagnent, augmentez n_steps avant de toucher aux autres hyperparamètres.

Avantages et limites du PPO

Avantages

  • Simplicité d’implémentation : contrairement au TRPO qui nécessite des inversions de matrices de Fisher, le PPO se code en quelques dizaines de lignes. Cette simplicité réduit considérablement le risque de bugs.
  • Stabilité remarquable : le mécanisme de clipping empêche les mises à jour destructrices de la politique, ce qui conduit à des courbes d’apprentissage régulières et prévisibles.
  • Efficacité computationnelle : le PPO est nettement plus rapide que le TRPO en termes de temps d’entraînement. La mise à jour se fait par simple descente de gradient stochastique.
  • Robustesse aux hyperparamètres : le PPO fonctionne raisonnablement bien avec les valeurs par défaut sur une grande variété d’environnements, ce qui en fait un excellent point de départ.
  • Support des espaces d’actions continus et discrets : le même algorithme s’applique aux deux cas, ce qui simplifie son utilisation.

Limites

  • Sample inefficiency : comme tous les algorithmes en ligne, le PPO nécessite un grand nombre d’interactions avec l’environnement. Dans des contextes réels (robotique, contrôle industriel), cela peut être prohibitif en termes de temps et de ressources.
  • Sensible au choix de n_epochs : si le ratio entre le nombre de données collectées et le nombre d’epochs de mise à jour est mal calibré, l’algorithme peut soit sous-utiliser les données (surapprentissage insuffisant) soit les utiliser trop de fois (overfitting sur des données obsolètes).
  • Pas de garantie théorique forte : contrairement au TRPO qui dispose de garanties de monotonicité, le PPO est une heuristique. Le clipping ne garantit pas formellement que la performance ne diminuera pas à chaque étape.
  • Peut converger vers des sous-optima locaux : dans des environnements à récompenses très clairsemées ou à paysages de récompense complexes, le PPO peut rester bloqué dans des politiques sous-optimales.

Cas d’usage concrets

1. Alignement des grands modèles de langage (RLHF)

Le PPO est l’algorithme fondamental derrière le Reinforcement Learning from Human Feedback (RLHF), utilisé pour aligner les modèles de langage avec les préférences humaines. Dans ce contexte, le PPO entraîne un modèle de langage à produire des réponses préférées par des annotateurs humains. Le mécanisme de clipping est particulièrement important ici : les LLM sont des politiques énormes, et des mises à jour trop agressives pourraient dégrader gravement la qualité linguistique. Le PPO a été utilisé par OpenAI pour entraîner ChatGPT, et reste la référence de l’industrie en 2026.

2. Contrôle de robots humanoïdes

Des équipes de recherche comme celles de Boston Dynamics et de Berkeley utilisent le PPO pour entraîner des politiques de locomotion pour des robots humanoïdes. Le PPO excelle dans ces tâches car il peut gérer des espaces d’actions continus de grande dimension — un robot humanoïde possède souvent plus de 30 degrés de liberté, nécessitant un contrôle fin et coordonné. Le clipping assure que chaque mise à jour ne détruit pas les compétences déjà acquises (marcher, se tenir debout).

3. Jeux vidéo et agents de jeu

Le PPO est utilisé dans des benchmarks classiques comme Atari, MuJoCo, et des environnements de jeu plus complexes comme Dota 2 et StarCraft II (OpenAI Five). Même si AlphaStar a utilisé des méthodes plus sophistiquées, le PPO reste une approche de référence pour l’apprentissage de stratégies de jeu grâce à sa capacité à apprendre des politiques stochastiques adaptatives.

4. Trading algorithmique

Des hedge funds et des fintech expérimentent le PPO pour des stratégies de trading automatisées. Le clipping du PPO est particulièrement adapté à ce domaine : les marchés financiers étant hautement non stationnaires, une politique qui évolue trop agressivement peut perdre rapidement de l’argent. Les mises à jour progressives du PPO permettent un ajustement continu et prudent de la stratégie de trading.

Voir aussi

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur la façon dont les données de vos commentaires sont traitées.