Skip to main content

Reinforced Frog

Github

Jumper Frog (Frogger Game) in python with AI reinforcement 🐸

README-1666710399073.gif armored_frog.png

Présentation du jeu et Objectif

L'objectif principal est de faire apprendre par renforcement un agent sur le jeu Frogger.

Contexte

Ce projet a été réalisé dans le cadre du cours d'apprentissage par renforcement. Il a été réalisé par 3 étudiants en 5ᵉ année d'architecture logicielle.

Jeu original

Règle du jeu
Image du jeu original
Frogger est un jeu d'arcade classique. Le but du jeu est de diriger des grenouilles jusqu'à leurs maisons. Pour cela, le joueur doit d'abord traverser une route en évitant des voitures qui roulent à différentes vitesses, puis une rivière aux courants changeants et enfin, à nouveaux, une route. La grenouille meurt si elle touche une voiture ou si elle tombe dans la rivière. Frogger_game_arcade.png

Objectif

L'objectif est de faire apprendre à un agent à traverser la route et la rivière en évitant les voitures et l'eau.
Pour cela, nous allons utiliser l'algorithme Q-Learning. L'agent va apprendre à traverser la route et la rivière en apprenant à associer une action à un état. L'agent va donc découvrir comment associer une action à un état.

Pour cela, nous allons également devoir développer le jeu Frogger en utilisant la librairie arcade. Le seul langage utilisé est le Python, nous n'utilisons pas de librairie externe mis à part arcade et quelques librairies utilitaires.

Installation

Prérequis

  • Python 3.10 minimum
  • PIP3

Installation des dépendances

Après avoir cloné le projet, il faut installer les dépendances avec la commande suivante :

pip3 install -r requirements.txt

Utilisation

Environnement

Avant de lancer le jeu, il faut créer le fichier .env à la racine du projet. Ce fichier contient les variables d'environnement nécessaire au bon fonctionnement du jeu. Vous pouvez vous baser sur le fichier .env.example pour créer le fichier .env.

Variable
Description
Valeur conseillée
AGENT_COUNT Nombre d'agents en simultané sur la carte 1-10
AGENT_DEBUG Afficher les informations debugs en console des agents (WIN/LOOSE) false
ARCADE_INSIGHTS Afficher les informations de l'agent sur le jeu arcade true
AGENT_GAMMA Taux de prise en compte de l'état futur 0.1
AGENT_LEARNING_FILE Emplacement du fichier qtable qtable/nom-du-fichier.xz
AGENT_LEARNING_RATE Taux d'apprentissage de l'agent 0.6
AGENT_VISIBLE_COLS_ARROUND Nombre de colonnes visible autour de l'agent dans son environnement 4-6
AGENT_VISIBLE_LINES_ABOVE Nombre de lignes visible devant l'agent dans son environnement 1-2
EXPLORE_RATE Taux d'exploration sur les actions déterminés de l'agent 0.05-0.1
EXPLORE_RATE_DECAY Taux de diminution du taux d'exploration 0.999
GENERATE_HISTORY_GRAPH Générer le graphique de progression d'apprentissage en même temps que la sauvegarde de la Q-Table true
HASH_QTABLE Hash des lignes de l'environnement (permet de diminuer un peu la taille en mémoire) false
LEARNING_MODE Passer en mode apprentissage (console seulement) ou en mode graphique (arcade) true pour apprendre un peu, puis false
LEARNING_TYPE Type d'apprentissage (QLEARNINGMQLEARNINGDQLEARNING) QLEARNING-MQLEARNING 
LEARNING_TIME Temps de l'apprentissage en minute 45
LEARNING_PRINT_STATS_EVERY Afficher en console les stats d'appretissage tous les x secondes 30-60
LEARNING_SAVE_QTABLE_EVERY Fréquence de sauvegarde de la Q-Table tous les x secondes (opération lourde) 60-600
QTABLE_HISTORY_FILE Emplacement des fichiers d'historique history/nom-du-fichier.history
QTABLE_HISTORY_PACKETS Paquets pour l'historique = au nombre d'agents (1-10)
WORLD_TYPE Type de monde ( 0 -> Route + Eau, 1 -> Route seulement, 2 -> Eau uniquement) 0

