این مقاله ای در مورد چگونگی ایجاد یک موتور شطرنج هوش مصنوعی است که کاملاً از ابتدا شروع کردم تا موتور شطرنج هوش مصنوعی خودم را ساختم.

از آنجا که ایجاد یک موتور شطرنج هوش مصنوعی از ابتدا یک کار نسبتاً پیچیده است، این مقاله طولانی خواهد بود، اما با ما همراه باشید، زیرا محصولی که در نهایت با آن مواجه خواهید شد، پروژه جالبی برای نمایش خواهد بود!

پیش نیازها

این مقاله بیشتر مفاهیم را با جزئیات توضیح خواهد داد. با این حال، برخی از پیش نیازهای توصیه شده برای دنبال کردن این آموزش وجود دارد. شما باید با موارد زیر آشنا باشید:

  • پایتون
  • نحوه استفاده از ترمینال
  • نوت بوک ژوپیتر
  • مفاهیم اساسی هوش مصنوعی
  • قوانین شطرنج

از ابزارهای زیر نیز استفاده خواهم کرد:

  • پایتون
  • بسته های مختلف پایتون
  • استاک ماهی

فهرست مطالب

  • بخش 1: چگونه یک مجموعه داده تولید کنیم
  • بخش 2: نحوه رمزگذاری داده ها
  • قسمت 3: چگونه مدل هوش مصنوعی را آموزش دهیم
  • نتیجه

بخش 1: چگونه یک مجموعه داده تولید کنیم

در این بخش، من از Stockfish برای ایجاد مجموعه داده بزرگی از حرکات از موقعیت های مختلف استفاده خواهم کرد. این داده‌ها می‌توانند بعداً برای آموزش هوش مصنوعی شطرنج استفاده شوند.

چگونه Stockfish را دانلود کنیم

مهمترین جزء موتور شطرنج من Stockfish است، بنابراین به شما نحوه نصب آن را نشان خواهم داد.

به صفحه دانلود وب سایت Stockfish بروید و نسخه را برای شما دانلود کنید. من خودم از ویندوز استفاده می کنم، بنابراین نسخه ویندوز (سریعتر) را انتخاب کردم:

0_GxqQ42GNX21JB1GN
اگر رایانه شخصی ویندوز دارید، دکمه دانلود که با رنگ قرمز مشخص شده است را فشار دهید

پس از دانلود، فایل فشرده را در هر مکانی از رایانه شخصی خود که می خواهید موتور شطرنج شما باشد، استخراج کنید. به خاطر داشته باشید که آن را در کجا قرار می دهید زیرا برای مرحله بعدی به مسیر نیاز دارید.

نحوه ترکیب Stockfish با پایتون

اکنون باید موتور را نیز در پایتون قرار دهید. شما می‌توانید این کار را به صورت دستی انجام دهید، اما من استفاده از بسته Python Stockfish را آسان‌تر دیدم زیرا تمام عملکردهای مورد نیاز را دارد.

ابتدا بسته را از pip (ترجیحا در محیط مجازی شما):

pip install stockfish

سپس می توانید آن را با استفاده از دستور زیر وارد کنید:

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

توجه داشته باشید که باید مسیر خود را به فایل اجرایی Stockfish بدهید:

0_MSlKl_UJHCvdpje6
فایل اجرایی stockfish دومین فایل از پایین است

می توانید مسیر فایل را از ساختار پوشه کپی کنید، یا اگر در ویندوز 11 هستید، می توانید ctrl + shift + c را فشار دهید تا مسیر فایل به طور خودکار کپی شود.

عالی! اکنون Stockfish را در پایتون در دسترس دارید!

نحوه تولید مجموعه داده

اکنون به یک مجموعه داده نیاز دارید تا بتوانید موتور شطرنج هوش مصنوعی را آموزش دهید! شما می توانید این کار را با وادار کردن Stockfish به بازی و یادآوری هر موقعیت و حرکاتی که می توانید از آنجا انجام دهید انجام دهید.

با توجه به اینکه Stockfish یک موتور قوی شطرنج است، این حرکات در امتداد بهترین حرکات ممکن خواهد بود.

ابتدا یک بسته شطرنج و NumPy را نصب کنید (مقدارهای زیادی برای انتخاب وجود دارد، اما من از مورد زیر استفاده خواهم کرد).

