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

SAE

Rapport de la SAE

Réalisé par Evan Nunes, Rostaing Damien, Mattéo T, Mattéo B, Hicham F, Paulo MP

IUT Montpellier-Sète × Université Montpellier

Tuteur / Client : Marin Bougeret & Victor Poupet

Année Universitaire 2025 – 2026

Du 29 Septembre au ?? Mai

Introduction

Dans le cadre de notre formation en BUT Informatique, nous avons choisi de nous consacrer à un projet ambitieux et stimulant : l’apprentissage par renforcement (“Reinforcement Learning”). Ce domaine de l’intelligence artificielle, en pleine expansion, permet à un agent d’apprendre à prendre des décisions optimales en interagissant avec un environnement dynamique. L’agent observe son environnement, agit, reçoit une récompense, et ajuste ses actions en conséquence, créant ainsi une boucle d’apprentissage autonome.

Notre projet, intitulé « Apprentissage par renforcement, application à l’écriture d’IA jouant à des jeux d’arcade », a pour objectif d’explorer les mécanismes fondamentaux du RL. Nous nous sommes fixés comme défi de concevoir et d’entraîner des agents capables de jouer à des jeux variés, allant de jeux simplistes — où le nombre d’états est limité — à des jeux plus complexes, comme des classiques d’arcade tels qu’Arkanoid ou Tetris, voire des jeux combinatoires. Ce travail est encadré par Marin Bougeret et Victor Poupet, et s’inscrit dans une démarche à la fois théorique et pratique.

Objectifs du projet

Ce projet vise plusieurs objectifs majeurs :

  • Comprendre et formaliser les algorithmes classiques d’apprentissage par renforcement : Nous avons étudié en profondeur les principes sous-jacents de ces algorithmes, leurs forces, leurs limites, et leurs propriétés mathématiques.
  • Découvrir les bonnes pratiques pour entraîner ces algorithmes : Cela inclut la définition des entrées adaptées pour l’agent, la conception de systèmes de récompenses efficaces, et l’optimisation des paramètres pour garantir une convergence rapide et robuste.
  • Explorer les arbres de recherche de Monte Carlo (MCTS) : Nous aborderons également les méthodes hybrides, comme le Monte Carlo Tree Search (MCTS), utilisé avec succès dans des applications.

Outils et méthodologie

Pour mener à bien ce projet, nous avons utilisé le langage Python et la bibliothèque Gymnasium. Cette librairie, spécialement conçue pour faciliter l’implémentation d’algorithmes de RL, nous a fourni les outils nécessaires pour modéliser des environnements, simuler des interactions, et évaluer les performances de nos agents. Bien que la maîtrise préalable de Python ne fût pas obligatoire, ce projet a été l’occasion d’approfondir nos compétences en programmation et en algorithmique.

Motivations

Ce sujet nous a particulièrement attirés pour son aspect pluridisciplinaire, mêlant algorithmes avancés, mathématiques appliquées, et expérimentation pratique. Il offre une opportunité unique de comprendre comment les machines peuvent apprendre à résoudre des tâches complexes de manière autonome, tout en développant des compétences techniques et analytiques essentielles dans le domaine de l’IA.

Structure du rapport

Ce rapport retrace notre démarche, depuis l’étude théorique des algorithmes de RL jusqu’à leur implémentation concrète. Nous y détaillons :

  1. Les concepts clés de l’apprentissage par renforcement.
  2. Les choix méthodologiques et techniques que nous avons effectués.
  3. Les résultats obtenus, ainsi que les défis rencontrés et les solutions apportées.
  4. Les perspectives d’amélioration et les pistes pour de futurs travaux.

Mode dev md-book

Note à savoir

le rapport md-book est disponible en ligne au lien donné

Docker (dev)

Dockerfile

FROM debian:trixie

WORKDIR /workspace

RUN apt update && apt install -y curl build-essential


RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"


RUN cargo install mdbook mdbook-katex

EXPOSE 8180

ENTRYPOINT ["/bin/bash", "-c", "mdbook serve -n 0.0.0.0 -p 8180"]

Build de la Dockerfile

docker build -t mdbook-live .

Création du containeur

docker run -it --rm -p 8180:8180-v lien/vers/git/sae:/workspace mdbook-live

lien si rien changé en local local

SI vous ne pouvez pas utiliser docker ou que vous avez envie de le faire manuellement

Linux:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env

la deuxième ligne permet de réactualiser l'env bash/zsh ou on est pour faire en sorte de ne pas avoir a redémarre le terminal

OU