Pour lancer le jeu, il faut lancer la commande suivante :

python3 main.py

Développement du jeu

Présentation de la librairie arcade

Arcade est une librairie Python permettant de créer des jeux vidéo. Elle est basée sur Pyglet et permet de créer des jeux vidéo 2D. Elle permet de créer des jeux vidéo en 2D avec des sprites, des animations, des sons, des effets de particules…,

img.png

Configuration des règles

Afin de pouvoir modifier rapidement la configuration de notre jeu (difficulté, tokens, actions possibles…,), nous avons écrit toute la configuration dans le fichier config.py. Ce fichier est lu par le jeu.

Tokens

Ce fichier de configuration contient les différents tokens utilisés dans le jeu. Ces derniers permettent au jeu d'avoir une représentation textuelle de son environement, ce qui va grandement nous aider pour l'apprentissage de l'agent.

CAR_TOKEN = 'C'
TRUCK_TOKEN = 'Z'
TURTLE_TOKEN = 'T'
TURTLE_L_TOKEN = 'TL'
TURTLE_XL_TOKEN = 'TXL'
REVERSED_CAR_TOKEN = 'RC'
REVERSED_TRUCK_TOKEN = 'RZ'
REVERSED_TURTLE_TOKEN = 'RT'
REVERSED_TURTLE_L_TOKEN = 'RTL'
...

ACTION_UP = 'U'
ACTION_DOWN = 'D'
ACTION_LEFT = 'L'
ACTION_RIGHT = 'R'
ACTION_NONE = 'N'
...

WATER_COMMONS_TOKENS = [TURTLE_TOKEN, TURTLE_L_TOKEN, TURTLE_XL_TOKEN, REVERSED_TURTLE_TOKEN, REVERSED_TURTLE_L_TOKEN,
                        ...]
...

WIN_STATES = [EXIT_TOKEN]

Arcade

Ce fichier de configuration contient les différents paramètres de la librairie arcade. Ces derniers permettent de définir les sprites des différentes entités, ainsi que leur taille et le scaling.

SCALE = 1
SPRITE_SIZE = 64 * SCALE

...


def get_sprite_resources(name: str, sprite_size: float = 0.5):
  return arcade.Sprite(f":resources:images/{name}.png", sprite_size * SCALE)


def get_sprite_local(name: str, sprite_size: float = 0.5):
  return arcade.Sprite(f"assets/sprite/{name}.png", sprite_size * SCALE)


ENTITIES: Dict[str, WorldEntity] = {
  CAR_TOKEN: WorldEntity(1, 1, CAR_TOKEN, get_sprite_local("car_1", 0.65)),
  ...
}

...

WORLD_WIDTH = 180
WORLD_HEIGHT = 117
WORLD_SCALING = 9

Représentation du monde

Le monde est représenté par une matrice de caractères. Chaque caractère représente une entité du monde. Les entités sont représentées par des tokens. Ces derniers sont définis dans le fichier de configuration.

La classe permettant de représenter le monde est la classe World. Cette classe permet de

Cette dernière permet de :

  • Créer un monde, avec la bonne configuration
  • Gérer la mise à jour (déplacement) des entités dans le monde
  • Gérer les collisions entre les entités
  • Gérer les mouvements, récompenses des joueurs

Représentation du monde pour l'agent

À chaque état, l'agent reçoit une représentation du monde sous forme de liste de chaîne caractères. Chaque élément représente une ligne visible (définit dans les variables d'environnement) du monde.

Ainsi, l'agent ne voit pas toute la carte, mais tout au plus 2 ligne devant lui, 1 ligne derrière lui, et 4 colonnes sur les côtés.

README-1667658404493.png

World Entity