هر خط را (به صورت جداگانه) در ترمینال وارد کنید:

pip install chess
pip install numpy

سپس بسته‌ها را وارد کنید (به یاد داشته باشید همانطور که قبلاً در این مقاله نشان داده شد، Stockfish را نیز وارد کنید):

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

شما همچنین به برخی از توابع کمکی در اینجا نیاز دارید:

#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\ownProgrammingProjects18061402\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

به یاد داشته باشید که مسیر فایل را در قسمت تغییر دهید findNextIdx عملکرد، زیرا این برای رایانه شما شخصی است.

یک پوشه داده در پوشه ای که کدنویسی می کنید ایجاد کنید و مسیر را کپی کنید (اما همچنان آن را حفظ کنید *.npy در پایان)

را checkEndCondition تابع از توابع بسته Chess pip برای بررسی اینکه آیا بازی قرار است به پایان برسد استفاده می کند.

را saveData تابع یک بازی را در فایل‌های npy ذخیره می‌کند که راهی بسیار بهینه برای ذخیره آرایه‌ها است.

تابع از findNextIdx عملکرد ذخیره در یک فایل جدید (به خاطر داشته باشید که در اینجا یک پوشه جدید به نام data برای ذخیره تمام داده ها ایجاد کنید).

در نهایت، runGame تابع باعث می شود تا بتوانید بازی ای را که ذخیره کرده اید اجرا کنید تا موقعیت های بعد را بررسی کنید numMoves تعداد حرکات

سپس می توانید در نهایت به عملکردی که بازی های شطرنج را استخراج می کند برسید:

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)

در اینجا شما ابتدا یک حداکثر حد تعیین می کنید تا یک بازی بی نهایت طول نکشد.

سپس، تعداد بازی‌هایی را که می‌خواهید اجرا کنید اجرا می‌کنید و مطمئن می‌شوید که هم Stockfish و هم بسته Chess pip به موقعیت شروع بازنشانی شده‌اند.

در مرحله بعد، 3 حرکت برتر پیشنهاد شده توسط Stockfish را دریافت می کنید و یکی از آنها را برای بازی انتخاب می کنید (80٪ تغییر برای بهترین حرکت، 15٪ تغییر برای دومین حرکت برتر، 5٪ تغییر برای سومین حرکت برتر). دلیل اینکه شما همیشه بهترین حرکت را انتخاب نمی کنید این است که انتخاب حرکت تصادفی تر باشد.

سپس، یک حرکت را انتخاب می‌کنید (حتی اگر کمتر از سه حرکت احتمالی وجود داشته باشد هیچ خطایی رخ نمی‌دهد)، موقعیت تخته را با استفاده از FEN (روشی برای رمزگذاری موقعیت شطرنج) و همچنین حرکت انجام شده از آن موقعیت را ذخیره می‌کنید.

اگر بازی تمام شد، حلقه را شکسته و تمام موقعیت ها و حرکات انجام شده از آن موقعیت ها را ذخیره می کنید. اگر بازی تمام نشد، تا پایان بازی به انجام حرکات ادامه دهید.

سپس می توانید یک بازی را با این موارد استخراج کنید:

mineGames(1)

به یاد داشته باشید که در اینجا یک پوشه داده ایجاد کنید، زیرا این جایی است که من بازی ها را ذخیره می کنم!

چگونه یک بازی ماین شده را بررسی کنیم

را اجرا کنید mineGames با استفاده از دستور زیر، یک بازی را استخراج کنید:

mineGames(1)

با استفاده از دستور زیر می توانید با یک تابع کمکی که قبلا نشان داده شده بود به این بازی دسترسی پیدا کنید:

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

با فرض وجود 12 حرکت در بازی، چیزی شبیه به این خواهید دید:

0_pjARgYsCMqZjj8lK
خروجی از موقعیت برد چاپ پس از 12 حرکت. (توجه داشته باشید که آخرین خط فقط با testBoard چاپ می شود، زیرا در یک نوت بوک Jupyter، یک متغیر اگر به تنهایی در انتهای سلول نوشته شود چاپ می شود).

و بس، اکنون می‌توانید هر تعداد بازی را که می‌خواهید استخراج کنید.

