DDPG : Guide Complet — Gradient de Politique Déterministe Profond

DDPG : Guide Complet — Gradient de Politique Déterministe Profond

Deep Deterministic Policy Gradient : Guide complet — Actor-Critic pour Actions Continues

Résumé — Le DDPG (Deep Deterministic Policy Gradient) est un algorithme de reinforcement learning actor-critic conçu pour les espaces d’action continus. Conçu par Lillicrap et al. en 2015, il combine trois idées fondamentales: un actor déterministe (qui produit une action précise plutôt qu’une distribution), des target networks pour stabiliser l’apprentissage, et un replay buffer pour décorréler les expériences. C’est l’extension naturelle du DQN aux environnements où les actions sont continues (ex: angle d’un volant, couple d’un moteur) plutôt que discrètes (gauche/droite/saut).


Principe mathématique

1. Le problème des actions continues

Le DQN fonctionne pour des actions discrètes car il calcule Q(s,a) pour chaque action possible et prend le max. Avec des actions continues (ex: un angle entre -180° et +180°), le max sur un espace continu est impossible à calculer directement.

2. Politique déterministe

DDPG utilise un actor déterministe mu(s|theta^mu) qui produit directement l’action optimale (pas une distribution de probabilités). Le critic Q(s,a|theta^Q) évalue cette action:

Actor: a = mu(s | theta^mu)  # action déterministe
Critic: Q(s, a | theta^Q)   # valeur Q de l'état-action

3. Mise à jour du critic

On utilise des target networks pour stabiliser, comme dans DQN:

y_i = r_i + gamma · Q'(s_{i+1}, mu'(s_{i+1} | theta^{mu'}) | theta^{Q'})
Loss critic: L = 1/N · somme (y_i - Q(s_i, a_i | theta^Q))^2

4. Mise à jour de l’actor

L’actor est mis à jour par ascent de gradient de la politique, en utilisant le gradient du critic:

grad_theta_mu J = E[grad_a Q(s,a | theta^Q) · grad_{theta^mu} mu(s | theta^mu)]

Intuitivement: on demande au critic “comment améliorer l’action?” (grad_a Q) et on propage ce signal à travers l’actor pour mettre à jour ses paramètres.

5. Soft update des target networks

Au lieu de copier périodiquement les poids (hard update), DDPG met à jour les targets continûment par moyenne mobile (Polyak averaging):

theta' <-- tau · theta + (1 - tau) · theta'

Où tau << 1 (typiquement 0.001). Cela assure une évolution douce des targets, évitant les oscillations du DQN classique.

6. Bruit d’exploration: Ornstein-Uhlenbeck

Pour l’exploration dans les espaces continus, DDPG ajoute un bruit corrélé dans le temps au lieu du bruit indépendant d’un epsilon-greedy:

dx_t = theta_ou · (mu_ou - x_t) · dt + sigma_ou · dW_t

Le bruit OU a une mémoire: si une action a été tirée dans une direction, le bruit tend à persister dans cette direction, ce qui est utile pour les contrôles physiques (inertie).


Intuition

DDPG est comme un apprenti chef cuisinier. Le critic est le critique gastronomique qui goûte chaque plat et note sa qualité sur 10. L’actor est le cuisinier qui propose des recettes avec des actions continues: quantité de sel (de 0 à 5g), temps de cuisson (de 0 à 60 min), température (de 100 à 250°C).

Le critique ne dit pas juste “bon” ou “mauvais” — il donne une note précise et suggère: “un peu plus de sel améliorerait le plat”. Le cuisinier ajuste sa recette selon ces notes. Les deux s’améliorent ensemble au fil des essais.


Implémentation Python

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np


class Actor(nn.Module):
    """Réseau déterministe: état -> action continue."""
    def __init__(self, state_dim, action_dim, max_action):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, 256), nn.ReLU(),
            nn.Linear(256, 256), nn.ReLU(),
            nn.Linear(256, action_dim), nn.Tanh()
        )
        self.max_action = max_action

    def forward(self, state):
        return self.net(state) * self.max_action


