CONTENTS

PPO depuis les basesDraft

L’apprentissage par renforcement (RL) est une forme distincte d’apprentissage automatique utilisée pour des problèmes généralement ouverts. On peut le comparer à l’apprentissage supervisé (SL), qui est essentiellement une tâche de régression en haute dimension. En SL, on nous donne un jeu de données d’entrées et de sorties, , et notre tâche est d’apprendre une fonction qui minimise l’erreur quadratique moyenne (MSE) sur notre jeu de données. Cependant, de nombreux problèmes utiles ne peuvent pas être exprimés dans ce cadre. Supposons que vous vouliez apprendre à un agent humanoïde virtuel à faire un salto. Quel serait notre jeu de données ? Comment pourriez-vous collecter un tel jeu de données ? Ce n’est pas simple. Ce qui est simple, en revanche, c’est de savoir si l’agent a effectivement fait un salto. Nous vérifions simplement :

  • Les pieds de l’agent ont-ils quitté le sol
  • Les pieds de l’agent ont-ils pointé vers le haut à un moment donné
  • Les pieds de l’agent sont-ils revenus au sol

Nous voyons que pour cette tâche, la réussite peut être facilement définie, mais les contrôles articulaires exacts nécessaires à la réussite ne le peuvent pas. C’est dans de telles circonstances que nous utilisons le RL.

À un haut niveau, le RL est une méthode d’essais et d’erreurs. Dans les méthodes de gradient de politique (PG), nous commençons avec une politique, essayons un tas de tactiques, voyons lesquelles ont réussi, et guidons la politique pour qu’elle ressemble davantage à celles qui ont réussi. Dans cet article, nous allons explorer le RL et les méthodes de gradient de politique depuis les bases, pour aboutir à PPO ( Schulman et al., 2017 Schulman et al. (2017) Proximal Policy Optimization AlgorithmsarXiv preprint arXiv:1707.06347 ) , qui est, et est resté, la ligne de base fondamentale pour les méthodes PG. Nous aborderons également les approches antérieures par région de confiance qui ont inspiré PPO.

Préliminaires

Processus de Décision Markoviens

Nous voulons d’abord formaliser l’idée d’agents, d’environnements et de récompenses afin de pouvoir les traiter mathématiquement. La formalisation la plus courante est celle des Processus de Décision Markoviens (MDP) ( Bellman, 1957 Bellman (1957) A Markovian Decision ProcessJournal of Mathematics and Mechanics ; Sutton and Barto, 2018 Sutton & Barto (2018) Reinforcement Learning: An IntroductionThe MIT Press ) .

Un MDP est défini par les éléments suivants :

  • : l’ensemble de tous les états possibles de l’environnement
  • : l’ensemble de toutes les actions que l’agent peut effectuer
  • : l’ensemble de toutes les récompenses que l’agent peut recevoir
  • : une fonction de transition, qui nous indique : si nous avons pris l’action dans l’état , quelle est la probabilité de recevoir la récompense et de passer à l’état suivant ?
  • : le facteur d’actualisation temporel, c’est-à-dire qu’une récompense au temps vaut fois moins qu’au temps .
  • : la distribution initiale des états

Dans un environnement défini par un MDP, nous pouvons avoir une politique , qui nous donne la distribution de probabilité à partir de laquelle nous échantillonnons actuellement les actions. Pour le reste de cet article, nous supposerons qu’il s’agit d’un réseau de neurones paramétré par des poids .

Nous pouvons générer une trajectoire , qui est simplement une séquence d’états, d’actions et de récompenses dans le temps

Une fois que vous avez assimilé toutes ces variables, notre objectif devrait être clair. Nous voulons apprendre les poids de de telle sorte que les récompenses futures actualisées espérées, que nous notons , soient maximisées. Formellement, nous voulons maximiser

Puisque nous voulons utiliser la rétropropagation sur pour optimiser cet objectif, nous devons exprimer en fonction du gradient de . Cependant, l’opération n’est pas différentiable, c’est-à-dire que les gradients ne peuvent pas passer à travers l’action d’échantillonner depuis pour revenir vers . Nous voulons que le soit à l’intérieur de l’espérance, pas à l’extérieur.

Cela devient possible une fois que nous remarquons que

Cela nous permet simplement d’échantillonner des trajectoires depuis la politique, de multiplier la log-probabilité de la trajectoire par sa récompense totale actualisée, et d’effectuer la rétropropagation sans problème.

Nous ne pouvons pas calculer le gradient d’une estimation, mais nous pouvons calculer l’estimation d’un gradient !

Vanilla Policy Gradient