مدتی طول می کشد و پتانسیل هایی برای بهینه سازی این فرآیند استخراج وجود دارد، مانند موازی سازی شبیه سازی های بازی (زیرا هر بازی کاملاً از دیگری جدا است).

برای کد کامل قسمت 1، می توانید کد کامل را در GitHub من بررسی کنید.

بخش 2: نحوه رمزگذاری داده ها

در این قسمت شما حرکات و موقعیت های شطرنج را به همان روشی که DeepMind با AlphaZero انجام داد رمزگذاری می کنید!

من از داده هایی که در قسمت 1 این مجموعه جمع آوری کردید استفاده خواهم کرد.

به عنوان یادآوری، شما Stockfish را نصب کردید و مطمئن شدید که می توانید به آن در رایانه دسترسی داشته باشید. سپس آن را وادار کردید علیه خودش بازی کند، در حالی که تمام حرکات و موقعیت ها را ذخیره می کردید.

شما اکنون یک مشکل یادگیری نظارت شده دارید، زیرا ورودی موقعیت فعلی است و برچسب (حرکت صحیح از موقعیت ها) حرکتی است که Stockfish تصمیم گرفت بهترین حرکت است.

نحوه نصب و وارد کردن بسته ها

ابتدا باید تمام بسته های مورد نیاز را نصب و وارد کنید، که اگر قسمت 1 این مجموعه را دنبال کرده باشید، ممکن است برخی از آنها را قبلا داشته باشید.

همه واردات در زیر آمده است – به یاد داشته باشید هنگام نصب از طریق فقط یک خط را وارد کنید pip:

pip install numpy
pip install gym-chess
pip install chess

علاوه بر این، از آن زمان باید یک تغییر کوچک در یکی از فایل های بسته gym-chess ایجاد کنید np.int استفاده شد که اکنون منسوخ شده است.

در فایل با مسیر نسبی (از محیط مجازی) venv\Lib\site-packages\gym_chess\alphazero\board_encoding.py جایی که venv نام محیط مجازی من است، شما باید “np.int” را جستجو کنید و “int” را جایگزین آنها کنید.

اگر این کار را نکنید، یک پیام خطایی خواهید دید که نشان می دهد np.int منسوخ شده است.

من همچنین مجبور شدم VS Code را پس از جایگزینی “np.int” با “int” دوباره راه اندازی کنم تا کار کند.

تمام وارداتی که نیاز دارید در زیر آمده است:

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

و سپس شما همچنین باید محیط ورزشگاه را برای رمزگذاری و رمزگشایی حرکات ایجاد کنید:

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

نحوه کدگذاری موقعیت ها و حرکات تخته

رمزگذاری یک عنصر مهم در هوش مصنوعی است، زیرا به ما امکان می دهد مشکلات را به روشی قابل خواندن برای هوش مصنوعی نشان دهیم.

به جای تصویری از یک صفحه شطرنج، یا رشته ای که یک حرکت شطرنج مانند “d2d4” را نشان می دهد، در عوض با استفاده از آرایه ها (فهرست اعداد) آن را نشان می دهید.

پیدا کردن نحوه انجام این کار به صورت دستی بسیار چالش برانگیز است، اما خوشبختانه برای ما، بسته Gym-chess Python قبلاً این مشکل را برای ما حل کرده است.

من قصد ندارم وارد جزئیات بیشتر در مورد نحوه کدگذاری آن‌ها شوم، اما می‌توانید با استفاده از کد زیر ببینید که یک موقعیت با یک آرایه شکل (8،8،119) نشان داده می‌شود، و تمام حرکات ممکن با یک آرایه (4672) ارائه می‌شوند. (1 ستون با 4672 مقدار).

اگر می‌خواهید در این مورد بیشتر بخوانید، می‌توانید مقاله AlphaZero را بررسی کنید، اگرچه این مقاله کاملاً پیچیده‌ای برای درک کامل است.

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

کدام خروجی ها:

0_yDTpZm519oQl-fJm
خروجی از حالت چاپ (خط اول) و فضای عمل (خط دوم)

همچنین می توانید رمزگذاری یک حرکت را بررسی کنید. از نماد رشته تا نماد رمزگذاری شده. اطمینان حاصل کنید که محیط را بازنشانی کنید زیرا ممکن است خطا بدهد اگر این کار را نکنید:

#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')

با استفاده از این، اکنون می توانید توابعی برای رمزگذاری حرکات و موقعیت هایی که از قسمت 1 ذخیره کرده اید، در جایی که یک مجموعه داده را ایجاد کرده اید، داشته باشید.

نحوه ایجاد توابع برای رمزگذاری حرکات

این توابع از بسته Gym-Chess کپی شده اند، اما با ترفندهای کوچک، بنابراین به یک کلاس وابسته نیست.

من به صورت دستی این توابع را تغییر دادم تا رمزگذاری آسان تر باشد. من در مورد درک کامل این توابع نگران نباشم، زیرا آنها بسیار پیچیده هستند.

فقط بدانید که آنها راهی هستند برای اطمینان از اینکه حرکت هایی که انسان ها می فهمند، به روشی تبدیل می شوند که رایانه ها می توانند درک کنند.

#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

بنابراین اکنون می توانید یک حرکت را به عنوان یک رشته (به عنوان مثال: “e2e4” برای حرکت از e2 به e4) بدهید و یک عدد (نسخه کدگذاری شده حرکت) را خروجی می دهد.

پیشنهاد می‌کنیم بخوانید:  دکمه راه اندازی را با جاوا اسکریپت بر روی کلید Enter کلیک کنید

چگونه یک تابع برای رمزگذاری موقعیت ها ایجاد کنیم

رمزگذاری موقعیت ها کمی دشوارتر است. من تابعی را از بسته gym-chess (“encodeBoard”) گرفتم زیرا در استفاده مستقیم از بسته مشکلاتی داشتم. تابعی که کپی کردم در زیر است:

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)

من هم اضافه کردم encodeBoardFromFen تابع، از آنجایی که تابع کپی شده نیاز به یک صفحه شطرنج دارد که با استفاده از بسته Python Chess نمایش داده می‌شود، بنابراین من ابتدا از FEN-notation (روشی برای رمزگذاری موقعیت‌های شطرنج به یک رشته – نمی‌توانید از آن استفاده کنید زیرا نیاز دارید رمزگذاری به صورت اعداد باشد) به یک صفحه شطرنج که در آن بسته داده شده است.

سپس تمام آنچه را که برای رمزگذاری تمام فایل های خود نیاز دارید در اختیار دارید.

نحوه خودکار کردن رمزگذاری برای تمام فایل های داده خام

اکنون که می‌توانید حرکت‌ها و موقعیت‌ها را رمزگذاری کنید، این فرآیند را برای همه فایل‌های موجود در پوشه خود که از قسمت 1 این سری تولید کرده‌اید، خودکار خواهید کرد. این شامل یافتن تمام فایل هایی است که باید داده ها را در آنها رمزگذاری کنید و آنها را در فایل های جدید ذخیره کنید.

توجه داشته باشید که از قسمت 1 ساختار پوشه را کمی تغییر دادم.

من الان یک پدر و مادر دارم Data پوشه، و در این پوشه، من را دارم rawData، که حرکات در قالب رشته و موقعیت ها در قالب FEN (از قسمت 1) است.

من هم دارم preparedData پوشه زیر پوشه داده، جایی که حرکت ها و موقعیت های کدگذاری شده ذخیره می شوند.

توجه داشته باشید که حرکت ها و موقعیت های کدگذاری شده در فایل های جداگانه ذخیره می شوند زیرا رمزگذاری ها دارای ابعاد متفاوتی هستند.

0_oiZbBdWwveJNMCPe
ساختار پوشه برای داده ها مطمئن شوید که دو پوشه به نام‌های readyData و rawData در پوشه Data وجود دارد. پوشه Data در همان سطح فایل های نوت بوک شما قرار دارد.
#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)

من ابتدا محیط را ایجاد می کنم و آن را ریست می کنم.

سپس، من تمام فایل های داده خام ساخته شده از قسمت 1 را باز می کنم و آن را رمزگذاری می کنم. من هم این کار را در یک انجام می دهم try/catch بیانیه، همانطور که من گاهی اوقات خطاهایی را در رمزگذاری حرکت می بینم.