class Critic(nn.Module):
    """Réseau Q: (état, action) -> valeur scalaire."""
    def __init__(self, state_dim, action_dim):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim + action_dim, 256), nn.ReLU(),
            nn.Linear(256, 256), nn.ReLU(),
            nn.Linear(256, 1)
        )

    def forward(self, state, action):
        return self.net(torch.cat([state, action], dim=1))


class OUNoise:
    """Bruit d'Ornstein-Uhlenbeck pour l'exploration."""
    def __init__(self, action_dim, mu=0.0, theta=0.15, sigma=0.2):
        self.action_dim = action_dim
        self.mu = mu
        self.theta = theta
        self.sigma = sigma
        self.state = np.ones(self.action_dim) * self.mu

    def sample(self):
        dx = self.theta * (self.mu - self.state)
        dx += self.sigma * np.random.randn(self.action_dim)
        self.state += dx
        return self.state.copy()


class ReplayBuffer:
    """Buffer d'expériences pour briser la corrélation temporelle."""
    def __init__(self, capacity):
        self.buffer = []
        self.capacity = capacity
        self.pos = 0

    def push(self, state, action, reward, next_state, done):
        if len(self.buffer) < self.capacity:
            self.buffer.append(None)
        self.buffer[self.pos] = (state, action, reward, next_state, done)
        self.pos = (self.pos + 1) % self.capacity

    def sample(self, batch_size):
        batch = np.random.choice(len(self.buffer), batch_size, replace=False)
        states = torch.FloatTensor(np.array([self.buffer[i][0] for i in batch]))
        actions = torch.FloatTensor(np.array([self.buffer[i][1] for i in batch]))
        rewards = torch.FloatTensor(np.array([self.buffer[i][2] for i in batch])).unsqueeze(1)
        next_states = torch.FloatTensor(np.array([self.buffer[i][3] for i in batch]))
        dones = torch.FloatTensor(np.array([self.buffer[i][4] for i in batch])).unsqueeze(1)
        return states, actions, rewards, next_states, dones


# DDPG Agent
class DDPG:
    def __init__(self, state_dim, action_dim, max_action, tau=0.005, gamma=0.99, lr=3e-4):
        self.actor = Actor(state_dim, action_dim, max_action)
        self.actor_target = Actor(state_dim, action_dim, max_action)
        self.critic = Critic(state_dim, action_dim)
        self.critic_target = Critic(state_dim, action_dim)

        # Copier les poids initiaux
        self.actor_target.load_state_dict(self.actor.state_dict())
        self.critic_target.load_state_dict(self.critic.state_dict())

        self.actor_opt = torch.optim.Adam(self.actor.parameters(), lr=lr)
        self.critic_opt = torch.optim.Adam(self.critic.parameters(), lr=lr)

        self.tau = tau
        self.gamma = gamma

    def select_action(self, state, noise=None):
        state = torch.FloatTensor(state).unsqueeze(0)
        with torch.no_grad():
            action = self.actor(state).cpu().numpy()[0]
        if noise is not None:
            action += noise.sample()
        return np.clip(action, -self.actor.max_action, self.actor.max_action)

    def update(self, replay_buffer, batch_size=256):
        states, actions, rewards, next_states, dones = replay_buffer.sample(batch_size)

        # Critic loss
        with torch.no_grad():
            target_actions = self.actor_target(next_states)
            target_q = self.critic_target(next_states, target_actions)
            target_q = rewards + (1 - dones) * self.gamma * target_q
        current_q = self.critic(states, actions)
        critic_loss = F.mse_loss(current_q, target_q)

        self.critic_opt.zero_grad()
        critic_loss.backward()
        self.critic_opt.step()

        # Actor loss
        actor_loss = -self.critic(states, self.actor(states)).mean()
        self.actor_opt.zero_grad()
        actor_loss.backward()
        self.actor_opt.step()

        # Soft update des targets
        for param, target in zip(self.actor.parameters(), self.actor_target.parameters()):
            target.data.copy_(self.tau * param.data + (1 - self.tau) * target.data)
        for param, target in zip(self.critic.parameters(), self.critic_target.parameters()):
            target.data.copy_(self.tau * param.data + (1 - self.tau) * target.data)