OK, assez de théorie. Voyons si cette chose fonctionne réellement.

Nous allons utiliser l’environnement CartPole-v1 de gymnasium, où l’objectif est de maintenir un poteau attaché à un chariot en position verticale le plus longtemps possible ( Towers et al., 2024 Towers et al. (2024) Gymnasium: A Standard Interface for Reinforcement Learning EnvironmentsarXiv preprint arXiv:2407.17032 ) .

À chaque pas de temps, l’environnement nous donne son état—qui dans ce cas est un vecteur de dimension 4 contenant l’angle du poteau, la vitesse angulaire du poteau, la position du chariot et la vitesse du chariot. Nous passons ensuite cet état à travers un réseau de politique, qui produit une distribution sur deux actions—pousser à gauche ou pousser à droite. Nous recevons une récompense de pour chaque pas de temps où nous survivons.

Voici le pseudocode. Le fichier exécutable complet est ici.

policy = Policy()  # une couche linéaire + softmax
optimizer = optim.Adam(policy.parameters(), lr=alpha)

for iteration in range(num_iterations):
    rewards, action_log_probs = [], []
    state = env.reset()  # état initial depuis ρ_0
    done = False
    while not done:
        log_probs = policy(state)  # (2,); a un grad
        action_dist = Categorical(log_probs)
        action = action_dist.sample()
        log_prob = action_dist.log_prob(action)  # (1,); a un grad
        state, r, done = env.step(action.detach())
        action_log_probs.append(log_prob)
        rewards.append(r)
    action_log_probs = torch.stack(action_log_probs)
    # calculer le retour de la trajectoire ∑_{t=0}^T γ^t r_t
    R = 0.0
    for r in reversed(rewards):
        R = r + gamma * R
    # pour maximiser J, on minimise -J
    loss = -(action_log_probs.sum() * R)  # le gradient ne passe que par les log probs
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

À un haut niveau, cela va

  • Effectuer un déploiement (rollout) en utilisant la politique actuelle
  • Calculer la vraisemblance de ce déploiement
  • Maximiser R fois cette probabilité
    • Si R est positif, augmenter la vraisemblance de la trajectoire
    • Si R est négatif, diminuer la vraisemblance de la trajectoire

Résultats

Récompenses moyennes sur 100 exécutions, chacune durant 1000 itérations.

Les résultats sont… assez mauvais. En plissant très fort les yeux, la courbe a à peine une tendance à la hausse — mais c’est toujours nul. Il y a quelques problèmes évidents avec cet algorithme. Premièrement, cet environnement ne donne que des récompenses positives, donc est toujours positif. Cela signifie que peu importe la médiocrité de la trajectoire lors d’une itération, l’algorithme augmentera toujours sa probabilité. La seule différence que font les récompenses est l’amplitude de l’augmentation. Deuxièmement, notre attribution de crédit est au niveau de la séquence. Chaque action d’une trajectoire est récompensée de manière égale en fonction du succès de la trajectoire entière, donc l’algorithme manque d’un contrôle fin sur les choix d’actions.

Concentrons-nous d’abord sur le problème d’attribution de crédit.

REINFORCE

Au lieu de récompenser chaque action dans avec , nous pouvons plutôt attribuer des récompenses au niveau des pas de temps à chaque action . En revenant à notre estimation du gradient, nous pouvons la décomposer par pas de temps

Pourquoi le second terme est-il nul ? Intuitivement, c’est parce que les récompenses du passé ( pour ) sont effectivement des constantes au temps . Une fois ceci pris en compte, l’espérance s’évalue proprement à zéro. Soit l’historique jusqu’au temps . Le second terme est équivalent à

Nous remarquons que l’espérance intérieure se réduit à 0

Et il en va de même pour l’expression entière

Ainsi, nous voyons que l’équation 4. est égale à

Bon, qu’est-ce que cela nous dit ? “Le gradient de la politique est préservé sous une attribution du crédit au niveau des pas si nous utilisons la récompense future, ”. Cela nous permet d’attribuer le crédit de manière plus fine sans compromettre la justesse. Cet algorithme est connu sous le nom de REINFORCE ( Williams, 1992 Williams (1992) Simple Statistical Gradient-Following Algorithms for Connectionist Reinforcement LearningMachine Learning ) .

Implémentation

Nous n’avons besoin de modifier que quelques lignes de code par rapport à notre implémentation PG de base :

policy = Policy()
optimizer = optim.Adam(policy.parameters(), lr=alpha)