اولین عبارت استثنا برای این است که اگر حرکتی نادیده گرفته شود (بنابراین برنامه فکر می کند نوبت اشتباه است). اگر این اتفاق بیفتد، رمزگذاری کار نخواهد کرد، بنابراین عبارت استثنا، چرخش را تغییر می‌دهد و دوباره تلاش می‌کند. این بهینه ترین کد نیست، اما رمزگذاری بخش کوچکی از کل زمان اجرا برای ایجاد یک موتور شطرنج هوش مصنوعی است و بنابراین قابل قبول است.

مطمئن شوید که ساختار پوشه درستی دارید و تمام پوشه های مختلف را ایجاد کرده اید. در غیر این صورت یک خطا دریافت خواهید کرد.

اکنون صفحه شطرنج و حرکات خود را رمزگذاری کرده اید. اگر می خواهید، می توانید کد کامل این قسمت را در GitHub من بررسی کنید.

قسمت 3: چگونه مدل هوش مصنوعی را آموزش دهیم

این سومین و آخرین بخش در ساخت موتور شطرنج هوش مصنوعی شماست!

در قسمت 1 یاد گرفتید که چگونه یک مجموعه داده ایجاد کنید و در قسمت 2 به رمزگذاری مجموعه داده نگاه کردید تا بتوان از آن برای یک هوش مصنوعی استفاده کرد.

اکنون از این مجموعه داده کدگذاری شده برای آموزش هوش مصنوعی خود با استفاده از PyTorch استفاده خواهید کرد!

نحوه وارد کردن بسته ها

مثل همیشه، تمام وارداتی که در آموزش استفاده خواهد شد را دارید. اکثر آنها ساده هستند، اما شما باید PyTorch را نصب کنید، که توصیه می کنم با استفاده از این وب سایت نصب کنید.

در اینجا می‌توانید کمی به پایین اسکرول کنید، جایی که برخی از گزینه‌ها را می‌بینید که از چه ساخت و سیستم عاملی استفاده می‌کنید.

پس از انتخاب گزینه هایی که برای شما اعمال می شود، کدی دریافت خواهید کرد که می توانید برای نصب PyTorch در ترمینال قرار دهید.

گزینه هایی را که انتخاب کردم در تصویر زیر مشاهده می کنید، اما به طور کلی توصیه می کنم از ساخت پایدار استفاده کنید و سیستم عامل خود را انتخاب کنید.

سپس، بسته ای را که بیشتر به آن عادت دارید انتخاب کنید (Conda یا pip احتمالاً ساده ترین است زیرا می توانید آن را در ترمینال بچسبانید).

CUDA 11.7/11.8 را انتخاب کنید (مهم نیست کدام یک) و با استفاده از دستور داده شده در پایین نصب کنید.

0_UJVBkAt40X6-FXuV
انتخاب های من هنگام نصب PyTorch.

سپس می توانید تمام بسته های خود را با کد زیر وارد کنید:

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

نحوه نصب CUDA

این یک مرحله اختیاری است که به شما امکان می دهد از پردازنده گرافیکی خود برای آموزش سریعتر مدل خود استفاده کنید. نیازی به این کار نیست، اما در زمان آموزش هوش مصنوعی در زمان شما صرفه جویی می کند.

نحوه نصب CUDA بسته به سیستم عامل شما متفاوت است، اما من از ویندوز استفاده می کنم و این آموزش را دنبال کردم.

اگر از MacOS یا لینوکس استفاده می‌کنید، می‌توانید با جستجو کردن در گوگل، آموزش «نصب CUDA Mac/Linux» را پیدا کنید.

برای بررسی اینکه آیا CUDA در دسترس دارید (GPU شما در دسترس است)، می توانید از این کد استفاده کنید:

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

کدام خروجی ها True اگر GPU شما در دسترس است. با این حال، اگر پردازنده گرافیکی در دسترس ندارید، نگران نباشید، تنها نقطه ضعف در اینجا این است که آموزش مدل بیشتر طول می کشد، که در انجام پروژه های سرگرمی مانند این چندان مهم نیست.

نحوه ایجاد روش های رمزگذاری

سپس چند روش کمکی برای رمزگذاری و رمزگشایی از بسته Python Gym-Chess تعریف می کنم.