# Test sur Pendulum-v1 (environnement d'action continue)
try:
    import gymnasium as gym
except ImportError:
    import gym as gym

env = gym.make('Pendulum-v1')
agent = DDPG(env.observation_space.shape[0], env.action_space.shape[0], env.action_space.high[0])
noise = OUNoise(env.action_space.shape[0])
buffer = ReplayBuffer(100000)

for episode in range(100):
    state, _ = env.reset()
    episode_reward = 0
    for step in range(200):
        action = agent.select_action(state, noise)
        next_state, reward, done, _, _ = env.step(action)
        buffer.push(state, action, reward, next_state, done)

        if len(buffer.buffer) > 1000:
            agent.update(buffer)

        state = next_state
        episode_reward += reward
        if done:
            break
    print('Episode ' + str(episode) + ' | Reward: ' + str(round(episode_reward, 1)))

env.close()

Hyperparamètres

Hyperparamètre Valeur typique Description
tau 0.001-0.01 Soft update rate pour les target networks
gamma 0.95-0.99 Discount factor pour les récompenses futures
actor_lr 1e-4 – 1e-3 Taux d’apprentissage de l’actor
critic_lr 1e-4 – 1e-3 Taux d’apprentissage du critic
buffer_size 1e5 – 1e6 Capacité du replay buffer
noise_sigma 0.1-0.3 Écart-type du bruit OU pour l’exploration

Avantages

  1. Actions continues : Contrairement au DQN, DDPG gère naturellement les espaces d’action continus.
  2. Efficace en échantillons : Le replay buffer réutilise les expériences passées, améliorant l’efficacité.
  3. Stable : Target networks + soft updates + replay buffer = apprentissage stable comparé aux politiques gradient policy gradient naïfs.

Limites

  1. Hypersensible aux hyperparamètres : DDPG est connu pour être instable si les hyperparamètres ne sont pas bien réglés (surtout la taille du bruit d’exploration).
  2. Surestimation du critic : Comme DQN, le critic a tendance à surestimer les valeurs Q, ce qui dégrade les performances. TD3 (Twin Delayed DDPG) corrige ce problème avec deux critics et un délai de mise à jour de l’actor.
  3. Exploration limitée : Le bruit OU est un ajout heuristique à l’action, pas une exploration structurelle de l’espace d’action.
  4. Non applicable aux actions discrètes : DDPG est conçu pour les actions continues. Pour les discrètes, on utilise DQN ou PPO.

4 cas d’usage concrets

1. Contrôle de bras robotique

DDPG entraîne un bras robotique à saisir des objets. Les actions sont les angles des articulations (continus), les récompenses sont basées sur la distance à l’objet et la réussite de la préhension.

2. Commerce algorithmique

Les actions continues de DDPG (proportion du portefeuille à allouer à chaque actif) sont adaptées au trading algorithmique où les décisions sont continues et non discrètes.

3. Conduite autonome

Le contrôle de direction et d’accélération/freinage sont des actions continues. DDPG apprend à maintenir la voie et la vitesse sécuritaire dans des environnements simulés.

4. Optimisation de consommation énergétique

Dans les data centers, DDPG optimise la ventilation et la climatisation en continu (températures, vitesse des ventilateurs) pour minimiser la consommation énergétique tout en respectant les contraintes thermiques.


Conclusion

DDPG a ouvert la voie au reinforcement learning pour les espaces d’action continus. Bien que dépassé par TD3 et SAC (Soft Actor-Critic) en termes de stabilité et de performance, ses principes fondamentaux (actor-critic déterministe, target networks, replay buffer) restent la base de la plupart des algorithmes modernes pour les actions continues.

Son successeur direct, TD3, corrige les principaux problèmes de DDPG (surestimation Q, sensibilité aux hyperparamètres) avec seulement quelques modifications élégantes: deux critics pour réduire la surestimation, un délai pour mettre à jour l’actor moins souvent, et du bruit smooth sur les actions cibles.


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.