for iteration in range(num_iterations):
    rewards, action_log_probs = [], []
    state = env.reset()
    done = False
    while not done:
        log_probs = policy(state)
        action_dist = Categorical(log_probs)
        action = action_dist.sample()
        log_prob = action_dist.log_prob(action)
        state, r, done = env.step(action.detach())
        action_log_probs.append(log_prob)
        rewards.append(r)
    action_log_probs = torch.stack(action_log_probs)
    G = torch.zeros_like(action_log_probs)
    R = 0.0
    for r in reversed(rewards):
    for t, r in reversed(enumerate(rewards)):
        R = r + gamma * R
        G[t] = R
    loss = -(action_log_probs.sum() * R)
    loss = -(action_log_probs * G).sum()
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

Résultats

Cette simple modification semble apporter une amélioration significative des performances.

Récompenses moyennes sur 100 exécutions, chacune durant 1000 itérations.

Cela montre que l’attribution du crédit est importante ! Cependant, nous sommes encore loin de résoudre CartPole, ce qui se produit si nous maintenons le pôle en équilibre pendant 500 pas de temps, ou de manière équivalente si nous recevons 500 récompenses de trajectoire. La dernière amélioration dont nous avons besoin concerne les lignes de base.

Lignes de base

Si nous revenons en arrière, le deuxième problème que nous avions était que la probabilité qu’une action soit prise était toujours augmentée. Notre algorithme n’a essentiellement aucun moyen de dire si une action est mauvaise, seulement de caractériser à quel point elle était bonne.

Une manière intuitive de déterminer si une action est bonne ou mauvaise est simplement de mesurer la performance de par rapport à une ligne de base. Une façon de faire cela est d’exécuter plusieurs déroulements à chaque itération, de prendre la moyenne de sur ces déroulements — appelons-la — et d’utiliser cette quantité comme ligne de base.

Ainsi, notre nouveau gradient de politique est

Il y a une mise en garde : notre estimation du gradient de politique sera biaisée si n’est pas indépendant de — il est facile de voir que serait un estimateur de , où est le nombre de déroulements. Nous devons donc utiliser la moyenne leave-one-out (LOO) comme ligne de base. Cela signifie que pour la trajectoire , nous ne considérons que le retour moyen de tous les autres déroulements. Ainsi, notre estimateur de Monte-Carlo devient

Autre mise en garde : chaque déroulement n’a pas nécessairement la même longueur, puisqu’ils échouent à des moments différents. La moyenne doit donc être calculée pour chaque à travers les déroulements si ce est valide. C’est plus facile à illustrer en code qu’en équations, donc je vais simplement le mettre ici.

env = gym.make_vec("CartPole-v1", num_envs=args.n_rollouts)
for iter in range(iterations):
    done_envs = np.zeros((N,), dtype=bool)
    rewards, log_probs_history, entropies, alive_masks_history = [], [], [], []
    done = False
    while not done:
        alive_mask = torch.as_tensor(~done_envs, dtype=torch.float32)
        state_tensor = torch.as_tensor(state, dtype=torch.float32)
        log_probs = policy.forward(state_tensor)
        dist = torch.distributions.Categorical(logits=log_probs)
        action = dist.sample()
        state, reward, terminated, truncated, info = env.step(action.numpy())
        rewards.append(torch.as_tensor(reward, dtype=torch.float32) * alive_mask)
        log_probs_history.append(dist.log_prob(action) * alive_mask)
        entropies.append(dist.entropy().detach())
        alive_masks_history.append(alive_mask)
        done_envs |= terminated | truncated
        done = np.all(done_envs)

    T = len(log_probs_history)
    action_log_probs = torch.stack(log_probs_history)  # (T, N)
    alive_masks = torch.stack(alive_masks_history)  # (T, N)
    G = torch.zeros_like(action_log_probs)
    running = torch.zeros(N, dtype=action_log_probs.dtype)
    rewards = torch.stack(rewards)  # (T, N)
    for t in reversed(range(T)):
        running = rewards[t, :] + gamma * running
        G[t, :] = running

    # Ligne de base moyenne leave-one-out pour une estimation non biaisée du gradient
    # La ligne de base de chaque déroulement exclut son propre retour pour maintenir l'indépendance
    alive_count = alive_masks.sum(dim=-1, keepdim=True)  # (T, 1)
    total_G = G.sum(dim=-1, keepdim=True)  # (T, 1)
    baseline = (total_G - G) / (alive_count - 1).clamp(min=1)
    A = G - baseline
    loss = -(action_log_probs * A).sum()

