Recherche de site Web

Créez un moteur d'échecs IA autonome à partir de zéro avec l'apprentissage par imitation


Ceci est un article sur la façon dont j'ai créé un moteur d'échecs IA, en partant de zéro pour créer mon propre moteur d'échecs IA.

Parce que créer un moteur d'échecs IA à partir de zéro est une tâche relativement complexe, cet article sera long, mais restez à l'écoute, car le produit que vous obtiendrez sera un projet sympa à présenter !

Conditions préalables

Cet article expliquera la plupart des concepts en détail. Cependant, certains prérequis sont recommandés pour suivre le didacticiel. Vous devez connaître les éléments suivants :

  • Python
  • Comment utiliser le terminal
  • Carnet Jupyter
  • Concepts fondamentaux de l'IA
  • Règles d'échecs

J'utiliserai également les outils suivants :

  • Python
  • Différents packages Python
  • Stockfisch

Table des matières

  • Partie 1  : Comment générer un ensemble de données
  • Partie 2  : Comment encoder des données
  • Partie 3 : Comment entraîner le modèle d'IA
  • Conclusion

Partie 1 : Comment générer un ensemble de données

Dans cette partie, j'utiliserai Stockfish pour générer un large ensemble de données de mouvements à partir de différentes positions. Ces données peuvent ensuite être utilisées ultérieurement pour entraîner l’IA des échecs.

Comment télécharger Stockfish

Le composant le plus important de mon moteur d'échecs est Stockfish, je vais donc vous montrer comment l'installer.

Accédez à la page de téléchargement du site Web Stockfish et téléchargez la version qui vous convient. J'utilise moi-même Windows, j'ai donc choisi la version Windows (plus rapide) :

Après le téléchargement, extrayez le fichier zip à l'emplacement de votre PC où vous souhaitez que votre moteur d'échecs se trouve. Rappelez-vous où vous le placez car vous avez besoin du chemin pour l'étape suivante.

Comment intégrer Stockfish avec Python

Maintenant, vous devez également intégrer le moteur dans Python. Vous pouvez le faire manuellement, mais j'ai trouvé plus facile d'utiliser le package Python Stockfish car il possède toutes les fonctions dont vous avez besoin.

Installez d'abord le package depuis pip (de préférence dans votre environnement virtuel) :

pip install stockfish

Vous pouvez ensuite l'importer à l'aide de la commande suivante :

from stockfish import Stockfish
stockfish = Stockfish(path=r"C:\Users\eivin\Documents\ownProgrammingProjects18062023\ChessEngine\stockfish\stockfish\stockfish-windows-2022-x86-64-avx2")

Notez que vous devez donner votre propre chemin vers le fichier exécutable Stockfish :

Vous pouvez copier le chemin du fichier à partir de la structure des dossiers, ou si vous êtes sous Windows 11, vous pouvez appuyer sur ctrl+shift + c pour copier automatiquement le chemin du fichier.

Super! Stockfish est désormais disponible en Python !

Comment générer un ensemble de données

Vous avez maintenant besoin d’un ensemble de données pour pouvoir entraîner le moteur d’échecs IA ! Vous pouvez le faire en faisant jouer à Stockfish et en mémorisant chaque position et les mouvements que vous pourriez effectuer à partir de là.

Ces mouvements seront les meilleurs possibles, étant donné que Stockfish est un moteur d'échecs puissant.

Tout d’abord, installez un package d’échecs et NumPy (il y a beaucoup de choix, mais j’utiliserai celui ci-dessous).

Saisissez chaque ligne (individuellement) dans le terminal :

pip install chess
pip install numpy

Importez ensuite les packages (n'oubliez pas d'importer également Stockfish comme indiqué plus haut dans cet article) :

import chess
import random
from pprint import pprint
import numpy as np
import os
import glob
import time

Vous avez également besoin de quelques fonctions d'assistance ici :

#helper functions:
def checkEndCondition(board):
 if (board.is_checkmate() or board.is_stalemate() or board.is_insufficient_material() or board.can_claim_threefold_repetition() or board.can_claim_fifty_moves() or board.can_claim_draw()):
  return True
 return False

#save
def findNextIdx():
 files = (glob.glob(r"C:\Users\eivin\Documents\ownProgrammingProjects18062023\ChessEngine\data\*.npy"))
 if (len(files) == 0):
  return 1 #if no files, return 1
 highestIdx = 0
 for f in files:
  file = f
  currIdx = file.split("movesAndPositions")[-1].split(".npy")[0]
  highestIdx = max(highestIdx, int(currIdx))

 return int(highestIdx)+1

def saveData(moves, positions):
 moves = np.array(moves).reshape(-1, 1)
 positions = np.array(positions).reshape(-1,1)
 movesAndPositions = np.concatenate((moves, positions), axis = 1)
 nextIdx = findNextIdx()
 np.save(f"data/movesAndPositions{nextIdx}.npy", movesAndPositions)
 print("Saved successfully")

def runGame(numMoves, filename = "movesAndPositions1.npy"):
 """run a game you stored"""
 testing = np.load(f"data/{filename}")
 moves = testing[:, 0]
 if (numMoves > len(moves)):
  print("Must enter a lower number of moves than maximum game length. Game length here is: ", len(moves))
  return

 testBoard = chess.Board()

 for i in range(numMoves):
  move = moves[i]
  testBoard.push_san(move)
 return testBoard

Pensez à modifier le chemin du fichier dans la fonction findNextIdx , car celui-ci est personnel pour votre ordinateur.

Créez un dossier de données dans le dossier que vous codez et copiez le chemin (tout en conservant le *.npy à la fin)

La fonction checkEndCondition utilise les fonctions du package Chess pip pour vérifier si la partie doit être terminée.

La fonction saveData enregistre un jeu dans des fichiers npy, ce qui constitue un moyen hautement optimisé de stocker des tableaux.

