MCTS蒙特卡洛搜索树实现井字棋游戏

   利用蒙特卡洛搜索树实现简单的井字棋游戏,重点不是井字棋,是熟悉蒙特卡洛搜索树的应用,而且我们知道,MCTS可以应用到非常复杂的博弈游戏中,比如象棋,围棋,在搜索空间非常大的时候,普通的极大极小搜索树无法应用,这是由于硬件设备的限制。

    但是井字棋游戏的搜索空间很小,第一层只有9个子节点,对应9个可选的位置,同样我们可以看出,第二层只有9*8个子节点,以此类推,直到搜索到结束也不会占用很大的空间,所以井字棋是可以利用极大极小搜索配合α-β剪枝来做到最优落子的。

但是我本次就不使用这种穷举的方法了,而使用MCTS来实现,而且在这种小游戏中,模拟次数足够的情况下,落子也几乎是最优的。


蒙特卡洛搜索树

  简单介绍一下MCTS,也就是蒙特卡洛搜索树算法,大体分为四步,选择,扩展,模拟,回传。通过不断重复这四步,也就可以不断扩展这棵搜索树,最后到达时间限制或者到达模拟次数限制之后,最终可以形成一棵不对称的树。由于每个节点记录了选择的次数数据,因此树构建完成后可以选择根节点下一层中选择次数最大的子节点作为落子,因为我们知道,虽然一开始的选择节点是很随机的,但是通过回传操作,节点的价值是不断更新的,因此最终选择次数最多的节点我们认为可能是效果比较好的节点,事实也证明这是完全正确的想法。

1,选择

   就是选择一个节点,刚开始时,只有根节点,没有子节点可以选择就跳到下一步,扩展,如果有子节点,就根据子节点的价值抽样选择一个,然后再看选择的这个子节点还有没有子节点,如果有,继续往下选择,直到选择到叶子结点。然后进入下一步。

2,扩展

   选择到叶子节点之后,就可以进行扩展,将叶子结点的子节点展开,可以展开一个,也可以展开多个,要根据实际情况而定。比如我们熟知的阿尔法狗算法,每次都展开所有的子节点,然后根据神经网络输出的概率给每个子节点赋值先验概率,这里不多说,感兴趣的去了解阿尔法狗的论文。而正常的MCTS,展开一个的较为常见,其实本质上区别不大,因为我们会记录节点是否已完全展开,没有完全展开的节点,接下来的模拟中会继续展开的,所以本质上展开一个和多个区别不大,因为最终基本都会展开。

3,模拟

  这也是MCTS中较为重要的一步,根据一个策略,通常随机策略的效果就非常不错,借用阿尔法狗第一作者David Silver在伦敦大学强化学习课程中说的一句话:不要以为随机策略是很糟糕的策略,它常常可以取得非常不错的效果。  所以这里我们一般就是采用随机策略,从刚才扩展的叶子结点开始一直模拟到游戏结束。具体什么意思呢?就是从这个叶子结点的局面开始,博弈双方都随机的从可用的地方落子一直到比赛胜负,这个胜负的结果一定程度上就反映了了这个局面的情况,如果胜了,至少可以有种落子的方式可以赢一次不是吗,当然我们也能想象到这个结果是很不可靠的,毕竟随机落子的,事实上确实是不可靠的,但是好处就是快,而且我们可以模拟很多次,上千次,这样即使是随机的,如果大部分都是赢的话,也足以说明这个局面的赢面是大的,所以本质上MCTS就是以频率逼近概率的算法

4,回传

   上一步模拟出胜负结果之后,这个结果一般是1,-1和0,代表胜负平,然后把这个结果回传,更新这个路径上的节点的值。举个具体的例子,比如说模拟的结果是胜利,也就是1,那么第二步那个叶子结点的价值就加1,当然还有其他的值需要更新,比如选择的次数也要加1,然后它的父节点的值就要加-1,因为是博弈,对对手是胜利,对自己就是失败,所以取相反数,然后父节点的父节点就是加1,依次递归到根节点,更新整个路径上的节点的值。


    到此为止,上面四步完成后,一次蒙特卡洛搜索就结束了,然后在进行第二次,第三次,成百上千次,都结束之后,根据根节点的子节点的选择次数信息,选择计数最大的那一步落子,到这里才算真正的下第一步棋,也就是前面成百上千次的MCTS都只是在脑袋里想想而已,并不是真正的下棋。


