Passez à l'échelle avec le deep RL
Le lac gelé a 64 cases et 4 actions : sa table 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).
Des tables aux fonctions : l’approximation
Section intitulée « Des tables aux fonctions : l’approximation »L’idée est simple : au lieu de stocker une valeur par état, on apprend une fonction paramétrée , où 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 comme dans une régression : la cible de Q-learning, , joue le rôle de l’étiquette, et on minimise l’erreur quadratique par descente de gradient.
Mais attention : ce n’est pas une régression ordinaire. La cible dépend elle-même de , 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.
DQN : Q-learning avec un réseau de neurones
Section intitulée « DQN : Q-learning avec un réseau de neurones »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é.
- La mémoire de rejeu (experience replay). Chaque transition 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.
- Le réseau cible (target network). La cible est calculée avec une copie figée du réseau, de paramètres , 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 s’écrit :
- sont les poids du réseau qu’on entraîne ;
- sont les poids figés du réseau cible ;
- pour une transition terminale, la cible se réduit à .
L’agent agit toujours de façon ε-greedy, avec un 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 torchimport torch.nn as nn
n_entrees, n_actions, gamma = 4, 2, 0.99reseau = 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éeoptimiseur = 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înes, 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 , 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 , 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, , 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 :
On fait une montée de gradient sur . 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 :
- est la direction dans laquelle modifier pour rendre l’action plus probable dans l’état ;
- 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 , puis faire . C’est l’idée « renforcer ce qui a marché », au sens littéral.
En PyTorch, on n’écrit pas le gradient à la main : on construit une « perte » dont le gradient est l’opposé de l’estimation de , 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 Jperte.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 par , où est une référence qui ne dépend pas de l’action, typiquement une estimation de . Cela ne change pas le gradient en moyenne, mais réduit fortement sa variance. La quantité 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 , et le critique, qui estime par différences temporelles. L’acteur utilise l’erreur TD du critique, , comme estimation de l’avantage. On retrouve le compromis biais-variance entre Monte-Carlo et TD.
PPO, l’algorithme de référence
Section intitulée « PPO, l’algorithme de référence »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 et maximise :
où est l’avantage estimé et ramène le ratio dans l’intervalle (souvent ). 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.
| Famille | Ce qu’on apprend | Actions | Exemples |
|---|---|---|---|
| Fondée sur la valeur | , politique ε-greedy | discrètes | DQN, double DQN |
| Gradient de politique | directement | discrètes ou continues | REINFORCE, PPO |
| Acteur-critique | et (ou ) | discrètes ou continues | A2C, 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 :
- Affinage supervisé : on entraîne d’abord le modèle à imiter des réponses rédigées par des humains.
- 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 à donner un meilleur score à la réponse préférée, avec le modèle de Bradley-Terry : la probabilité que la réponse soit préférée à vaut , où est la fonction sigmoïde, et on minimise .
- 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.
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.
Applications et limites
Section intitulée « Applications et limites »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.
En résumé
Section intitulée « En résumé »- Quand les états sont trop nombreux pour une table, on approche ou 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 : 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.
Exercices
Section intitulée « Exercices »Exercice · Calculer une cible DQN
facileUne transition du mini-lot est , non terminale. Le réseau entraîné donne , et le réseau cible donne . Avec , calculez la cible et la contribution de cette transition à la perte quadratique.
Voir la solution
Cible : . Erreur : . Contribution à la perte : . Le gradient poussera 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, Falseq_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_saprint(round(cible, 3), round(erreur, 3), round(erreur ** 2, 4)) # 1.188 0.188 0.0353gamma <- 0.99; r <- 0; termine <- FALSEq_sa <- 1 # Q(s, a ; theta)q_cible_suiv <- c(1.2, 0.8) # Q(s', . ; theta^-)
cible <- r + gamma * max(q_cible_suiv) * (1 - termine)erreur <- cible - q_saround(c(cible = cible, erreur = erreur, perte = erreur^2), 4) # 1.188 0.188 0.0353Exercice · Le ratio tronqué de PPO
moyenAvec , calculez la valeur de dans les deux cas suivants, et interprétez :
- et (une bonne action devenue 50 % plus probable) ;
- et (une mauvaise action devenue deux fois moins probable).
Voir la solution
- et . Le minimum vaut 2,4. L’objectif ne récompense plus l’augmentation au-delà de 20 % : le gradient par rapport à est nul dans cette zone, et la mise à jour arrête de pousser cette action.
- et . Le minimum vaut , la valeur tronquée, qui ne dépend plus de . 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 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 nulfor rho in (1.3, 1.5, 2.0): print(rho, round(objectif_ppo(rho, 2), 3)) # toujours 2.4objectif_ppo <- function(rho, avantage, eps = 0.2) { min(rho * avantage, min(max(rho, 1 - eps), 1 + eps) * avantage)}
c(objectif_ppo(1.5, 2), objectif_ppo(0.5, -1)) # 2.4 -0.8
# Au-delà de la zone de confiance, l'objectif ne dépend plus de rho : gradient nulsapply(c(1.3, 1.5, 2), objectif_ppo, avantage = 2) # 2.4 2.4 2.4Exercice · Entraîner un modèle de récompense
difficilePour une question donnée, un annotateur préfère la réponse à la réponse . Le modèle de récompense leur attribue actuellement les scores et .
- 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 ?
- Même question si les scores étaient inversés.
- Pourquoi seule la différence des scores compte-t-elle ? Quelle conséquence pour l’étape d’optimisation par PPO ?
Voir la solution
- . La perte vaut .
- et la perte vaut . Le modèle se trompe de préférence, la perte est bien plus forte, et le gradient poussera à augmenter et à diminuer .
- Le modèle de Bradley-Terry ne dépend que de : 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ésprint(bradley_terry(11.5, 10.5)) # (0.731, 0.313) : +10 partout ne change rienbradley_terry <- function(r_prefere, r_autre) { p <- plogis(r_prefere - r_autre) # sigmoïde : probabilité de la préférence round(c(proba = p, perte = -log(p)), 3)}
bradley_terry(1.5, 0.5) # 0.731 0.313bradley_terry(0.5, 1.5) # 0.269 1.313 : scores inversésbradley_terry(11.5, 10.5) # 0.731 0.313 : +10 partout ne change rien