از طریق منوی جستجو مطلب مورد نظر خود در وبلاگ را به سرعت پیدا کنید
یک موتور شطرنج هوش مصنوعی خودبازی را از ابتدا با آموزش تقلید ایجاد کنید
سرفصلهای مطلب
این مقاله ای در مورد چگونگی ایجاد یک موتور شطرنج هوش مصنوعی است که کاملاً از ابتدا شروع کردم تا موتور شطرنج هوش مصنوعی خودم را ساختم.
از آنجا که ایجاد یک موتور شطرنج هوش مصنوعی از ابتدا یک کار نسبتاً پیچیده است، این مقاله طولانی خواهد بود، اما با ما همراه باشید، زیرا محصولی که در نهایت با آن مواجه خواهید شد، پروژه جالبی برای نمایش خواهد بود!
پیش نیازها
این مقاله بیشتر مفاهیم را با جزئیات توضیح خواهد داد. با این حال، برخی از پیش نیازهای توصیه شده برای دنبال کردن این آموزش وجود دارد. شما باید با موارد زیر آشنا باشید:
- پایتون
- نحوه استفاده از ترمینال
- نوت بوک ژوپیتر
- مفاهیم اساسی هوش مصنوعی
- قوانین شطرنج
از ابزارهای زیر نیز استفاده خواهم کرد:
- پایتون
- بسته های مختلف پایتون
- استاک ماهی
فهرست مطالب
- بخش 1: چگونه یک مجموعه داده تولید کنیم
- بخش 2: نحوه رمزگذاری داده ها
- قسمت 3: چگونه مدل هوش مصنوعی را آموزش دهیم
- نتیجه
بخش 1: چگونه یک مجموعه داده تولید کنیم
در این بخش، من از Stockfish برای ایجاد مجموعه داده بزرگی از حرکات از موقعیت های مختلف استفاده خواهم کرد. این دادهها میتوانند بعداً برای آموزش هوش مصنوعی شطرنج استفاده شوند.
چگونه Stockfish را دانلود کنیم
مهمترین جزء موتور شطرنج من Stockfish است، بنابراین به شما نحوه نصب آن را نشان خواهم داد.
به صفحه دانلود وب سایت Stockfish بروید و نسخه را برای شما دانلود کنید. من خودم از ویندوز استفاده می کنم، بنابراین نسخه ویندوز (سریعتر) را انتخاب کردم:
پس از دانلود، فایل فشرده را در هر مکانی از رایانه شخصی خود که می خواهید موتور شطرنج شما باشد، استخراج کنید. به خاطر داشته باشید که آن را در کجا قرار می دهید زیرا برای مرحله بعدی به مسیر نیاز دارید.
نحوه ترکیب 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 بدهید:
می توانید مسیر فایل را از ساختار پوشه کپی کنید، یا اگر در ویندوز 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 حرکت در بازی، چیزی شبیه به این خواهید دید:
و بس، اکنون میتوانید هر تعداد بازی را که میخواهید استخراج کنید.
مدتی طول می کشد و پتانسیل هایی برای بهینه سازی این فرآیند استخراج وجود دارد، مانند موازی سازی شبیه سازی های بازی (زیرا هر بازی کاملاً از دیگری جدا است).
برای کد کامل قسمت 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)
کدام خروجی ها:
همچنین می توانید رمزگذاری یک حرکت را بررسی کنید. از نماد رشته تا نماد رمزگذاری شده. اطمینان حاصل کنید که محیط را بازنشانی کنید زیرا ممکن است خطا بدهد اگر این کار را نکنید:
#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) بدهید و یک عدد (نسخه کدگذاری شده حرکت) را خروجی می دهد.
چگونه یک تابع برای رمزگذاری موقعیت ها ایجاد کنیم
رمزگذاری موقعیت ها کمی دشوارتر است. من تابعی را از بسته 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
پوشه زیر پوشه داده، جایی که حرکت ها و موقعیت های کدگذاری شده ذخیره می شوند.
توجه داشته باشید که حرکت ها و موقعیت های کدگذاری شده در فایل های جداگانه ذخیره می شوند زیرا رمزگذاری ها دارای ابعاد متفاوتی هستند.
#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 را انتخاب کنید (مهم نیست کدام یک) و با استفاده از دستور داده شده در پایین نصب کنید.
سپس می توانید تمام بسته های خود را با کد زیر وارد کنید:
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 باشد.
را BATCH_SIZE
متغیر اندازه دسته ای را که مدل آموزش می دهد تعیین می کند. به طور کلی، اندازه دستهای بالاتر به این معنی است که مدل میتواند سریعتر تمرین کند، اما اندازه دسته شما توسط قدرت GPU شما محدود میشود.
من توصیه میکنم با سایز کم دسته 4 تست کنید و سپس سعی کنید آن را افزایش دهید و ببینید که آیا آموزش همچنان همانطور که باید کار میکند یا خیر. اگر یک نوع خطای حافظه را دریافت کردید، دوباره اندازه دسته را کاهش دهید.
سپس داده ها را با کد زیر بارگذاری می کنید. مطمئن شوید که ساختار پوشه و نام فایل شما در اینجا صحیح است. شما باید یک پوشه داده اولیه در همان جایی که کد شما قرار دارد داشته باشید.
سپس در داخل این پوشه داده، باید یک داشته باشید preparedData
پوشه، که حاوی فایلهایی است که میخواهید روی آنها آموزش دهید. این فایل ها باید نامگذاری شوند moves{i}.npy
و positions{i}.npy
، جایی که i نمایه فایل است. اگر فایل ها را مانند قبل کدگذاری کرده اید، همه چیز باید درست باشد.
#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
شما آزاد هستید که معماری را هر طور که دوست دارید تغییر دهید.
در اینجا، من فقط برخی از پارامترهای ساده را انتخاب کردم که به خوبی کار می کردند، اگرچه جایی برای بهبود وجود دارد. چند نمونه از تغییراتی که می توانید ایجاد کنید عبارتند از:
- ماژولهای PyTorch CNN را اضافه کنید (به یاد داشته باشید که قبل از اضافه کردن آنها آرایه را صاف نکنید)
- توابع فعال سازی را در لایه های مخفی تغییر دهید. من اکنون از ReLU استفاده می کنم، اما می توان آن را به عنوان مثال Sigmoid یا Tanh تغییر داد، که می توانید در اینجا اطلاعات بیشتری در مورد آنها بخوانید.
- تعداد لایه های مخفی را تغییر دهید. هنگام تغییر این، باید به یاد داشته باشید که یک تابع فعال سازی را بین هر لایه در آن اضافه کنید
forward()
تابع. - تعداد نورون ها را در هر لایه پنهان تغییر دهید. اگر میخواهید تعداد نورونها را تغییر دهید، باید این قانون را به خاطر بسپارید که تعداد نورونهای بیرون در لایه 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 دقیقه بود.
چگونه مدل خود را تست کنیم
آزمایش مدل شما بخش مهمی از بررسی اینکه آیا چیزی که ایجاد کرده اید کار می کند یا خیر. من دو روش برای بررسی مدل پیاده سازی کرده ام:
خودت در مقابل هوش مصنوعی
اولین راه این است که خودتان را در برابر هوش مصنوعی بازی کنید. در اینجا شما تصمیم می گیرید که یک حرکت را انجام دهید، سپس به هوش مصنوعی اجازه می دهید حرکت را تعیین کند و غیره. توصیه می کنم این کار را در یک نوت بوک انجام دهید تا بتوانید سلول های مختلف را برای اقدامات مختلف اجرا کنید.
ابتدا مدلی را بارگذاری کنید که از آموزش ذخیره شده است. در اینجا، من مسیر فایل را از فایلی که هنگام اجرای آموزش ایجاد شده است، دریافت می کنم، که مسیر بهترین مدل شما را ذخیره می کند. البته شما همچنین می توانید به صورت دستی مسیر را به مدلی که ترجیح می دهید استفاده کنید تغییر دهید.
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
این حالت تخته را نیز چاپ می کند تا بتوانید راحت تر تصمیم بگیرید که حرکت خود را انجام دهید:
به انجام هر حرکت دیگری ادامه دهید، اجازه دهید هوش مصنوعی هر حرکت دیگری را انجام دهد و ببینید چه کسی برنده می شود!
اگر می خواهید از حرکتی پشیمان شوید، می توانید از موارد زیر استفاده کنید:
#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
که بعد از پایان بازی موقعیت تابلو را چاپ می کند.
تأمل در عملکرد موتور شطرنج
من سعی کردم مدل را در حدود 100 هزار موقعیت و حرکت آموزش دهم و متوجه شدم که عملکرد مدل هنوز برای شکست دادن یک ربات شطرنج سطح پایین (500 ELO) کافی نیست.
این می تواند دلایل مختلفی داشته باشد. شطرنج یک بازی بسیار پیچیده است، که احتمالاً به حرکات و موقعیت های بسیار بیشتری نیاز دارد تا یک ربات مناسب توسعه یابد.
علاوه بر این، چندین عنصر از رباتی که شما تغییر می دهید وجود دارد که به طور بالقوه برای بهبود آن تغییر می کند. معماری را می توان بهبود بخشید، برای مثال با افزودن یک CNN در ابتدای تابع جلو، به طوری که ربات اطلاعات مکانی را دریافت کند.
همچنین میتوانید تعداد لایههای پنهان در لایههای کاملاً متصل یا تعداد نورونهای هر لایه را تغییر دهید.
یک راه مطمئن برای بهبود بیشتر مدل، تغذیه داده های بیشتر به آن است، زیرا با استفاده از کد استخراج در این مقاله به تعداد نامحدودی از داده ها دسترسی دارید.
علاوه بر این، من فکر می کنم این نشان می دهد که یک موتور شطرنج یادگیری تقلیدی یا به داده های زیادی نیاز دارد یا آموزش یک موتور شطرنج صرفاً از طریق یادگیری تقلیدی ممکن است ایده مطلوبی نباشد.
با این حال، یادگیری تقلیدی می تواند به عنوان بخشی از موتور شطرنج مورد استفاده قرار گیرد، برای مثال، اگر روش های جستجوی سنتی را نیز پیاده کنید، و یادگیری تقلید را به آن اضافه کنید.
نتیجه
تبریک میگم شما اکنون موتور شطرنج هوش مصنوعی خود را از ابتدا ساخته اید و امیدوارم در این راه چیزی یاد گرفته باشید. اگر میخواهید این موتور را بهبود ببخشید، میتوانید دائماً آن را بهتر کنید و مطمئن شوید که رقابت بهتر و بهتری را پشت سر میگذارد.
اگر میخواهید کد کامل کنید، به GitHub من سر بزنید.
این آموزش در اصل قسمت به قسمت در رسانه من نوشته شده است، می توانید هر قسمت را در اینجا مشاهده کنید:
- بخش 1: تولید مجموعه داده
- قسمت 2: رمزگذاری با روش AlphaZero
- قسمت 3: آموزش مدل
اگر علاقه مند هستید و می خواهید در مورد موضوعات مشابه بیشتر بدانید، می توانید من را در این آدرس بیابید:
منتشر شده در 1402-12-26 13:41:04