مجبور شدم تغییراتی در بسته ایجاد کنم تا کار کند. بیشتر کدها از بسته کپی می شوند، فقط با چند ترفند کوچک باعث می شود کد به کلاس و غیره وابسته نباشد.

توجه داشته باشید که لازم نیست تمام کدهای زیر را درک کنید، زیرا روشی که Deepmind تمام حرکات را در شطرنج رمزگذاری می کند، پیچیده است.

#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
 

نحوه بارگذاری داده ها

در قسمت 1، چند بازی شطرنج را استخراج کردید، و سپس در قسمت 2، آن را رمزگذاری کردید تا بتوان از آن برای آموزش یک مدل استفاده کرد.

شما اکنون این داده ها را در اشیاء بارگذار داده PyTorch بارگیری می کنید، بنابراین برای آموزش مدل در دسترس است. اگر قسمت 1 یا 2 این آموزش را انجام نداده اید، می توانید چند فایل آموزشی آماده را در این پوشه Google Drive پیدا کنید.

ابتدا چند فراپارامتر را تعریف کنید:

FRACTION_OF_DATA = 1
BATCH_SIZE = 4

را FRACTION_OF_DATA متغیر، فقط در صورتی وجود دارد که بخواهید مدل را سریع آموزش دهید و نمی خواهید آن را بر روی مجموعه داده کامل آموزش دهید. مطمئن شوید که این مقدار > 0 و ≤ 1 باشد.

پیشنهاد می‌کنیم بخوانید:  دریافت تاریخ امروز در YYYY-MM-DD در Python

را BATCH_SIZE متغیر اندازه دسته ای را که مدل آموزش می دهد تعیین می کند. به طور کلی، اندازه دسته‌ای بالاتر به این معنی است که مدل می‌تواند سریع‌تر تمرین کند، اما اندازه دسته شما توسط قدرت GPU شما محدود می‌شود.

من توصیه می‌کنم با سایز کم دسته 4 تست کنید و سپس سعی کنید آن را افزایش دهید و ببینید که آیا آموزش همچنان همانطور که باید کار می‌کند یا خیر. اگر یک نوع خطای حافظه را دریافت کردید، دوباره اندازه دسته را کاهش دهید.

سپس داده ها را با کد زیر بارگذاری می کنید. مطمئن شوید که ساختار پوشه و نام فایل شما در اینجا صحیح است. شما باید یک پوشه داده اولیه در همان جایی که کد شما قرار دارد داشته باشید.

سپس در داخل این پوشه داده، باید یک داشته باشید preparedData پوشه، که حاوی فایل‌هایی است که می‌خواهید روی آن‌ها آموزش دهید. این فایل ها باید نامگذاری شوند moves{i}.npy و positions{i}.npy، جایی که i نمایه فایل است. اگر فایل ها را مانند قبل کدگذاری کرده اید، همه چیز باید درست باشد.

0_3LT9odIm09DPFS59
ساختار پوشه زرد پوشه‌ها و فیروزه‌ای فایل‌ها هستند.
#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)

نحوه تعریف مدل یادگیری عمیق

سپس می توانید معماری مدل را تعریف کنید:

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

شما آزاد هستید که معماری را هر طور که دوست دارید تغییر دهید.

در اینجا، من فقط برخی از پارامترهای ساده را انتخاب کردم که به خوبی کار می کردند، اگرچه جایی برای بهبود وجود دارد. چند نمونه از تغییراتی که می توانید ایجاد کنید عبارتند از:

  1. ماژول‌های PyTorch CNN را اضافه کنید (به یاد داشته باشید که قبل از اضافه کردن آنها آرایه را صاف نکنید)
  2. توابع فعال سازی را در لایه های مخفی تغییر دهید. من اکنون از ReLU استفاده می کنم، اما می توان آن را به عنوان مثال Sigmoid یا Tanh تغییر داد، که می توانید در اینجا اطلاعات بیشتری در مورد آنها بخوانید.
  3. تعداد لایه های مخفی را تغییر دهید. هنگام تغییر این، باید به یاد داشته باشید که یک تابع فعال سازی را بین هر لایه در آن اضافه کنید forward() تابع.
  4. تعداد نورون ها را در هر لایه پنهان تغییر دهید. اگر می‌خواهید تعداد نورون‌ها را تغییر دهید، باید این قانون را به خاطر بسپارید که تعداد نورون‌های بیرون در لایه n باید نورون‌های درون لایه n+1 باشد. بنابراین برای مثال، linear1 1000 نورون را می گیرد و 2000 نورون را خروجی می دهد. سپس linear2 باید 2000 نورون را جذب کند. سپس می‌توانید آزادانه تعداد نورون‌های خروجی را در linear2 انتخاب کنید، اما مقدار باید با تعداد نورون‌های ورودی در خطی 3 و غیره مطابقت داشته باشد. ورودی لایه 1 و خروجی لایه آخر با پارامترها تنظیم می شوند INPUT_SIZE، و OUTPUT_SIZE.