La fonction utilise la fonction findNextIdx pour enregistrer dans un nouveau fichier (n'oubliez pas ici de créer un nouveau dossier appelé data pour stocker toutes les données).

Enfin, la fonction runGame vous permet d'exécuter une partie que vous avez enregistrée pour vérifier les positions après numMoves nombre de mouvements.

Ensuite, vous pouvez enfin accéder à la fonction qui exploite les parties d'échecs :

def mineGames(numGames : int):
 """mines numGames games of moves"""
 MAX_MOVES = 500 #don't continue games after this number

 for i in range(numGames):
  currentGameMoves = []
  currentGamePositions = []
  board = chess.Board()
  stockfish.set_position([])

  for i in range(MAX_MOVES):
   #randomly choose from those 3 moves
   moves = stockfish.get_top_moves(3)
   #if less than 3 moves available, choose first one, if none available, exit
   if (len(moves) == 0):
    print("game is over")
    break
   elif (len(moves) == 1):
    move = moves[0]["Move"]
   elif (len(moves) == 2):
    move = random.choices(moves, weights=(80, 20), k=1)[0]["Move"]
   else:
    move = random.choices(moves, weights=(80, 15, 5), k=1)[0]["Move"]

   currentGamePositions.append(stockfish.get_fen_position())
   board.push_san(move)
   currentGameMoves.append(move)
   stockfish.set_position(currentGameMoves)
   if (checkEndCondition(board)):
    print("game is over")
    break
  saveData(currentGameMoves, currentGamePositions)

Ici, vous définissez d’abord une limite maximale afin qu’un jeu ne dure pas infiniment longtemps.

Ensuite, vous exécutez le nombre de parties que vous souhaitez exécuter et assurez-vous que Stockfish et le package Chess pip sont réinitialisés à la position de départ.

Ensuite, vous obtenez les 3 meilleurs coups suggérés par Stockfish et choisissez l'un d'entre eux pour jouer (80 % de changement pour le meilleur coup, 15 % de changement pour le deuxième meilleur coup, 5 % de changement pour le troisième meilleur coup). La raison pour laquelle vous ne choisissez pas toujours le meilleur coup est que la sélection de coup est plus stochastique.

Ensuite, vous choisissez un coup (en vous assurant qu'aucune erreur ne se produit même s'il y a moins de trois coups possibles), enregistrez la position sur l'échiquier à l'aide de FEN (un moyen de coder une position d'échecs), ainsi que le coup effectué à partir de cette position.

Si le jeu est terminé, vous rompez la boucle et stockez toutes les positions et les mouvements effectués à partir de ces positions. Si le jeu n'est pas terminé, vous continuez à jouer jusqu'à ce que le jeu soit terminé.

Vous pouvez ensuite exploiter un jeu avec :

mineGames(1)

N'oubliez pas de créer un dossier de données ici, car c'est là que je stocke les jeux !

Comment évaluer un jeu miné

Exécutez la fonction mineGames pour exploiter un jeu à l'aide de la commande suivante :

mineGames(1)

Vous pouvez accéder à ce jeu avec une fonction d'assistance présentée précédemment en utilisant la commande suivante :

testBoard = runGame(12, "movesAndPositions1.npy")
testBoard

En supposant qu'il y a eu 12 coups dans le jeu, vous verrez alors quelque chose comme ceci :

Et voilà, vous pouvez désormais miner autant de jeux que vous le souhaitez.

Cela va prendre un certain temps, et il existe des possibilités d'optimisation de ce processus de minage, comme par exemple la parallélisation des simulations de jeu (puisque chaque jeu est complètement séparé de l'autre).

Pour le code complet de la partie 1, vous pouvez consulter le code complet sur mon GitHub.

Partie 2  : Comment encoder des données

Dans cette partie, vous encoderez les mouvements et les positions des échecs de la même manière que DeepMind l'a fait avec AlphaZero !

J'utiliserai les données que vous avez recueillies dans la première partie de cette série.

Pour rappel, vous avez installé Stockfish et vous êtes assuré de pouvoir y accéder sur l'ordinateur. Vous lui avez ensuite fait jouer des jeux contre lui-même, pendant que vous stockiez tous les mouvements et positions.

Vous avez maintenant un problème d'apprentissage supervisé, puisque l'entrée est la position actuelle et l'étiquette (le mouvement correct à partir des positions) est le mouvement que Stockfish a décidé d'être le meilleur.

Comment installer et importer des packages

Tout d’abord, vous devez installer et importer tous les packages requis, dont certains que vous possédez peut-être déjà si vous avez suivi la première partie de cette série.

Toutes les importations sont ci-dessous – n'oubliez pas de saisir une seule ligne à la fois lors de l'installation via pip :

pip install numpy
pip install gym-chess
pip install chess

De plus, vous devez apporter une petite modification à l'un des fichiers du package gym-chess depuis que np.int a été utilisé, qui est désormais obsolète.

Dans le fichier avec le chemin relatif (depuis l'environnement virtuel) venv\Lib\site-packages\gym_chess\alphazero\board_encoding.pyvenv est le nom de mon environnement virtuel, vous devez rechercher "np.int" et les remplacer par "int".

Si vous ne le faites pas, vous verrez un message d'erreur indiquant que np.int est obsolète.

J'ai également dû redémarrer VS Code après avoir remplacé "np.int" par "int", pour que cela fonctionne.

Toutes les importations dont vous avez besoin sont ci-dessous :

import numpy as np
import gym
import chess
import os
import gym.spaces
from gym_chess.alphazero.move_encoding import utils, queenmoves, knightmoves, underpromotions
from typing import List

Et puis vous devez également créer l’environnement du gymnase pour encoder et décoder les mouvements :

env = gym.make('ChessAlphaZero-v0')

Comment encoder les positions et les mouvements du tableau

L’encodage est un élément important au sein de l’IA, car il nous permet de représenter les problèmes de manière lisible pour l’IA.

Au lieu d'une image d'un échiquier ou d'une chaîne représentant un coup d'échecs comme "d2d4", vous représentez cela à l'aide de tableaux (listes de nombres).

Découvrir comment faire cela manuellement est assez difficile, mais heureusement pour nous, le package Python gym-chess a déjà résolu ce problème pour nous.

Je ne vais pas entrer plus en détail sur la façon dont ils l'ont codé, mais vous pouvez voir en utilisant le code ci-dessous qu'une position est représentée par un tableau en forme (8,8,119), et que tous les mouvements possibles sont donnés avec un tableau (4672). (1 colonne avec 4672 valeurs).

Si vous souhaitez en savoir plus à ce sujet, vous pouvez consulter l'article AlphaZero, bien qu'il s'agisse d'un article assez compliqué à bien comprendre.

#code to print action and state space
env = gym.make('ChessAlphaZero-v0')
env.reset()
print(env.observation_space)
print(env.action_space)

Quelles sorties :

Vous pouvez également consulter l'encodage d'un coup. De la notation sous forme de chaîne à la notation codée. Assurez-vous de réinitialiser l'environnement, car cela pourrait générer une erreur si vous ne le faites pas :

#first set the environment and make sure to reset the positions
env = gym.make('ChessAlphaZero-v0')
env.reset()

#encoding the move e2 to e4
move = chess.Move.from_uci('e2e4')
print(env.encode(move))
# -> outputs: 877

#decoding the encoded move 877
print(env.decode(877))
# -> outputs: Move.from_uci('e2e4')

Avec cela, vous pouvez désormais disposer de fonctions pour encoder les mouvements et les positions que vous avez stockés à partir de la partie 1 où vous avez généré un ensemble de données.

Comment créer des fonctions pour encoder des mouvements

Ces fonctions sont copiées du package Gym-Chess, mais avec de petites modifications afin qu'elles ne dépendent pas d'une classe.

J'ai modifié manuellement ces fonctions pour qu'elles soient plus faciles à encoder. Je ne m'inquiéterais pas trop de bien comprendre ces fonctions, car elles sont assez compliquées.

Sachez simplement qu'ils sont un moyen de garantir que les mouvements que les humains comprennent sont convertis de manière à ce que les ordinateurs puissent les comprendre.

#fixing encoding funcs from openai

def encodeKnight(move: chess.Move):
    _NUM_TYPES: int = 8

    #: Starting point of knight moves in last dimension of 8 x 8 x 73 action array.
    _TYPE_OFFSET: int = 56

    #: Set of possible directions for a knight move, encoded as 
    #: (delta rank, delta square).
    _DIRECTIONS = utils.IndexedTuple(
        (+2, +1),
        (+1, +2),
        (-1, +2),
        (-2, +1),
        (-2, -1),
        (-1, -2),
        (+1, -2),
        (+2, -1),
    )

    from_rank, from_file, to_rank, to_file = utils.unpack(move)

    delta = (to_rank - from_rank, to_file - from_file)
    is_knight_move = delta in _DIRECTIONS
    
    if not is_knight_move:
        return None

    knight_move_type = _DIRECTIONS.index(delta)
    move_type = _TYPE_OFFSET + knight_move_type

    action = np.ravel_multi_index(
        multi_index=((from_rank, from_file, move_type)),
        dims=(8, 8, 73)
    )

    return action

def encodeQueen(move: chess.Move):
    _NUM_TYPES: int = 56 # = 8 directions * 7 squares max. distance
    _DIRECTIONS = utils.IndexedTuple(
        (+1,  0),
        (+1, +1),
        ( 0, +1),
        (-1, +1),
        (-1,  0),
        (-1, -1),
        ( 0, -1),
        (+1, -1),
    )

    from_rank, from_file, to_rank, to_file = utils.unpack(move)

    delta = (to_rank - from_rank, to_file - from_file)

    is_horizontal = delta[0] == 0
    is_vertical = delta[1] == 0
    is_diagonal = abs(delta[0]) == abs(delta[1])
    is_queen_move_promotion = move.promotion in (chess.QUEEN, None)

    is_queen_move = (
        (is_horizontal or is_vertical or is_diagonal) 
            and is_queen_move_promotion
    )

    if not is_queen_move:
        return None

    direction = tuple(np.sign(delta))
    distance = np.max(np.abs(delta))

    direction_idx = _DIRECTIONS.index(direction)
    distance_idx = distance - 1

    move_type = np.ravel_multi_index(
        multi_index=([direction_idx, distance_idx]),
        dims=(8,7)
    )

    action = np.ravel_multi_index(
        multi_index=((from_rank, from_file, move_type)),
        dims=(8, 8, 73)
    )

    return action

def encodeUnder(move):
    _NUM_TYPES: int = 9 # = 3 directions * 3 piece types (see below)
    _TYPE_OFFSET: int = 64
    _DIRECTIONS = utils.IndexedTuple(
        -1,
        0,
        +1,
    )
    _PROMOTIONS = utils.IndexedTuple(
        chess.KNIGHT,
        chess.BISHOP,
        chess.ROOK,
    )

    from_rank, from_file, to_rank, to_file = utils.unpack(move)

    is_underpromotion = (
        move.promotion in _PROMOTIONS 
        and from_rank == 6 
        and to_rank == 7
    )

    if not is_underpromotion:
        return None

    delta_file = to_file - from_file

    direction_idx = _DIRECTIONS.index(delta_file)
    promotion_idx = _PROMOTIONS.index(move.promotion)

    underpromotion_type = np.ravel_multi_index(
        multi_index=([direction_idx, promotion_idx]),
        dims=(3,3)
    )

    move_type = _TYPE_OFFSET + underpromotion_type

    action = np.ravel_multi_index(
        multi_index=((from_rank, from_file, move_type)),
        dims=(8, 8, 73)
    )

    return action

def encodeMove(move: str, board) -> int:
    move = chess.Move.from_uci(move)
    if board.turn == chess.BLACK:
        move = utils.rotate(move)

    action = encodeQueen(move)

    if action is None:
        action = encodeKnight(move)

    if action is None:
        action = encodeUnder(move)

    if action is None:
        raise ValueError(f"{move} is not a valid move")

    return action

Alors maintenant, vous pouvez donner un coup sous forme de chaîne (par exemple : "e2e4" pour le passage de e2 à e4), et cela génère un nombre (la version codée du coup).

Comment créer une fonction pour encoder les positions

L'encodage des positions est un peu plus difficile. J'ai pris une fonction du package gym-chess ("encodeBoard") car j'ai eu quelques problèmes en utilisant directement le package. La fonction que j'ai copiée est ci-dessous :

def encodeBoard(board: chess.Board) -> np.array:
 """Converts a board to numpy array representation."""

 array = np.zeros((8, 8, 14), dtype=int)

 for square, piece in board.piece_map().items():
  rank, file = chess.square_rank(square), chess.square_file(square)
  piece_type, color = piece.piece_type, piece.color
 
  # The first six planes encode the pieces of the active player, 
  # the following six those of the active player's opponent. Since
  # this class always stores boards oriented towards the white player,
  # White is considered to be the active player here.
  offset = 0 if color == chess.WHITE else 6
  
  # Chess enumerates piece types beginning with one, which you have
  # to account for
  idx = piece_type - 1
 
  array[rank, file, idx + offset] = 1

 # Repetition counters
 array[:, :, 12] = board.is_repetition(2)
 array[:, :, 13] = board.is_repetition(3)

 return array

def encodeBoardFromFen(fen: str) -> np.array:
 board = chess.Board(fen)
 return encodeBoard(board)

J'ai également ajouté la fonction encodeBoardFromFen , puisque la fonction copiée nécessitait un échiquier représenté à l'aide du package Python Chess, je convertis donc d'abord à partir de la notation FEN (un moyen d'encoder les échecs positions dans une chaîne - vous ne pouvez pas l'utiliser car vous avez besoin que l'encodage soit en nombres) sur un échiquier donné dans ce package.

Vous disposez alors de tout ce dont vous avez besoin pour encoder tous vos fichiers.

Comment automatiser l'encodage de tous les fichiers de données brutes

Maintenant que vous pouvez encoder des mouvements et des positions, vous allez automatiser ce processus pour tous les fichiers de votre dossier que vous avez générés à partir de la première partie de cette série. Cela implique de trouver tous les fichiers dans lesquels vous devez encoder les données et de les enregistrer dans de nouveaux fichiers.

Notez qu'à partir de la partie 1, j'ai légèrement modifié la structure des dossiers.

J'ai maintenant un dossier parent Data , et dans ce dossier, j'ai le rawData, qui correspond aux mouvements au format chaîne et aux positions dans FEN. -format (de la partie 1).

J'ai également le dossier preparedData sous le dossier data, où les mouvements et positions encodés seront stockés.

Notez que les mouvements et positions codés seront stockés dans des fichiers séparés puisque les encodages ont des dimensions différentes.

#function to encode all moves and positions from rawData folder
def encodeAllMovesAndPositions():
    board = chess.Board() #this is used to change whose turn it is so that the encoding works
    board.turn = False #set turn to black first, changed on first run

    #find all files in folder:
    files = os.listdir('data/rawData')
    for idx, f in enumerate(files):
        movesAndPositions = np.load(f'data/rawData/{f}', allow_pickle=True)
        moves = movesAndPositions[:,0]
        positions = movesAndPositions[:,1]
        encodedMoves = []
        encodedPositions = []

        for i in range(len(moves)):
            board.turn = (not board.turn) #swap turns
            try:
                encodedMoves.append(encodeMove(moves[i], board)) 
                encodedPositions.append(encodeBoardFromFen(positions[i]))
            except:
                try:
                    board.turn = (not board.turn) #change turn, since you  skip moves sometimes, you  might need to change turn
                    encodedMoves.append(encodeMove(moves[i], board)) 
                    encodedPositions.append(encodeBoardFromFen(positions[i]))
                except:
                    print(f'error in file: {f}')
                    print("Turn: ", board.turn)
                    print(moves[i])
                    print(positions[i])
                    print(i)
                    break
            
        np.save(f'data/preparedData/moves{idx}', np.array(encodedMoves))
        np.save(f'data/preparedData/positions{idx}', np.array(encodedPositions))
    
encodeAllMovesAndPositions()

#NOTE: shape of files:
#moves: (number of moves in gamew)
#positions: (number of moves in game, 8, 8, 14) (number of moves in game is including both black and white moves)

Je crée d’abord l’environnement et le réinitialise.

Ensuite, j'ouvre tous les fichiers de données brutes créés à partir de la partie 1 et je les code. Je le fais également dans une instruction try/catch, car je vois parfois des erreurs avec les encodages de déplacement.

La première instruction except concerne si un mouvement est ignoré (le programme pense donc que ce n'est pas le bon tour). Si cela se produit, l'encodage ne fonctionnera pas, donc l'instruction except change de tour et réessaye. Ce n'est pas le code le plus optimal, mais l'encodage ne représente qu'une partie mineure du temps d'exécution total pour créer un moteur d'échecs IA, et il est donc acceptable.

Assurez-vous d'avoir la bonne structure de dossiers et d'avoir créé tous les différents dossiers. Sinon, vous recevrez une erreur.

Vous avez maintenant codé votre échiquier et vos mouvements. Si vous le souhaitez, vous pouvez consulter le code complet de cette partie sur mon GitHub.

Partie 3 : Comment entraîner le modèle d'IA

Il s'agit de la troisième et dernière partie de la création de votre propre moteur d'échecs IA !

Dans la première partie, vous avez appris à créer un ensemble de données et dans la deuxième partie, vous avez examiné l'encodage de l'ensemble de données afin qu'il puisse être utilisé pour une IA.

Vous allez maintenant utiliser cet ensemble de données codées pour entraîner votre propre IA à l'aide de PyTorch !

Comment importer des packages

Comme toujours, vous disposez de tous les importations qui seront utilisées dans le tutoriel. La plupart sont simples, mais vous devez installer PyTorch, que je recommande d'installer à l'aide de ce site Web.

Ici, vous pouvez faire défiler un peu vers le bas, où vous voyez quelques options pour la version et le système d'exploitation sur lesquels vous vous trouvez.

Après avoir sélectionné les options qui s'appliquent à vous, vous obtiendrez du code que vous pourrez coller dans le terminal pour installer PyTorch.

Vous pouvez voir les options que j'ai choisies dans l'image ci-dessous, mais en général, je recommande d'utiliser la version stable et de choisir votre propre système d'exploitation.

Ensuite, sélectionnez le package auquel vous êtes le plus habitué (Conda ou pip est probablement le plus simple car vous pouvez simplement le coller dans le terminal).

Sélectionnez CUDA 11.7/11.8 (peu importe lequel) et installez à l'aide de la commande donnée en bas.

Vous pouvez ensuite importer tous vos packages avec le code ci-dessous :

import numpy as np
import torch
import torch.nn as nn
import torch.functional as F
import torchvision
import torchvision.transforms as transforms
from torch.utils.tensorboard import SummaryWriter
from datetime import datetime
import gym
import gym_chess
import os
import chess
from tqdm import tqdm
from gym_chess.alphazero.move_encoding import utils
from pathlib import Path
from typing import Optional

Comment installer CUDA

Il s'agit d'une étape facultative qui vous permet d'utiliser votre GPU pour entraîner votre modèle beaucoup plus rapidement. Ce n’est pas obligatoire, mais cela vous fera gagner du temps lors de l’entraînement de votre IA.

La façon dont vous installez CUDA varie en fonction de votre système d'exploitation, mais j'utilise Windows et j'ai suivi ce tutoriel.

Si vous êtes sous MacOS ou Linux, alors vous pouvez trouver un tutoriel en recherchant sur Google : « installer CUDA Mac/Linux ».

Pour vérifier si vous avez CUDA disponible (votre GPU est disponible), vous pouvez utiliser ce code :

#check if cuda available
torch.cuda.is_available()

Qui génère True si votre GPU est disponible. Si vous ne disposez pas d'un GPU, ne vous inquiétez pas, le seul inconvénient ici est que la formation du modèle prend plus de temps, ce qui n'est pas si grave lorsque vous réalisez des projets de loisirs comme celui-ci.

Comment créer des méthodes d'encodage

Je définis ensuite quelques méthodes d'assistance pour l'encodage et le décodage à partir du package Python Gym-Chess.

J'ai dû apporter quelques modifications au package pour qu'il fonctionne. La majeure partie du code est copiée à partir du package, avec seulement quelques petites modifications rendant le code indépendant d'une classe, etc.

Notez que vous n'êtes pas obligé de comprendre tout le code ci-dessous, car la façon dont Deepmind encode tous les mouvements aux échecs est compliquée.

#helper methods:

#decoding moves from idx to uci notation
def _decodeKnight(action: int) -> Optional[chess.Move]:
    _NUM_TYPES: int = 8

    #: Starting point of knight moves in last dimension of 8 x 8 x 73 action array.
    _TYPE_OFFSET: int = 56

    #: Set of possible directions for a knight move, encoded as 
    #: (delta rank, delta square).
    _DIRECTIONS = utils.IndexedTuple(
        (+2, +1),
        (+1, +2),
        (-1, +2),
        (-2, +1),
        (-2, -1),
        (-1, -2),
        (+1, -2),
        (+2, -1),
    )

    from_rank, from_file, move_type = np.unravel_index(action, (8, 8, 73))

    is_knight_move = (
        _TYPE_OFFSET <= move_type
        and move_type < _TYPE_OFFSET + _NUM_TYPES
    )

    if not is_knight_move:
        return None

    knight_move_type = move_type - _TYPE_OFFSET

    delta_rank, delta_file = _DIRECTIONS[knight_move_type]

    to_rank = from_rank + delta_rank
    to_file = from_file + delta_file

    move = utils.pack(from_rank, from_file, to_rank, to_file)
    return move

def _decodeQueen(action: int) -> Optional[chess.Move]:

    _NUM_TYPES: int = 56 # = 8 directions * 7 squares max. distance

    #: Set of possible directions for a queen move, encoded as 
    #: (delta rank, delta square).
    _DIRECTIONS = utils.IndexedTuple(
        (+1,  0),
        (+1, +1),
        ( 0, +1),
        (-1, +1),
        (-1,  0),
        (-1, -1),
        ( 0, -1),
        (+1, -1),
    )
    from_rank, from_file, move_type = np.unravel_index(action, (8, 8, 73))
    
    is_queen_move = move_type < _NUM_TYPES

    if not is_queen_move:
        return None

    direction_idx, distance_idx = np.unravel_index(
        indices=move_type,
        shape=(8,7)
    )

    direction = _DIRECTIONS[direction_idx]
    distance = distance_idx + 1

    delta_rank = direction[0] * distance
    delta_file = direction[1] * distance

    to_rank = from_rank + delta_rank
    to_file = from_file + delta_file

    move = utils.pack(from_rank, from_file, to_rank, to_file)
    return move

def _decodeUnderPromotion(action):
    _NUM_TYPES: int = 9 # = 3 directions * 3 piece types (see below)

    #: Starting point of underpromotions in last dimension of 8 x 8 x 73 action 
    #: array.
    _TYPE_OFFSET: int = 64

    #: Set of possibel directions for an underpromotion, encoded as file delta.
    _DIRECTIONS = utils.IndexedTuple(
        -1,
        0,
        +1,
    )

    #: Set of possibel piece types for an underpromotion (promoting to a queen
    #: is implicitly encoded by the corresponding queen move).
    _PROMOTIONS = utils.IndexedTuple(
        chess.KNIGHT,
        chess.BISHOP,
        chess.ROOK,
    )

    from_rank, from_file, move_type = np.unravel_index(action, (8, 8, 73))

    is_underpromotion = (
        _TYPE_OFFSET <= move_type
        and move_type < _TYPE_OFFSET + _NUM_TYPES
    )

    if not is_underpromotion:
        return None

    underpromotion_type = move_type - _TYPE_OFFSET

    direction_idx, promotion_idx = np.unravel_index(
        indices=underpromotion_type,
        shape=(3,3)
    )

    direction = _DIRECTIONS[direction_idx]
    promotion = _PROMOTIONS[promotion_idx]

    to_rank = from_rank + 1
    to_file = from_file + direction

    move = utils.pack(from_rank, from_file, to_rank, to_file)
    move.promotion = promotion

    return move

#primary decoding function, the ones above are just helper functions
def decodeMove(action: int, board) -> chess.Move:
        move = _decodeQueen(action)
        is_queen_move = move is not None

        if not move:
            move = _decodeKnight(action)

        if not move:
            move = _decodeUnderPromotion(action)

        if not move:
            raise ValueError(f"{action} is not a valid action")

        # Actions encode moves from the perspective of the current player. If
        # this is the black player, the move must be reoriented.
        turn = board.turn
        
        if turn == False: #black to move
            move = utils.rotate(move)

        # Moving a pawn to the opponent's home rank with a queen move
        # is automatically assumed to be queen underpromotion. However,
        # since queenmoves has no reference to the board and can thus not
        # determine whether the moved piece is a pawn, you have to add this
        # information manually here
        if is_queen_move:
            to_rank = chess.square_rank(move.to_square)
            is_promoting_move = (
                (to_rank == 7 and turn == True) or 
                (to_rank == 0 and turn == False)
            )

            piece = board.piece_at(move.from_square)
            if piece is None: #NOTE I added this, not entirely sure if it's correct
                return None
            is_pawn = piece.piece_type == chess.PAWN

            if is_pawn and is_promoting_move:
                move.promotion = chess.QUEEN

        return move

def encodeBoard(board: chess.Board) -> np.array:
 """Converts a board to numpy array representation."""

 array = np.zeros((8, 8, 14), dtype=int)

 for square, piece in board.piece_map().items():
  rank, file = chess.square_rank(square), chess.square_file(square)
  piece_type, color = piece.piece_type, piece.color
 
  # The first six planes encode the pieces of the active player, 
  # the following six those of the active player's opponent. Since
  # this class always stores boards oriented towards the white player,
  # White is considered to be the active player here.
  offset = 0 if color == chess.WHITE else 6
  
  # Chess enumerates piece types beginning with one, which you have
  # to account for
  idx = piece_type - 1
 
  array[rank, file, idx + offset] = 1

 # Repetition counters
 array[:, :, 12] = board.is_repetition(2)
 array[:, :, 13] = board.is_repetition(3)

 return array
 

Comment charger les données

Dans la première partie, vous avez extrait des parties d'échecs, puis dans la deuxième partie, vous les avez codées afin qu'elles puissent être utilisées pour entraîner un modèle.

Vous chargez maintenant ces données dans les objets du chargeur de données PyTorch, elles sont donc disponibles pour que le modèle puisse s'entraîner. Si vous n'avez pas effectué la partie 1 ou 2 de ce didacticiel, vous pouvez trouver des fichiers de formation prêts à l'emploi dans ce dossier Google Drive.

Tout d’abord, définissez quelques hyperparamètres :

FRACTION_OF_DATA = 1
BATCH_SIZE = 4

La variable FRACTION_OF_DATA est là juste au cas où vous souhaiteriez entraîner le modèle rapidement et ne voudriez pas l'entraîner sur l'ensemble de données complet. Assurez-vous que cette valeur est > 0 et ≤ 1.

La variable BATCH_SIZE décide de la taille du lot sur laquelle le modèle s'entraîne. En général, une taille de lot plus élevée signifie que le modèle peut s'entraîner plus rapidement, mais la taille de votre lot est limitée par la puissance de votre GPU.

Je recommande de tester avec une taille de lot faible de 4, puis d'essayer de l'augmenter et de voir si la formation fonctionne toujours comme elle le devrait. Si vous obtenez une erreur de mémoire, essayez à nouveau de réduire la taille du lot.

Vous chargez ensuite les données avec le code ci-dessous. Assurez-vous que la structure de vos dossiers et le nom de vos fichiers sont corrects ici. Vous devriez avoir un dossier de données initial au même endroit où se trouve votre code.

Ensuite, dans ce dossier de données, vous devriez avoir un dossier preparedData , qui contient les fichiers sur lesquels vous souhaitez vous entraîner. Ces fichiers doivent être nommés moves{i}.npy et positions{i}.npy, où i est l'index du fichier. Si vous avez encodé les fichiers comme je l’ai fait plus tôt, tout devrait être correct.

#dataset

#loading training data

allMoves = []
allBoards = []

files = os.listdir('data/preparedData')
numOfEach = len(files) // 2 # half are moves, other half are positions

for i in range(numOfEach):
    try:
        moves = np.load(f"data/preparedData/moves{i}.npy", allow_pickle=True)
        boards = np.load(f"data/preparedData/positions{i}.npy", allow_pickle=True)
        if (len(moves) != len(boards)):
            print("ERROR ON i = ", i, len(moves), len(boards))
        allMoves.extend(moves)
        allBoards.extend(boards)
    except:
        print("error: could not load ", i, ", but is still going")

allMoves = np.array(allMoves)[:(int(len(allMoves) * FRACTION_OF_DATA))]
allBoards = np.array(allBoards)[:(int(len(allBoards) * FRACTION_OF_DATA))]
assert len(allMoves) == len(allBoards), "MUST BE OF SAME LENGTH"

#flatten out boards
# allBoards = allBoards.reshape(allBoards.shape[0], -1)

trainDataIdx = int(len(allMoves) * 0.8)

#NOTE transfer all data to GPU if available
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
allBoards = torch.from_numpy(np.asarray(allBoards)).to(device)
allMoves = torch.from_numpy(np.asarray(allMoves)).to(device)

training_set = torch.utils.data.TensorDataset(allBoards[:trainDataIdx], allMoves[:trainDataIdx])
test_set = torch.utils.data.TensorDataset(allBoards[trainDataIdx:], allMoves[trainDataIdx:])
# Create data loaders for your datasets; shuffle for training, not for validation

training_loader = torch.utils.data.DataLoader(training_set, batch_size=BATCH_SIZE, shuffle=True)
validation_loader = torch.utils.data.DataLoader(test_set, batch_size=BATCH_SIZE, shuffle=False)

Comment définir le modèle d'apprentissage profond

Vous pouvez ensuite définir l'architecture du modèle :

class Model(torch.nn.Module):

    def __init__(self):
        super(Model, self).__init__()
        self.INPUT_SIZE = 896 
        # self.INPUT_SIZE = 7*7*13 #NOTE changing input size for using cnns
        self.OUTPUT_SIZE = 4672 # = number of unique moves (action space)
        
        #can try to add CNN and pooling here (calculations taking into account spacial features)

        #input shape for sample is (8,8,14), flattened to 1d array of size 896
        # self.cnn1 = nn.Conv3d(4,4,(2,2,4), padding=(0,0,1))
        self.activation = torch.nn.ReLU()
        self.linear1 = torch.nn.Linear(self.INPUT_SIZE, 1000)
        self.linear2 = torch.nn.Linear(1000, 1000)
        self.linear3 = torch.nn.Linear(1000, 1000)
        self.linear4 = torch.nn.Linear(1000, 200)
        self.linear5 = torch.nn.Linear(200, self.OUTPUT_SIZE)
        self.softmax = torch.nn.Softmax(1) #use softmax as prob for each move, dim 1 as dim 0 is the batch dimension
 
    def forward(self, x): #x.shape = (batch size, 896)
        x = x.to(torch.float32)
        # x = self.cnn1(x) #for using cnns
        x = x.reshape(x.shape[0], -1)
        x = self.linear1(x)
        x = self.activation(x)
        x = self.linear2(x)
        x = self.activation(x)
        x = self.linear3(x)
        x = self.activation(x)
        x = self.linear4(x)
        x = self.activation(x)
        x = self.linear5(x)
        # x = self.softmax(x) #do not use softmax since you are using cross entropy loss
        return x

    def predict(self, board : chess.Board):
        """takes in a chess board and returns a chess.move object. NOTE: this function should definitely be written better, but it works for now"""
        with torch.no_grad():
            encodedBoard = encodeBoard(board)
            encodedBoard = encodedBoard.reshape(1, -1)
            encodedBoard = torch.from_numpy(encodedBoard)
            res = self.forward(encodedBoard)
            probs = self.softmax(res)

            probs = probs.numpy()[0] #do not want tensor anymore, 0 since it is a 2d array with 1 row

            #verify that move is legal and can be decoded before returning
            while len(probs) > 0: #try max 100 times, if not throw an error
                moveIdx = probs.argmax()
                try: #TODO should not have try here, but was a bug with idx 499 if it is black to move
                    uciMove = decodeMove(moveIdx, board)
                    if (uciMove is None): #could not decode
                        probs = np.delete(probs, moveIdx)
                        continue
                    move = chess.Move.from_uci(str(uciMove))
                    if (move in board.legal_moves): #if legal, return, else: loop continues after deleting the move
                        return move 
                except:
                    pass
                probs = np.delete(probs, moveIdx) #TODO probably better way to do this, but it is not too time critical as it is only for predictions
                                             #remove the move so its not chosen again next iteration
            
            #TODO can return random move here as well!
            return None #if no legal moves found, return None

Vous êtes libre de modifier l’architecture comme bon vous semble.

Ici, je viens de choisir quelques paramètres simples qui ont bien fonctionné, même s'il y a place à l'amélioration. Voici quelques exemples de modifications que vous pouvez apporter :

  1. Ajoutez des modules PyTorch CNN (n'oubliez pas de ne pas aplatir le tableau avant de les ajouter)
  2. Modifiez les fonctions d'activation dans les couches cachées. J'utilise maintenant ReLU, mais cela pourrait être modifié par exemple par Sigmoid ou Tanh, sur lequel vous pouvez en savoir plus ici.
  3. Modifiez le nombre de calques cachés. Lorsque vous modifiez cela, vous devez penser à ajouter une fonction d'activation entre chaque couche dans la fonction forward().
  4. Modifiez le nombre de neurones dans chaque couche cachée. Si vous souhaitez modifier le nombre de neurones, vous devez vous rappeler la règle selon laquelle le nombre de neurones dans la couche n doit être celui des neurones dans la couche n+1. Ainsi, par exemple, Linear1 prend 1 000 neurones et génère 2 000 neurones. Alors Linear2 doit accueillir 2000 neurones. Vous pouvez alors choisir librement le nombre de neurones de sortie sur linéaire2, mais le nombre doit correspondre au nombre de neurones d'entrée sur linéaire 3, et ainsi de suite. L'entrée de la couche 1 et la sortie de la dernière couche sont cependant définies avec les paramètres INPUT_SIZE et OUTPUT_SIZE.

En plus de l'architecture du modèle et des fonctions forward, qui sont obligatoires lors de la création d'un modèle profond, j'ai également défini une fonction predict(), pour faciliter l'attribution d'une position d'échecs au modèle, puis il produit le mouvement qu'il recommande.

Comment entraîner le modèle

Lorsque vous disposez de toutes les données requises et que le modèle est défini, vous pouvez commencer à entraîner le modèle. Tout d’abord, vous définissez une fonction pour entraîner une époque et enregistrer le meilleur modèle :

#helper functions for training
def train_one_epoch(model, optimizer, loss_fn, epoch_index, tb_writer):
    running_loss = 0.
    last_loss = 0.

    # Here, you use enumerate(training_loader) instead of
    # iter(training_loader) so that you can track the batch
    # index and do some intra-epoch reporting
    for i, data in enumerate(training_loader):

        # Every data instance is an input + label pair
        inputs, labels = data

        # Zero your gradients for every batch!
        optimizer.zero_grad()

        # Make predictions for this batch
        outputs = model(inputs)

        # Compute the loss and its gradients
        loss = loss_fn(outputs, labels)
        loss.backward()

        # Adjust learning weights
        optimizer.step()

        # Gather data and report
        running_loss += loss.item()
        if i % 1000 == 999:
            last_loss = running_loss / 1000 # loss per batch
            # print('  batch {} loss: {}'.format(i + 1, last_loss))
            tb_x = epoch_index * len(training_loader) + i + 1
            tb_writer.add_scalar('Loss/train', last_loss, tb_x)
            running_loss = 0.

    return last_loss

#the 3 functions below help store the best model you have created yet
def createBestModelFile():
    #first find best model if it exists:
    folderPath = Path('./savedModels')
    if (not folderPath.exists()):
        os.mkdir(folderPath)

    path = Path('./savedModels/bestModel.txt')

    if (not path.exists()):
        #create the files
        f = open(path, "w")
        f.write("10000000") #set to high number so it is overwritten with better loss
        f.write("\ntestPath")
        f.close()

def saveBestModel(vloss, pathToBestModel):
    f = open("./savedModels/bestModel.txt", "w")
    f.write(str(vloss.item()))
    f.write("\n")
    f.write(pathToBestModel)
    print("NEW BEST MODEL FOUND WITH LOSS:", vloss)

def retrieveBestModelInfo():
    f = open('./savedModels/bestModel.txt', "r")
    bestLoss = float(f.readline())
    bestModelPath = f.readline()
    f.close()
    return bestLoss, bestModelPath

Notez que cette fonction est essentiellement copiée de la documentation PyTorch, avec une légère modification en important le modèle, l'optimiseur et la fonction de perte en tant que paramètres de fonction.

Vous définissez ensuite les hyperparamètres comme ci-dessous. Notez que c’est quelque chose que vous pouvez régler pour améliorer davantage votre modèle.

#hyperparameters
EPOCHS = 60
LEARNING_RATE = 0.001
MOMENTUM = 0.9

Exécutez la formation avec le code ci-dessous :

#run training

createBestModelFile()

bestLoss, bestModelPath = retrieveBestModelInfo()

timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
writer = SummaryWriter('runs/fashion_trainer_{}'.format(timestamp))
epoch_number = 0

model = Model()
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=LEARNING_RATE, momentum=MOMENTUM)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)

best_vloss = 1_000_000.

for epoch in tqdm(range(EPOCHS)):
    if (epoch_number % 5 == 0):
        print('EPOCH {}:'.format(epoch_number + 1))

    # Make sure gradient tracking is on, and do a pass over the data
    model.train(True)
    avg_loss = train_one_epoch(model, optimizer, loss_fn, epoch_number, writer)

    running_vloss = 0.0
    # Set the model to evaluation mode, disabling dropout and using population
    # statistics for batch normalization.

    model.eval()

    # Disable gradient computation and reduce memory consumption.
    with torch.no_grad():
        for i, vdata in enumerate(validation_loader):
            vinputs, vlabels = vdata
            voutputs = model(vinputs)

            vloss = loss_fn(voutputs, vlabels)
            running_vloss += vloss

    avg_vloss = running_vloss / (i + 1)

    #only print every 5 epochs
    if epoch_number % 5 == 0:
        print('LOSS train {} valid {}'.format(avg_loss, avg_vloss))

    # Log the running loss averaged per batch
    # for both training and validation
    writer.add_scalars('Training vs. Validation Loss',
                    { 'Training' : avg_loss, 'Validation' : avg_vloss },
                    epoch_number + 1)
    writer.flush()

    # Track best performance, and save the model's state
    if avg_vloss < best_vloss:
        best_vloss = avg_vloss

        if (bestLoss > best_vloss): #if better than previous best loss from all models created, save it
            model_path = 'savedModels/model_{}_{}'.format(timestamp, epoch_number)
            torch.save(model.state_dict(), model_path)
            saveBestModel(best_vloss, model_path)

    epoch_number += 1

print("\n\nBEST VALIDATION LOSS FOR ALL MODELS: ", bestLoss)

Ce code est également fortement inspiré de la documentation PyTorch.

En fonction du nombre de couches dans votre modèle, du nombre de neurones dans les couches, du nombre d'époques, si vous utilisez un GPU ou non, et de plusieurs autres facteurs, le temps nécessaire à l'entraînement du modèle peut prendre de quelques secondes à plusieurs. heures.

Comme vous pouvez le voir ci-dessous, le temps estimé pour entraîner mon modèle ici était d'environ 2 minutes.

Comment tester votre modèle

Tester votre modèle est un élément essentiel pour vérifier si ce que vous avez créé fonctionne. J'ai implémenté deux façons de vérifier le modèle :

Vous-même contre l'IA

La première consiste à jouer contre l’IA. Ici, vous décidez d'un mouvement, puis vous laissez l'IA décider du mouvement, et ainsi de suite. Je recommande de le faire dans un bloc-notes afin que vous puissiez exécuter différentes cellules pour différentes actions.

Tout d’abord, chargez un modèle enregistré lors de la formation. Ici, j'obtiens le chemin d'accès au fichier à partir du fichier créé lors de l'exécution de la formation, qui stocke le chemin d'accès à votre meilleur modèle. Vous pouvez bien sûr également modifier manuellement le chemin vers le modèle que vous préférez utiliser.

saved_model = Model()

#load best model path from your file
f = open("./savedModels/bestModel.txt", "r")
bestLoss = float(f.readline())
model_path = f.readline()
f.close()

model.load_state_dict(torch.load(model_path))

Ensuite, définissez l'échiquier :

#play your own game
board = chess.Board()

Ensuite, vous pouvez agir en exécutant le code dans la cellule ci-dessous en modifiant la chaîne de la première ligne. Assurez-vous qu’il s’agit d’une démarche légale :

moveStr = "e2e4"
move = chess.Move.from_uci(moveStr)
board.push(move)

Ensuite, vous pouvez laisser l'IA décider du prochain mouvement avec la cellule ci-dessous :

#make ai move:
aiMove = saved_model.predict(board)
board.push(aiMove)
board

Cela imprimera également l'état du plateau afin que vous puissiez décider de votre propre mouvement plus facilement :

Continuez à faire un mouvement sur deux, laissez l'IA jouer un mouvement sur deux et voyez qui gagne !

Si vous voulez regretter un déménagement, vous pouvez utiliser :

#regret move:
board.pop()

Stockfish contre votre IA

Vous pouvez également automatiser le processus de test en définissant Stockfish sur un ELO spécifique et en laissant votre IA jouer contre lui :

Tout d'abord, chargez votre modèle (assurez-vous de remplacer le model_path par votre propre modèle) :

saved_model = Model()
model_path = "savedModels/model_20230702_150228_46" #TODO CHANGE THIS PATH
model.load_state_dict(torch.load(model_path))

Importez ensuite Stockfish et définissez-le sur un ELO spécifique. N'oubliez pas de remplacer le chemin d'accès au moteur Stockfish par votre propre chemin où vous avez le programme Stockfish) :

