Aller au contenu

Apprenez à agir avec SARSA et Q-learning

Retour au lac gelé, mais cette fois sans tricher : le robot n’a plus accès à env.unwrapped.P. Il ne peut que faire env.step(action) et regarder ce qui arrive. Pourra-t-il quand même trouver le chemin de 14 pas que value iteration avait calculé en partie 1 ?

Oui, et ce chapitre montre comment. Vous avez tous les ingrédients : l’itération sur les politiques (chapitres 3 et 4), les différences temporelles (chapitre 5) et l’exploration ε-greedy (chapitre 6). Il ne reste qu’à les assembler. Le résultat, ce sont les deux algorithmes les plus célèbres de l’apprentissage par renforcement tabulaire : SARSA et Q-learning.

Le contrôle sans modèle : estimer Q, agir ε-greedy

Section intitulée « Le contrôle sans modèle : estimer Q, agir ε-greedy »

Le schéma général reste celui de l’itération généralisée sur les politiques : on alterne une étape d’évaluation et une étape d’amélioration. Deux changements le rendent utilisable sans modèle :

  1. On estime Q(s,a)Q(s, a) plutôt que V(s)V(s). L’amélioration consiste alors à prendre arg⁡max⁡aQ(s,a)\arg\max_a Q(s, a), ce qui ne demande pas de connaître PP.
  2. On agit avec une politique ε-greedy par rapport à QQ, pour que toutes les actions continuent d’être essayées.

Et plutôt que d’évaluer complètement une politique avant de l’améliorer, on fait les deux en même temps, à chaque pas : chaque mise à jour de QQ modifie aussitôt la politique ε-greedy qui en découle. C’est l’équivalent sans modèle de value iteration, qui améliorait à chaque balayage.

Toute la question est alors : vers quelle cible faut-il corriger Q(s,a)Q(s, a) après une transition ? Les deux algorithmes de ce chapitre ne diffèrent que sur ce point.

SARSA applique TD(0) à la fonction QQ. Après avoir fait l’action aa dans l’état ss, reçu rr et atteint s′s', l’agent choisit déjà son action suivante a′a' avec sa politique ε-greedy, puis met à jour :

Q(s,a)←Q(s,a)+α[ r+γ Q(s′,a′)−Q(s,a) ]Q(s, a) \leftarrow Q(s, a) + \alpha \Big[\, r + \gamma \, Q(s', a') - Q(s, a) \,\Big]
  • r+γQ(s′,a′)r + \gamma Q(s', a') est la cible : la récompense reçue, plus la valeur estimée de l’action réellement choisie ensuite ;
  • si s′s' est terminal, la cible se réduit à rr.

Le nom vient du quintuplet utilisé à chaque mise à jour : (s,a,r,s′,a′)(s, a, r, s', a'), soit State, Action, Reward, State, Action.

SARSA est dit on-policy (sur politique) : il évalue et améliore la politique qu’il suit réellement, exploration comprise. Si l’agent explore une fois sur dix, SARSA apprend la valeur d’un agent qui explore une fois sur dix.

Q-learning : apprendre la valeur de la meilleure conduite

Section intitulée « Q-learning : apprendre la valeur de la meilleure conduite »

Q-learning change un seul symbole dans la cible : au lieu de l’action réellement choisie ensuite, il prend la meilleure action possible dans s′s' :

