Trouvez la politique optimale avec value iteration
Policy iteration trouve la politique optimale du lac gelé en quatre itérations seulement. Mais chacune de ces itérations contient une évaluation complète, avec ses dizaines de balayages. Faut-il vraiment attendre que l’évaluation ait convergé avant d’améliorer la politique ? Pas du tout, et c’est l’idée de value iteration : améliorer à chaque balayage.
Dans ce chapitre, vous allez construire cet algorithme, le comparer sans tricher avec policy iteration, puis faire le bilan complet du TP sur le lac gelé, y compris un résultat surprenant avec . Nous terminerons par les limites de la programmation dynamique, qui motivent toute la partie 2.
Une seule mise à jour pour évaluer et améliorer
Section intitulée « Une seule mise à jour pour évaluer et améliorer »Rappelez-vous l’équation de Bellman d’optimalité du chapitre 2. Value iteration la transforme directement en règle de mise à jour, exactement comme l’évaluation de politique le faisait avec l’équation d’espérance :
La seule différence avec l’évaluation, c’est le à la place de la moyenne . On peut la lire de deux façons :
- c’est un balayage d’évaluation où, dans chaque état, on suppose que l’agent prend la meilleure action au vu des valeurs actuelles ;
- c’est policy iteration où l’évaluation est tronquée à un seul balayage avant chaque amélioration.
On arrête quand le plus grand changement passe sous un seuil ( en TP). La politique n’apparaît nulle part pendant les itérations : on l’extrait une seule fois à la fin, en prenant l’action gloutonne par rapport à la valeur obtenue.
Pourquoi est-ce que ça converge vers ? Pour la même raison que l’évaluation : l’opérateur de Bellman d’optimalité est une contraction de rapport (en norme infinie, c’est-à-dire sur l’écart maximal entre deux états). Pour , il a un unique point fixe, , et les itérés s’en approchent géométriquement, quel que soit le point de départ.
import numpy as np
def value_iteration(env, gamma=0.9, theta=1e-6, max_balayages=1000): n_etats, n_actions = env.observation_space.n, env.action_space.n V = np.zeros(n_etats)
def valeurs_actions(s): q = np.zeros(n_actions) for a in range(n_actions): for p, s_suiv, r, termine in env.unwrapped.P[s][a]: q[a] += p * (r + gamma * V[s_suiv]) return q
for k in range(max_balayages): delta = 0.0 for s in range(n_etats): ancien = V[s] V[s] = valeurs_actions(s).max() # Bellman d'optimalité, en place delta = max(delta, abs(ancien - V[s])) if delta < theta: print(f"Convergence après {k + 1} balayages") break
# Extraction de la politique, une seule fois à la fin politique = np.zeros((n_etats, n_actions)) for s in range(n_etats): politique[s, np.argmax(valeurs_actions(s))] = 1.0 return politique, VPolicy iteration ou value iteration ?
Section intitulée « Policy iteration ou value iteration ? »En TP, sur le lac 8x8 non glissant avec , value iteration a convergé en 15 balayages. Policy iteration, elle, avait trouvé la politique optimale en 4 itérations. La réponse rédigée en TP concluait donc : « policy iteration converge plus vite, 4 itérations contre 15 ».
C’est un piège classique. Ces deux nombres ne mesurent pas la même chose ! Une itération de policy iteration contient une évaluation complète, et nous avons vu au chapitre 3 que ces évaluations avaient demandé 26, 15, 15 et 15 balayages. Le bon décompte est donc :
| Policy iteration | Value iteration | |
|---|---|---|
| Itérations « externes » | 4 | 15 |
| Balayages de tous les états | 26 + 15 + 15 + 15 = 71 | 15 |
| Améliorations de politique | 4 | 1 (l’extraction finale) |
Le coût réel d’un balayage est le même dans les deux cas : pour chaque état, parcourir chaque action et chaque transition. En balayages, value iteration est ici près de cinq fois moins coûteuse que policy iteration. La conclusion honnête n’est donc pas celle du TP.
Cela ne veut pas dire que value iteration gagne toujours. Quand est très proche de 1 et que l’espace d’états est grand, value iteration peut demander énormément de balayages, alors que policy iteration trouve la bonne politique en très peu d’améliorations. Le tableau suivant résume les deux approches.
| Policy iteration | Value iteration | |
|---|---|---|
| Équation utilisée | Bellman d’espérance, puis argmax | Bellman d’optimalité (max dans la mise à jour) |
| Nombre d’itérations externes | faible | plus élevé |
| Coût d’une itération | élevé (évaluation complète) | un seul balayage |
| Critère d’arrêt | la politique ne change plus | |
| Politique | disponible à chaque itération | extraite à la fin |
Entre les deux extrêmes, on peut évaluer une politique avec quelques balayages seulement avant de l’améliorer (on parle de policy iteration modifiée). Toutes ces variantes sont des cas de l’itération généralisée sur les politiques (generalized policy iteration) : évaluation et amélioration alternent, plus ou moins poussées, et se tirent mutuellement vers l’optimum. Ce schéma reviendra dans toute la partie 2.
Le bilan du TP sur le lac gelé
Section intitulée « Le bilan du TP sur le lac gelé »Voici les résultats du TP, pour les quatre valeurs de testées (lac 8x8 non glissant).
| Évaluation de la politique aléatoire | Policy iteration | Value iteration | |
|---|---|---|---|
| 0,8 | 17 balayages | 3 itérations (17 + 15 + 15 balayages) | 15 balayages |
| 0,9 | 26 balayages | 4 itérations (26 + 15 + 15 + 15) | 15 balayages |
| 0,95 | 37 balayages | 4 itérations (37 + 15 + 15 + 15) | 15 balayages |
| 1,0 | 71 balayages | plafond de 200 itérations atteint | 15 balayages |
Les deux méthodes donnent-elles la même politique ?
Section intitulée « Les deux méthodes donnent-elles la même politique ? »Oui : pour , policy iteration et value iteration renvoient la même politique. C’est attendu, puisque les deux convergent vers et en extraient une politique par le même argmax (qui prend la première action en cas d’égalité). En général, s’il y a plusieurs actions optimales dans un état, deux méthodes peuvent renvoyer des politiques différentes mais également optimales, avec la même fonction de valeur.
Voici la fonction de valeur optimale pour (valeurs recalculées et arrondies ; les trous et l’objectif valent 0) :
0.254 0.282 0.314 0.349 0.387 0.430 0.478 0.5310.282 0.314 0.349 0.387 0.430 0.478 0.531 0.5900.314 0.349 0.387 H 0.478 0.531 0.590 0.6560.349 0.387 0.430 0.478 0.531 H 0.656 0.7290.314 0.349 0.387 H 0.590 0.656 0.729 0.8100.282 H H 0.590 0.656 0.729 H 0.9000.314 H 0.478 0.531 H 0.810 H 1.0000.349 0.387 0.430 H 0.810 0.900 1.000 GChaque valeur est une puissance de 0,9 : une case à pas de l’objectif vaut . La case de départ, à 14 pas, vaut . La carte se lit comme une carte de distances à l’objectif, déformée par les trous.
Pourquoi value iteration converge-t-elle toujours en 15 balayages ?
Section intitulée « Pourquoi value iteration converge-t-elle toujours en 15 balayages ? »Le nombre de balayages de value iteration ne dépend pas de ici. Le lac est déterministe : la valeur d’une case est fixée dès que celle de la case suivante sur le meilleur chemin l’est. Comme au chapitre 3, l’information remonte depuis l’objectif d’environ une case par balayage, le chemin le plus long vers G fait 14 pas, et le 15e balayage constate que plus rien ne bouge. La valeur de change les nombres (0,9 puissance ou 0,8 puissance ), pas le nombre d’étapes pour les obtenir.
Et si l’on part de la politique optimale ?
Section intitulée « Et si l’on part de la politique optimale ? »En TP, relancer policy iteration à partir de la politique optimale a donné une seule itération (une évaluation de 15 balayages) : l’amélioration redonne la même politique, et l’algorithme s’arrête aussitôt. C’est rassurant, et c’est une propriété utile en pratique : si l’environnement change un peu, repartir de l’ancienne politique (démarrage à chaud, warm start) coûte beaucoup moins cher que repartir de zéro.
Le cas surprenant de γ = 1
Section intitulée « Le cas surprenant de γ = 1 »Avec , policy iteration n’a jamais déclaré la politique stable : le plafond de 200 itérations a été atteint, avec des évaluations de 3, 4 et 5 balayages qui se répétaient en boucle. Que s’est-il passé ?
En rejouant le calcul pas à pas, on comprend le mécanisme. Sans actualisation, atteindre l’objectif rapporte 1 quelle que soit la durée du trajet. Après la deuxième évaluation, toutes les cases depuis lesquelles G est accessible valent donc exactement 1. Mais alors, dans une telle case, pousser contre le mur ou reculer vaut aussi 1 : on reste sur une case qui vaut 1 ! Beaucoup d’actions sont à égalité, et np.argmax prend la première, « gauche ». La nouvelle politique va à gauche presque partout, se coince contre les bords ou tourne en rond, et n’atteint plus jamais l’objectif : sa valeur retombe à 0. L’amélioration suivante ne répare que quelques cases proches de G (les seules qui valent encore 1), et le même mécanisme se reproduit : les évaluations courtes de 3, 4 et 5 balayages s’expliquent ainsi, puisque presque toutes les valeurs sont nulles.
Value iteration n’est pas épargnée. Elle converge bien, en 15 balayages, vers la bonne valeur (). Mais si l’on déroule la politique qu’elle extrait avec , le robot parti de S reste bloqué et n’atteint jamais G : la valeur est juste, la politique extraite par argmax ne l’est pas.
Les limites de la programmation dynamique
Section intitulée « Les limites de la programmation dynamique »Dernière question du TP : pourquoi toutes ces méthodes exigent-elles de connaître et , et qu’est-ce que cela implique ?
Chaque mise à jour calcule une espérance exacte : . Sans les probabilités de transition, impossible de faire cette somme. Et l’amélioration de politique a besoin de comparer les actions via , donc de savoir où chaque action peut mener. D’où plusieurs limites :
- Le modèle est rarement connu. Un robot réel, un marché, un joueur humain : personne ne fournit les probabilités de transition. FrozenLake est un cas d’école où
env.unwrapped.Pdonne tout. - Chaque balayage parcourt tous les états. Pour chaque état et chaque action, il faut sommer sur les états suivants, soit jusqu’à opérations par balayage. C’est trivial pour 64 cases, impossible pour les positions d’un jeu de go (plus de ).
- La malédiction de la dimension. Si l’état est décrit par plusieurs variables, leur nombre de combinaisons explose. Un état continu (une position, une vitesse) doit être discrétisé, ce qui aggrave encore le problème.
Les réponses à ces limites structurent la suite du cours : apprendre à partir d’échantillons au lieu de calculer des espérances exactes (Monte-Carlo, différences temporelles, Q-learning), et approcher ou par une fonction paramétrée, typiquement un réseau de neurones (deep RL). Une troisième voie, l’apprentissage avec modèle (model-based RL), consiste à apprendre et à partir des interactions, puis à planifier avec le modèle appris.
En résumé
Section intitulée « En résumé »- Value iteration applique l’équation de Bellman d’optimalité balayage après balayage, puis extrait la politique optimale par un argmax final.
- Elle converge vers car l’opérateur d’optimalité est une contraction de rapport .
- En TP : 15 balayages pour value iteration, contre 4 itérations mais 71 balayages pour policy iteration ; il faut comparer en balayages, pas en itérations.
- Les deux méthodes ont donné la même politique ; partir de la politique optimale ne demande qu’une itération de policy iteration.
- Avec , les égalités entre actions ont fait échouer le critère d’arrêt de policy iteration (plafond de 200 itérations) : rien ne pousse l’agent à progresser.
- La programmation dynamique exige le modèle et un balayage de tous les états : elle ne passe pas à l’échelle, d’où les méthodes sans modèle et l’approximation de fonctions.
Exercices
Section intitulée « Exercices »Exercice · Compter le coût réel
facileÀ partir des résultats du TP, calculez le nombre total de balayages de policy iteration pour et pour , et comparez à value iteration. Qu’en concluez-vous ?
Voir la solution
- : balayages pour policy iteration, contre 15 pour value iteration (environ 3 fois moins).
- : balayages, contre 15 (plus de 5 fois moins).
Sur ce lac déterministe, value iteration est toujours la moins coûteuse en balayages, et l’écart grandit avec , parce que la première évaluation (celle de la politique aléatoire) devient de plus en plus longue.
En code
# Balayages des évaluations successives de policy iteration (résultats du TP)policy_iteration = {0.8: [17, 15, 15], 0.9: [26, 15, 15, 15], 0.95: [37, 15, 15, 15]}value_iteration = 15
for gamma, balayages in policy_iteration.items(): total = sum(balayages) print(gamma, total, round(total / value_iteration, 1))# 0.8 47 3.1# 0.9 71 4.7# 0.95 82 5.5# Balayages des évaluations successives de policy iteration (résultats du TP)policy_iteration <- list("0.8" = c(17, 15, 15), "0.9" = c(26, 15, 15, 15), "0.95" = c(37, 15, 15, 15))value_iteration <- 15
total <- sapply(policy_iteration, sum)rbind(total, rapport = round(total / value_iteration, 1))# 0.8 0.9 0.95# total 47.0 71.0 82.0# rapport 3.1 4.7 5.5Exercice · Value iteration sur un lac glissant miniature
moyenUn couloir A, B, G : depuis A, l’action « droite » mène en B avec probabilité 0,8 et laisse en A avec probabilité 0,2 ; l’action « rester » laisse en A. Depuis B, « droite » atteint G (récompense 1) avec probabilité 0,8 et laisse en B avec probabilité 0,2 ; « rester » laisse en B. On prend et on part de .
- Faites deux balayages synchrones de value iteration.
- Calculez la valeur exacte et en résolvant les équations de Bellman d’optimalité (en supposant que « droite » est optimale partout).
Voir la solution
- Balayage 1 : en A, droite vaut et rester vaut 0, donc ; en B, droite vaut , donc . Balayage 2 : en A, droite vaut ; en B, droite vaut . Donc .
- En B : , donc . En A : , donc . On vérifie que « rester » est moins bon : en B, il vaut . Avec l’aléa, la convergence n’est plus finie en quelques balayages : les valeurs s’approchent de la limite géométriquement.
En code
import numpy as np
gamma = 0.9
def balayage(V): """Un balayage synchrone de value iteration : renvoie V et Q (actions : droite, rester).""" VA, VB = V qA = [0.8 * gamma * VB + 0.2 * gamma * VA, gamma * VA] qB = [0.8 * 1 + 0.2 * gamma * VB, gamma * VB] # G terminal : valeur 0 return np.array([max(qA), max(qB)]), (qA, qB)
V = np.zeros(2)for k in range(1, 3): V, _ = balayage(V) print(k, V.round(3))# 1 [0. 0.8]# 2 [0.576 0.944]
for k in range(500): # on itère jusqu'à convergence V, (qA, qB) = balayage(V)print(V.round(3), round(qB[1], 3)) # [0.857 0.976], rester en B : 0.878print(round(0.8 / 0.82, 3), round(0.72 * (0.8 / 0.82) / 0.82, 3)) # 0.976 0.857 (formules exactes)gamma <- 0.9balayage <- function(V) { # actions : droite, rester ; G terminal de valeur 0 qA <- c(0.8 * gamma * V[2] + 0.2 * gamma * V[1], gamma * V[1]) qB <- c(0.8 * 1 + 0.2 * gamma * V[2], gamma * V[2]) list(V = c(max(qA), max(qB)), qB = qB)}
V <- c(0, 0)for (k in 1:2) { V <- balayage(V)$V; cat(k, round(V, 3), "\n") }# 1 0 0.8# 2 0.576 0.944
for (k in 1:500) { res <- balayage(V); V <- res$V } # jusqu'à convergenceround(V, 3); round(res$qB[2], 3) # 0.857 0.976 ; rester en B : 0.878round(c(0.72 * (0.8 / 0.82) / 0.82, 0.8 / 0.82), 3) # 0.857 0.976 (formules exactes)Exercice · Réparer policy iteration pour γ = 1
difficileReprenez la fonction amelioration_politique du chapitre 3. Modifiez-la pour qu’elle ne change l’action d’un état que si une autre action est strictement meilleure (de plus de ) que l’action actuelle. Expliquez pourquoi cette modification règle le problème observé avec .
Voir la solution
def amelioration_prudente(env, V, politique, gamma=1.0, tol=1e-9): n_etats, n_actions = env.observation_space.n, env.action_space.n nouvelle = politique.copy() for s in range(n_etats): q = np.zeros(n_actions) for a in range(n_actions): for p, s_suiv, r, termine in env.unwrapped.P[s][a]: q[a] += p * (r + gamma * V[s_suiv]) actuelle = np.argmax(politique[s]) # l'action (ou la plus probable) de la politique actuelle meilleure = np.argmax(q) if q[meilleure] > q[actuelle] + tol: # on ne change que si c'est strictement mieux nouvelle[s] = 0.0 nouvelle[s, meilleure] = 1.0 return nouvelle(Pour la toute première amélioration, qui part de la politique aléatoire, on peut garder la version d’origine afin d’obtenir une politique déterministe.)
Avec , le problème venait des égalités : dès qu’une politique atteint l’objectif depuis toutes les cases possibles, toutes ces cases valent 1, et np.argmax remplaçait l’action actuelle par « gauche », qui vaut aussi 1 sur le papier mais fait tourner en rond. Avec la version prudente, l’action actuelle, qui vaut déjà le maximum, est conservée : l’amélioration renvoie exactement la même politique et l’algorithme s’arrête. Le théorème d’amélioration reste valable, puisqu’on ne change une action que pour une action strictement meilleure.
En code
import numpy as np
# Couloir A (0), B (1), G (2, terminal), gamma = 1 ; actions 0 = gauche, 1 = droitesuivant = np.array([[0, 1], [0, 2]])recompense = np.array([[0.0, 0.0], [0.0, 1.0]])
def evaluer(pol, balayages=200): V = np.zeros(3) for _ in range(balayages): # évaluation en place (gamma = 1) for s in range(2): V[s] = recompense[s, pol[s]] + V[suivant[s, pol[s]]] return V
def policy_iteration(prudente, n_iter=7): pol = np.array([0, 0]) # résultat de la 1re amélioration depuis l'aléatoire historique = ["gg"] # (action en A, action en B), g = gauche, d = droite for _ in range(n_iter): Q = recompense + evaluer(pol)[suivant] nouvelle = Q.argmax(axis=1) # égalité : la première action (gauche) if prudente: # ne change que si c'est strictement mieux garder = Q.max(axis=1) <= Q[[0, 1], pol] + 1e-9 nouvelle = np.where(garder, pol, nouvelle) if np.array_equal(nouvelle, pol): return " -> ".join(historique) + " : stable" pol = nouvelle historique.append("".join("gd"[a] for a in pol)) return " -> ".join(historique) + " : jamais stable"
print(policy_iteration(prudente=False))# gg -> gd -> dd -> gg -> gd -> dd -> gg -> gd : jamais stableprint(policy_iteration(prudente=True))# gg -> gd -> dd : stable# Couloir A (1), B (2), G (3, terminal), gamma = 1 ; actions 1 = gauche, 2 = droitesuivant <- matrix(c(1, 2, 1, 3), nrow = 2, byrow = TRUE)recompense <- matrix(c(0, 0, 0, 1), nrow = 2, byrow = TRUE)
evaluer <- function(pol, balayages = 200) { V <- c(0, 0, 0) for (k in 1:balayages) for (s in 1:2) # évaluation en place (gamma = 1) V[s] <- recompense[s, pol[s]] + V[suivant[s, pol[s]]] V}
policy_iteration <- function(prudente, n_iter = 7) { pol <- c(1, 1) # résultat de la 1re amélioration depuis l'aléatoire historique <- "gg" # (action en A, action en B), g = gauche, d = droite for (k in 1:n_iter) { Q <- recompense + matrix(evaluer(pol)[suivant], 2) nouvelle <- apply(Q, 1, which.max) # égalité : la première action if (prudente) { # ne change que si strictement mieux garder <- apply(Q, 1, max) <= Q[cbind(1:2, pol)] + 1e-9 nouvelle[garder] <- pol[garder] } if (all(nouvelle == pol)) return(paste(paste(historique, collapse = " -> "), ": stable")) pol <- nouvelle historique <- c(historique, paste(c("g", "d")[pol], collapse = "")) } paste(paste(historique, collapse = " -> "), ": jamais stable")}
policy_iteration(prudente = FALSE)# gg -> gd -> dd -> gg -> gd -> dd -> gg -> gd : jamais stablepolicy_iteration(prudente = TRUE)# gg -> gd -> dd : stable