请在桌面浏览器中打开此链接,保证最好的浏览体验
关注公众号后回复
docker-geometric
,获取 Docker Image
1 | # 导入image |
Pytorch Geometric 系列将结合 pytorch geometric 类库,从原理结合实践来讲解深度图神经网络。和前几期一样,这一系列所有环境已经为大家封装成了 docker image。预制的 image 中既包含了 pytorch 1.8 和对应的 geometric,networkx,Jubyter notebook 还有画图涉及到的 matplotlib 和 hvplot 类库。除此之外,也包含了预下载的 geometric dateset 和所用到的代码。方便大家一键开始 coding。
获取docker image 环境的方式,小伙伴们可以通过关注 MyEncyclopedia 公众号并在公众号对话框中回复
docker-geometric
后获得
关于具体使用我制作的 docker image 这个主题,大家可以再公众号或者同名
B站账号,找到 docker 封装工程依赖
一集,
其中包括如下高级用法
如果觉得下载镜像麻烦的小伙伴们,也可以自己准备对应的 python package,再拷贝下方链接的代码直接运行。
https://mydoc.myencyclopedia.top/pub/code/cora-plot
这个系列涉及到的 python package 包括 - torch-geometric
networkx
hvplot
livelossplot
当预制的 docker image 下载并装载完毕后,通过下面命令来启动 container 内置的 Jupyter notebook 服务器,这样可以使得主机种的浏览器访问到 container 中的服务器。
1 | docker run -p 8888:8888 -it env-conda-x jupyter notebook --allow-root --ip 0.0.0.0 |
下一步,我们会通过 geometric 类库加载 cora 数据集。这一步通常来说需要从网上下载,但是预制的 docker image 已经为大家下载好了 planetoid 图数据集。Planetoid 包含了 Cora,Pubmed 和 Citeseer。
因此,加载数据集这一步执行非常快
1 | import numpy as np |
然后我们将 cora 转换成 networkx 格式。networkx 是 python 中一个比较流行的图类库。我们在后面 visualization 中也会利用networkx 的功能。
Cora 有 7 种节点类型,我们将每种节点类型赋予不同颜色,有助于更好 visualization。
1 | from torch_geometric.utils import to_networkx |
接着,调用 networkx 的 spring_layout 计算每个节点的弹簧布局下的位置,这一步执行会比较耗时。
1 | import matplotlib.pyplot as plt |
我们首先来看一下 matplotlib 的渲染效果。
1 | plt.figure(figsize=(16,12)) |
因为 matplotlib 只能画出一张静态图片, 无法做 interaction,也无法动态缩放。因此渲染效果不是特别好,尤其是对于 cora 这种数据量比较大的 graph 尤为显著。
我们看到图片种尽管有七种颜色的节点,但是当中存在的这块密集的点,我们很难看出节点和节点之间的关系。
我们换一个类库 hvplot,它的渲染和交换效果如下。
代码和 matplotlib 大致一致。注意渲染的时候 hvplot 需要将多个图片数据以乘法形式返回,借助 reduce 函数我们将 7 种节点的图相乘,再乘以描绘边的图,呈现出叠加的完整图片。
1 | import hvplot.networkx as hvnx |
上一篇我们从原理层面解析了AlphaGo Zero如何改进MCTS算法,通过不断自我对弈,最终实现从零棋力开始训练直至能够打败任何高手。在本篇中,我们在已有的N子棋OpenAI Gym 环境中用Pytorch实现一个简化版的AlphaGo Zero算法。本篇所有代码在 github MyEncyclopedia/ConnectNGym 中,其中部分参考了SongXiaoJun 的 AlphaZero_Gomoku。
上一篇中,我们知道AlphaGo Zero 的MCTS树搜索是基于传统MCTS 的UCT (UCB for Tree)的改进版PUCT(Polynomial Upper Confidence Trees)。局面节点的PUCT值由两部分组成,分别是代表Exploitation的action value Q值,和代表Exploration的U值。 \[ PUCT(s, a) =Q(s,a) + U(s,a) \] U值计算由这些参数决定:系数\(c_{puct}\),节点先验概率P(s, a) ,父节点访问次数,本节点的访问次数。具体公式如下 \[ U(s, a)=c_{p u c t} \cdot P(s, a) \cdot \frac{\sqrt{\Sigma_{b} N(s, b)}}{1+N(s, a)} \]
因此在实现过程中,对于一个树节点来说,需要保存其Q值、节点访问次数 _visit_num和先验概率 _prior。其中,_prior在节点初始化后不变,Q值和 visit_num随着游戏MCTS模拟进程而改变。此外,节点保存了 parent和_children变量,用于维护父子关系。c_puct为class variable,作为全局参数。
1 | class TreeNode: |
和上面的计算公式相对应,下列代码根据节点状态计算PUCT(s, a)。
1 | class TreeNode: |
AlphaGo Zero MCTS在playout时遇到已经被展开的节点,会根据selection规则选择子节点,该规则本质上是在所有子节点中选择最大的PUCT值的节点。
\[ a=\operatorname{argmax}_a(PUCT(s, a))=\operatorname{argmax}_a(Q(s,a) + U(s,a)) \]
1 | class TreeNode: |
新的叶节点一旦在playout时产生,关联的 v 值会一路向上更新至根节点,具体新节点的v值将在下一节中解释。
1 | class TreeNode: |
AlphaGo Zero MCTS 在训练阶段分为如下几个步骤。游戏初始局面下,整个局面树的建立由子节点的不断被探索而丰富起来。AlphaGo Zero对弈一次即产生了一次完整的游戏开始到结束的动作系列。在对弈过程中的某一游戏局面,需要采样海量的playout,又称MCTS模拟,以此来决定此局面的下一步动作。一次playout可视为在真实游戏状态树的一种特定采样,playout可能会产生游戏结局,生成真实的v值;也可能explore 到新的叶子节点,此时v值依赖策略价值网络的输出,目的是利用训练的神经网络来产生高质量的游戏对战局面。每次playout会从当前给定局面递归向下,向下的过程中会遇到下面三种节点情况。
海量的playout模拟后,建立了游戏状态树的节点信息。但至此,AI玩家只是收集了信息,还仍未给定局面落子,而落子的决定由Play规则产生。下图展示了给定局面(Current节点)下,MCST模拟进行的多次playout探索后生成的局面树,play规则根据这些节点信息,产生Current 节点的动作分布 \(\pi\) ,确定下一步落子。
对于当前需要做落子决定的某游戏局面\(s_0\),根据如下play公式生成落子分布 $$ ,子局面的落子概率正比于其访问次数的某次方。其中,某次方的倒数称为温度参数(Temperature)。
\[ \pi\left(a \mid s_{0}\right)=\frac{N\left(s_{0}, a\right)^{1 / \tau}}{\sum_{b} N\left(s_{0}, b\right)^{1 / \tau}} \]
1 | class MCTSAlphaGoZeroPlayer(BaseAgent): |
在训练模式时,考虑到偏向exploration的目的,在\(\pi\) 落子分布的基础上增加了 Dirichlet 分布。
\[ P(s,a) = (1-\epsilon)*\pi(a \mid s) + \epsilon * \boldsymbol{\eta} \quad (\boldsymbol{\eta} \sim \operatorname{Dir}(0.3)) \]
1 | class MCTSAlphaGoZeroPlayer(BaseAgent): |
一次完整的AI对弈就是从初始局面迭代play直至游戏结束,对弈生成的数据是一系列的 $(s, , z) $。
如下图 s0 到 s5 是某次井字棋的对弈。最终结局是先手黑棋玩家赢,即对于黑棋玩家 z = +1。需要注意的是:z = +1 是对于所有黑棋面临的局面,即s0, s2, s4,而对应的其余白棋玩家来说 z = -1。
\[ \begin{align*} &0: (s_0, \vec{\pi_0}, +1) \\ &1: (s_1, \vec{\pi_1}, -1) \\ &2: (s_2, \vec{\pi_2}, +1) \\ &3: (s_3, \vec{\pi_3}, -1) \\ &4: (s_4, \vec{\pi_4}, +1) \end{align*} \]
以下代码展示如何在AI对弈时收集数据 $(s, , z) $
1 | class MCTSAlphaGoZeroPlayer(BaseAgent): |
一次playout会从当前局面根据PUCT selection规则下沉到叶子节点,如果此叶子节点非游戏终结点,则会扩展当前节点生成下一层新节点,其先验分布由策略价值网络输出的action分布决定。一次playout最终会得到叶子节点的 v 值,并沿着MCTS树向上更新沿途的所有父节点 Q值。 从上一篇文章已知,游戏节点的数量随着参数而指数级增长,举例来说,井字棋(k=3,m=n=3)的状态数量是5478,k=3,m=n=4时是6035992 ,k=m=n=4时是9722011 。如果我们将初始局面节点作为根节点,同时保存海量playout探索得到的局面节点,实现时会发现我们无法将所有探索到的局面节点都保存在内存中。这里的一种解决方法是在一次self play中每轮playout之后,将根节点重置成落子的节点,从而有效控制整颗局面树中的节点数量。
1 | class MCTSAlphaGoZeroPlayer(BaseAgent): |
为了将信息有效的传递给策略神经网络,必须从当前玩家的角度编码游戏局面。局面不仅要反映棋盘上黑白棋子的位置,也需要考虑最后一个落子的位置以及是否为当前玩家棋局。因此,我们将某局面按照当前玩家来编码,返回类型为4个棋盘大小组成的ndarray,即shape [4, board_size, board_size],其中
例如之前游戏对弈中的前四步:
s1->s2 后局面s2的编码:当前玩家为黑棋玩家,编码局面s2 返回如下ndarray,数组[0] 为s2黑子位置,[1]为白子位置,[2]表示最后一个落子(1, 1) ,[3] 全1表示当前是黑棋落子的局面。
s2->s3 后局面s3的编码:当前玩家为白棋玩家,编码返回如下,数组[0] 为s3白子位置,[1]为黑子位置,[2]表示最后一个落子(1, 0) ,[3] 全0表示当前是白棋落子的局面。具体代码实现如下。
1 | NetGameState = NDArray[(4, Any, Any), np.int] |
策略价值网络是一个共享参数 \(\theta\) 的双头网络,给定上面的游戏局面编码会产生预估的p和v。
\[ \vec{p_{\theta}}, v_{\theta}=f_{\theta}(s) \] 结合真实游戏对弈后产生三元组数据 $(s, , z) $ ,按照论文中的loss 来训练神经网络。 \[ l=\sum_{t}\left(v_{\theta}\left(s_{t}\right)-z_{t}\right)^{2}-\vec{\pi_{t}} \cdot \log \left(\vec{p_{\theta}}\left(s_{t}\right)\right) + c {\lVert \theta \rVert}^2 \]
下面代码为Pytorch backward部分。
1 | def backward_step(self, state_batch: List[NetGameState], probs_batch: List[ActionProbs], |
Youtube, Deepmind AlphaZero - Mastering Games Without Human Knowledge, David Silver
Mastering the game of Go with deep neural networks and tree search
Mastering Chess and Shogi by Self-Play with a General Reinforcement Learning Algorithm
Update your browser to view this website correctly. Update my browser now