Крестики-нолики Монте-Карло - Бедный агент
Я пытаюсь реализовать поиск по дереву Монте-Карло, чтобы играть в крестики-нолики в Python. Моя текущая реализация выглядит следующим образом:
У меня есть класс Board, который обрабатывает изменения в крестики-нолики. Состояние платы представлено массивом 2x3x3, где каждая из матриц 2 3x3 является двоичными матрицами, индивидуально представляющими наличие X и присутствие O.
class Board:
'''
class handling state of the board
'''
def __init__(self):
self.state = np.zeros([2,3,3])
self.player = 0 # current player's turn
def copy(self):
'''
make copy of the board
'''
copy = Board()
copy.player = self.player
copy.state = np.copy(self.state)
return copy
def move(self, move):
'''
take move of form [x,y] and play
the move for the current player
'''
if np.any(self.state[:,move[0],move[1]]): return
self.state[self.player][move[0],move[1]] = 1
self.player ^= 1
def get_moves(self):
'''
return remaining possible board moves
(ie where there are no O's or X's)
'''
return np.argwhere(self.state[0]+self.state[1]==0).tolist()
def result(self):
'''
check rows, columns, and diagonals
for sequence of 3 X's or 3 O's
'''
board = self.state[self.player^1]
col_sum = np.any(np.sum(board,axis=0)==3)
row_sum = np.any(np.sum(board,axis=1)==3)
d1_sum = np.any(np.trace(board)==3)
d2_sum = np.any(np.trace(np.flip(board,1))==3)
return col_sum or row_sum or d1_sum or d2_sum
Затем у меня есть класс Node, который обрабатывает свойства узлов при построении дерева поиска:
class Node:
'''
maintains state of nodes in
the monte carlo search tree
'''
def __init__(self, parent=None, action=None, board=None):
self.parent = parent
self.board = board
self.children = []
self.wins = 0
self.visits = 0
self.untried_actions = board.get_moves()
self.action = action
def select(self):
'''
select child of node with
highest UCB1 value
'''
s = sorted(self.children, key=lambda c:c.wins/c.visits+0.2*sqrt(2*log(self.visits)/c.visits))
return s[-1]
def expand(self, action, board):
'''
expand parent node (self) by adding child
node with given action and state
'''
child = Node(parent=self, action=action, board=board)
self.untried_actions.remove(action)
self.children.append(child)
return child
def update(self, result):
self.visits += 1
self.wins += result
Наконец, у меня есть функция UCT, которая объединяет все. Эта функция берет объект Board и строит дерево поиска Монте-Карло, чтобы определить следующий наилучший возможный ход из данного состояния доски:
def UCT(rootstate, maxiters):
root = Node(board=rootstate)
for i in range(maxiters):
node = root
board = rootstate.copy()
# selection - select best child if parent fully expanded and not terminal
while node.untried_actions == [] and node.children != []:
node = node.select()
board.move(node.action)
# expansion - expand parent to a random untried action
if node.untried_actions != []:
a = random.choice(node.untried_actions)
board.move(a)
node = node.expand(a, board.copy())
# simulation - rollout to terminal state from current
# state using random actions
while board.get_moves() != [] and not board.result():
board.move(random.choice(board.get_moves()))
# backpropagation - propagate result of rollout game up the tree
# reverse the result if player at the node lost the rollout game
while node != None:
result = board.result()
if result:
if node.board.player==board.player:
result = 1
else: result = -1
else: result = 0
node.update(result)
node = node.parent
s = sorted(root.children, key=lambda c:c.wins/c.visits)
return s[-1].action
Я искал этот код в течение нескольких часов и просто не могу найти ошибку в моей реализации. Я протестировал множество состояний форума и сравнил двух агентов друг с другом, но функция возвращает плохие действия даже для самых простых состояний форума. Что мне не хватает и / или что не так с моей реализацией?
edit: вот пример того, как могут быть реализованы два агента для игры:
b = Board() # instantiate board
# while there are moves left to play and neither player has won
while b.get_moves() != [] and not b.result():
a = UCT(b,1000) # get next best move
b.move(a) # make move
print(b.state) # show state
1 ответ
Проблема заключается в следующем:
- Ваш
get_moves()
Функция не проверяет, закончилась ли игра. Он может генерировать непустой список ходов для государств, в которых кто-то уже выиграл. - При создании нового
Node
Вы также не проверяете, закончилось ли состояние игры, поэтому непустой списокuntried_actions
создано. - На этапах выбора и расширения алгоритма вы также не проверяете терминальные игровые состояния. Как только фаза расширения попадает на узел, который содержит игровое состояние, в котором кто-то уже выиграл, он с радостью применит дополнительное действие и снова создаст новый узел для дерева, который также будут успешно проходить на последующих фазах выбора.
- Для этих узлов, где игра продолжается после того, как кто-то уже выиграл,
result()
может вернуть неверного победителя. Он просто проверяет, выиграл ли последний игрок ход, что является правильным, если вы прекращаете играть, как только кто-то выигрывает, но может быть неверным, если вы продолжаете играть после того, как кто-то уже выиграл. Таким образом, вы распространяете все виды неверных результатов через дерево.
Самый простой способ исправить это будет изменить get_moves()
такой, что он возвращает пустой список, когда игра уже закончена. Тогда эти узлы всегда будут выходить из строя if node.untried_actions != []
check, что означает, что фаза расширения полностью пропускается, и вы сразу переходите к фазе Play-out, где происходит надлежащая проверка состояний игры терминала. Это можно сделать следующим образом:
def get_moves(self):
"""
return remaining possible board moves
(ie where there are no O's or X's)
"""
if self.result():
return []
return np.argwhere(self.state[0] + self.state[1] == 0).tolist()