# test elo  against stockfish
ELO_RATING = 500
from stockfish import Stockfish
#TODO CHANGE PATH BELOW
stockfish = Stockfish(path=r"C:\Users\eivin\Documents\ownProgrammingProjects18062023\ChessEngine\stockfish\stockfish\stockfish-windows-2022-x86-64-avx2")
stockfish.set_elo_rating(ELO_RATING)

Une cote de 100 ELO est assez mauvaise et quelque chose que votre moteur, espérons-le, battra.

Jouez ensuite au jeu avec ce script, qui exécutera :

board = chess.Board()
allMoves = [] #list of strings for saving moves for setting pos for stockfish

MAX_NUMBER_OF_MOVES = 150
for i in range(MAX_NUMBER_OF_MOVES): #set a limit for the game

 #first my ai move
 try:
  move = saved_model.predict(board)
  board.push(move)
  allMoves.append(str(move)) #add so stockfish can see
 except:
  print("game over. You lost")
  break

 # #then get stockfish move
 stockfish.set_position(allMoves)
 stockfishMove = stockfish.get_best_move_time(3)
 allMoves.append(stockfishMove)
 stockfishMove = chess.Move.from_uci(stockfishMove)
 board.push(stockfishMove)

stockfish.reset_engine_parameters() #reset elo rating

