Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Présentation de CliffWalking

Jeu 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é :

res cliffWalking

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.