如何使用Python构建自己的MuZero AI
作者:David Foster
发表时间:2019年12月2日
原文连接:
MuZero: The Walkthrough(Part1/3),https://medium.com/applied-data-science/how-to-build-your-own-muzero-in-python-f77d5718061a
MuZero: The Walkthrough(Part2/3),https://medium.com/applied-data-science/how-to-build-your-own-deepmind-muzero-in-python-part-2-3-f99dad7a7ad
MuZero: The Walkthrough(Part3/3),https://medium.com/applied-data-science/how-to-build-your-own-deepmind-muzero-in-python-part-3-3-ccea6b03538b
如果你想了解最复杂的人工智能系统是如何工作的,那么,你来对地方了!
在这篇由三部分组成的系列中(翻译后合并为一个长篇),我们将探讨DeepMind发布的MuZero模型内部工作机制,MuZero是AlphaZero年轻的弟弟,但甚至更令人印象深刻。
还可以查看我的最新帖子,关于如何训练多人棋盘游戏的强化学习智能体,使用自我对弈!
我们将研究一下MuZero论文附带的伪代码——所以,沏一杯茶,找张舒服的椅子,开始吧。
α \alpha α
到目前为止的故事…
2019年11月19日,DeepMind向全球发布了最新的基于模型的强化学习算法——MuZero(译者:2012年12月23日,论文被《自然》收录)。
从2016年的AlphaGo论文开始,这已经是DeepMind深度强化学习不断突破的一系列论文中的第四篇。
要阅读从AlphaGo到AlphaZero的全部历史,请查看我以前的博客。
AlphaZero被誉为是一种通用的算法,它可以在没有任何人类专家先验知识的情况下快速地做好某件事。
那么,现在呢?
μ \mu μ
MuZero:用学习模型规划玩转雅达利、围棋、国际象棋和日本将棋。
MuZero跨出了终极的一步,MuZero拒不接受人类玩家的策略,甚至也不接受游戏规则。
换句话说,对于国际象棋,AlphaZero面对的挑战是:
学习如何自己与自己玩好这个游戏,这里有规则手册,解释每个棋子如何移动,哪些移动是合法的。它还告诉你如何判断一个棋局已经被将死(或者是平局)。
而MuZero面对的挑战是:
学会如何自己与自己玩好这个游戏,我会告诉你当前棋局什么动作是合规的,什么时候某一方赢了(或是平局),但我不会告诉你游戏中的整体规则。
因此,除了制定能赢的策略,MuZero还必须开发自己的的环境动力学模型,以便能够理解其选择的含义和未来规划。
想象一下,在一场从未被提前告知规则的比赛中,你试图玩得比世界冠军更好。MuZero恰恰做到了这一点!
在这篇文章中,我们将详细介绍MuZero是如何实现这个惊人壮举的。
MuZero伪代码
随MuZero论文,DeepMind还发布了Python伪代码,详细描述了算法每个部分之间的相互作用关系。
在本文中,我们将把其中的类和函数掰开了揉碎了,按逻辑摆放,并解释每个部分的功能和原理。我们假设MuZero正在学习下国际象棋,但这个过程对于任何游戏起始都一样,只是参数不同而已。全部伪代码都来自DeepMind开源伪代码。
让我们从对整个过程的描述开始,起点是入口函数muzero
。
MuZero自我博弈(self-play)和训练(training)过程概述
def muzero(config: MuZeroConfig):
storage = SharedStorage()
replay_buffer = ReplayBuffer(config)
for _ in range(config.num_actors):
launch_job(run_selfplay, config, storage, replay_buffer)
train_network(config, storage, replay_buffer)
return storage.latest_network()
入口函数muzero
的参数是一个MuZeroConfig
对象,该对象存放了重要的运行参数,例如action_space_size
和num_actors
(要同时启动的游戏总数)。在其它函数中遇到这些参数时,我们再详细地介绍它们。
在较高的层次上,MuZero算法有两个独立的部分:自我博弈self-play(创建游戏数据)和训练training(不断改进神经网络)。算法的这两个部分都可以访问SharedStorage
和ReplayBuffer
对象,并分别保存神经网络和游戏数据。
共享存储和回放缓冲区
SharedStorage
对象包含用于保存神经网络和从存储中获取最新神经网络的方法。
class SharedStorage(object):
def __init__(self):
self._networks = {
}
def latest_network(self) -> Network:
if self._networks:
return self._networks[max(self._networks.keys())]
else:
# policy -> uniform, value -> 0, reward -> 0
return make_uniform_network()
def save_network(self, step: int, network: Network):
self._networks[step] = network
我们还需要一个ReplayBuffer
来存储生成的游戏数据。其形式如下:
class ReplayBuffer(object):
def __init__(self, config: MuZeroConfig):
self.window_size = config.window_size
self.batch_size = config.batch_size
self.buffer = []
def save_game(self, game):
if len(self.buffer) > self.window_size:
self.buffer.pop(0)
self.buffer.append(game)
...
注意window_size
参数是回放缓冲区中能保存的游戏最大数量。在MuZero中,设置为最近生成的1,000,000个游戏。
自我博弈(self-play)
在创建了共享存储和回放缓冲区之后,MuZero启动了num_actors
个独立运行的并行游戏环境。国际象棋的num_actors
设为3000。每个游戏环境都运行一个函数run_selfplay
,从共享存储中取出最新版本网络参数,用它玩游戏play_game
,并将游戏数据保存到回放缓冲区。
# Each self-play job is independent of all others; it takes the latest network
# snapshot, produces a game and makes it available to the training job by
# writing it to a shared replay buffer.
def run_selfplay(config: MuZeroConfig, storage: SharedStorage, replay_buffer: ReplayBuffer):
while True:
network = storage.latest_network()
game = play_game(config, network)
replay_buffer.save_game(game)
总之,MuZero不停地进行着成千上万次游戏,将这些游戏保存到一个回放缓冲区,然后根据这些游戏数据不断地训练自己。到目前为止,这与AlphaZero没什么不同。
我们将讨论AlphaZero和MuZero之间的关键区别——为什么MuZero有三个神经网络,而AlphaZero只有一个?
MuZero的3个神经网络
AlphaZero和MuZero都采用一种称为蒙特卡罗树搜索(MCTS)的技术来选择下一步动作的最佳方案。
这个想法是:为了选择下一步动作的最佳方案,就要“播放”从当前位置到未来可能出现的各种场景,然后用神经网络评估它们的价值,并选择未来预期价值最大化的动作。这似乎是我们人类下棋时脑子里面想的事,人工智能体也设计成利用这个技巧。
但是,这里有个问题,MuZero并不知道游戏规则,也就无从知晓一个给定的动作会如何影响游戏状态,所以它无法想象未来MCTS中的场景。它甚至不知道如何从给定的棋局中判断出下一步哪些动作是合法的,或者哪一方赢了。
MuZero论文中惊人的进展就是证明了这并不是问题。MuZero在自己的想象中创建了一个环境动力学模型,并在这个模型中进行优化学习如何玩游戏。
下图显示了AlphaZero和MuZero中MCTS进程之间的比较:
AlphaZero只有一个神经网络(预测predict),而MuZero则需要三个神经网络(预测predict、动力学dynamics、表征representation)
AlphaZero 预测(predict) 神经网络f
的工作是预测给定游戏状态下的策略p
和价值v
。该策略p
是全部动作的概率分布,价值v
是对未来奖励估计的单一数值。每次MCTS走到一个未探测的叶节点时,都会进行这种预测,这样就可以立即为新棋局分配一个估计价值,并为后续每个动作分配概率。这些值被反向写到树上,直到根节点,这样经过多次模拟,在探索了大量未来不同可能性之后,根节点对当前状态下未来价值变得胸有成竹。
MuZero同样有一个 预测(predict) 神经网络f
,但它所依据的“游戏状态”是一个隐藏表征,MuZero学习如何通过 动力学(dynamics) 神经网络g
生成隐藏表征。动力学网络获取当前隐藏状态s
和动作a
,输出奖励r
和新状态。注意,在AlphaZero中,MCTS树的状态转移是向环境要;而MuZero没那么奢侈,所以需要建立自己的的环境动力学模型!
最后,为了将当前观测到的游戏状态映射到初始表征,MuZero使用了第三种**表征(representation)**神经网络h
。
因此,MuZero需要两个推理函数,以便通过MCTS树进行预测:
-
initial_inference
为当前状态的初始推理,也就是h
(表征)后面接f
(预测)。 -
recurrent_inference
为沿着MCTS树,在状态之间转移的递归推演,g
(动力学)后接f
(预测)。
MuZero中的两类推演
虽然伪代码中没有提供精确的模型,但论文中给出了详细的描述。
class NetworkOutput(typing.NamedTuple):
value: float
reward: float
policy_logits: Dict[Action, float]
hidden_state: List[float]
class Network(object):
def initial_inference(self, image) -> NetworkOutput:
# r