五子棋版AlphaZero中的MCTS算法(一)——MCTS介绍和python实现

1 背景

本篇介绍MCTS(蒙特卡洛搜索树)算法,它被广泛应用在复杂决策和游戏中,在最近大火的大语言模型的推理模型中也有应用。2016年,AlphaGo战胜了韩国9段棋手李在石,MCTS就是其重要的组成部分,有兴趣读者可观看NetFlix的纪录片《阿尔法狗 AlphaGo (2017)》,B站就可以搜到。在后来的Alpha Zero中,MCTS继续成为其重要的模型基础。

本篇用Python代码搭建了一套五子棋游戏框架,并实现了3种AI策略:随机策略、规则策略和MCTS策略。代码将游戏逻辑、UI展现、AI策略这三部分做了解耦,并采用面向对象的设计原则,如有读者对代码结构设计感兴趣也可参考和讨论。

2 MCTS原理

个人认为MCTS的原理有如下特点:(1)简单,符合人的直觉;(2)巧妙,发挥了数据结构中树的优势,非常适合部署到计算机中,实现人机结合解决实际问题。因为模型比较简单,笔者将做简要介绍,不会涉及到规范的数学语言描述,以五子棋为例拆解该算法。

2.1 算法简介

从算法名称可以看出,MCTS算法有2个关键元素,即蒙特卡洛和搜索树。

蒙特卡洛,指使用随机模拟逼近结果。人在下棋的时候也会往后推演,思考我方棋子下到某个位置时,对方应如何应对。计算机就是在仿照这个过程,通过自身的高算力,向后模拟多个回合,并选择胜率最高的动作。

搜索树,指算法主体是用“树”这一数据结构实现。树是计算机中重要的数据结构,这里不展开说明,仅以形象化例子解释:想象一棵树,从根部向上,分叉出若干枝条,每个枝条又可以分叉,到了叶子处不再分叉。在数据结构中,树是倒过来的,最上层是根节点,往下分叉出若干子节点。每个子节点又会往后分叉,直到叶子节点。

每个棋盘局面可以认为是一个节点,新一轮落子后的棋盘是一个子节点。假设棋盘是19*19,那么最初空白棋盘是根节点,其子节点指后续可能的下法,一共19*19=361个,继续向下的子节点是361-1=360个,那么游戏进行5轮时,可能的下法有361*360*359*358*357。可以想象到计算量是非常大的,计算机穷举所有下法不现实,所以需要用蒙特卡洛方法模拟。只要模拟次数足够多,也可以达到和穷举类似的效果。

2.2 选择Selection

给定一个棋盘局面,下一步应该选择往哪个位置模拟?当然是有利于自己的位置了。标准是什么呢?

有2个标准:“经由此节点”模拟的胜率以及被探索的次数。胜率越高越好,这很好理解——哪一步胜率高就选择哪一步;探索次数越小越好,因为选择探索次数少的下法能够让模拟覆盖尽量多的路径,挖掘潜力。

比如之前模拟了50次,有如下2个子节点可以选择。

子节点获胜次数经过此节点总数
A1220
B230

显然A节点胜率高,而且探索次数少,应该选择A。

胜率和探索次数怎么组合成一个指标呢?可以使用UCB公式计算

UCB=\overline{V_i}+c*\sqrt{\frac{\log{N}}{n_i}}

加号左侧指该节点胜率,右侧的c是一个常数,一般取2.N表示父节点探索次数,n表示子节点探索次数。对于同一个父节点而言,其所有子节点的N是固定的值。从此式可知,胜率越大,子节点UCB越大,探索次数越小,子节点UCB越大,常数c就是在“利用”与“探索”之间发挥调节作用。

当然,如果当前局面是一个新的局面,即节点是叶子节点,暂无子节点,那么跳过这一步骤,直接进行扩展。如果当前局面已经分出胜负,那么也无模拟的必要了。