Pour généraliser les différentes entités que nous traitons, nous avons la classe WorldEntity. Cette dernière permet de regrouper pour chaque entité :

  • La taille de l'entité
  • Le token de l'entité
  • Le sprite de l'entité

World Line

La classe WorldLine permet de représenter une ligne du monde. Cette dernière permet de gérer les déplacements des entités sur chaque ligne, ainsi que leur fréquence d'apparition, vitesse…,

Joueurs

Le joueur est représenté par l'interface Player. Cette dernière permet de gérer le déplacement du joueur, de le gérer dans la classe principale Game. Cette interface nous permet de gérer plusieurs types de joueurs ( Humain, Agent).

from typing import Tuple, List

from arcade import Sprite

from display.entity.world_entity import WorldEntity
from game.world import World


class Player:
  def init(self, world: World, intial_state: Tuple[int, int], _initial_environment: bytes):
    pass

  def best_move(self, environment: [str]) -> str:
    pass

  def step(self, action: str, reward: float, new_state: Tuple[int, int], _environment: List[str]):
    pass

  def save_score(self):
    pass

  def update_state(self, new_state, new_environment):
    pass

  @property
  def sprite(self) -> Sprite:
    pass

  @property
  def world_entity(self) -> WorldEntity:
    pass

  @property
  def is_human(self) -> bool:
    pass

  @property
  def score(self) -> int:
    pass

  @property
  def state(self) -> Tuple[int, int]:
    pass

Joueur Humain

Le joueur humain est représenté par la classe HumanPlayer. Cette dernière permet de se déplacer avec les touches directionnelles du clavier.

Affichage graphique

L'affichage graphique est entièrement géré par la classe WorldWindow. Cette dernière permet de gérer l'affichage du monde sur arcade. Elle permet également de gérer la vitesse de jeu, le nombre de frames par seconde, les statistiques affichés…,

import arcade.color

from ai.Model import Model
from conf.config import *
from game.game import Game