je suppose (j'ai pas essayé mais ca doit marcher)

Debian based:

sudo apt install rustc cargo

Fedora based:

sudo dnf install rust cargo

Arch based:

sudo pacman -S rust

Après installation de rust

on va installer mdbook et un plugin pour prerender du Latex

après en allant dans le dossier de la SAE puis en rentrant dans le dossier mdbook

on fait

cargo install mdbook

puis

cargo install mdbook-katex

et enfin pour faire une serveur local pour le dev

mdbook serve --open

pour build:

mdbook build

Windows:

installer sur le site de rust le .exe en x64 et installer rust

ensuite installer le .exe mdbook ici installer mdbook-v0.4.52-x86_64-pc-windows-msvc.zip

glisser le .exe dans l'archive directement dans le dossier mdbook de la SAE

ensuite on installe le plugin ici il faut développer le v0.9.3-binaries et télécharger le mdbook-katex-v0.9.3-x86_64-pc-windows-gnu.zip et extraire le .exe au meme endroit que celui de mdbook

normalement si tout c'est bien fait les commandes sont les même que linux sauf que faut faire

.path\to\mdbook.exe ma_commande

Gestion des tâches

Ajouter une tâche

Tâches à faire

Titre Contenu Importance Date limite Actions

Tâches terminées

Titre Contenu Importance Date limite Actions
By Evan et Damien

Processus de Markov

théorique

Explication "global" du MP

Un processus de Markov est un processus stochastique dans lequel la prédiction du futur ne dépend que de l’état présent. Autrement dit, connaître les états passés ne rend pas la prédiction du futur plus précise : seul l’état actuel compte.

MP est défini par une fonction : S : Un nombre fini d'états P : La fonction de transition des probabilités

P : S -> [0, 1]

s, P(s,s') = 1

S : Un nombre fini d'états St : Un état défini par un temps t

pour simplifier, on a une fonction qui prend comme arguments s et s' et donne la probabilité de passer de l'état s à l'état s' on l'apelle Transition Probability Function.

La matrice de transition :

Si l’ensemble des états étant fini, on peut regrouper toutes les probabilités de transition dans une matrice de transition (P) :

Explication "détaillée" du MP

La chaîne de Markov (associé à un MP = (S, P)) est la séquence de variables aléatoires S0, S1, ..., St

1. Propriété de Markov

source: wikipédia (attention hyper complexe)

t 0,

Avant les explications, il est à noter qu'il y a plusieurs types de propriétés de Markov, chacune dans des ensembles différents ou avec des conditions différentes. Nous allons nous intéresser à la propriété de Markov dite "faible" (temps discret, espace discret) car c'est celle qui est la plus compréhensible et la plus utilisée.

On dit qu’un processus vérifie la propriété de Markov si :

En GROS le futur ne dépend que du présent, les états dans lesquels nous avons été sont inutiles et n'affectent rien.


2. Exemple

Par exemple, la météo :

Supposons :

  • S'il fait soleil aujourd’hui, il y a 50% de chances que demain il fasse encore soleil, 15% qu'il pleuve et 35% qu'il fasse nuageux.
  • S’il pleut, il y a 60% de chances que demain il pleuve encore, 25% qu'il y ait des orages et 15% qu'il y ait des nuages.
  • S'il il y a orages aujourd’hui, il y a 10% de chances que demain il fasse ensoleillé, 40% qu'il pleuve, 20% qu'il fasse encore orageux et 30% qu'il fasse nuageux.
  • S'il fait nuageux aujourd’hui, il y a 10% de chances que demain il fasse ensoleillé, 10% qu'il pleuve et 80% qu'il fasse encore nuageux.

Alors :

les états passés sont inutiles, seul ceux du présent comptent pour connaître les états futurs


3. Représentation par un graphe orienté

On peut représenter la chaîne sous forme de graphe où :

  • les nœuds = les états
  • les arcs = les transitions
  • les poids = probabilités de transition

L'exemple précedent donnerais alors :

Graphe_Orienté_MP


4. État stationnaire

Un processus de Markov converge parfois vers une distribution stationnaire = , ..., est telle que :

Une distribution stationnaire est une distribution de probabilités sur les états qui reste inchangée au fil du temps.

Informellement, au départ l’état dépend de là où on démarre, mais à force de répétition les probabilités s'affinent et arrivent à un état de distribution stable


Conclusion

Un processus de Markov est donc un modèle où l’évolution ne dépend que de l'état actuel. Les probabilités de transition peuvent être regroupées dans une matrice et permettent d’étudier le comportement du système à long terme.

Markov Reward Process (MRP)

Un Markov Reward Process (MRP) est un processus de Markov avec une fonction qui indique combien on gagne (ou perd) à chaque transition.


Définition

Un MRP est défini par le quadruplet :

ou (, ) est un MP

  • : récompense associée à l’état ou à la transition
  • : facteur d’actualisation (plus est proche de 1, plus les récompenses futures comptent)

Autrement dit :

Un MRP = Processus de Markov + Récompenses + Facteur d’actualisation.


Récompense

Etant donné un MRP :

  • = Valeur aléatoire = R( - 1, ), gain récolté en faisant la transition ->
  • par = + 1 + + ...

PAS FINIT


Valeur d’un état

La valeur d’un état mesure la récompense totale attendue à long terme, en partant de cet état :

  • = récompense immédiate obtenue en étant dans l’état
  • = qualité long terme de l’état

C’est ce qui indique si un état est bon ou mauvais à long terme. Ainsi, un état avec une grande valeur est considéré comme bon, car il mène en moyenne à beaucoup de récompenses futures.


Équation de Bellman

La fonction V vérifie l'équation de Bellman :

Cette équation exprime que :

  • on reçoit immédiatement
  • puis on se déplace vers un état futur
  • avec probabilité
  • tout en tenant compte de la valeur de cet état futur

Exemple

On a deux états :

Transitions :

  • Depuis Normal :

  • Depuis Bonus :

Récompenses :

Facteur d’actualisation :


Interprétation

  • L’état Bonus rapporte +5, mais ne dure pas.
  • Depuis Normal, on peut atteindre Bonus, mais ce n’est pas garanti.
  • La valeur permet de savoir si ça "vaut le coup" d’être dans un état.

Calcul de la valeur d'un MRP

Valeurs des variables sur les exemples :

P = np.array([[0.7, 0.3],
              [0.4, 0.6]])
R = np.array([5, 10])
gamma = 0.9

FORME VECTORIELLE

Fonction Bellman_Vector_Forme(P, R, gamma):

    n ← nombre de lignes de P

    I ← matrice identité de taille n × n

    A ← I - gamma × P

    A_inv ← inverse de A

    V ← A_inv × R

    Retourner V

Données :

Méthode vectorielle :

I une matrice d'identité donc :

donc :

Il faut maintenant calculer l’inverse de cette matrice, pour résoudre cela on applique cette formule :

FORME RECURSIVE

Fonction Bellman_Forme_Récursive(P, R, gamma):

    V ← vecteur nul de même taille que R
    seuil ← 0.0000001
    max_iterations ← 10000

    Pour k allant de 1 à max_iterations faire:

        V_nouveau ← R + gamma × P × V

        SI max(|V_nouveau − V|) < seuil alors:
            Sortir de la boucle
        FIN SI

        V ← V_nouveau
    FIN Pour

    Retourner V

Pour ( R, P ) et ( ), on prendra les mêmes données que la forme vectorielle.

Méthode récursive :

On commence la 1ʳᵉ itération avec un vecteur nul :


1ʳᵉ itération


2ᵉ itération


3ᵉ itération


On répète ces itérations jusqu’à atteindre un point de convergence lorsque la différence entre deux itérations devient minime.

Dans notre exemple, la convergence se fera à l’itération 172.


Itération 171 :

Itération 172 :


Donc :

Traces écrites :

bellman vector

bellman recursive


Conclusion

Un Markov Reward Process est :

  • une chaîne de Markov
  • avec une récompense
  • et une notion de gain à long terme via la valeur

C’est une étape essentielle entre :

  1. Chaînes de Markov (pas de choix)
  2. MDP (prise de décision et recherche de meilleure politique)

Markov Decision Process (MDP)

Un Markov Decision Process (MDP) est une extension du processus de Markov dans lequel un agent peut choisir une action à chaque étape.
Le passage d’un état à un autre ne dépend donc plus uniquement de l’état actuel, mais aussi de l’action effectuée.

L’idée global par rapport a MP :

À chaque état, l’agent choisit une action.
Cette action influence la probabilité des états futurs et la récompense qu’il reçoit.


Définition

Un MDP est défini par ces 5 variables :

ou (, ) est un MP

  • : probabilité de transition
  • : récompense immédiate recue en faisant l’action dans l’état
  • : facteur d’actualisation (favorise les récompenses immédiates si proche de 0, futures si proche de 1, en gros si on découvre ou si on joue avec nos connaissance)

Fonction de transition

La probabilité de transition dépend maintenant de l’action :

Pas comme le MP de base le MDP dépend de l'état et de la nouvelle variable l'action


Récompense

La récompense sert a savoir si une action étais bien ou pas:

Elle peut être positive (gain) ou négative (coût) ou neutre si elle n'a aucun importance.

Bien choisir ses récompenses fait change tout, si elles sont mal choisis cela peut mener a un mauvaise apprentissage (pour plus tard)


Politique

Une politique ou policy est une une fonction qui pour un état s nous dis quel action a effectuer :

ou (si stochastique) :


Valeur d’une politique

La valeur d’un état sous une politique est l’espérance des récompenses cumulées :

Cette formule mesure si dans longtemp nos action seront bien ou mal, (si le chemin est bon ou mauvais a long termes).


Objectif de l’agent

Trouver la meilleure politique telle que :

Autrement dit : choisir les actions permettant d’obtenir le maximum de récompenses cumulées.


Exemple

Le joueur peut être dans deux états :

À chaque tours, il a deux action possibles :

  • Tenter : on lance une pièce.
    • Si c’est face -> on gagne -> on va dans l’état Victoire
    • Si c’est pile -> on reste dans l’état Début
  • Abandonner : on tente rien, sans gagner -> on reste dans Début (et on gagne jamais)

Probabilités de transition

  • Depuis Début :

  • Depuis Victoire :

    • La partie est terminée (on reste en victoire ou on stop le jeu)

Récompenses

On gagne 1 point seulement quand on atteint Victoire :

Et :

La meilleure politique est donc (je crois) :

car c’est la seul qui permet d’obtenir une récompense.


MDP + politique

Il est possible d'obtenir un Markov Reward Process (MRP) à partir d'un MDP, en lui ajoutant une politique. Le MRP correspondant possède les propriétés suivantes:


Exemple

Données


Calcul de

Pour


Pour


Résultat final


Calcul de


Résultat final


En code

Fonction Construire_MRP_Depuis_MDP(P, R, Politique):

    n_etats ← nombre d'états dans P
    n_actions ← nombre d'actions dans P

    P_MRP ← matrice n_etats × n_etats remplie de 0
    R_MRP ← vecteur de taille n_etats rempli de 0

    Pour chaque état s de 0 à n_etats−1 faire:
        Pour chaque action a de 0 à n_actions−1 faire:

            P_MRP[s] ← P_MRP[s] + Politique[s][a] × P[s][a]

            R_MRP[s] ← R_MRP[s] + Politique[s][a] × R[s][a]
        FIN Pour
    FIN Pour
    Retourner P_MRP, R_MRP



Conclusion

Un MDP est donc un modèle qui fait une prise de décision étape par étape.
en gros il ajoute au MP de base :

  • Le choix d’actions
  • La récompense
  • La notion de politique optimale

Traces écrites :

MRP à partir d'un MDP

Algorithme

Policy Improvement

Définition de l’environnement

Valeurs des variables sur l’exemple :

states = ["North", "South", "East", "West"]
actions = ["clock", "anti-clock", "stay"]
gamma = 0.9  
horizon = 10  # noté H dans les diapositives

États et actions possibles

On considère un cercle découpé en 4 directions principales :
Nord, Est, Sud, Ouest, avec 3 actions possibles :

  • clock (sens horaire)
  • anti-clock (sens antihoraire)
  • stay (rester sur place)

Chaque action entraîne une transition déterministe vers un nouvel état :

Probabilités de transition

Récompenses associées ( R(s,a) )


Étape 1 — Politique initiale

On commence avec une politique arbitraire (non optimale) :


Étape 2 — Évaluation de la politique

On estime la valeur des états sous la politique courante :

L’algorithme procède par itérations successives (approche approchée sur un horizon (H = 10)) :

V = {s: 0 for s in states}
for _ in range(horizon):
    new_V = V.copy()
    for s in states:
        a = policy[s]
        new_V[s] = R[(s,a)] + gamma * sum(P[(s,a)][s2] * V[s2] for s2 in P[(s,a)])
    V = new_V

À la fin de cette boucle, on obtient une estimation stable de (V^{\pi}).


Étape 3 — Amélioration de la politique

Pour chaque état (s), on calcule la valeur d’action (Q(s,a)) :

et on met à jour la politique en choisissant l’action optimale :

Code correspondant :

for s in states:
    Q_values = {}
    for a in actions:
        Q_values[a] = R[(s,a)] + gamma * sum(P[(s,a)][s2] * V[s2] for s2 in P[(s,a)])
    policy[s] = max(Q_values, key=Q_values.get)

Étape 4 — Vérification de la stabilité

Si la politique ne change plus , on a atteint une politique optimale :

Sinon, on retourne à l’étape d’évaluation et on répète jusqu’à convergence.


Résultat final

Après convergence :

et les valeurs d’état associées maximisent les retours attendus sous cette politique optimale.


Formule générale de la Policy Iteration

Évaluation :

Amélioration :

On alterne ces deux étapes jusqu’à convergence.

Value Improvement

Définition de l’environnement

On considère le même environnement que pour les autres méthodes, afin de permettre une comparaison cohérente :

states = ["North", "South", "East", "West"]
actions = ["clock", "anti-clock", "stay"]
gamma = 0.9  
horizon = 10

Définition de Value Improvement

Value Improvement est un algorithme utilisé pour calculer la meilleure policy dans un MDP. Il sert à déterminer quelle est la meilleure action à faire dans chaque état pour maximiser la récompense cumulée.

Pour cela Value Improvement va appliquer de manière répétée l’équation de Bellman optimale :

Une fois les valeurs optimales trouvées, on en déduit la politique optimale :


Étape 1 — Initialisation

On commence avec une valeur d’état nulle pour tous les états :

Pour chaque état s :
    V[s] ← 0

Étape 2 — Itération de la valeur

Pour chaque état, on applique Bellman Optimality :

(Note : transitions déterministes → un seul s’ possible)

Pour iteration = 1 à H :
    Pour chaque état s :
        valeurs_actions = liste vide
        
        Pour chaque action a :
            s' = P(s, a)
            q = R(s,a) + gamma * V[s']
            ajouter q à valeurs_actions
        
        V_temp[s] = max(valeurs_actions)
    
    V = V_temp

À la fin, on obtient une estimation de .


Étape 3 — Extraction de la politique optimale

Une fois que les valeurs des états sont stabilisées, on choisit pour chaque état l’action qui maximise la valeur attendue :

Pour chaque état s :
    meilleur_score = -∞
    meilleure_action = null
    
    Pour chaque action a :
        s' = P(s, a)
        q = R(s,a) + gamma * V[s']
        
        Si q > meilleur_score :
            meilleur_score = q
            meilleure_action = a
    
    policy[s] = meilleure_action

Résultat final attendu

Après convergence, Value Improvement retourne :

  • La politique optimale
  • Les valeurs optimales d’état

Différence avec Policy Evaluation / Policy Improvement

MéthodeObjectifDépend d’une politique ?Type
Policy EvaluationÉvaluer OuiEstimation
Policy ImprovementAméliorer OuiOptimisation locale

Policy Evaluation

Qu’est-ce que la policy evaluation ?

La policy evaluation consiste à calculer la valeur d’une politique donnée.

Informellement : si l’agent suit cette politique, quelle récompense moyenne peut-il espérer ?

On cherche donc à calculer la fonction de valeur :

  • : la policy/politique
  • : la valeur d’un état en suivant la politique .
  • Optionnellement : : la valeur d’état-action.

Pourquoi évaluer une politique ?

Avant de vouloir améliorer une politique (policy improvement), il faut savoir si elle est bonne.

La policy evaluation permet :

  • d’estimer combien la politique rapporte en moyenne ;
  • d’identifier les états bons/mauvais ;

Équations de Bellman

La valeur d’un état sous la politique est définie par :

Ce que ça veut dire :

  • On suit ce que la politique dit de faire ().
  • On regarde les transitions possibles.
  • On ajoute la récompense immédiate + la valeur future.

Méthodes pour faire la policy evaluation

1 Approche exacte : Iterative Policy Evaluation

On applique l’équation de Bellman en boucle jusqu’à convergence.

Pseudo-code :

initialiser V(s) = 0 pour tout s

répéter
    pour chaque état s :
        V_new(s) = somme_a π(a|s) * somme_s' P(s'|s,a) * [R(s,a,s') + γ V(s')]
    remplacer V par V_new
jusqu’à convergence

2 Monte-Carlo Policy Evaluation

On fait plusieurs épisodes :

  • on suit la politique ,
  • on regarde le retour observé,
  • on moyenne les retours pour estimer V.

Adapté lorsque le modèle (transition, récompenses) n’est pas connu.


Résumé simple

TermeSignification
Politique (π)Règles de décision de l’agent
Policy EvaluationCalculer combien rapporte cette politique
MéthodesBellman, Monte-Carlo
UtilitéBase de la policy iteration et de l'amélioration des politiques

Exemple

1) Rappel (formule utilisée — horizon fini)

On utilise la version horizon fini (avec discount) : si est la valeur avec h étapes restantes et ,

est l’état déterministe résultant de .

Paramètres utilisés :

  • états = {North, East, South, West}
  • actions = {clock, anti-clock, stay}
  • = 0.9
  • horizon (H = 10)
  • politique (uniforme)

2) Exemple de calcul manuel (pour h=1, état North)

Pour vérifier la formule :

  • transitions depuis North :

    • clock → East, (R=2)
    • anti-clock → West, (R=2)
    • stay → North, (R=-2)

Avec :

(Ça colle avec les calculs programmés ci-dessous.)


3) Résultats (valeurs pour h = 0..10)