board

Ce qui imprimera la position du plateau une fois le jeu terminé.

Réflexion sur les performances du moteur d'échecs

J'ai essayé d'entraîner le modèle sur environ 100 000 positions et mouvements et j'ai découvert que les performances du modèle ne sont toujours pas suffisantes pour battre un robot d'échecs de bas niveau (500 ELO).

Il peut y avoir plusieurs raisons à cela. Les échecs sont un jeu très compliqué, qui nécessite probablement beaucoup plus de mouvements et de positions pour développer un robot décent.

De plus, vous modifiez plusieurs éléments du bot que vous modifiez potentiellement pour l'améliorer. L'architecture peut être améliorée, par exemple en ajoutant un CNN au début de la fonction forward, afin que le bot prenne en compte les informations spatiales.

Vous pouvez également modifier le nombre de couches cachées dans les couches entièrement connectées ou la quantité de neurones dans chaque couche.

Un moyen sûr d'améliorer encore le modèle consiste à lui fournir davantage de données, car vous avez accès à une quantité infinie de données en utilisant le code minier présenté dans cet article.

De plus, je pense que cela montre qu'un moteur d'échecs d'apprentissage par imitation a besoin de beaucoup de données ou que former un moteur d'échecs uniquement à partir d'un apprentissage par imitation n'est peut-être pas une idée optimale.

Néanmoins, l'apprentissage par imitation peut être utilisé dans le cadre d'un moteur d'échecs, par exemple, si vous implémentez également des méthodes de recherche traditionnelles et ajoutez par-dessus l'apprentissage par imitation.

Conclusion

Bravo! Vous avez maintenant créé votre propre moteur d’échecs IA à partir de zéro, et j’espère que vous avez appris quelque chose en cours de route. Vous pouvez constamment améliorer ce moteur si vous souhaitez l'améliorer et vous assurer qu'il bat de mieux en mieux la concurrence.

Si vous souhaitez coder complètement, consultez mon GitHub.

Ce tutoriel a été initialement écrit partie par partie sur mon Medium, vous pouvez consulter chaque partie ici :

  • Partie 1 : Génération de l'ensemble de données
  • Partie 2 : Encodage avec la méthode AlphaZero
  • Partie 3 : Entraîner le modèle

Si vous êtes intéressé et souhaitez en savoir plus sur des sujets similaires, vous pouvez me retrouver sur :

  • ✅Moyen
  • ✅Twitter
  • ✅LinkedIn

Articles connexes