4

我正在尝试实现蒙特卡洛树搜索以在 Python 中玩井字游戏。我目前的实现如下:

我有一个 Board 类来处理井字棋棋盘的更改。棋盘的状态由 2x3x3 numpy 数组表示,其中 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 功能,可以将所有内容整合在一起。该函数接受一个棋盘对象并构建蒙特卡洛搜索树,以确定给定棋盘状态的下一个最佳可能移动:

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

我已经搜索了这个代码几个小时,但在我的实现中根本找不到错误。我已经测试了许多棋盘状态并使两个代理相互对抗,但即使是最简单的棋盘状态,该函数也会返回糟糕的操作。我错过了什么和/或我的实施有什么问题?

编辑:这是一个如何实现两个代理的示例:

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
4

1 回答 1

3

问题似乎如下:

  • 您的get_moves()函数不会检查游戏是否已经结束。它可以为某人已经获胜的状态生成一个非空的移动列表。
  • 创建新的Node时,您也不会检查游戏状态是否已经结束,因此untried_actions会创建一个非空列表。
  • 在算法的选择扩展阶段,您也不会检查终端游戏状态。一旦扩展阶段遇到一个包含某人已经获胜的游戏状态的节点,它会很高兴地应用额外的操作并再次为树创建一个新节点,随后的选择阶段也将愉快地继续进行。
  • 对于在某人已经获胜后继续进行游戏的这些节点,result()可能会返回错误的获胜者。它只是检查最近下棋的玩家是否赢了,如果有人赢了就停止玩,这是正确的,但如果在有人已经赢了之后继续玩,则可能是不正确的。因此,您通过树传播各种不正确的结果。

解决此问题的最简单方法是进行修改get_moves(),使其在游戏结束时返回一个空列表。然后,这些节点将始终无法通过if node.untried_actions != []检查,这意味着完全跳过了扩展阶段,您直接进入播放阶段,对终端游戏状态进行适当的检查。这可以按如下方式完成:

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()
于 2018-03-24T10:28:22.497 回答