La majeure partie de la différence de code entre cette version et la précédente vient de l’utilisation de l’environnement vectorisé, qui nous oblige à suivre (un peu fastidieusement) l’état done de plusieurs trajectoires s’exécutant en parallèle.

Résultats

Récompense totale moyenne selon les méthodes. La zone ombrée représente l’écart interquartile.

La moyenne de base surpasse largement les deux autres ! Elle résout généralement la tâche (atteint 500 de récompense) en moins de 200 étapes. Notez également que si les graphiques précédents couvraient 1k étapes, celui-ci ne va que jusqu’à 250 !

Démonstration

Voici une stratégie entraînée avec cet algorithme, fonctionnant entièrement dans votre navigateur. Appuyez sur tab pour rivaliser avec elle.

500 +
click to start
h/l or / · space start · tab toggle human tap sides to control · tap center to start

Estimation d’Avantage Généralisée


Nos expériences montrent que l’ajout d’une ligne de base est crucial pour les performances. Cependant, notre méthode de calcul de cette ligne de base ne fonctionne pas nécessairement pour toutes les tâches. Pour déterminer le retour typique sur toutes les actions dans un état, notre méthode se contente de faire la moyenne des retours empiriques d’un groupe de déploiements. Cela peut être une estimation à variance élevée dans des tâches très aléatoires ou variables : le retour attendu n’est pas nécessairement une fonction de l’état .

Dans ce cas, nous pouvons envisager d’apprendre une fonction de Valeur comme un réseau de neurones qui est amorcé à partir des retours réels rencontrés pendant l’entraînement. C’est l’estimateur

qui nous indique les récompenses futures attendues étant donné que nous sommes dans l’état et que nous suivons la politique . Apprendre cette fonction exacte comme un réseau de neurones , cependant, n’est pas réalisable car évolue pendant l’entraînement. Il y aura donc toujours une erreur non nulle

Ainsi, nous échangeons un biais d’estimation accru (puisque nous abandonnons l’échantillonnage MC) contre une variance plus faible.

D’accord, donc si notre ligne de base est , quelle quantité devrions-nous optimiser ? Une option qui a du sens est l’avantage :

qui représente le retour attendu étant donné que nous avons pris l’action depuis l’état . nous indique à quel point prendre est censé être meilleur par rapport à l’action typique échantillonnée depuis .


La ligne de base moyenne est la plus simple de toutes. Cependant, l’idée qui la sous-tend est plus généralement représentée comme un avantage , qui nous indique combien de récompense supplémentaire est attendue en prenant l’action par rapport à l’action typique échantillonnée depuis .

Nous pouvons l’exprimer en termes de fonction

qui représente les récompenses futures attendues après avoir pris , et la fonction de valeur

—les récompenses futures attendues étant donné que nous sommes dans l’état . L’avantage est donné par

ce qui quantifie à quel point notre action choisie est meilleure que l’action typique sous la politique .

Actuellement, dans notre configuration, nous n’avons qu’un seul réseau . Pour

Références

  1. Schulman, J., Wolski, F., Dhariwal, P., Radford, A., and Klimov, O. (2017). Proximal Policy Optimization Algorithms. arXiv preprint arXiv:1707.06347. https://arxiv.org/abs/1707.06347.
  2. Bellman, R. (1957). A Markovian Decision Process. Journal of Mathematics and Mechanics. Vol. 6, No. 5, pp. 679-684. doi:10.1512/iumj.1957.6.56038. https://doi.org/10.1512/iumj.1957.6.56038.
  3. Sutton, R. S. and Barto, A. G. (2018). Reinforcement Learning: An Introduction. The MIT Press. https://mitpress.mit.edu/9780262039246/reinforcement-learning.
  4. Towers, M., Kwiatkowski, A., Terry, J., Balis, J. U., De Cola, G., Deleu, T., Goulão, M., Kallinteris, A., Krimmel, M., KG, A., Perez-Vicente, R., Pierré, A., Schulhoff, S., Tai, J. J., Tan, H., and Younis, O. G. (2024). Gymnasium: A Standard Interface for Reinforcement Learning Environments. arXiv preprint arXiv:2407.17032. doi:10.48550/arXiv.2407.17032. https://arxiv.org/abs/2407.17032.
  5. Williams, R. J. (1992). Simple Statistical Gradient-Following Algorithms for Connectionist Reinforcement Learning. Machine Learning. Vol. 8, No. 3-4, pp. 229-256. doi:10.1007/BF00992696. https://doi.org/10.1007/BF00992696.
Transparence IA

Codex CLI et Claude Code ont écrit l'implémentation de CartPole dans le navigateur et le code de traçage. Tout le reste est de moi.