علاوه بر معماری مدل و توابع فوروارد که هنگام ایجاد یک مدل عمیق الزامی است، a را نیز تعریف کردم predict() عملکرد، تا دادن موقعیت شطرنج به مدل آسان‌تر شود و سپس حرکتی که توصیه می‌کند را خروجی می‌دهد.

نحوه آموزش مدل

هنگامی که تمام داده های مورد نیاز را دارید و مدل تعریف می شود، می توانید آموزش مدل را شروع کنید. ابتدا یک تابع برای آموزش یک دوره و ذخیره بهترین مدل تعریف می کنید:

#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

توجه داشته باشید که این تابع در اصل از اسناد PyTorch کپی شده است، با یک تغییر جزئی با وارد کردن مدل، بهینه ساز و تابع ضرر به عنوان پارامترهای تابع.

سپس ابرپارامترها را مانند زیر تعریف می کنید. توجه داشته باشید که این چیزی است که می توانید برای بهبود بیشتر مدل خود تنظیم کنید.

#hyperparameters
EPOCHS = 60
LEARNING_RATE = 0.001
MOMENTUM = 0.9

آموزش را با کد زیر اجرا کنید:

#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)

این کد همچنین به شدت از اسناد PyTorch الهام گرفته شده است.

بسته به تعداد لایه‌های مدل، تعداد نورون‌ها در لایه‌ها، تعداد دوره‌ها، اگر از GPU استفاده می‌کنید یا نه، و چندین عامل دیگر، زمان شما برای آموزش مدل می‌تواند از چند ثانیه تا چند ثانیه طول بکشد. ساعت ها.

همانطور که در زیر می بینید، زمان تخمینی برای آموزش مدل من در اینجا حدود 2 دقیقه بود.

0_-JEKRkXNUxXy4CYh
فیلم آموزش مدل. با استفاده از LICEcap ضبط شده است

چگونه مدل خود را تست کنیم

آزمایش مدل شما بخش مهمی از بررسی اینکه آیا چیزی که ایجاد کرده اید کار می کند یا خیر. من دو روش برای بررسی مدل پیاده سازی کرده ام:

خودت در مقابل هوش مصنوعی

اولین راه این است که خودتان را در برابر هوش مصنوعی بازی کنید. در اینجا شما تصمیم می گیرید که یک حرکت را انجام دهید، سپس به هوش مصنوعی اجازه می دهید حرکت را تعیین کند و غیره. توصیه می کنم این کار را در یک نوت بوک انجام دهید تا بتوانید سلول های مختلف را برای اقدامات مختلف اجرا کنید.

ابتدا مدلی را بارگذاری کنید که از آموزش ذخیره شده است. در اینجا، من مسیر فایل را از فایلی که هنگام اجرای آموزش ایجاد شده است، دریافت می کنم، که مسیر بهترین مدل شما را ذخیره می کند. البته شما همچنین می توانید به صورت دستی مسیر را به مدلی که ترجیح می دهید استفاده کنید تغییر دهید.

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))

سپس صفحه شطرنج را تعریف کنید:

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

سپس می توانید با اجرای کد موجود در سلول زیر با تغییر رشته در خط اول حرکتی انجام دهید. مطمئن شوید که این یک حرکت قانونی است:

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

سپس می توانید به هوش مصنوعی اجازه دهید حرکت بعدی را با سلول زیر تعیین کند:

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

این حالت تخته را نیز چاپ می کند تا بتوانید راحت تر تصمیم بگیرید که حرکت خود را انجام دهید:

