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 ça converge en quelques secondes.

L'architecture

Un perceptron multicouche minuscule : 11 entrées → 24 neurones cachés (ReLU) → 3 sorties, soit 363 paramètres. De quoi tenir dans un fichier de quelques kilo-octets.

Les entrées : tout est relatif

Le réseau ne voit pas la grille en pixels. Il reçoit 11 indicateurs relatifs à sa tête et à sa direction — c'est ce qui le rend agnostique à la taille du plateau (entraîné en 28×18, il jouerait pareil ailleurs) :

[0..2]  danger devant · danger à droite · danger à gauche   (mur ou corps ?)
[3..6]  direction actuelle : haut · bas · gauche · droite     (one-hot)
[7..10] nourriture : à gauche · à droite · au-dessus · en-dessous

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 : manger domine très largement le score, la survie sert de départage, et un bonus non-linéaire récompense les longues parties. Un garde-fou « famine » tue les réseaux qui tournent en rond sans manger — sinon ils apprendraient à survivre éternellement sans jamais croquer une pomme.

L'entraînement

  • Population de 350 réseaux, 600 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 3 parties (pour ne pas récompenser un coup de chance), et le champion final est re-testé sur 16 parties.
  • Tourné en local sur un Mac M4, en NumPy, en ~9 minutes. Le champion mange ~58 pommes en moyenne (sur 16 parties).

L'inférence, côté navigateur

Les poids du champion sont exportés en JSON (~6,6 Ko) et chargés dans la page. À chaque pas, le navigateur calcule les 11 features puis fait la passe avant — quelques produits matriciels en JavaScript pur. Aucun backend, aucune dépendance, aucune charge serveur.

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

Honnêteté : un simple bot de pathfinding (BFS vers la pomme + test de survie) joue souvent mieux que ce réseau. Le RL ici, c'est pour le geste et le plaisir — pas pour battre un record. Comparez vous-même dans le terminal : autoplay (le bot) contre rlplay (le réseau).

← Retour au terminal