2.3 扩展Expansion

如果模拟到达了一个叶子节点,即一个新的局面,那么就把子节点全部罗列出来。即从当前节点扩展出子节点。

2.4 模拟Simulation

从扩展出的子节点往后模拟下棋,直到分出胜负或者步数超过给定的限额(比如30步,防止模拟消耗时间过长),注意模拟时经过的节点都是虚拟的节点,并没有被扩展,本次模拟结束后,这些虚拟的节点都会删除,他们唯一的作用就是帮助此次模拟分出胜负。

2.5 反向传播Backpropagation

分出胜负后,将沿途的节点(不包括虚拟节点)胜利次数加1,被探索次数也加1。

最后,根据所有子节点的胜率、探索次数,选择最佳的子节点作为下一步动作。

3 代码解析

代码已上传到Github中,欢迎大家clone或star。项目地址:GitHub - cufewxy/gobang: MCTS implement for gobang game

代码写得仓促,不完善的地方欢迎大家探讨。所需安装模块有:

tkinter,numpy

3.1 整体结构

文件名/文件夹作用
ai_strategy
所有AI的策略都在此实现,每个AI策略是一个py文件
data
对局结果,按日期存储,便于以后AI训练
ai_base.pyAI策略的基类,所有AI策略需要继承此基类
gobang.py游戏后台逻辑处理,包括判断游戏胜利条件,封装了落子接口
ui.py将游戏后台棋盘展示成窗口,方便玩家对局,或者重放某局游戏(暂未实现)

3.2 后台逻辑的实现

后台逻辑是在gobang.py中实现的,这里包含了游戏进行所需要的全部元素。

主要的代码对象如下

GoBangBoard类
类名GoBangBoard
作用棋盘类
主要属性
position
棋子位置,1和2表示玩家1和玩家2落子位置,0表示空位置
主要函数
put

落子

输入玩家编号、位置,将棋盘的position置为相应玩家的编码

将棋盘和游戏逻辑解耦,因为棋盘有可能被用于围棋等其他棋类,具有通用性。

GoBangGame类
类名GoBangGame
作用实现了五子棋游戏下棋逻辑、获胜条件判断
主要属性
cur_player
当前落子的玩家
result
游戏结果
board
棋盘,即GoBangBoard的对象
主要函数
proceed

游戏进行一轮下棋动作

输入位置,在棋盘落子后,检查获胜条件

check_winner

判断玩家是否获胜

输入为棋盘、玩家

输出获胜结果以及获胜连子的位置

3.3 UI的实现

ui.py负责渲染棋盘页面,将用户的点击动作发送到gobang.py封装的下棋接口中,并且会调用AI策略,实现与玩家对弈。

这里是参考了另一篇文章,略做修改改造成面向对象的形式,并适配AI策略和gobang.py后台算法,因此不展开叙述。参考的文章找不到了,如果涉及版权请联系我。

3.4 AI策略

策略基类

策略基类是在ai_base.py中实现的,定义了AIStrategy抽象基类,AI策略必须继承此类,并实现model函数。model是AI策略最重要的计算函数,输入当前的棋盘和玩家编号,输出落子的位置。

策略一:随机策略

import random

from ai_base import AIStrategy


class RandomStrategy(AIStrategy):
    """
    随机模型
    """

    def model(self, cur_board, ai_player):
        empty_list = []
        for i in range(len(cur_board)):
            for j in range(len(cur_board)):
                if cur_board[i][j] == 0:
                    empty_list.append([i, j])
        res = random.choice(empty_list)
        return res

随机策略是最简单的策略,在棋盘中所有的空位置中随机落子。

策略二:规则策略

基于经验构造出5条规则,AI根据规则做出决策。

情形动作
当我方有4个棋子相连且一头为空
落到其中一头,获胜
当对手有4个棋子相连且一头为空
堵住连珠
当我方有3个棋子相连且两头为空
落到其中一头。优先落到连续空位置大于1的一头(例如OOXXXO,X表示棋子,O表示空位置,应该落到左侧),如果都是1个空位置则任选一头
当对手有3个棋子相连且两头为空
落到其中一头。其中至少有一头的空位置大于1,否则没必要堵住(例如OXXXO)
不符合上述所有情况
调用随机策略,随机选择位置落子

策略三:MCTS策略

说实话,由于计算量过大,此策略的实际效果一般,实战意义小于算法示范的意义。单进程运行速度比较慢(可考虑拓展成多进程甚至多机器运行),AI要卡顿很久才能出招。若减小模拟次数,可能覆盖不了所有搜寻的空间,导致很多节点未被探索;若减小对局深度,则很多对局无法区分胜负,失去了模拟的意义。读者在运行程序时,可尝试缩减棋盘大小。

树的表示

使用class Tree表示

class Tree:
    def __init__(self, cur_player):
        self.root_node = Node(cur_player)

    def trim(self, node):
        self.root_node = node
        self.root_node.parent_node = None

属性包含root_node,表示根节点。

函数包含trim,即传入一个节点,裁剪使得此节点为根节点。

此树的遍历是通过节点完成的,因此Tree的属性和函数较少。

节点的表示

使用class Node表示。

class Node:
    def __init__(self, player):
        self.player = player
        self.child_node = []
        self.action = []  # 表示下棋的动作,没有在每个节点都保存棋盘,而是从根节点开始往下推演棋盘,否则内存过大
        self.parent_node = None
        self.visit_num = 0  # 访问次数
        self.win_num = 0  # 获胜次数
        self.if_win = None  # 1表示胜利,0表示未胜利,None表示无法判断

属性包括:

属性含义
player此节点是由哪个玩家走棋得到的。每个节点都对应下棋的玩家,同一层的所有节点对应的玩家是一致的
child_node子节点列表
action该节点的下棋动作。没有在每个节点都保存完整棋盘,而是从根节点开始往下推演棋盘,以减小内存开销
parent_node父节点
visit_num探索次数
win_num胜利次数
if_win表示此节点下玩家是否胜利。1表示胜利,0表示未胜利,None表示暂未判断

注意下棋是对局双方交替进行的,因此在设计算法时(尤其是模拟阶段和反向传播阶段)需要考虑以下方面:(1)每一步是哪个玩家出手;(2)结束时是哪个玩家胜利,反向传播时应该只更新胜利玩家的节点;(3)要考虑人类的走棋,AI要接收人类的走棋,从而更新自己棋盘。具体而言,需要注意以下逻辑。

内嵌规则策略AI

先查看当前棋面是否符合规则策略,如果符合则直接调用规则,不再应用MCTS算法。

更新棋盘树

AI和人类对局是交替进行的,因此AI需要识别出人类下了哪一步棋,更新棋盘。同时,对当前棋盘树做剪枝,将根节点替换为当前局面的节点。之所以没有重新初始化一棵树,是希望复用之前扩展得到的子节点胜利信息、探索信息。

模拟

模拟阶段是调用了“规则策略AI”进行对弈。

反向传播

注意反向传播时,所有途径节点的探索次数加1,但只有胜利的玩家对应的节点才会更新胜利次数。因此胜利次数加1的操作是隔一层应用一次的。

3.5 引申讨论——AlphaZero

程序运行起来较慢,事实上,AlphaGo/AlphaZero也并非单纯使用了MCTS算法,而是引入了其他模型,例如策略网络,负责预测下一步走棋;价值网络,负责评判胜率。

这样做有2个好处,第一,从速度上,这2个网络可以预先训练好,加快了AI决策的速度;第二,从效果上,这2个网络能够使用到棋谱数据或者是AI自对弈得到的数据,使得模型能够不断迭代,反观MCTS,每次开启新的对局,旧的对局数据就没有作用了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值