0_mkYWyk2zoU01fyEj
چاپ وضعیت برد پس از حرکت هوش مصنوعی

به انجام هر حرکت دیگری ادامه دهید، اجازه دهید هوش مصنوعی هر حرکت دیگری را انجام دهد و ببینید چه کسی برنده می شود!

اگر می خواهید از حرکتی پشیمان شوید، می توانید از موارد زیر استفاده کنید:

#regret move:
board.pop()

Stockfish در مقابل هوش مصنوعی شما

همچنین می‌توانید با تنظیم Stockfish روی یک ELO خاص، فرآیند آزمایش را خودکار کنید و اجازه دهید هوش مصنوعی در برابر آن بازی کند:

ابتدا مدل خود را بارگذاری کنید (حتماً آن را تغییر دهید model_path به مدل خودتون):

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

سپس Stockfish را وارد کنید و آن را روی یک ELO خاص تنظیم کنید. به یاد داشته باشید که مسیر موتور Stockfish را به مسیر خود که در آن برنامه Stockfish دارید تغییر دهید:

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

رتبه 100 ELO بسیار بد است و امیدواریم موتور شما از آن عبور کند.

سپس بازی را با این اسکریپت اجرا کنید که اجرا می شود:

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

که بعد از پایان بازی موقعیت تابلو را چاپ می کند.

0_TmLzgIp2R5_bNyy7
بعد از اینکه موتور شطرنج شما یک بازی را به Stockfish باخت، موقعیت خود را پیدا کنید

تأمل در عملکرد موتور شطرنج

من سعی کردم مدل را در حدود 100 هزار موقعیت و حرکت آموزش دهم و متوجه شدم که عملکرد مدل هنوز برای شکست دادن یک ربات شطرنج سطح پایین (500 ELO) کافی نیست.

این می تواند دلایل مختلفی داشته باشد. شطرنج یک بازی بسیار پیچیده است، که احتمالاً به حرکات و موقعیت های بسیار بیشتری نیاز دارد تا یک ربات مناسب توسعه یابد.

علاوه بر این، چندین عنصر از رباتی که شما تغییر می دهید وجود دارد که به طور بالقوه برای بهبود آن تغییر می کند. معماری را می توان بهبود بخشید، برای مثال با افزودن یک CNN در ابتدای تابع جلو، به طوری که ربات اطلاعات مکانی را دریافت کند.

همچنین می‌توانید تعداد لایه‌های پنهان در لایه‌های کاملاً متصل یا تعداد نورون‌های هر لایه را تغییر دهید.

یک راه مطمئن برای بهبود بیشتر مدل، تغذیه داده های بیشتر به آن است، زیرا با استفاده از کد استخراج در این مقاله به تعداد نامحدودی از داده ها دسترسی دارید.

علاوه بر این، من فکر می کنم این نشان می دهد که یک موتور شطرنج یادگیری تقلیدی یا به داده های زیادی نیاز دارد یا آموزش یک موتور شطرنج صرفاً از طریق یادگیری تقلیدی ممکن است ایده مطلوبی نباشد.

با این حال، یادگیری تقلیدی می تواند به عنوان بخشی از موتور شطرنج مورد استفاده قرار گیرد، برای مثال، اگر روش های جستجوی سنتی را نیز پیاده کنید، و یادگیری تقلید را به آن اضافه کنید.

نتیجه

تبریک میگم شما اکنون موتور شطرنج هوش مصنوعی خود را از ابتدا ساخته اید و امیدوارم در این راه چیزی یاد گرفته باشید. اگر می‌خواهید این موتور را بهبود ببخشید، می‌توانید دائماً آن را بهتر کنید و مطمئن شوید که رقابت بهتر و بهتری را پشت سر می‌گذارد.

اگر می‌خواهید کد کامل کنید، به GitHub من سر بزنید.

این آموزش در اصل قسمت به قسمت در رسانه من نوشته شده است، می توانید هر قسمت را در اینجا مشاهده کنید:

  • بخش 1: تولید مجموعه داده
  • قسمت 2: رمزگذاری با روش AlphaZero
  • قسمت 3: آموزش مدل

اگر علاقه مند هستید و می خواهید در مورد موضوعات مشابه بیشتر بدانید، می توانید من را در این آدرس بیابید: