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

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

Présentation du jeu CartPole

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 :

VariableSignificationIntervalle approximatif
cart_posPosition du chariot[-2.4, 2.4]
cart_velVitesse du chariot[-3.0, 3.0]
pole_angleAngle du poteau[-0.21, 0.21]
pole_velVitesse 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.