labo · note technique

Un réseau de neurones joue au Snake

Sur la page d'accueil, la commande rlplay du terminal lance une partie de Snake pilotée par un petit réseau de neurones que j'ai entraîné. Voici comment il marche - pour les curieux.

Neuroévolution, pas de rétropropagation

Plutôt qu'un apprentissage par gradient (DQN, policy-gradient…), j'utilise la neuroévolution : on crée une population de réseaux aux poids aléatoires, on les fait jouer, on garde les meilleurs, et on les recombine + mute pour la génération suivante. Pas de gradient, pas de fonction de perte - juste la sélection. Pour un problème aussi petit que Snake, c'est robuste et simple à mettre en place.

L'architecture

Un perceptron multicouche minuscule : 21 entrées → 16 neurones cachés (ReLU) → 3 sorties, soit 403 paramètres. De quoi tenir dans un fichier de 4 Ko.

Les entrées : tout est relatif

Le réseau ne voit pas la grille en pixels. Il reçoit 21 indicateurs relatifs à sa tête et à sa direction. Pour chacune des trois actions possibles, le jeu simule le coup et mesure ce qu'il en resterait :

pour gauche · tout droit · droite (5 × 3 = 15 entrées) :
  mort immédiate ?             (mur ou corps)
  espace libre atteignable     (flood fill depuis la nouvelle tête, normalisé)
  queue encore atteignable ?   (tant qu'on peut rejoindre sa queue, on n'est pas enfermé)
  rapproche de la pomme ?      (+1 / -1)
  mange la pomme ?

contexte (6 entrées) :
  pomme devant / à droite      (dans le repère de la tête)
  queue devant / à droite
  longueur du serpent · faim   (pas depuis la dernière pomme)

C'est le flood fill qui change tout : la première version du réseau ne voyait que les dangers collés à sa tête (11 entrées) et s'enfermait dans ses propres boucles dès que le serpent devenait long. Elle plafonnait à environ 55 pommes.

Les sorties : 3 choix égocentriques

Trois neurones de sortie, dont on prend le maximum (argmax) : tourner à gauche, tout droit, ou tourner à droite - relativement à la direction courante. Jamais de demi-tour suicidaire possible.

La récompense (fitness)

Ce qui guide la sélection : le nombre de pommes mangées, la survie ne servant qu'à départager les débutants. Un garde-fou « famine » arrête les réseaux qui tournent en rond sans manger (plus de 1 008 pas sans pomme) - sinon ils apprendraient à survivre éternellement sans jamais croquer une pomme.

L'entraînement

  • Population de 240 réseaux, 76 générations.
  • Élitisme + sélection par tournoi + croisement uniforme + mutation gaussienne (dont l'amplitude décroît au fil du temps - un « recuit »).
  • Chaque réseau est jugé sur la moyenne de 4 parties, les mêmes pour toute la génération (pour comparer à armes égales), et le meilleur est re-testé sur 12 parties neuves avant de devenir champion.
  • Tourné en local sur mon PC (Python + NumPy, 22 processus en parallèle), en ~23 minutes. Le simulateur Python reproduit exactement les règles du jeu du navigateur.

L'inférence, côté navigateur

Les poids du champion sont exportés en JSON (~4 Ko) et chargés dans la page. À chaque pas, le navigateur calcule les 21 entrées (trois petits flood fill sur une grille de 28 × 18) puis fait la passe avant - quelques produits matriciels en JavaScript pur. Aucun backend, aucune dépendance, aucune charge serveur.

h = relu(features · W1 + b1)     // 21 → 16
o = h · W2 + b2                  // 16 → 3
action = argmax(o)               // gauche / tout droit / droite

Les scores

Moyennes mesurées sur 60 parties accélérées, avec le code JavaScript du site (octobre 2026) :

  • Le réseau actuel : ~430 pommes par partie (médiane 453, record 497 sur un plateau de 504 cases).
  • L'ancien réseau à 11 entrées : ~55 pommes.
  • Le bot de pathfinding (autoplay : plus court chemin vers la pomme + test de survie) : ~160 pommes.

Honnêteté : le réseau ne trouve pas tout seul la notion d'espace libre, c'est moi qui la lui donne (le flood fill). Son mérite, c'est d'apprendre à arbitrer entre la pomme et la survie, mieux que les règles écrites à la main du bot. Il lui arrive encore de mourir tôt, et en fin de partie il finit souvent par tourner en rond quand la pomme est inaccessible. Comparez vous-même dans le terminal : autoplay (le bot) contre rlplay (le réseau).

← Retour au terminal