如何使用自对弈强化学习训练一个五子棋机器人Alpha Gobang Zero
前言
2016年3月,Alpha Go 与围棋世界冠军、职业九段棋手李世石进行围棋人机大战,以4比1的总比分获胜,在当时引起了轩然大波。2017年10月,谷歌公布了新版五子棋程序 AlphaGo Zero。与击败李世石的 Alpha Go 不同的是,AlphaGo Zero 在训练过程中没有使用任何人类棋谱,一切从零开始。训练了 72 小时后,它就以100比0的成绩完胜前辈 Alpha Go Lee。虽然 AlphaGo Zero 看起来十分强大,但是实现起来并不是很困难。下面从 AlphaGo Zero 的基本原理出发,设计并训练一个五子棋机器人 Alpha Gobang Zero.
策略-价值网络
与Alpha Go Lee不同的是,AlphaGo Zero 将分离的策略网络和价值网络组合在一个策略-价值网络中。下面将从策略-价值网络的输入与输出以及内部结构等方面对其进行介绍。
输入与输出
由于围棋棋盘的尺寸为 19×19,所以策略-价值网络接受 19×19×17 的输入 \(s_t\),这个输入代表了棋盘的状态。如下图所示,\(s_t\) 由当前玩家过去的8个落子位置特征平面、对手过去的8个落子位置特征平面和 1 个代表当前玩家颜色的特征平面组成。假设当前玩家使用黑棋,那么在当前玩家的每一落子位置特征平面中,玩家棋子所在位置的值为 1,其他位置的值为 0,对手的落子位置特征平面同理。对于最后一个颜色特征平面,由于当前玩家使用黑棋,所以特征平面的值为全 1。
输入策略-价值网络的 \(s\) 经过内部的层层的处理之后,得到移动概率向量 \(\boldsymbol{p} \in \mathbb{R}^{362}\)和当前玩家胜利的概率 \(v\) 。我们将 19×19 的棋盘展平为 361 维的棋盘,那么 p 的前 361 维的每一个元素 \(p_i\) 代表在361维棋盘的第 \(i\) 维的落子概率,最后一维代表停一手的概率。我们将在蒙特卡洛树小节中介绍这两个输出的使用方法,下面来看看 AlphaGo Zero 策略-价值网络的内部结构。
模型结构
如下图所示,策略-价值网络由 1 个 Convolutional block、19 或 39 个 Residual Block、1 个 Policy Head 和 1 个 Value Head 组成,其中 Policy Head 输出 \(\boldsymbol{p}\) ,而 Value Head 输出 \(v\) 。
Convolutional block
策略-价值网络的第一块是 Convolitional block,它由 1 个卷积层、1 个批归一化层和 1 个 ReLU 函数组成。由于输入 \(s_t\) 的维度为 19×19×17,所以卷积层包含 256 个滤波器组,每个组包含 17 个 3×3 大小的滤波器。在卷积过程中,滤波器的步长为 1,同时为了保持输入的宽高不变,需要置 padding 为 1。经过卷积模块、批归一化模块和 ReLU 处理后,Convolutional block 的输出为 19×19×256 的特征图像。
Residual block
为了提升网络的特征提取能力并防止出现梯度消失问题,在卷积层下面堆叠着 19 个或 39 个 Residual block,如下图所示,每个 Residual block 由 2 个组成类似于 Convolutional block 的子模块构成,唯一不同的就是在第二个子模块的非线性激活之前加上了跳连接,使输入与批归一化模块的输出相加再输入 ReLU 函数,最终输出 19×19×256 的特征图像。
Policy head
从最后一个残差块输出的特征图像作为 Policy head 的输入,经过 Policy head 内部的卷积层、批归一化层和全连接层的处理之后,得到维度为 19×19+1=362 的移动概率向量 \(\boldsymbol{p}\) 。实际上为了计算误差的方便,全连接层后会有一个 log_softmax,得到对数概率 \(\log \boldsymbol{p}\)。
Value head
最后一个残差块的输出还会输入 Value head 中,与 Policy head 不同的是,Value head 里面有两个全连接层:第一个全连接层将输入映射为 256 维的向量,第二个全连接层再将 256 维的向量变为标量,最后经过 \(\tanh\) 函数将这个标量压缩到 \([-1,1]\) 区间,得到 \(v\)。
Alpha Gobang Zero 的网络结构
Alpha Gobang Zero 的策略-价值网络延续了 AlphaGo Zero 的架构,由于算力的限制,对 AlphaGo Zero 的神经网络作出以下修改:
- 使用了 9×9 的棋盘,输入 \(s_t\) 只保留当前玩家和对手过去3步的落子记录,去掉了代表当前玩家的颜色特征平面(主要是训练过程中发现加了这一层误差降不下来),所以 \(s_t\) 的维度为 9×9×6;
- Convolutional layer 的卷积层的输出维度减少128维;
- Residual layer 只有 4 个;
- \(\boldsymbol{p}\) 的维度是 9×9=81 维,因为五子棋没有停一手的操作;
- Value head 的第一个全连接层将输入向量映射到 128 维,而不是 256 维。
蒙特卡洛树搜索
在 19×19 的棋盘上,要穷举出接下来的所有走法是不太现实的一件事,所以 AlphaGo 系列都使用了蒙特卡洛树搜索(MCTS)算法。如下图所示,AlphaGo Zero 的 MCTS 包含四个步骤,分别是:选择、拓展与评估、反向传播和演绎。下面来详细介绍 MCTS 的各过程。
选择
蒙特卡洛树的每一个节点代表一种棋盘状态 \(s_i\)(下面使用状态来命名节点),树上的每一个父节点 \(s\) 与其所有子节点的边上都存着一些变量:
- \(P(s,a)\) 代表从父节点 \(s\) 进行动作 \(a\) 后到达子节点 \(s_c\) 的先验概率;
- \(N(s,a)\) 代表对子节点 \(s_c\) 的访问次数;
- \(Q(s,a)\) 代表子节点 \(s_c\) 上的累计平均奖赏;
- \(U(s,a) = c_{puct} P(s,a) \sqrt{\Sigma_b N(s,b)} / \left(1 + N(s,a) \right)\),代表在子节点 \(s_c\) 上应用上限置信区间算法(UCT)得到的值,其中 \(c_{puct}\) 为探索常数,它的值越大,就越有可能探索未被访问或者访问次数较少的子节点;
假设棋盘上当前落子数为 \(t\),棋盘状态表示为 \(s_t\),那么蒙特卡洛树的根节点就对应着这个 \(s_t\)。又假设我们打算对当前局面进行 \(n_{iters}\) 次蒙特卡洛树搜索,那么每一次搜索都会从根节点出发,根据 \(a_t^*= \text{argmax}_{a_t} \left\{Q(s_t,a_t )+U(s_t,a_t )\right\}\) 进行动作 \(a_t^*\) (对应一维棋盘上的一个落点)到达子节点 \(s_{t+1}\),接着重复上述步骤直至遇到叶节点 \(s_L\) 或者游戏结束为止。
拓展与评估
当我们在选择过程中遇到叶节点 \(s_L\)(这个节点对应的游戏还未结束)时,先前介绍的神经网络就可以派上用场了。我们将叶节点对应的棋盘状态输入策略-价值网络,神经网络对棋局进行评估后得到移动概率向量 \(\boldsymbol{p}\) 和当前玩家获胜的概率 \(v\)。需要指出的是,这里的当前玩家可能不是根节点对应的那个玩家,因为每进行一次选择动作,就会切换一次当前玩家。
移动概率向量 \(\boldsymbol{p}\) 将用来拓展叶节点 \(s_L\),\(\boldsymbol{p}\) 中的每一个元素分别 \(s_L\) 的一个子节点的先验概率 \(P(s,a)\),同时我们需要将所有子节点的访问次数初始化为 0。
反向传播
在拓展与评估步骤中我们得到了叶节点对应的玩家的获胜概率 \(v\),所谓的反向传播,就是指将这个 \(v\) 传播到从根节点到叶节点这一路的所有节点上(不包含叶节点),我们可以使用递归做到这一点。由于这些节点的当前玩家一直在切换,所以将 \(-v\) 传入递归函数。至此我们完成了一次搜索。
演绎
当我们完成 \(n_{iters}\) 次搜索后,根节点的每个子节点都被访问过若干次了。接下来就根据根节点的各个子节点的访问次数 \(N(s,a)\),计算选择动作 \(a\) 的概率:
其中 \(\tau\) 为温度常数。我们最后根据每个节点的 \(\pi\) 来随机选择一种动作 \(a^*\) 并在棋盘上执行。从公式可以看出,温度常数越小,就越有可能选择 \(\pi\) 最大的那种动作,即越趋近于仅利用,而温度常数越大,越趋近于仅探索。
训练 Alpha Gobang Zero
前面已经介绍了AlphaGo Zero的工作原理,下面来看看在不依赖于任何人类棋谱的情况下,如何从零训练一只能有正常人水平的 Alpha Gobang Zero。
自对弈
Alpha Gobang Zero 总共自对弈了 4400 局,我们使用自对弈来生成用于训练的数据,其中每一局的过程都是相同的:
- 清空棋盘,初始化三个空列表:
pi_list
、z_list
、feature_planes_list
,分别用存储在一局中每个动作对应的 \(\boldsymbol{\pi}\),这一局的赢家对每一个动作的当前玩家的奖赏值,及这一局中的每个棋盘状态 \(s_t\); - 将当前的棋盘状态 \(s_t\) 添加到
feature_planes_list
中,并根据 \(s_t\) 执行 500 次蒙特卡洛树搜索,得到 \(a^*\) 和 \(\boldsymbol{\pi}\),注意这里的 \(\boldsymbol{\pi}\) 是一个向量,维数 9×9=81,代表所有动作的移动概率。将 \(\boldsymbol{\pi}\) 添加到pi_list
中; - 使用 \(a^*\) 更新棋盘并判断游戏是否结束,如果还未结束回到步骤2,如果结束进行骤4;
- 根据最后的赢家计算出
z_list
中的每一个元素,计算规则为:赢家与每一个动作的当玩家相同则为 1,不同为 -1,平局为 0。由于五子棋具有旋转不变性和镜像对称性,所以将做了旋转变换和水平镜像变换的各个(feature_planes_list, pi_list,z_list)
添加到 self-play 数据集中,其中feature_planes_list
中的各feature_planes
在训练过程中将作为策略-价值网络的输入,pi_list
和z_list
的各元素将作为标签; - 结束一局自对弈。
需要指出的是,根据论文中的说法,自对弈前 30 步的温度常数 \(\tau=1\),后面的温度常数 \(\tau \to 0\)。同时为了增加探索,在拓展步骤中需要给策略-价值网络的输出 \(\boldsymbol{p}\) 添加狄利克雷噪声,使得 \(P(s,a)=(1-\varepsilon) p_a+ \varepsilon \eta_a\),其中 \(\eta_a \sim Dir(0.03)\),\(\ \varepsilon=0.25\)。
在 AlphaGo Zero 的 MCTS 中,为了提高搜索速度而使用了多线程。考虑到做这个项目只是出于学习目的,所以并没有将多线程添加到代码逻辑中。
训练
训练方法
当 self-play 数据集的长度超过 start_train_size
时,就可以正式开始训练了。训练步骤为:
- 从数据集中随机抽出大小为
batch_size
的样本集; - 将样本集含有的
feature_planes_list
作为一个mini_batch
输入策略-价值网络,输出维度为(batch_size, 81)
的批量 \(\log \boldsymbol{p}\) 和维度为(batch_size, 1)
的批量 \(v\); - 根据损失函数 \(\mathscr{l}=(z-v)^2- \boldsymbol{\pi}^T \log \boldsymbol{p}+c\Vert \theta \Vert\) 更新神经网络的参数,其中 \(c\) 是控制 L2 权重正则化水平的参数;
- 结束一次训练。
每当我们完成一次策略-价值网络的训练之后,就可以接着进行一局自对弈以产生新的数据,然后再进行一次训练,就这样一直循环下去。随着训练次数的增加,学习率会逐渐减小,具体变化如下表所示:
训练次数 | 学习率 |
---|---|
0-1500 | \(10^{-2}\) |
1500-2500 | \(10^{-3}\) |
>2500 | \(10^{-4}\) |
有一点需要指出:Alpha Gobang Zero 的自对弈数据始终由最新的神经网络产生,而在 AlphaGo Zero 中自对弈数据是由历史最佳的策略-价值网络产生的。只使用最新模型产生训练数据的原因主要有两点:
- 像 AlphaGo Zero 那样频繁评估以决定历史最优模型的操作是很耗时的;
- Deep Mind 后来在 Nature上 发表了一篇论文《Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning Algorithm》,里面的 Alpha Zero 也只使用最新模型来产生数据。
虽然不使用历史最优模型来产生数据,但是根据 AlphaGo Zero 的评估思想,我们会定期让当前模型和历史最优模型进行 PK,如果当前模型的胜率超过 55%,就将历史最优模型更新为当前模型。
训练结果
最终的损失曲线如下图所示:
可以看到即使训练到了后期,损失曲线还是在振荡,有一点原因可能是:受到现实条件的约束,训练的时候并没有连续自对弈 4400 局下来,而是每天训练上 1000 局左右就停掉,好让电脑可以休息上一段时间,结果在接着训练的时候忘了调整学习率了,直至第3300局才意识到这一点。
虽然损失曲线一直在振荡,不过在和 Alpha Gobang Zero 对弈的过程中可以明显感受到它的水平在一直提高,自对弈到 3000 局的时候就已经有正常人的水平了。下面来看些 Alpha Gobang Zero 自对弈的棋谱:
可以看到训练了 800 次之后,AI 还是有些进步的,知道开局的时候该往中间走了,而且在对方快连成五颗的情况下也知道要去堵他了。但如第二张棋谱所示,在白方4、6、8已经连成3颗时,黑方还不会去堵它。而且在已经连成4颗棋子的情况下,白方居然没有绝杀黑方,而是下了 12 这个棋。对比第一张图就可以得知:此时的AI还只会下前几手,后面表现的越来越差,几乎是在乱走,所以没有绝杀对方。
可以看到训练到4400局之后,AI 已经掌握了开局和攻守的诀窍。一开始双方就挨得很近,试图阻止对方连成3颗。在黑方的5、7、9连成3颗时,白方也及时堵住。在白方8、14、10连成3颗时,黑方暂时没有理他,而是在左下角下了15,与自己的1、3、13连成了4颗,企图先下手为强。在白方将其堵住之后,黑方才在右上角下了17将其堵住。在此之后双方交替出现连续3颗的情况,但是都被对手及时堵住了,说明AI已经学会了堵3颗的技巧。最后几步双方都杀的很急,不过感觉白方确实要弱一些,最终黑方取胜。
最后给出一张自己被 AI(白方)击败的游戏截图:
写在最后
Alpha Gobang Zero 的实现原理和过程已经介绍完毕,具体代码请移步 github,以上~