hNorthEastSouthWest
00.00000.00000.00000.0000
10.66674.66671.66673.3333
23.26676.76674.56675.0333
35.18679.04676.57677.1933
47.094710.90978.51179.0203
58.774112.621510.199210.7213
610.236814.188111.647912.2011
711.503215.628812.876913.4774
812.909016.754914.337514.8503
914.020917.867115.449515.9624
1015.021818.867916.450416.9632

La colonne h donne le nombre d’étapes restantes. La dernière ligne est donc (valeur attendue en suivant uniforme pendant 10 étapes).


4) Interprétation rapide

  • East a la valeur la plus élevée (≈ 18.868 à l’horizon 10) : c’est logique car depuis East il y a une action avec grosse récompense (anti-clock = 10) et en moyenne la politique explore cette action 1/3 du temps.
  • North a une valeur plus faible au départ (récompense de stay négative), mais en plusieurs étapes la valeur augmente car les transitions mènent à états plus rémunérateurs.
  • Les valeurs augmentent avec h (plus d’étapes = plus de récompenses accumulées, malgré le discount).

Monte Carlo Online Control on Policy improvement (MCOC on P-imp)

Qu'est ce que le Monte Carlo Online Control (MCOC) ?

Le Monte Carlo online control est un algorithme, qui à partir des résultats récoltés, améliore la politique de jeu actuelle, dans le but de trouver la meilleure possible.

Pourquoi utiliser le MCOC ?

Cet algorithme est très utile pour apprendre une politique optimale à partir des intéractions avec l'environnement, car il se base sur son expérience. Cet algorithme est simple et possède un équilibre entre exploration et exploitation, il explore la plus part du temps l'action la plus optimate, mais explore de temps en temps d'autres actions selon .

Réalisation de l'Algorithme

Pour réaliser cet algorithme, nous commençons par initialiser les données suivants : ou

  • s correspond à l'état
  • a correspond à l'action
  • Q (s, a) correspond à la valeur d'un état en effectuant une action, et en suivant la politique
  • N (s, a) correspond au nombre de visite
  • correspond aux taux d'explorations dans la politique -greedy
  • correspond à la politique utilisée
  • k correspond au compteur d'épisode

Utilisation de l'algorithme sur CliffWalking-v1

Nous avons utilisé cet algorithme sur l'environnement gymnasium CliffWalking afin de trouver le chemin le plus optimale, et voici le résultat obtenu :

Code de l'algorithme

def MCOC(gamma, num_episodes):
    env = gym.make("CliffWalking-v1")
    num_states = env.observation_space.n
    num_actions = env.action_space.n

    k = 1
    epsilon = 1

    #Q(s, a) & N(s, a) = 0 | Pour tout (s, a), eps = 1 / nb actions
    Q_values = np.zeros((num_states, num_actions))
    N_visits = np.zeros((num_states, num_actions)) # visites
    policy = np.ones((num_states, num_actions)) / num_actions


    #policy evaluation
    for g in range(1, num_episodes + 1):
        episodes = []
        print("epîsode : ", g)
        state, info = env.reset()

        while True:
            #choisir la meilleur action
            best_action = np.argmax(Q_values[state, :])

            #remplir le tab des proba
            for a in range(num_actions):
                if a == best_action:
                    policy[state, a] = 1 - epsilon + (epsilon / num_actions)
                else:
                    policy[state, a] = epsilon / num_actions

            #extraire les prob
            p = [policy[state, a] for a in range(num_actions)]
            p = np.array(p, dtype=float)
            p = p / p.sum()

            #choisir l'action selon p et la jouer
            action = np.random.choice(range(num_actions), p=p)
            next_state, reward, terminated, truncated, _ = env.step(action)
            # stockage echantillons
            episodes.append((state, action, reward))

            #sortir de la boucle si fin du jeu
            if (terminated or truncated):
                break

            state = next_state

        #calculer Gkt
        G = {}
        T = len(episodes)
        G[T-1] = episodes[T-1][2] #dernière recompense

        # Calculer g_kt
        for t in range(T - 2, -1, -1):
            G[t] = episodes[t][2] + gamma * G[t + 1]

        visited = set()
        for t, (state, action, reward) in enumerate(episodes):
            if (state, action) not in visited:
                N_visits[state, action] += 1
                Q_values[state, action] = Q_values[state, action] + (math.sqrt(1 / N_visits[state, action]) * (G[t] - Q_values[state, action]))

        #policy improvement
        k = k + 1
        epsilon = 1 / k

        for state in range(num_states):
            best_action = np.argmax(Q_values[state, :])

            # remplir le tab des proba
            for a in range(num_actions):
                if a == best_action:
                    policy[state, a] = 1 - epsilon + (epsilon / num_actions)
                else:
                    policy[state, a] = epsilon / num_actions
        print(epsilon)
        print(datetime.datetime.now())

    return Q_values, policy

Données utilisées :

Les essais

  • Pour 500 000 essais : L'algorithme a réussi à trouver un chemin valide vers le cookie, mais le chemin n'est pas encore très optimale Nous avons obtenu -21 rewards.

cliffwalking_500K_episodes

Tableau des Q-values pour 500 000 essais

ÉtatHautBasDroiteGauche
0-9.99210801-8.49905365-9.96873511-9.97954181
1-9.97323781-9.91215445-8.33228183-9.99915935
2-10.00388677-10.23180185-10.07166236-9.73473535
3-9.99997101-7.45813417-10.91437617-9.95598325
4-10.08972844-10.69451803-7.17570464-9.48198106
5-10.62325605-14.82546777-11.90430301-9.42308069
6-11.24479547-12.86744750-9.30914768-10.01729238
7-11.77483490-5.21703100-19.58074902-10.97093166
8-7.23376538-4.68559000-6.86972649-8.75980866
9-7.42512591-4.09510000-11.70929681-7.43962579
10-8.10874203-3.43900000-6.86184204-16.38296229
11-8.43373724-6.87076747-2.71000000-9.64044024
12-8.64914828-10.17597973-10.14655403-9.99706122
13-9.95544362-8.14697981-9.99285715-9.88178552
14-10.10210937-7.94108868-13.73108281-9.54514337
15-7.71232075-9.98579396-14.60393894-26.60352352
16-18.52465973-6.86189404-43.23296514-14.34072477
17-15.92786108-6.51321560-44.97597411-22.37019632
18-9.50219137-6.12579511-14.49257139-8.91879225
19-5.69532790-10.20272995-14.03531045-7.69182985
20-21.48923512-6.41501248-45.57956412-19.92503971
21-4.74245989-12.62295891-75.88020779-14.73487408
22-21.78871323-3.00659425-18.52295667-19.87208179
23-7.73863520-3.30384668-1.90000000-6.81267563
24-8.78423345-10.98565642-12.08450243-10.34463924
25-12.75062101-15.07860915-112.05585907-9.95740964
26-73.21164858-10.07111925-172.38504021-79.69196470
27-87.01499553-9.17163996-154.48678363-40.52259530
28-32.13884154-7.89704097-194.23900592-46.24976750
29-52.09242593-7.27913591-151.87316411-61.28637140
30-6.53033714-51.30627756-115.04817395-86.94765189
31-6.91409448-24.81664312-109.00000000-71.63522003
32-32.84144992-95.79782884-174.47402603-37.23675123
33-59.30309834-93.13388119-125.51040985-94.29982186
34-21.57773586-2.71339474-109.00000001-113.95936886
35-2.95814227-5.30958996-1.00000000-3.76921467
36-8.90581011-108.83108543-23.27851669-14.85203034
370.000000000.000000000.000000000.00000000
380.000000000.000000000.000000000.00000000
390.000000000.000000000.000000000.00000000
400.000000000.000000000.000000000.00000000
390.000000000.000000000.000000000.00000000
..........
470.000000000.000000000.000000000.00000000
  • Pour essais : L'algorithme a réussi à trouver une politique meilleure que celle d'avant, mais il n'a toujours pas trouver la plus optimale.

Nous avons obtenu -17 rewards, mieux que l'essai d'avant.

cliffwalking_1M500K_episodes

Tableau complet des Q-values pour 1 500 00 essai

ÉtatHautBasDroiteGauche
0-9.73968709-8.79530549-10.01206850-10.00210456
1-9.82760327-7.45813417-9.82045119-9.48656012
2-9.38771459-7.17570464-9.67018699-8.72685760
3-39.00742696-6.86189404-18.88764391-20.60904758
4-9.80729351-6.51321560-9.36848747-9.33764982
5-8.90235402-6.12579511-9.98311813-9.40489783
6-8.87090982-5.69532790-10.82553428-9.70654697
7-10.20017481-5.21703100-9.73340105-9.24997950
8-8.27001219-4.68559000-8.80989162-9.21368897
9-6.89449227-4.09510000-5.26835750-8.29645442
10-6.29605240-10.31025816-3.43900000-7.90454438
11-9.86702374-11.27842042-12.14744609-7.41845880
12-9.53854322-7.94108868-9.89780956-9.83118541
13-7.71232075-31.66444089-48.59003512-14.84989546
14-43.32729696-51.40595705-80.50679846-9.19894502
15-77.49439888-10.47056594-174.37853581-34.81151133
16-7.74044526-93.08833580-108.77052604-41.73735940
17-10.00705926-20.82933280-41.09802945-16.79272460
18-10.51242932-12.63048211-23.28944054-13.63519096
19-9.70343515-10.11398361-14.61795607-11.87211981
20-7.67016727-12.93871109-13.51868006-16.50237695
21-11.52459252-4.14465537-42.52443696-16.17919930
22-7.12802652-2.71000000-7.74301966-8.75302852
23-10.46136461-4.33880088-1.90000000-12.03576050
24-8.14697981-27.51830793-103.81940139-43.26027093
25-12.19593457-106.44048041-217.18284325-76.28859311
26-65.54268233-123.81896868-222.35297007-138.54435033
27-52.06961435-259.93566374-200.83775287-123.88062530
28-29.80513049-84.55516706-237.14257815-85.73754356
29-10.04178274-33.83151188-175.42188689-28.89546268
30-33.62342717-23.85200519-120.11647267-19.09340401
31-38.08828723-37.52484643-208.09527507-10.21000165
32-10.55714899-26.89897112-217.17444279-181.12780164
33-29.49168276-15.97045241-112.41843795-13.14863558
34-3.90615667-14.19701269-109.00000000-148.15154582
35-2.92966683-7.31796367-1.00000000-7.42077200
36-8.33228183-185.72414176-92.70514590-101.35436571
370.000000000.000000000.000000000.00000000
380.000000000.000000000.000000000.00000000
390.000000000.000000000.000000000.00000000
  • Pour essais : Cette fois-ci, ce nombre d'essai a donné le même résultat que pour 1 500 000 essais, avec egalement -17 rewards.

