在本篇博文中,你将会了解并实现AlphaZero。AlphaZero是一个令人大开眼界且超乎寻常的强化学习算法,它以绝对的优势战胜了多名围棋以及国际象棋冠军。本文将会带你使用AlphaZero来解决一个益智小游戏(Dots and Boxes)并将其部署成一个纯JavaScript构建的Web应用。
AlphaZero最关键也是最令人诧异的一点,就是其能够在不依赖于外部先验知识的情况下在棋盘类游戏中获得超越人类的表现。AlphaZero通过自我博弈汲取经验知识来不断精通游戏。
我们会借助于Github上由Surag Nair开发的一个“简化后的、高度灵活的、经过注释的且易于理解的”Python版AlphaZero来进行该项目。
你大可以先去这里玩一玩这个游戏。而Web应用以及具体的JavaScript实现代码可以在这里获取得到。这份代码是从该Python实现中移植过来的。
https://carlos-aguayo.github.io/alphazero/
有关AlphaZero的原理,你可以阅读这篇由Silver,David等人撰写的论文:“Mastering the game of Go without human knowledge” nature 550.7676 (2017): 354–359.
Dots and Boxes小游戏
Dots and Boxes是一个常见的儿童益智游戏,不过其具有令人讶异的复杂度。
该游戏中,两名玩家轮流在两个相邻点之间放置一条水平或垂直线。如果某个 1×1 的小正方形的 4 条边都被连上了,那么补齐这个小方块的一方就获得 1 分,得分的玩家被奖励多走一步,再连一条线。当棋盘已满时,游戏结束,并且得分最高的玩家获胜。
(译者注:这个游戏相当有意思,建议先去玩玩看,点这里。能不能战胜AlphaZero就看你了!)
人工智能与棋盘游戏
机器是否能够产生智能,我们已经为此思考了很久很久。那么,该如何验证机器具有智能呢?一个常用方法就是玩棋盘游戏,比如国际象棋,看看其是否具有超人的能力,甚至击败世界冠军。
1957年,Herbert Simon预言计算机系统能够在十年内击败国际象棋冠军。虽说实际上花的时间长了点,但是在1997年5月,计算机击败了当时的国际象棋冠军——Garry Kasparov。
(译者注:战胜Kasparov的机器被命名为DeepBlue,意为“深蓝”)
尽管这一里程碑事件意义非凡,但人们仍可以争论这一计算机系统是否“智能”。
这一类计算机系统由以下三个组件构成:
- 人为定义的评价函数;
- 博弈树搜索算法;
- 极为强悍的硬件设备。
评价函数会将棋盘盘面作为输入并输出该盘面的“价值”。高价值表示当前玩家处于非常有利的位置。例如,在国际象棋棋盘上,玩家即将进行“将死”时就会对应一个非常高的值。
博弈树搜索算法(比如 Minimax)在所有可能的走棋中进行搜索,寻找那些能够确保得到高价值棋盘盘面的路径。对于那些已经明知不可能有效的路径可以直接放弃搜索,从而使算法变得更有效率。这就是 Alpha-beta剪枝 的作用。
最后,搭配上异常强悍的硬件,你就将拥有一台能够打败国际象棋世界冠军的机器。
问题在哪儿?经验丰富的棋手人为地精心调制这些评价函数。这些计算机系统还依赖于一本本记录着最佳走棋的开局棋谱。游戏中局,还会用到通过研究大师们的博弈而精心构造的评价函数。这些函数还会经由象棋大师们进一步的优化调整。
例如,我们完全就可以为 Dots and Boxes 构造一个评价函数。一个合理而直接的选择就是做一个得分的比较。得分的正向差值越大,游戏盘面就对我们越有利。大多数情况下,这是可行的。然而,在 Dots and Boxes 中,就像许多其他棋盘类游戏一样,最佳的走法可能需要牺牲短期利益来换取长期利益。在 Dots and Boxes 游戏中,有时最好不要急于得分并获得额外先手,相反,要迫使对手走某一步棋。因此,我们必须考虑大量复杂场景并精心调制评价函数!
击败Kasparov的评价函数需要识别多达8000个盘面特征!而且其中绝大多数都是手动描述并调整的!
所以,倒也不是贬低这个击败国际象棋世界冠军重要里程碑的意思,只是,需要顶级玩家来定义这些计算机的行为并手动调整如此多的变量实在是有够折腾人的。
AlphaZero是什么?为何它如此令人心潮澎湃?
AlphaZero是首个能够在国际象棋、围棋等游戏中达到超越人类水平、击败世界冠军的计算机系统,且它仅依赖于游戏规则,无需任何人类先验知识。
仅凭给定的游戏规则,AlphaZero即可进行自我博弈。逐步习得游戏策略与技巧,很快即可获得超人的表现。
像DeepBlue这样的系统会需要国际象棋专家的协助,而AlphaZero却是凭借自我博弈来变强大的。不单单是在国际象棋上,哪怕是围棋,AlphaZero同样表现出超越人类的强大统治力。考虑到围棋相较于其他棋盘游戏更大的博弈空间等因素,对计算机来说,围棋是个极为复杂的游戏。
人类从几千年来数百万次的博弈中方才积累了诸如围棋和国际象棋等游戏的技艺,而AlphaZero,一个仅使用游戏规则信息的算法,却能够在几天时间内重新寻获这些知识并发现新的游戏策略。
甚至还有一部关于它的纪录片。
(译者注:这部纪录片很值得一看,无法访问YouTube的同学可以在B站观看,链接在此。不过需要注明的是,本纪录片中实际上使用的是AlphaGo算法,而非AlphaZero,准确来说,AlphaZero是AlphaGo的进阶版本,全名为AlphaGo Zero。纪录片中与李世石博弈的AlphaGo在跟 AlphaGo Zero 博弈时,0-100全负,并且,AlphaGo Zero在训练中未使用任何手工设计的特征或者围棋领域的专业知识,仅仅以历史棋面作为输入,其训练数据全部来自于自我博弈。可谓恐怖如斯!)
AlphaZero是怎么做到仅凭自我博弈就习得技艺的呢?
回想一下,像DeepBlue那样依赖于人为定义的“评价函数”的系统会把棋盘的盘面状态作为输入,再输出该状态的“价值”。
如今,对于深度学习模型来说,输入一张照片然后识别出照片里是猫还是狗简直简单到爆了。那么有个想法就是,把棋盘盘面作为一个深度学习模型的输入并且训练它,让它预测这样的盘面布置是会输还是会赢。
但是,要训练一个机器学习模型,就需要数据,海量的数据。从哪儿能得到那么多棋局博弈的数据呢?很简单,我们就让电脑自己跟自己下着玩儿,生成一堆棋局,然后再把它们做成一个数据集用来训练。
AlphaZero的训练算法
这个算法简单明了:
1. 让计算机自我博弈数局,记录每一步走棋。一旦胜负已分,就给之前的每一步走棋打上标签——棋面最终是“赢”或是“输”。如此一来,我们就获得了一个可以用于神经网络(Neural Network,NN)训练的数据集,让该网络学会判断给定棋面是“赢面”还是“输面”;
2. 复制这个神经网络。用上一步得到的数据集训练该克隆网络;
3. 让克隆网络与原始神经网络互相博弈;
4. 上一步中获胜的网络留下,败者弃之;
5. 重复第1步。
呼哈,就像是魔法似的,经过多轮迭代后,你就将获得一个世界级模型。这个模型在短短4小时内便超越了最强大的计算机象棋程序。
AlphaZero的组件
AlphaZero由两部分构成。我们已经提及了第一部分,就是神经网络。第二部分则是“蒙特卡洛树搜索(Monte Carlo Tree Search)”,或者简称MCTS。
1. 神经网络(NN)。以棋面作为输入,输出该棋面的“价值”,外加所有可能走法的概率分布。
2. 蒙特卡洛树搜索(MCTS)。理想情况下,使用神经网络就足以选择下一步走法了。不过,我们仍然希望考虑尽可能多的棋面,并确保我们的的确确选择了最好的走法。MTCS和Minimax一样,是一种可以帮助我们寻找可能棋面的算法。与Minimax不同的是,MTCS能够帮助我们更加高效地搜寻博弈树。
让我们深入细节,看一看下一步走棋究竟是如何得到的
我们不妨先看看AlphaZero在决定下一步走棋(竞技模式)时具体干了什么,然后再去探究它的训练过程,这样可以帮助我们更容易地理解AlphaZero。
神经网络在分类这件事儿上表现得异常出色,例如区分猫跟狗。所以这里的想法很简单直接,神经网络能学会区分棋局输赢的类别吗?更具体地来说,就是让神经网络预测一个表示棋局输赢概率的数值。此外,它还将输出所有可能走法的概率分布,来表示我们下一步应该如何决策。
神经网络将博弈状态作为输入并输出一个输赢概率数值以及所有可能走法的概率分布。对于Dots and boxes这个小游戏来说,游戏状态由三个元素表示:首先,某一条线是否已被占用,这可以用一个含有0与1的数组来表示,如果玩家已经画了某条线,则置其为1,否则为0;第二,当前的走法是否是空过;第三,双方玩家的得分。我们可以用这三个元素来表示所有需要的信息,用其计算当前盘面的价值并预测下一步的走法。
让我们分析一下下图中的博弈情形,该轮轮到蓝色玩家走。蓝色方有两个选择,按照图中上面的走法来画线就会输,按照下面的走法就会赢。
将棋面送入神经网络,我们就能得到下一步走在不同位置的概率:
move_probability[0]: 9.060527501880689e-12
move_probability[1]: 3.9901679182996475e-10
move_probability[2]: 3.0028431828490586e-15
move_probability[3]: 7.959351400188552e-09
move_probability[4]: 5.271672681717021e-11
move_probability[5]: 4.101417122592821e-12
move_probability[6]: 1.2123925357696643e-16
move_probability[7]: 6.445387395019553e-23
move_probability[8]: 2.8522254313207743e-22
move_probability[9]: 0.0002768792328424752
move_probability[10]: 1.179791128073232e-13
move_probability[11]: 5.543385303737047e-13
move_probability[12]: 3.2618200407341646e-07
move_probability[13]: 4.302984970292259e-14
move_probability[14]: 2.7477634988877216e-16
move_probability[15]: 1.3767548163795204e-14
move_probability[16]: 8.998188305575638e-11
move_probability[17]: 7.494002147723222e-07
move_probability[18]: 8.540691764924446e-11
move_probability[19]: 9.55116696843561e-09
move_probability[20]: 4.6348909953086714e-12
move_probability[21]: 0.46076449751853943
move_probability[22]: 2.179317506813483e-20
move_probability[23]: 0.5389575362205505
move_probability[24]: 5.8165523789057046e-15
同时,我们还能得到当前棋局的赢面有多大:
-0.99761635
出相关的代码。
这些输出值有一些很有意思的地方,我们来细品一下:
在所有可能画线的位置,23号、21号以及9号的概率值最大。如果神经网络选择在23号以及21号位置处画线,那么它就能够得到1分。另外,23号才是能够赢下来的走法,而相应地,从网络输出的概率来看,23号位置的概率(0.53)恰好比21号的(0.46)稍微高一点儿。
神经网络也会给不能够画线的位置输出一个概率值。虽然如此,但是代码上还是要进行限制,以确保计算机不会在不合规则的位置画线。
棋面的输赢概率为-0.99。这意味着AlphaZero认为它已经输掉游戏了。这个概率值的范围是-1(输)到1(赢)。这个值本应该很接近于1(赢)而不是-1(输)的,毕竟我们知道目前这个局面赢面很大。也许我们应该多训练几轮来让AlphaZero准确预估棋面的输赢概率。
我们很容易利用神经网络的输出来决定下一步的走法。
在棋盘游戏中(现实生活中也是),玩家在决定下一步怎么走的时候往往会“多想几步”。AlphaZero也一样。我们用神经网络来选择最佳的下一步走法后,其余低概率的位置就被忽略掉了。像Minimax这一类传统的AI博弈树搜索算法效率都很低,因为这些算法在做出最终选择前需要穷尽每一种走法。即使是带有较少分支因子的游戏也会使其博弈搜索空间变得像是脱缰的野马似的难以驾驭。分支因子就是所有可能的走法的数量。这个数量会随着游戏的进行不断变化。因此,你可以试着计算一个平均分支因子数,国际象棋的平均分支因子是35,而围棋则是250。
这意味着,在国际象棋中,仅走两步就有1,225(35²)种可能的棋面,而在围棋中,这个数字会变成62,500(250²)。在Dots and Boxes游戏中,对于一个3×3大小的棋盘,初始的分支因子数是24,随着棋盘不断被填充,这个数字会不断减少(除非空过)。所以,在行至中局,分支因子变为15的时候,仅走3步就会有多达2730(15*14*13)种可能的棋面。
现在,时代变了,神经网络将指导并告诉我们哪些博弈路径值得探索,从而避免被许多无用的搜索路径所淹没。现在神经网络告诉我们23号和21号都是非常值得一探究竟的走法。
接着,蒙特卡洛树搜索算法就将登场啦!
蒙特卡洛树搜索(MCTS)
神经网络为我们指示了下一步可能的走法。蒙特卡洛树搜索算法将帮助我们遍历这些节点来最终选择下一步的走法。
去这个链接看看论文中有关蒙特卡洛树搜索的图形化描述。
使用MCTS的具体做法是这样的,给定一个棋面,MCTS共进行N次模拟。N是模型的超参数。N次模拟结束后,下一步的走法将是这N次模拟中所经次数最多的一步。你可以由这里的代码一窥究竟:
# https://github.com/suragnair/alpha-zero-general/blob/5156c7fd1d2f3e5fefe732a4b2e0ffc5b272f819/MCTS.py#L37-L48
for i in range(self.args.numMCTSSims): # self.args.numMCTSSims, the number of MCTS simulations to compute
self.search(canonicalBoard) # "search" is a MCTS simulations
s = self.game.stringRepresentation(canonicalBoard)
# Count how many times we have visited each node
counts = [self.Nsa[(s, a)] if (s, a) in self.Nsa else 0 for a in range(self.game.getActionSize())]
if temp == 0:
# Pick the node that was visited the most
bestAs = np.array(np.argwhere(counts == np.max(counts))).flatten()
bestA = np.random.choice(bestAs)
probs = [0] * len(counts)
probs[bestA] = 1
return probs
进行N次MCTS模拟
一次MCTS模拟从当前的棋盘状态出发,沿着博弈树中具有最大“置信区间上界(UCB)”值(后文会给出定义)的节点不断向下追溯,直到遇到之前从未见过的棋盘状态,也叫做“叶子”状态。这就是原论文中Part A所谓的“选择(Select)”。
置信区间上界是什么呢?用数学形式来说就是 Q(s, a) + U(s, a)。其中 s 是状态,a 是走法。Q(s, a) 是我们希望由走法“a”构成状态“s”能够获得的期望值,与Q-Learning中的期望值一致。记住了,在这种情况下,该值的范围是-1(输)到1(赢)。U(s, a) ∝ P(s, a) / (1 + N(s, a))。这意味着U正比于P和N。其中,P(s, a) 是元组 (s, a) 的先验概率值,这个值是从神经网络那里得到的,而 N(s, a) 是已经访问过状态 s 与对应的走法 a 的次数。
# Upper Confidence Bound
ucb = Qsa[(s,a)] + Ps[s,a] * sqrt(Ns[s]) / (1 + Nsa[(s,a)]
UCB的要点在于,其起初更倾向于具有较高先验概率(P)和较低访问次数(N)的走法,但渐渐地会倾向于具有较高动作价值(Q)的走法。
你不妨看看这里的代码好好理解一下。
# https://github.com/suragnair/alpha-zero-general/blob/5156c7fd1d2f3e5fefe732a4b2e0ffc5b272f819/MCTS.py#L105-L121
cur_best = -float('inf')
best_act = -1
# pick the action with the highest upper confidence bound
for a in range(self.game.getActionSize()):
if valids[a]:
if (s, a) in self.Qsa:
u = self.Qsa[(s, a)] + self.args.cpuct * self.Ps[s][a] * math.sqrt(self.Ns[s]) / (
1 + self.Nsa[(s, a)])
else:
u = self.args.cpuct * self.Ps[s][a] * math.sqrt(self.Ns[s] + EPS) # Q = 0 ?
if u > cur_best:
cur_best = u
best_act = a
a = best_act
next_s, next_player = self.game.getNextState(canonicalBoard, 1, a)
next_s = self.game.getCanonicalForm(next_s, next_player)
# Recursively visit the node
v = self.search(next_s)
Part A——选择具有最高置信区间上界值的走法
一旦找到一个叶子状态,就把这个棋面状态送入神经网络。这是论文中称作的Part B,“扩展与评估”。且看代码。
# leaf node
self.Ps[s], v = self.nnet.predict(canonicalBoard)
valids = self.game.getValidMoves(canonicalBoard, 1)
self.Ps[s] = self.Ps[s] * valids # masking invalid moves
sum_Ps_s = np.sum(self.Ps[s])
self.Ps[s] /= sum_Ps_s # renormalize
self.Vs[s] = valids
self.Ns[s] = 0
Part B——扩展与评估
最后,我们将传回神经网络返回的值。这就是论文所说的Part C——“备份”。您可以在此处看到相关代码。
v = self.search(next_s)
if (s, a) in self.Qsa:
self.Qsa[(s, a)] = (self.Nsa[(s, a)] * self.Qsa[(s, a)] + v) / (self.Nsa[(s, a)] + 1)
self.Nsa[(s, a)] += 1
else:
self.Qsa[(s, a)] = v
self.Nsa[(s, a)] = 1
self.Ns[s] += 1
return -v
决定下一步如何走
让我们来看看AlphaZero面对上文提及的棋面时会决定如何走
AlphaZero会进行50次蒙特卡洛树搜索模拟。
你可以用这个在线notebook复现下面展示的结果。
下面展示的就是每次迭代的路径:
Simulation #1 -> Expand root node
Simulation #2 -> 23
Simulation #3 -> 21
Simulation #4 -> 9
Simulation #5 -> 17
Simulation #6 -> 12
Simulation #7 -> 19
Simulation #8 -> 3
Simulation #9 -> 18
Simulation #10 -> 23,24
Simulation #11 -> 21,24
Simulation #12 -> 23,24,21
Simulation #13 -> 21,24,23,24
Simulation #14 -> 23,24,9
Simulation #15 -> 23,24,17
Simulation #16 -> 21,24,9
Simulation #17 -> 23,24,12
Simulation #18 -> 23,24,18
Simulation #19 -> 21,24,17
Simulation #20 -> 23,24,21,24,9
Simulation #21 -> 21,24,19
Simulation #22 -> 23,24,3
Simulation #23 -> 21,24,18
Simulation #24 -> 23,24,19
Simulation #25 -> 21,24,23,24,17
Simulation #26 -> 23,24,21,24,18
Simulation #27 -> 23,24,21,24,3
Simulation #28 -> 21,24,3
Simulation #29 -> 23,24,21,24,19
Simulation #30 -> 21,24,12
Simulation #31 -> 23,24,21,24,9,24
Simulation #32 -> 21,24,23,24,12
Simulation #33 -> 23,24,21,24,9,24,18
Simulation #34 -> 21,24,23,24,9,24,17
Simulation #35 -> 23,24,21,24,9,24,12
Simulation #36 -> 23,24,21,24,9,24,3
Simulation #37 -> 21,24,23,24,9,24,19
Simulation #38 -> 23,24,21,24,9,24,18,17
Simulation #39 -> 21,24,23,24,9,24,18,17,24
Simulation #40 -> 23,24,21,24,9,24,18,17,24,19
Simulation #41 -> 21,24,23,24,9,24,18,17,24,19,24
Simulation #42 -> 23,24,9,21
Simulation #43 -> 23,24,9,18
Simulation #44 -> 23,24,9,17
Simulation #45 -> 23,24,9,19
Simulation #46 -> 23,24,9,12
Simulation #47 -> 23,24,9,21,24
Simulation #48 -> 23,24,9,3
Simulation #49 -> 23,24,9,21,24,18
Simulation #50 -> 23,24,9,21,24,17