Q(s,a)←Q(s,a)+α[ r+γmax⁡a′Q(s′,a′)−Q(s,a) ]Q(s, a) \leftarrow Q(s, a) + \alpha \Big[\, r + \gamma \max_{a'} Q(s', a') - Q(s, a) \,\Big]

Reconnaissez-vous la cible ? C’est l’équation de Bellman d’optimalité pour Q∗Q^* (chapitre 2), dans laquelle l’espérance sur s′s' est remplacée par un seul échantillon. Q-learning est donc une value iteration échantillonnée, sur QQ au lieu de VV.

Q-learning est dit off-policy (hors politique) : il apprend la valeur de la politique gloutonne, optimale, alors qu’il agit avec une autre politique (ε-greedy, qui explore). On distingue la politique de comportement, qui choisit les actions, et la politique cible, dont on apprend la valeur. Sous des conditions classiques (toutes les paires état-action visitées indéfiniment, taux d’apprentissage décroissant de façon appropriée), QQ converge vers Q∗Q^* quelle que soit la politique de comportement.

gauchebasdroitehaut0,00,10,20,30,40,50,60,70,20,60,40,1tirée(exploration)maxQ(s′, ·) dans l’état suivant0,10,20,30,40,50,6cible 0,18→ 0,468SARSAcible 0,54→ 0,504Q-learningQ(s, a) = 0,5mise à jour de Q(s, a), α = 0,1
Même transition, deux cibles : SARSA prend l'action réellement tirée (gauche, 0,2), Q-learning la meilleure (bas, 0,6). Avec α = 0,1, chacun ne fait qu'un dixième du chemin vers sa cible.

La différence entre les deux algorithmes se voit sur un exemple classique, la marche au bord de la falaise (cliff walking). Une grille, un départ et une arrivée séparés par une falaise ; chaque pas coûte −1-1, tomber coûte −100-100 et renvoie au départ. Le chemin le plus court longe le bord de la falaise.

  • Q-learning apprend ce chemin le plus court, car c’est la politique optimale si l’on n’explore plus. Mais pendant l’apprentissage, l’agent explore une fois sur dix, et un pas au hasard au bord du vide le fait tomber régulièrement. Ses récompenses pendant l’apprentissage sont donc mauvaises.
  • SARSA apprend la valeur de l’agent qui explore : il « sait » qu’un pas au hasard près du bord est dangereux, et il apprend un chemin plus sûr, un peu en retrait. Chemin plus long, mais moins de chutes pendant l’apprentissage.
falaise : −100 et retour au départDAchaque pas : −1Q-learning : le plus court, au bord du vide (13 pas)SARSA : plus long, en retrait (17 pas)
Chemins gloutons appris après 500 épisodes (ε = 0,1). Q-learning apprend le plus court, juste au-dessus du vide ; SARSA, qui tient compte de ses propres pas au hasard, passe loin du bord.
0100200300400500épisodes (ε = 0,1 fixe)−100−80−60−40−200somme des récompensespar épisode (moyenne sur 50)SARSA tombe rarementQ-learning tombe plus souventpendant l’apprentissageSARSAQ-learning
Pendant l'apprentissage, la politique « optimale » de Q-learning rapporte moins : un pas d'exploration au bord de la falaise coûte −100. SARSA, plus prudent, tombe rarement.

Aucun des deux n’est meilleur dans l’absolu. Si l’on n’a le droit de se tromper qu’en simulation et qu’on déploie ensuite une politique gloutonne, Q-learning vise la bonne cible. Si l’agent apprend en conditions réelles (un robot qui ne doit pas tomber), la prudence de SARSA est précieuse. Quand ε\varepsilon décroît vers 0, les deux convergent vers la politique optimale.

SARSAQ-learning
Cibler+γQ(s′,a′)r + \gamma Q(s', a'), a′a' choisier+γmax⁡a′Q(s′,a′)r + \gamma \max_{a'} Q(s', a')
Typeon-policyoff-policy
Ce qu’il apprendla valeur de la politique ε-greedy suivieQ∗Q^*, la valeur de la politique optimale
Comportement pendant l’apprentissageplus prudentplus risqué
Équation sous-jacenteBellman d’espéranceBellman d’optimalité

Voici Q-learning sur le lac 8x8 non glissant du TP, sans jamais consulter le modèle. L’agent ne voit que ce que lui renvoie env.step : le nouvel état, la récompense, et deux indicateurs de fin (épisode terminé dans un trou ou sur l’objectif, ou tronqué au bout de 200 pas).

import numpy as np
import gymnasium as gym
env = gym.make("FrozenLake-v1", map_name="8x8", is_slippery=False)
rng = np.random.default_rng(0)
n_etats, n_actions = env.observation_space.n, env.action_space.n
Q = np.zeros((n_etats, n_actions))
alpha, gamma, n_episodes = 0.1, 0.9, 5000
def epsilon_greedy(s, eps):
if rng.random() < eps:
return int(rng.integers(n_actions)) # exploration
meilleures = np.flatnonzero(Q[s] == Q[s].max())
return int(rng.choice(meilleures)) # égalités tranchées au hasard
for episode in range(n_episodes):
eps = max(0.05, 1 - episode / (0.8 * n_episodes)) # de 1 à 0,05
s, _ = env.reset()
termine = tronque = False
while not (termine or tronque):
a = epsilon_greedy(s, eps)
s_suiv, r, termine, tronque, _ = env.step(a)
cible = r if termine else r + gamma * Q[s_suiv].max() # Bellman d'optimalité échantillonnée
Q[s, a] += alpha * (cible - Q[s, a])
s = s_suiv
# On déroule la politique gloutonne apprise
s, _ = env.reset()
for pas in range(1, 101):
s, r, termine, tronque, _ = env.step(int(Q[s].argmax()))
if termine or tronque:
break
print(f"Fin après {pas} pas, récompense {r}, max Q(S, .) = {Q[0].max():.3f}")

Avec ces réglages, on s’attend à ce que l’agent trouve un chemin de 14 pas jusqu’à l’objectif, et à ce que max⁡aQ(S,a)\max_a Q(S, a) s’approche de 0,913≈0,2540{,}9^{13} \approx 0{,}254 : la valeur V∗(S)V^*(S) que value iteration calculait en partie 1 avec le modèle complet. Pour obtenir SARSA, il suffit de choisir l’action suivante avant la mise à jour et de remplacer Q[s_suiv].max() par Q[s_suiv, a_suiv]. SARSA trouve aussi le chemin, mais son estimation de Q(S,⋅)Q(S, \cdot) reste un peu plus basse, car elle tient compte des pas d’exploration qui font parfois tomber dans un trou.

02 0004 000épisodes0,00,20,40,60,81,0taux de succès (moyenne glissante)Q-learningSARSAε (de 1 à 0,05)02 0004 000épisodes0,000,050,100,150,200,250,30max Q(S, ·)0,2540,219V*(S) = 0,9¹³ ≈ 0,254
Le code du chapitre, rejoué sans gymnasium (graine 0). Rien ne se passe tant que l'objectif n'a pas été trouvé ; ensuite Q-learning retrouve exactement 0,9¹³ ≈ 0,254, et SARSA reste plus bas car il compte les pas d'exploration.

Trois détails de ce code comptent beaucoup :

  • Les égalités tranchées au hasard. Au début, toutes les valeurs sont nulles. Avec argmax, l’agent choisirait toujours « gauche » en exploitation et buterait contre le bord. Tirer au hasard parmi les meilleures actions évite ce blocage.
  • La décroissance de ε\varepsilon. La récompense est très rare (une seule case sur 64) : il faut beaucoup explorer au début pour la trouver une première fois, puis la propager.
  • La distinction terminé / tronqué. Si l’épisode est terminé, il n’y a pas de suite, et la cible est rr. S’il est seulement tronqué par la limite de temps, l’état suivant a une vraie valeur, et on continue à faire du bootstrap.

Sur le lac glissant, le même code fonctionne, mais l’apprentissage est beaucoup plus lent et bruité : une même action peut mener à trois cases différentes, et il faut de nombreuses visites de chaque paire (état, action) pour que QQ moyenne correctement ces issues. Un taux d’apprentissage plus faible et davantage d’épisodes deviennent nécessaires.

  • Le contrôle sans modèle estime Q(s,a)Q(s, a), agit ε-greedy par rapport à QQ et améliore la politique à chaque pas.
  • SARSA met à jour vers r+γQ(s′,a′)r + \gamma Q(s', a'), avec l’action a′a' réellement choisie : il est on-policy.
  • Q-learning met à jour vers r+γmax⁡a′Q(s′,a′)r + \gamma \max_{a'} Q(s', a') : il est off-policy et converge vers Q∗Q^* sous des conditions classiques.
  • Sur la falaise, Q-learning apprend le chemin optimal mais risqué, SARSA un chemin plus sûr qui tient compte de l’exploration.
  • Le max⁡\max de Q-learning surestime les valeurs bruitées ; le double Q-learning corrige ce biais.
  • Au lac gelé non glissant, Q-learning retrouve sans modèle le chemin de 14 pas et une valeur de départ proche de 0,254.

Exercice · Une mise à jour de chaque

facile

L’agent est en ss avec Q(s,a)=0,3Q(s, a) = 0{,}3. Il fait aa, reçoit r=1r = 1 et atteint l’objectif (état terminal). Calculez la nouvelle valeur de Q(s,a)Q(s, a) avec SARSA puis avec Q-learning (α=0,5\alpha = 0{,}5, γ=0,9\gamma = 0{,}9).

Voir la solution

L’état suivant est terminal : il n’y a pas d’action suivante, et les deux cibles se réduisent à r=1r = 1. Les deux algorithmes donnent la même mise à jour : Q(s,a)←0,3+0,5×(1−0,3)=0,65Q(s, a) \leftarrow 0{,}3 + 0{,}5 \times (1 - 0{,}3) = 0{,}65. SARSA et Q-learning ne diffèrent que par la façon d’estimer la suite, et ici il n’y a pas de suite.

En code

alpha, gamma = 0.5, 0.9
Q_sa, r = 0.3, 1.0
# État suivant terminal : pas de Q(s', a') ni de max, la cible se réduit à r
cible_sarsa = r
cible_qlearning = r
print(round(Q_sa + alpha * (cible_sarsa - Q_sa), 3), round(Q_sa + alpha * (cible_qlearning - Q_sa), 3)) # 0.65 0.65

Exercice · Expected SARSA

moyen

Une variante, Expected SARSA, remplace Q(s′,a′)Q(s', a') par l’espérance de Q(s′,⋅)Q(s', \cdot) sous la politique ε-greedy : ∑a′π(a′∣s′)Q(s′,a′)\sum_{a'} \pi(a' \mid s') Q(s', a'). Reprenez l’exemple du chapitre (Q(s,a)=0,5Q(s, a) = 0{,}5, r=0r = 0, Q(s′,⋅)=(0,2;0,6;0,4;0,1)Q(s', \cdot) = (0{,}2 ; 0{,}6 ; 0{,}4 ; 0{,}1), α=0,1\alpha = 0{,}1, γ=0,9\gamma = 0{,}9) avec ε=0,2\varepsilon = 0{,}2 et 4 actions. Calculez la mise à jour. Pourquoi cette variante a-t-elle moins de variance que SARSA ?

Voir la solution

La politique ε-greedy donne 0,850{,}85 à l’action gloutonne (bas) et 0,050{,}05 à chacune des trois autres. L’espérance vaut 0,05×0,2+0,85×0,6+0,05×0,4+0,05×0,1=0,5450{,}05 \times 0{,}2 + 0{,}85 \times 0{,}6 + 0{,}05 \times 0{,}4 + 0{,}05 \times 0{,}1 = 0{,}545.

Cible : 0+0,9×0,545=0,49050 + 0{,}9 \times 0{,}545 = 0{,}4905. Mise à jour : Q(s,a)←0,5+0,1×(0,4905−0,5)≈0,499Q(s, a) \leftarrow 0{,}5 + 0{,}1 \times (0{,}4905 - 0{,}5) \approx 0{,}499.

SARSA utilise l’action a′a' tirée au hasard : selon le tirage, sa cible vaut 0,18 ou 0,54 (ou 0,36, ou 0,09). Expected SARSA remplace ce tirage par sa moyenne exacte, qu’on sait calculer puisqu’on connaît sa propre politique. Une source d’aléa disparaît, donc la variance diminue. Remarquez qu’avec ε=0\varepsilon = 0, Expected SARSA devient exactement Q-learning.

En code

import numpy as np
alpha, gamma, eps = 0.1, 0.9, 0.2
Q_sa, r = 0.5, 0.0
Q_suiv = np.array([0.2, 0.6, 0.4, 0.1]) # gauche, bas, droite, haut
pi = np.full(4, eps / 4)
pi[np.argmax(Q_suiv)] += 1 - eps # [0.05 0.85 0.05 0.05]
esperance = pi @ Q_suiv
cible = r + gamma * esperance
print(round(esperance, 4), round(cible, 4), round(Q_sa + alpha * (cible - Q_sa), 4))
# 0.545 0.4905 0.499
# Les cibles possibles de SARSA selon l'action tirée, et leur dispersion
cibles_sarsa = r + gamma * Q_suiv
print(cibles_sarsa.round(3), round(pi @ cibles_sarsa, 4)) # [0.18 0.54 0.36 0.09], moyenne 0.4905
print(round(np.sqrt(pi @ (cibles_sarsa - cible) ** 2), 3)) # écart-type 0.126 (0 pour Expected SARSA)

Exercice · Diagnostiquer un agent bloqué

difficile

Un camarade lance Q-learning sur le lac 8x8 non glissant avec ε=0,01\varepsilon = 0{,}01 constant, QQ initialisée à 0 et une sélection de l’action gloutonne par np.argmax(Q[s]). Après 5 000 épisodes, QQ est encore entièrement nulle et l’agent n’a jamais atteint l’objectif. Expliquez pourquoi, et proposez trois corrections.

Voir la solution

Tant que l’objectif n’a jamais été atteint, toutes les récompenses sont nulles et QQ reste à 0. Avec des valeurs toutes égales, np.argmax renvoie toujours la première action, « gauche ». En exploitation (99 % du temps), l’agent va donc à gauche et bute contre le bord gauche, ou erre à peine. Les 1 % d’actions aléatoires ne suffisent pas à parcourir les 14 pas minimaux vers G, en évitant les trous, avant que l’épisode ne s’arrête (trou ou limite de 200 pas). La récompense n’est jamais découverte, donc rien ne s’apprend.

Corrections possibles :

  1. Explorer beaucoup plus au début : ε\varepsilon partant de 1 et décroissant progressivement, comme dans le code du chapitre.
  2. Trancher les égalités au hasard parmi les actions de valeur maximale, pour que l’exploitation ne se réduise pas à « toujours gauche ».
  3. Initialiser QQ de façon optimiste (par exemple à 1) : chaque action essayée qui ne rapporte rien voit sa valeur baisser, ce qui pousse l’agent vers les actions encore inexplorées.

On peut aussi remodeler la récompense (par exemple une petite pénalité par pas), mais cela change le problème et demande de la prudence.

En code

import numpy as np
CARTE = "SFFFFFFF" "FFFFFFFF" "FFFHFFFF" "FFFFFHFF" "FFFHFFFF" "FHHFFFHF" "FHFFHFHF" "FFFHFFFG"
DI, DJ = [0, 1, 0, -1], [-1, 0, 1, 0] # gauche, bas, droite, haut
def pas(s, a):
"""Lac 8x8 non glissant : renvoie (état suivant, récompense, terminé)."""
i, j = min(max(s // 8 + DI[a], 0), 7), min(max(s % 8 + DJ[a], 0), 7)
s2 = 8 * i + j
return s2, float(CARTE[s2] == "G"), CARTE[s2] in "GH"
def q_learning(eps_debut, egalites_au_hasard, q_init=0.0, n_episodes=5000, alpha=0.1, gamma=0.9):
rng, Q, succes = np.random.default_rng(0), np.full((64, 4), q_init), 0
for ep in range(n_episodes):
eps = max(0.01, eps_debut * (1 - ep / (0.8 * n_episodes))) # constant si eps_debut = 0.01
s = 0
for t in range(200): # troncature à 200 pas
if rng.random() < eps:
a = int(rng.integers(4))
elif egalites_au_hasard:
a = int(rng.choice(np.flatnonzero(Q[s] == Q[s].max())))
else:
a = int(np.argmax(Q[s])) # toujours « gauche » si égalité
s2, r, fini = pas(s, a)
Q[s, a] += alpha * ((r if fini else r + gamma * Q[s2].max()) - Q[s, a])
succes += r
s = s2
if fini:
break
return int(succes), round(float(Q[0].max()), 3) # nombre de succès, max Q(S, .)
print(q_learning(0.01, False)) # (0, 0.0) : jamais de succès, Q reste nulle
print(q_learning(1.0, True)) # (2622, 0.254) : corrections 1 et 2
print(q_learning(0.01, False, q_init=1.0)) # (4748, 0.254) : correction 3, Q optimiste