cliffwalking_3M_episodes

Tableau des Q-values pour 3 M

ÉtatHautBasDroiteGauche
0-10.00000000-9.99952035-9.78915549-10.00895063
1-10.40719630-10.22516870-10.30796138-9.88561573
2-9.96079924-9.97416406-9.81946060-9.94031704
3-10.33552644-10.26348156-12.40173589-9.93004583
4-9.98137921-9.81280882-9.32341053-10.05834800
5-9.99948833-10.04787892-8.61535552-10.04088156
6-9.83185884-10.68067133-13.30885043-9.66022277
7-10.51013447-10.57849359-7.35378941-11.82801515
8-7.21014527-4.68559000-9.25241037-9.70690467
9-15.95963822-4.09510000-24.83795002-7.76776130
10-18.73551441-6.58889350-3.43900000-9.54866684
11-10.25058886-7.87281481-4.46692319-10.21244381
12-9.93331604-7.94108868-9.95762312-9.96602376
13-10.14213617-7.71232075-10.93937212-9.74880369
14-9.99924587-7.45813417-18.82381697-10.26317073
15-9.89840874-7.17570464-14.26891926-9.94959006
16-9.41167345-6.86189404-9.26863322-11.73770607
17-10.41937483-6.51321560-10.23685069-8.84124320
18-12.92264311-6.12579511-17.69565613-9.37879884
19-10.48280678-5.69532790-10.96807833-11.21849028
20-5.21703100-6.77179924-13.21333476-17.06518644
21-15.06093399-4.49421692-59.95728545-14.62329667
22-7.69542262-2.71000000-7.76124538-5.97427829
23-8.04166036-6.16398826-1.90000000-4.25717948
24-8.14697981-12.38935829-13.34840030-10.01624639
25-9.98306595-14.90251466-124.8241587-14.37421629
26-17.41506340-21.72810194-117.09896139-35.60793951
27-9.65693692-52.98819085-145.01937378-40.67283392
28-76.45936165-8.48155211-164.45193539-71.52772704
29-8.11971076-84.91320547-185.11469793-31.70094808
30-10.72671977-26.89375654-116.83432829-14.51942861
31-8.43344453-54.13948058-109.04560678-34.61956956
32-7.20908545-58.31642906-112.24431514-73.03241194
33-42.18930564-32.19347870-165.70347789-66.62344412
34-66.35801093-1.91106281-199.17342723-99.10172950
35-5.79111997-1.90000000-1.00000000-4.66174354
36-8.33228183-140.49575488-31.65184903-22.42365944
370.000000000.000000000.000000000.00000000
380.000000000.000000000.000000000.00000000
390.000000000.000000000.000000000.00000000
400.000000000.000000000.000000000.00000000
410.000000000.000000000.000000000.00000000
420.000000000.000000000.000000000.00000000
430.000000000.000000000.000000000.00000000
440.000000000.000000000.000000000.00000000
450.000000000.000000000.000000000.00000000
460.000000000.000000000.000000000.00000000
470.000000000.000000000.000000000.00000000

Conclusion

Il faut effectuer encore des millions d'episodes afin que l'agent puisse tout explorer et trouver la meilleur politique possible, qui donnerait le schéma suivant :

cliffwalking_bp


Utilisation de l'algorithme sur Cartpole-v1

Pour utiliser l'algorithme sur cartpole-v1, Nous avons du discretiser l'espace des etats de l'environnement avec une intervalle de 12 car Cartpole possède des états continus, alors que le MCOC nécessite des états discret.

Cartpole-v1 nous fournit les informations suivant :

  • Position du chariot : de -4.8 à 4.8
  • Vitesse du chariot : -∞ à +∞
  • Angle du pôle : −0.418 rad à +0.418 rad
  • Vitesse angulaire du pôle : −∞ à +∞

Code de l'algorithme :

def create_bins(num_bins=12):
    return [
        np.linspace(-4.8, 4.8, num_bins),
        np.linspace(-5, 5, num_bins),
        np.linspace(-0.418, 0.418, num_bins),
        np.linspace(-5, 5, num_bins)
    ]

bins = create_bins()


def discretize(state):
    return tuple(int(np.digitize(s, b)) for s, b in zip(state, bins))


def MCOC(gamma, num_episodes):
    env = gym.make("CartPole-v1")

    # 6 bins par dimension → 6^4 = 1296 états
    num_states = 12 ** 4
    num_actions = env.action_space.n

    k = 1
    epsilon = 1

    Q_values = np.zeros((num_states, num_actions))
    N_visits = np.zeros((num_states, num_actions))
    policy = np.ones((num_states, num_actions)) / num_actions
    data = []

    
    for g in range(1, num_episodes + 1):
        episodes = []
        rewardepisode = 0

        state_cont, info = env.reset()
        state = get_index(discretize(state_cont))

        while True:
            # best action
            best_action = np.argmax(Q_values[state, :])

            # remplir proba
            for a in range(num_actions):
                if a == best_action:
                    policy[state, a] = 1 - epsilon + (epsilon / num_actions)
                else:
                    policy[state, a] = epsilon / num_actions

            p = policy[state]
            p = p / p.sum()

            action = np.random.choice(range(num_actions), p=p)
            next_state_cont, reward, terminated, truncated, _ = env.step(action)
            rewardepisode += reward
            data.append((g, rewardepisode))
            next_state = get_index(discretize(next_state_cont))
            episodes.append((state, action, reward))

            if terminated or truncated:
                break

            state = next_state

        
        G = {}
        T = len(episodes)
        G[T - 1] = episodes[T - 1][2]

        for t in range(T - 2, -1, -1):
            G[t] = episodes[t][2] + gamma * G[t + 1]

        visited = set()
        for t, (state, action, reward) in enumerate(episodes):
            if (state, action) not in visited:
                visited.add((state, action))
                N_visits[state, action] += 1
                alpha = math.sqrt(1 / N_visits[state, action])
                Q_values[state, action] += alpha * (G[t] - Q_values[state, action])

    
        k += 1
        epsilon = 1 / k

        for s in range(num_states):
            best_action = np.argmax(Q_values[s, :])
            for a in range(num_actions):
                if a == best_action:
                    policy[s, a] = 1 - epsilon + (epsilon / num_actions)
                else:
                    policy[s, a] = epsilon / num_actions

        print(g , rewardepisode)

    return Q_values, policy, data

Données utilisées :

Essai et Résultat

Nous avons effectué un test de 10 000 episodes sur cartpole avec cet algorithme, et cela fût un front succès. L'algorithme a atteint le reward maximal à atteindre (500), comme on peut le voir dans le graphique en dessous, l'algorithme a été constant, du 1000 ème episode jusqu'à la fin.

cartpole_10K

SARSA (State, Action, Reward, next-State, next-Action)

Qu'est ce que le SARSA ?

SARSA est un algorithme d’apprentissage par renforcement on-policy, c’est-à-dire qu’il met à jour sa fonction de state-value en suivant exactement la politique utilisée pour agir. Contrairement au Q-Learning (off-policy), SARSA apprend en évaluant les actions réellement choisies, même lorsqu'il s'agit d'un choix aléatoire (exploration).

L’acronyme signifie :

  • s : état actuel
  • a : action choisie
  • r : récompense reçue
  • s’ : état suivant
  • a’ : action suivante choisie par la même politique

La mise à jour est définie ainsi :


Pourquoi utiliser SARSA ?

SARSA est particulièrement intéressant pour :

  • apprendre à partir de l’expérience réelle dans l’environnement,
  • gérer naturellement l’exploration via -greedy,
  • conserver un comportement plus conservateur que Q-Learning (utile dans des environnements où des actions dangereuses mènent à des chutes rapides).

Comme il s’agit d’un algorithme on-policy, il évolue progressivement vers une politique sûre et performante tout en tenant compte des actions exploratoires.


Réalisation de l’algorithme

Dans cette implémentation, nous utilisons SARSA sur l’environnement CartPole-v1. Comme l’espace d’états est continu, nous devons le discrétiser avant l’apprentissage.

Variables initialisées :

  • pour tout couple état-action
  • nombre de visites des couples
  • time-step gobal (multi-épisodique)
  • alpha
  • apprentissage on-policy
  • : -greedy sur

Structure générale du SARSA :

  1. Observer
  2. Choisir selon -greedy
  3. Exécuter , recevoir et observer
  4. Choisir selon la même politique
  5. Mettre à jour :

  1. Passer à ,
  2. Boucler sur les étapes 3 à 6 jusqu’à la fin de l’épisode

