Aller au contenu

Passez à l'échelle avec le deep RL

Le lac gelé a 64 cases et 4 actions : sa table QQ tient en 256 nombres. Un jeu vidéo Atari, lui, s’observe à travers des images de 210 × 160 pixels en couleurs. Le nombre d’écrans possibles dépasse de très loin le nombre d’atomes de l’univers : impossible d’y associer une ligne de tableau, et encore moins de visiter chaque état plusieurs fois. Comment un agent a-t-il pu apprendre à jouer à ces jeux, ou battre des champions de go ?

La réponse est l’apprentissage par renforcement profond (deep reinforcement learning) : remplacer les tables par des réseaux de neurones qui généralisent d’un état à l’autre. Ce chapitre en donne les idées principales, sans prétendre à l’exhaustivité : DQN, qui prolonge Q-learning, puis les méthodes de gradient de politique, jusqu’à leur usage le plus médiatisé, l’alignement des grands modèles de langage (RLHF).

L’idée est simple : au lieu de stocker une valeur par état, on apprend une fonction paramétrée Q(s,a;θ)Q(s, a ; \theta), où θ\theta désigne les poids d’un réseau de neurones. Le réseau prend en entrée une description de l’état (des pixels, des mesures de capteurs) et renvoie une valeur par action.

Le gain est double. D’abord, la mémoire ne dépend plus du nombre d’états, mais du nombre de paramètres. Ensuite, et surtout, le réseau généralise : deux écrans presque identiques recevront des valeurs proches, même si l’un d’eux n’a jamais été vu. C’est exactement ce que fait un modèle en apprentissage supervisé (voir le cours d’apprentissage automatique).