class WorldWindow(arcade.Window):
  def __init__(self, game: Game, env, model: Model):
    super().__init__(
      int(game.world.width / WORLD_SCALING * SPRITE_SIZE),
      int(game.world.height / WORLD_SCALING * SPRITE_SIZE),
      'REINFORCED FROG',
      update_rate=1 / 60
    )
    self.__height = int(game.world.height / WORLD_SCALING * SPRITE_SIZE)

  # ...

  # ...

  def setup(self):
    self.setup_world_states()
    self.setup_players_states()
    self.setup_world_entities_state()

  def setup_world_entities_state(self):
    self.__entities_sprites = arcade.SpriteList()
    for state in self.__game.world.world_entities_states.keys():
      world_entity: WorldEntity = self.__game.world.get_world_entity(state)
      if world_entity is not None:
        sprite = self.__get_entity_sprite(state, world_entity)
        self.__entities_sprites.append(sprite)

  def setup_world_states(self):
    self.__world_sprites = arcade.SpriteList()
    for state in self.__game.world.world_states:
      world_entity: WorldEntity = self.__game.world.get_world_line_entity(state)
      if world_entity is not None:
        sprite = self.__get_environment_sprite(state, world_entity)
        self.__world_sprites.append(sprite)

  def setup_players_states(self):
    self.__players_sprites = arcade.SpriteList()
    for player in self.__game.players:
      sprite = player.sprite
      sprite.center_x, sprite.center_y = (
        self.__get_xy_state((player.state[0] + WORLD_SCALING // 2, player.state[1] + WORLD_SCALING // 2)))
      self.__players_sprites.append(sprite)

  def __draw_debug(self):

  # ...

  def on_draw(self):
    arcade.start_render()
    self.__world_sprites.draw()
    self.__entities_sprites.draw()
    self.__players_sprites.draw()
    if self.__debug == 1:
      self.__draw_debug()
    elif self.__debug == 2:
      self.__draw_collisions_debug()
    if self.__env['ARCADE_INSIGHTS']:
      self.__draw_model_insights()

  def __draw_model_insights(self):

  # ...

  def on_update(self, delta_time: float):
    self.__game.step()
    self.setup_players_states()
    self.__players_sprites.update()
    self.setup_world_entities_state()
    self.__entities_sprites.update()

  # ...

Développement de l'IA

Pour l'IA, nous avons utilisé 3 méthodes principales :

  • Q-Learning
  • Multi-Q-Learning
  • Deep Q-Learning

Q-Learning

Le Q-Learning est une méthode d'apprentissage par renforcement. Cette méthode permet de déterminer la meilleure action à effectuer dans un état donné. Pour cela, elle utilise une fonction de valeur Q qui permet de déterminer la valeur d'une action dans un état donné. Cette fonction est mise à jour à chaque étape de l'apprentissage.

Implémentation

L'implémentation du Q-Learning est géré par la classe QLearning. Cette dernière permet de gérer la table de Q-Learning, de mettre à jour les valeurs de la table, de récupérer la meilleure action à effectuer, de sauvegarder la table de Q-Learning, ...

Cette classe permet également (comme les autres méthodes d'apprentissage) de gérer l'exploration et l'exploitation, ainsi que l'historique de progression.

Nous nous basons sur l'équation de Bellman pour mettre à jour la table de Q-Learning :

README-1667658736226.png

La Qtable est formé sous forme de dictionnaire récursif de N+1 dimension, N étant le nombre total de lignes visible. L'idée est de fusionner ensemble les clés communes afin de ne pas consommer trop de mémoire vive (on peut avoir plus de 5 millions de clés différentes). La première dimension représente la première ligne visible, la deuxième dimension la seconde…, La dernière dimension représente les actions possibles.

README-1667659666276.png

README-1667659673471.png

Apprentissage

Nous avons testé différents hyper-paramètres pour l'apprentissage sur plusieurs heures. Les résultats les plus convaincants ont été obtenus avec les paramètres suivants :

  • AGENT_GAMMA = 0.1
  • AGENT_LEARNING_RATE = 0.6
  • AGENT_VISIBLE_COLS_ARROUND = 4
  • AGENT_VISIBLE_LINES_ABOVE = 2 # 1
  • EXPLORE_RATE = 0.1
  • EXPLORE_RATE_DECAY = 0.9999

Nous avoisinons un taux de réussite plus haut (~95%) avec une ligne visible devant contre ~90% avec 2 lignes visibles. Cependant, nous avons remarqué que les mouvements de la grenouille étaient plus fluides avec 2 lignes visibles, car une meilleure anticipation.

 QLEARNING_L1_C4.history.pngQLEARNING_L2_C4.history.png

Les pics sont dûs au rechargement du taux d'exploration (1x par heure)

Multi Q-Learning

Lors de l'apprentissage par le Q-Learning, nous avons remarqué que nous avions beaucoup d'états différents, plusieurs millions. Pourtant, beaucoup d'état sont similaires, ont des lignes identiques, mais sont consiédérés comme des états différents.

Dans le cas où nous avons 4 lignes visibles, ainsi que 4 colonnes visibles. Pour 5 tokens différents, avec une grille de (17x4) 68 cases, nous avons en théorie 68^5 = 1,453,933,568 possibilités max, ce qui est GIGANTESQUE.

Pour résoudre ce problème, nous avons implémenté le Multi Q-Learning. Cette méthode consiste à séparer notre unique Qtable en 3 :

  • Une Qtable pour les lignes du haut, ne gérant que l'action de monter
  • Une Qtable pour les lignes du bas, ne gérant que l'action de descendre
  • Une Qtable pour la ligne centrale, ne gérant que les actions de gauche, droite ou de rester

À chaque itération, nous fusionnons la liste d'action possible en fonction de l'environnement donnée.

Ainsi, nous réduisons le nombre d'états possible (pour la même configuration) à (2x17)^5 + 17^5 + 17^5 = 48,275,138 possibilités max, ce qui est beaucoup plus raisonnable.

Implémentation

L'implémentation du Multi Q-Learning est géré par la classe MultiQtable. Cette dernière permet de gérer les 3 Qtables, de mettre à jour les valeurs de la table, de récupérer la meilleure action à effectuer, de sauvegarder les tables de Q-Learning, ...

README-1667662135336.png

README-1667662138576.png

README-1667662140907.png

README-1667662142763.png

Cette classe permet également (comme les autres méthodes d'apprentissage) de gérer l'exploration et l'exploitation, ainsi que l'historique de progression.

La classe est sensiblement la même que celle du Q-Learning, à la différence que nous avons 3 Qtables, que nous fusionnons au moment où il est nécessaire et récupérer / mettre à jour le poids des actions selon un état donné.

from typing import Dict

from ai.Model import Model
from conf.config import ACTION_UP, ACTION_DOWN, ACTION_LEFT, ACTION_RIGHT, ACTION_NONE


class MultiQtable(Model):
  def __init__(self, alpha: float, gamma: float, score_history_packets: int, visible_lines_above: int):
    self.__visible_lines_above = visible_lines_above
    self.__qtable: Dict[str: Dict[str: float]] = {"UP": {}, "CENTER": {}, "DOWN": {}}
    # ...

  def get_state_actions(self, state: [str]) -> Dict[str, float]:
    actions = {}
    up_state = "\n".join(state[:self.__visible_lines_above])
    center_state = "\n".join(state[self.__visible_lines_above:self.__visible_lines_above + 1])
    down_state = "\n".join(state[self.__visible_lines_above + 1:self.__visible_lines_above + 2])

    if up_state not in self.__qtable["UP"]:
      self.__qtable["UP"][up_state] = {ACTION_UP: 0}
    up = self.__qtable["UP"][up_state]

    if center_state not in self.__qtable["CENTER"]:
      self.__qtable["CENTER"][center_state] = {
        ACTION_LEFT: 0, ACTION_RIGHT: 0, ACTION_NONE: 0}
    center = self.__qtable["CENTER"][center_state]

    if down_state not in self.__qtable["DOWN"]:
      self.__qtable["DOWN"][down_state] = {
        ACTION_DOWN: 0}
    down = self.__qtable["DOWN"][down_state]

    actions.update(up)
    actions.update(center)
    actions.update(down)

    return actions

  def update_state(self, state: [str], max_q: float,
                   reward: float,
                   action: str):
    qtable = self.get_state_actions(state)
    new_q = (1 - self.__alpha) * qtable[action] + self.__alpha * (reward + self.__gamma * max_q)

    if action == ACTION_UP:
      up_state = "\n".join(state[:self.__visible_lines_above])
      self.__qtable["UP"][up_state][action] = new_q
    elif action == ACTION_DOWN:
      down_state = "\n".join(state[self.__visible_lines_above + 1:self.__visible_lines_above + 2])
      self.__qtable["DOWN"][down_state][action] = new_q
    else:
      center_state = "\n".join(state[self.__visible_lines_above:self.__visible_lines_above + 1])
      self.__qtable["CENTER"][center_state][action] = new_q

    self.__increment_step_count()

Apprentissage

Nous avons testé différents hyper-paramètres pour l'apprentissage sur plusieurs heures. Les résultats les plus convaincants ont été obetnus avec les paramètres suivants :

  • AGENT_GAMMA = 0.1
  • AGENT_LEARNING_RATE = 0.6
  • AGENT_VISIBLE_COLS_ARROUND = 4 # 6
  • AGENT_VISIBLE_LINES_ABOVE = 2 # 1
  • EXPLORE_RATE = 0.1
  • EXPLORE_RATE_DECAY = 0.9999

MQLEARNING_L1_C4.history.png

MQLEARNING_L1_C6.history.png

MQLEARNING_L2_C4.history.png

MQLEARNING_L2_C6.history.png

Les pics sont dûs au rechargement du taux d'exploration (1x par heure)

Deep Q-Learning

Malgré tout nos efforts le problème auquel nous faisons face est un problème qui a un grand nombre de possiblité. Le deep Learning permet de s'abstraire de cette contrainte. De faire développer à la machine un instinct qui va lui permettre de choisir une action, qui ne dépendera pas du nombre de possibilités.

Encore une fois la class est sensiblement la même que pour la QTable.

Nous avons utilisé la librairy sklearn dans laquelle nous avons utilisé MLPRegressor.

Nous avons pris le problème de manière que l'objectif soit de maximiser la meilleure decision. Nous avons donc favorisé l'utilisation de regression. Ce qui est cohérent avec notre système de récompense.

Nous avons essayé plusieurs modèles : Des modèles avec 1, 2 ou 3 couches cachées de neurone. Nous avons aussi essayé d'entrainer le modèle avec différente fonction d'activation et différente solver. (RELU, tanh et sgd, adam)

Nos résultats les plus probants viennent du modèle le plus simple à entraîner qui est le réseau à une couche de 1000 neurones avec la fonction d'activation tanh, Le solver sgd (Stochastic Gradient Descent).

Ayant passé beaucoup de temps sur l'apprentissage en Q-Learning, nous avons décidé de ne pas approfondir le Deep Q-Learning plus que ça.

Version avec une seule couche cachée :

self.__mlp = MLPRegressor(
  hidden_layer_sizes=1000,
  activation='tanh',
  solver='sgd',
  learning_rate_init=self.__alpha,
  max_iter=1,
  warm_start=True
)

Version avec 3 couches cachées :

self.__mlp = MLPRegressor(
  hidden_layer_sizes=(1000, 1000, 1000),
  activation='relu',
  solver='adam',
  learning_rate_init=self.__alpha,
  max_iter=199,
  warm_start=True
)

Apprentissage avec modèle pré-entrainé (Q-Learning)

Nous avons également tenté de transformer une Q-Table en modèle de Deep Q-Learning. Nous espérions que cela permettrait d'accélérer l'apprentissage. Nous avons donc entrainé un modèle avec la Q-Table et nous avons ensuite utilisé ce modèle pour entraîner le modèle Deep Q-Learning.

Malheureusement, nous n'avons pas réussi à obtenir de résultats probants avec cette méthode.

Apprentissage continu

Pour être capable d'apprendre toutes les possibilités, qui peuvent être nombreuses, nous avons besoin de laisser tourner l'apprentissage pendant plusieurs heures, voire plusieurs jours.

Cela nous ralentit dans la recherche du bon algorithme, des bons paramètres... Nous avons donc mis en place tout un tas d'amélioration de performances pour pouvoir faire tourner l'apprentissage plus rapidement. Nous avons également mis en place un système permettant de faire tourner en mode non graphique (pour plus de performances) lors de l'apprentissage afin de laisser tourner ce dernier en arrière-plan.

Docker

Nous avons mis en place un environnement docker pour pouvoir faire tourner l'apprentissage dans un conteneur. Cela nous donne une grande souplesse dans le lancement de nos apprentissages, il suffit juste de changer les avriables d'environnement.

La difficulté principale a été de pouvoir faire tourner le jeu dans un conteneur docker, car il faut faire en sorte que le jeu se lance en mode no-graphic et ne tente pas d'utiliser la librairie Arcade afin de ne pas planter...

Nous avons pu ainsi, grâce à un serveur et un script docker-compose, lancer plusieurs apprentissages en parallèle sur notre serveur.

README-1667740782756.png

FROM python:3.10

RUN apt-get update \
  && apt-get install -y -qq --no-install-recommends \
    libxext6 \
    libx11-6 \
    libglvnd0 \
    libgl1 \
    libglx0 \
    libegl1 \
    freeglut3-dev \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
VOLUME /app/save

ENV AGENT_COUNT=5 \
    AGENT_DEBUG=false \
    ARCADE_INSIGHTS=true \
    AGENT_GAMMA=0.1 \
    AGENT_LEARNING_FILE='save/qtable_l2c4.xz' \
    AGENT_LEARNING_RATE=0.6 \
    AGENT_VISIBLE_COLS_ARROUND=4 \
    AGENT_VISIBLE_LINES_ABOVE=2 \
    EXPLORE_RATE=-1 \
    EXPLORE_RATE_DECAY=0.999 \
    GENERATE_HISTORY_GRAPH=true \
    HASH_QTABLE=false \
    LEARNING_MODE=true \
    LEARNING_TYPE=QLEARNING \
    LEARNING_TIME=600 \
    LEARNING_PRINT_STATS_EVERY=60 \
    LEARNING_SAVE_QTABLE_EVERY=300 \
    QTABLE_HISTORY_FILE='save/qtable_l2c4.history' \
    QTABLE_HISTORY_PACKETS=10 \
    WORLD_TYPE=0


CMD [ "python","-u", "./main.py" ]

Problèmes rencontrés

Pendant la réalisation de ce projet, nous avons rencontré plusieurs problèmes.

Implémentation du jeu

Le premier challenge était de développer le jeu, avec une librairie que nous ne connaissions pas, dans un langage que nous avons peu l'habitude d'utiliser.

Nous sommes partis sur un développement en DDD (Domain Driven Design) avec des classes typés, afin de pouvoir facilement ajouter de nouvelles fonctionnalités (différents agents d'apprentissage, différents types de monde, différents types d'affichage, ...).

Apprentissage

Il fallait également mettre au point la façon dont nous allions faire apprendre notre agent, à un moment où nous découvrions tout juste le Q-Learning.

Nous avons donc dû faire plusieurs tests pour trouver les bons paramètres, la façon de représenter notre environnement, à l'agent... Au début, nous étions frené car nous lassions une trop grosse visibilité à l'agent, ce qui faisait que l'apprentissage ne convergait pas.

Nous avons fini par trouver les réglages optimaux après plusieurs éssais.

Performances

Nous avons également rencontré des problèmes de performances, notamment lors de l'apprentissage, qui nous ont obligé à mettre en place des améliorations de performances.

Nous avons passé plusieurs dizaines d'heures à optimiser notre code, qui était fonctionnel, pour réduire au plus possible le nombre d'itérations, de boucles, etc...

Nous avons également fait passer les librairies python au peigne fin pour trouver les méthodes les plus optimisés dans certains cas d'utilisation pour traiter des lots plus rapidement. Il faut savoir que certaines librairies de python sont plus optimisées que d'autres, notamment pour les opérations mathématiques. Certaines sont égalements compilées en C, ce qui les rend plus performantes.

Taille des Qtables

Nous avons également rencontré des problèmes de taille de Qtable, qui nous ont obligé à mettre en place un système de compression des Qtables.

Nous avons pu compresser le nombre d'entrées grâce à un système d'arbre regrouprant les préfixes communs de notre Q-table.

Nous avons aussi fait en sorte que la taille de nos caractères représentant nos états soient le plus petit possible, afin d'utiliser le moins de caractères.

Nous pouvons également activer la hashage de nos états, pour passer d'une ligne de 17 caractères (136 bits) à une ligne de 8 caractères (64 bits). Cela nous permet de garder un peu d'espace en mémoire et sur le disque.

Lorsque nous sauvegardons nos Qtables, nous les compressons également (LZMA), afin de réduire leur taille.

Conclusion

Au départ, nous ne pensions pas que le sujet allait être si complexe pour un jeu dont les règles sont pourtant simples. Le simple fait de passer d'un environnement statique à un environnement stochastique, nous a obligé à repenser la façon dont l'agent perçoit son environnement et apprend de ce dernier.

Nous avons également appris à utiliser une nouvelle librairie, et à développer en python.

Ce projet nous a fait nous investir à fond, et nous a permis de découvrir de nouvelles choses.

Nous avons pris plaisir sur la partie optimisation à découvrir comment rendre notre apprentissage encore plus rapide, afin d'obtenir des résultats satisfaisant encore plus rapidement. Pour ce faire, nous avons dû tout de même laisser tourner l'apprentissage pendant plusieurs jours sur un serveur dédié.