Réalisation d'une IA jouant au jeu Mountain Car avec Q-Learning
Présentation du jeu Mountain Car

Le jeu Mountain Car est un environnement de type classic control disponible dans Gymnasium. Le but est simple :
- Une voiture doit atteindre le drapeau en haut de la colline droite (en prenant d'abord de l'élan sur la colline gauche).
- La voiture possède 3 choix de déplacement: accélérer vers la gauche, la droite ou ne pas accélérer.
- L’agent reçoit une récompense de -1 à chaque timestep (il n'y a aucune récompense positive).
Principe du Q-Learning
Le Q-Learning est un algorithme d’apprentissage par renforcement qui permet à un agent d’apprendre une politique optimale en interagissant avec son environnement. L’idée est d’apprendre une table de valeurs Q(s,a), où :
- Q(s,a) représente la valeur d’une action a dans un état s.
- L’agent met à jour cette table en utilisant la récompense immédiate et une estimation de la valeur future des états.
La formule de mise à jour de Q est la suivante :
- : taux d’apprentissage (learning rate)
- : facteur d’escompte (discount factor)
- r : récompense reçue après l’action
- s′ : nouvel état après l’action
Étapes pour réaliser l’IA
Initialisation de l’environnement et des paramètres
On commence par importer les bibliothèques nécessaires et initialiser l'environnement :
from collections import defaultdict
import gymnasium as gym
import numpy as np
import matplotlib.pyplot as plt
# Initialisation de l'environnement
env = gym.make("MountainCar-v0")
learning_rate = 0.1 # Taux d'apprentissage (alpha)
n_episodes = 10000 # Nombre d'épisodes
start_epsilon = 1.0 # Probabilité d'exploration (100% au début)
epsilon_decay = start_epsilon / (n_episodes / 2) # Décroissance de epsilon à chaque épisode
final_epsilon = 0.1 # Valeur minimale d'epsilon
discount_factor = 0.99 # Facteur d'escompte (gamma)
Discrétisation
Le problème de Mountain Car est que ses états sont continus (position et vitesse réelles). Mais le Q-Learning repose sur une table Q discrète : il faut donc convertir les valeurs continues en indices entiers.
Pour cela, on divise chaque dimension de l’espace des états en un certain nombre de bacs (bins).
Par exemple :
Si n_bins = (20, 16), cela signifie :
- La position est découpée en 20 intervalles
- La vitesse est découpée en 16 intervalles
- Cela donne une table Q de taille 20 * 16 * 3 (car 3 actions possibles)
Plus les intervalles sont nombreux :
- plus l’approximation de l’espace est fine, et donc on aura une meilleure précision,
- mais la table Q devient plus grande donc l'apprentissage prend plus de temps
def discretize_state(self, state):
ratios = (state - self.obs_low) / self.bin_width
new_state = np.clip((ratios).astype(int), 0, np.array(self.n_bins) - 1)
return tuple(new_state)
Boucle d’entraînement
Pour chaque épisode, l’agent interagit avec l’environnement en choisissant des actions selon une stratégie -gloutonne. À chaque étape, la table Q est mise à jour selon la formule du Q-Learning.
rewards_per_episode = [] # tableau pour stocker le reward de chaque épsiode (uniquement visuel)
for episode in range(n_episodes):
state, info = env.reset()
state = agent.discretize_state(state) # Discrétisation de l'état initial
done = False
total_reward = 0
while not done:
action = agent.get_action(state) # Stratégie epsilon-gloutonne
next_state, reward, terminated, truncated, info = env.step(action) # Exécution de l'action
next_state = agent.discretize_state(next_state) # Discrétisation
agent.update(state, action, reward, next_state, terminated) # Mise à jour de la table Q
state = next_state # Passage à l'état suivant
total_reward += reward
done = terminated or truncated
agent.decay_epsilon() # Décroissance de epsilon
rewards_per_episode.append(total_reward) # Ajout du reward de cet épisode
env.close()
# Visualisation de l'apprentissage sous forme de graphe:
window = 100
moving_avg = np.convolve(rewards_per_episode, np.ones(window) / window, mode="valid")
plt.plot(moving_avg)
plt.title("MountainCar - Moyenne des récompenses")
plt.xlabel("Épisodes")
plt.ylabel("Récompense moyenne")
plt.show()
Méthodes de l'agent
class MountainCarAgent:
def __init__(self, env: gym.Env,
learning_rate: float,
initial_epsilon: float,
epsilon_decay: float,
final_epsilon: float,
discount_factor: float,
n_bins=(20, 16)): # nombre d'intervalles de discrétisation
self.env = env
# Paramètres du Q-learning
self.lr = learning_rate
self.discount_factor = discount_factor
self.epsilon = initial_epsilon
self.epsilon_decay = epsilon_decay
self.final_epsilon = final_epsilon
self.training_error = []
# Discrétisation
self.n_bins = n_bins
self.obs_low = env.observation_space.low # vitesse et position minimale
self.obs_high = env.observation_space.high # vitesse et position maximale
self.bin_width = (self.obs_high - self.obs_low) / np.array(self.n_bins)
# Q-table stockée sous forme de dictionnaire
self.q_values = defaultdict(lambda: np.zeros(env.action_space.n))
def get_action(self, state) -> int: # Stratégie epsilon-gloutonne
if np.random.random() < self.epsilon:
return self.env.action_space.sample() # Exploration : action aléatoire
else:
return int(np.argmax(self.q_values[state])) # Exploitation : action avec la meilleure valeur Q
def update(self, state, action, reward, next_state, terminated): # Mise à jour de la table Q
future_q = (not terminated) * np.max(self.q_values[next_state])
target = reward + self.discount_factor * future_q
td_error = target - self.q_values[state][action]
self.q_values[state][action] += self.lr * td_error
self.training_error.append(td_error)
def decay_epsilon(self): # Décroissance de epsilon
self.epsilon = max(self.final_epsilon, self.epsilon - self.epsilon_decay)
Points à retenir
- Discrétisation : on découpe l’espace continu des états pour l’adapter au Q-Learning, le paramètre
n_bins: définit la résolution de cette discrétisation. - Exploration/Exploitation : au début, l’agent explore beaucoup ( grand), puis il exploite ce qu’il a appris.
- Récompenses : elles sont toujours négatives ce qui fait que l’agent apprend à atteindre le drapeau le plus vite possible.