Présentation de CliffWalking

Le jeu CliffWalking est un environnement classique de Gymnasium.
Le but du jeu est simple, la plateau de jeu est en 4x12, un agent doit partir d’un point de départ (case [3, 0])et atteindre l'arrivée (case [3, 11]) le plus rapidement possible tout en évitant de tomber de la falaise.
Objectifs :
Apprendre à atteindre l'arrivée le plus rapidement possible.
Éviter les cases du qui représentent la falaise car sinon l'agent recevra une récompense négative à la hauteur -100 et devra recommencer depuis son point de départ.
Chaque déplacement de l'agent coûte une pénalité de -1.
Principe du Q-Learning
Voir la partie sur Taxi
Structure du code
Pour le code, on commence avec l'importation des bibliothèques.
from collections import defaultdict
import gymnasium as gym
import numpy as np
from tqdm import tqdm
import matplotlib.pyplot as plt
Ensuite on créée une classe qui implémente le comportement de l’agent Q-Learning. Elle gère différentes choses :
-
la table Q qui enregistre les valeurs Q pour chaque état et action
-
le epsilon-greedy qui équilibre exploration et exploitation.
-
La mise à jour de la table Q selon la formule du Q-Learning.
-
la diminution progressive de epsilon, pour réduire l’exploration au fil du temps.
class CliffAgent:
def __init__(self,
env: gym.Env,
learning_rate: float,
initial_epsilon: float,
epsilon_decay: float,
final_epsilon: float,
discount_factor: float):
self.env = env
# Table Q initialisée à zéro pour chaque état/action
self.q_values = defaultdict(lambda: np.zeros(env.action_space.n))
self.lr = learning_rate
self.discount_factor = discount_factor
# Paramètres d’exploration
self.epsilon = initial_epsilon
self.epsilon_decay = epsilon_decay
self.final_epsilon = final_epsilon
# Historique de l’erreur de mise à jour
self.training_error = []
L’agent choisit soit une action aléatoire avec la probabilité de epsilon, on parle alors d'exploration sinon on parle d'exploitation c'est à dire que l'agent choisiral’action la plus optimisée selon sa table Q.
def get_action(self, state) -> int:
if np.random.random() < self.epsilon:
return self.env.action_space.sample() # Exploration
else:
return int(np.argmax(self.q_values[state])) # Exploitation
À chaque interaction avec l’environnement, la valeur Q(s,a) est mise à jour en fonction de la récompense reçue et de la valeur estimée du prochain état.
def update(self, state, action, reward, next_state, terminated):
# Estimation de la meilleure valeur future
future_q = (not terminated) * np.max(self.q_values[next_state])
# Cible de mise à jour (target)
target = reward + self.discount_factor * future_q
# Différence temporelle (TD error)
temporal_difference = target - self.q_values[state][action]
# Mise à jour de Q(s,a)
self.q_values[state][action] += self.lr * temporal_difference
# Sauvegarde de l’erreur pour analyse
self.training_error.append(temporal_difference)
Au fil des épisodes, le epsilon va diminuer pour que l’agent se fie davantage à ce qu’il a appris.
def decay_epsilon(self):
self.epsilon = max(self.final_epsilon, self.epsilon - self.epsilon_decay)
On initialise les derniers paramètres et on entraîne l’agent sur 6000 épisodes.
learning_rate = 0.01
n_episodes = 6000
start_epsilon = 1.0
epsilon_decay = start_epsilon / (n_episodes / 2)
final_epsilon = 0.1
discount_factor = 0.95
env = gym.make("CliffWalking-v1", render_mode="ansi")
agent = CliffAgent(
env=env,
learning_rate=learning_rate,
initial_epsilon=start_epsilon,
epsilon_decay=epsilon_decay,
final_epsilon=final_epsilon,
discount_factor=discount_factor
)
L’agent joue chaque épisode, met à jour ses valeurs Q, puis diminue epsilon. Chaque épisode correspond à une tentative complète pour atteindre la case d'arrivée.
rewards_per_episode = []
for episode in tqdm(range(n_episodes)):
state, info = env.reset()
done = False
total_reward = 0
while not done:
# Choix de l’action selon epsilon-greedy
action = agent.get_action(state)
# Exécution dans l’environnement
next_state, reward, terminated, truncated, info = env.step(action)
# Mise à jour des valeurs Q
agent.update(state, action, reward, next_state, terminated)
total_reward += reward
done = terminated or truncated
state = next_state
# Décroissance de epsilon à chaque épisode
agent.decay_epsilon()
rewards_per_episode.append(total_reward)
env.close()
Enfin pour évaluer la progression au cours des épisodes, on peut générer un graphique.
window = 100
moving_avg = np.convolve(rewards_per_episode, np.ones(window)/window, mode="valid")
plt.plot(moving_avg)
plt.title("CliffWalking - Moyenne des récompenses")
plt.xlabel("Épisodes")
plt.ylabel("Récompense moyenne")
plt.show()
Voici le graphique à la fin des épisodes qu'on a fixé :

On remarque bien l'évolution où l'agent va tomber énormément de fois de la falaise et faire des mouvements inutiles au début des épisodes car on a des récompenses de plus -5000, pour au final d'une manière exponentielle, l'agent tendra vers les -14 après au bout de 1000 épisodes.