On ajuste alors θ\theta comme dans une régression : la cible de Q-learning, y=r+γmax⁡a′Q(s′,a′;θ)y = r + \gamma \max_{a'} Q(s', a' ; \theta), joue le rôle de l’étiquette, et on minimise l’erreur quadratique (y−Q(s,a;θ))2\big(y - Q(s, a ; \theta)\big)^2 par descente de gradient.

Mais attention : ce n’est pas une régression ordinaire. La cible dépend elle-même de θ\theta, et elle bouge à chaque mise à jour. Les exemples successifs sont très corrélés (deux images consécutives d’un jeu se ressemblent). Et les données dépendent de la politique, qui change aussi. Une application naïve de Q-learning avec un réseau de neurones diverge souvent.

L’algorithme DQN (deep Q-network) a été le premier à apprendre à jouer à des dizaines de jeux Atari à partir des seuls pixels et du score. Il reprend Q-learning et ajoute deux garde-fous contre l’instabilité.

  1. La mémoire de rejeu (experience replay). Chaque transition (s,a,r,s′,termineˊ)(s, a, r, s', \text{terminé}) est stockée dans une grande mémoire tampon (replay buffer). Pour apprendre, on tire au hasard un mini-lot de transitions dans cette mémoire. Le tirage aléatoire casse la corrélation entre exemples successifs, et chaque transition sert plusieurs fois, ce qui économise des interactions.
  2. Le réseau cible (target network). La cible est calculée avec une copie figée du réseau, de paramètres θ−\theta^-, qu’on ne met à jour que de temps en temps (par exemple tous les quelques milliers de pas). La cible ne bouge donc plus à chaque mise à jour : on retrouve, par morceaux, une régression presque ordinaire.

La fonction de perte minimisée sur un mini-lot BB s’écrit :

L(θ)=1∣B∣∑(s,a,r,s′)∈B(r+γmax⁡a′Q(s′,a′;θ−)−Q(s,a;θ))2L(\theta) = \frac{1}{\lvert B \rvert} \sum_{(s, a, r, s') \in B} \Big( r + \gamma \max_{a'} Q(s', a' ; \theta^-) - Q(s, a ; \theta) \Big)^2
  • θ\theta sont les poids du réseau qu’on entraîne ;
  • θ−\theta^- sont les poids figés du réseau cible ;
  • pour une transition terminale, la cible se réduit à rr.
Environnementjeu, simulateurAgent ε-greedyargmax Q(s, · ; θ)Mémoire de rejeu(s, a, r, s′, terminé)Mini-lot au hasardRéseau θQ(s, a ; θ)Réseau cible θ⁻figé, sert à la cibleaction as′, rstocketireQ(s, a ; θ)r + γ max Q(s′, · ; θ⁻)copie tousles C pasperte : (cible − Q(s, a ; θ))², descente de gradient sur θ seulement
Les deux garde-fous de DQN : la mémoire de rejeu casse la corrélation entre transitions successives, et la cible est calculée par une copie figée θ⁻ du réseau.

L’agent agit toujours de façon ε-greedy, avec un ε\varepsilon qui décroît au fil de l’entraînement. Voici l’étape de mise à jour, écrite avec PyTorch sur un mini-lot déjà tiré de la mémoire :

import torch
import torch.nn as nn
n_entrees, n_actions, gamma = 4, 2, 0.99
reseau = nn.Sequential(nn.Linear(n_entrees, 64), nn.ReLU(), nn.Linear(64, n_actions))
reseau_cible = nn.Sequential(nn.Linear(n_entrees, 64), nn.ReLU(), nn.Linear(64, n_actions))
reseau_cible.load_state_dict(reseau.state_dict()) # copie figée
optimiseur = torch.optim.Adam(reseau.parameters(), lr=1e-3)
def mise_a_jour(s, a, r, s_suiv, termine):
"""Un pas de DQN sur un mini-lot (tenseurs de même longueur)."""
q = reseau(s).gather(1, a.unsqueeze(1)).squeeze(1) # Q(s, a ; theta)
with torch.no_grad(): # pas de gradient dans la cible
q_suiv = reseau_cible(s_suiv).max(dim=1).values
cible = r + gamma * q_suiv * (1 - termine)
perte = nn.functional.mse_loss(q, cible)
optimiseur.zero_grad()
perte.backward()
optimiseur.step()
return perte.item()
# Un mini-lot factice de 32 transitions, pour vérifier que tout s'enchaîne
s, s_suiv = torch.randn(32, n_entrees), torch.randn(32, n_entrees)
a = torch.randint(0, n_actions, (32,))
r, termine = torch.randn(32), torch.zeros(32)
print(mise_a_jour(s, a, r, s_suiv, termine))

De nombreuses améliorations ont suivi. Le double DQN applique l’idée du double Q-learning (chapitre 7) : le réseau entraîné choisit la meilleure action dans s′s', le réseau cible l’évalue, ce qui réduit la surestimation des valeurs. La mémoire de rejeu prioritaire tire plus souvent les transitions dont l’erreur est grande. L’architecture dueling sépare la valeur de l’état et l’avantage de chaque action.

Optimiser directement la politique : le gradient de politique

Section intitulée « Optimiser directement la politique : le gradient de politique »

DQN a une limite structurelle : il faut calculer max⁡a′Q(s′,a′)\max_{a'} Q(s', a'), donc énumérer les actions. Comment faire pour un bras robotique dont chaque articulation prend un angle continu ? Et si la meilleure stratégie est elle-même aléatoire (comme au jeu pierre-feuille-ciseaux) ?

Les méthodes de gradient de politique (policy gradient) prennent le problème par l’autre bout : elles paramètrent directement la politique, πθ(a∣s)\pi_\theta(a \mid s), par exemple par un réseau qui renvoie les probabilités des actions (ou la moyenne et l’écart-type d’une loi normale pour des actions continues). On cherche alors les paramètres qui maximisent le retour moyen :

J(θ)=Eπθ[ G0 ]J(\theta) = \mathbb{E}_{\pi_\theta}\big[\, G_0 \,\big]

On fait une montée de gradient sur JJ. Le résultat central, le théorème du gradient de politique, donne une expression de ce gradient qu’on peut estimer à partir d’épisodes joués, sans connaître le modèle :

∇θJ(θ)=Eπθ[∑tGt ∇θlog⁡πθ(at∣st)]\nabla_\theta J(\theta) = \mathbb{E}_{\pi_\theta}\Big[ \sum_t G_t \, \nabla_\theta \log \pi_\theta(a_t \mid s_t) \Big]
  • ∇θlog⁡πθ(at∣st)\nabla_\theta \log \pi_\theta(a_t \mid s_t) est la direction dans laquelle modifier θ\theta pour rendre l’action ata_t plus probable dans l’état sts_t ;
  • GtG_t pondère cette direction : une action suivie d’un bon retour devient plus probable, une action suivie d’un mauvais retour moins probable.

L’algorithme REINFORCE applique cette formule telle quelle, avec des retours Monte-Carlo : jouer un épisode, calculer les GtG_t, puis faire θ←θ+α∑tGt∇θlog⁡πθ(at∣st)\theta \leftarrow \theta + \alpha \sum_t G_t \nabla_\theta \log \pi_\theta(a_t \mid s_t). C’est l’idée « renforcer ce qui a marché », au sens littéral.

action 1(jouée, G = 2)action 20,00,20,40,6probabilité π(a)0,50,5500,50,450avant : θ = (0 ; 0)après : θ = (0,1 ; −0,1)
L'exemple : l'action 1, suivie d'un retour G = 2, passe de 0,5 à 0,550 de probabilité ; l'action 2 perd d'autant.

En PyTorch, on n’écrit pas le gradient à la main : on construit une « perte » dont le gradient est l’opposé de l’estimation de ∇J\nabla J, et on laisse l’optimiseur la minimiser.

import torch
# Log-probabilités des actions jouées pendant un épisode (sorties du réseau de politique)
log_probas = torch.tensor([-0.7, -1.2, -0.3], requires_grad=True)
retours = torch.tensor([0.81, 0.9, 1.0]) # G_t calculés en fin d'épisode
perte = -(retours * log_probas).sum() # minimiser -J revient à maximiser J
perte.backward()
print(log_probas.grad) # tensor([-0.8100, -0.9000, -1.0000])

Réduire la variance : ligne de base et acteur-critique

Section intitulée « Réduire la variance : ligne de base et acteur-critique »

REINFORCE est sans biais, mais sa variance est énorme, comme celle de Monte-Carlo (chapitre 5). Deux idées la réduisent :

  • La ligne de base (baseline). On remplace GtG_t par Gt−b(st)G_t - b(s_t), où bb est une référence qui ne dépend pas de l’action, typiquement une estimation de V(st)V(s_t). Cela ne change pas le gradient en moyenne, mais réduit fortement sa variance. La quantité Gt−V(st)G_t - V(s_t) mesure si l’action a fait mieux ou moins bien que prévu : on l’appelle l’avantage.
  • L’acteur-critique (actor-critic). On apprend en parallèle deux réseaux : l’acteur, qui est la politique πθ\pi_\theta, et le critique, qui estime VV par différences temporelles. L’acteur utilise l’erreur TD du critique, δ=r+γV(s′)−V(s)\delta = r + \gamma V(s') - V(s), comme estimation de l’avantage. On retrouve le compromis biais-variance entre Monte-Carlo et TD.

Un pas de gradient trop grand peut détruire une bonne politique en une seule mise à jour, et comme les données suivantes sont collectées avec cette politique dégradée, il est difficile de s’en remettre. PPO (proximal policy optimization) limite l’ampleur de chaque changement. Il compare la nouvelle et l’ancienne politique par le ratio ρ=πθ(a∣s)/πθancien(a∣s)\rho = \pi_\theta(a \mid s) / \pi_{\theta_{\text{ancien}}}(a \mid s) et maximise :

LCLIP(θ)=E[min⁡(ρ A^,  clip(ρ,1−ϵ,1+ϵ) A^)]L^{\text{CLIP}}(\theta) = \mathbb{E}\Big[ \min\big( \rho \, \hat{A}, \; \text{clip}(\rho, 1 - \epsilon, 1 + \epsilon) \, \hat{A} \big) \Big]

où A^\hat{A} est l’avantage estimé et clip\text{clip} ramène le ratio dans l’intervalle [1−ϵ,1+ϵ][1 - \epsilon, 1 + \epsilon] (souvent ϵ=0,2\epsilon = 0{,}2). Une fois que la probabilité d’une bonne action a augmenté de 20 %, il n’y a plus rien à gagner à l’augmenter davantage dans cette mise à jour. Simple, robuste et peu sensible à ses réglages, PPO est devenu l’algorithme par défaut de nombreux travaux.

0,00,81,01,22,0ratio ρ = π nouvelle / π ancienne0,00,51,01,52,0plat : plus rienà gagner au-delàde 1 + εzone [1 − ε ; 1 + ε]avantage positif (Â = 1)ρ Â (non tronqué)objectif de PPO0,00,81,01,22,0ratio ρ = π nouvelle / π ancienne−2,0−1,5−1,0−0,50,0plat en deçàde 1 − εavantage négatif (Â = −1)
L'objectif tronqué (vert) suit ρ près de 1 puis devient plat : au-delà de la zone [0,8 ; 1,2], pousser la probabilité plus loin dans le « bon » sens ne rapporte plus rien.
FamilleCe qu’on apprendActionsExemples
Fondée sur la valeurQ(s,a;θ)Q(s, a ; \theta), politique ε-greedydiscrètesDQN, double DQN
Gradient de politiqueπθ(a∣s)\pi_\theta(a \mid s) directementdiscrètes ou continuesREINFORCE, PPO
Acteur-critiqueπθ\pi_\theta et VV (ou QQ)discrètes ou continuesA2C, PPO, SAC

RLHF : le renforcement au service des modèles de langage

Section intitulée « RLHF : le renforcement au service des modèles de langage »

L’usage le plus connu aujourd’hui du deep RL est l’apprentissage par renforcement à partir de retours humains (reinforcement learning from human feedback, RLHF), qui a servi à rendre les grands modèles de langage (LLM) plus utiles et plus sûrs. Le problème : on ne sait pas écrire une fonction de récompense pour « une bonne réponse ». En revanche, un humain sait souvent dire laquelle de deux réponses il préfère.

Le RLHF procède en trois étapes :

  1. Affinage supervisé : on entraîne d’abord le modèle à imiter des réponses rédigées par des humains.
  2. Modèle de récompense : on montre à des annotateurs des paires de réponses à une même question, et ils indiquent leur préférée. On entraîne un réseau rϕr_\phi à donner un meilleur score à la réponse préférée, avec le modèle de Bradley-Terry : la probabilité que la réponse ygy_g soit préférée à ypy_p vaut σ(rϕ(yg)−rϕ(yp))\sigma\big(r_\phi(y_g) - r_\phi(y_p)\big), où σ\sigma est la fonction sigmoïde, et on minimise −log⁡σ(rϕ(yg)−rϕ(yp))-\log \sigma\big(r_\phi(y_g) - r_\phi(y_p)\big).
  3. Optimisation de la politique : le LLM est la politique ; l’état est le texte déjà écrit, l’action le mot (token) suivant, et la récompense le score du modèle de récompense sur la réponse complète. On l’optimise avec PPO, en ajoutant une pénalité de divergence de Kullback-Leibler qui l’empêche de trop s’éloigner du modèle de l’étape 1.
1. Affinage superviséimiter des réponsesrédigées par des humains2. Modèle de récompenseapprendre un score rsur des paires :P(A préférée à B) = σ(r(A) − r(B))3. Optimisation par PPOmaximiser le score r− pénalité KL vers le modèle 1annotateurs : « je préfère la réponse A à la B »la pénalité KL retient le modèle près de l’étape 1 (contre le détournement de récompense)
Les trois étapes du RLHF. Le score appris à l'étape 2 guide PPO à l'étape 3, et la pénalité KL garde le modèle proche de celui de l'étape 1.

Pourquoi cette pénalité ? Parce que le modèle de récompense n’est qu’une approximation des préférences humaines. Un optimiseur puissant trouve vite ses failles et produit des réponses qui obtiennent un score élevé sans être meilleures : c’est le détournement de récompense (reward hacking). Rester proche du modèle de départ limite ce risque.

Des variantes plus récentes, comme DPO (direct preference optimization), se passent du modèle de récompense explicite et de PPO : elles ajustent directement le modèle de langage sur les paires de préférences, avec une perte de classification. Le raisonnement reste celui de l’apprentissage par renforcement, mais la mise en œuvre est bien plus simple.

Au-delà des jeux et des LLM, l’apprentissage par renforcement sert dans les décisions séquentielles : gestion de stocks, tarification dynamique, enchères publicitaires, recommandation vue comme une suite de décisions qui façonnent l’engagement à long terme, contrôle de systèmes (robotique, réseaux électriques, climatisation de centres de données), souvent entraîné d’abord en simulation. Quand le modèle est connu et petit, la programmation dynamique de la partie 1 suffit, et il ne faut pas s’en priver.

Ses limites sont tout aussi importantes à connaître :

  • Il faut énormément d’interactions : des millions de parties pour un jeu Atari. C’est acceptable en simulation, rarement dans le monde réel.
  • La récompense est difficile à spécifier : l’agent optimise exactement ce qu’on lui demande, pas ce qu’on voulait dire, et exploite la moindre faille.
  • L’évaluation hors ligne est délicate : estimer ce que vaudrait une nouvelle politique à partir de données collectées par une autre est un problème difficile (off-policy evaluation).
  • Le passage de la simulation au réel (sim-to-real) déçoit souvent, car le simulateur n’est jamais parfait.
  • Les résultats sont instables : forte sensibilité aux hyperparamètres et à la graine aléatoire, ce qui impose de répéter les expériences.
  • Quand les états sont trop nombreux pour une table, on approche QQ ou π\pi par un réseau de neurones qui généralise entre états voisins.
  • Approximation, bootstrap et off-policy réunis peuvent rendre l’apprentissage instable (la triade mortelle).
  • DQN entraîne un réseau vers la cible de Q-learning, stabilisé par une mémoire de rejeu et un réseau cible figé.
  • Les gradients de politique optimisent directement πθ\pi_\theta : REINFORCE rend plus probables les actions suivies de bons retours ; une ligne de base et un critique réduisent la variance.
  • PPO limite l’ampleur de chaque mise à jour par un ratio tronqué ; c’est l’algorithme utilisé dans le RLHF des modèles de langage, avec un modèle de récompense appris sur des préférences humaines.
  • Le RL demande beaucoup d’interactions et une récompense bien spécifiée ; l’évaluation hors ligne et le passage au réel restent difficiles.

Exercice · Calculer une cible DQN

facile

Une transition du mini-lot est (s,a,r=0,s′)(s, a, r = 0, s'), non terminale. Le réseau entraîné donne Q(s,a;θ)=1,0Q(s, a ; \theta) = 1{,}0, et le réseau cible donne Q(s′,⋅ ;θ−)=(1,2;0,8)Q(s', \cdot \, ; \theta^-) = (1{,}2 ; 0{,}8). Avec γ=0,99\gamma = 0{,}99, calculez la cible et la contribution de cette transition à la perte quadratique.

Voir la solution

Cible : y=0+0,99×max⁡(1,2;0,8)=0,99×1,2=1,188y = 0 + 0{,}99 \times \max(1{,}2 ; 0{,}8) = 0{,}99 \times 1{,}2 = 1{,}188. Erreur : 1,188−1,0=0,1881{,}188 - 1{,}0 = 0{,}188. Contribution à la perte : 0,1882≈0,03530{,}188^2 \approx 0{,}0353. Le gradient poussera Q(s,a;θ)Q(s, a ; \theta) vers le haut, en direction de 1,188, sans modifier le réseau cible.

En code

import numpy as np
gamma, r, termine = 0.99, 0.0, False
q_sa = 1.0 # Q(s, a ; theta)
q_cible_suiv = np.array([1.2, 0.8]) # Q(s', . ; theta^-)
cible = r + gamma * q_cible_suiv.max() * (1 - termine)
erreur = cible - q_sa
print(round(cible, 3), round(erreur, 3), round(erreur ** 2, 4)) # 1.188 0.188 0.0353

Exercice · Le ratio tronqué de PPO

moyen

Avec ϵ=0,2\epsilon = 0{,}2, calculez la valeur de min⁡(ρA^,  clip(ρ,0,8,1,2) A^)\min\big(\rho \hat{A}, \; \text{clip}(\rho, 0{,}8, 1{,}2)\, \hat{A}\big) dans les deux cas suivants, et interprétez :

  1. ρ=1,5\rho = 1{,}5 et A^=2\hat{A} = 2 (une bonne action devenue 50 % plus probable) ;
  2. ρ=0,5\rho = 0{,}5 et A^=−1\hat{A} = -1 (une mauvaise action devenue deux fois moins probable).
Voir la solution
  1. ρA^=3\rho \hat{A} = 3 et clip(1,5)×2=1,2×2=2,4\text{clip}(1{,}5) \times 2 = 1{,}2 \times 2 = 2{,}4. Le minimum vaut 2,4. L’objectif ne récompense plus l’augmentation au-delà de 20 % : le gradient par rapport à ρ\rho est nul dans cette zone, et la mise à jour arrête de pousser cette action.
  2. ρA^=−0,5\rho \hat{A} = -0{,}5 et clip(0,5)×(−1)=0,8×(−1)=−0,8\text{clip}(0{,}5) \times (-1) = 0{,}8 \times (-1) = -0{,}8. Le minimum vaut −0,8-0{,}8, la valeur tronquée, qui ne dépend plus de ρ\rho. Avoir réduit la probabilité de la mauvaise action de plus de 20 % n’apporte plus rien : PPO empêche là aussi un changement trop brutal. Dans les deux cas, le min⁡\min choisit l’estimation la plus pessimiste, ce qui rend la mise à jour prudente.

En code

import numpy as np
def objectif_ppo(rho, avantage, eps=0.2):
return min(rho * avantage, np.clip(rho, 1 - eps, 1 + eps) * avantage)
print(round(objectif_ppo(1.5, 2), 3), round(objectif_ppo(0.5, -1), 3)) # 2.4 -0.8
# Au-delà de la zone de confiance, l'objectif ne dépend plus de rho : gradient nul
for rho in (1.3, 1.5, 2.0):
print(rho, round(objectif_ppo(rho, 2), 3)) # toujours 2.4

Exercice · Entraîner un modèle de récompense

difficile

Pour une question donnée, un annotateur préfère la réponse ygy_g à la réponse ypy_p. Le modèle de récompense leur attribue actuellement les scores rϕ(yg)=1,5r_\phi(y_g) = 1{,}5 et rϕ(yp)=0,5r_\phi(y_p) = 0{,}5.

  1. Selon le modèle de Bradley-Terry, quelle probabilité le modèle donne-t-il à la préférence observée ? Que vaut la perte −log⁡σ(rg−rp)-\log \sigma(r_g - r_p) ?
  2. Même question si les scores étaient inversés.
  3. Pourquoi seule la différence des scores compte-t-elle ? Quelle conséquence pour l’étape d’optimisation par PPO ?
Voir la solution
  1. σ(1,5−0,5)=σ(1)=1/(1+e−1)≈0,731\sigma(1{,}5 - 0{,}5) = \sigma(1) = 1/(1 + e^{-1}) \approx 0{,}731. La perte vaut −ln⁡0,731≈0,313-\ln 0{,}731 \approx 0{,}313.
  2. σ(−1)≈0,269\sigma(-1) \approx 0{,}269 et la perte vaut −ln⁡0,269≈1,313-\ln 0{,}269 \approx 1{,}313. Le modèle se trompe de préférence, la perte est bien plus forte, et le gradient poussera à augmenter rϕ(yg)r_\phi(y_g) et à diminuer rϕ(yp)r_\phi(y_p).
  3. Le modèle de Bradley-Terry ne dépend que de rg−rpr_g - r_p : ajouter une même constante à tous les scores ne change rien. Les récompenses ne sont donc définies qu’à une constante près, et leur échelle absolue n’a pas de sens. En pratique, on les normalise avant PPO, et c’est la comparaison entre réponses (via l’avantage) qui guide l’optimisation, pas la valeur brute du score.

En code

import numpy as np
sigmoide = lambda x: 1 / (1 + np.exp(-x))
def bradley_terry(r_prefere, r_autre):
p = sigmoide(r_prefere - r_autre) # probabilité de la préférence observée
return round(float(p), 3), round(float(-np.log(p)), 3) # et perte -log sigma
print(bradley_terry(1.5, 0.5)) # (0.731, 0.313)
print(bradley_terry(0.5, 1.5)) # (0.269, 1.313) : scores inversés
print(bradley_terry(11.5, 10.5)) # (0.731, 0.313) : +10 partout ne change rien