井字棋

   然后就是根据上面介绍的MCTS实现自己的东西了,比如我要实现一个井字棋游戏,其实重点就是构建这样一棵树,游戏的部分很简单,定义棋盘:一就是定义棋面状态,用一个3*3的2维数据就行了,表示每个位置的落子情况,是圈还是叉还是空白。二是游戏的棋盘状态,需要包含上一步的棋面状态,,这部分你还可以定义一些必要的功能函数,比如游戏是否结束,判断谁是胜者,定义落子函数,获取合法的落子位置等等,这些都是很常见的函数。定义落子:必要时还可以定义落子类,也就是动作类,包含落子的坐标还有取值,取值就代表圈还是叉,这样游戏落子的很多地方就方便了许多。

   重点是树的构建,这也是对你的数据结构熟练度的考察,你需要首先定义节点类,变量至少要包含必要的价值和计数值,还有记录父节点和子节点的信息,还有上面介绍的一系列操作,选择,扩展,模拟,回传等等。然后在定义蒙特卡洛搜索树类,利用上面的节点类,主要完成树的构建,还有进行多次的模拟,并且还要定义获取最优落子的函数。


   具体代码参见我的Github,代码并不复杂,喜欢的可以点个Star支持一下。MCTS也就是电脑先手,模拟1000次落子一次,你可以修改成自己先手,只需要修改run.py的代码即可,我就不多说了。







  • 14
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
好的,以下是一个基于Python实现的基于MCTS和UCT的五子棋游戏AI的代码示例: ``` python import random import math class TreeNode: def __init__(self, state, parent=None): self.state = state self.parent = parent self.children = [] self.visits = 0 self.score = 0 def UCT(node): C = 1.4 if node.visits == 0: return float('inf') return (node.score / node.visits) + C * math.sqrt(math.log(node.parent.visits) / node.visits) def MCTS(state, iterations): root = TreeNode(state) for i in range(iterations): node = root # selection while node.children: node = max(node.children, key=UCT) # expansion if node.visits > 0: moves = node.state.get_moves() for move in moves: if move not in [c.state.last_move for c in node.children]: child_state = node.state.apply_move(move) child_node = TreeNode(child_state, node) node.children.append(child_node) # simulation sim_node = node while sim_node.children: sim_node = random.choice(sim_node.children) score = simulate(sim_node.state) # backpropagation while node: node.visits += 1 node.score += score node = node.parent return max(root.children, key=lambda c: c.visits).state.last_move def simulate(state): player = state.get_current_player() while not state.is_terminal(): move = random.choice(state.get_moves()) state = state.apply_move(move) player = state.get_current_player() if state.get_winner() == player: return 1 elif state.get_winner() == None: return 0.5 else: return 0 class Board: def __init__(self, width=15, height=15, win_length=5): self.width = width self.height = height self.win_length = win_length self.board = [[None for y in range(height)] for x in range(width)] self.last_move = None def get_moves(self): moves = [] for x in range(self.width): for y in range(self.height): if self.board[x][y] == None: moves.append((x, y)) return moves def apply_move(self, move): x, y = move player = self.get_current_player() new_board = Board(self.width, self.height, self.win_length) new_board.board = [row[:] for row in self.board] new_board.board[x][y] = player new_board.last_move = move return new_board def get_current_player(self): if sum(row.count(None) for row in self.board) % 2 == 0: return "X" else: return "O" def is_terminal(self): if self.get_winner() != None: return True for x in range(self.width): for y in range(self.height): if self.board[x][y] == None: return False return True def get_winner(self): for x in range(self.width): for y in range(self.height): if self.board[x][y] == None: continue if x + self.win_length <= self.width: if all(self.board[x+i][y] == self.board[x][y] for i in range(self.win_length)): return self.board[x][y] if y + self.win_length <= self.height: if all(self.board[x][y+i] == self.board[x][y] for i in range(self.win_length)): return self.board[x][y] if x + self.win_length <= self.width and y + self.win_length <= self.height: if all(self.board[x+i][y+i] == self.board[x][y] for i in range(self.win_length)): return self.board[x][y] if x + self.win_length <= self.width and y - self.win_length >= -1: if all(self.board[x+i][y-i] == self.board[x][y] for i in range(self.win_length)): return self.board[x][y] return None def __str__(self): return "\n".join(" ".join(self.board[x][y] or "-" for x in range(self.width)) for y in range(self.height)) if __name__ == "__main__": board = Board() while not board.is_terminal(): if board.get_current_player() == "X": x, y = map(int, input("Enter move (x y): ").split()) board = board.apply_move((x, y)) else: move = MCTS(board, 1000) print("AI move:", move) board = board.apply_move(move) print(board) print("Winner:", board.get_winner()) ``` 该代码定义了一个 `TreeNode` 类来保存节点的状态和统计信息,实现了基于UCB公式的UCT算法和基于MCTS和UCT的五子棋AI。同时,代码还定义了一个 `Board` 类来表示五子棋游戏的状态和规则,并实现了判断胜负、获取可行落子位置等方法。在 `__main__` 函数中,代码通过交替输入玩家落子位置和调用AI选择落子位置的方式,实现了人机对战的功能。 希望这个代码对你有所帮助!
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值