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

CartPole est un environnement de contrôle classique disponible dans Gymnasium.
Le but est simple :
- Un chariot peut se déplacer horizontalement.
- Un poteau est placé sur le chariot.
- L’agent doit maintenir le poteau en équilibre le plus longtemps possible.
- À chaque pas où le poteau tient, l’agent reçoit +1.
- L’épisode se termine si le poteau dépasse un certain angle ou si la limite de temps est atteinte (500 de reward).
L'objectif de l’agent est donc maximiser la durée de survie du poteau.
Principe du Q-Learning
On cherche à apprendre une fonction qui donne la valeur d’une action dans un état .
La mise à jour de la table Q est :
- : taux d’apprentissage
- : facteur d’escompte
- : récompense immédiate
- : nouvel état atteint après l’action
L'agent doit aussi résoudre le compromis exploration/exploitation :
- Avec probabilité , il choisit une action aléatoire (exploration).
- Sinon, il choisit l’action qui maximise (exploitation).
Discrétisation de l’espace d’état
L’environnement CartPole renvoie un état composé de 4 variables continues :
| Variable | Signification | Intervalle approximatif |
|---|---|---|
| cart_pos | Position du chariot | [-2.4, 2.4] |
| cart_vel | Vitesse du chariot | [-3.0, 3.0] |
| pole_angle | Angle du poteau | [-0.21, 0.21] |
| pole_vel | Vitesse angulaire du poteau | [-3.5, 3.5] |
Comme une table Q nécessite des états discrets, on découpe chaque dimension en bins :
self.bins = {
'cart_pos': np.linspace(-2.4, 2.4, 40),
'cart_vel': np.linspace(-3.0, 3.0, 40),
'pole_angle': np.linspace(-0.21, 0.21, 40),
'pole_vel': np.linspace(-3.5, 3.5, 40)
}
La fonction de discrétisation transforme l’état continu en tuple d’indices :
def discretize(self, state):
state = np.clip(state, [-2.4, -3.0, -0.21, -3.5], [2.4, 3.0, 0.21, 3.5])
cart_pos, cart_vel, pole_angle, pole_vel = state
b0 = np.digitize(cart_pos, self.bins['cart_pos'])
b1 = np.digitize(cart_vel, self.bins['cart_vel'])
b2 = np.digitize(pole_angle, self.bins['pole_angle'])
b3 = np.digitize(pole_vel, self.bins['pole_vel'])
return (b0, b1, b2, b3)
Stratégie -gloutonne
def choose_action(self, state, epsilon):
if random.uniform(0,1) < epsilon:
return random.randint(0, 1) # action aléatoire
else:
state_disc = self.discretize(state)
return np.argmax([self.get_q(state_disc, a) for a in [0,1]])
Mise à jour de la Q-table
def learn(self, s, a, r, s_next):
s = self.discretize(s)
s_next = self.discretize(s_next)
max_q_next = max([self.get_q(s_next, a_next) for a_next in [0,1]], default=0.0)
old_q = self.get_q(s,a)
new_q = old_q + self.alpha * (r + self.gamma * max_q_next - old_q)
self.q[(s,a)] = new_q
Code complet d’entraînement
import gymnasium as gym
import random, numpy as np
class Q_learning:
def __init__(self, alpha=0.1, gamma=0.9):
self.q = {}
self.alpha = alpha
self.gamma = gamma
self.bins = {
'cart_pos': np.linspace(-2.4, 2.4, 40),
'cart_vel': np.linspace(-3.0, 3.0, 40),
'pole_angle': np.linspace(-0.21, 0.21, 40),
'pole_vel': np.linspace(-3.5, 3.5, 40)
}
def get_q(self, s, a):
return self.q.get((s,a), 0.0)
def discretize(self, state):
state = np.clip(state, [-2.4, -3.0, -0.21, -3.5], [2.4, 3.0, 0.21, 3.5])
cart_pos, cart_vel, pole_angle, pole_vel = state
return (
np.digitize(cart_pos, self.bins['cart_pos']),
np.digitize(cart_vel, self.bins['cart_vel']),
np.digitize(pole_angle, self.bins['pole_angle']),
np.digitize(pole_vel, self.bins['pole_vel'])
)
def choose_action(self, state, epsilon):
if random.uniform(0,1) < epsilon:
return random.randint(0,1)
state_disc = self.discretize(state)
return np.argmax([self.get_q(state_disc, a) for a in [0,1]])
def learn(self, s, a, r, s_next):
s = self.discretize(s)
s_next = self.discretize(s_next)
max_q_next = max([self.get_q(s_next, a_next) for a_next in [0,1]])
old_q = self.get_q(s,a)
self.q[(s,a)] = old_q + self.alpha * (r + self.gamma * max_q_next - old_q)
env = gym.make("CartPole-v1")
AI = Q_learning()
epsilon = 1.0
epsilon_min = 0.01
epsilon_decay = 0.9995
n_episodes = 10000
for episode in range(n_episodes):
state, info = env.reset()
done = False
total_reward = 0
while not done:
action = AI.choose_action(state, epsilon)
next_state, reward, terminated, truncated, info = env.step(action)
done = terminated or truncated
AI.learn(state, action, reward, next_state)
state = next_state
total_reward += reward
if epsilon > epsilon_min:
epsilon *= epsilon_decay
Points à retenir
- Les états continus sont discrétisés pour permettre une table Q.
- commence élevé pour explorer, puis diminue pour exploiter.
- La récompense étant positive, l’agent apprend à maintenir l’équilibre le plus longtemps possible.