Implémentation

  • Discrétisation en n_bins pour chaque variable de l’état
  • Mise à jour SARSA pas à pas
  • Exploration décroissante proportionnelle à
  • (pas obligatoire pour l'algorithme)

Code utilisé

import gymnasium as gym
import random
import numpy as np

class SARSA:
    def __init__(self, alpha=0.1, gamma=0.9, n_bins=10):
        self.q = {}
        self.alpha = alpha
        self.gamma = gamma
        self.bins = {
            'cart_pos': np.linspace(-2.4, 2.4, n_bins),
            'cart_vel': np.linspace(-3.0, 3.0, n_bins),
            'pole_angle': np.linspace(-0.21, 0.21, n_bins),
            'pole_vel': np.linspace(-3.5, 3.5, n_bins)
        }

    def get_q(self, s, a):
        return self.q.get((s, a), 0.0)

    def choose_action(self, state, epsilon):
        if random.random() < 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 discretize(self, state):
        state = np.clip(state, [-2.4, -3.0, -0.21, -3.5], [2.4, 3.0, 0.21, 3.5])
        b = [
            np.digitize(state[0], self.bins['cart_pos']),
            np.digitize(state[1], self.bins['cart_vel']),
            np.digitize(state[2], self.bins['pole_angle']),
            np.digitize(state[3], self.bins['pole_vel'])
        ]
        return tuple(b)

    def learn(self, s, a, r, s_next, a_next):
        s = self.discretize(s)
        s_next = self.discretize(s_next)
        q_next = self.get_q(s_next, a_next)
        old_q = self.get_q(s, a)
        self.q[(s, a)] = old_q + self.alpha * (r + self.gamma * q_next - old_q)


env = gym.make("CartPole-v1")

alpha = 0.1
gamma = 0.9
nb_bins = 10
agent = SARSA(alpha=alpha, gamma=gamma, n_bins=nb_bins)
epsilon = 1.0
epsilon_min = 0.05
t = 0

for episode in range(20000):
    state, _ = env.reset()
    action = agent.choose_action(state, epsilon)
    total = 0
    done = False

    while not done:
        t += 1

        if epsilon > epsilon_min:
            epsilon = max(1 / t, epsilon_min)

        next_state, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated
        next_action = agent.choose_action(next_state, epsilon)

        agent.learn(state, action, reward, next_state, next_action)

        state, action = next_state, next_action
        total += reward

    print(f"Episode {episode+1}, score: {total}, epsilon={epsilon:.2f}")

env.close()

Itérations avec plusieurs paramêtres

Avec = 1/t et constant

Avec les paramêtres "par défaut", les résultats obtenus sont assez faibles comparés au reward maximal de 500. C'est notamment dû au fait que diminue très vite.

premier_essaye

Remarque sur la discrétisation

Pour cet environnement, la "résolution" (le nombre de bins) idéal pour les variables d'état semble être entre 15 et 25. On utilisera donc 20 bins pour les prochains tests:

20_bins


Alpha Dynamique

Ensuite, on veut que le taux d'apprentissage général () évolue au fil des épisodes pour donner plus d'importance à la politique apprise au bout d'un certains temps (sinon on risque de stagner / être ralentit à cause des choix exploratoires).

alpha_dynamique

On peut observer ici que l'agent arrive un peu plus vite à des rewards "stables".


En utilisant N(s, a)

Maintenant, on a une phase majoritairement d'exploration, et une autre pour l'exploitation, mais on continue de diminuer nos chances d'explorer sans regarder si on a déjà une "bonne" politique pour l'état. Pour remédier à ça, on va modifier nos chances selon le nombre de visites de la paire :

Cette solution permet d'avoir une décroissance de l'exploration qui est spécifique à chaque paire .

avec_N


En utilisant la racine carrée de N(s, a)

Avec on permet à l'agent d'explorer un peu plus, ce qui affecte grandement les résulats. Comme on peut le voir ci-dessous, il est commun que l'agent s'approche de la politique optimale, mais cela dépend encore beaucoup de l'aléatoire durant les 2000 premiers épisodes.

avec_sqrt(N)_1

Meilleur essai sur 20 tentatives:

avec_sqrt(N)_2

Contre-exemple:

avec_sqrt(N)_2


Conclusion

En résumé, SARSA est un algorithme robuste mais fortement sensible à l’initialisation stochastique : les premiers épisodes jouent un rôle déterminant, et un bon mécanisme de gestion de l’exploration (comme une décroissance dépendant des visites) peut faire toute la différence entre un apprentissage lent et la convergence vers une politique optimale.

Logo Gymnasium

Nous avons utilisé en premier lieu une bibliothèque appelée Gymnasium pour développer et tester nos algorithmes d’apprentissage par renforcement. Gymnasium est une bibliothèque open-source spécialement conçue pour fournir des environnements standardisés permettant d’entraîner et d’évaluer des agents d’IA.

Avec Gymnasium, il est possible de :

  • Créer et manipuler des environnements variés, allant de jeux simples à des simulations plus complexes.
  • Tester facilement nos algorithmes grâce à une interface simple et unifiée.
  • Visualiser les résultats en temps réel, ce qui facilite le débogage et l’amélioration des modèles.

Cette bibliothèque nous a permis de nous concentrer sur la logique de nos agents, sans avoir à recoder les environnements de zéro. Elle est largement adoptée dans la communauté du Reinforcement Learning pour sa flexibilité et sa simplicité d’utilisation.

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

Présentation du jeu Taxi

Jeu du taxi

Le jeu Taxi est un environnement de type toy text disponible dans Gymnasium. Le but est simple :

  • Un taxi doit récupérer un passager à un emplacement donné, puis le déposer à une destination précise dans une grille.
  • Le taxi peut se déplacer dans quatre directions (nord, sud, est, ouest) et a la capacité de prendre ou déposer un passager.
  • L’agent reçoit des récompenses négatives à chaque action (pour encourager la rapidité) et une récompense positive lorsqu’il dépose le passager à la bonne destination.

Cet environnement est idéal pour débuter avec le Q-Learning, car il est suffisamment simple pour être compris, mais assez complexe pour illustrer les défis du Reinforcement Learning.

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 deux environnements :

  • Un pour l’entraînement (sans rendu visuel).
  • Un autre pour l’évaluation (avec rendu visuel).
import gymnasium as gym
import numpy as np

# Initialisation des environnements
env = gym.make("Taxi-v3")
eval_env = gym.make("Taxi-v3", render_mode="human")  # Pour visualiser l'agent en action

# Nombre d'états et d'actions possibles
state_space = env.observation_space.n  # 500 états possibles
action_space = env.action_space.n      # 6 actions possibles

# Initialisation de la table Q avec des zéros
Q_table = np.zeros((state_space, action_space))

# Hyperparamètres
alpha = 0.1        # Taux d'apprentissage
gamma = 0.5        # Facteur d'escompte (importance des récompenses futures)
epsilon = 1.0      # Probabilité d'exploration (100% au début)
epsilon_dim = 0.999 # Décroissance de epsilon à chaque épisode
epsilon_min = 0.001 # Valeur minimale de epsilon

Boucle d’entraînement

Pour chaque épisode, l’agent interagit avec l’environnement en choisissant des actions selon une stratégie -gloutonne :

  • Exploration : Avec une probabilité , l’agent choisit une action aléatoire.
  • Exploitation : Sinon, il choisit l’action avec la plus grande valeur Q pour l’état courant.

À chaque étape, la table Q est mise à jour selon la formule du Q-Learning.

for ep in range(10000):
    terminated = False
    truncated = False

    # Utilisation de l'environnement d'évaluation pour les 10 derniers épisodes
    if ep >= 9990:
        observation, info = eval_env.reset()
        current_env = eval_env
    else:
        observation, info = env.reset()
        current_env = env

    while not (terminated or truncated):
        # Stratégie epsilon-gloutonne
        if np.random.uniform(0, 1) < epsilon:
            # Exploration : action aléatoire
            action = current_env.action_space.sample()
        else:
            # Exploitation : action avec la meilleure valeur Q
            valid_actions = np.where(info["action_mask"] == 1)[0]  # Actions valides
            q_values = Q_table[observation, valid_actions]
            action = valid_actions[np.argmax(q_values)]

        # Exécution de l'action
        next_state, reward, terminated, truncated, info = current_env.step(action)
        print("Récompense :", reward)

        # Mise à jour de la table Q
        Q_table[observation, action] = (1 - alpha) * Q_table[observation, action] + alpha * (reward + gamma * np.max(Q_table[next_state]))

        # Passage à l'état suivant
        observation = next_state

    # Décroissance de epsilon
    epsilon = max(epsilon_min, epsilon * epsilon_dim)

Points à retenir

  • Exploration vs Exploitation : Le paramètre permet de trouver un équilibre entre découvrir de nouvelles stratégies et exploiter les connaissances acquises.
  • Décroissance de : diminue exponentiellement pour favoriser l’exploitation en fin d’entraînement.
  • Actions valides : Le masque d’actions (action_mask) évite de choisir des actions invalides dans certains états.

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.

Réalisation d'une IA jouant au jeu Acrobot avec MCTS

Présentation du jeu Acrobot

Acrobot

Le jeu Acrobot est un environnement de type contrôle classique disponible dans Gymnasium. Le but du jeu est simple :

Un système à deux segments articulés (un pendule double) doit amener l’extrémité du second segment au dessus d’une certaine hauteur. L’agent peut appliquer un couple moteur limité sur l’articulation centrale, ce qui influence le mouvement des deux segments. L’agent reçoit des récompenses négatives à chaque étape, ce qui a pour but d'encourager l'agent à trouver la solution le plus rapidement possible. La réussite est atteinte lorsque l’extrémité du pendule dépasse la hauteur cible (la ligne noire).

Cet environnement est idéal pour expérimenter avec des algorithmes de planification comme le MCTS, car il combine une dynamique continue avec des actions discrètes simples, tout en offrant un défi non trivial de contrôle et de planification à court et long terme.

Monte Carlo Tree Search (MCTS)

Le Monte Carlo Tree Search (MCTS) est un algorithme de planification et d’apprentissage par simulation qui permet à un agent de choisir des actions en explorant un arbre de décisions. L’idée est d’estimer la valeur des actions en simulant des parties complètes depuis l’état courant et en utilisant ces simulations pour guider le choix :

  • Chaque nœud de l’arbre représente un état de l’environnement, et chaque branche correspond à une action possible.
  • L’agent sélectionne les actions à explorer en équilibrant exploitation (choisir les actions avec de bonnes valeurs estimées) et exploration (essayer des actions moins connues) via une formule comme UCB1 :

où :

  • (Vi) : valeur cumulée du nœud (i)
  • (Ni) : nombre de visites du nœud (i)
  • (Np) : nombre de visites du parent
  • (c) : coefficient d’exploration

(Algorithme UCB1, réalisé en python, permettant de choisir le meilleur noeud parmis plusieurs)

def ucb1(children, c=1.0):
    parent_visits = sum(child.visits for child in children.values()) + 1e-5
    best_score = -float('inf')
    best_child = None
    for action, child in children.items():
        if child.visits == 0:
            score = float('inf')
        else:
            exploit = child.value / child.visits
            explore = math.sqrt(math.log(parent_visits) / child.visits)
            score = exploit + c * explore
        if score > best_score:
            best_score = score
            best_child = child
    return best_child
  • Après chaque simulation, la récompense finale est propagée vers le haut de l’arbre (backpropagation) pour mettre à jour les valeurs des nœuds traversés.
  • À la fin des itérations, l’action choisie est celle qui maximise la valeur moyenne estimée au nœud racine.

Ainsi, MCTS permet à l’agent de planifier plusieurs coups à l’avance sans connaître explicitement la dynamique de l’environnement, en s’appuyant sur des simulations pour guider ses décisions.

Étapes pour réaliser l’IA

Réalisation des fonctions nécessaires :

Importation des bibliothèques nécessaires et création de l'objet "Node" (noeud)

L'objet Node (noeud) possède :

  • Un état
  • un parent
  • un enfant
  • le nombre de visites
  • une valeur
import gymnasium as gym
import math
import random
import copy

env = gym.make("Acrobot-v1")
obs, info = env.reset()

# Crtion d'un objet représentant le noeud d'un arbre
class Node:
    def __init__(self, state, parent):
        self.parent = parent
        self.state = state
        self.visits = 0
        self.value = 0.0
        self.children = {}

La fonction de simulation

Cette fonction permet, à partir d'un noeud, de faire X simulations à partir du noeud de la fin du jeu.

def simulate(Inode: Node, max_steps):
    total_reward = 0
    env_simu = copy.deepcopy(env.unwrapped)
    env_simu.state = Inode.state
    steps = 0
    done = False 
    
    #Tant que le programme n'a pas réussi, ou que le max_steps n'a pas été atteint, on réalise des simulations
    while not done and steps < max_steps:
        steps += 1
        action = env_simu.action_space.sample()
        obs, reward, terminated, truncated, _ = env_simu.step(action)
        total_reward += reward

        if terminated:
            done = True

    return env_simu.state, done, total_reward

La fonction "Backpropagate"

La méthode backpropagate joue un rôle central dans le MCTS en permettant de remonter la récompense obtenue après une simulation à travers l’arbre de décision

def backpropagate(node: Node, reward):
    while node is not None:
        node.visits += 1
        node.value += reward
        node = node.parent

La fonction de réalisation de cycle

La fonction cycle représente une itération complète de MCTS à partir d’un nœud donné. Elle commence par créer les enfants du nœud si nécessaire, puis sélectionne un noeud au hasard parmi les enfants nouvellement créés. À partir de ce nœud, plusieurs simulations sont effectuées, chaque simulation produisant une récompense cumulée qui est ensuite propagée vers le haut de l’arbre grâce à backpropagate

def cycle(noeud, env):
    simulate_node = noeud
    all_actions = list(range(env.action_space.n))
    if noeud.children == {}:
        for action in all_actions:
            noeud.children[action] = Node(state=copy.deepcopy(noeud.state), parent=noeud)
        simulate_node = random.choice(list(noeud.children.values()))
    for _ in range(random.randint(50, 100)):
        final_state, done, epdone, total_reward = simulate(simulate_node, max_steps=300)
        simulate_node.state = final_state
        backpropagate(simulate_node, reward=total_reward)
    return final_state, done, epdone, total_reward

La fonction "MCTS"

Dans cette fonction, on rassemble tout ce que l'on a codé auparavant. On commence par initialisé le noeud racine ainsi que certaines variables. Ensuite, dans la boucle "while", tant que le jeu n'est pas terminé, ou que les episodes ne se sont pas écoulés et que le compteur est inférieur aux nombres d'itérations choisis, on effectue des cycles.

def mcts(env, iterations=100):
    racine = Node(state=copy.deepcopy(env.unwrapped.state), parent=None)
    done = False
    compteur = 0
    while not done and compteur < iterations:
        final_state, done, epdone, total_reward = cycle(noeud=racine, env=env)
        compteur += 1
        print('episode :', compteur)
        print('recompense actuel :', total_reward)

    if done:
        print('Agent a reussi')
    else:
        print('Agent a pas réussi')

    print("Nb d'itérations :", compteur)
    print("Nombre de visites racine :", racine.visits)
    print("Nombre d’enfants :", len(racine.children))
    return racine

Observations

  • Après réflexion, la réalisation d’un MCTS sur un jeu tel que Acrobot n’est pas très idéale car l’environnement possède un espace d’état continu et infini avec une dynamique physique complexe, or, le MCTS est efficace pour des espaces discret et limitées, comme le jeu de plateau Taxi. Pour ce type d’environnement, le Q-Learning est plus adapté pour apprendre une politique optimale.

Réalisation d'une IA jouant au blackjack avec MCTS

Présentation du blackjack

Jeu du blackjack

Principe du jeu

Le Blackjack est un jeu de cartes où le but est de battre le croupier (dealer) en obtenant une main dont la valeur est la plus proche possible de 21, sans jamais la dépasser.

Règles principales

  • Les cartes numérotées valent leur valeur (ex. 7 = 7 points).
  • Les figures (Valet, Dame, Roi) valent 10 points.
  • L’As vaut 1 ou 11 points, selon ce qui avantage le joueur.
  • Le joueur commence avec 2 cartes, tout comme le croupier (une visible, une cachée).
  • Le joueur peut :
    • Hit : tirer une nouvelle carte.
    • Stand : s’arrêter et conserver sa main actuelle.
  • Le croupier joue ensuite selon des règles fixes :
    • Il tire tant que sa main est ≤ 16.
    • Il reste à 17 ou plus.

Objectif

  • Gagner si :
    • Votre total est supérieur à celui du croupier sans dépasser 21.
    • Le croupier dépasse 21 .
  • Perdre si vous dépassez 21 ou si le croupier a une meilleure main.

Particularités de la version Gymnasium

  • Le jeu se joue dans un environnement simulé (Blackjack-v1) de la bibliothèque Gymnasium.
  • Les différents états possibles sont indépendants les uns les autres car il y a remise dans la pioche à chaque tirage.

Principe du Monte Carlo Tree Search (MCTS)

Le Monte Carlo Tree Search (MCTS) est un algorithme d’apprentissage par renforcement basé sur la simulation.
Il permet à un agent de prendre des décisions optimales en explorant un arbre de recherche et en simulant de nombreuses parties à partir des états possibles.

L’idée est d’équilibrer l’exploration (essayer de nouvelles actions) et l’exploitation (choisir les actions prometteuses déjà connues) pour estimer la meilleure décision à chaque étape.


1. Sélection (Selection)

À partir de la racine (état actuel), l’algorithme descend dans l’arbre en sélectionnant les nœuds selon une politique d’équilibre entre exploration et exploitation, à l’aide de la formule UCB1 (Upper Confidence Bound) :

  • : somme des récompenses du nœud i
  • : nombre de visites du nœud i
  • : nombre total de simulations depuis le nœud parent
  • : constante d’exploration (généralement 2)

2. Expansion (Expansion)

Si le nœud sélectionné n’est pas terminal, on ajoute un ou plusieurs nouveaux nœuds enfants correspondant aux actions possibles depuis cet état.

3. Simulation (Rollout)

Une simulation aléatoire est exécutée à partir du nouvel état jusqu’à la fin de la partie (ou un horizon fixé).
Le résultat (victoire, défaite, score) fournit une évaluation de cet état.

4. Rétropropagation (Backpropagation)

Le résultat de la simulation est propagé en remontant l’arbre : chaque nœud met à jour son nombre de visites et son score moyen.

À force de répétitions, le MCTS améliore ses estimations pour chaque action.
L’action optimale depuis l’état initial est généralement celle avec le plus grand nombre de visites ou la meilleure moyenne de récompense.


Structure de notre IA

L’intelligence artificielle repose sur deux classes principales.

La classe State

Cette classe représente un état du jeu, c’est-à-dire la situation actuelle dans une partie de Blackjack : les cartes du joueur, la carte visible du croupier, et la présence éventuelle d’un as compté comme 11.

Elle contient plusieurs attributs essentiels.
L’attribut state correspond à l’état du jeu tel qu’il est fourni par l’environnement Gymnasium.
visits indique combien de fois cet état a déjà été rencontré au cours des simulations.
actions regroupe les deux choix possibles dans une partie de Blackjack : « stand » (Action(0)) ou « hit » (Action(1)).
Enfin, __utc_c est une constante d’exploration utilisée dans le calcul de la valeur UCB, qui permet de gérer le compromis entre exploration et exploitation.

La méthode UCB_value(self, action) calcule la valeur UCB (Upper Confidence Bound) pour une action donnée. C’est à cet endroit que se décide l’équilibre entre tester de nouvelles actions et exploiter celles qui semblent déjà prometteuses.

La méthode interne __select_action(self) choisit l’action à jouer selon les valeurs UCB ou, si l’état n’a encore jamais été visité, de manière aléatoire. Concrètement, si self.visits vaut 0, on n’a encore aucune donnée, donc l’action est tirée au hasard. Sinon, on compare les deux valeurs UCB (pour les actions 0 et 1) et on garde celle qui a la meilleure valeur. En cas d’égalité parfaite, une action aléatoire est choisie afin de conserver un minimum d’exploration.

La méthode select_learning_action(self) sert d’interface publique pour la sélection d’une action pendant la phase d’apprentissage. Elle se contente d’appeler __select_action() et applique donc la logique du MCTS avec exploration activée.

La méthode select_operating_action(self) est utilisée quand l’IA joue réellement, c’est-à-dire lorsqu’elle doit exploiter pleinement ce qu’elle a appris sans explorer. Pour cela, la constante d’exploration __utc_c est temporairement désactivée, puis l’action est choisie en fonction des valeurs moyennes observées. Une fois la sélection faite, la valeur d’origine de __utc_c est rétablie. Ce procédé permet à l’IA de prendre la meilleure décision possible à partir de son expérience accumulée.

Enfin, la méthode backtraking(self) incrémente simplement le compteur de visites de l’état (self.visits += 1). Elle est appelée à la fin de chaque simulation afin de mettre à jour l’arbre de recherche.

Code source :

import gymnasium as gym
import math
import random

# specificly design for gymnasium blackjack , previous step doesn't influence the outcom of current
# so we redesigned some part of classical MCST to be optimized to this challenge !
class State:
    __utc_c = 1
    def __init__(self, state):
        self.state = state            
        self.visits = 0
        self.actions = [Action(0),Action(1)]

    
    # pré-requis : self.visits > 0
    def UCB_value(self,action):
        act_visits = max(1,action.get_visits()) # pour éviter la division par 0
        act_value = action.get_value()
        act_avg = act_value/act_visits

        exploration = math.sqrt(self.__utc_c*math.log(self.visits)/act_visits)

        return act_avg + exploration
    
    def choose_random_action(self):
        if random.uniform(0,1) >= 0.5:
            return self.actions[0]
        else:
            return self.actions[1]
    
    def __select_action(self):
        if self.visits == 0:
            return self.choose_random_action()
        
        act0_UCB = self.UCB_value(self.actions[0])
        act1_UCB = self.UCB_value(self.actions[1])

        if act0_UCB > act1_UCB :
            return self.actions[0]
        elif act0_UCB == act1_UCB:
            return self.choose_random_action()
        else:
            return self.actions[1]
        
    def select_learning_action(self):
        return self.__select_action()
    
    def select_operating_action(self):
        temp = self.__utc_c
        self.__utc_c = 0
        a =  self.__select_action()
        self.__utc_c = temp
        return a
    
    def backtraking(self):
        self.visits = self.visits + 1

La classe Action

La classe Action représente une action possible dans une partie de Blackjack. Elle correspond concrètement au choix que l’agent peut faire à un instant donné : tirer une nouvelle carte (hit) ou s’arrêter (stand). Chaque objet Action garde en mémoire ses statistiques individuelles au fil des simulations.

Lors de son initialisation, la classe reçoit un paramètre order qui identifie l’action à effectuer dans l’environnement (0 pour stand, 1 pour hit). Trois attributs sont ensuite définis :

  • order, qui indique le type d’action,

  • visits, le nombre de fois où cette action a été jouée depuis cet état,

  • value, la somme totale des récompenses obtenues lorsque cette action a été choisie.

Plusieurs méthodes permettent d’accéder à ces informations.
get_order() renvoie simplement le numéro associé à l’action.
get_visits() retourne le nombre de fois où cette action a été essayée.
get_value() donne la valeur totale accumulée grâce aux récompenses obtenues.

La méthode backtracking(self, reward) est appelée après une simulation, lorsque le résultat (ou la récompense) de l’action est connu. Elle met à jour les statistiques associées en incrémentant le compteur de visites et en ajoutant la récompense reçue à la valeur totale. C’est grâce à cette mise à jour que l’algorithme peut progressivement estimer la qualité réelle de chaque action et améliorer sa prise de décision au fil des parties.

Code source :

class Action:
    def __init__(self,order): 
        self.order = order   # la valeur de l'action dans l'environnement     
        self.visits = 0
        self.value = 0.0

    def get_order(self):
        return self.order
    
    def get_visits(self):
        return self.visits
    
    def get_value(self):
        return self.value
    
    def backtracking(self,reward):
        self.visits = self.visits + 1
        self.value += reward

Dans l'ensemble l'IA est un set de l'ensemble des states que l'on crée au cours du temps


Fonctionnalités développées

Nous avons développé 4 grandes fonctionnalités :

Entraînement de l’IA

Tout commence avec la fonction ai_training(). Celle-ci crée d’abord un environnement de jeu Blackjack grâce à la bibliothèque Gymnasium. L’utilisateur choisit ensuite combien de parties l’IA doit jouer pour s’entraîner. Ce nombre d’épisodes détermine la durée et la qualité de l’apprentissage : plus il est élevé, plus l’agent aura exploré de scénarios possibles.

Au début de chaque partie, on réinitialise le jeu et on crée un dictionnaire dic_node_game qui va garder en mémoire les états rencontrés et les actions associées pendant cette partie. Ce dictionnaire est temporaire et sera utilisé plus tard pour mettre à jour les statistiques.

Pendant le déroulement du jeu, l’IA observe l’état actuel (c’est-à-dire les cartes du joueur, la carte visible du croupier et la présence d’un as utilisable). Si cet état n’a jamais été rencontré auparavant, on le crée et on l’ajoute à la liste des états découverts.
Ensuite, l’agent choisit une action à l’aide de la méthode select_learning_action() de la classe State.

L’environnement exécute alors cette action grâce à la fonction env.step(), qui renvoie le nouvel état, la récompense obtenue et des indicateurs indiquant si la partie est terminée. Si le jeu n’est pas encore fini, l’IA continue à jouer jusqu’à ce que la manche se conclue.

Quand la partie se termine, le programme affiche la récompense finale (+1.5 pour un blackjack,+1 pour une victoire, -1 pour une défaite, 0 pour une égalité). Ensuite, la phase de mise à jour commence : pour chaque couple (état, action) rencontré pendant la partie, on appelle les méthodes backtraking() sur l’état et backtracking(reward) sur l’action. Ces deux appels servent à incrémenter les compteurs de visites et à mettre à jour les valeurs de récompense moyenne. C’est ce mécanisme qui permet à l’IA d’apprendre quelles décisions mènent le plus souvent à la victoire.

Une fois toutes les parties d’entraînement terminées, l’ensemble des états et des actions explorés est sauvegardé grâce à la classe AI_saver. Cela permet de réutiliser plus tard le modèle entraîné sans devoir recommencer tout le processus.

Extrait du code source :

def ai_training():
    descovered_states = {}

    # Utilisation de Gymnasium pour l'environnement de jeu
    env = gym.make("Blackjack-v1")

    n_episodes_train = ask_number("Please choose the number of game that the ai should train on")
    reward = 0
    for episode in range(n_episodes_train):
        print("#################################################")
        print(f"nouvelle épisode {episode}")
        state, info = env.reset() # Nouvelle partie
        reward = 0
        done = False

        dic_node_game = {} # Un dictionnaire State -> Action qui retient le déroulement de la partie

        while not done:
            if state not in descovered_states:
                descovered_states[state] = State(state)
    
            node_state = descovered_states[state]

            action = node_state.select_learning_action()

            dic_node_game[node_state] = action
    
        
            # On joue l'action proposer par l'IA et récupère le retour de l'environnement
            next_state, reward, terminated, truncated, info = env.step(action.get_order())
            done = terminated or truncated

            state = next_state
    
        # en dehors du while, la partie est finie
        print(f"Episode finished! Total reward: {reward}")
        # On rétropropage le reward de la partie
        for n_state,n_action in dic_node_game.items():
            n_state.backtraking()
            n_action.backtracking(reward)
    ai_saver.save(descovered_states,game_name,n_episodes_train)
    env.close()

Gestion des IA et sauvegarde des modèles

Pour éviter de devoir réentraîner une intelligence artificielle à chaque exécution, une partie essentielle du projet consiste à pouvoir sauvegarder et recharger une IA déjà entraînée. C’est exactement le rôle de la classe AI_saver, qui s’appuie sur le module Pickle de Python. L’objectif est de rendre la gestion des IA simple, pratique et flexible, tout en permettant de conserver plusieurs versions d’un même modèle.

Le fonctionnement général est le suivant : lorsqu’une IA termine sa phase d’apprentissage, l’utilisateur peut la sauvegarder dans un dossier spécial nommé .ai_saves. À ce moment-là, le programme lui demande de choisir un nom pour la retrouver plus facilement par la suite. Le fichier est ensuite automatiquement complété par la date et l’heure de la sauvegarde, ainsi que par le nombre d’épisodes d’entraînement effectués. Cela donne des noms de fichiers à la fois uniques et informatifs, comme par exemple
blackjack_ai_test_02-11-2025--17:30_trained_5000_times.plk.

Si le dossier n’existe pas encore, il est créé automatiquement grâce à os.makedirs. Une fois le nom final prêt, l’objet représentant l’IA est sauvegardé dans un fichier binaire grâce à la fonction pickle.dump().

Pickle est un module standard de Python qui permet de sérialiser et désérialiser des objets. La sérialisation, c’est le processus qui consiste à transformer un objet Python (par exemple une instance de classe contenant des dictionnaires, des listes et même d’autres objets) en une suite d’octets. Ces octets peuvent ensuite être enregistrés dans un fichier ou envoyés à travers un réseau. La désérialisation, c’est l’opération inverse : à partir du fichier binaire, Pickle reconstitue exactement l’objet d’origine, avec toutes ses valeurs, sa structure et son état interne.

Concrètement, quand on écrit pickle.dump(ai, f), Pickle parcourt récursivement tous les attributs de l’objet ai, traduit leur contenu en une forme binaire, et les écrit dans le fichier ouvert en mode wb (write binary). Lors du chargement avec ai = pickle.load(f), le module lit ces mêmes octets et reconstruit un objet Python identique à celui qui avait été sauvegardé, en restaurant toutes les références et les valeurs. C’est ce qui permet de retrouver une IA exactement dans l’état où elle était après son apprentissage, comme si elle n’avait jamais été arrêtée.

Il faut cependant noter que Pickle est spécifique à Python. Les fichiers produits ne sont pas lisibles directement par d’autres langages, et il ne faut pas charger un fichier Pickle provenant d’une source non fiable, car le processus de désérialisation peut exécuter du code malveillant.

La méthode load() de la classe AI_saver permet de restaurer une IA précédemment sauvegardée. Elle commence par lister tous les fichiers disponibles dans le dossier .ai_saves, éventuellement filtrés selon le nom du jeu, comme "blackjack". Cette recherche se fait grâce à la fonction list_files(), qui utilise le module glob pour parcourir les fichiers avec l’extension .plk.

Le programme affiche ensuite la liste des fichiers trouvés et demande à l’utilisateur de choisir celui qu’il souhaite recharger. Une fois la sélection faite, le fichier est ouvert en lecture binaire et passé à pickle.load(), qui reconstruit l’objet complet en mémoire. L’IA ainsi rechargée est ensuite renvoyée pour être utilisée dans les autres fonctions du programme, comme la visualisation ou la comparaison de stratégies.

Ce système de sauvegarde et de restauration rend la gestion des intelligences artificielles bien plus simple. Il devient possible d’entraîner plusieurs modèles différents, de les comparer entre eux ou de reprendre l’apprentissage là où on s’était arrêté, sans perte de données.

Code source :

import pickle
from datetime import datetime
import glob
import os
import sys

# retourne tous les fichiers dans le directory qui contiennent substring ou tous si None
def list_files(directory, substring=None):
    all_files = glob.glob(os.path.join(directory, "*.plk"))
    all_files = [f for f in all_files if os.path.isfile(f)]
    if substring:
        all_files = [f for f in all_files if substring in os.path.basename(f)]
    return all_files

def ask_number_in_list(msg, values_list):
    while True:
        try:
            val = int(input(f"{msg} {values_list} : "))
            if val in values_list:
                return val
            else:
                print(f"Wrong input. Please choose beetween {values_list}.")
        except ValueError:
            print("Invalid input. Please choose a number.")


class AI_saver:
    folder_name = ".ai_saves"
    def __init__(self):
        pass

    def save(self,ai,game_name="no_specified",nb_training=None):

        # construction du nom du fichier
        now = datetime.now()
        result = input("Choose a name to find more easely your ai :")
        name = self.folder_name+"/"+game_name+"_ai_"+result # nom de base du fichier
        name = name + "_" + now.strftime("%d-%m-%Y--%H:%M") # on ajoute la date

        # ajout du nombre d'entrainement eventuellement ainsi que l'extension du fichier 
        if nb_training is None :
            name += "_no_training_given.plk"
        else:
            name += "_trained_"+str(nb_training)+"_times.plk"

        # Création du dossier si nécessaire
        os.makedirs(os.path.dirname(name), exist_ok=True)

        with open(name, "wb") as f:
            pickle.dump(ai,f)

        print(f"the ai as been save under the name {name}")
    
    def load(self,game_name=None):
        availables_files = list_files(self.folder_name,game_name)

        if len(availables_files) == 0:
            print("files not found : exiting")
            sys.exit()
        
        choices = []
        print("here is the list of file found with their number choice:")
        for i in range(len(availables_files)):
            choices.append(i)
            print(f"choice {i} : {availables_files[i]}")
        
        choice = ask_number_in_list("select the ai you want to restore : ",choices)

        print(f"You have been select the file {availables_files[choice]}")

        with open(availables_files[choice], "rb") as f:
            ai_loaded = pickle.load(f)
        
        return ai_loaded

Comparaison de l’efficacité de l’IA

Une fois l’intelligence artificielle entraînée et sauvegardée, il est important de pouvoir évaluer ses performances et de les comparer à d’autres stratégies. C’est exactement le rôle de la fonction methods_comparaison(). Elle permet de mesurer, sur un grand nombre de parties, à quel point notre IA joue mieux qu’un joueur aléatoire ou qu’une stratégie classique de croupier.

Le principe est simple : on recharge d’abord une IA déjà entraînée puis on fait jouer trois « joueurs » différents sur le même environnement de Blackjack. Le premier est l’IA, le deuxième représente le comportement typique d’un croupier (tirer une carte quand la main est de 16 ou moins, et s’arrêter sinon), et le troisième joue complètement au hasard, en choisissant aléatoirement entre « hit » et « stand ».

Pour chacun de ces joueurs, le programme lance une série de parties, dont le nombre est défini par l’utilisateur. À chaque tour, il enregistre les résultats dans un dictionnaire qui compte le nombre de victoires, de défaites, d’égalités et la somme totale des récompenses. Ces statistiques sont mises à jour grâce à la fonction count_stats(), qui ajoute +1 dans la catégorie correspondante selon le résultat de la partie et cumule le score total pour calculer une moyenne à la fin.

Pendant les tests, l’IA joue de manière purement déterministe grâce à la méthode select_operating_action() de la classe State, c’est-à-dire qu’elle choisit toujours l’action qu’elle estime la meilleure, sans aucune exploration. Cette approche permet d’évaluer objectivement la qualité de son apprentissage, sans l’influence du hasard. En revanche, le joueur aléatoire sert de référence minimale, et la stratégie du croupier donne une idée de la performance moyenne d’un joueur humain raisonnable.

Une fois toutes les parties jouées, le programme affiche un récapitulatif complet des résultats pour les trois stratégies. Il calcule les pourcentages de victoires, de défaites et d’égalités, ainsi que la récompense moyenne obtenue. Ce dernier indicateur est particulièrement important, car il reflète le gain moyen attendu à long terme.

Grâce à cette comparaison, on peut évaluer objectivement l’efficacité de l’IA. Si elle dépasse largement les deux autres méthodes, cela signifie que son apprentissage a été efficace et qu’elle a bien intégré les stratégies gagnantes du Blackjack. Ce type de test permet aussi d’ajuster le nombre d’épisodes d’entraînement nécessaires ou de vérifier si le modèle commence à surapprendre (c’est-à-dire à trop s’adapter à des cas spécifiques).

Extrait du code source :

def count_stats(dic,value):
    if value>0:
        dic["win"] += 1
    elif value<0:
        dic["lose"] += 1
    else:
        dic["draw"] += 1
    
    dic["total"] += value


def methods_comparaison():
    descovered_states = ai_saver.load(game_name)
    env = gym.make("Blackjack-v1")

    n_episodes_train = ask_number("Please choose the number of game that the ai and methods should play")

    # plein de partie de l'ia
    ai_stats = {"win":0,"draw":0,"lose":0,"total":0}
    for _ in range(n_episodes_train):
        state, info = env.reset()
        done = False

        while not done:
            if state not in descovered_states:
                descovered_states[state] = State(state)
    
            node_state = descovered_states[state]

            action = node_state.select_operating_action()
    
            next_state, reward, terminated, truncated, info = env.step(action.get_order())
            done = terminated or truncated

            state = next_state
    
        count_stats(ai_stats,reward)
    print("ai's games finished")

    # Stratégie du dealer (« hit » si <= 16 « stand » sinon)
    dealer_stats = {"win":0,"draw":0,"lose":0,"total":0}
    for _ in range(n_episodes_train):
        state, info = env.reset()
        done = False

        while not done:
            hand,a,b = state
            if hand>16:
                action = 0
            else:
                action = 1

    
            next_state, reward, terminated, truncated, info = env.step(action)
            done = terminated or truncated

            state = next_state
    
        count_stats(dealer_stats,reward)
    print("dealer's strategie games finished")
    
    # Choix aléatoire entre « hit » et « stand » à chaque fois
    random_stats = {"win":0,"draw":0,"lose":0,"total":0}
    for _ in range(n_episodes_train):
        state, info = env.reset()
        done = False

        while not done:

            if random.uniform(0,1) >= 0.5:
                action = 0
            else:
                action = 1
    
            next_state, reward, terminated, truncated, info = env.step(action)
            done = terminated or truncated

            state = next_state
    
        count_stats(random_stats,reward)
    
    # affichage des résultats et des différentes statistiques 
    print(f"Based on the {n_episodes_train} games here's our results :")
    print(f"  Ai   : {round(ai_stats["win"]*100/n_episodes_train,2)} % of win; {round(ai_stats["draw"]*100/n_episodes_train,2)} % of draw; {round(ai_stats["lose"]*100/n_episodes_train,2)} % of lose; for a reward average of {round(ai_stats["total"]/n_episodes_train,5)}")
    print(f"Dealer : {round(dealer_stats["win"]*100/n_episodes_train,2)} % of win; {round(dealer_stats["draw"]*100/n_episodes_train,2)} % of draw; {round(dealer_stats["lose"]*100/n_episodes_train,2)} % of lose; for a reward average of {round(dealer_stats["total"]/n_episodes_train,5)}")
    print(f"Random : {round(random_stats["win"]*100/n_episodes_train,2)} % of win; {round(random_stats["draw"]*100/n_episodes_train,2)} % of draw; {round(random_stats["lose"]*100/n_episodes_train,2)} % of lose; for a reward average of {round(random_stats["total"]/n_episodes_train,5)}")

    env.close()

Visualisation et interaction avec l’IA

Une fois l’intelligence artificielle entraînée, il est essentiel de pouvoir observer son comportement en situation réelle et d’interagir directement avec elle pour comprendre la logique de ses décisions. C’est tout l’intérêt des parties du programme dédiées à la visualisation et à l’interprétation des choix de l’IA. L’idée est de permettre à l’utilisateur non seulement de voir comment l’IA applique ce qu’elle a appris, mais aussi de lui poser des questions comme à un véritable joueur expérimenté.

Lorsqu’on lance la phase de visualisation, l’IA précédemment sauvegardée est rechargée depuis le disque à l’aide du module de gestion des modèles. Le programme crée alors un environnement de jeu Blackjack grâce à la bibliothèque Gymnasium, configuré en mode graphique afin que le déroulement de chaque partie soit affiché à l’écran. L’utilisateur choisit combien de parties il souhaite observer, et l’IA se met à jouer automatiquement en prenant ses décisions selon la stratégie qu’elle a apprise. Chaque état rencontré pendant la partie — c’est-à-dire la somme des cartes du joueur, la carte visible du croupier et la présence éventuelle d’un as compté comme 11 — est analysé par le modèle. L’IA choisit ensuite la meilleure action possible selon ses connaissances, qu’il s’agisse de rester ou de tirer une nouvelle carte. Un léger délai entre chaque action rend la progression plus fluide et permet de suivre clairement la logique du jeu.

Ce système offre une fenêtre graphique sur le fonctionnement interne de l’intelligence artificielle. Plutôt que de se limiter à des statistiques, on peut visualiser ses choix, constater son comportement face à des situations critiques et voir comment elle adapte sa stratégie en fonction des circonstances. Il devient alors possible d’évaluer intuitivement la cohérence de son apprentissage : si elle prend les bonnes décisions, si elle évite les risques inutiles ou au contraire s’aventure trop souvent à tirer une carte supplémentaire.

En parallèle, le programme permet aussi une interaction directe avec l’IA à travers une interface textuelle où l’utilisateur peut lui demander conseil. On indique simplement les informations de la main en cours — la somme totale de ses cartes, la carte visible du croupier et la présence ou non d’un as utilisable — et l’IA répond en proposant l’action qu’elle estime la plus judicieuse. Derrière cette simplicité, elle exploite exactement la même logique que lorsqu’elle joue seule : elle identifie l’état correspondant dans sa mémoire et applique la politique optimale qu’elle a construite durant son apprentissage. Si la situation demandée n’a jamais été rencontrée, l’IA prévient qu’elle ne connaît pas la réponse, ce qui traduit bien la limite naturelle de tout apprentissage basé sur l’expérience.

Extrait du code source :

def blackjack_visual():
    descovered_states = ai_saver.load(game_name)
    dt = 0.7 # Délai entre deux actions pour faciliter l’observation de l’IA (en secondes)
    env = gym.make("Blackjack-v1",render_mode="human")

    n_episodes_train = ask_number("Please choose the number of game that the ai should play")
    for _ in range(n_episodes_train):
        state, info = env.reset()
        done = False

        while not done:
            if state not in descovered_states:
                descovered_states[state] = State(state)
    
            node_state = descovered_states[state]

            action = node_state.select_operating_action()
    
            next_state, reward, terminated, truncated, info = env.step(action.get_order())
            done = terminated or truncated

            state = next_state
            time.sleep(dt)
    
    env.close()
    
def ai_suggestion():
    descovered_states = ai_saver.load(game_name)
    done = False
    while not done:
        choice = ask_number_in_list("select 0 to get the ai response or 1 to exit",[0,1])
        if choice == 1:
            done = True
        else:
            player = ask_number("please enter the sum of your cards")
            dealer = ask_number("please enter dealer's cards")
            ace = ask_number("please enter 1 if you have a playable ace (count as 11) or 0 otherwise")
            state = player,dealer,ace,
            if state not in descovered_states:
                print("The ai don't know the solution for this case")
            else:
                node_state = descovered_states[state]
                action = node_state.select_operating_action()
                print(f"Knowing that a 0 means a stay and a 1 a hit, the ai recommands : {action.get_order()}")

Points clés à retenir

  • MCTS + UCB : L’IA utilise la recherche Monte Carlo Tree Search (MCTS) avec un score UCB pour équilibrer exploration et exploitation.
  • Suivi et mise à jour des états : Les résultats des simulations servent à mettre à jour l’arbre de recherche, guidant ainsi les décisions futures.
  • Paramètres de l’IA : Le nombre d’itérations et le facteur UCB influencent directement la précision et le style de jeu de l’IA.
  • Sauvegarde : L’état de l’IA peut être sauvegardé et rechargé via pickle, évitant ainsi de réentraîner l’IA à chaque utilisation.

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

Présentation du jeu Mountain Car

MountainCar

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.

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.