<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.0">Jekyll</generator><link href="https://archwalker.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://archwalker.github.io/" rel="alternate" type="text/html" /><updated>2021-07-16T16:06:48+08:00</updated><id>https://archwalker.github.io/feed.xml</id><title type="html">ArchWalker</title><subtitle>Personal Blog on Graph Neural Networks, Distributed Computing, Functional Programming
</subtitle><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><entry><title type="html">GNN 教程：图神经网络框架和他们的设计理念对比</title><link href="https://archwalker.github.io/blog/2020/04/06/GNN-Framework-Comparison.html" rel="alternate" type="text/html" title="GNN 教程：图神经网络框架和他们的设计理念对比" /><published>2020-04-06T12:00:00+08:00</published><updated>2020-04-06T12:00:00+08:00</updated><id>https://archwalker.github.io/blog/2020/04/06/GNN-Framework-Comparison</id><content type="html" xml:base="https://archwalker.github.io/blog/2020/04/06/GNN-Framework-Comparison.html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;此为原创文章，未经许可，禁止转载&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;最近我们开源了我们在阿里内部场景上使用的超大规模图神经网络计算框架 &lt;a href=&quot;https://github.com/alibaba/graph-learn&quot;&gt;graph-learn&lt;/a&gt;，graph-learn作为从业务实践角度出发而孵化的GNN框架，原生支持单机多卡，多机多卡，CPU、GPU等分布式集群的超大规模图数据的存储、调度与计算。与此同时，还有很多优秀的图计算框架也已经开源并仍活跃地维护着，他们包括Amazon AI lab的 &lt;a href=&quot;https://www.dgl.ai/&quot;&gt;DGL&lt;/a&gt;，Matthias Fey 博士的 &lt;a href=&quot;https://pytorch-geometric.readthedocs.io/en/latest/index.html&quot;&gt;pytorch_geometric&lt;/a&gt;等。我阅读了这些框架的文档，整理一篇文章介绍下各个框架的设计理念以及一些可以互相借鉴的地方。&lt;/p&gt;

&lt;h2 id=&quot;pytorch_geometric-下文将简写成pyg&quot;&gt;Pytorch_geometric (下文将简写成PyG)&lt;/h2&gt;

&lt;p&gt;Pytorch_geometric 是我最早接触的GNN框架，它将GNN的更新用下面的抽象表示：
\(\mathbf{x}_{i}^{(k)}=\gamma^{(k)}\left(\mathbf{x}_{i}^{(k-1)}, \square_{j \in \mathcal{N}(i)} \phi^{(k)}\left(\mathbf{x}_{i}^{(k-1)}, \mathbf{x}_{j}^{(k-1)}, \mathbf{e}_{j, i}\right)\right)\)
其中$\square$用来表示聚合函数，如max，mean等，$\gamma$和$\phi$用来表示一个可微的变换函数，比如多层感知机MLPs。$\mathbf{x}$表示节点的表征，可以是最初的输入特征，或者是更新之后的embeddings。$e$表示边上的特征，总结起来，节点$i$的更新是由两部分组成，一部分来自其本身，一部分来自其邻居节点和关联的边特征。&lt;/p&gt;

&lt;p&gt;​	根据这个模型抽象，作者设计的编程接口是这样的，基类&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessagePassing&lt;/code&gt;提供参数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agg&lt;/code&gt;来实现聚合函数$\square$，基类的两个成员函数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt;分别对应$\phi^{(k)}\left(\mathbf{x}&lt;em&gt;{i}^{(k-1)}, \mathbf{x}&lt;/em&gt;{j}^{(k-1)}, \mathbf{e}&lt;em&gt;{j, i}\right)$和$\gamma^{(k)}\left(\mathbf{x}&lt;/em&gt;{i}^{(k-1)},…\right)$的实现，用户继承&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessagePassing&lt;/code&gt;并实现具体的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update&lt;/code&gt;函数以实现自定义的GNN，最后通过在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forward&lt;/code&gt;方法中调用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;propagate&lt;/code&gt;方法驱动整个计算流程的进行。&lt;/p&gt;

&lt;p&gt;​	为了说明这样的编程抽象接口对于大多数GNN算法都是通用的，作者在&lt;a href=&quot;https://pytorch-geometric.readthedocs.io/en/latest/notes/create_gnn.html&quot;&gt;文档&lt;/a&gt;中以GCN layer和EdgeConv layer为例子，详细说明了实现的步骤。PyG也提供了非常多的已实现的&lt;a href=&quot;https://github.com/rusty1s/pytorch_geometric/tree/master/examples&quot;&gt;模型&lt;/a&gt;作为通用性的证明。&lt;/p&gt;

&lt;p&gt;​	在编程模型中，PyG模型的一个特点是需要显示传递边信息的邻接表，编程中用$e$来表示，它是一个$[2, \textrm{num_edges}]$的二维矩阵，显然，对于超大规模的数据，这样一个矩阵在内存中是存不下的，因此作者提供了split的策略，通过内建的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;torch_geometric.data.Dataset&lt;/code&gt;接口，PyG能自动将一个大图切分成多个小图，每个小图用上述的逻辑进行计算，每个小图上的计算仍然需要传递每个小图的边矩阵。&lt;/p&gt;

&lt;p&gt;​	PyG目前不提供分布式计算的逻辑，因此大图拆分后的小图计算是串行的，小图内部的计算可以并行起来，因此PyG安装时依赖了torch-cluster, torch-scatter等库。&lt;/p&gt;

&lt;h2 id=&quot;deep-graph-library-下文将简写成dgl&quot;&gt;Deep Graph Library (下文将简写成DGL)&lt;/h2&gt;

&lt;p&gt;DGL是目前非常优秀的图计算框架，它将GNN邻居汇聚用”消息传递“这种计算模式统一起来，提供了非常完善的”消息传递”式全图计算框架，DGL 的核心为消息传递机制（message passing），用户在使用的时候可以不考虑当前batch size大小、邻居个数是否对齐等信息，极大得简化了编程流程。&lt;/p&gt;

&lt;p&gt;DGL计算的核心是消息函数 （message function）和汇聚函数（reduce function）。如下图所示，假设我们需要更新目的节点的Embedding，那么DGL将计算抽象成了两个部分：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tva1.sinaimg.cn/large/00831rSTly1gdiya5nj4rj30eh06qq37.jpg&quot; alt=&quot;DGL&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;消息函数（message function）：对每个源节点来说，准备他的目的节点需要的信息（比如源节点的Embedding和与目的节点链接的边的Embedding或者weight等），然后把这些信息作为消息传递到目的节点的Mailbox里。如上图所示：对每条边（edge）来说，每个源节点将会将自身的Embedding（src.data）和边的Embedding(edge.data)传递到目的节点；对于每个目的节点来说，它可能会受到多个源节点传过来的消息，它会将这些消息存储在”邮箱”中。&lt;/li&gt;
  &lt;li&gt;汇聚函数（reduce function）：汇聚函数的目的是根据源节点传过来的消息更新更新目的节点Embedding，对目的节点来说，它先从邮箱（Mailbox）中汇聚源节点传递过来的消息（message），并清空邮箱（Mailbox）内消息；然后目的节点结合汇聚后的结果和原Embedding以做一次Embedding更新。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;根据这两个抽象，利用DGL的编程接口实现一个自定义图模型只需要提供两个函数，即&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message_function&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reduce_function&lt;/code&gt;，前者用来指导如何对源节点的数据和边数据进行选择与加工然后传递到目的节点的邮箱；后者用来指导目的节点如何利用邮箱中它的邻居传递过来的消息和自身Embedding进行融合以更新自身Embedding。最后通过在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forward&lt;/code&gt;函数中调用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_all(message_function, reduce_function)&lt;/code&gt;来驱动整个计算流程的进行。DGL一个非常明显的优势是整个编程的抽象中不需要传递任何关于图结构的信息，即不需要边的邻接矩阵或者邻接表，边结构相关的信息在数据载入的过程中被底层的框架捕获并对上提供所需要的信息(主要是邻居的查询)，不需要用户在编程中进行干预。&lt;/p&gt;

&lt;p&gt;DGL提供了详细的&lt;a href=&quot;https://docs.dgl.ai/tutorials/models/1_gnn/1_gcn.html&quot;&gt;教程&lt;/a&gt;解释了如何利用消息传递的机制实现GCN，为了证明这种编程抽象是合理的，它也提供了很多已实现的&lt;a href=&quot;https://github.com/dmlc/dgl/tree/master/examples/pytorch&quot;&gt;模型&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;这种”消息传递“的机制需要全图被预先载入到内存中，因此不适合在大规模数据集上训练，为了应对超大规模数据的问题，DGL团队采用和GraphSAGE类似的思路，将一个batch内所需要更新目的节点相关的信息一次性提取出来，构成”计算子图“，称为“NodeFLow”。计算子图被载入到内存中通过”消息传递“这样的方式计算，子图和子图之间的采样和更新都是可并行的。在最新的0.4.3版本中，DGL已经支持了单机多卡的并行，和多机cpu版本的分布式计算。&lt;/p&gt;

&lt;h2 id=&quot;graph-learn-下文将简写成gl&quot;&gt;Graph-learn （下文将简写成GL)&lt;/h2&gt;

&lt;p&gt;GL是从阿里内部业务场景出发抽象出来的图神经网络框架，首要的目的是为了解决内部场景应用时候要面对的超大规模图数据的问题，我们面临的图数据规模从几千万到几百亿不等，面对如此大的数据规模，在存储上我们采用分布式的方案，在计算上我们采用了和GraphSAGE类似的子图计算架构，即把一个batch内所需要更新目的节点相关的信息一次性提取出来，构成计算子图，以batch为单位更新梯度，我们把这个流程称为sampling。整个存储和计算都是可以是分布式的，在实际场景上，我们已经实现了单机多卡，多机多卡、CPU、GPU等分布式集群的训练和预测。&lt;/p&gt;

&lt;p&gt;我们将GNN的算法流程抽象成以下步骤：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tva1.sinaimg.cn/large/007S8ZIlly1gdyy2iset7j30yk0iiaeb.jpg&quot; alt=&quot;3&quot; /&gt;&lt;/p&gt;

&lt;p&gt;其中AGGREGATE的功能类似于PyG中的$\square$和$\phi$，而COMBINE的功能类似于PyG中的$\gamma$，而SAMPLE就是用来返回节点邻居的函数，实现上SAMPLE内部进行了非常多的系统端优化，以至于整个采样的时间相较于训练基本可以忽略不计。当然，虽然它的名字叫SAMPLE采样，但是对于像GCN和GAT这种需要全部邻居参与计算的模型，我们也提供了能够返回全部邻居的接口。对于一个batch的数据，由于每个节点的邻居数据是不定的，这时候全邻居SAMPLE的返回结果将会被封装成一个SparseTensor，并为每个源节点提供必要的邻居定位信息segment_ids以供下游算法使用。&lt;/p&gt;

&lt;p&gt;对于每个采样出来的计算子图，我们的编程接口是这么设计的：每个图算法最核心的卷积层更新被统一成&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;def forward(self, self_vecs, neigh_vecs, segment_ids)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样的形式，其中self_vecs是目的节点自身的Embedding，而neigh_vecs是源节点的Embedding，用户需要在这个函数中自定义自己的计算逻辑，即这个函数即做了AGGREGATE的工作，也做了COMBINE的工作，函数的输出即是目的节点更新后的Embedding。segment_ids是在全邻居采样返回的neigh_vecs为SparseTensor时定位每个目的节点的neigh是哪几个用到的，是这里提供一个GCN conv layer的&lt;a href=&quot;https://github.com/alibaba/graph-learn/blob/master/graphlearn/python/model/tf/layers/gcn_conv.py&quot;&gt;例子&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;定义完卷积层之后，对每个自定义的图算法，我们提供了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LearningBasedModel&lt;/code&gt;这个基类来处理和训练相关的东西，用户需要重写&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_encoders&lt;/code&gt;这个函数提供对于多跳的邻居如何逐层更新Embedding的逻辑（即如何堆叠之前实现的conv layer)，最后重写&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt;函数，以驱动整个计算流程的进行。&lt;/p&gt;

&lt;p&gt;我们提供了详细的&lt;a href=&quot;https://github.com/alibaba/graph-learn/blob/master/docs/quick_start.md&quot;&gt;教程&lt;/a&gt;解释了如何graph-learn实现GCN，也提供了很多已实现的&lt;a href=&quot;https://github.com/alibaba/graph-learn/tree/master/examples/tf&quot;&gt;模型&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;graph-learn架构、原理等一些其他的参考文档：&lt;/p&gt;

&lt;p&gt;https://yq.aliyun.com/articles/752628&lt;/p&gt;

&lt;p&gt;https://yq.aliyun.com/articles/752630&lt;/p&gt;

&lt;p&gt;https://yq.aliyun.com/articles/752637&lt;/p&gt;

&lt;p&gt;https://yq.aliyun.com/articles/752645&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">引言 此为原创文章，未经许可，禁止转载 最近我们开源了我们在阿里内部场景上使用的超大规模图神经网络计算框架 graph-learn，graph-learn作为从业务实践角度出发而孵化的GNN框架，原生支持单机多卡，多机多卡，CPU、GPU等分布式集群的超大规模图数据的存储、调度与计算。与此同时，还有很多优秀的图计算框架也已经开源并仍活跃地维护着，他们包括Amazon AI lab的 DGL，Matthias Fey 博士的 pytorch_geometric等。我阅读了这些框架的文档，整理一篇文章介绍下各个框架的设计理念以及一些可以互相借鉴的地方。 Pytorch_geometric (下文将简写成PyG) Pytorch_geometric 是我最早接触的GNN框架，它将GNN的更新用下面的抽象表示： \(\mathbf{x}_{i}^{(k)}=\gamma^{(k)}\left(\mathbf{x}_{i}^{(k-1)}, \square_{j \in \mathcal{N}(i)} \phi^{(k)}\left(\mathbf{x}_{i}^{(k-1)}, \mathbf{x}_{j}^{(k-1)}, \mathbf{e}_{j, i}\right)\right)\) 其中$\square$用来表示聚合函数，如max，mean等，$\gamma$和$\phi$用来表示一个可微的变换函数，比如多层感知机MLPs。$\mathbf{x}$表示节点的表征，可以是最初的输入特征，或者是更新之后的embeddings。$e$表示边上的特征，总结起来，节点$i$的更新是由两部分组成，一部分来自其本身，一部分来自其邻居节点和关联的边特征。 ​ 根据这个模型抽象，作者设计的编程接口是这样的，基类MessagePassing提供参数agg来实现聚合函数$\square$，基类的两个成员函数message和update分别对应$\phi^{(k)}\left(\mathbf{x}{i}^{(k-1)}, \mathbf{x}{j}^{(k-1)}, \mathbf{e}{j, i}\right)$和$\gamma^{(k)}\left(\mathbf{x}{i}^{(k-1)},…\right)$的实现，用户继承MessagePassing并实现具体的message和update函数以实现自定义的GNN，最后通过在forward方法中调用propagate方法驱动整个计算流程的进行。 ​ 为了说明这样的编程抽象接口对于大多数GNN算法都是通用的，作者在文档中以GCN layer和EdgeConv layer为例子，详细说明了实现的步骤。PyG也提供了非常多的已实现的模型作为通用性的证明。 ​ 在编程模型中，PyG模型的一个特点是需要显示传递边信息的邻接表，编程中用$e$来表示，它是一个$[2, \textrm{num_edges}]$的二维矩阵，显然，对于超大规模的数据，这样一个矩阵在内存中是存不下的，因此作者提供了split的策略，通过内建的torch_geometric.data.Dataset接口，PyG能自动将一个大图切分成多个小图，每个小图用上述的逻辑进行计算，每个小图上的计算仍然需要传递每个小图的边矩阵。 ​ PyG目前不提供分布式计算的逻辑，因此大图拆分后的小图计算是串行的，小图内部的计算可以并行起来，因此PyG安装时依赖了torch-cluster, torch-scatter等库。 Deep Graph Library (下文将简写成DGL) DGL是目前非常优秀的图计算框架，它将GNN邻居汇聚用”消息传递“这种计算模式统一起来，提供了非常完善的”消息传递”式全图计算框架，DGL 的核心为消息传递机制（message passing），用户在使用的时候可以不考虑当前batch size大小、邻居个数是否对齐等信息，极大得简化了编程流程。 DGL计算的核心是消息函数 （message function）和汇聚函数（reduce function）。如下图所示，假设我们需要更新目的节点的Embedding，那么DGL将计算抽象成了两个部分： 消息函数（message function）：对每个源节点来说，准备他的目的节点需要的信息（比如源节点的Embedding和与目的节点链接的边的Embedding或者weight等），然后把这些信息作为消息传递到目的节点的Mailbox里。如上图所示：对每条边（edge）来说，每个源节点将会将自身的Embedding（src.data）和边的Embedding(edge.data)传递到目的节点；对于每个目的节点来说，它可能会受到多个源节点传过来的消息，它会将这些消息存储在”邮箱”中。 汇聚函数（reduce function）：汇聚函数的目的是根据源节点传过来的消息更新更新目的节点Embedding，对目的节点来说，它先从邮箱（Mailbox）中汇聚源节点传递过来的消息（message），并清空邮箱（Mailbox）内消息；然后目的节点结合汇聚后的结果和原Embedding以做一次Embedding更新。 根据这两个抽象，利用DGL的编程接口实现一个自定义图模型只需要提供两个函数，即message_function和reduce_function，前者用来指导如何对源节点的数据和边数据进行选择与加工然后传递到目的节点的邮箱；后者用来指导目的节点如何利用邮箱中它的邻居传递过来的消息和自身Embedding进行融合以更新自身Embedding。最后通过在forward函数中调用update_all(message_function, reduce_function)来驱动整个计算流程的进行。DGL一个非常明显的优势是整个编程的抽象中不需要传递任何关于图结构的信息，即不需要边的邻接矩阵或者邻接表，边结构相关的信息在数据载入的过程中被底层的框架捕获并对上提供所需要的信息(主要是邻居的查询)，不需要用户在编程中进行干预。 DGL提供了详细的教程解释了如何利用消息传递的机制实现GCN，为了证明这种编程抽象是合理的，它也提供了很多已实现的模型。 这种”消息传递“的机制需要全图被预先载入到内存中，因此不适合在大规模数据集上训练，为了应对超大规模数据的问题，DGL团队采用和GraphSAGE类似的思路，将一个batch内所需要更新目的节点相关的信息一次性提取出来，构成”计算子图“，称为“NodeFLow”。计算子图被载入到内存中通过”消息传递“这样的方式计算，子图和子图之间的采样和更新都是可并行的。在最新的0.4.3版本中，DGL已经支持了单机多卡的并行，和多机cpu版本的分布式计算。 Graph-learn （下文将简写成GL) GL是从阿里内部业务场景出发抽象出来的图神经网络框架，首要的目的是为了解决内部场景应用时候要面对的超大规模图数据的问题，我们面临的图数据规模从几千万到几百亿不等，面对如此大的数据规模，在存储上我们采用分布式的方案，在计算上我们采用了和GraphSAGE类似的子图计算架构，即把一个batch内所需要更新目的节点相关的信息一次性提取出来，构成计算子图，以batch为单位更新梯度，我们把这个流程称为sampling。整个存储和计算都是可以是分布式的，在实际场景上，我们已经实现了单机多卡，多机多卡、CPU、GPU等分布式集群的训练和预测。 我们将GNN的算法流程抽象成以下步骤： 其中AGGREGATE的功能类似于PyG中的$\square$和$\phi$，而COMBINE的功能类似于PyG中的$\gamma$，而SAMPLE就是用来返回节点邻居的函数，实现上SAMPLE内部进行了非常多的系统端优化，以至于整个采样的时间相较于训练基本可以忽略不计。当然，虽然它的名字叫SAMPLE采样，但是对于像GCN和GAT这种需要全部邻居参与计算的模型，我们也提供了能够返回全部邻居的接口。对于一个batch的数据，由于每个节点的邻居数据是不定的，这时候全邻居SAMPLE的返回结果将会被封装成一个SparseTensor，并为每个源节点提供必要的邻居定位信息segment_ids以供下游算法使用。 对于每个采样出来的计算子图，我们的编程接口是这么设计的：每个图算法最核心的卷积层更新被统一成 def forward(self, self_vecs, neigh_vecs, segment_ids) 这样的形式，其中self_vecs是目的节点自身的Embedding，而neigh_vecs是源节点的Embedding，用户需要在这个函数中自定义自己的计算逻辑，即这个函数即做了AGGREGATE的工作，也做了COMBINE的工作，函数的输出即是目的节点更新后的Embedding。segment_ids是在全邻居采样返回的neigh_vecs为SparseTensor时定位每个目的节点的neigh是哪几个用到的，是这里提供一个GCN conv layer的例子。 定义完卷积层之后，对每个自定义的图算法，我们提供了LearningBasedModel这个基类来处理和训练相关的东西，用户需要重写_encoders这个函数提供对于多跳的邻居如何逐层更新Embedding的逻辑（即如何堆叠之前实现的conv layer)，最后重写build函数，以驱动整个计算流程的进行。 我们提供了详细的教程解释了如何graph-learn实现GCN，也提供了很多已实现的模型。 graph-learn架构、原理等一些其他的参考文档： https://yq.aliyun.com/articles/752628 https://yq.aliyun.com/articles/752630 https://yq.aliyun.com/articles/752637 https://yq.aliyun.com/articles/752645</summary></entry><entry><title type="html">GNN 教程(特别篇)：一文遍览GNN的代表模型</title><link href="https://archwalker.github.io/blog/2019/11/10/GNN-Go-Through-Main-Models.html" rel="alternate" type="text/html" title="GNN 教程(特别篇)：一文遍览GNN的代表模型" /><published>2019-11-10T12:00:00+08:00</published><updated>2019-11-10T12:00:00+08:00</updated><id>https://archwalker.github.io/blog/2019/11/10/GNN-Go-Through-Main-Models</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/11/10/GNN-Go-Through-Main-Models.html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;此为原创文章，未经许可，禁止转载&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GNN的各种模型在近两年来非常火热，在各个会议、期刊上新的模型层出不穷，他们有的做了理论创新，有的对前人的工作提出了改进，在这篇博文中，我想要带大家回顾GNN在近两年来的一些模型的异同，着重体现在他们的数学表达式上的差异。&lt;/p&gt;

&lt;p&gt;这篇博文主要遵循 &lt;a href=&quot;https://docs.dgl.ai/api/python/nn.pytorch.html&quot;&gt;DGL&lt;/a&gt; 框架和&lt;a href=&quot;https://pytorch-geometric.readthedocs.io/en/latest/modules/nn.html#module-torch_geometric.nn.conv.message_passing&quot;&gt;PyTorch geometric&lt;/a&gt;的梳理脉络，加上一些对公式以及背后思想的解释。这篇博文面向的读者是对图神经网络已经有了一定程度的了解的学者。&lt;/p&gt;

&lt;p&gt;文章中整理的GNN模型只是目前提出各种创新的一小部分，欢迎大家补充其他的模型。才疏学浅，如有疏漏，欢迎大家指正，可以通过github pull request 的方式，也可以留言或者发邮件给我，谢谢！&lt;/p&gt;

&lt;h2 id=&quot;卷积层的设计&quot;&gt;卷积层的设计&lt;/h2&gt;

&lt;h3 id=&quot;graphconv&quot;&gt;GraphConv&lt;/h3&gt;

&lt;p&gt;来自论文&lt;a href=&quot;https://arxiv.org/abs/1609.02907&quot;&gt;Semi-Supervised Classification with Graph Convolutional Networks (ICLR 2017)&lt;/a&gt; 一作是&lt;a href=&quot;https://arxiv.org/search/stat?searchtype=author&amp;amp;query=Kipf%2C+T+N&quot;&gt;Thomas N. Kipf&lt;/a&gt;，他当时在Amsterdam大学读博士。&lt;/p&gt;

&lt;p&gt;这篇论文是可谓是图神经网络的开山之作，在我们前序的&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/01/GNN-Triplets-GCN.html&quot;&gt;博文&lt;/a&gt;中也有解析，文中提出了一个简单且有效的图卷积算法：&lt;/p&gt;

\[h_i^{(l+1)} = \sigma(b^{(l)} + \sum_{j\in\mathcal{N}(i)}\frac{1}{c_{ij}}h_j^{(l)}W^{(l)})\]

&lt;p&gt;其中 \(\mathcal{N}(i)\) 表示节点$i$的邻居节点，$c_{ij}=\sqrt{\vert\mathcal{N}(i)\vert}\sqrt{\vert\mathcal{N}(j)\vert}$ 是正则化项，这个公式的思想很简单，节点$i$更新后的Embedding为邻居节点Embedding的加权表示。在论文的实现中，$W^{(l)}$ 以&lt;em&gt;Glorot uniform initialization&lt;/em&gt; 的方式初始化，$b^{(l)}$被初始化为0。&lt;/p&gt;

&lt;h3 id=&quot;relgraphconv&quot;&gt;RelGraphConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/abs/1703.06103&quot;&gt;Modeling Relational Data with Graph Convolutional Networks (ESWC 2018 Best Student Research Paper)&lt;/a&gt; &lt;a href=&quot;https://arxiv.org/search/stat?searchtype=author&amp;amp;query=Kipf%2C+T+N&quot;&gt;Thomas N. Kipf&lt;/a&gt;仍是并列一作。&lt;/p&gt;

&lt;p&gt;这篇论文主要解决的问题是如果图数据有不同种类的边那么该如何进行卷积，作者提出的做法是对不同种类的边进行加权求和处理：&lt;/p&gt;

\[h_i^{(l+1)} = \sigma(\sum_{r\in\mathcal{R}}
\sum_{j\in\mathcal{N}^r(i)}\frac{1}{c_{i,r}}W_r^{(l)}h_j^{(l)}+W_0^{(l)}h_i^{(l)})\]

&lt;p&gt;式中 \(\mathcal{N}^r(i)\) 表示在边类型为$r$时节点$i$的邻居节点，$c_{i,r}=\vert\mathcal{N}^r(i)\vert$是正则化项，\(W_r^{(l)} = \sum_{b=1}^B a_{rb}^{(l)}V_b^{(l)}\) 是对$W_r$的基向量分解，这样分解的原因是如果有太多种的边类型的时候，通过分解，只需要学习基向量$V_b^{(l)}$，减少欠拟合的风险。&lt;/p&gt;

&lt;h3 id=&quot;tagconv&quot;&gt;TAGConv&lt;/h3&gt;

&lt;p&gt;来自论文&lt;a href=&quot;https://arxiv.org/pdf/1710.10370.pdf&quot;&gt;Topology Adaptive Graph Convolutional Networks&lt;/a&gt; 一作是来自CMU的Jian Du(杜建)，他当时在CMU做postdoc。&lt;/p&gt;

&lt;p&gt;这篇论文是将GCN中对卷积的简化做了部分还原，细节在我们之前关于谱图卷积的理论&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/16/GNN-Spectral-Graph.html&quot;&gt;博文&lt;/a&gt;中介绍过，具体而言，GCN模型是对卷积核进行Chebyshev多项式近似后取$k=1$，这篇论文提出的方法将变量$k$保留下来作为超参：&lt;/p&gt;

\[\mathbf{X}^{\prime} = \sum_{k=0}^K \mathbf{D}^{-1/2} \mathbf{A}
\mathbf{D}^{-1/2}\mathbf{X} \mathbf{\Theta}_{k}\]

&lt;p&gt;与上面的记号稍有不同，这个公式用的是矩阵的更新形式，其中\(\mathbf{D}_{ii}=\sum_{j=0}A_{ij}\)表示节点$i$的度。&lt;/p&gt;

&lt;h3 id=&quot;gatconv&quot;&gt;GATConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1710.10903.pdf&quot;&gt;Graph Attention Network (ICLR 2018)&lt;/a&gt; 也是GNN各种模型中一个比较知名的模型，在我们之前的&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/01/GNN-Triplets-GAT.html&quot;&gt;博文&lt;/a&gt;中介绍过，一作是剑桥大学的Petar Velickovic，这篇文章是在Yoshua Bengio的指导下完成的。&lt;/p&gt;

&lt;p&gt;论文的核心思想是对邻居的重要性进行学习，利用学习到的重要性权重进行加权求和再对自身Embedding更新：&lt;/p&gt;

\[h_i^{(l+1)} = \sum_{j\in \mathcal{N}(i)} \alpha_{i,j} W^{(l)} h_j^{(l)}\]

&lt;p&gt;其中 $\alpha_{i,j}$是邻居$j$对节点$i$的相对重要性权重，是通过下式学习得到的：&lt;/p&gt;

\[\begin{align}\begin{aligned}\alpha_{ij}^{l} &amp;amp; = \mathrm{softmax_i} (e_{ij}^{l})\\e_{ij}^{l} &amp;amp; = \mathrm{LeakyReLU}\left(\vec{a}^T [W h_{i} \vert W h_{j}]\right)\end{aligned}\end{align}\]

&lt;p&gt;值得一提的是，同年的ICLR上，还有一篇关于Graph Attention的论文 &lt;a href=&quot;https://arxiv.org/abs/1803.03735&quot;&gt;Attention-based Graph Neural Network for Semi-supervised Learning&lt;/a&gt; 文章的主要思想是根据当前节点和邻居节点Embedding的cosine相似度作为Attention的加权因子，做了详细的实验和分析。&lt;/p&gt;

&lt;h3 id=&quot;pointconv&quot;&gt;PointConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/abs/1612.00593&quot;&gt;PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation&lt;/a&gt; 这是较早得将GNN应用在点云上的文章：&lt;/p&gt;

\[\mathbf{x}^{\prime}_i = \gamma_{\mathbf{\Theta}} \left( \max_{j \in
\mathcal{N}(i) \cup \{ i \}} h_{\mathbf{\Theta}} ( \mathbf{x}_j,
\mathbf{p}_j - \mathbf{p}_i) \right),\]

&lt;p&gt;其中\(\gamma_{\mathbf{\Theta}}\)和\(h_{\mathbf{\Theta}}\)都表示多层感知机层，\(\mathbf{P} \in \mathbb{R}^{N \times D}\) 表示每个点的坐标向量。&lt;/p&gt;

&lt;h3 id=&quot;edgeconv&quot;&gt;EdgeConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1801.07829&quot;&gt;Dynamic Graph CNN for Learning on Point Clouds (TOG 2019)&lt;/a&gt; 第一作者是MIT的博士Yue Wang, 这是另一一篇将图神经网络应用在点云上的文章，对于邻居节点Embedding的汇聚方法，他们是这么定义的：&lt;/p&gt;

\[x_i^{(l+1)} = \max_{j \in \mathcal{N}(i)} \mathrm{ReLU}(
\Theta \cdot (x_j^{(l)} - x_i^{(l)}) + \Phi \cdot x_i^{(l)})\]

&lt;p&gt;这里使用两个节点Embedding的差在点云的数据上有其场景意义，因为在点云的数据集上，节点的Embedding一般取的是节点的坐标向量，所以两个节点Embedding的差表示的是两个坐标向量的差，即自当前节点$i$出发，到其邻居节点$j$的向量。&lt;/p&gt;

&lt;h3 id=&quot;sageconv&quot;&gt;SAGEConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1706.02216.pdf&quot;&gt;Inductive Representation Learning on Large Graphs&lt;/a&gt; 第一作者是Stanford的博士&lt;a href=&quot;https://arxiv.org/search/cs?searchtype=author&amp;amp;query=Hamilton%2C+W+L&quot;&gt;William L. Hamilton&lt;/a&gt; 这篇文章是一篇非常经典的文章，里面提到的采样汇聚的方法也是目前将图神经网络应用到大规模数据集上的基础，我们在之前的&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/01/GNN-Triplets-GraphSAGE.html&quot;&gt;博文&lt;/a&gt;中也对其进行了详细的介绍：&lt;/p&gt;

\[\begin{align}\begin{aligned}h_{\mathcal{N}(i)}^{(l+1)} &amp;amp; = \mathrm{aggregate}
\left(\{h_{j}^{l}, \forall j \in \mathcal{N}(i) \}\right)\\h_{i}^{(l+1)} &amp;amp; = \sigma \left(W \cdot \mathrm{concat}
(h_{i}^{l}, h_{\mathcal{N}(i)}^{l+1} + b) \right)\\h_{i}^{(l+1)} &amp;amp; = \mathrm{norm}(h_{i}^{l})\end{aligned}\end{align}\]

&lt;p&gt;文章的思想如下，因为图数据每个节点的邻居个数是不一定的，带来了计算上的一些困难，所以文章通过采样的方法确保每个节点参与汇聚的邻居个数一定。在公式中$j$是采样得到的节点之一，aggregate函数对采样得到的节点集合进行汇聚，邻居集合的汇总Embedding和节点$i$本身的Embedding拼起来，再经过转换得到节点$i$更新后的Embedding。&lt;/p&gt;

&lt;h3 id=&quot;sgconv&quot;&gt;SGConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1902.07153.pdf&quot;&gt;Simplifying Graph Convolutional Networks (ICML 2019)&lt;/a&gt; 第一作者是来自Cornell的&lt;a href=&quot;https://arxiv.org/search/cs?searchtype=author&amp;amp;query=Wu%2C+F&quot;&gt;Felix Wu&lt;/a&gt; ，文章通过实验发现不需要在每个卷积层进行线性变化和激活，这些操作可以合在一起做：&lt;/p&gt;

\[H^{l+1} = (\hat{D}^{-1/2} \hat{A} \hat{D}^{-1/2})^K H^{l} \Theta^{l}\]

&lt;p&gt;这里$H^{(l+1)}$可以直接作为节点Embedding的输出结果。个人认为使用$H^{(l+1)}$表达是不准确的，这样写好像在每一层都需要这个卷积操作，反而加大了计算量，没有达到论文中“简化”的目的。&lt;/p&gt;

&lt;h3 id=&quot;appnpconv&quot;&gt;APPNPConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1810.05997.pdf&quot;&gt;Predict then Propagate: Graph Neural Networks meet Personalized PageRank&lt;/a&gt; ，文章试着将节点Embedding用一个类似于PageRank的框架更新：&lt;/p&gt;

\[\begin{align}\begin{aligned}H^{0} &amp;amp; = X\\H^{t+1} &amp;amp; = (1-\alpha)\left(\hat{D}^{-1/2}
\hat{A} \hat{D}^{-1/2} H^{t} + \alpha H^{0}\right)\end{aligned}\end{align}\]

&lt;p&gt;即每个节点的Embedding更新时会包含一部分历史的Embedding。&lt;/p&gt;

&lt;h3 id=&quot;ginconv&quot;&gt;GINConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1810.00826.pdf&quot;&gt;How Powerful are Graph Neural Networks? (ICLR 2019 oral)&lt;/a&gt;，这是一篇比较有名的文章，作者是来自MIT的xukeyulu，这篇论文算是比较早的想要从理论上分析GNN模型的表达能力的文章，在我们的&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/22/GNN-Theory-Power.html&quot;&gt;博文&lt;/a&gt;中有详细的介绍，文章想要研究GNN在图同构测试中能力，通过和Weisfeiler-Leman对比，得到一个具有和Weisfeiler-Leman想当能力的图神经网络模型：&lt;/p&gt;

\[h_i^{(l+1)} = f_\Theta \left((1 + \epsilon) h_i^{l} +
\mathrm{aggregate}\left(\left\{h_j^{l}, j\in\mathcal{N}(i)
\right\}\right)\right)\]

&lt;p&gt;这篇论文主要做了两点创新，第一，公式中的aggregate采用add而非大部分GNN模型中的mean pooling，第二，给节点自身的Embedding加了少许扰动$\epsilon$。后来的很多GNN模型都采用了该论文提出的方法作为子模块。&lt;/p&gt;

&lt;h3 id=&quot;gatedgraphconv&quot;&gt;GatedGraphConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1511.05493.pdf&quot;&gt;Gated Graph Sequence Neural Networks&lt;/a&gt;，这篇论文是一篇早期的探索图神经网络中的长依赖的论文，一作是来自多伦多大学的Yujia Li，论文利用了时序建模中的GRU模块：&lt;/p&gt;

\[\begin{align}\begin{aligned}h_{i}^{0} &amp;amp; = [ x_i \vert \mathbf{0} ]\\a_{i}^{t} &amp;amp; = \sum_{j\in\mathcal{N}(i)} W_{e_{ij}} h_{j}^{t}\\h_{i}^{t+1} &amp;amp; = \mathrm{GRU}(a_{i}^{t}, h_{i}^{t})\end{aligned}\end{align}\]

&lt;p&gt;可以看到，和之前介绍的GNN模型不同，邻居节点汇聚后Embedding $a_i^t$不再直接加到自身Embedding上(GCN)，也不再直接concat到自身Embedding上(GraphSAGE)，而是采用GRU的方式汇聚，以保持对长依赖的建模。&lt;/p&gt;

&lt;h3 id=&quot;gmmconv&quot;&gt;GMMConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;http://openaccess.thecvf.com/content_cvpr_2017/papers/Monti_Geometric_Deep_Learning_CVPR_2017_paper.pdf&quot;&gt;Geometric Deep Learning on Graphs and Manifolds using Mixture Model CNNs (CVPR 2017)&lt;/a&gt; ，论文提出了一个叫MoNet的框架，这个框架我不是特别熟悉，故暂且仅留下公式：&lt;/p&gt;

\[\begin{align}\begin{aligned}h_i^{l+1} &amp;amp; = \mathrm{aggregate}\left(\left\{\frac{1}{K}
 \sum_{k}^{K} w_k(u_{ij}), \forall j\in \mathcal{N}(i)\right\}\right)\\w_k(u) &amp;amp; = \exp\left(-\frac{1}{2}(u-\mu_k)^T \Sigma_k^{-1} (u - \mu_k)\right)\end{aligned}\end{align}\]

&lt;h3 id=&quot;chebconv&quot;&gt;ChebConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1606.09375.pdf&quot;&gt;Convolutional Neural Networks on Graphs with Fast Localized Spectral Filtering&lt;/a&gt;，这是对于谱图卷积的切比雪夫多项式近似，细节在我们之前关于谱图卷积的理论&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/16/GNN-Spectral-Graph.html&quot;&gt;博文&lt;/a&gt;中介绍过，公式为：&lt;/p&gt;

\[\begin{align}\begin{aligned}h_i^{l+1} &amp;amp;= \sum_{k=0}^{K-1} W^{k, l}z_i^{k, l}\\Z^{0, l} &amp;amp;= H^{l}\\Z^{1, l} &amp;amp;= \hat{L} \cdot H^{l}\\Z^{k, l} &amp;amp;= 2 \cdot \hat{L} \cdot Z^{k-1, l} - Z^{k-2, l}\\\hat{L} &amp;amp;= 2\left(I - \hat{D}^{-1/2} \hat{A} \hat{D}^{-1/2}\right)/\lambda_{max} - I\end{aligned}\end{align}\]

&lt;h3 id=&quot;agnnconv&quot;&gt;AGNNConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/abs/1803.03735&quot;&gt;Attention-based Graph Neural Network for Semi-Supervised Learning&lt;/a&gt;，上文中介绍过，这篇论文和Graph Attention Network 一起投稿在ICLR 2018上，这篇论文的主要思想是通过余弦相似度计算邻居节点和当前节点的加权权重，但是这篇论文最终被拒了，有可能是因为提出的方法比较简单，不过论文中做了详尽的实验分析，还是值得一看的：&lt;/p&gt;

\[H^{l+1} = P H^{l}\\
P_{ij} = \mathrm{softmax}_i ( \beta \cdot \cos(h_i^l, h_j^l))\]

&lt;h3 id=&quot;nnconv&quot;&gt;NNConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1704.01212.pdf&quot;&gt;Neural Message Passing for Quantum Chemistry&lt;/a&gt;，主要用来解决边上有权重的图神经网络改如何更新Embedding的问题，提出的架构为：&lt;/p&gt;

\[h_{i}^{l+1} = h_{i}^{l} + \mathrm{aggregate}\left(\left\{
f_\Theta (e_{ij}) \cdot h_j^{l}, j\in \mathcal{N}(i) \right\}\right)\]

&lt;p&gt;其中边上的权重$e_{ij}$被显示的建模到模型中来，作为邻居加权求和的权重，这里免去了对邻居权重的学习，直接用边的权重表征邻居的相对重要性。&lt;/p&gt;

&lt;h3 id=&quot;dnaconv&quot;&gt;DNAConv&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/abs/1904.04849&quot;&gt;Just Jump: Towards Dynamic Neighborhood Aggregation in Graph Neural Networks&lt;/a&gt; ，也着重于邻居相对重要性，与Graph Attention Network不同，这篇论文的方法更加直接得将“QKV”式的attention引入到公式中：&lt;/p&gt;

\[\mathbf{x}_v^{(t)} = h_{\mathbf{\Theta}}^{(t)} \left( \mathbf{x}_{v
\leftarrow v}^{(t)}, \left\{ \mathbf{x}_{v \leftarrow w}^{(t)} : w \in
\mathcal{N}(v) \right\} \right)\]

&lt;p&gt;其中邻居节点的Embedding由”QKV”式的attention计算：&lt;/p&gt;

\[\mathbf{x}_{v \leftarrow w}^{(t)} = \textrm{Attention} \left(
\mathbf{x}^{(t-1)}_v \, \mathbf{\Theta}_Q^{(t)}, [\mathbf{x}_w^{(1)},
\ldots, \mathbf{x}_w^{(t-1)}] \, \mathbf{\Theta}_K^{(t)}, \,
[\mathbf{x}_w^{(1)}, \ldots, \mathbf{x}_w^{(t-1)}] \,
\mathbf{\Theta}_V^{(t)} \right)\]

&lt;p&gt;其中 \(\mathbf{\Theta}_Q^{(t)}, \mathbf{\Theta}_K^{(t)}, \mathbf{\Theta}_V^{(t)}\) 分别代表query, key 和 value 的隐射矩阵。&lt;/p&gt;

&lt;h2 id=&quot;池化层的设计&quot;&gt;池化层的设计&lt;/h2&gt;

&lt;p&gt;池化层其实就是上文中提到的各个aggregator，用来将邻居的Embedding聚合起来生成一个汇总的Embedding再和节点自身的Embedding进行操作。&lt;/p&gt;

&lt;h3 id=&quot;sumpooling&quot;&gt;SumPooling&lt;/h3&gt;

&lt;p&gt;顾名思义，将邻居Embedding的每一维求和：&lt;/p&gt;

\[r^{(i)} = \sum_{k=1}^{N_i} x^{(i)}_k\]

&lt;p&gt;其中$x_k^{(i)}$表示邻居$k$ Embedding的第$i$维。&lt;/p&gt;

&lt;h3 id=&quot;avgpooling&quot;&gt;AvgPooling&lt;/h3&gt;

&lt;p&gt;将邻居Embedding的每一维求均值：&lt;/p&gt;

\[r^{(i)} = \frac{1}{N_i}\sum_{k=1}^{N_i} x^{(i)}_k\]

&lt;h3 id=&quot;maxpooling&quot;&gt;MaxPooling&lt;/h3&gt;

&lt;p&gt;将邻居Embedding的按每一维取最大值：&lt;/p&gt;

\[r^{(i)} = \max_{k=1}^{N_i}\left( x^{(i)}_k \right)\]

&lt;h3 id=&quot;sortpooling&quot;&gt;SortPooling&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://www.cse.wustl.edu/~ychen/public/DGCNN.pdf&quot;&gt;An End-to-End Deep Learning Architecture for Graph Classification&lt;/a&gt; ，这篇文章的主要思路是通过 &lt;a href=&quot;https://archwalker.github.io/blog/2019/06/22/GNN-Theory-WL.html&quot;&gt;WL算法&lt;/a&gt; 可以对节点进行着色，而节点的颜色可以定义节点之间的次序，有了节点的次序，我们就可以通过1-D卷积的方法进行卷积运算。因为过程比较抽象，具体请参考原论文。&lt;/p&gt;

&lt;h3 id=&quot;topkpooling&quot;&gt;TopKPooling&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/abs/1905.05178&quot;&gt;Graph U-Nets&lt;/a&gt; ，论文的主要思路是将节点Embedding隐射到1维空间中选择其中top k个节点再进行图卷积的计算，以下是构造top k节点小图的选取过程：&lt;/p&gt;

\[\begin{align}\begin{aligned}\mathbf{y} &amp;amp;= \frac{\mathbf{X}\mathbf{p}}{\| \mathbf{p} \|}\\\mathbf{i} &amp;amp;= \mathrm{top}_k(\mathbf{y})\\\mathbf{X}^{\prime} &amp;amp;= (\mathbf{X} \odot
\mathrm{tanh}(\mathbf{y}))_{\mathbf{i}}\\\mathbf{A}^{\prime} &amp;amp;= \mathbf{A}_{\mathbf{i},\mathbf{i}}\end{aligned}\end{align}\]

&lt;p&gt;也可以设置一个阈值 $\tilde{\alpha}$：&lt;/p&gt;

\[\begin{align}\begin{aligned}\mathbf{y} &amp;amp;= \mathrm{softmax}(\mathbf{X}\mathbf{p})\\\mathbf{i} &amp;amp;= \mathbf{y}_i &amp;gt; \tilde{\alpha}\\\mathbf{X}^{\prime} &amp;amp;= (\mathbf{X} \odot \mathbf{y})_{\mathbf{i}}\\\mathbf{A}^{\prime} &amp;amp;= \mathbf{A}_{\mathbf{i},\mathbf{i}},\end{aligned}\end{align}\]

&lt;h3 id=&quot;sagpooling&quot;&gt;SAGPooling&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/abs/1904.08082&quot;&gt;Self-Attention Graph Pooling&lt;/a&gt;，论文可以看做是TopKPooling的衍生工作，在TopKPooling中，节点的排序因子$y$按照线性映射的方式生成，而在这篇文章的工作中，作者是用GNN的方法来生成节点排序因子：&lt;/p&gt;

\[\begin{align}\begin{aligned}\mathbf{y} &amp;amp;= \textrm{GNN}(\mathbf{X}, \mathbf{A})\\\mathbf{i} &amp;amp;= \mathrm{top}_k(\mathbf{y})\\\mathbf{X}^{\prime} &amp;amp;= (\mathbf{X} \odot
\mathrm{tanh}(\mathbf{y}))_{\mathbf{i}}\\\mathbf{A}^{\prime} &amp;amp;= \mathbf{A}_{\mathbf{i},\mathbf{i}}\end{aligned}\end{align}\]

&lt;p&gt;对应的设置阈值$\tilde{\alpha}$的版本：&lt;/p&gt;

\[\begin{align}\begin{aligned}\mathbf{y} &amp;amp;= \mathrm{softmax}(\textrm{GNN}(\mathbf{X},\mathbf{A}))\\\mathbf{i} &amp;amp;= \mathbf{y}_i &amp;gt; \tilde{\alpha}\\\mathbf{X}^{\prime} &amp;amp;= (\mathbf{X} \odot \mathbf{y})_{\mathbf{i}}\\\mathbf{A}^{\prime} &amp;amp;= \mathbf{A}_{\mathbf{i},\mathbf{i}},\end{aligned}\end{align}\]

&lt;h3 id=&quot;globalattentionpooling&quot;&gt;GlobalAttentionPooling&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/abs/1511.05493.pdf&quot;&gt;Gated Graph Sequence Neural Networks&lt;/a&gt; 对图中所有节点进行pooling，主要用来做对整个图的分类等任务：&lt;/p&gt;

\[r^{(i)} = \sum_{k=1}^{N_i}\mathrm{softmax}\left(f_{gate}
  \left(x^{(i)}_k\right)\right) f_{feat}\left(x^{(i)}_k\right)\]

&lt;p&gt;其中$r^{(i)}$ 被称作“读出器”，是对于整个图的Embedding描述。&lt;/p&gt;

&lt;h3 id=&quot;set2set&quot;&gt;Set2Set&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1511.06391.pdf&quot;&gt;Order Matters: Sequence to sequence for sets&lt;/a&gt;，和Graph attention 有点类似，其中加权求和的权重是通过LSTM建模得到的：&lt;/p&gt;

\[\begin{align}\begin{aligned}q_t &amp;amp;= \mathrm{LSTM} (q^*_{t-1})\\\alpha_{i,t} &amp;amp;= \mathrm{softmax}(x_i \cdot q_t)\\r_t &amp;amp;= \sum_{i=1}^N \alpha_{i,t} x_i\\q^*_t &amp;amp;= q_t \Vert r_t\end{aligned}\end{align}\]

&lt;h3 id=&quot;settrasformerencoder--settrasformerdecoder&quot;&gt;SetTrasformerEncoder &amp;amp; SetTrasformerDecoder&lt;/h3&gt;

&lt;p&gt;来自论文 &lt;a href=&quot;https://arxiv.org/pdf/1810.00825.pdf&quot;&gt;Set Transformer: A Framework for Attention-based Permutation-Invariant Neural Networks&lt;/a&gt;，是Set2Set的衍生模型，公式也有点复杂，可以查下原论文。&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">引言 此为原创文章，未经许可，禁止转载 GNN的各种模型在近两年来非常火热，在各个会议、期刊上新的模型层出不穷，他们有的做了理论创新，有的对前人的工作提出了改进，在这篇博文中，我想要带大家回顾GNN在近两年来的一些模型的异同，着重体现在他们的数学表达式上的差异。 这篇博文主要遵循 DGL 框架和PyTorch geometric的梳理脉络，加上一些对公式以及背后思想的解释。这篇博文面向的读者是对图神经网络已经有了一定程度的了解的学者。 文章中整理的GNN模型只是目前提出各种创新的一小部分，欢迎大家补充其他的模型。才疏学浅，如有疏漏，欢迎大家指正，可以通过github pull request 的方式，也可以留言或者发邮件给我，谢谢！ 卷积层的设计 GraphConv 来自论文Semi-Supervised Classification with Graph Convolutional Networks (ICLR 2017) 一作是Thomas N. Kipf，他当时在Amsterdam大学读博士。 这篇论文是可谓是图神经网络的开山之作，在我们前序的博文中也有解析，文中提出了一个简单且有效的图卷积算法： \[h_i^{(l+1)} = \sigma(b^{(l)} + \sum_{j\in\mathcal{N}(i)}\frac{1}{c_{ij}}h_j^{(l)}W^{(l)})\] 其中 \(\mathcal{N}(i)\) 表示节点$i$的邻居节点，$c_{ij}=\sqrt{\vert\mathcal{N}(i)\vert}\sqrt{\vert\mathcal{N}(j)\vert}$ 是正则化项，这个公式的思想很简单，节点$i$更新后的Embedding为邻居节点Embedding的加权表示。在论文的实现中，$W^{(l)}$ 以Glorot uniform initialization 的方式初始化，$b^{(l)}$被初始化为0。 RelGraphConv 来自论文 Modeling Relational Data with Graph Convolutional Networks (ESWC 2018 Best Student Research Paper) Thomas N. Kipf仍是并列一作。 这篇论文主要解决的问题是如果图数据有不同种类的边那么该如何进行卷积，作者提出的做法是对不同种类的边进行加权求和处理： \[h_i^{(l+1)} = \sigma(\sum_{r\in\mathcal{R}} \sum_{j\in\mathcal{N}^r(i)}\frac{1}{c_{i,r}}W_r^{(l)}h_j^{(l)}+W_0^{(l)}h_i^{(l)})\] 式中 \(\mathcal{N}^r(i)\) 表示在边类型为$r$时节点$i$的邻居节点，$c_{i,r}=\vert\mathcal{N}^r(i)\vert$是正则化项，\(W_r^{(l)} = \sum_{b=1}^B a_{rb}^{(l)}V_b^{(l)}\) 是对$W_r$的基向量分解，这样分解的原因是如果有太多种的边类型的时候，通过分解，只需要学习基向量$V_b^{(l)}$，减少欠拟合的风险。 TAGConv 来自论文Topology Adaptive Graph Convolutional Networks 一作是来自CMU的Jian Du(杜建)，他当时在CMU做postdoc。 这篇论文是将GCN中对卷积的简化做了部分还原，细节在我们之前关于谱图卷积的理论博文中介绍过，具体而言，GCN模型是对卷积核进行Chebyshev多项式近似后取$k=1$，这篇论文提出的方法将变量$k$保留下来作为超参： \[\mathbf{X}^{\prime} = \sum_{k=0}^K \mathbf{D}^{-1/2} \mathbf{A} \mathbf{D}^{-1/2}\mathbf{X} \mathbf{\Theta}_{k}\] 与上面的记号稍有不同，这个公式用的是矩阵的更新形式，其中\(\mathbf{D}_{ii}=\sum_{j=0}A_{ij}\)表示节点$i$的度。 GATConv 来自论文 Graph Attention Network (ICLR 2018) 也是GNN各种模型中一个比较知名的模型，在我们之前的博文中介绍过，一作是剑桥大学的Petar Velickovic，这篇文章是在Yoshua Bengio的指导下完成的。 论文的核心思想是对邻居的重要性进行学习，利用学习到的重要性权重进行加权求和再对自身Embedding更新： \[h_i^{(l+1)} = \sum_{j\in \mathcal{N}(i)} \alpha_{i,j} W^{(l)} h_j^{(l)}\] 其中 $\alpha_{i,j}$是邻居$j$对节点$i$的相对重要性权重，是通过下式学习得到的： \[\begin{align}\begin{aligned}\alpha_{ij}^{l} &amp;amp; = \mathrm{softmax_i} (e_{ij}^{l})\\e_{ij}^{l} &amp;amp; = \mathrm{LeakyReLU}\left(\vec{a}^T [W h_{i} \vert W h_{j}]\right)\end{aligned}\end{align}\] 值得一提的是，同年的ICLR上，还有一篇关于Graph Attention的论文 Attention-based Graph Neural Network for Semi-supervised Learning 文章的主要思想是根据当前节点和邻居节点Embedding的cosine相似度作为Attention的加权因子，做了详细的实验和分析。 PointConv 来自论文 PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation 这是较早得将GNN应用在点云上的文章： \[\mathbf{x}^{\prime}_i = \gamma_{\mathbf{\Theta}} \left( \max_{j \in \mathcal{N}(i) \cup \{ i \}} h_{\mathbf{\Theta}} ( \mathbf{x}_j, \mathbf{p}_j - \mathbf{p}_i) \right),\] 其中\(\gamma_{\mathbf{\Theta}}\)和\(h_{\mathbf{\Theta}}\)都表示多层感知机层，\(\mathbf{P} \in \mathbb{R}^{N \times D}\) 表示每个点的坐标向量。 EdgeConv 来自论文 Dynamic Graph CNN for Learning on Point Clouds (TOG 2019) 第一作者是MIT的博士Yue Wang, 这是另一一篇将图神经网络应用在点云上的文章，对于邻居节点Embedding的汇聚方法，他们是这么定义的： \[x_i^{(l+1)} = \max_{j \in \mathcal{N}(i)} \mathrm{ReLU}( \Theta \cdot (x_j^{(l)} - x_i^{(l)}) + \Phi \cdot x_i^{(l)})\] 这里使用两个节点Embedding的差在点云的数据上有其场景意义，因为在点云的数据集上，节点的Embedding一般取的是节点的坐标向量，所以两个节点Embedding的差表示的是两个坐标向量的差，即自当前节点$i$出发，到其邻居节点$j$的向量。 SAGEConv 来自论文 Inductive Representation Learning on Large Graphs 第一作者是Stanford的博士William L. Hamilton 这篇文章是一篇非常经典的文章，里面提到的采样汇聚的方法也是目前将图神经网络应用到大规模数据集上的基础，我们在之前的博文中也对其进行了详细的介绍： \[\begin{align}\begin{aligned}h_{\mathcal{N}(i)}^{(l+1)} &amp;amp; = \mathrm{aggregate} \left(\{h_{j}^{l}, \forall j \in \mathcal{N}(i) \}\right)\\h_{i}^{(l+1)} &amp;amp; = \sigma \left(W \cdot \mathrm{concat} (h_{i}^{l}, h_{\mathcal{N}(i)}^{l+1} + b) \right)\\h_{i}^{(l+1)} &amp;amp; = \mathrm{norm}(h_{i}^{l})\end{aligned}\end{align}\] 文章的思想如下，因为图数据每个节点的邻居个数是不一定的，带来了计算上的一些困难，所以文章通过采样的方法确保每个节点参与汇聚的邻居个数一定。在公式中$j$是采样得到的节点之一，aggregate函数对采样得到的节点集合进行汇聚，邻居集合的汇总Embedding和节点$i$本身的Embedding拼起来，再经过转换得到节点$i$更新后的Embedding。 SGConv 来自论文 Simplifying Graph Convolutional Networks (ICML 2019) 第一作者是来自Cornell的Felix Wu ，文章通过实验发现不需要在每个卷积层进行线性变化和激活，这些操作可以合在一起做： \[H^{l+1} = (\hat{D}^{-1/2} \hat{A} \hat{D}^{-1/2})^K H^{l} \Theta^{l}\] 这里$H^{(l+1)}$可以直接作为节点Embedding的输出结果。个人认为使用$H^{(l+1)}$表达是不准确的，这样写好像在每一层都需要这个卷积操作，反而加大了计算量，没有达到论文中“简化”的目的。 APPNPConv 来自论文 Predict then Propagate: Graph Neural Networks meet Personalized PageRank ，文章试着将节点Embedding用一个类似于PageRank的框架更新： \[\begin{align}\begin{aligned}H^{0} &amp;amp; = X\\H^{t+1} &amp;amp; = (1-\alpha)\left(\hat{D}^{-1/2} \hat{A} \hat{D}^{-1/2} H^{t} + \alpha H^{0}\right)\end{aligned}\end{align}\] 即每个节点的Embedding更新时会包含一部分历史的Embedding。 GINConv 来自论文 How Powerful are Graph Neural Networks? (ICLR 2019 oral)，这是一篇比较有名的文章，作者是来自MIT的xukeyulu，这篇论文算是比较早的想要从理论上分析GNN模型的表达能力的文章，在我们的博文中有详细的介绍，文章想要研究GNN在图同构测试中能力，通过和Weisfeiler-Leman对比，得到一个具有和Weisfeiler-Leman想当能力的图神经网络模型： \[h_i^{(l+1)} = f_\Theta \left((1 + \epsilon) h_i^{l} + \mathrm{aggregate}\left(\left\{h_j^{l}, j\in\mathcal{N}(i) \right\}\right)\right)\] 这篇论文主要做了两点创新，第一，公式中的aggregate采用add而非大部分GNN模型中的mean pooling，第二，给节点自身的Embedding加了少许扰动$\epsilon$。后来的很多GNN模型都采用了该论文提出的方法作为子模块。 GatedGraphConv 来自论文 Gated Graph Sequence Neural Networks，这篇论文是一篇早期的探索图神经网络中的长依赖的论文，一作是来自多伦多大学的Yujia Li，论文利用了时序建模中的GRU模块： \[\begin{align}\begin{aligned}h_{i}^{0} &amp;amp; = [ x_i \vert \mathbf{0} ]\\a_{i}^{t} &amp;amp; = \sum_{j\in\mathcal{N}(i)} W_{e_{ij}} h_{j}^{t}\\h_{i}^{t+1} &amp;amp; = \mathrm{GRU}(a_{i}^{t}, h_{i}^{t})\end{aligned}\end{align}\] 可以看到，和之前介绍的GNN模型不同，邻居节点汇聚后Embedding $a_i^t$不再直接加到自身Embedding上(GCN)，也不再直接concat到自身Embedding上(GraphSAGE)，而是采用GRU的方式汇聚，以保持对长依赖的建模。 GMMConv 来自论文 Geometric Deep Learning on Graphs and Manifolds using Mixture Model CNNs (CVPR 2017) ，论文提出了一个叫MoNet的框架，这个框架我不是特别熟悉，故暂且仅留下公式： \[\begin{align}\begin{aligned}h_i^{l+1} &amp;amp; = \mathrm{aggregate}\left(\left\{\frac{1}{K} \sum_{k}^{K} w_k(u_{ij}), \forall j\in \mathcal{N}(i)\right\}\right)\\w_k(u) &amp;amp; = \exp\left(-\frac{1}{2}(u-\mu_k)^T \Sigma_k^{-1} (u - \mu_k)\right)\end{aligned}\end{align}\] ChebConv 来自论文 Convolutional Neural Networks on Graphs with Fast Localized Spectral Filtering，这是对于谱图卷积的切比雪夫多项式近似，细节在我们之前关于谱图卷积的理论博文中介绍过，公式为： \[\begin{align}\begin{aligned}h_i^{l+1} &amp;amp;= \sum_{k=0}^{K-1} W^{k, l}z_i^{k, l}\\Z^{0, l} &amp;amp;= H^{l}\\Z^{1, l} &amp;amp;= \hat{L} \cdot H^{l}\\Z^{k, l} &amp;amp;= 2 \cdot \hat{L} \cdot Z^{k-1, l} - Z^{k-2, l}\\\hat{L} &amp;amp;= 2\left(I - \hat{D}^{-1/2} \hat{A} \hat{D}^{-1/2}\right)/\lambda_{max} - I\end{aligned}\end{align}\] AGNNConv 来自论文 Attention-based Graph Neural Network for Semi-Supervised Learning，上文中介绍过，这篇论文和Graph Attention Network 一起投稿在ICLR 2018上，这篇论文的主要思想是通过余弦相似度计算邻居节点和当前节点的加权权重，但是这篇论文最终被拒了，有可能是因为提出的方法比较简单，不过论文中做了详尽的实验分析，还是值得一看的： \[H^{l+1} = P H^{l}\\ P_{ij} = \mathrm{softmax}_i ( \beta \cdot \cos(h_i^l, h_j^l))\] NNConv 来自论文 Neural Message Passing for Quantum Chemistry，主要用来解决边上有权重的图神经网络改如何更新Embedding的问题，提出的架构为： \[h_{i}^{l+1} = h_{i}^{l} + \mathrm{aggregate}\left(\left\{ f_\Theta (e_{ij}) \cdot h_j^{l}, j\in \mathcal{N}(i) \right\}\right)\] 其中边上的权重$e_{ij}$被显示的建模到模型中来，作为邻居加权求和的权重，这里免去了对邻居权重的学习，直接用边的权重表征邻居的相对重要性。 DNAConv 来自论文 Just Jump: Towards Dynamic Neighborhood Aggregation in Graph Neural Networks ，也着重于邻居相对重要性，与Graph Attention Network不同，这篇论文的方法更加直接得将“QKV”式的attention引入到公式中： \[\mathbf{x}_v^{(t)} = h_{\mathbf{\Theta}}^{(t)} \left( \mathbf{x}_{v \leftarrow v}^{(t)}, \left\{ \mathbf{x}_{v \leftarrow w}^{(t)} : w \in \mathcal{N}(v) \right\} \right)\] 其中邻居节点的Embedding由”QKV”式的attention计算： \[\mathbf{x}_{v \leftarrow w}^{(t)} = \textrm{Attention} \left( \mathbf{x}^{(t-1)}_v \, \mathbf{\Theta}_Q^{(t)}, [\mathbf{x}_w^{(1)}, \ldots, \mathbf{x}_w^{(t-1)}] \, \mathbf{\Theta}_K^{(t)}, \, [\mathbf{x}_w^{(1)}, \ldots, \mathbf{x}_w^{(t-1)}] \, \mathbf{\Theta}_V^{(t)} \right)\] 其中 \(\mathbf{\Theta}_Q^{(t)}, \mathbf{\Theta}_K^{(t)}, \mathbf{\Theta}_V^{(t)}\) 分别代表query, key 和 value 的隐射矩阵。 池化层的设计 池化层其实就是上文中提到的各个aggregator，用来将邻居的Embedding聚合起来生成一个汇总的Embedding再和节点自身的Embedding进行操作。 SumPooling 顾名思义，将邻居Embedding的每一维求和： \[r^{(i)} = \sum_{k=1}^{N_i} x^{(i)}_k\] 其中$x_k^{(i)}$表示邻居$k$ Embedding的第$i$维。 AvgPooling 将邻居Embedding的每一维求均值： \[r^{(i)} = \frac{1}{N_i}\sum_{k=1}^{N_i} x^{(i)}_k\] MaxPooling 将邻居Embedding的按每一维取最大值： \[r^{(i)} = \max_{k=1}^{N_i}\left( x^{(i)}_k \right)\] SortPooling 来自论文 An End-to-End Deep Learning Architecture for Graph Classification ，这篇文章的主要思路是通过 WL算法 可以对节点进行着色，而节点的颜色可以定义节点之间的次序，有了节点的次序，我们就可以通过1-D卷积的方法进行卷积运算。因为过程比较抽象，具体请参考原论文。 TopKPooling 来自论文 Graph U-Nets ，论文的主要思路是将节点Embedding隐射到1维空间中选择其中top k个节点再进行图卷积的计算，以下是构造top k节点小图的选取过程： \[\begin{align}\begin{aligned}\mathbf{y} &amp;amp;= \frac{\mathbf{X}\mathbf{p}}{\| \mathbf{p} \|}\\\mathbf{i} &amp;amp;= \mathrm{top}_k(\mathbf{y})\\\mathbf{X}^{\prime} &amp;amp;= (\mathbf{X} \odot \mathrm{tanh}(\mathbf{y}))_{\mathbf{i}}\\\mathbf{A}^{\prime} &amp;amp;= \mathbf{A}_{\mathbf{i},\mathbf{i}}\end{aligned}\end{align}\] 也可以设置一个阈值 $\tilde{\alpha}$： \[\begin{align}\begin{aligned}\mathbf{y} &amp;amp;= \mathrm{softmax}(\mathbf{X}\mathbf{p})\\\mathbf{i} &amp;amp;= \mathbf{y}_i &amp;gt; \tilde{\alpha}\\\mathbf{X}^{\prime} &amp;amp;= (\mathbf{X} \odot \mathbf{y})_{\mathbf{i}}\\\mathbf{A}^{\prime} &amp;amp;= \mathbf{A}_{\mathbf{i},\mathbf{i}},\end{aligned}\end{align}\] SAGPooling 来自论文 Self-Attention Graph Pooling，论文可以看做是TopKPooling的衍生工作，在TopKPooling中，节点的排序因子$y$按照线性映射的方式生成，而在这篇文章的工作中，作者是用GNN的方法来生成节点排序因子： \[\begin{align}\begin{aligned}\mathbf{y} &amp;amp;= \textrm{GNN}(\mathbf{X}, \mathbf{A})\\\mathbf{i} &amp;amp;= \mathrm{top}_k(\mathbf{y})\\\mathbf{X}^{\prime} &amp;amp;= (\mathbf{X} \odot \mathrm{tanh}(\mathbf{y}))_{\mathbf{i}}\\\mathbf{A}^{\prime} &amp;amp;= \mathbf{A}_{\mathbf{i},\mathbf{i}}\end{aligned}\end{align}\] 对应的设置阈值$\tilde{\alpha}$的版本： \[\begin{align}\begin{aligned}\mathbf{y} &amp;amp;= \mathrm{softmax}(\textrm{GNN}(\mathbf{X},\mathbf{A}))\\\mathbf{i} &amp;amp;= \mathbf{y}_i &amp;gt; \tilde{\alpha}\\\mathbf{X}^{\prime} &amp;amp;= (\mathbf{X} \odot \mathbf{y})_{\mathbf{i}}\\\mathbf{A}^{\prime} &amp;amp;= \mathbf{A}_{\mathbf{i},\mathbf{i}},\end{aligned}\end{align}\] GlobalAttentionPooling 来自论文 Gated Graph Sequence Neural Networks 对图中所有节点进行pooling，主要用来做对整个图的分类等任务： \[r^{(i)} = \sum_{k=1}^{N_i}\mathrm{softmax}\left(f_{gate} \left(x^{(i)}_k\right)\right) f_{feat}\left(x^{(i)}_k\right)\] 其中$r^{(i)}$ 被称作“读出器”，是对于整个图的Embedding描述。 Set2Set 来自论文 Order Matters: Sequence to sequence for sets，和Graph attention 有点类似，其中加权求和的权重是通过LSTM建模得到的： \[\begin{align}\begin{aligned}q_t &amp;amp;= \mathrm{LSTM} (q^*_{t-1})\\\alpha_{i,t} &amp;amp;= \mathrm{softmax}(x_i \cdot q_t)\\r_t &amp;amp;= \sum_{i=1}^N \alpha_{i,t} x_i\\q^*_t &amp;amp;= q_t \Vert r_t\end{aligned}\end{align}\] SetTrasformerEncoder &amp;amp; SetTrasformerDecoder 来自论文 Set Transformer: A Framework for Attention-based Permutation-Invariant Neural Networks，是Set2Set的衍生模型，公式也有点复杂，可以查下原论文。</summary></entry><entry><title type="html">GNN 教程：图上的预训练任务下篇</title><link href="https://archwalker.github.io/blog/2019/08/08/GNN-Pretraining-1.html" rel="alternate" type="text/html" title="GNN 教程：图上的预训练任务下篇" /><published>2019-08-08T12:00:00+08:00</published><updated>2019-08-08T12:00:00+08:00</updated><id>https://archwalker.github.io/blog/2019/08/08/GNN-Pretraining-1</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/08/08/GNN-Pretraining-1.html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;此为原创文章，未经许可，禁止转载&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;前一篇博文&lt;a href=&quot;https://archwalker.github.io/blog/2019/07/18/GNN-Pretain-0.html&quot;&gt;GNN 教程：图上的预训练任务上篇&lt;/a&gt;已经向大家介绍了一种图上预训练的方式，通过设计边重建、Centrality Score Ranking、保留图簇信息三种图预训练任务来生成节点embedding，以从局部到全局捕获节点的图结构信息；然后，将预训练模型生成的节点Embedding在特定的任务中做微调，最终应用于该特定任务中。&lt;/p&gt;

&lt;p&gt;本博文将向大家介绍来自论文 &lt;a href=&quot;https://arxiv.org/abs/1905.12265&quot;&gt;Pre-training Graph Neural Networks&lt;/a&gt; 的另一种预训练方式，该论文重点讨论下面三个问题：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;如何生成节点 embedding，以捕获节点和它邻居的在结构的相似性(相邻节点的Embedding在投影空间中相近)；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;如何生成可组合(composable)的节点 embedding，这些Embedding通过pooling的方式能够刻画整个图的Embedding；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;如何生成领域知识(domain-knowledge)相关的节点 embedding&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;动机&quot;&gt;动机&lt;/h2&gt;

&lt;p&gt;虽然 GNN 模型及其变体在图结构数据的学习方面取得了成功，但是 GNNs 存在以下两个主要挑战：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;目前应用于特定领域的 GNNs 算法，主要属于有监督学习算法，该算法需要采用大量带标注的图数据进行训练，才能获得更高准确度的模型。然而，由于带标签数据的极度稀缺以及标注成本过高等问题，导致 GNNs 训练模型容易出现过拟合现象；&lt;/li&gt;
  &lt;li&gt;在 GNNs 中，需要预测测试集中的一些图，这些图具有不同于训练集中所包含图的图结构，导致容易陷入 out-of-distribution 预测问题。举个例子，预测新合成的，与训练集分子结构不同的分子的化学属性，或者来自新物种的蛋白质的功能，其具有与先前研究的物种不同的PPI网络结构。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;预训练策略&quot;&gt;预训练策略&lt;/h2&gt;

&lt;p&gt;和&lt;a href=&quot;https://archwalker.github.io/blog/2019/07/18/GNN-Pretain-0.html&quot;&gt;上篇论文&lt;/a&gt;不同的是，这篇论文设计预训练任务将会涉及到node-level和graph-level两个层次上。见下左图(a)：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww2.sinaimg.cn/large/006tNc79ly1g5skvg3ygrj31jc0gudn8.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;为了学习领域知识(在论文中是分子的类别)，在节点的层次上，作者设计了masking任务，在图的层次上，作者设计了图分类的任务；为了学习图结构信息，在节点的层次上，作者设计了上下文预测的任务，在图的层次上，作者设计了图分类的任务。&lt;/p&gt;

&lt;p&gt;(b)图解释了作者结合节点层次和图层次设计预训练任务的原因，如果仅考虑节点层次，那么不同类型的节点(图中形状不一样的节点)的Embedding会相似，但是由这些节点组合而成的图Embedding不可分，相反如果只考虑图层次，虽然图Embedding可分了，但是学习到的节点Embedding却没有语义上的一致性(相同类型的节点在Embedding空间中不相近)，因此作者提出了要结合节点层次和图层次共同设计与训练任务。在这篇论文中，作者设计了3个预训练任务，和前一篇博文中我们介绍的预训练任务稍有不同。&lt;/p&gt;

&lt;h2 id=&quot;预训练任务&quot;&gt;预训练任务&lt;/h2&gt;

&lt;h3 id=&quot;任务1-上下文预测学习可以捕获局部图结构的节点embedding&quot;&gt;任务1 上下文预测：学习可以捕获局部图结构的节点Embedding&lt;/h3&gt;

&lt;p&gt;大多数现有的无监督节点表示学习方法被设计用于节点分类，并且要求附近节点具有类似的 embeddings。这不适用于整个图的表示学习，其中捕获局部邻域的结构相似性更重要。&lt;/p&gt;

&lt;p&gt;NLP 领域中提出了一种分布式假设(distributional hypothesis)，分布式假设的意思是如果词的含义相近，那么它们的上下文也应该相似，Word2vec就是成功利用这一假设的词向量表征方法。举个例子，”姚明” 和 “易建联” 都是NBA篮球运动员，所以他们可能会出现在介绍中国NBA球星的上下文中。&lt;/p&gt;

&lt;p&gt;受这种分布式假设的启发，作者将分布式假设应用于复杂图领域中，以更好的训练 GNNs 预训练模型。这里需要解决以下两个问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如何在图领域中定义 “词” 和 “上下文”；&lt;/li&gt;
  &lt;li&gt;如何在预测问题中表示上下文。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面将对上述问题的解决方法进行介绍。&lt;/p&gt;

&lt;h4 id=&quot;图领域-词-和-上下文的定义&quot;&gt;图领域 “词” 和 “上下文”的定义&lt;/h4&gt;

&lt;p&gt;如何将”词”的概念类比到图领域呢？由于GNN中每个节点的Embedding都是由其邻居节点更新的，因此作者采用节点 $v$ 的 $K$-hop 子图结构对$v$进行编码，最终获得的节点 embeddings $h_v^{(K)}$ 作为节点$v$的”词”表示。该子图结构（Substructure）如下图所示。如果左图中的红色节点是$v$，那么它的整个$K$-hop领域就是这个节点的”词”表示，根据GNN的聚合算法，$K$-hop领域内的所有邻居都将自身的Embedding聚合到$v$上来，形成$v$的”词向量”$h_v^{(k)}。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww1.sinaimg.cn/large/006tNc79ly1g5vpyhsb1rj30ss0ca416.jpg&quot; alt=&quot;Screen Shot 2019-08-11 at 2.24.29 PM&quot; /&gt;&lt;/p&gt;

&lt;p&gt;类似地，类比到NLP上，上下文指词前后的内容，那么图中的上下文（Context）可以被定义为围绕 K-hop 子图的图结构。例如：左图中 $r_1$-hop 和 $r_2$-hop 间的节点形成了节点中间红色节点的上下文。这里定义 $r_1 &amp;lt; K$ ，这样一些节点在词和上下文之间可以共享。&lt;/p&gt;

&lt;h4 id=&quot;使用另一个-gnn-将上下文编码到一个固定向量中&quot;&gt;使用另一个 GNN 将上下文编码到一个固定向量中&lt;/h4&gt;

&lt;p&gt;定义完图上的”词”和”上下文”的概念之后，我们还需要将”词”和”上下文”表征出来，和NLP的任务不同，图中的”词”和”上下文”都是有结构的数据，因为他们都是子图的结构。在论文中，作者将这两个Embedding分别使用两个GNN进行编码，对于节点$v$，只需要使用传统GNN的聚合公式即可得到节点$v$的词向量表示(见右图上边部分)，再使用另一个GNN(记做GNN‘，见右图下边部分)对节点$v$的”上下文”结构中涉及到的节点($r_1$-hop和$r_2$-hop之间的节点)的Embedding做一个pooling来表征节点$v$的上下文信息。我们将节点$v$的”词”表征记做为$h_v^{(K)}$，而”上下文”表征记做为$c_v^{G^\prime}$，其中$G^\prime$表示用来对上下文进行编码的另一个GNN。&lt;/p&gt;

&lt;h4 id=&quot;通过负采样进行训练&quot;&gt;通过负采样进行训练&lt;/h4&gt;

&lt;p&gt;得到节点的”词”表征和”上下文”表征之后，我们可以通过一个二分类任务进行学习&lt;/p&gt;

\[\sigma\left(h_{v}^{(K) \top} c_{v^{\prime}}^{G^{\prime}}\right) \approx \mathbf{1}\left\{v \text { and } v^{\prime} \text { are the same node in } G\right\}\]

&lt;p&gt;其中\(\mathbf{1}(\cdot)\) 是指示符函数，即：我们希望对于相同的节点，它的自身表征(“词”表征)和它的邻域表征(“上下文”表征)尽量相似，当然，对于不同节点的自身表征和邻域表征，我们希望它们尽量不相似，论文中通过负采样技术构建了这种负样本，在此不加赘述。&lt;/p&gt;

&lt;p&gt;做一个小结，通过设计上下文预测的预训练任务，我们希望GNN能够将具有相似邻域的节点映射到相近的Embedding空间中，即使得学习到的节点Embedding能够捕获其邻域信息。&lt;/p&gt;

&lt;h3 id=&quot;任务2-masking训练节点-embeddings-以获得领域相关知识&quot;&gt;任务2 masking：训练节点 embeddings 以获得领域相关知识&lt;/h3&gt;

&lt;p&gt;任务2的设计和前面介绍的论文很相似，不同于前一篇博文中介绍的是，这篇论文的是对节点/边的某些属性进行masking，然后利用学习到的节点Embedding进行预测。下图展示了masking节点的任务，在论文所实验的分子结构的场景中，被masking的信息是节点的分子类型，然后通过节点Embedding来预测这些分子类型（碳、氮、氧，硫等）。同理，对于masking边的预训练任务，我们可以通过边的两个端点的Embedding来预测边的类型（单键、双键等）&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww1.sinaimg.cn/large/006tNc79ly1g5wp5sdva0j30d20cwt98.jpg&quot; alt=&quot;Screen Shot 2019-08-11 at 3.26.21 PM&quot; /&gt;&lt;/p&gt;

&lt;p&gt;做一个小结，设计masking预训练任务的目的是来让模型学习有意义节点Embedding，以反映出特定领域知识。如上图所示，通过对分子结构图进行掩码，GNNs 能够学习一些化学规则。&lt;/p&gt;

&lt;h3 id=&quot;任务3-图预测任务训练节点embedding以组合成合理的全图embedding&quot;&gt;任务3 图预测任务：训练节点Embedding以组合成合理的全图Embedding&lt;/h3&gt;

&lt;p&gt;上文中我们介绍了仅设计节点预训练任务的弊端，这节作者将重点放在如何设计图级别预训练的方法来学习可组合的节点embeddings：&lt;/p&gt;

&lt;p&gt;1) 通过对特定领域的图标签进行预测，比如这个图是什么或者属于那种类型，这其实是有监督的方式;
2) 通过对图级别结构进行自监督预测，例如图编辑距离或图结构相似性。&lt;/p&gt;

&lt;p&gt;在论文中，作者主要关注第一种方式，其实就是一种有监督的训练方式来调整节点Embedding。&lt;/p&gt;

&lt;p&gt;做一个小结，本节中介绍了利用预训练 GNNs 来生成可组合的节点 embeddings 。通过组合这些Embedding能够获得整个图的有效表示，以确保图 embeddings 并且可以适用到各种下游任务中。&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">引言 此为原创文章，未经许可，禁止转载 前一篇博文GNN 教程：图上的预训练任务上篇已经向大家介绍了一种图上预训练的方式，通过设计边重建、Centrality Score Ranking、保留图簇信息三种图预训练任务来生成节点embedding，以从局部到全局捕获节点的图结构信息；然后，将预训练模型生成的节点Embedding在特定的任务中做微调，最终应用于该特定任务中。 本博文将向大家介绍来自论文 Pre-training Graph Neural Networks 的另一种预训练方式，该论文重点讨论下面三个问题： 如何生成节点 embedding，以捕获节点和它邻居的在结构的相似性(相邻节点的Embedding在投影空间中相近)； 如何生成可组合(composable)的节点 embedding，这些Embedding通过pooling的方式能够刻画整个图的Embedding； 如何生成领域知识(domain-knowledge)相关的节点 embedding 动机 虽然 GNN 模型及其变体在图结构数据的学习方面取得了成功，但是 GNNs 存在以下两个主要挑战： 目前应用于特定领域的 GNNs 算法，主要属于有监督学习算法，该算法需要采用大量带标注的图数据进行训练，才能获得更高准确度的模型。然而，由于带标签数据的极度稀缺以及标注成本过高等问题，导致 GNNs 训练模型容易出现过拟合现象； 在 GNNs 中，需要预测测试集中的一些图，这些图具有不同于训练集中所包含图的图结构，导致容易陷入 out-of-distribution 预测问题。举个例子，预测新合成的，与训练集分子结构不同的分子的化学属性，或者来自新物种的蛋白质的功能，其具有与先前研究的物种不同的PPI网络结构。 预训练策略 和上篇论文不同的是，这篇论文设计预训练任务将会涉及到node-level和graph-level两个层次上。见下左图(a)： 为了学习领域知识(在论文中是分子的类别)，在节点的层次上，作者设计了masking任务，在图的层次上，作者设计了图分类的任务；为了学习图结构信息，在节点的层次上，作者设计了上下文预测的任务，在图的层次上，作者设计了图分类的任务。 (b)图解释了作者结合节点层次和图层次设计预训练任务的原因，如果仅考虑节点层次，那么不同类型的节点(图中形状不一样的节点)的Embedding会相似，但是由这些节点组合而成的图Embedding不可分，相反如果只考虑图层次，虽然图Embedding可分了，但是学习到的节点Embedding却没有语义上的一致性(相同类型的节点在Embedding空间中不相近)，因此作者提出了要结合节点层次和图层次共同设计与训练任务。在这篇论文中，作者设计了3个预训练任务，和前一篇博文中我们介绍的预训练任务稍有不同。 预训练任务 任务1 上下文预测：学习可以捕获局部图结构的节点Embedding 大多数现有的无监督节点表示学习方法被设计用于节点分类，并且要求附近节点具有类似的 embeddings。这不适用于整个图的表示学习，其中捕获局部邻域的结构相似性更重要。 NLP 领域中提出了一种分布式假设(distributional hypothesis)，分布式假设的意思是如果词的含义相近，那么它们的上下文也应该相似，Word2vec就是成功利用这一假设的词向量表征方法。举个例子，”姚明” 和 “易建联” 都是NBA篮球运动员，所以他们可能会出现在介绍中国NBA球星的上下文中。 受这种分布式假设的启发，作者将分布式假设应用于复杂图领域中，以更好的训练 GNNs 预训练模型。这里需要解决以下两个问题： 如何在图领域中定义 “词” 和 “上下文”； 如何在预测问题中表示上下文。 下面将对上述问题的解决方法进行介绍。 图领域 “词” 和 “上下文”的定义 如何将”词”的概念类比到图领域呢？由于GNN中每个节点的Embedding都是由其邻居节点更新的，因此作者采用节点 $v$ 的 $K$-hop 子图结构对$v$进行编码，最终获得的节点 embeddings $h_v^{(K)}$ 作为节点$v$的”词”表示。该子图结构（Substructure）如下图所示。如果左图中的红色节点是$v$，那么它的整个$K$-hop领域就是这个节点的”词”表示，根据GNN的聚合算法，$K$-hop领域内的所有邻居都将自身的Embedding聚合到$v$上来，形成$v$的”词向量”$h_v^{(k)}。 类似地，类比到NLP上，上下文指词前后的内容，那么图中的上下文（Context）可以被定义为围绕 K-hop 子图的图结构。例如：左图中 $r_1$-hop 和 $r_2$-hop 间的节点形成了节点中间红色节点的上下文。这里定义 $r_1 &amp;lt; K$ ，这样一些节点在词和上下文之间可以共享。 使用另一个 GNN 将上下文编码到一个固定向量中 定义完图上的”词”和”上下文”的概念之后，我们还需要将”词”和”上下文”表征出来，和NLP的任务不同，图中的”词”和”上下文”都是有结构的数据，因为他们都是子图的结构。在论文中，作者将这两个Embedding分别使用两个GNN进行编码，对于节点$v$，只需要使用传统GNN的聚合公式即可得到节点$v$的词向量表示(见右图上边部分)，再使用另一个GNN(记做GNN‘，见右图下边部分)对节点$v$的”上下文”结构中涉及到的节点($r_1$-hop和$r_2$-hop之间的节点)的Embedding做一个pooling来表征节点$v$的上下文信息。我们将节点$v$的”词”表征记做为$h_v^{(K)}$，而”上下文”表征记做为$c_v^{G^\prime}$，其中$G^\prime$表示用来对上下文进行编码的另一个GNN。 通过负采样进行训练 得到节点的”词”表征和”上下文”表征之后，我们可以通过一个二分类任务进行学习 \[\sigma\left(h_{v}^{(K) \top} c_{v^{\prime}}^{G^{\prime}}\right) \approx \mathbf{1}\left\{v \text { and } v^{\prime} \text { are the same node in } G\right\}\] 其中\(\mathbf{1}(\cdot)\) 是指示符函数，即：我们希望对于相同的节点，它的自身表征(“词”表征)和它的邻域表征(“上下文”表征)尽量相似，当然，对于不同节点的自身表征和邻域表征，我们希望它们尽量不相似，论文中通过负采样技术构建了这种负样本，在此不加赘述。 做一个小结，通过设计上下文预测的预训练任务，我们希望GNN能够将具有相似邻域的节点映射到相近的Embedding空间中，即使得学习到的节点Embedding能够捕获其邻域信息。 任务2 masking：训练节点 embeddings 以获得领域相关知识 任务2的设计和前面介绍的论文很相似，不同于前一篇博文中介绍的是，这篇论文的是对节点/边的某些属性进行masking，然后利用学习到的节点Embedding进行预测。下图展示了masking节点的任务，在论文所实验的分子结构的场景中，被masking的信息是节点的分子类型，然后通过节点Embedding来预测这些分子类型（碳、氮、氧，硫等）。同理，对于masking边的预训练任务，我们可以通过边的两个端点的Embedding来预测边的类型（单键、双键等） 做一个小结，设计masking预训练任务的目的是来让模型学习有意义节点Embedding，以反映出特定领域知识。如上图所示，通过对分子结构图进行掩码，GNNs 能够学习一些化学规则。 任务3 图预测任务：训练节点Embedding以组合成合理的全图Embedding 上文中我们介绍了仅设计节点预训练任务的弊端，这节作者将重点放在如何设计图级别预训练的方法来学习可组合的节点embeddings： 1) 通过对特定领域的图标签进行预测，比如这个图是什么或者属于那种类型，这其实是有监督的方式; 2) 通过对图级别结构进行自监督预测，例如图编辑距离或图结构相似性。 在论文中，作者主要关注第一种方式，其实就是一种有监督的训练方式来调整节点Embedding。 做一个小结，本节中介绍了利用预训练 GNNs 来生成可组合的节点 embeddings 。通过组合这些Embedding能够获得整个图的有效表示，以确保图 embeddings 并且可以适用到各种下游任务中。</summary></entry><entry><title type="html">GNN 教程：图攻击与图对抗</title><link href="https://archwalker.github.io/blog/2019/07/20/GNN-Attack-0.html" rel="alternate" type="text/html" title="GNN 教程：图攻击与图对抗" /><published>2019-07-20T12:00:00+08:00</published><updated>2019-07-20T12:00:00+08:00</updated><id>https://archwalker.github.io/blog/2019/07/20/GNN-Attack-0</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/07/20/GNN-Attack-0.html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;此为原创文章，未经许可，禁止转载&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这篇博文主要介绍的对图神经网络进行攻击，即：通过对某些节点的特征进行扰动、或者对图结构进行扰动使得图神经网络对于特定节点分类任务失效(分类错误)，研究图神经网络的对抗攻击是有必要的，因为这会帮助我们构建更加鲁棒的图神经网络模型。博文的主要内容来源于 KDD 2018 Best Paper：&lt;a href=&quot;https://arxiv.org/abs/1805.07984&quot;&gt;Adversarial Attacks on Neural Networks for Graph Data&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;基本记号&quot;&gt;基本记号&lt;/h2&gt;

&lt;p&gt;这篇文章的主要记号仍然沿用了图神经网路的惯例，图卷积层表示为&lt;/p&gt;

\[H^{(l+1)}=\sigma\left(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(l)} W^{(l)}\right)\]

&lt;p&gt;其中 $\tilde{A}=A+I_N$ 表示加了自环(self-loop)后的邻接矩阵，\(\tilde{D}_{ii}=\sum_j \tilde{A}_{ij}\)，$H$是节点的embedding矩阵，$H^{(0)}=X$即为节点的初始embedding，$W$是一个可训练的权重向量。使用这样的记号，单隐层GCN模型可以表示为&lt;/p&gt;

\[Z=f_{\theta}(A, X)=\operatorname{softmax}\left(\hat{A} \sigma\left(\hat{A} X W^{(1)}\right) W^{(2)}\right)\]

&lt;p&gt;其中$\hat{A}=\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}}$，输出$Z_{vc}$表示将节点$v$分类到类别$c$的概率。$\theta$表示所有参数的集合，即\(\theta=\left\{W^{(1)}, W^{(2)}\right\}\)。参数$\theta$通过交叉熵优化：&lt;/p&gt;

\[L(\theta ; A, X)=-\sum_{v \in \mathcal{V}_{L}} \ln Z_{v, c_{v}} \quad, \quad Z=f_{\theta}(A, X)\]

&lt;p&gt;其中$\mathcal{V}_L$表示有标签的节点集合。$c_v$表示节点$v$的类别，训练完成之后，$Z$表示每个节点的类别概率。&lt;/p&gt;

&lt;h2 id=&quot;攻击模型&quot;&gt;攻击模型&lt;/h2&gt;

&lt;p&gt;攻击模型的目标是通过对图$G^{(0)}=\left(A^{(0)}, X^{(0)}\right)$进行细微的扰动，得到图$G^{\prime}=\left(A^{\prime}, X^{\prime}\right)$，使得在图$G’$是上进行的节点分类性能下降。对于邻接矩阵$A^{(0)}$的变动称为&lt;strong&gt;结构攻击&lt;/strong&gt;，对于特征矩阵$X^{(0)}$的扰动称为&lt;strong&gt;特征攻击&lt;/strong&gt;。&lt;/p&gt;

&lt;h3 id=&quot;定义目标节点和攻击者节点&quot;&gt;定义：目标节点和攻击者节点&lt;/h3&gt;

&lt;p&gt;具体而言，我们的目的是对特定的&lt;strong&gt;目标节点&lt;/strong&gt;$v_0\in\mathcal{V}$进行攻击，即改变$v_0$的预测类别，因为图数据边的原因，节点和节点之间是相互关联的，因此我们不一定要通过直接改变目标节点$v_0$的特征使得神经网络对其分类错误，我们也可以通过改变目标节点周围的邻居，我们称这些邻居为&lt;strong&gt;攻击者节点&lt;/strong&gt;$\mathcal{A} \subseteq \mathcal{V}$。攻击者节点必须满足以下性质：&lt;/p&gt;

\[X_{u i}^{\prime} \neq X_{u i}^{(0)} \Rightarrow u \in \mathcal{A} \quad, \quad A_{u v}^{\prime} \neq A_{u v}^{(0)} \Rightarrow u \in \mathcal{A} \vee v \in \mathcal{A}\]

&lt;p&gt;第一个式子表示所有特征有变动的节点都属于攻击者节点，第二个式子表示如果两个节点之间的连边有变动，那么他们都是攻击者节点。&lt;/p&gt;

&lt;p&gt;如果目标节点$v_0\notin\mathcal{A}$，我们称这种攻击为&lt;strong&gt;间接攻击&lt;/strong&gt;，因为我们不是通过直接扰动$v_0$的特征或者关联的边来改变对其的预测分类的。如果目标节点$v_0\in\mathcal{A}$，我们称这种攻击为直接攻击。&lt;/p&gt;

&lt;h3 id=&quot;限制尽可能小得改变图&quot;&gt;限制：尽可能小得改变图&lt;/h3&gt;

&lt;p&gt;对于攻击者来说，他需要尽可能少得改变图数据(节点特征和边信息)以保证攻击不被轻易发现，下式设定了一个对于图进行攻击的次数限制$\Delta$：&lt;/p&gt;

\[\sum_{u} \sum_{i}\left|X_{u i}^{(0)}-X_{u i}^{\prime}\right|+\sum_{u&amp;lt;v}\left|A_{u v}^{(0)}-A_{u v}^{\prime}\right| \leq \Delta\]

&lt;p&gt;这个式子表示所有特征被扰动和邻边被扰动的节点的总数不能超过某一个阈值$\Delta$。记所有满足这个限制的图$G’$集合为$\mathcal{P}^{G^0}_{\Delta,\mathcal{A}}$，那么图上的攻击就可以转化为一个优化问题：&lt;/p&gt;

&lt;p&gt;给定图$G^{(0)}=\left(A^{(0)}, X^{(0)}\right)$，一个目标节点$v_0$，攻击节点集合$\mathcal{A}$。假设$c_{old}$表示$v_0$在图$G^{(0)}$上的分类结果(或者真实标签)，攻击问题的优化定义为：&lt;/p&gt;

\[\begin{array}{c}{\arg \max _{\left(A^{\prime}, X^{\prime}\right) \in \mathcal{P}_{\Delta, \mathcal{A}}^{G 0}} \max _{c \neq c_{o l d}} \ln Z_{v_{0}, c}^{*}-\ln Z_{v_{0}, c_{o l d}}^{*}} \\ {\text {subject to } Z^{*}=f_{\theta^{*}}\left(A^{\prime}, X^{\prime}\right) \text { with } \theta^{*}=\arg \min _{\theta} L\left(\theta ; A^{\prime}, X^{\prime}\right)}\end{array}\]

&lt;p&gt;这个优化问题的意思是：给定所有符合条件(攻击的次数足够少)的攻击后的图$G’=(A’, X’)\in\mathcal{P}_{\Delta, \mathcal{A}}^{G^0}$，找到能够使得目标节点的预测值和被攻击之前的正确值之间差距最大的图结构$G’=(A’, X’)$。注意到在上式中，我们用到了$\theta^{*}$，即每个攻击后的图结构都是在数据集上进行重新训练以得到最优的参数。因此这实际上是一个&lt;strong&gt;二层优化问题(bi-level optimization problem)&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;(5)式是一个大致衡量图变动的指标，在一些复杂的情况下，具体的$\Delta$值很难设定，为了使图变动不容易被发现，我们需要更加精细的指标来衡量图的变动情况。&lt;/p&gt;

&lt;h3 id=&quot;结构扰动限制&quot;&gt;结构扰动限制&lt;/h3&gt;

&lt;p&gt;图结构最重要的特征是度分布，在真实的网络中，度分布常常以power-law的形式出现，即图中节点的度和具有该度的节点数量服从$p(x) \propto x^{-\alpha}$的分布，作者希望改变结构后的图$G’$仍然能服从相似的度分布，即原图的度分布和改变后$\alpha$大致相当，$\alpha$可由下式近似计算&lt;/p&gt;

\[\alpha_{G} \approx 1+\left|\mathcal{D}_{G}\right| \cdot\left[\sum_{d_{i} \in \mathcal{D}_{G}} \log \frac{d_{i}}{d_{\min }-\frac{1}{2}}\right]^{-1}\]

&lt;p&gt;式中$d_{min}$是一个阈值，所有低于这个阈值的节点不参与计算，$d_v^G$表示节点$v$的度，\(\mathcal{D}_G=\left\{d_v^G\vert v\in\mathcal{V}, d_v^G\ge d_{min}\right\}\)表示关于度的multiset。通过这个式子，我们能估计出原图\(\alpha_{G^{(0)}}\)和改变后的图\(\alpha_{G'}\)，同样，我们也能够估计出组合这两个图的\(\alpha_{comb}\)，其中\(\mathcal{D}_{comb}=\mathcal{D}_{G^{(0)}} \cup \mathcal{D}_{G^{\prime}}\)。&lt;/p&gt;

&lt;p&gt;给定$\alpha_x$，$\mathcal{D}_x$的log-likelihood可以通过下式评估：&lt;/p&gt;

\[l\left(\mathcal{D}_{x}\right)=\left|\mathcal{D}_{x}\right| \cdot \log \alpha_{x}+\left|\mathcal{D}_{x}\right| \cdot \alpha_{x} \cdot \log d_{\min }+\left(\alpha_{x}+1\right) \sum_{d_{i} \in \mathcal{D}_{x}} \log d_{i}\]

&lt;p&gt;使用这些log-likelihood，我们能够通过假设检验的方式评估是否两个图\(\mathcal{D}_{G^{(0)}}\)和\(\mathcal{D}_{G^\prime}\)来自于同样的power-law分布，建立两个对立的假设&lt;/p&gt;

\[l\left(H_{0}\right)=l\left(\mathcal{D}_{c o m b}\right) \quad \text { and } \quad l\left(H_{1}\right)=l\left(\mathcal{D}_{G^{(0)}}\right)+l\left(\mathcal{D}_{G^{\prime}}\right)\]

&lt;p&gt;最终的测试统计量可以写成这样的形式：&lt;/p&gt;

\[\Lambda\left(G^{(0)}, G^{\prime}\right)=-2 \cdot l\left(H_{0}\right)+2 \cdot l\left(H_{1}\right)\]

&lt;p&gt;当图的规模非常大的时候，这个统计量服从自由度为1的$\chi^2$分布(卡方分布)。最终，我们只接受扰动后的图$G^{\prime}=\left(A^{\prime}, X^{\prime}\right)$，使得度分布满足&lt;/p&gt;

\[\Lambda\left(G^{(0)}, G^{\prime}\right)&amp;lt;\tau \approx 0.004\]

&lt;h3 id=&quot;特征扰动限制&quot;&gt;特征扰动限制&lt;/h3&gt;

&lt;p&gt;为了对特征的扰动进行更加精细得刻画，我们构建了一个基于特征共现图$C=(\mathcal{F}, E)$的概率随机游走模型，即$\mathcal{F}$是特征的集合，$E \subseteq \mathcal{F} \times \mathcal{F}$类似于一个邻接矩阵，元素$E_{ij}$表示特征$i$和特征$j$共现过。将特征$i$加入到节点$u$的特征集中需要满足下面的条件：&lt;/p&gt;

\[p\left(i | S_{u}\right)=\frac{1}{\left|S_{u}\right|} \sum_{j \in S_{u}} 1 / d_{j} \cdot E_{i j}&amp;gt;\sigma\]

&lt;p&gt;这个式子的意思是，节点$u$的特征集合的所有特征出发，在图上进行一步随机游走，要是有足够大的概率能够到达特征$i$，那么我们认为添加特征$i$到节点$u$的特征集合中是不易被发现的，本文选择了最大的可到达概率的一般，即\(\sigma=0.5 \cdot \frac{1}{\left\vert S_{u}\right\vert} \sum_{j \in S_{u}} 1 / d_{j}\)。&lt;/p&gt;

&lt;p&gt;采样相同的统计测试原理，我们只接受扰动后的图$G^{\prime}=\left(A^{\prime}, X^{\prime}\right)$，使得特征值满足：&lt;/p&gt;

\[\forall u \in \mathcal{V} : \forall i \in \mathcal{F} : X_{u i}^{\prime}=1 \Rightarrow i \in S_{u} \vee p\left(i | S_{u}\right)&amp;gt;\sigma\]

&lt;p&gt;即，对原图每个节点添加的特征要满足共现概率足够大的要求。&lt;/p&gt;

&lt;h3 id=&quot;小结&quot;&gt;小结&lt;/h3&gt;

&lt;p&gt;综上，我们从\(\mathcal{P}_{\Delta, \mathcal{A}}^{G^0}\)选出满足式(11)和式(13)的子集\(\hat{\mathcal{P}}_{\Delta, \mathcal{A}}^{G^0}\)来保证对图的扰动不容易被发现。&lt;/p&gt;

&lt;h2 id=&quot;生成对抗图&quot;&gt;生成对抗图&lt;/h2&gt;

&lt;p&gt;直接求解(6)(11)(13)是比较困难的，因此作者在论文中提出了一个代理模型，通过对代理模型进行攻击得到一个被攻击的图模型，然后这个图模型被用来接着训练最终的模型。&lt;/p&gt;

&lt;p&gt;为了得到一个易于处理但是依然能够保留图卷积操作的代理模型，作者对图卷积层进行了简化，移除了非线性激活函数：&lt;/p&gt;

\[Z^{\prime}=\operatorname{softmax}\left(\hat{A} \hat{A} X W^{(1)} W^{(2)}\right)=\operatorname{softmax}\left(\hat{A}^{2} X W\right)\]

&lt;p&gt;其中$W^{(1)}$和$W^{(2)}$都是可学习的神经网络权重，因此它们被一个$W\in\mathbb{R}^{N\times K}$所替代。&lt;/p&gt;

&lt;p&gt;由于模型的目标是使得目标节点$v_0$的log-probabilities变化量最大，softmax可以直接忽略掉，因为softmax是单调函数。也就是可以把log-probabilities简化为$\hat{A}^2XW$，给定一个在没有任何扰动的数据集上训练好的代理模型，得到模型参数$W$，代理损失被定义为：&lt;/p&gt;

\[\mathcal{L}_{S}\left(A, X ; W, v_{0}\right)=\max _{c \neq c_{o l d}}\left[\hat{A}^{2} X W\right]_{v_{0} c}-\left[\hat{A}^{2} X W\right]_{v_{0} c_{o l d}}\]

&lt;p&gt;$\hat{A}^2XW$输出是一个$N\times K$的矩阵，表示每个节点被预测到每个类上的class probability。上面的式子表示在输出$v_0$的所有class probability中，找到使得class probability最大的一个，且这个类别和节点的真实类别不能相同。&lt;/p&gt;

&lt;p&gt;攻击目标就变成了：&lt;/p&gt;

\[{\arg\max} _{\left(A^{\prime}, X^{\prime}\right) \in \mathcal{P}_{\Delta, \mathcal{A}}^{G 0}}
\mathcal{L}_{S}\left(A^{\prime}, X^{\prime} ; W, v_{0}\right)\]

&lt;p&gt;也就是对所有扰动后的图，选出一个，使得所有目标节点被误分类的可能性最高。&lt;/p&gt;

&lt;p&gt;当然，这个被选出的扰动后的图还要满足扰动不易被发现的条件，即满足式(11)(13)，联合优化式(11)(13)是困难的，因此作者提出了一个贪心的近似策略。首先定义两个得分函数，用来衡量对结构扰动和对特征扰动的代理模型的损失。&lt;/p&gt;

\[\begin{aligned} s_{\text {struct}}\left(e ; G, v_{0}\right) &amp;amp; :=\mathcal{L}_{s}\left(A^{\prime}, X ; W, v_{0}\right) \\ s_{f e a t}\left(f ; G, v_{0}\right) &amp;amp; :=\mathcal{L}_{s}\left(A, X^{\prime} ; W, v_{0}\right) \end{aligned}\]

&lt;p&gt;其中 $A^{\prime} :=A \pm e\left(\text { i.e. } a_{u v}^{\prime}=a_{v u}^{\prime}=1-a_{u v}\right)$ 且 $X^\prime := X \pm f(i.e. x^\prime_{ui}=1-x_{ui})$分别表示对结构和对特征的扰动。$A^\prime$是$A$添加或者删除一个边得到的，而$X^\prime$是$X$添加或者删除一个特征得到的。整个攻击过程可以归结为：给定一个当前状态$G^{(t)}$，在满足不易被发现的条件下，生成所有可能的结构扰动$C_{struct}$，在它们之中选择一个使得log-probablities 改变量最大，也就是$s_{struct}$最大。然后在这个基础之上，生成所有可能的特征扰动$C_{feat}$， 在它们之中选择一个使得log-probablities该变量最大，也就是$s_{feat}$最大，记作状态$G^{t+1}$，重复这两个步骤一直更新，知道超过不易被发现的度量$\Delta$。如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww1.sinaimg.cn/large/006tNc79ly1g58elavwl8j30ww0ta44d.jpg&quot; alt=&quot;Screen Shot 2019-07-22 at 10.23.07 AM&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这个算法能够work的前提是$s_{struct}$和$s_{feat}$的计算要足够高效，因此作者在论文中花了一些篇幅介绍如何高效得计算这些数值，在此不再赘述。&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">引言 此为原创文章，未经许可，禁止转载 这篇博文主要介绍的对图神经网络进行攻击，即：通过对某些节点的特征进行扰动、或者对图结构进行扰动使得图神经网络对于特定节点分类任务失效(分类错误)，研究图神经网络的对抗攻击是有必要的，因为这会帮助我们构建更加鲁棒的图神经网络模型。博文的主要内容来源于 KDD 2018 Best Paper：Adversarial Attacks on Neural Networks for Graph Data 基本记号 这篇文章的主要记号仍然沿用了图神经网路的惯例，图卷积层表示为 \[H^{(l+1)}=\sigma\left(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(l)} W^{(l)}\right)\] 其中 $\tilde{A}=A+I_N$ 表示加了自环(self-loop)后的邻接矩阵，\(\tilde{D}_{ii}=\sum_j \tilde{A}_{ij}\)，$H$是节点的embedding矩阵，$H^{(0)}=X$即为节点的初始embedding，$W$是一个可训练的权重向量。使用这样的记号，单隐层GCN模型可以表示为 \[Z=f_{\theta}(A, X)=\operatorname{softmax}\left(\hat{A} \sigma\left(\hat{A} X W^{(1)}\right) W^{(2)}\right)\] 其中$\hat{A}=\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}}$，输出$Z_{vc}$表示将节点$v$分类到类别$c$的概率。$\theta$表示所有参数的集合，即\(\theta=\left\{W^{(1)}, W^{(2)}\right\}\)。参数$\theta$通过交叉熵优化： \[L(\theta ; A, X)=-\sum_{v \in \mathcal{V}_{L}} \ln Z_{v, c_{v}} \quad, \quad Z=f_{\theta}(A, X)\] 其中$\mathcal{V}_L$表示有标签的节点集合。$c_v$表示节点$v$的类别，训练完成之后，$Z$表示每个节点的类别概率。 攻击模型 攻击模型的目标是通过对图$G^{(0)}=\left(A^{(0)}, X^{(0)}\right)$进行细微的扰动，得到图$G^{\prime}=\left(A^{\prime}, X^{\prime}\right)$，使得在图$G’$是上进行的节点分类性能下降。对于邻接矩阵$A^{(0)}$的变动称为结构攻击，对于特征矩阵$X^{(0)}$的扰动称为特征攻击。 定义：目标节点和攻击者节点 具体而言，我们的目的是对特定的目标节点$v_0\in\mathcal{V}$进行攻击，即改变$v_0$的预测类别，因为图数据边的原因，节点和节点之间是相互关联的，因此我们不一定要通过直接改变目标节点$v_0$的特征使得神经网络对其分类错误，我们也可以通过改变目标节点周围的邻居，我们称这些邻居为攻击者节点$\mathcal{A} \subseteq \mathcal{V}$。攻击者节点必须满足以下性质： \[X_{u i}^{\prime} \neq X_{u i}^{(0)} \Rightarrow u \in \mathcal{A} \quad, \quad A_{u v}^{\prime} \neq A_{u v}^{(0)} \Rightarrow u \in \mathcal{A} \vee v \in \mathcal{A}\] 第一个式子表示所有特征有变动的节点都属于攻击者节点，第二个式子表示如果两个节点之间的连边有变动，那么他们都是攻击者节点。 如果目标节点$v_0\notin\mathcal{A}$，我们称这种攻击为间接攻击，因为我们不是通过直接扰动$v_0$的特征或者关联的边来改变对其的预测分类的。如果目标节点$v_0\in\mathcal{A}$，我们称这种攻击为直接攻击。 限制：尽可能小得改变图 对于攻击者来说，他需要尽可能少得改变图数据(节点特征和边信息)以保证攻击不被轻易发现，下式设定了一个对于图进行攻击的次数限制$\Delta$： \[\sum_{u} \sum_{i}\left|X_{u i}^{(0)}-X_{u i}^{\prime}\right|+\sum_{u&amp;lt;v}\left|A_{u v}^{(0)}-A_{u v}^{\prime}\right| \leq \Delta\] 这个式子表示所有特征被扰动和邻边被扰动的节点的总数不能超过某一个阈值$\Delta$。记所有满足这个限制的图$G’$集合为$\mathcal{P}^{G^0}_{\Delta,\mathcal{A}}$，那么图上的攻击就可以转化为一个优化问题： 给定图$G^{(0)}=\left(A^{(0)}, X^{(0)}\right)$，一个目标节点$v_0$，攻击节点集合$\mathcal{A}$。假设$c_{old}$表示$v_0$在图$G^{(0)}$上的分类结果(或者真实标签)，攻击问题的优化定义为： \[\begin{array}{c}{\arg \max _{\left(A^{\prime}, X^{\prime}\right) \in \mathcal{P}_{\Delta, \mathcal{A}}^{G 0}} \max _{c \neq c_{o l d}} \ln Z_{v_{0}, c}^{*}-\ln Z_{v_{0}, c_{o l d}}^{*}} \\ {\text {subject to } Z^{*}=f_{\theta^{*}}\left(A^{\prime}, X^{\prime}\right) \text { with } \theta^{*}=\arg \min _{\theta} L\left(\theta ; A^{\prime}, X^{\prime}\right)}\end{array}\] 这个优化问题的意思是：给定所有符合条件(攻击的次数足够少)的攻击后的图$G’=(A’, X’)\in\mathcal{P}_{\Delta, \mathcal{A}}^{G^0}$，找到能够使得目标节点的预测值和被攻击之前的正确值之间差距最大的图结构$G’=(A’, X’)$。注意到在上式中，我们用到了$\theta^{*}$，即每个攻击后的图结构都是在数据集上进行重新训练以得到最优的参数。因此这实际上是一个二层优化问题(bi-level optimization problem)。 (5)式是一个大致衡量图变动的指标，在一些复杂的情况下，具体的$\Delta$值很难设定，为了使图变动不容易被发现，我们需要更加精细的指标来衡量图的变动情况。 结构扰动限制 图结构最重要的特征是度分布，在真实的网络中，度分布常常以power-law的形式出现，即图中节点的度和具有该度的节点数量服从$p(x) \propto x^{-\alpha}$的分布，作者希望改变结构后的图$G’$仍然能服从相似的度分布，即原图的度分布和改变后$\alpha$大致相当，$\alpha$可由下式近似计算 \[\alpha_{G} \approx 1+\left|\mathcal{D}_{G}\right| \cdot\left[\sum_{d_{i} \in \mathcal{D}_{G}} \log \frac{d_{i}}{d_{\min }-\frac{1}{2}}\right]^{-1}\] 式中$d_{min}$是一个阈值，所有低于这个阈值的节点不参与计算，$d_v^G$表示节点$v$的度，\(\mathcal{D}_G=\left\{d_v^G\vert v\in\mathcal{V}, d_v^G\ge d_{min}\right\}\)表示关于度的multiset。通过这个式子，我们能估计出原图\(\alpha_{G^{(0)}}\)和改变后的图\(\alpha_{G'}\)，同样，我们也能够估计出组合这两个图的\(\alpha_{comb}\)，其中\(\mathcal{D}_{comb}=\mathcal{D}_{G^{(0)}} \cup \mathcal{D}_{G^{\prime}}\)。 给定$\alpha_x$，$\mathcal{D}_x$的log-likelihood可以通过下式评估： \[l\left(\mathcal{D}_{x}\right)=\left|\mathcal{D}_{x}\right| \cdot \log \alpha_{x}+\left|\mathcal{D}_{x}\right| \cdot \alpha_{x} \cdot \log d_{\min }+\left(\alpha_{x}+1\right) \sum_{d_{i} \in \mathcal{D}_{x}} \log d_{i}\] 使用这些log-likelihood，我们能够通过假设检验的方式评估是否两个图\(\mathcal{D}_{G^{(0)}}\)和\(\mathcal{D}_{G^\prime}\)来自于同样的power-law分布，建立两个对立的假设 \[l\left(H_{0}\right)=l\left(\mathcal{D}_{c o m b}\right) \quad \text { and } \quad l\left(H_{1}\right)=l\left(\mathcal{D}_{G^{(0)}}\right)+l\left(\mathcal{D}_{G^{\prime}}\right)\] 最终的测试统计量可以写成这样的形式： \[\Lambda\left(G^{(0)}, G^{\prime}\right)=-2 \cdot l\left(H_{0}\right)+2 \cdot l\left(H_{1}\right)\] 当图的规模非常大的时候，这个统计量服从自由度为1的$\chi^2$分布(卡方分布)。最终，我们只接受扰动后的图$G^{\prime}=\left(A^{\prime}, X^{\prime}\right)$，使得度分布满足 \[\Lambda\left(G^{(0)}, G^{\prime}\right)&amp;lt;\tau \approx 0.004\] 特征扰动限制 为了对特征的扰动进行更加精细得刻画，我们构建了一个基于特征共现图$C=(\mathcal{F}, E)$的概率随机游走模型，即$\mathcal{F}$是特征的集合，$E \subseteq \mathcal{F} \times \mathcal{F}$类似于一个邻接矩阵，元素$E_{ij}$表示特征$i$和特征$j$共现过。将特征$i$加入到节点$u$的特征集中需要满足下面的条件： \[p\left(i | S_{u}\right)=\frac{1}{\left|S_{u}\right|} \sum_{j \in S_{u}} 1 / d_{j} \cdot E_{i j}&amp;gt;\sigma\] 这个式子的意思是，节点$u$的特征集合的所有特征出发，在图上进行一步随机游走，要是有足够大的概率能够到达特征$i$，那么我们认为添加特征$i$到节点$u$的特征集合中是不易被发现的，本文选择了最大的可到达概率的一般，即\(\sigma=0.5 \cdot \frac{1}{\left\vert S_{u}\right\vert} \sum_{j \in S_{u}} 1 / d_{j}\)。 采样相同的统计测试原理，我们只接受扰动后的图$G^{\prime}=\left(A^{\prime}, X^{\prime}\right)$，使得特征值满足： \[\forall u \in \mathcal{V} : \forall i \in \mathcal{F} : X_{u i}^{\prime}=1 \Rightarrow i \in S_{u} \vee p\left(i | S_{u}\right)&amp;gt;\sigma\] 即，对原图每个节点添加的特征要满足共现概率足够大的要求。 小结 综上，我们从\(\mathcal{P}_{\Delta, \mathcal{A}}^{G^0}\)选出满足式(11)和式(13)的子集\(\hat{\mathcal{P}}_{\Delta, \mathcal{A}}^{G^0}\)来保证对图的扰动不容易被发现。 生成对抗图 直接求解(6)(11)(13)是比较困难的，因此作者在论文中提出了一个代理模型，通过对代理模型进行攻击得到一个被攻击的图模型，然后这个图模型被用来接着训练最终的模型。 为了得到一个易于处理但是依然能够保留图卷积操作的代理模型，作者对图卷积层进行了简化，移除了非线性激活函数： \[Z^{\prime}=\operatorname{softmax}\left(\hat{A} \hat{A} X W^{(1)} W^{(2)}\right)=\operatorname{softmax}\left(\hat{A}^{2} X W\right)\] 其中$W^{(1)}$和$W^{(2)}$都是可学习的神经网络权重，因此它们被一个$W\in\mathbb{R}^{N\times K}$所替代。 由于模型的目标是使得目标节点$v_0$的log-probabilities变化量最大，softmax可以直接忽略掉，因为softmax是单调函数。也就是可以把log-probabilities简化为$\hat{A}^2XW$，给定一个在没有任何扰动的数据集上训练好的代理模型，得到模型参数$W$，代理损失被定义为： \[\mathcal{L}_{S}\left(A, X ; W, v_{0}\right)=\max _{c \neq c_{o l d}}\left[\hat{A}^{2} X W\right]_{v_{0} c}-\left[\hat{A}^{2} X W\right]_{v_{0} c_{o l d}}\] $\hat{A}^2XW$输出是一个$N\times K$的矩阵，表示每个节点被预测到每个类上的class probability。上面的式子表示在输出$v_0$的所有class probability中，找到使得class probability最大的一个，且这个类别和节点的真实类别不能相同。 攻击目标就变成了： \[{\arg\max} _{\left(A^{\prime}, X^{\prime}\right) \in \mathcal{P}_{\Delta, \mathcal{A}}^{G 0}} \mathcal{L}_{S}\left(A^{\prime}, X^{\prime} ; W, v_{0}\right)\] 也就是对所有扰动后的图，选出一个，使得所有目标节点被误分类的可能性最高。 当然，这个被选出的扰动后的图还要满足扰动不易被发现的条件，即满足式(11)(13)，联合优化式(11)(13)是困难的，因此作者提出了一个贪心的近似策略。首先定义两个得分函数，用来衡量对结构扰动和对特征扰动的代理模型的损失。 \[\begin{aligned} s_{\text {struct}}\left(e ; G, v_{0}\right) &amp;amp; :=\mathcal{L}_{s}\left(A^{\prime}, X ; W, v_{0}\right) \\ s_{f e a t}\left(f ; G, v_{0}\right) &amp;amp; :=\mathcal{L}_{s}\left(A, X^{\prime} ; W, v_{0}\right) \end{aligned}\] 其中 $A^{\prime} :=A \pm e\left(\text { i.e. } a_{u v}^{\prime}=a_{v u}^{\prime}=1-a_{u v}\right)$ 且 $X^\prime := X \pm f(i.e. x^\prime_{ui}=1-x_{ui})$分别表示对结构和对特征的扰动。$A^\prime$是$A$添加或者删除一个边得到的，而$X^\prime$是$X$添加或者删除一个特征得到的。整个攻击过程可以归结为：给定一个当前状态$G^{(t)}$，在满足不易被发现的条件下，生成所有可能的结构扰动$C_{struct}$，在它们之中选择一个使得log-probablities 改变量最大，也就是$s_{struct}$最大。然后在这个基础之上，生成所有可能的特征扰动$C_{feat}$， 在它们之中选择一个使得log-probablities该变量最大，也就是$s_{feat}$最大，记作状态$G^{t+1}$，重复这两个步骤一直更新，知道超过不易被发现的度量$\Delta$。如下： 这个算法能够work的前提是$s_{struct}$和$s_{feat}$的计算要足够高效，因此作者在论文中花了一些篇幅介绍如何高效得计算这些数值，在此不再赘述。</summary></entry><entry><title type="html">GNN 教程：图上的预训练任务上篇</title><link href="https://archwalker.github.io/blog/2019/07/18/GNN-Pretraining-0.html" rel="alternate" type="text/html" title="GNN 教程：图上的预训练任务上篇" /><published>2019-07-18T12:00:00+08:00</published><updated>2019-07-18T12:00:00+08:00</updated><id>https://archwalker.github.io/blog/2019/07/18/GNN-Pretraining-0</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/07/18/GNN-Pretraining-0.html">&lt;h2 id=&quot;0-引言&quot;&gt;0 引言&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;此为原创文章，未经许可，禁止转载&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;虽然 GNN 模型及其变体在图结构数据的学习方面取得了成功，但是训练一个准确的 GNN 模型需要大量的带标注的图数据，而标记样本需要消耗大量的人力资源，为了解决这样的问题，一些学者开始研究Graph Pre-training的框架以获取能够迁移到不同任务上的通用图结构信息表征。&lt;/p&gt;

&lt;p&gt;在NLP和CV领域中，学者已经提出了大量的预训练架构。比如：&lt;a href=&quot;https://arxiv.org/abs/1810.04805&quot;&gt;BERT(Devlin et al., 2018)&lt;/a&gt;和&lt;a href=&quot;http://fcv2011.ulsan.ac.kr/files/announcement/513/r-cnn-cvpr.pdf&quot;&gt;VGG Nets (Girshick et al., 2014)&lt;/a&gt;，这些模型被用来从未标注的数据中学习输入数据的通用表征，并为模型提供更合理的初始化参数，以简化下游任务的训练过程。&lt;/p&gt;

&lt;p&gt;这篇博文将向大家介绍图上的预训练模型，来自论文&lt;a href=&quot;https://arxiv.org/abs/1905.13728&quot;&gt;Pre-Training Graph Neural Networks for Generic Structural Feature Extraction&lt;/a&gt; 重点讨论下面两个问题：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;GNNs 是否能够从预训练中受益？&lt;/li&gt;
  &lt;li&gt;设置哪几种预训练任务比较合理？&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;1-预训练介绍&quot;&gt;1 预训练介绍&lt;/h2&gt;

&lt;p&gt;本节将向大家介绍什么是模型的预训练。对于一般的模型，如果我们有充足的数据和标签，我们可以通过有监督学习得到非常好的结果。但是在现实生活中，我们常常有大量的数据而仅仅有少量的标签，而标注数据需要耗费大量的精力，若直接丢掉这些未标注的数据也很可惜。因此学者们开始研究如何从未标注的数据中使模型受益。&lt;/p&gt;

&lt;p&gt;一个简单的做法是我们自己为这些未标注数据”造标签”，当然这些标签和我们学习任务的最终标签不一样，否则我们也不用模型学习了。举个简单例子，比如我们想用图神经网络做图上节点的分类，然而有标签的节点很少，这时候我们可以设计一些其他任务，比如利用图神经网络预测节点的度，节点的度信息可以简单的统计得到，通过这样的学习，我们希望图神经网络能够学习到每个节点在图结构中的局部信息，而这些信息对于我们最终的节点分类任务是有帮助的。&lt;/p&gt;

&lt;p&gt;在上面的例子中，节点的标签是我们最终想要预测的标签，而节点的度是我们造出来的标签，通过使用图神经网络预测节点的度，我们可以得到1)适用于节点度预测的节点embedding 2)适用于节点度预测任务的图神经网络的权重矩阵，然后我们可以1)将节点embedding接到分类器中并使用有标签的数据进行分类学习 2)直接在图神经网络上使用有标签的数据继续训练，调整权重矩阵，以得到适用于节点分类任务的模型。&lt;/p&gt;

&lt;p&gt;以上就是预训练的基本思想，下面我们来看图神经网络中的预训练具体是如何做的。&lt;/p&gt;

&lt;h2 id=&quot;2-gcn-预训练模型框架介绍&quot;&gt;2 GCN 预训练模型框架介绍&lt;/h2&gt;

&lt;p&gt;如果我们想要利用预训练增强模型的效果，就要借助预训练为节点发掘除了节点自身embedding之外的其他特征，在图数据集上，节点所处的图结构特征很重要，因此本论文中使用三种不同的学习任务以学习图中节点的图结构特征。通过精心设计这三种不同任务，每个节点学到了从局部到全局的图结构特征，这三个任务如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;边重建：首先mask一些边得到带有噪声的图结构，训练图神经网络预测mask掉的边；&lt;/li&gt;
  &lt;li&gt;Centrality Score Ranking：通过对每个节点计算不同的 Centrality Score，其中，包括：Eigencentrality, Betweenness, Closeness和 Subgraph Centrality；然后，通过各个 Centrality Score 的排序值作为label训练 GCN；&lt;/li&gt;
  &lt;li&gt;保留图簇信息：计算每个节点所属的子图，然后训练 GNNs 得到节点特征表示，要求这些节点特征表示仍然能保留节点的子图归属信息。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;整个预训练的框架如下图所示，首先从图中抽取节点的结构特征比如(Degree, K-Core, Clustering Coefficient等)，然后将这些结构特征作为embedding来学习设定的三个预训练任务，label使用的是从图中抽取的各个任务对应的label，最后得到节点embedding表征接到下游的学习任务中。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww1.sinaimg.cn/large/006tNc79ly1g50tgjly0fj31g60mwgst.jpg&quot; alt=&quot;Screen Shot 2019-07-15 at 20.53.02&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;图注：应用 GCN 作为子模块的图预训练框架&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;21-预训练任务介绍&quot;&gt;2.1 预训练任务介绍&lt;/h3&gt;

&lt;h4 id=&quot;任务-1边重建&quot;&gt;任务 1：边重建&lt;/h4&gt;

&lt;p&gt;任务 1 的思路是这样的，首先，随机删除输入图 $\mathcal{G}$ 中一些已存在的边以获得带有噪声的图结构 \(\mathcal{G}^*\)；然后， GNN 模型使用\(\mathcal{G}^*\)作为输入，记作编码器\(\mathcal{F}^{rec}(\mathcal{G}^*)\)，学习到的表征信息输入到 &lt;a href=&quot;https://cs.stanford.edu/~danqi/papers/nips2013.pdf&quot;&gt;NTN&lt;/a&gt; 模型中，NTN 模型是一个解码器，记作$\mathcal{D}^{rec}(\cdot, \cdot)$，以一对节点的embedding作为输入，预测这两个节点是否相连：&lt;/p&gt;

\[\hat{\mathbf{A}}_{u, v}=\mathcal{D}^{r e c}\left(\mathcal{F}^{r e c}\left(\mathcal{G}^{*}\right)[u], \mathcal{F}^{r e c}\left(\mathcal{G}^{*}\right)[v]\right)\]

&lt;p&gt;其中，$\mathcal{F}^{rec}$ 和 $\mathcal{D}^{rec}$ 采用二元交叉熵损失函数进行联合优化：&lt;/p&gt;

\[\mathcal{L}_{r e c}=-\sum_{u, v \in \mathcal{V}}\left(\mathbf{A}_{u, v} \log \left(\hat{\mathbf{A}}_{u, v}\right)+\left(1-\mathbf{A}_{u, v}\right) \log \left(1-\hat{\mathbf{A}}_{u, v}\right)\right)\]

&lt;p&gt;&lt;strong&gt;通过边重建任务，预训练的GNN能够学习到节点embedding的一种较为鲁棒的表示，这种表示在含有噪声或者边信息部分丢失的图数据中很有效。&lt;/strong&gt;&lt;/p&gt;

&lt;h4 id=&quot;任务-2centrality-score-ranking&quot;&gt;任务 2：Centrality Score Ranking&lt;/h4&gt;

&lt;p&gt;作为图的重要指标之一，Centrality Score 能够根据节点位于图中的结构角色来衡量节点的重要性。通过预训练 GNN来对节点的各种 Centrality Score 进行排序，GNN便能够捕获每个节点位于图中的结构角色。论文中，作者主要用了如下4中Centrality Score：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jstor.org/stable/2780000&quot;&gt;Eigencentrality&lt;/a&gt;：从高分节点对邻居贡献更多的角度衡量节点的影响，它描述了节点的在图中的’hub’角色，和PageRank非常类似。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jstor.org/stable/3033543&quot;&gt;Betweenness&lt;/a&gt; 衡量某节点位于其他节点之间的最短路径上的次数，他描述了节点在整个图中的 ‘bridge’ 角色；&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://www.springerlink.com/index/u57264845r413784.pdf&quot;&gt;Closeness&lt;/a&gt; 衡量某节点与其他节点间的最短路径的总长度，他描述了节点在整个图中的 ‘broadcaster’ 角色。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/cond-mat/0504730&quot;&gt;Subgraph Centrality&lt;/a&gt; 衡量某节点对所有子图的参与度(到所有子图最近路径长度的和)，他描述了节点在整个图中的’motif’角色。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上四种Centrality Score描述了节点在整个图中所承担的不同角色，因此，通过这四种Centrality Score的学习任务节点的embedding能够标注不同粒度的图结构信息。&lt;/p&gt;

&lt;p&gt;但是，由于Centrality Score在不同尺度的图之间无可比性，因此，需要利用Centrality Score的相对次序作为任务学习的标签。也就是说，对于节点对 $(u, v)$ 和 Centrality Score $s$，他们间的相对次序记作 $\mathbf{R}_{u, v}^s=\left(s_u&amp;gt;s_v\right)$，解码器 $\mathcal{D}^{\operatorname{rank}}_s$ 通过 $\hat{S}_v=\mathcal{D}^{rank}_s\left(\mathcal{F}^{rank}(\mathcal{G^*})[v]\right)$ 估计排序。利用&lt;a href=&quot;https://icml.cc/2015/wp-content/uploads/2015/06/icml_ranking.pdf&quot;&gt;(Burges et al., 2005)&lt;/a&gt;所定义的成对排序方法，由以下公式估计排名的概率：&lt;/p&gt;

\[\hat{\mathbf{R}}_{u, v}^{s}=\frac{\exp \left(\hat{S}_u-\hat{S}_v\right)}{1+\exp \left(\hat{S}_u-\hat{S}_v\right)}\]

&lt;p&gt;最后，我们通过下式优化每一个Centrality Score $s$ 的 $\mathcal{F}^{rank}$ 和 $\mathcal{D}^{\operatorname{rank}}_s$：&lt;/p&gt;

\[\mathcal{L}_{r a n k}=-\sum_s \sum_{u, v \in \mathcal{V}}\left(\mathbf{R}_{u, v}^s \log \left(\hat{\mathbf{R}}_{u, v}^s\right)+\left(1-\mathbf{R}_{u, v}^s\right) \log \left(1-\hat{\mathbf{R}}_{u, v}^s\right)\right)\]

&lt;p&gt;&lt;strong&gt;通过Centrality Score Ranking任务，预训练的GNN能够学习到图中的每一个节点在全局中起到的作用。&lt;/strong&gt;&lt;/p&gt;

&lt;h4 id=&quot;任务-3保留图簇信息&quot;&gt;任务 3：保留图簇信息&lt;/h4&gt;

&lt;p&gt;作为图的重要指标之一，子图结构意味着簇内部节点的连接更加密集而簇间的节点连接稀疏。假设图中节点属于$K$个不同的簇 \(\mathcal{C}=\left\{C_i\right\}^K_{i=1}\)，并且存在指示函数 \(\left\{\mathbf{I}_c(\cdot) \vert C \in \mathcal{C}\right\}\) 来告知给定节点是否属于簇 $C$。这个指示函数可以通过程序的算法得到，比如联通子图算法。然后我们预训练 GCN 以学习特定的节点表示，要求该表示能在一定程度上保留节点所属簇信息。&lt;/p&gt;

&lt;p&gt;大致做法如下，首先，使用一个基于注意力机制的aggregator$\mathcal{A}$来获取簇信息的表示：&lt;/p&gt;

\[\mathcal{A}\left(\left\{\mathcal{F}_W(\mathcal{G})[v] \vert v \in C\right\}\right)\]

&lt;p&gt;然后，使用&lt;a href=&quot;https://cs.stanford.edu/~danqi/papers/nips2013.pdf&quot;&gt;NTN&lt;/a&gt;模型作为一个解码器$D^{cluster}(\cdot, \cdot)$来评估节点$v$属于簇$C$的可能性：&lt;/p&gt;

\[S(v, C)=\mathcal{D}^{c l u s t e r}\left(\mathcal{F}^{c l u s t e r}\left(\mathcal{G}^{*}\right)[v], \mathcal{A}\left(\left\{\mathcal{F}^{c l u s t e r}\left(\mathcal{G}^{*}\right)[v] | v \in C\right\}\right)\right)\]

&lt;p&gt;节点$v$属于簇$C$的概率可表示为：&lt;/p&gt;

\[P(v \in C)=\frac{\exp (S(v, C))}{\sum_{C^{\prime} \in \mathcal{C}} \exp \left(S\left(v, C^{\prime}\right)\right)}\]

&lt;p&gt;最后，通过以下方法进行对 $\mathcal{F}^{cluster}$ 和 $\mathcal{D}^{cluster}$ 进行优化：&lt;/p&gt;

\[\mathcal{L}_{\text {cluster}}=-\sum_{v \in \mathcal{V}} \mathbf{I}(v) \log (P(v \in \mathbf{I}(v)))\]

&lt;p&gt;&lt;strong&gt;通过保留图簇信息的预训练任务，GNN能够学习到将图中的节点嵌入到可以保留对应簇信息的表示空间中。&lt;/strong&gt;&lt;/p&gt;

&lt;h4 id=&quot;本节小结&quot;&gt;本节小结&lt;/h4&gt;

&lt;p&gt;在此做一个小结，我们设计了边重建、Centrality Score Ranking、保留图簇信息三种图预训练任务，这三个任务能够生成节点embedding对图结构的局部到全局表示，有利于下游的学习任务。&lt;/p&gt;

&lt;h3 id=&quot;22-应用于下游任务&quot;&gt;2.2 应用于下游任务&lt;/h3&gt;

&lt;p&gt;通过上面所提到的带有 \(\mathcal{L}_{\text {rec}}\) 、\(\mathcal{L}_{\operatorname{rank}}\) 和 \(\mathcal{L}_{\text {cluster}}\) 的三种任务上的预训练能够捕GNN来为图中节点生成结构相关的通用表征。接下去，我们可以将这些表征用于下游的任务，主要有两种应用方式：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;作为额外特征：前面我们说到了，预训练GNN后学习到的节点表征与图的结构信息相关，那么这些表征可以结合节点自身的embedding作为节点新的embedding参与下游模型中。&lt;/li&gt;
  &lt;li&gt;微调（Fine Tuning，FT）：预训练GNN后我们不仅得到节点的表征，还得到了GNN的网络参数，这些参数也和图结构学习息息相关，那么我们可以通过在预训练模型之后添加一个与下游任务相关的输出层，以根据特定任务对预训练模型参数进行微调。&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;本节小结-1&quot;&gt;本节小结&lt;/h4&gt;

&lt;p&gt;在此做一个小结，利用 2.1 节所提到方法预训练模型，使预训练模型能够从局部到全局上捕获图结构信息的不同属性，然后将预训练模型在特定的任务中做微调，最终应用于该特定任务中。举个例子，2.1 节所提到的训练预训练模型过程好比我们在高中阶段所学习的语、数、英、物、化、生等基础学科，主要用于夯实基础知识；而2.2节所提到的预训练模型在特定任务中的特征提取和微调过程，相当于我们在大学期间基于已有的基础知识，针对所选专业进行进一步强化，从而获得能够应用于实际场景的作用技能。&lt;/p&gt;

&lt;h2 id=&quot;后记&quot;&gt;后记&lt;/h2&gt;

&lt;p&gt;本篇博文重点介绍了 GNN 的预训练模型，该模型通过捕获未标注图数据中通用的结构信息以提供有用的表征信息或者参数来提高 GCN 下游任务的性能。图上的预训练任务有很多种，本篇论文中精心设计的三种从局部到全局捕获图结构信息，在下一篇博文中，我们还将介绍其他可行的预训练任务。&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">0 引言 此为原创文章，未经许可，禁止转载 虽然 GNN 模型及其变体在图结构数据的学习方面取得了成功，但是训练一个准确的 GNN 模型需要大量的带标注的图数据，而标记样本需要消耗大量的人力资源，为了解决这样的问题，一些学者开始研究Graph Pre-training的框架以获取能够迁移到不同任务上的通用图结构信息表征。 在NLP和CV领域中，学者已经提出了大量的预训练架构。比如：BERT(Devlin et al., 2018)和VGG Nets (Girshick et al., 2014)，这些模型被用来从未标注的数据中学习输入数据的通用表征，并为模型提供更合理的初始化参数，以简化下游任务的训练过程。 这篇博文将向大家介绍图上的预训练模型，来自论文Pre-Training Graph Neural Networks for Generic Structural Feature Extraction 重点讨论下面两个问题： GNNs 是否能够从预训练中受益？ 设置哪几种预训练任务比较合理？ 1 预训练介绍 本节将向大家介绍什么是模型的预训练。对于一般的模型，如果我们有充足的数据和标签，我们可以通过有监督学习得到非常好的结果。但是在现实生活中，我们常常有大量的数据而仅仅有少量的标签，而标注数据需要耗费大量的精力，若直接丢掉这些未标注的数据也很可惜。因此学者们开始研究如何从未标注的数据中使模型受益。 一个简单的做法是我们自己为这些未标注数据”造标签”，当然这些标签和我们学习任务的最终标签不一样，否则我们也不用模型学习了。举个简单例子，比如我们想用图神经网络做图上节点的分类，然而有标签的节点很少，这时候我们可以设计一些其他任务，比如利用图神经网络预测节点的度，节点的度信息可以简单的统计得到，通过这样的学习，我们希望图神经网络能够学习到每个节点在图结构中的局部信息，而这些信息对于我们最终的节点分类任务是有帮助的。 在上面的例子中，节点的标签是我们最终想要预测的标签，而节点的度是我们造出来的标签，通过使用图神经网络预测节点的度，我们可以得到1)适用于节点度预测的节点embedding 2)适用于节点度预测任务的图神经网络的权重矩阵，然后我们可以1)将节点embedding接到分类器中并使用有标签的数据进行分类学习 2)直接在图神经网络上使用有标签的数据继续训练，调整权重矩阵，以得到适用于节点分类任务的模型。 以上就是预训练的基本思想，下面我们来看图神经网络中的预训练具体是如何做的。 2 GCN 预训练模型框架介绍 如果我们想要利用预训练增强模型的效果，就要借助预训练为节点发掘除了节点自身embedding之外的其他特征，在图数据集上，节点所处的图结构特征很重要，因此本论文中使用三种不同的学习任务以学习图中节点的图结构特征。通过精心设计这三种不同任务，每个节点学到了从局部到全局的图结构特征，这三个任务如下： 边重建：首先mask一些边得到带有噪声的图结构，训练图神经网络预测mask掉的边； Centrality Score Ranking：通过对每个节点计算不同的 Centrality Score，其中，包括：Eigencentrality, Betweenness, Closeness和 Subgraph Centrality；然后，通过各个 Centrality Score 的排序值作为label训练 GCN； 保留图簇信息：计算每个节点所属的子图，然后训练 GNNs 得到节点特征表示，要求这些节点特征表示仍然能保留节点的子图归属信息。 整个预训练的框架如下图所示，首先从图中抽取节点的结构特征比如(Degree, K-Core, Clustering Coefficient等)，然后将这些结构特征作为embedding来学习设定的三个预训练任务，label使用的是从图中抽取的各个任务对应的label，最后得到节点embedding表征接到下游的学习任务中。 图注：应用 GCN 作为子模块的图预训练框架 2.1 预训练任务介绍 任务 1：边重建 任务 1 的思路是这样的，首先，随机删除输入图 $\mathcal{G}$ 中一些已存在的边以获得带有噪声的图结构 \(\mathcal{G}^*\)；然后， GNN 模型使用\(\mathcal{G}^*\)作为输入，记作编码器\(\mathcal{F}^{rec}(\mathcal{G}^*)\)，学习到的表征信息输入到 NTN 模型中，NTN 模型是一个解码器，记作$\mathcal{D}^{rec}(\cdot, \cdot)$，以一对节点的embedding作为输入，预测这两个节点是否相连： \[\hat{\mathbf{A}}_{u, v}=\mathcal{D}^{r e c}\left(\mathcal{F}^{r e c}\left(\mathcal{G}^{*}\right)[u], \mathcal{F}^{r e c}\left(\mathcal{G}^{*}\right)[v]\right)\] 其中，$\mathcal{F}^{rec}$ 和 $\mathcal{D}^{rec}$ 采用二元交叉熵损失函数进行联合优化： \[\mathcal{L}_{r e c}=-\sum_{u, v \in \mathcal{V}}\left(\mathbf{A}_{u, v} \log \left(\hat{\mathbf{A}}_{u, v}\right)+\left(1-\mathbf{A}_{u, v}\right) \log \left(1-\hat{\mathbf{A}}_{u, v}\right)\right)\] 通过边重建任务，预训练的GNN能够学习到节点embedding的一种较为鲁棒的表示，这种表示在含有噪声或者边信息部分丢失的图数据中很有效。 任务 2：Centrality Score Ranking 作为图的重要指标之一，Centrality Score 能够根据节点位于图中的结构角色来衡量节点的重要性。通过预训练 GNN来对节点的各种 Centrality Score 进行排序，GNN便能够捕获每个节点位于图中的结构角色。论文中，作者主要用了如下4中Centrality Score： Eigencentrality：从高分节点对邻居贡献更多的角度衡量节点的影响，它描述了节点的在图中的’hub’角色，和PageRank非常类似。 Betweenness 衡量某节点位于其他节点之间的最短路径上的次数，他描述了节点在整个图中的 ‘bridge’ 角色； Closeness 衡量某节点与其他节点间的最短路径的总长度，他描述了节点在整个图中的 ‘broadcaster’ 角色。 Subgraph Centrality 衡量某节点对所有子图的参与度(到所有子图最近路径长度的和)，他描述了节点在整个图中的’motif’角色。 以上四种Centrality Score描述了节点在整个图中所承担的不同角色，因此，通过这四种Centrality Score的学习任务节点的embedding能够标注不同粒度的图结构信息。 但是，由于Centrality Score在不同尺度的图之间无可比性，因此，需要利用Centrality Score的相对次序作为任务学习的标签。也就是说，对于节点对 $(u, v)$ 和 Centrality Score $s$，他们间的相对次序记作 $\mathbf{R}_{u, v}^s=\left(s_u&amp;gt;s_v\right)$，解码器 $\mathcal{D}^{\operatorname{rank}}_s$ 通过 $\hat{S}_v=\mathcal{D}^{rank}_s\left(\mathcal{F}^{rank}(\mathcal{G^*})[v]\right)$ 估计排序。利用(Burges et al., 2005)所定义的成对排序方法，由以下公式估计排名的概率： \[\hat{\mathbf{R}}_{u, v}^{s}=\frac{\exp \left(\hat{S}_u-\hat{S}_v\right)}{1+\exp \left(\hat{S}_u-\hat{S}_v\right)}\] 最后，我们通过下式优化每一个Centrality Score $s$ 的 $\mathcal{F}^{rank}$ 和 $\mathcal{D}^{\operatorname{rank}}_s$： \[\mathcal{L}_{r a n k}=-\sum_s \sum_{u, v \in \mathcal{V}}\left(\mathbf{R}_{u, v}^s \log \left(\hat{\mathbf{R}}_{u, v}^s\right)+\left(1-\mathbf{R}_{u, v}^s\right) \log \left(1-\hat{\mathbf{R}}_{u, v}^s\right)\right)\] 通过Centrality Score Ranking任务，预训练的GNN能够学习到图中的每一个节点在全局中起到的作用。 任务 3：保留图簇信息 作为图的重要指标之一，子图结构意味着簇内部节点的连接更加密集而簇间的节点连接稀疏。假设图中节点属于$K$个不同的簇 \(\mathcal{C}=\left\{C_i\right\}^K_{i=1}\)，并且存在指示函数 \(\left\{\mathbf{I}_c(\cdot) \vert C \in \mathcal{C}\right\}\) 来告知给定节点是否属于簇 $C$。这个指示函数可以通过程序的算法得到，比如联通子图算法。然后我们预训练 GCN 以学习特定的节点表示，要求该表示能在一定程度上保留节点所属簇信息。 大致做法如下，首先，使用一个基于注意力机制的aggregator$\mathcal{A}$来获取簇信息的表示： \[\mathcal{A}\left(\left\{\mathcal{F}_W(\mathcal{G})[v] \vert v \in C\right\}\right)\] 然后，使用NTN模型作为一个解码器$D^{cluster}(\cdot, \cdot)$来评估节点$v$属于簇$C$的可能性： \[S(v, C)=\mathcal{D}^{c l u s t e r}\left(\mathcal{F}^{c l u s t e r}\left(\mathcal{G}^{*}\right)[v], \mathcal{A}\left(\left\{\mathcal{F}^{c l u s t e r}\left(\mathcal{G}^{*}\right)[v] | v \in C\right\}\right)\right)\] 节点$v$属于簇$C$的概率可表示为： \[P(v \in C)=\frac{\exp (S(v, C))}{\sum_{C^{\prime} \in \mathcal{C}} \exp \left(S\left(v, C^{\prime}\right)\right)}\] 最后，通过以下方法进行对 $\mathcal{F}^{cluster}$ 和 $\mathcal{D}^{cluster}$ 进行优化： \[\mathcal{L}_{\text {cluster}}=-\sum_{v \in \mathcal{V}} \mathbf{I}(v) \log (P(v \in \mathbf{I}(v)))\] 通过保留图簇信息的预训练任务，GNN能够学习到将图中的节点嵌入到可以保留对应簇信息的表示空间中。 本节小结 在此做一个小结，我们设计了边重建、Centrality Score Ranking、保留图簇信息三种图预训练任务，这三个任务能够生成节点embedding对图结构的局部到全局表示，有利于下游的学习任务。 2.2 应用于下游任务 通过上面所提到的带有 \(\mathcal{L}_{\text {rec}}\) 、\(\mathcal{L}_{\operatorname{rank}}\) 和 \(\mathcal{L}_{\text {cluster}}\) 的三种任务上的预训练能够捕GNN来为图中节点生成结构相关的通用表征。接下去，我们可以将这些表征用于下游的任务，主要有两种应用方式： 作为额外特征：前面我们说到了，预训练GNN后学习到的节点表征与图的结构信息相关，那么这些表征可以结合节点自身的embedding作为节点新的embedding参与下游模型中。 微调（Fine Tuning，FT）：预训练GNN后我们不仅得到节点的表征，还得到了GNN的网络参数，这些参数也和图结构学习息息相关，那么我们可以通过在预训练模型之后添加一个与下游任务相关的输出层，以根据特定任务对预训练模型参数进行微调。 本节小结 在此做一个小结，利用 2.1 节所提到方法预训练模型，使预训练模型能够从局部到全局上捕获图结构信息的不同属性，然后将预训练模型在特定的任务中做微调，最终应用于该特定任务中。举个例子，2.1 节所提到的训练预训练模型过程好比我们在高中阶段所学习的语、数、英、物、化、生等基础学科，主要用于夯实基础知识；而2.2节所提到的预训练模型在特定任务中的特征提取和微调过程，相当于我们在大学期间基于已有的基础知识，针对所选专业进行进一步强化，从而获得能够应用于实际场景的作用技能。 后记 本篇博文重点介绍了 GNN 的预训练模型，该模型通过捕获未标注图数据中通用的结构信息以提供有用的表征信息或者参数来提高 GCN 下游任务的性能。图上的预训练任务有很多种，本篇论文中精心设计的三种从局部到全局捕获图结构信息，在下一篇博文中，我们还将介绍其他可行的预训练任务。</summary></entry><entry><title type="html">GNN 教程：DGL框架-大规模分布式训练</title><link href="https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-DistributedTraining.html" rel="alternate" type="text/html" title="GNN 教程：DGL框架-大规模分布式训练" /><published>2019-07-07T21:00:00+08:00</published><updated>2019-07-07T21:00:00+08:00</updated><id>https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-DistributedTraining</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-DistributedTraining.html">&lt;p&gt;&lt;strong&gt;此为原创文章，转载务必保留&lt;a href=&quot;https://archwalker.github.io&quot;&gt;出处&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;前面的文章中我们介绍了DGL如何利用采样的技术缩小计算图的规模来通过mini-batch的方式训练模型，当图特别大的时候，非常多的batches需要被计算，因此运算时间又成了问题，一个容易想到解决方案是采用并行计算的技术，很多worker同时采样，计算并且更新梯度。这篇博文重点介绍DGL的并行计算框架。&lt;/p&gt;

&lt;h2 id=&quot;多进程方案&quot;&gt;多进程方案&lt;/h2&gt;

&lt;p&gt;概括而言，目前DGL(version 0.3)采用的是多进程的并行方案，分布式的方案正在开发中。见下图，DGL的并行计算框架分为两个主要部分：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Graph Store&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sampler&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sampler&lt;/code&gt;被用来从大图中构建许多计算子图(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;)，DGL能够自动得在多个设备上并行运行多个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Sampler&lt;/code&gt;的实例。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Graph Store&lt;/code&gt;存储了大图的embedding信息和结构信息，到目前为止，DGL提供了内存共享式的Graph Store，以用来支持多进程，多GPU的并行训练。DGL未来还将提供分布式的Graph Store，以支持超大规模的图训练。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面来分别介绍它们。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww3.sinaimg.cn/large/006tNc79ly1g4kplvtvkqj31gu0u0ane.jpg&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;graph-store&quot;&gt;Graph Store&lt;/h3&gt;

&lt;p&gt;graph store 包含两个部分，server和client，其中server需要作为守护进程(daemon)在训练之前运行起来。比如如下脚本启动了一个graph store server 和 4个worker，并且载入了reddit数据集：&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python3 run_store_server.py &lt;span class=&quot;nt&quot;&gt;--dataset&lt;/span&gt; reddit &lt;span class=&quot;nt&quot;&gt;--num-workers&lt;/span&gt; 4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在训练过程中，这4个worker将会和client交互以取得训练样本。用户需要做的仅仅是编写训练部分的代码。首先需要创建一个client对象连接到对应的server。下面的脚本中用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shared_memory&lt;/code&gt;初始化&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;store_type&lt;/code&gt;表明client连接的是一个内存共享式的server。&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;g &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; dgl.contrib.graph_store.create_graph_from_store&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;reddit&quot;&lt;/span&gt;, &lt;span class=&quot;nv&quot;&gt;store_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;shared_mem&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在采样的&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/30/GNN-Framework-DGL-NodeFlow.html&quot;&gt;博文&lt;/a&gt;中，我们已经详细介绍了如何通过采样的技术来减小计算子图的规模。回忆一下，图模型的每一层进行了如下的计算：
\(z_{v}^{(l+1)}=\sum_{u \in \mathcal{N}^{(l)}(v)} \tilde{A}_{u v} h_{u}^{(l)} \qquad h_{v}^{(l+1)}=\sigma\left(z_{v}^{(l+1)} W^{(l)}\right)\)
&lt;a href=&quot;https://arxiv.org/abs/1710.10568&quot;&gt;control-variate sampling&lt;/a&gt;用如下的方法近似了$z_v^{(l+1)}$：
\(\begin{aligned} \hat{z}_{v}^{(l+1)}=&amp;amp; \frac{|\mathcal{N}(v)|}{\left|\hat{\mathcal{N}}^{(l)}(v)\right|} \sum_{u \in \hat{\mathcal{N}}^{(l)}(v)} \tilde{A}_{u v}\left(\hat{h}_{u}^{(l)}-\overline{h}_{u}^{(l)}\right)+\sum_{u \in \mathcal{N}(v)} \tilde{A}_{u \nu} \overline{h}_{u}^{(l)} \\ &amp;amp; \hat{h}_{v}^{(l+1)}=\sigma\left(\hat{z}_{v}^{(l+1)} W^{(l)}\right) \end{aligned}\)
除了进行这样的近似，作者还采用了预处理的技巧了把采样的层数减少了1。具体来说，GCN的输入是$X$的原始embedding，预处理之后GCN的输入是$\tilde{A}X$，这种方式使得最早的一层无需进行邻居embedding的融合计算(也就是无需采样)，因为左乘以邻接矩阵已经做了这样的计算，因为，需要采样的层数就减少了1。&lt;/p&gt;

&lt;p&gt;对于一个大图来说，$\tilde{A}$和$X$都可能很大。两个矩阵的乘法就要通过分布式计算的方式完成，即每一个trainer(worker)负责计算一部分，然后聚合起来。DGL提供了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_all&lt;/code&gt;来进行这种计算：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'features'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
             &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'preprocess'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
             &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'preprocess'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'preprocess'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'norm'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;初看这段代码和矩阵计算没有任何关系啊，其实这段代码要从语义上理解，在语义上$\tilde{A}X$表示邻接矩阵和特征矩阵的乘法，即对于每个节点的特征跟新为邻居特征的和。那么再看上面这段代码就容易了，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy_src&lt;/code&gt;将节点特征取出来，并发送出去, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sum&lt;/code&gt;接受到来自邻居的特征并求和，求和结果再发给节点，最后节点自身进行一下renormalize。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_all&lt;/code&gt;在graph store中是分布式进行的，每个trainer都会分派到一部分节点进行更新。&lt;/p&gt;

&lt;p&gt;节点和边的数据现在全部存储在graph store中，因此访问他们不再像以前那样用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g.ndata/g.edata&lt;/code&gt;那样简单，因为这两个方法会读取整个节点和边的数据，而这些数据在graph store中并不存在(他们可能是分开存储的)，因此用户只能通过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g.nodes[node_ids].data[embed_name]&lt;/code&gt;来访问特定节点的Embedding数据。(注意：这种读数据的方式是通用的，并不是graph store特有的，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g.ndata&lt;/code&gt;即是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g.nodes[:].data&lt;/code&gt;的缩写)。&lt;/p&gt;

&lt;p&gt;为了高效地初始化节点和边tensor，DGL提供了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;init_ndata&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;init_edata&lt;/code&gt;这两种方法。这两种方法都会讲初始化的命令发送到graph store server上，由server来代理初始化工作，下面展示了一个例子：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init_ndata&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_hidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'float32'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;init_ndata&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'agg_h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n_hidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'float32'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;其中&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h_i&lt;/code&gt;存储&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;i&lt;/code&gt;层节点Embedding，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agg_h_i&lt;/code&gt;存储&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;i&lt;/code&gt;节点邻居Embedding的聚集后的结果。&lt;/p&gt;

&lt;p&gt;初始化节点数据之后，我们可以通过control-variate sampling的方法来训练GCN)，这个方法在之前的&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/30/GNN-Framework-DGL-NodeFlow.html&quot;&gt;博文&lt;/a&gt;中介绍过&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NeighborSampler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;batch_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_neighbors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                          &lt;span class=&quot;n&quot;&gt;neighbor_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'in'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_hops&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                          &lt;span class=&quot;n&quot;&gt;seed_nodes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;labeled_nodes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_blocks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# aggregate history on the original graph
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer_parent_nid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
               &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'agg_h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)})&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# We need to copy data in the NodeFlow to the right context.
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_from_parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right_context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply_layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'preprocess'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])})&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_blocks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;prev_h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# compute delta_h, the difference of the current activation and the history
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'delta_h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prev_h&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# refresh the old history
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# aggregate the delta_h
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block_compute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                         &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'delta_h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                         &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'delta_h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)})&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;delta_h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'delta_h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;agg_h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'agg_h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# control variate estimator
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delta_h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;agg_h&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply_layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])})&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# update history
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_to_parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;和原来代码稍有不同的是，这里&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right_context&lt;/code&gt;表示数据在哪个设备上，通过将数据调度到正确的设备上，我们就可以完成多设备的分布式训练。&lt;/p&gt;

&lt;h3 id=&quot;distributed-sampler&quot;&gt;Distributed Sampler&lt;/h3&gt;

&lt;p&gt;因为我们有多个设备可以进行并行计算(比如说多GPU，多CPU)，那么需要不断地给每个设备提供&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nodeflow&lt;/code&gt;(计算子图实例)。DGL采用的做法是分出一部分设备专门负责采样，将采样作为服务提供给计算设备，计算设备只负责在采样后的子图上进行计算。DGL支持同时在多个设备上运行多个采样程序，每个采样程序都可以将采样结果发到计算设备上。&lt;/p&gt;

&lt;p&gt;一个分布式采样的示例可以这样写，首先，在训练之前用户需要创建一个分布式&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SamplerReceiver&lt;/code&gt;对象：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;sampler&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dgl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;contrib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sampling&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SamplerReceiver&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;graph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ip_addr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_sampler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SamplerReceiver&lt;/code&gt;类用来从其他设备上接收采样出来的子图，这个API的三个参数分别为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;parent_graph&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ip_address&lt;/code&gt;, 和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;number_of_samplers&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;然后，用户只需要在单机版的训练代码中改变一行：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sampler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_blocks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# aggregate history on the original graph
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer_parent_nid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
               &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
               &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'agg_h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)})&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;其中，代码&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for nf in sampler&lt;/code&gt;用来代替原单机采样代码：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NeighborSampler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;batch_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_neighbors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                          &lt;span class=&quot;n&quot;&gt;neighbor_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'in'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_hops&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                          &lt;span class=&quot;n&quot;&gt;seed_nodes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;labeled_nodes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;其他所有的部分都可以保持不变。&lt;/p&gt;

&lt;p&gt;因此，额外的开发工作主要是要编写运行在采样设备上的采样逻辑。对于邻居采样来说，开发者只需要拷贝单机采样的代码就可以了：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dgl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;contrib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sampling&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SamplerSender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trainer_address&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_epoch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dgl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;contrib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sampling&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NeighborSampler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;graph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;batch_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_neighbors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                       &lt;span class=&quot;n&quot;&gt;neighbor_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'in'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                       &lt;span class=&quot;n&quot;&gt;shuffle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shuffle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                       &lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_workers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                       &lt;span class=&quot;n&quot;&gt;num_hops&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_hops&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                       &lt;span class=&quot;n&quot;&gt;add_self_loop&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add_self_loop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                       &lt;span class=&quot;n&quot;&gt;seed_nodes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seed_nodes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;trainer_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# tell trainer I have finished current epoch
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;sender&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;signal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trainer_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;后话&quot;&gt;后话&lt;/h2&gt;

&lt;p&gt;本篇博文重点介绍了DGL的并行计算框架，其主要由采样层-计算层-存储层三层构建而来，采样和计算分布在不同的机器上，可以并行执行。通过这种方式，在存储充足的情况下，DGL可以处理数以亿计节点和边的大图。&lt;/p&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.dgl.ai/tutorials/models/5_giant_graph/2_giant.html&quot;&gt;DGL tutorial on Large-Scale Training&lt;/a&gt;&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">此为原创文章，转载务必保留出处 引言 前面的文章中我们介绍了DGL如何利用采样的技术缩小计算图的规模来通过mini-batch的方式训练模型，当图特别大的时候，非常多的batches需要被计算，因此运算时间又成了问题，一个容易想到解决方案是采用并行计算的技术，很多worker同时采样，计算并且更新梯度。这篇博文重点介绍DGL的并行计算框架。 多进程方案 概括而言，目前DGL(version 0.3)采用的是多进程的并行方案，分布式的方案正在开发中。见下图，DGL的并行计算框架分为两个主要部分：Graph Store和Sampler Sampler被用来从大图中构建许多计算子图(NodeFlow)，DGL能够自动得在多个设备上并行运行多个Sampler的实例。 Graph Store存储了大图的embedding信息和结构信息，到目前为止，DGL提供了内存共享式的Graph Store，以用来支持多进程，多GPU的并行训练。DGL未来还将提供分布式的Graph Store，以支持超大规模的图训练。 下面来分别介绍它们。 Graph Store graph store 包含两个部分，server和client，其中server需要作为守护进程(daemon)在训练之前运行起来。比如如下脚本启动了一个graph store server 和 4个worker，并且载入了reddit数据集： python3 run_store_server.py --dataset reddit --num-workers 4 在训练过程中，这4个worker将会和client交互以取得训练样本。用户需要做的仅仅是编写训练部分的代码。首先需要创建一个client对象连接到对应的server。下面的脚本中用shared_memory初始化store_type表明client连接的是一个内存共享式的server。 g = dgl.contrib.graph_store.create_graph_from_store(&quot;reddit&quot;, store_type=&quot;shared_mem&quot;) 在采样的博文中，我们已经详细介绍了如何通过采样的技术来减小计算子图的规模。回忆一下，图模型的每一层进行了如下的计算： \(z_{v}^{(l+1)}=\sum_{u \in \mathcal{N}^{(l)}(v)} \tilde{A}_{u v} h_{u}^{(l)} \qquad h_{v}^{(l+1)}=\sigma\left(z_{v}^{(l+1)} W^{(l)}\right)\) control-variate sampling用如下的方法近似了$z_v^{(l+1)}$： \(\begin{aligned} \hat{z}_{v}^{(l+1)}=&amp;amp; \frac{|\mathcal{N}(v)|}{\left|\hat{\mathcal{N}}^{(l)}(v)\right|} \sum_{u \in \hat{\mathcal{N}}^{(l)}(v)} \tilde{A}_{u v}\left(\hat{h}_{u}^{(l)}-\overline{h}_{u}^{(l)}\right)+\sum_{u \in \mathcal{N}(v)} \tilde{A}_{u \nu} \overline{h}_{u}^{(l)} \\ &amp;amp; \hat{h}_{v}^{(l+1)}=\sigma\left(\hat{z}_{v}^{(l+1)} W^{(l)}\right) \end{aligned}\) 除了进行这样的近似，作者还采用了预处理的技巧了把采样的层数减少了1。具体来说，GCN的输入是$X$的原始embedding，预处理之后GCN的输入是$\tilde{A}X$，这种方式使得最早的一层无需进行邻居embedding的融合计算(也就是无需采样)，因为左乘以邻接矩阵已经做了这样的计算，因为，需要采样的层数就减少了1。 对于一个大图来说，$\tilde{A}$和$X$都可能很大。两个矩阵的乘法就要通过分布式计算的方式完成，即每一个trainer(worker)负责计算一部分，然后聚合起来。DGL提供了update_all来进行这种计算： g.update_all(fn.copy_src(src='features', out='m'), fn.sum(msg='m', out='preprocess'), lambda node : {'preprocess': node.data['preprocess'] * node.data['norm']}) 初看这段代码和矩阵计算没有任何关系啊，其实这段代码要从语义上理解，在语义上$\tilde{A}X$表示邻接矩阵和特征矩阵的乘法，即对于每个节点的特征跟新为邻居特征的和。那么再看上面这段代码就容易了，copy_src将节点特征取出来，并发送出去, sum接受到来自邻居的特征并求和，求和结果再发给节点，最后节点自身进行一下renormalize。 update_all在graph store中是分布式进行的，每个trainer都会分派到一部分节点进行更新。 节点和边的数据现在全部存储在graph store中，因此访问他们不再像以前那样用 g.ndata/g.edata那样简单，因为这两个方法会读取整个节点和边的数据，而这些数据在graph store中并不存在(他们可能是分开存储的)，因此用户只能通过g.nodes[node_ids].data[embed_name]来访问特定节点的Embedding数据。(注意：这种读数据的方式是通用的，并不是graph store特有的，g.ndata即是g.nodes[:].data的缩写)。 为了高效地初始化节点和边tensor，DGL提供了init_ndata和init_edata这两种方法。这两种方法都会讲初始化的命令发送到graph store server上，由server来代理初始化工作，下面展示了一个例子： for i in range(n_layers): g.init_ndata('h_{}'.format(i), (features.shape[0], args.n_hidden), 'float32') g.init_ndata('agg_h_{}'.format(i), (features.shape[0], args.n_hidden), 'float32') 其中h_i存储i层节点Embedding，agg_h_i存储i节点邻居Embedding的聚集后的结果。 初始化节点数据之后，我们可以通过control-variate sampling的方法来训练GCN)，这个方法在之前的博文中介绍过 for nf in NeighborSampler(g, batch_size, num_neighbors, neighbor_type='in', num_hops=L-1, seed_nodes=labeled_nodes): for i in range(nf.num_blocks): # aggregate history on the original graph g.pull(nf.layer_parent_nid(i+1), fn.copy_src(src='h_{}'.format(i), out='m'), lambda node: {'agg_h_{}'.format(i): node.data['m'].mean(axis=1)}) # We need to copy data in the NodeFlow to the right context. nf.copy_from_parent(ctx=right_context) nf.apply_layer(0, lambda node : {'h' : layer(node.data['preprocess'])}) h = nf.layers[0].data['h'] for i in range(nf.num_blocks): prev_h = nf.layers[i].data['h_{}'.format(i)] # compute delta_h, the difference of the current activation and the history nf.layers[i].data['delta_h'] = h - prev_h # refresh the old history nf.layers[i].data['h_{}'.format(i)] = h.detach() # aggregate the delta_h nf.block_compute(i, fn.copy_src(src='delta_h', out='m'), lambda node: {'delta_h': node.data['m'].mean(axis=1)}) delta_h = nf.layers[i + 1].data['delta_h'] agg_h = nf.layers[i + 1].data['agg_h_{}'.format(i)] # control variate estimator nf.layers[i + 1].data['h'] = delta_h + agg_h nf.apply_layer(i + 1, lambda node : {'h' : layer(node.data['h'])}) h = nf.layers[i + 1].data['h'] # update history nf.copy_to_parent() 和原来代码稍有不同的是，这里right_context表示数据在哪个设备上，通过将数据调度到正确的设备上，我们就可以完成多设备的分布式训练。 Distributed Sampler 因为我们有多个设备可以进行并行计算(比如说多GPU，多CPU)，那么需要不断地给每个设备提供nodeflow(计算子图实例)。DGL采用的做法是分出一部分设备专门负责采样，将采样作为服务提供给计算设备，计算设备只负责在采样后的子图上进行计算。DGL支持同时在多个设备上运行多个采样程序，每个采样程序都可以将采样结果发到计算设备上。 一个分布式采样的示例可以这样写，首先，在训练之前用户需要创建一个分布式SamplerReceiver对象： sampler = dgl.contrib.sampling.SamplerReceiver(graph, ip_addr, num_sampler) SamplerReceiver类用来从其他设备上接收采样出来的子图，这个API的三个参数分别为parent_graph, ip_address, 和number_of_samplers 然后，用户只需要在单机版的训练代码中改变一行： for nf in sampler: for i in range(nf.num_blocks): # aggregate history on the original graph g.pull(nf.layer_parent_nid(i+1), fn.copy_src(src='h_{}'.format(i), out='m'), lambda node: {'agg_h_{}'.format(i): node.data['m'].mean(axis=1)}) ... 其中，代码for nf in sampler用来代替原单机采样代码： for nf in NeighborSampler(g, batch_size, num_neighbors, neighbor_type='in', num_hops=L-1, seed_nodes=labeled_nodes): 其他所有的部分都可以保持不变。 因此，额外的开发工作主要是要编写运行在采样设备上的采样逻辑。对于邻居采样来说，开发者只需要拷贝单机采样的代码就可以了： sender = dgl.contrib.sampling.SamplerSender(trainer_address) ... for n in num_epoch: for nf in dgl.contrib.sampling.NeighborSampler(graph, batch_size, num_neighbors, neighbor_type='in', shuffle=shuffle, num_workers=num_workers, num_hops=num_hops, add_self_loop=add_self_loop, seed_nodes=seed_nodes): sender.send(nf, trainer_id) # tell trainer I have finished current epoch sender.signal(trainer_id) 后话 本篇博文重点介绍了DGL的并行计算框架，其主要由采样层-计算层-存储层三层构建而来，采样和计算分布在不同的机器上，可以并行执行。通过这种方式，在存储充足的情况下，DGL可以处理数以亿计节点和边的大图。 Reference DGL tutorial on Large-Scale Training</summary></entry><entry><title type="html">GNN 教程：DGL框架-子图和采样</title><link href="https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-NodeFlow.html" rel="alternate" type="text/html" title="GNN 教程：DGL框架-子图和采样" /><published>2019-07-07T20:30:00+08:00</published><updated>2019-07-07T20:30:00+08:00</updated><id>https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-NodeFlow</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-NodeFlow.html">&lt;p&gt;&lt;strong&gt;此为原创文章，转载务必保留&lt;a href=&quot;https://archwalker.github.io&quot;&gt;出处&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;之前我们大致介绍了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DGL&lt;/code&gt;这个框架，以及如何使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DGL&lt;/code&gt;编写一个GCN模型，用在学术数据集上，这样的模型是workable的。然而，现实生活中我们还会遇到非常庞大的图数据，庞大到邻接矩阵和特征矩阵不能同时塞进内存中，这时如何解决这样的问题呢？&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DGL&lt;/code&gt;采用了和GraphSAGE类似的邻居采样策略，通过构建计算子图缩小了每次计算的图规模，这篇博文将会介绍&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DGL&lt;/code&gt;提供的采样模型。&lt;/p&gt;

&lt;h2 id=&quot;gcn中暴露的问题&quot;&gt;GCN中暴露的问题&lt;/h2&gt;

&lt;p&gt;首先我们回顾一下GCN的逐层embedding更新公式，给定图 $\mathcal{G}=(\mathcal{V}, \mathcal{E})$, 我们用在程序中用邻接矩阵$A\in\mathbb{R}^{\vert\mathcal{V}\vert\times\vert\mathcal{V}\vert}$和及节点embedding$H\in\mathbb{R}^{\vert\mathcal{V}\vert\times d}$表示它，那么一个$L$-层的GCN网络采用如下的更新公式，$l+1$层节点$v$的embedding $h_v^{(l+1)}$取决于它所有在$l$层的邻居embedding$h_u^{(l)}$&lt;/p&gt;

\[z_{v}^{(l+1)}=\sum_{u \in \mathcal{N}(v)} \tilde{A}_{u v} h_{u}^{(l)}\]

\[h_{v}^{(l+1)}=\sigma\left(z_{v}^{(l+1)} W^{(l)}\right)\]

&lt;p&gt;其中，$\mathcal{N}(v)$是节点$v$的邻居节点集合，$\tilde{A}$是正规化后的$A$，比如$\tilde{A}=D^{-1}A$，$W$是可训练的权重矩阵。&lt;/p&gt;

&lt;p&gt;在节点分类的任务中，我们采用如下形式计算loss：&lt;/p&gt;

\[loss = \frac{1}{\left|\mathcal{V}_{\mathcal{L}}\right|} \sum_{v \in \mathcal{V}_{\mathcal{L}}} f\left(y_{v}, z_{\nu}^{(L)}\right)\]

&lt;p&gt;其中 $f(\cdot, \cdot)$可以是任意的损失函数，比如交叉熵损失。&lt;/p&gt;

&lt;p&gt;之前我们在GCN博文中提到，因为计算节点embedding的更新需要载入整个邻接矩阵$\tilde{A}$和特征矩阵$H$进入到内存中(如果利用显卡加速计算，那么这些矩阵将会被载入到显存中)，这样就暴露出一个问题，当图的规模特别大的时候，$\tilde{A}$会变得特别大，当图中每个节点的特征维数特别高的时候$H$会变得特别大，这两种情况都会导致整个图没法载入到内存(或者显存)中从而无法计算。&lt;/p&gt;

&lt;p&gt;解决这个问题的方式，正如我们在GraphSAGE的博文中提到的那样，通过mini-batch训练的方式，每次只构建该batch内节点的一个子图进行更新。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DGL&lt;/code&gt;这个框架自version 0.3之后正式支持这种mini-batch的训练方式，下面我们重点介绍一下它的编程框架。&lt;/p&gt;

&lt;h2 id=&quot;dgl&quot;&gt;DGL&lt;/h2&gt;

&lt;p&gt;和GraphSAGE中一致，DGL将mini-batch 训练的前期准备工作分为两个层面，首先建立为了更新一个batch内节点embedding而需要的所有邻居节点信息的子图，其次为了保证子图的大小不会受到”超级节点“影响，通过采样的技术将每个节点的邻居个数保持一致，使得这一批节点和相关邻居的embedding能够构成一个Tensor放入显卡中计算。&lt;/p&gt;

&lt;p&gt;这两个模块分别叫&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Neighbor Sampling&lt;/code&gt;，下面来详细得介绍它们。&lt;/p&gt;

&lt;h3 id=&quot;nodeflow&quot;&gt;NodeFlow&lt;/h3&gt;

&lt;p&gt;记一个batch内需要更新embedding的节点集合为$\mathcal{V}_B$，从这个节点集合出发，我们可以根据边信息查找计算所要用到的所有邻居节点，比如在下图的例子中，图结构如a)所示，假设我们使用的是2层GCN模型(每个节点的更新考虑其2度以内的邻居embedding)，某个batch内我们需要更新节点$D$的embedding，根据更新规则，为了更新$D$，我们需要其一度节点的embedding信息，即需要节点$A, B, E, G$，而这些节点的更新又需要节点$C, D, F$的embedding。因此我们的计算图如图b)所示，先由$C, D, F$(Layer 0)更新$A, B, E, G$的embedding，再由$A, B, E, G$的embedding更新$D$的embedding。这样的计算图在DGL中叫做&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;是一种层次结构的图，节点被组织在$L+1$层之内(比如上面例子中2层的GCN节点分布在Layer0, Layer1 和 Layer2中)，只有在相邻的层之间才存在边，两个相邻的层称为块(block)。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;是反向建立的，首先确立一个batch内需要更新的节点集合(即Layer2中的节点$D$)，然后这个节点的1阶邻居构成了NodeFlow的下一层(即Layer1中的节点$A, B, E, G$)，再将下一层的节点当做是需要更新的节点集合，重复该操作，直到建立好所有$L+1$层节点信息。&lt;/p&gt;

&lt;p&gt;通过这种方式，在每个batch的训练中，我们实际上将原图a)转化成了一个子图b)，因此当原图很大无法塞进内存的时候，我们可以通过调小batch_size解决这样的问题。&lt;/p&gt;

&lt;p&gt;根据逐层更新公式可知，每一个block之间的计算是完全独立的，因此&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;提供了函数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;block_compute&lt;/code&gt;以提供底层embedding向高层的传递和计算工作。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww4.sinaimg.cn/large/006tNc79ly1g4j3uufw20j30lf05s0tx.jpg&quot; alt=&quot;image0&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;neighbor-sampling&quot;&gt;Neighbor Sampling&lt;/h3&gt;

&lt;p&gt;现实生活中的图的节点分布常常是长尾的，这意味着有一些“超级节点”的度非常高，而还有一大部分节点的度很小。如果我们在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;的建立过程中关联到“超级节点“的话，”超级节点“就会为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;的下一层带来很多节点，使得整个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;非常庞大，违背了设计小的计算子图的初衷。为了解决这样的问题，GraphSAGE提出了邻居采样的策略，通过为每个节点采样一定数量的邻居来近似$z_v^{(l+1)}$，加上采样策略之后，节点embedding的更新公式变为：&lt;/p&gt;

\[\hat{z}_{v}^{(l+1)}=\frac{|\mathcal{N}(v)|}{\left|\hat{\mathcal{N}}^{(l)}(v)\right|} \sum_{u \in \hat{\mathcal{N}}^{(l)}(v)} \tilde{A}_{u v} \hat{h}_{u}^{(l)}\]

\[\hat{h}_{v}^{(l+1)}=\sigma\left(\hat{z}_{v}^{(l+1)} W^{(l)}\right)\]

&lt;p&gt;其中$\hat{\mathcal{N}}^{(l)}$ 表示采样后的邻居集合。假设$D^{(l)}, l=0,\cdots,L$表示$l$层采样的邻居数量(D^(L)表示该batch的节点个数)，称为第$l$层的”感知野“(respective field)，那么通过采样技术一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;的节点数就能被控制在$\sum^{L}_{i=0}D^{(l)}$内。&lt;/p&gt;

&lt;h2 id=&quot;具体实现&quot;&gt;具体实现&lt;/h2&gt;

&lt;p&gt;在具体实现中，采样和计算是两个独立的模型，也就是说，我们通过采样获得子图，再将这个子图输入到标准的GCN模型中训练，这种解耦合的方式使模型变得非常灵活，因为我们可以对采样的方式进行定制，比如&lt;a href=&quot;https://arxiv.org/abs/1710.10568&quot;&gt;Stochastic Training of Graph Convolutional Networks with Variance Reduction&lt;/a&gt;选择特定的邻居以将方差控制在一定的范围内。这种模型与采样分离的方式也是大部分支持超大规模图计算框架的方式（包括这里介绍的DGL，之后我们要介绍的Euler）。&lt;/p&gt;

&lt;p&gt;DGL提供&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NeighborSampler&lt;/code&gt;类来构建采样后的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NeighborSampler&lt;/code&gt;返回的是一个迭代器，生成&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;实例，们来看看DGL提供的一个结合采样策略的GCN实例代码：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# dropout probability
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dropout&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.2&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# batch size
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;batch_size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1000&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# number of neighbors to sample
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_neighbors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# number of epochs
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_epochs&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# initialize the model and cross entropy loss
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GCNSampling&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n_hidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n_classes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;mx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;relu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dropout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'GCN'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;initialize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;loss_fcn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gluon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;SoftmaxCELoss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# use adam optimizer
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;trainer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gluon&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Trainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;collect_params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'adam'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'learning_rate'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.03&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;'wd'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_epochs&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dgl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;contrib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sampling&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NeighborSampler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;batch_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                   &lt;span class=&quot;n&quot;&gt;num_neighbors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                   &lt;span class=&quot;n&quot;&gt;neighbor_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'in'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                   &lt;span class=&quot;n&quot;&gt;shuffle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                   &lt;span class=&quot;n&quot;&gt;num_hops&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                   &lt;span class=&quot;n&quot;&gt;seed_nodes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;train_nid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# When `NodeFlow` is generated from `NeighborSampler`, it only contains
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# the topology structure, on which there is no data attached.
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# Users need to call `copy_from_parent` to copy specific data,
&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# such as input node features, from the original graph.
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_from_parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;autograd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;record&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# forward
&lt;/span&gt;            &lt;span class=&quot;n&quot;&gt;pred&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;batch_nids&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer_parent_nid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;astype&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'int64'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;batch_labels&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;batch_nids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;# cross entropy loss
&lt;/span&gt;            &lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;loss_fcn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pred&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;batch_labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;batch_nids&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# backward
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;backward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# optimization
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;trainer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;batch_size&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Epoch[{}]: loss {}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;asscalar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# We only train the model with 32 mini-batches just for demonstration.
&lt;/span&gt;        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;上面的代码中，model由&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GCNsampling&lt;/code&gt;定义，虽然它的名字里有sampling，但这只是一个标准的GCN模型，其中没有任何和采样相关的内容，和采样相关代码的定义在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dgl.contrib.sampling.Neighborsampler&lt;/code&gt;中，使用图结构&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;g&lt;/code&gt;初始化这个类，并且定义采样的邻居个数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;num_neighbors&lt;/code&gt;，它返回的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nf&lt;/code&gt;即是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodeFlow&lt;/code&gt;实例，采样后的子图。因为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nf&lt;/code&gt;只会返回子图的拓扑结构，不会附带节点Embedding，所以需要调用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy_from_parent()&lt;/code&gt;方法来获取Embedding，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;layer_parent_nid&lt;/code&gt;返回该nodeflow中每一层的节点id，根据上面的图示，当前batch内的节点(称为种子节点)位于最高层，所以&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;layer_parent_nid(-1)&lt;/code&gt;返回当前batch内的节点id。剩下的步骤就是一个标准的模型训练代码，包括前向传播，计算loss，反向传播在此不再赘述。&lt;/p&gt;

&lt;h3 id=&quot;control-variate&quot;&gt;Control Variate&lt;/h3&gt;

&lt;p&gt;通过采样而估计的$\hat{Z}^{(\cdot)}$是无偏的，但是方差会较大，因此需要采大量的邻居样本来减少方差，因此在GraphSAGE的原论文中，作者设定了$D^{(0)}=25$，$D^{(1)}=10$。但是这样做在每一次采样中我们都有大量的邻居需要聚合，因此control variate和核心思路是缓存历史上计算过的聚合值$\bar{h}_n^{(l)}$，根据$\bar{h}_n^{(l)}$和本次采样的邻居共同估计$h_v^{(l)}$，同时在每一轮中更新$\bar{h}_n^{(l)}$。通过使用这种计算，每一个节点采样两个邻居就足够了。&lt;/p&gt;

&lt;p&gt;Control variate方法的原理为：给定随机变量$X$，我们想要估计它的期望$\mathbb{E}[X] = \theta$，为此我们寻找另一个随机变量$Y$，$Y$和$X$强相关并且$Y$的期望$\mathbb{E}[Y]$能够被轻松地计算得到。通过$Y$估计$X$期望的近似值$\tilde{X}$ 可以表示为：
\(\tilde{X}=X-Y+\mathbb{E}[Y]\\
\mathbb{V} \mathbb{A} \mathbb{R}[\tilde{X}]=\mathbb{VAR}[X]+\mathbb{VAR}[Y]-2 \cdot \mathbb{COV}[X, Y]\\\)
具体到我们的场景上，$X$是某次采样节点邻居的聚合，$Y$是该节点所有邻居的聚合。基于control variate的方法训练GCN的过程为：
\(\begin{align}
\hat{z}_{v}^{(l+1)}&amp;amp;=\frac{|\mathcal{N}(v)|}{\left|\hat{\mathcal{N}}^{(l)}(v)\right|} \sum_{u \in \hat{\mathcal{N}}^{(l)}(v)} \tilde{A}_{u v}\left(\hat{h}_{u}^{(l)}-\overline{h}_{u}^{(l)}\right)+\sum_{u \in \mathcal{N}(v)} \tilde{A}_{u \nu} \overline{h}_{u}^{(l)}\\
\hat{h}_{v}^{(l+1)}&amp;amp;=\sigma\left(\hat{z}_{v}^{(l+1)} W^{(l)}\right)
\end{align}\)
那么上面的代码可以按照这种思路改写为：&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ndata&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_0'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ndata&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nd&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zeros&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shape&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n_hidden&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# With control-variate sampling, we only need to sample 2 neighbors to train GCN.
&lt;/span&gt;  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dgl&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;contrib&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sampling&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NeighborSampler&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;batch_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expand_factor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                 &lt;span class=&quot;n&quot;&gt;neighbor_type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'in'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;num_hops&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;L&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                                                 &lt;span class=&quot;n&quot;&gt;seed_nodes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;train_nid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_blocks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# aggregate history on the original graph
&lt;/span&gt;      &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pull&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layer_parent_nid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
             &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
             &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'agg_h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mailbox&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)})&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_from_parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'features'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;num_blocks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;prev_h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# compute delta_h, the difference of the current activation and the history
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'delta_h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;prev_h&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# refresh the old history
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;detach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# aggregate the delta_h
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;block_compute&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                         &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'delta_h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
                         &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'delta_h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;axis&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)})&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;delta_h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'delta_h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;agg_h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'agg_h_{}'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)]&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# control variate estimator
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;delta_h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;agg_h&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply_layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;lambda&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;layer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])})&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;layers&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;].&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# update history
&lt;/span&gt;        &lt;span class=&quot;n&quot;&gt;nf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_to_parent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;上文代码中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nf&lt;/code&gt;是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NeighborSampler&lt;/code&gt;返回的对象，在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nf&lt;/code&gt;的对象的每一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;block&lt;/code&gt;内，首先调用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pull&lt;/code&gt;函数获取$\hat{h}^{(l)}$(即代码中的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agg_h_{}&lt;/code&gt;)，然后计算$\bar{h}_u^{(l)}$和$\hat{h}_u^{(l)}-\bar{h}_u^{(l)}$(即代码中的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;delta_h&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agg_h&lt;/code&gt;)，最后将更新后的结果拷贝回原大图中。&lt;/p&gt;

&lt;h2 id=&quot;后话&quot;&gt;后话&lt;/h2&gt;

&lt;p&gt;这一篇博文介绍了DGL这个框架怎么对大图进行计算的，总结起来，它吸取了GraphSAGE的思路，通过为每个mini-batch构建子图并采样邻居的方式将图规模控制在可计算的范围内。这种采样-计算分离的模型基本是目前所有图神经网络计算大图时所采用的策略。&lt;/p&gt;

&lt;p&gt;有两个细节没有介绍，第一、具体的采样方法，对于邻居的采样方法有很多种，除了最容易想到的重采样/负采样策略很多学者还提出了一些更加优秀的策略，之后我们会在”加速计算、近似方法”模块中详细讨论这些方法的原理；第二、对于超大规模的图，很多框架采用的是分布式的方式，典型的如Euler，这一系列我们还将写一篇关于Euler的博文，介绍它与DGL的异同，它的分布式架构和在超大规模图计算上做的创新。&lt;/p&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://docs.dgl.ai/tutorials/models/5_giant_graph/1_sampling_mx.html&quot;&gt;DGL Tutorial on NodeFlow and Neighbor Sampling&lt;/a&gt;&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">此为原创文章，转载务必保留出处 引言 之前我们大致介绍了DGL这个框架，以及如何使用DGL编写一个GCN模型，用在学术数据集上，这样的模型是workable的。然而，现实生活中我们还会遇到非常庞大的图数据，庞大到邻接矩阵和特征矩阵不能同时塞进内存中，这时如何解决这样的问题呢？DGL采用了和GraphSAGE类似的邻居采样策略，通过构建计算子图缩小了每次计算的图规模，这篇博文将会介绍DGL提供的采样模型。 GCN中暴露的问题 首先我们回顾一下GCN的逐层embedding更新公式，给定图 $\mathcal{G}=(\mathcal{V}, \mathcal{E})$, 我们用在程序中用邻接矩阵$A\in\mathbb{R}^{\vert\mathcal{V}\vert\times\vert\mathcal{V}\vert}$和及节点embedding$H\in\mathbb{R}^{\vert\mathcal{V}\vert\times d}$表示它，那么一个$L$-层的GCN网络采用如下的更新公式，$l+1$层节点$v$的embedding $h_v^{(l+1)}$取决于它所有在$l$层的邻居embedding$h_u^{(l)}$ \[z_{v}^{(l+1)}=\sum_{u \in \mathcal{N}(v)} \tilde{A}_{u v} h_{u}^{(l)}\] \[h_{v}^{(l+1)}=\sigma\left(z_{v}^{(l+1)} W^{(l)}\right)\] 其中，$\mathcal{N}(v)$是节点$v$的邻居节点集合，$\tilde{A}$是正规化后的$A$，比如$\tilde{A}=D^{-1}A$，$W$是可训练的权重矩阵。 在节点分类的任务中，我们采用如下形式计算loss： \[loss = \frac{1}{\left|\mathcal{V}_{\mathcal{L}}\right|} \sum_{v \in \mathcal{V}_{\mathcal{L}}} f\left(y_{v}, z_{\nu}^{(L)}\right)\] 其中 $f(\cdot, \cdot)$可以是任意的损失函数，比如交叉熵损失。 之前我们在GCN博文中提到，因为计算节点embedding的更新需要载入整个邻接矩阵$\tilde{A}$和特征矩阵$H$进入到内存中(如果利用显卡加速计算，那么这些矩阵将会被载入到显存中)，这样就暴露出一个问题，当图的规模特别大的时候，$\tilde{A}$会变得特别大，当图中每个节点的特征维数特别高的时候$H$会变得特别大，这两种情况都会导致整个图没法载入到内存(或者显存)中从而无法计算。 解决这个问题的方式，正如我们在GraphSAGE的博文中提到的那样，通过mini-batch训练的方式，每次只构建该batch内节点的一个子图进行更新。DGL这个框架自version 0.3之后正式支持这种mini-batch的训练方式，下面我们重点介绍一下它的编程框架。 DGL 和GraphSAGE中一致，DGL将mini-batch 训练的前期准备工作分为两个层面，首先建立为了更新一个batch内节点embedding而需要的所有邻居节点信息的子图，其次为了保证子图的大小不会受到”超级节点“影响，通过采样的技术将每个节点的邻居个数保持一致，使得这一批节点和相关邻居的embedding能够构成一个Tensor放入显卡中计算。 这两个模块分别叫NodeFlow和Neighbor Sampling，下面来详细得介绍它们。 NodeFlow 记一个batch内需要更新embedding的节点集合为$\mathcal{V}_B$，从这个节点集合出发，我们可以根据边信息查找计算所要用到的所有邻居节点，比如在下图的例子中，图结构如a)所示，假设我们使用的是2层GCN模型(每个节点的更新考虑其2度以内的邻居embedding)，某个batch内我们需要更新节点$D$的embedding，根据更新规则，为了更新$D$，我们需要其一度节点的embedding信息，即需要节点$A, B, E, G$，而这些节点的更新又需要节点$C, D, F$的embedding。因此我们的计算图如图b)所示，先由$C, D, F$(Layer 0)更新$A, B, E, G$的embedding，再由$A, B, E, G$的embedding更新$D$的embedding。这样的计算图在DGL中叫做NodeFlow。 NodeFlow是一种层次结构的图，节点被组织在$L+1$层之内(比如上面例子中2层的GCN节点分布在Layer0, Layer1 和 Layer2中)，只有在相邻的层之间才存在边，两个相邻的层称为块(block)。NodeFlow是反向建立的，首先确立一个batch内需要更新的节点集合(即Layer2中的节点$D$)，然后这个节点的1阶邻居构成了NodeFlow的下一层(即Layer1中的节点$A, B, E, G$)，再将下一层的节点当做是需要更新的节点集合，重复该操作，直到建立好所有$L+1$层节点信息。 通过这种方式，在每个batch的训练中，我们实际上将原图a)转化成了一个子图b)，因此当原图很大无法塞进内存的时候，我们可以通过调小batch_size解决这样的问题。 根据逐层更新公式可知，每一个block之间的计算是完全独立的，因此NodeFlow提供了函数block_compute以提供底层embedding向高层的传递和计算工作。 Neighbor Sampling 现实生活中的图的节点分布常常是长尾的，这意味着有一些“超级节点”的度非常高，而还有一大部分节点的度很小。如果我们在NodeFlow的建立过程中关联到“超级节点“的话，”超级节点“就会为NodeFlow的下一层带来很多节点，使得整个NodeFlow非常庞大，违背了设计小的计算子图的初衷。为了解决这样的问题，GraphSAGE提出了邻居采样的策略，通过为每个节点采样一定数量的邻居来近似$z_v^{(l+1)}$，加上采样策略之后，节点embedding的更新公式变为： \[\hat{z}_{v}^{(l+1)}=\frac{|\mathcal{N}(v)|}{\left|\hat{\mathcal{N}}^{(l)}(v)\right|} \sum_{u \in \hat{\mathcal{N}}^{(l)}(v)} \tilde{A}_{u v} \hat{h}_{u}^{(l)}\] \[\hat{h}_{v}^{(l+1)}=\sigma\left(\hat{z}_{v}^{(l+1)} W^{(l)}\right)\] 其中$\hat{\mathcal{N}}^{(l)}$ 表示采样后的邻居集合。假设$D^{(l)}, l=0,\cdots,L$表示$l$层采样的邻居数量(D^(L)表示该batch的节点个数)，称为第$l$层的”感知野“(respective field)，那么通过采样技术一个NodeFlow的节点数就能被控制在$\sum^{L}_{i=0}D^{(l)}$内。 具体实现 在具体实现中，采样和计算是两个独立的模型，也就是说，我们通过采样获得子图，再将这个子图输入到标准的GCN模型中训练，这种解耦合的方式使模型变得非常灵活，因为我们可以对采样的方式进行定制，比如Stochastic Training of Graph Convolutional Networks with Variance Reduction选择特定的邻居以将方差控制在一定的范围内。这种模型与采样分离的方式也是大部分支持超大规模图计算框架的方式（包括这里介绍的DGL，之后我们要介绍的Euler）。 DGL提供NeighborSampler类来构建采样后的NodeFlow，NeighborSampler返回的是一个迭代器，生成NodeFlow实例，们来看看DGL提供的一个结合采样策略的GCN实例代码： # dropout probability dropout = 0.2 # batch size batch_size = 1000 # number of neighbors to sample num_neighbors = 4 # number of epochs num_epochs = 1 # initialize the model and cross entropy loss model = GCNSampling(in_feats, n_hidden, n_classes, L, mx.nd.relu, dropout, prefix='GCN') model.initialize() loss_fcn = gluon.loss.SoftmaxCELoss() # use adam optimizer trainer = gluon.Trainer(model.collect_params(), 'adam', {'learning_rate': 0.03, 'wd': 0}) for epoch in range(num_epochs): i = 0 for nf in dgl.contrib.sampling.NeighborSampler(g, batch_size, num_neighbors, neighbor_type='in', shuffle=True, num_hops=L, seed_nodes=train_nid): # When `NodeFlow` is generated from `NeighborSampler`, it only contains # the topology structure, on which there is no data attached. # Users need to call `copy_from_parent` to copy specific data, # such as input node features, from the original graph. nf.copy_from_parent() with mx.autograd.record(): # forward pred = model(nf) batch_nids = nf.layer_parent_nid(-1).astype('int64') batch_labels = labels[batch_nids] # cross entropy loss loss = loss_fcn(pred, batch_labels) loss = loss.sum() / len(batch_nids) # backward loss.backward() # optimization trainer.step(batch_size=1) print(&quot;Epoch[{}]: loss {}&quot;.format(epoch, loss.asscalar())) i += 1 # We only train the model with 32 mini-batches just for demonstration. if i &amp;gt;= 32: break 上面的代码中，model由GCNsampling定义，虽然它的名字里有sampling，但这只是一个标准的GCN模型，其中没有任何和采样相关的内容，和采样相关代码的定义在dgl.contrib.sampling.Neighborsampler中，使用图结构g初始化这个类，并且定义采样的邻居个数num_neighbors，它返回的nf即是NodeFlow实例，采样后的子图。因为nf只会返回子图的拓扑结构，不会附带节点Embedding，所以需要调用copy_from_parent()方法来获取Embedding，layer_parent_nid返回该nodeflow中每一层的节点id，根据上面的图示，当前batch内的节点(称为种子节点)位于最高层，所以layer_parent_nid(-1)返回当前batch内的节点id。剩下的步骤就是一个标准的模型训练代码，包括前向传播，计算loss，反向传播在此不再赘述。 Control Variate 通过采样而估计的$\hat{Z}^{(\cdot)}$是无偏的，但是方差会较大，因此需要采大量的邻居样本来减少方差，因此在GraphSAGE的原论文中，作者设定了$D^{(0)}=25$，$D^{(1)}=10$。但是这样做在每一次采样中我们都有大量的邻居需要聚合，因此control variate和核心思路是缓存历史上计算过的聚合值$\bar{h}_n^{(l)}$，根据$\bar{h}_n^{(l)}$和本次采样的邻居共同估计$h_v^{(l)}$，同时在每一轮中更新$\bar{h}_n^{(l)}$。通过使用这种计算，每一个节点采样两个邻居就足够了。 Control variate方法的原理为：给定随机变量$X$，我们想要估计它的期望$\mathbb{E}[X] = \theta$，为此我们寻找另一个随机变量$Y$，$Y$和$X$强相关并且$Y$的期望$\mathbb{E}[Y]$能够被轻松地计算得到。通过$Y$估计$X$期望的近似值$\tilde{X}$ 可以表示为： \(\tilde{X}=X-Y+\mathbb{E}[Y]\\ \mathbb{V} \mathbb{A} \mathbb{R}[\tilde{X}]=\mathbb{VAR}[X]+\mathbb{VAR}[Y]-2 \cdot \mathbb{COV}[X, Y]\\\) 具体到我们的场景上，$X$是某次采样节点邻居的聚合，$Y$是该节点所有邻居的聚合。基于control variate的方法训练GCN的过程为： \(\begin{align} \hat{z}_{v}^{(l+1)}&amp;amp;=\frac{|\mathcal{N}(v)|}{\left|\hat{\mathcal{N}}^{(l)}(v)\right|} \sum_{u \in \hat{\mathcal{N}}^{(l)}(v)} \tilde{A}_{u v}\left(\hat{h}_{u}^{(l)}-\overline{h}_{u}^{(l)}\right)+\sum_{u \in \mathcal{N}(v)} \tilde{A}_{u \nu} \overline{h}_{u}^{(l)}\\ \hat{h}_{v}^{(l+1)}&amp;amp;=\sigma\left(\hat{z}_{v}^{(l+1)} W^{(l)}\right) \end{align}\) 那么上面的代码可以按照这种思路改写为： g.ndata['h_0'] = features for i in range(L): g.ndata['h_{}'.format(i+1)] = mx.nd.zeros((features.shape[0], n_hidden)) # With control-variate sampling, we only need to sample 2 neighbors to train GCN. for nf in dgl.contrib.sampling.NeighborSampler(g, batch_size, expand_factor=2, neighbor_type='in', num_hops=L, seed_nodes=train_nid): for i in range(nf.num_blocks): # aggregate history on the original graph g.pull(nf.layer_parent_nid(i+1), fn.copy_src(src='h_{}'.format(i), out='m'), lambda node: {'agg_h_{}'.format(i): node.mailbox['m'].mean(axis=1)}) nf.copy_from_parent() h = nf.layers[0].data['features'] for i in range(nf.num_blocks): prev_h = nf.layers[i].data['h_{}'.format(i)] # compute delta_h, the difference of the current activation and the history nf.layers[i].data['delta_h'] = h - prev_h # refresh the old history nf.layers[i].data['h_{}'.format(i)] = h.detach() # aggregate the delta_h nf.block_compute(i, fn.copy_src(src='delta_h', out='m'), lambda node: {'delta_h': node.data['m'].mean(axis=1)}) delta_h = nf.layers[i + 1].data['delta_h'] agg_h = nf.layers[i + 1].data['agg_h_{}'.format(i)] # control variate estimator nf.layers[i + 1].data['h'] = delta_h + agg_h nf.apply_layer(i + 1, lambda node : {'h' : layer(node.data['h'])}) h = nf.layers[i + 1].data['h'] # update history nf.copy_to_parent() 上文代码中，nf是NeighborSampler返回的对象，在nf的对象的每一个block内，首先调用pull函数获取$\hat{h}^{(l)}$(即代码中的agg_h_{})，然后计算$\bar{h}_u^{(l)}$和$\hat{h}_u^{(l)}-\bar{h}_u^{(l)}$(即代码中的delta_h和agg_h)，最后将更新后的结果拷贝回原大图中。 后话 这一篇博文介绍了DGL这个框架怎么对大图进行计算的，总结起来，它吸取了GraphSAGE的思路，通过为每个mini-batch构建子图并采样邻居的方式将图规模控制在可计算的范围内。这种采样-计算分离的模型基本是目前所有图神经网络计算大图时所采用的策略。 有两个细节没有介绍，第一、具体的采样方法，对于邻居的采样方法有很多种，除了最容易想到的重采样/负采样策略很多学者还提出了一些更加优秀的策略，之后我们会在”加速计算、近似方法”模块中详细讨论这些方法的原理；第二、对于超大规模的图，很多框架采用的是分布式的方式，典型的如Euler，这一系列我们还将写一篇关于Euler的博文，介绍它与DGL的异同，它的分布式架构和在超大规模图计算上做的创新。 Reference DGL Tutorial on NodeFlow and Neighbor Sampling</summary></entry><entry><title type="html">GNN 教程：DGL框架-消息和GCN的实现</title><link href="https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-GCN.html" rel="alternate" type="text/html" title="GNN 教程：DGL框架-消息和GCN的实现" /><published>2019-07-07T20:00:00+08:00</published><updated>2019-07-07T20:00:00+08:00</updated><id>https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-GCN</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/07/07/GNN-Framework-DGL-GCN.html">&lt;p&gt;&lt;strong&gt;此为原创文章，转载务必保留&lt;a href=&quot;https://archwalker.github.io&quot;&gt;出处&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;图神经网络的计算模式大致相似，节点的Embedding需要汇聚其邻接节点Embedding以更新，从线性代数的角度来看，这就是邻接矩阵和特征矩阵相乘。然而邻接矩阵通常都会很大，因此另一种计算方法是将邻居的Embedding传递到当前节点上，再进行更新。很多图并行框架都采用详细传递的机制进行运算(比如Google的Pregel)。而图神经网络框架DGL也采用了这样的思路。从本篇博文开始，我们DGL](https://docs.dgl.ai/index.html)做一个系统的介绍，我们主要关注他的设计，尤其是应对大规模图计算的设计。这篇文章将会介绍DGL的核心概念 — 消息传递机制，并且使用DGL框架实现GCN算法。&lt;/p&gt;

&lt;h2 id=&quot;dgl-核心--消息传递&quot;&gt;DGL 核心 — 消息传递&lt;/h2&gt;

&lt;p&gt;DGL 的核心为消息传递机制（message passing），主要分为消息函数 （message function）和汇聚函数（reduce function）。如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww3.sinaimg.cn/large/006tNc79ly1g4r9g38x1lj316n0ewgom.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;消息函数（message function）：传递消息的目的是将节点计算时需要的信息传递给它，因此对每条边来说，每个源节点将会将自身的Embedding（e.src.data）和边的Embedding(edge.data)传递到目的节点；对于每个目的节点来说，它可能会受到多个源节点传过来的消息，它会将这些消息存储在”邮箱”中。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;汇聚函数（reduce function）：汇聚函数的目的是根据邻居传过来的消息更新跟新自身节点Embedding，对每个节点来说，它先从邮箱（v.mailbox[‘m’]）中汇聚消息函数所传递过来的消息（message），并清空邮箱（v.mailbox[‘m’]）内消息；然后该节点结合汇聚后的结果和该节点原Embedding，更新节点Embedding。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;下面我们以GCN的算法为例，详细说明消息传递的机制是如何work的。&lt;/p&gt;

&lt;h2 id=&quot;用消息传递的方式实现gcn&quot;&gt;用消息传递的方式实现GCN&lt;/h2&gt;

&lt;h3 id=&quot;gcn-的线性代数表达&quot;&gt;GCN 的线性代数表达&lt;/h3&gt;

&lt;p&gt;GCN 的逐层传播公式如下所示：&lt;/p&gt;

\[H^{(l+1)}=\sigma\left(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(l)} W^{(l)}\right)\]

&lt;p&gt;从线性代数的角度，节点Embedding$H^{(l+1)}$的的更新方式为首先左乘邻接矩阵$\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}}$以汇聚邻居Embedding，再为新Embedding做一次线性变换(右乘$W^{(l)}$)，简而言之：每个节点拿到邻居节点信息汇聚到自身 embedding 上在进行一次变换。具体 GCN 内容介绍可参考之前的&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/01/GNN-Triplets-GCN.html&quot;&gt;博文&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&quot;从消息传递的角度分析&quot;&gt;从消息传递的角度分析&lt;/h3&gt;

&lt;p&gt;上面的数学描述可以利用消息传递的机制实现为：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;在 GCN 中每个节点都有属于自己的表示 $h_i$;&lt;/li&gt;
  &lt;li&gt;根据消息传递（message passing）的范式，每个节点将会收到来自邻居节点发送的Embedding；&lt;/li&gt;
  &lt;li&gt;每个节点将会对来自邻居节点的 Embedding进行汇聚以得到中间表示 $\hat{h}_i$ ；&lt;/li&gt;
  &lt;li&gt;对中间节点表示 $\hat{h}_i$ 进行线性变换，然后在利用非线性函数$f$进行计算：$h^{new}_u=f\left(W_u \hat{h}_u\right)$;&lt;/li&gt;
  &lt;li&gt;利用新的节点表示 $h^{new}_u$ 对该节点的表示 $h_u$进行更新。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;具体实现&quot;&gt;具体实现&lt;/h3&gt;

&lt;p&gt;step 1，引入相关包&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;dgl&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;dgl.function&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;torch&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;th&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;torch.nn&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;torch.nn.functional&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;dgl&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DGLGraph&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;step 2，我们需要定义 GCN 的 message 函数和 reduce 函数， message 函数用于发送节点的Embedding，reduce 函数用来对收到的 Embedding 进行聚合。在这里，每个节点发送Embedding的时候不需要任何处理，所以可以通过内置的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy_scr&lt;/code&gt;实现，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;out='m'&lt;/code&gt;表示发送到目的节点后目的节点的mailbox用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt;来标识这个消息是源节点的Embedding。目的节点的reduce函数很简单，因为按照GCN的数学定义，邻接矩阵和特征矩阵相乘，以为这更新后的特征矩阵的每一行是原特征矩阵某几行相加的形式，”某几行”是由邻接矩阵选定的，即对应节点的邻居所在的行。因此目的节点reduce只需要通过&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sum&lt;/code&gt;将接受到的信息相加就可以了。&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;gcn_msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;copy_src&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;src&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gcn_reduce&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'m'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;step 3，我们定义一个应用于节点的 node UDF(user defined function)，即定义一个全连接层（fully-connected layer）来对中间节点表示 $\hat{h}_i$ 进行线性变换，然后在利用非线性函数$f$进行计算：$h^{new}_u=f\left(W_u \hat{h}_u\right)$。&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;NodeApplyModule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;activation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NodeApplyModule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linear&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Linear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;activation&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;activation&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;forward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;linear&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;activation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;step 4，我们定义 GCN 的Embedding更新层，以实现在所有节点上进行消息传递，并利用 NodeApplyModule 对节点信息进行计算更新。&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;GCN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;in_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;activation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;GCN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply_mod&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;NodeApplyModule&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;in_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;out_feats&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;activation&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;forward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;feature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ndata&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;feature&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;update_all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gcn_msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;gcn_reduce&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply_nodes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;apply_mod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ndata&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pop&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'h'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;step 5，最后，我们定义了一个包含两个 GCN 层的图神经网络分类器。我们通过向该分类器输入特征大小为 1433 的训练样本，以获得该样本所属的类别编号，类别总共包含 7 类。&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Net&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nn&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Module&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;nb&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Net&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gcn1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GCN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1433&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;relu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gcn2&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;GCN&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;relu&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;forward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gcn1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gcn2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;x&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;net&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Net&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;net&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;step 6，加载 cora 数据集，并进行数据预处理。&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;dgl.data&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;citation_graph&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;citegrh&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;load_cora_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;citegrh&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;load_cora&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;features&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;FloatTensor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LongTensor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ByteTensor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;train_mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;graph&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# add self loop
&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;remove_edges_from&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;selfloop_edges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DGLGraph&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;add_edges&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nodes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nodes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;step 7，训练 GCN 神经网络。&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;time&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;numpy&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;load_cora_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;optimizer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;th&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;optim&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Adam&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;net&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;parameters&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;1e-3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;dur&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;t0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;logits&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;net&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;g&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;features&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;logp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;log_softmax&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logits&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;F&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nll_loss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;optimizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;zero_grad&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;backward&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;optimizer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;dur&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;t0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Epoch {:05d} | Loss {:.4f} | Time(s) {:.4f}&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;loss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;np&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mean&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dur&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;后话&quot;&gt;后话&lt;/h2&gt;

&lt;p&gt;本篇博文介绍了如何利用图神经网络框架DGL编写GCN模型，接下来我们会介绍如何利用DGL实现GraphSAGE中的采样机制，以减少运算规模。&lt;/p&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.dgl.ai/tutorials/basics/2_basics.html&quot;&gt;DGL Basics&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.dgl.ai/tutorials/models/1_gnn/1_gcn.html&quot;&gt;Graph Convolutional Network&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.dgl.ai/tutorials/basics/3_pagerank.html&quot;&gt;PageRank with DGL Message Passing&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzI2MDE5MTQxNg==&amp;amp;mid=2649695390&amp;amp;idx=1&amp;amp;sn=ad628f54c97968d6fff55907c47cb77e&quot;&gt;DGL 作者答疑！关于 DGL 你想知道的都在这里&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">此为原创文章，转载务必保留出处 引言 图神经网络的计算模式大致相似，节点的Embedding需要汇聚其邻接节点Embedding以更新，从线性代数的角度来看，这就是邻接矩阵和特征矩阵相乘。然而邻接矩阵通常都会很大，因此另一种计算方法是将邻居的Embedding传递到当前节点上，再进行更新。很多图并行框架都采用详细传递的机制进行运算(比如Google的Pregel)。而图神经网络框架DGL也采用了这样的思路。从本篇博文开始，我们DGL](https://docs.dgl.ai/index.html)做一个系统的介绍，我们主要关注他的设计，尤其是应对大规模图计算的设计。这篇文章将会介绍DGL的核心概念 — 消息传递机制，并且使用DGL框架实现GCN算法。 DGL 核心 — 消息传递 DGL 的核心为消息传递机制（message passing），主要分为消息函数 （message function）和汇聚函数（reduce function）。如下图所示： 消息函数（message function）：传递消息的目的是将节点计算时需要的信息传递给它，因此对每条边来说，每个源节点将会将自身的Embedding（e.src.data）和边的Embedding(edge.data)传递到目的节点；对于每个目的节点来说，它可能会受到多个源节点传过来的消息，它会将这些消息存储在”邮箱”中。 汇聚函数（reduce function）：汇聚函数的目的是根据邻居传过来的消息更新跟新自身节点Embedding，对每个节点来说，它先从邮箱（v.mailbox[‘m’]）中汇聚消息函数所传递过来的消息（message），并清空邮箱（v.mailbox[‘m’]）内消息；然后该节点结合汇聚后的结果和该节点原Embedding，更新节点Embedding。 下面我们以GCN的算法为例，详细说明消息传递的机制是如何work的。 用消息传递的方式实现GCN GCN 的线性代数表达 GCN 的逐层传播公式如下所示： \[H^{(l+1)}=\sigma\left(\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}} H^{(l)} W^{(l)}\right)\] 从线性代数的角度，节点Embedding$H^{(l+1)}$的的更新方式为首先左乘邻接矩阵$\tilde{D}^{-\frac{1}{2}} \tilde{A} \tilde{D}^{-\frac{1}{2}}$以汇聚邻居Embedding，再为新Embedding做一次线性变换(右乘$W^{(l)}$)，简而言之：每个节点拿到邻居节点信息汇聚到自身 embedding 上在进行一次变换。具体 GCN 内容介绍可参考之前的博文。 从消息传递的角度分析 上面的数学描述可以利用消息传递的机制实现为： 在 GCN 中每个节点都有属于自己的表示 $h_i$; 根据消息传递（message passing）的范式，每个节点将会收到来自邻居节点发送的Embedding； 每个节点将会对来自邻居节点的 Embedding进行汇聚以得到中间表示 $\hat{h}_i$ ； 对中间节点表示 $\hat{h}_i$ 进行线性变换，然后在利用非线性函数$f$进行计算：$h^{new}_u=f\left(W_u \hat{h}_u\right)$; 利用新的节点表示 $h^{new}_u$ 对该节点的表示 $h_u$进行更新。 具体实现 step 1，引入相关包 import dgl import dgl.function as fn import torch as th import torch.nn as nn import torch.nn.functional as F from dgl import DGLGraph step 2，我们需要定义 GCN 的 message 函数和 reduce 函数， message 函数用于发送节点的Embedding，reduce 函数用来对收到的 Embedding 进行聚合。在这里，每个节点发送Embedding的时候不需要任何处理，所以可以通过内置的copy_scr实现，out='m'表示发送到目的节点后目的节点的mailbox用m来标识这个消息是源节点的Embedding。目的节点的reduce函数很简单，因为按照GCN的数学定义，邻接矩阵和特征矩阵相乘，以为这更新后的特征矩阵的每一行是原特征矩阵某几行相加的形式，”某几行”是由邻接矩阵选定的，即对应节点的邻居所在的行。因此目的节点reduce只需要通过sum将接受到的信息相加就可以了。 gcn_msg = fn.copy_src(src='h', out='m') gcn_reduce = fn.sum(msg='m', out='h') step 3，我们定义一个应用于节点的 node UDF(user defined function)，即定义一个全连接层（fully-connected layer）来对中间节点表示 $\hat{h}_i$ 进行线性变换，然后在利用非线性函数$f$进行计算：$h^{new}_u=f\left(W_u \hat{h}_u\right)$。 class NodeApplyModule(nn.Module): def __init__(self, in_feats, out_feats, activation): super(NodeApplyModule, self).__init__() self.linear = nn.Linear(in_feats, out_feats) self.activation = activation def forward(self, node): h = self.linear(node.data['h']) h = self.activation(h) return {'h' : h} step 4，我们定义 GCN 的Embedding更新层，以实现在所有节点上进行消息传递，并利用 NodeApplyModule 对节点信息进行计算更新。 class GCN(nn.Module): def __init__(self, in_feats, out_feats, activation): super(GCN, self).__init__() self.apply_mod = NodeApplyModule(in_feats, out_feats, activation) def forward(self, g, feature): g.ndata['h'] = feature g.update_all(gcn_msg, gcn_reduce) g.apply_nodes(func=self.apply_mod) return g.ndata.pop('h') step 5，最后，我们定义了一个包含两个 GCN 层的图神经网络分类器。我们通过向该分类器输入特征大小为 1433 的训练样本，以获得该样本所属的类别编号，类别总共包含 7 类。 class Net(nn.Module): def __init__(self): super(Net, self).__init__() self.gcn1 = GCN(1433, 16, F.relu) self.gcn2 = GCN(16, 7, F.relu) def forward(self, g, features): x = self.gcn1(g, features) x = self.gcn2(g, x) return x net = Net() print(net) step 6，加载 cora 数据集，并进行数据预处理。 from dgl.data import citation_graph as citegrh def load_cora_data(): data = citegrh.load_cora() features = th.FloatTensor(data.features) labels = th.LongTensor(data.labels) mask = th.ByteTensor(data.train_mask) g = data.graph # add self loop g.remove_edges_from(g.selfloop_edges()) g = DGLGraph(g) g.add_edges(g.nodes(), g.nodes()) return g, features, labels, mask step 7，训练 GCN 神经网络。 import time import numpy as np g, features, labels, mask = load_cora_data() optimizer = th.optim.Adam(net.parameters(), lr=1e-3) dur = [] for epoch in range(30): if epoch &amp;gt;=3: t0 = time.time() logits = net(g, features) logp = F.log_softmax(logits, 1) loss = F.nll_loss(logp[mask], labels[mask]) optimizer.zero_grad() loss.backward() optimizer.step() if epoch &amp;gt;=3: dur.append(time.time() - t0) print(&quot;Epoch {:05d} | Loss {:.4f} | Time(s) {:.4f}&quot;.format( epoch, loss.item(), np.mean(dur))) 后话 本篇博文介绍了如何利用图神经网络框架DGL编写GCN模型，接下来我们会介绍如何利用DGL实现GraphSAGE中的采样机制，以减少运算规模。 Reference DGL Basics Graph Convolutional Network PageRank with DGL Message Passing DGL 作者答疑！关于 DGL 你想知道的都在这里</summary></entry><entry><title type="html">GNN 教程：GNN 模型有多强？</title><link href="https://archwalker.github.io/blog/2019/06/22/GNN-Theory-Power.html" rel="alternate" type="text/html" title="GNN 教程：GNN 模型有多强？" /><published>2019-06-22T12:00:00+08:00</published><updated>2019-06-22T12:00:00+08:00</updated><id>https://archwalker.github.io/blog/2019/06/22/GNN-Theory-Power</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/06/22/GNN-Theory-Power.html">&lt;h2 id=&quot;引言&quot;&gt;引言&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;此为原创文章，未经许可，禁止转载&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;前面的文章中，我们介绍了GNN的三个基本模型GCN、GraphSAGE、GAT，分析了经典的GCN逐层传播公式是如何由谱图卷积推导而来的。GNN模型现在正成为学术研究的热点话题，那么我们不经想问，GNN模型到底有多强呢？之前的&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/22/GNN-Theory-WL.html&quot;&gt;文章&lt;/a&gt;我们介绍了用来衡量GNN表达能力的算法—Weisfeiler-Leman，这篇文章我们将以该算法为基础，向大家介绍ICLR 2019的oral论文 &lt;a href=&quot;http://arxiv.org/abs/1810.00826&quot;&gt;How powerful are graph neural networks&lt;/a&gt; 。&lt;/p&gt;

&lt;h2 id=&quot;图神经网络-graph-neural-network&quot;&gt;图神经网络 Graph Neural Network&lt;/h2&gt;

&lt;p&gt;GNNs 利用图结构和节点初始特征$X_0$学习图节点的表示(embeddings)$h_v$，或者整个图的表示$h_G$。前面我们介绍了GCN、GraphSAGE和GAT这三种模型，图内节点都是通过各种聚合邻居的策略(neighborhood aggregation strategy)迭代式更新的，在$K$步迭代之后，每个节点的Embedding都融合了它$k$ hop所有邻居的信息。转化成数学形式：&lt;/p&gt;

\[a_{v}^{(k)}=\text { AGGREGATE }^{(k)}\left(\left\{h_{u}^{(k-1)} : u \in \mathcal{N}(v)\right\}\right)\]

\[h_{v}^{(k)}=\operatorname{COMBINE}^{(k)}\left(h_{v}^{(k-1)}, a_{v}^{(k)}\right)\]

&lt;p&gt;$h_v^{(k)}$ 即为$k$步迭代之后节点$v$的Embedding。\(\text{AGGREGATE}\) 以及 \(\text{COMBINE}\) 的不同区分了不同的图神经网络模型。比如在使用max-pooling聚合的GraphSAGE模型中，\(\text{AGGREGATE}\) 具有如下形式：&lt;/p&gt;

\[a_{v}^{(k)}=\operatorname{MAX}\left(\left\{\operatorname{ReL} \mathrm{U}\left(W \cdot h_{u}^{(k-1)}\right), \forall u \in \mathcal{N}(v)\right\}\right)\]

&lt;p&gt;\(\text{COMBINE}\) 的形式为&lt;/p&gt;

\[h_{v}^{(k)}=\operatorname{CONCAT}\left(h_{v}^{(k-1)}, a_{v}^{(k)}\right)\]

&lt;p&gt;而在GCN中，\(\text{AGGREGATE}\) 和 \(\text{COMBINE}\) 被组合到了一起：&lt;/p&gt;

\[h_{v}^{(k)}=\operatorname{ReLU}\left(W \cdot \operatorname{MEAN}\left\{h_{u}^{(k-1)}, \forall u \in \mathcal{N}(v) \cup\{v\}\right\}\right)\]

&lt;p&gt;具体细节可以参考我们之前的博文&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/01/GNN-Triplets-GCN.html&quot;&gt;GCN&lt;/a&gt; &lt;a href=&quot;https://archwalker.github.io/blog/2019/06/01/GNN-Triplets-GraphSAGE.html&quot;&gt;GraphSAGE&lt;/a&gt;，在此不在赘述。&lt;/p&gt;

&lt;p&gt;对于节点分类的问题，学习到$h_v^{(k)}$就可以接到下游的分类器了。而另一种图机器学习任务，即对于整个图的分类，我们需要融合节点Embedding以表示出整个图的Embedding，再接到下游分类器。具体来说，通过设计一个读出器函数\(\text{READOUT}\), 我们聚合节点Embedding而求出整个图的Embedding：&lt;/p&gt;

\[h_{G}=\operatorname{READOUT}\left(\left\{h_{v}^{(N)} \vert v \in G\right\}\right)\]

&lt;h2 id=&quot;理论分析&quot;&gt;理论分析&lt;/h2&gt;

&lt;p&gt;对于GNN中的每个节点来说，他们都是通过递归的融合邻居信息来捕获图的结构信息和邻居特征信息，因此每个节点的更新路径是一个树结构，节点位于树根，从叶子节点逐层向上更新Embedding直到根节点。比如在下图中，对于一个两层的图神经网络，节点$B$ 的更新路径是一个高为2的数，$B$ 位于树根。更新方向如下箭头方向所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://ww2.sinaimg.cn/large/006tNc79ly1g4cjiq909tj31ao0q0wkg.jpg&quot; alt=&quot;Screen Shot 2019-06-24 at 8.06.32 PM&quot; /&gt;&lt;/p&gt;

&lt;p&gt;为了分析GNN的表示能力，我们转而研究什么样的GNN结构能够保证将两个不同的节点投影到不同的Embedding空间中(即为它们生成不同的Embedding)。直觉上可知，最强大的GNN结构仅会将拥有完全相同子树结构的两个节点投影到相同的Embedding空间(即这两个节点的Embedding相同，他们的邻居的Embedding相同，数量也相同)。因为子树结构可以通过节点递归得定义得到(如上图中$B$的二度子树结构可以定义为$B$的一度子树结构以及一度子树$(A, C, E)$的1度子树结构)，因此我们的分析可以简化为”最强大的GNN结构仅会将拥有完全相同1度邻域的两个节点投影到相同的Embedding空间”。这里1度领域不仅包括节点的1度邻居，也包括节点自身。即为以节点为根，高为1的子树结构。&lt;/p&gt;

&lt;p&gt;自此，我们的分析框架就建立了，那么下一步是将节点的一度邻居表示出来，由于图上节点邻居没有相对的次序性，因此MultiSet这个数据结构最适合表示这样的领域。Multiset和set不同，Multiset允许集合中的元素出现多次。&lt;strong&gt;Multiset是一个2元组 $X=(S, m)$, 其中$S$是集合中的元素，$m$表示该元素出现的次数&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;在此做一个小结，GNN的表示能力可以这样分析：最强大的GNN能够将两个节点投影到不同的Embedding空间(为这两个节点生成不同的Embedding向量)，除非这两个节点Embedding和1度邻居完全相同。节点的1度邻居可以用Multiset表示，因此最强大的GNN一定能够将两个不同的Multiset映射到不同的Embedding空间。所以最终问题就转化成了设计这样的GNN函数，使得
\(GNN(v_1, Multiset_1) = GNN(v_2, Multiset_2)\)
成立当且仅当  $h_1 = h_2$ and $Multiset_1 = Multiset_2$，$h$ 为节点$v$的Embedding表示，multiset中包含节点的1度邻居信息，包括邻居的数量和Embedding。&lt;/p&gt;

&lt;h2 id=&quot;gin-和weisfeiler-leman算法一样强大&quot;&gt;GIN 和Weisfeiler-Leman算法一样强大&lt;/h2&gt;

&lt;p&gt;有了这些理论分析后，要想设计一个强大的GNN模型，我们要做到是设计\(\text{AGGREGATE}\)、\(\text{COMBINE}\) 以及 \(\text{READOUT}\) 函数使得经过这三个函数映射后不同的multiset能够保持不同，即这些函数都是单射函数(injective function)。为此，作者证明了几个定理：&lt;/p&gt;

&lt;p&gt;首先是为设计单射性质的 \(\text{AGGREGATE}\) 和 \(\text{COMBINE}\) 而证明的：&lt;/p&gt;

&lt;p&gt;**定理5. ** 存在函数 $f: \mathcal{X}\rightarrow\mathbb{R}^n$ 使得 $h(X)=\sum_{x \in X} f(x)$ 对于每个multiset $X\in \mathcal{X}$ 是不同的。再者，任何作用于multiset上的函数 $g$ 能够被如下的形式分解: $g(X)=\phi\left(\sum_{x \in X} f(x)\right)$&lt;/p&gt;

&lt;p&gt;具体证明详见论文，在这里通俗的说下这个定理的目的是什么。这个定理说，对于任意multiset $X$, 都有一个对应的单射函数$g(X)$，这就意味着如果节点的邻居有差别，那么我们可以通过 $g(X)$ 这个函数将它们区分出来，回忆在&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/22/GNN-Theory-WL.html&quot;&gt;Weisfeiler-Leman算法&lt;/a&gt;的例子中，函数$g$ 将邻居Embedding排序之后拼起来，虽然简单，但是满足单射的要求。&lt;/p&gt;

&lt;p&gt;上文中，我们将节点自身归到节点的1度领域中，GCN和GAT中就是这样表示的。在GraphSAGE中，节点自身和其1度邻居是分开对待的，为此，作者证明了如下引理：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;引理6.&lt;/strong&gt; 存在函数 \(f:\mathcal{X}\rightarrow\mathbb{R}^n\) 使得存在数 \(\epsilon\)，函数\(h(c, X) = (1 + \epsilon)\cdot f(c) + \sum_{x\in X} f(x)\) 对于每一对 $(c, X)$ 是单射的，其中 $c\in \mathcal{X}$ 和 $X\subset \mathcal{X}$ 是一个有限的multiset，再者，任何作用于$(c, X)$ 这样的multiset对的函数 $g$ 能够被分解成  $g(c, X)=\phi\left((1+\epsilon) \cdot f(c)+\sum_{x \in X} f(x)\right)$ 这样的形式，其中 \(\phi\) 是一个函数。&lt;/p&gt;

&lt;p&gt;通俗来说，对于任意multiset对$(c, X)$，都有一个对应的单射函数 $g(c, X)$，这里如果把 $c$ 看成节点自身，$X$ 看成是节点的1度领域，那么意味着如果节点自身或者其邻居有差别，我们可以通过 $g(c, X)$ 把它们区分出来，回忆在&lt;a href=&quot;https://archwalker.github.io/blog/2019/06/22/GNN-Theory-WL.html&quot;&gt;Weisfeiler-Leman算法&lt;/a&gt;的例子中，函数 $g$ 的定义是，将节点自身和其1度邻居的拼接向量再拼接起来并用”,”分开，同样简单有效。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;引理6&lt;/strong&gt; 从数学的角度上证明了如果GNN能够学习到$g(c, X)$，GNN能够达到Weisfeiler-Leman的分类能力。那么我们面临的问题是如何设计GNN的结构以学习到这样的$g(c, X)$，观察$g(c, X)$ 的形式可知，它主要是将复合函数$f\circ g$ 作用在节点Embedding上，而根据神经网络的&lt;strong&gt;通用近似理论&lt;/strong&gt;，我们可以将复合函数 $f\circ \phi$ 建模为一个多层感知机MLP，综合以上推导，作者得出了一个和Weisfeiler-Leman具有相同能力的GNN模型，叫做 GIN (Graph Isomorphism Network)：
\(h_{v}^{(k)}=\operatorname{MLP}^{(k)}\left(\left(1+\epsilon^{(k)}\right) \cdot h_{v}^{(k-1)}+\sum_{u \in \mathcal{N}(v)} h_{u}^{(k-1)}\right)\)&lt;/p&gt;

&lt;p&gt;小结一下：这个式子设计了合理的 \(\text{AGGREGATE}\) 和 \(\text{COMBINE}\) ，使他们成为单射函数，对于图中节点的分类任务，这个式子已经和 Weisfeiler-Leman 算法能力一致了，对于图的分类任务，我们还要设计 \(\text{READOUT}\) 单射函数，使得所有的节点Embedding综合而成的图Embedding仍然是唯一的。\(\text{READOUT}\) 函数的设计博文中不再赘述，详情见论文。&lt;/p&gt;

&lt;h2 id=&quot;gin模型和其他图模型的比较&quot;&gt;GIN模型和其他图模型的比较&lt;/h2&gt;

&lt;p&gt;上面我们介绍了这么多，得出了GIN的逐层embedding更新公式，我们说他和Weisfeiler-Leman的能力相同，然而之前提到的图模型比如GraphSAGE和GCN却没有这样强大的能力，下面我们从他们的逐层更新公式上举例子来看看原因：它们的逐层更新公式区别主要有三点&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;GIN 采用了MLP来对节点聚合后的Embedding做非线性隐射，而GCN和GraphSAGE采用的单层感知机的结构(一个权重矩阵$W$加上非线性激活函数\(\sigma\))。然而只有MLP才具有通用近似的能力(MLP 能够拟合 $f\circ g$)，根据上面的理论分析，使用单层感知机的结构会导致逐层更新公式不再是单射函数，导致对于不同的节点和邻居，模型有分辨不出来的风险；&lt;/li&gt;
  &lt;li&gt;GIN 使用 $(1+\epsilon)$  对自身的Embedding进行了加权处理，通过引入对自身Embedding的扰动避免了自身节点Embedding $a$和邻居聚合后Embedding $b$加和相同的情况下， 如果$a, b$ 互换，那么无法分辨的问题。&lt;/li&gt;
  &lt;li&gt;GIN 使用加和(sum)作为邻居Embedding的聚合方式，sum 比 mean 或者 pooling 的方式更强，论文中举了几个例子帮助理解&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;http://ww1.sinaimg.cn/large/006tNc79ly1g4cjizlte0j30yw0b4go2.jpg&quot; alt=&quot;Screen Shot 2019-06-24 at 6.39.40 PM&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假设我们关注的都是位于图中间蓝色节点一次更新后的Embedding，因为该节点的邻居个数在左右图不同 ，因此节点Embedding更新后的结果应当要有差异。对(a)，通过mean或者max后的邻居信息均是1个蓝色节点代表的Embedding，不能分辨邻居有差异；对(b)，max聚合后的邻居信息是绿色和红色节点Embedding的最大值，不能分辨邻居有差异；对(c)，mean聚合后是绿色和红色节点代表Embedding的均值，不能分辨邻居有差异；max聚合后是绿色和红色节点代表Embedding的最大值，同样不能分辨邻居差异。&lt;/p&gt;

&lt;p&gt;而如果采用sum的方式，以上3中情况邻居聚合后的差异都能分辨出来，因此sum 比 mean 或者 max-pooling 的表达能力更强，至少在图同构测试或者节点分类的任务上。&lt;/p&gt;

&lt;h2 id=&quot;关于表达能力的讨论&quot;&gt;关于表达能力的讨论&lt;/h2&gt;

&lt;p&gt;谈到这里，有些对图神经网络比较熟悉的读者可能有疑惑了，在GraphSAGE的论文中，作者比较了各种聚合方式(mean, max-pooling等)，实验结论是mean聚合方式在在实验结果上比较好啊，这不是与该篇论文的分析相悖吗？确实，笔者在多种数据集的实验中也发现mean聚合的效果比较好。作者在接下来的几个小节中解释了可能的原因。&lt;/p&gt;

&lt;h3 id=&quot;mean-学习邻居embedding分布&quot;&gt;mean 学习邻居Embedding分布&lt;/h3&gt;

&lt;p&gt;考察这样两个multiset $X_1=(S, m)$， $X_2=(S, k\cdot m)$，即， $X_2$ 包含 multiset $X_1$ 中的所有元素，且每个元素的数量是 $X_1$ 的 $k$ 倍，那么mean aggregator是无法区分他们的，因此我们可以说，mean aggregator可以用来分辨multiset中元素的分布(比率)，而不是multiset。&lt;/p&gt;

&lt;p&gt;因此 mean aggreator 在特定的任务可能表现得更好，比如在图上统计或者分布信息比特征信息更重要的时候。或者，考虑另一种情况，当节点Embedding极少重复的话，mean aggregator和sum aggregator的表达能力基本是一致的，在很多machine learning的任务中，由于节点的Embedding基本都存在差异，并且mean aggregator能够保持聚合后的每一维的scale和聚合前一致，因此在任务中可能会更好。&lt;/p&gt;

&lt;h3 id=&quot;max-pooling-学习具有表示性的embedding&quot;&gt;max-pooling 学习具有表示性的Embedding&lt;/h3&gt;

&lt;p&gt;考察这样两个multiset $X_1=(S_1, m_1)$， $X_2 = (S_2, m_2)$ 其中$max(S_1) = max(S_2)$，即$X_1$中最大的元素和$X_2$ 相同，那么max-pooling aggregator 是无法区分他们的，因此我们可以说，max-pooling aggregator 可以用来分辨multiset具有代表性的元素或者multiset的“骨架”(skeleton)。在&lt;a href=&quot;https://arxiv.org/pdf/1612.00593.pdf&quot;&gt;PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation&lt;/a&gt;中，作者阐述了max-pooling aggreator 可以用来分辨3D 点云的“骨架”，对噪音和离群点具有鲁棒性。max-pooling aggreator 可能在这类的任务中表现得更好。&lt;/p&gt;

&lt;h2 id=&quot;后记&quot;&gt;后记&lt;/h2&gt;

&lt;p&gt;这篇博文重点介绍了GNN的表示能力，简单来说，可以通过设计特殊GNN的架构使得其达到Weisfeiler-Leman算法在图同构分类中的效果。到目前为止，我们介绍了图神经网络的三个基本模型，GCN、GraphSAGE、GAT；介绍了图卷积神经网络和谱图卷积的关系；介绍了图神经网络模型的表达能力；至此，图神经网络的理论部分暂时告一段落，接下来的博文将会向大家介绍目前图神经网络存在的框架，使大家对图神经网络编程有所了解，毕竟纸上谈兵终非长远之策。&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">引言 此为原创文章，未经许可，禁止转载 前面的文章中，我们介绍了GNN的三个基本模型GCN、GraphSAGE、GAT，分析了经典的GCN逐层传播公式是如何由谱图卷积推导而来的。GNN模型现在正成为学术研究的热点话题，那么我们不经想问，GNN模型到底有多强呢？之前的文章我们介绍了用来衡量GNN表达能力的算法—Weisfeiler-Leman，这篇文章我们将以该算法为基础，向大家介绍ICLR 2019的oral论文 How powerful are graph neural networks 。 图神经网络 Graph Neural Network GNNs 利用图结构和节点初始特征$X_0$学习图节点的表示(embeddings)$h_v$，或者整个图的表示$h_G$。前面我们介绍了GCN、GraphSAGE和GAT这三种模型，图内节点都是通过各种聚合邻居的策略(neighborhood aggregation strategy)迭代式更新的，在$K$步迭代之后，每个节点的Embedding都融合了它$k$ hop所有邻居的信息。转化成数学形式： \[a_{v}^{(k)}=\text { AGGREGATE }^{(k)}\left(\left\{h_{u}^{(k-1)} : u \in \mathcal{N}(v)\right\}\right)\] \[h_{v}^{(k)}=\operatorname{COMBINE}^{(k)}\left(h_{v}^{(k-1)}, a_{v}^{(k)}\right)\] $h_v^{(k)}$ 即为$k$步迭代之后节点$v$的Embedding。\(\text{AGGREGATE}\) 以及 \(\text{COMBINE}\) 的不同区分了不同的图神经网络模型。比如在使用max-pooling聚合的GraphSAGE模型中，\(\text{AGGREGATE}\) 具有如下形式： \[a_{v}^{(k)}=\operatorname{MAX}\left(\left\{\operatorname{ReL} \mathrm{U}\left(W \cdot h_{u}^{(k-1)}\right), \forall u \in \mathcal{N}(v)\right\}\right)\] \(\text{COMBINE}\) 的形式为 \[h_{v}^{(k)}=\operatorname{CONCAT}\left(h_{v}^{(k-1)}, a_{v}^{(k)}\right)\] 而在GCN中，\(\text{AGGREGATE}\) 和 \(\text{COMBINE}\) 被组合到了一起： \[h_{v}^{(k)}=\operatorname{ReLU}\left(W \cdot \operatorname{MEAN}\left\{h_{u}^{(k-1)}, \forall u \in \mathcal{N}(v) \cup\{v\}\right\}\right)\] 具体细节可以参考我们之前的博文GCN GraphSAGE，在此不在赘述。 对于节点分类的问题，学习到$h_v^{(k)}$就可以接到下游的分类器了。而另一种图机器学习任务，即对于整个图的分类，我们需要融合节点Embedding以表示出整个图的Embedding，再接到下游分类器。具体来说，通过设计一个读出器函数\(\text{READOUT}\), 我们聚合节点Embedding而求出整个图的Embedding： \[h_{G}=\operatorname{READOUT}\left(\left\{h_{v}^{(N)} \vert v \in G\right\}\right)\] 理论分析 对于GNN中的每个节点来说，他们都是通过递归的融合邻居信息来捕获图的结构信息和邻居特征信息，因此每个节点的更新路径是一个树结构，节点位于树根，从叶子节点逐层向上更新Embedding直到根节点。比如在下图中，对于一个两层的图神经网络，节点$B$ 的更新路径是一个高为2的数，$B$ 位于树根。更新方向如下箭头方向所示： 为了分析GNN的表示能力，我们转而研究什么样的GNN结构能够保证将两个不同的节点投影到不同的Embedding空间中(即为它们生成不同的Embedding)。直觉上可知，最强大的GNN结构仅会将拥有完全相同子树结构的两个节点投影到相同的Embedding空间(即这两个节点的Embedding相同，他们的邻居的Embedding相同，数量也相同)。因为子树结构可以通过节点递归得定义得到(如上图中$B$的二度子树结构可以定义为$B$的一度子树结构以及一度子树$(A, C, E)$的1度子树结构)，因此我们的分析可以简化为”最强大的GNN结构仅会将拥有完全相同1度邻域的两个节点投影到相同的Embedding空间”。这里1度领域不仅包括节点的1度邻居，也包括节点自身。即为以节点为根，高为1的子树结构。 自此，我们的分析框架就建立了，那么下一步是将节点的一度邻居表示出来，由于图上节点邻居没有相对的次序性，因此MultiSet这个数据结构最适合表示这样的领域。Multiset和set不同，Multiset允许集合中的元素出现多次。Multiset是一个2元组 $X=(S, m)$, 其中$S$是集合中的元素，$m$表示该元素出现的次数。 在此做一个小结，GNN的表示能力可以这样分析：最强大的GNN能够将两个节点投影到不同的Embedding空间(为这两个节点生成不同的Embedding向量)，除非这两个节点Embedding和1度邻居完全相同。节点的1度邻居可以用Multiset表示，因此最强大的GNN一定能够将两个不同的Multiset映射到不同的Embedding空间。所以最终问题就转化成了设计这样的GNN函数，使得 \(GNN(v_1, Multiset_1) = GNN(v_2, Multiset_2)\) 成立当且仅当 $h_1 = h_2$ and $Multiset_1 = Multiset_2$，$h$ 为节点$v$的Embedding表示，multiset中包含节点的1度邻居信息，包括邻居的数量和Embedding。 GIN 和Weisfeiler-Leman算法一样强大 有了这些理论分析后，要想设计一个强大的GNN模型，我们要做到是设计\(\text{AGGREGATE}\)、\(\text{COMBINE}\) 以及 \(\text{READOUT}\) 函数使得经过这三个函数映射后不同的multiset能够保持不同，即这些函数都是单射函数(injective function)。为此，作者证明了几个定理： 首先是为设计单射性质的 \(\text{AGGREGATE}\) 和 \(\text{COMBINE}\) 而证明的： **定理5. ** 存在函数 $f: \mathcal{X}\rightarrow\mathbb{R}^n$ 使得 $h(X)=\sum_{x \in X} f(x)$ 对于每个multiset $X\in \mathcal{X}$ 是不同的。再者，任何作用于multiset上的函数 $g$ 能够被如下的形式分解: $g(X)=\phi\left(\sum_{x \in X} f(x)\right)$ 具体证明详见论文，在这里通俗的说下这个定理的目的是什么。这个定理说，对于任意multiset $X$, 都有一个对应的单射函数$g(X)$，这就意味着如果节点的邻居有差别，那么我们可以通过 $g(X)$ 这个函数将它们区分出来，回忆在Weisfeiler-Leman算法的例子中，函数$g$ 将邻居Embedding排序之后拼起来，虽然简单，但是满足单射的要求。 上文中，我们将节点自身归到节点的1度领域中，GCN和GAT中就是这样表示的。在GraphSAGE中，节点自身和其1度邻居是分开对待的，为此，作者证明了如下引理： 引理6. 存在函数 \(f:\mathcal{X}\rightarrow\mathbb{R}^n\) 使得存在数 \(\epsilon\)，函数\(h(c, X) = (1 + \epsilon)\cdot f(c) + \sum_{x\in X} f(x)\) 对于每一对 $(c, X)$ 是单射的，其中 $c\in \mathcal{X}$ 和 $X\subset \mathcal{X}$ 是一个有限的multiset，再者，任何作用于$(c, X)$ 这样的multiset对的函数 $g$ 能够被分解成 $g(c, X)=\phi\left((1+\epsilon) \cdot f(c)+\sum_{x \in X} f(x)\right)$ 这样的形式，其中 \(\phi\) 是一个函数。 通俗来说，对于任意multiset对$(c, X)$，都有一个对应的单射函数 $g(c, X)$，这里如果把 $c$ 看成节点自身，$X$ 看成是节点的1度领域，那么意味着如果节点自身或者其邻居有差别，我们可以通过 $g(c, X)$ 把它们区分出来，回忆在Weisfeiler-Leman算法的例子中，函数 $g$ 的定义是，将节点自身和其1度邻居的拼接向量再拼接起来并用”,”分开，同样简单有效。 引理6 从数学的角度上证明了如果GNN能够学习到$g(c, X)$，GNN能够达到Weisfeiler-Leman的分类能力。那么我们面临的问题是如何设计GNN的结构以学习到这样的$g(c, X)$，观察$g(c, X)$ 的形式可知，它主要是将复合函数$f\circ g$ 作用在节点Embedding上，而根据神经网络的通用近似理论，我们可以将复合函数 $f\circ \phi$ 建模为一个多层感知机MLP，综合以上推导，作者得出了一个和Weisfeiler-Leman具有相同能力的GNN模型，叫做 GIN (Graph Isomorphism Network)： \(h_{v}^{(k)}=\operatorname{MLP}^{(k)}\left(\left(1+\epsilon^{(k)}\right) \cdot h_{v}^{(k-1)}+\sum_{u \in \mathcal{N}(v)} h_{u}^{(k-1)}\right)\) 小结一下：这个式子设计了合理的 \(\text{AGGREGATE}\) 和 \(\text{COMBINE}\) ，使他们成为单射函数，对于图中节点的分类任务，这个式子已经和 Weisfeiler-Leman 算法能力一致了，对于图的分类任务，我们还要设计 \(\text{READOUT}\) 单射函数，使得所有的节点Embedding综合而成的图Embedding仍然是唯一的。\(\text{READOUT}\) 函数的设计博文中不再赘述，详情见论文。 GIN模型和其他图模型的比较 上面我们介绍了这么多，得出了GIN的逐层embedding更新公式，我们说他和Weisfeiler-Leman的能力相同，然而之前提到的图模型比如GraphSAGE和GCN却没有这样强大的能力，下面我们从他们的逐层更新公式上举例子来看看原因：它们的逐层更新公式区别主要有三点 GIN 采用了MLP来对节点聚合后的Embedding做非线性隐射，而GCN和GraphSAGE采用的单层感知机的结构(一个权重矩阵$W$加上非线性激活函数\(\sigma\))。然而只有MLP才具有通用近似的能力(MLP 能够拟合 $f\circ g$)，根据上面的理论分析，使用单层感知机的结构会导致逐层更新公式不再是单射函数，导致对于不同的节点和邻居，模型有分辨不出来的风险； GIN 使用 $(1+\epsilon)$ 对自身的Embedding进行了加权处理，通过引入对自身Embedding的扰动避免了自身节点Embedding $a$和邻居聚合后Embedding $b$加和相同的情况下， 如果$a, b$ 互换，那么无法分辨的问题。 GIN 使用加和(sum)作为邻居Embedding的聚合方式，sum 比 mean 或者 pooling 的方式更强，论文中举了几个例子帮助理解 假设我们关注的都是位于图中间蓝色节点一次更新后的Embedding，因为该节点的邻居个数在左右图不同 ，因此节点Embedding更新后的结果应当要有差异。对(a)，通过mean或者max后的邻居信息均是1个蓝色节点代表的Embedding，不能分辨邻居有差异；对(b)，max聚合后的邻居信息是绿色和红色节点Embedding的最大值，不能分辨邻居有差异；对(c)，mean聚合后是绿色和红色节点代表Embedding的均值，不能分辨邻居有差异；max聚合后是绿色和红色节点代表Embedding的最大值，同样不能分辨邻居差异。 而如果采用sum的方式，以上3中情况邻居聚合后的差异都能分辨出来，因此sum 比 mean 或者 max-pooling 的表达能力更强，至少在图同构测试或者节点分类的任务上。 关于表达能力的讨论 谈到这里，有些对图神经网络比较熟悉的读者可能有疑惑了，在GraphSAGE的论文中，作者比较了各种聚合方式(mean, max-pooling等)，实验结论是mean聚合方式在在实验结果上比较好啊，这不是与该篇论文的分析相悖吗？确实，笔者在多种数据集的实验中也发现mean聚合的效果比较好。作者在接下来的几个小节中解释了可能的原因。 mean 学习邻居Embedding分布 考察这样两个multiset $X_1=(S, m)$， $X_2=(S, k\cdot m)$，即， $X_2$ 包含 multiset $X_1$ 中的所有元素，且每个元素的数量是 $X_1$ 的 $k$ 倍，那么mean aggregator是无法区分他们的，因此我们可以说，mean aggregator可以用来分辨multiset中元素的分布(比率)，而不是multiset。 因此 mean aggreator 在特定的任务可能表现得更好，比如在图上统计或者分布信息比特征信息更重要的时候。或者，考虑另一种情况，当节点Embedding极少重复的话，mean aggregator和sum aggregator的表达能力基本是一致的，在很多machine learning的任务中，由于节点的Embedding基本都存在差异，并且mean aggregator能够保持聚合后的每一维的scale和聚合前一致，因此在任务中可能会更好。 max-pooling 学习具有表示性的Embedding 考察这样两个multiset $X_1=(S_1, m_1)$， $X_2 = (S_2, m_2)$ 其中$max(S_1) = max(S_2)$，即$X_1$中最大的元素和$X_2$ 相同，那么max-pooling aggregator 是无法区分他们的，因此我们可以说，max-pooling aggregator 可以用来分辨multiset具有代表性的元素或者multiset的“骨架”(skeleton)。在PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation中，作者阐述了max-pooling aggreator 可以用来分辨3D 点云的“骨架”，对噪音和离群点具有鲁棒性。max-pooling aggreator 可能在这类的任务中表现得更好。 后记 这篇博文重点介绍了GNN的表示能力，简单来说，可以通过设计特殊GNN的架构使得其达到Weisfeiler-Leman算法在图同构分类中的效果。到目前为止，我们介绍了图神经网络的三个基本模型，GCN、GraphSAGE、GAT；介绍了图卷积神经网络和谱图卷积的关系；介绍了图神经网络模型的表达能力；至此，图神经网络的理论部分暂时告一段落，接下来的博文将会向大家介绍目前图神经网络存在的框架，使大家对图神经网络编程有所了解，毕竟纸上谈兵终非长远之策。</summary></entry><entry><title type="html">GNN 教程：Weisfeiler-Leman 算法</title><link href="https://archwalker.github.io/blog/2019/06/22/GNN-Theory-WL.html" rel="alternate" type="text/html" title="GNN 教程：Weisfeiler-Leman 算法" /><published>2019-06-22T11:00:00+08:00</published><updated>2019-06-22T11:00:00+08:00</updated><id>https://archwalker.github.io/blog/2019/06/22/GNN-Theory-WL</id><content type="html" xml:base="https://archwalker.github.io/blog/2019/06/22/GNN-Theory-WL.html">&lt;h2 id=&quot;一引言&quot;&gt;一、引言&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;此为原创文章，未经许可，禁止转载&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;前面的文章中，我们介绍了GNN的三个基本模型GCN、GraphSAGE、GAT，分析了经典的GCN逐层传播公式是如何由谱图卷积推导而来的。GNN模型现在正处于学术研究的热点话题，那么我们不经想问，GNN模型到底有多强呢？&lt;/p&gt;

&lt;p&gt;我们的目的是分析GNN的表达能力，我们需要一个模型作为衡量标准。比如说如果我们想衡量GBDT的分类能力的话，通常情况下我们会使用同样的数据集，采用不同的分类模型如LR, RF, SVM等做对比。对于GNN模型，我们采用的对比模型叫做Weisfeiler-Leman，其常被用做图同构测试(Graph Isomorphism Test)，图同构测试即给定两个图，返回他们的拓扑结构是否相同。图同构问题是一个非常难的问题，目前为止还没有多项式算法能够解决它，而Weisfeiler-Leman算法是一个多项式算法在大多数case上能够奏效，所以在这里我们用它来衡量GNN的表达能力，这篇博文详细介绍了Weisfeiler-Leman算法，作为我们分析GNN表达能力的基础。&lt;/p&gt;

&lt;h2 id=&quot;二weisfeiler-leman-算法介绍&quot;&gt;二、Weisfeiler-Leman 算法介绍&lt;/h2&gt;

&lt;h3 id=&quot;21-动机&quot;&gt;2.1 动机&lt;/h3&gt;

&lt;p&gt;Graph 的相似性问题是指判断给定两个 Graph 是否同构。如果两个图中对应节点的特征信息（attribute）和结构信息（structure）都相同，则称这两个图同构。因此我们需要一种高效的计算方法能够将的特征信息及结构位置信息(邻居信息)隐射到一个数值，我们称这个数值为节点的ID(Identification)。最后，两个图的相似度问题可以转化为两个图节点集合ID的 Jaccard 相似度问题。&lt;/p&gt;

&lt;h3 id=&quot;22-weisfeiler-leman-算法思路&quot;&gt;2.2 Weisfeiler-Leman 算法思路&lt;/h3&gt;

&lt;p&gt;一般地，图中的每个节点都具有特征（attribute）和结构（structure）两种信息，需要从这两方面入手，来计算几点ID。很自然地，特征信息（attribute）即节点自带的Embedding，而结构信息可以通过节点的邻居来刻画，举个例子，如果两个节点Embedding相同，并且他们连接了Embedding完全相同的邻居，我们是无法区分这两个节点的，因此这两个节点ID相同。由此，可以想到，我们可以通过 hashing 来高效判断是否两个节点ID一致。1维的Weisfeiler-Lehman正是这样做的。如果设 $h_i$ 表示节点 $v_i$ 的特征信息（attribute），那么 Weisfeiler-Leman 算法的更新函数可表示为：&lt;/p&gt;

\[h_l^{(t)}(v)=\operatorname{HASH}\left(h_{l}^{(t-1)}(v), \mathcal{F}\left\{ h_l^{(t-1)}(u) | u \in N(v)\right\}\right)\]

&lt;p&gt;在上式中，\(\mathcal{F}\)表示邻居Embedding的聚合函数，可以简单的将邻居Embedding排序后拼接起来(concatenate)。看到这里，有的读者可能产生了疑问，这个式子不是和之前GraphSAEG的跟新公式一样吗，那是不是意味着GraphSAGE具有和Weisfeiler-Leman算法相同的能力？确实这个式子在GraphSAGE中\(\mathcal{F}\)表示邻居节点的聚合(比如求和、Pooling等方式)，而$\text{HASH}$在GraphSAGE中是一个单层的感知机。这些差别实际上导致了GraphSAGE并没有完全的Weisfeiler-Leman算法的能力，在后一篇博文中我们会详细说明它。&lt;/p&gt;

&lt;p&gt;下面我们通过一个形象的例子来说明Weisfeiler-Leman算法具体是如何操作的。&lt;/p&gt;

&lt;h3 id=&quot;23-weisfeiler-leman-算法图形举例说明&quot;&gt;2.3 Weisfeiler-Leman 算法图形举例说明&lt;/h3&gt;

&lt;p&gt;给定两个图$G$和$G’$，其中每个节点的Embedding为这个节点的标签（实际应用中，有些时候我们并拿不到节点的标签，这时可以对节点都标上一个相同的标签如”1”，这个时候我们将完全用节点位于图中的结构信息来区分节点，因为他们的Embedding都相同）&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tva1.sinaimg.cn/large/007S8ZIlly1gdvfcr0tnpj317a0pwgpl.jpg&quot; alt=&quot;Screen Shot 2020-04-16 at 10.37.05&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;给定图 $G$ 和 $G’$&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;如何比较 $G$ 和 $G’$的相似性问题呢？Weisfeiler-lehman 算法的思路如下：&lt;/p&gt;

&lt;p&gt;step 1、对邻居节点标签信息进行聚合，以获得一个带标签的字符串（整理默认采用升序排序的方法进行排序）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tva1.sinaimg.cn/large/007S8ZIlly1gdvfcrlw6dj31js0pugrv.jpg&quot; alt=&quot;Screen Shot 2020-04-16 at 10.38.27&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;第一步的结果，这里需要注意，图中利用逗号将两部分进行分开，第一部分是该节点的ID，第二部分是该节点的邻居节点ID按升序排序的结构（eg：对于节点 5，他的邻居节点为2，3，4，所以他的结果为”5,234”）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;step 2、为了能够生成一个一一对应的字典，我们将每个节点的字符串hash处理后得到节点的新ID。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tva1.sinaimg.cn/large/007S8ZIlly1gdvfcza5d4j317e0pw0vt.jpg&quot; alt=&quot;Screen Shot 2020-04-16 at 10.39.07&quot; /&gt;&lt;/p&gt;

&lt;p&gt;step 3、将哈希处理过的ID重新赋值给相应的结点，以完成第一次迭代。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tva1.sinaimg.cn/large/007S8ZIlly1gdvfd371d3j318o0pu78s.jpg&quot; alt=&quot;Screen Shot 2020-04-16 at 10.39.30&quot; /&gt;&lt;/p&gt;

&lt;p&gt;第一次迭代的结果为：$G={6、6、8、10、11、13}，G’={6，7，9，10，12，13}$。这样即可以获得图中每个节点ID。接下去，可以采用 Jaccard 公式计算$G$ 和 $G’$的相似度。如果两个图同构的话，在迭代过程中$G$和$G’$将会相同。&lt;/p&gt;

&lt;p&gt;至此Weisfeiler-Leman算法就介绍完了，作为下一篇博文的引文，我们简要得分析一下Weisfeiler-Leman算法和GCN逐层更新公式的关系。&lt;/p&gt;

&lt;h2 id=&quot;三weisfeiler-leman-算法与-gcn-间的转换&quot;&gt;三、Weisfeiler-Leman 算法与 GCN 间的转换&lt;/h2&gt;

&lt;p&gt;GCN逐层更新公式为：&lt;/p&gt;

\[h_{i}^{(l+1)}=\sigma\left(\sum_{j \in \mathcal{N}_{i}} \frac{1}{c_{i j}} h_{j}^{(l)} W^{(l)}\right)\]

&lt;p&gt;简单来说，GCN的逐层更新公式对Weisfeiler-Leman算法做了两点近似：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;用单层感知机近似\(\text{HASH}\)函数，上式中\(\sigma, W^{(l)}\)即为单层感知机模型&lt;/li&gt;
  &lt;li&gt;用加权平均替代邻居信息拼接，上式中\(\frac{1}{c_{i,j}}\)表示节点$v_j$的Embedding聚合到节点$v_i$时需要进行的归一化因子&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过与 Weisfeiler-Lehman 算法的类比，我们可以理解即使是具有随机权重的未经训练的 GCN 模型也可以看做是图中节点的强大特征提取器。&lt;/p&gt;

&lt;h2 id=&quot;四后话&quot;&gt;四、后话&lt;/h2&gt;

&lt;p&gt;即使GCN、GraphSAGE、GAT和Weifeiler-Leman算法如此之像，但正如我们分析的那样，他们都做了一些近似，将$\text{HASH}$近似为单层感知机会导致一部分的精度损失，因为单层感知机不是单射函数。拼接邻居方式的近似引入了另一层精度损失，因为比如求和，pooling等邻居聚合方式可能作用于不同的邻居集合下而得到相同的结果，所以不管是哪个模型，都没有达到目前Weisfeiler-Leman算法在图同构问题上的能力。在下一篇博文中我们将会详细分析这些近似方法带来的损失，并给出如何解决这些问题。&lt;/p&gt;

&lt;h2 id=&quot;参考资料&quot;&gt;参考资料&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;http://arxiv.org/abs/1609.02907&quot;&gt;SEMI-SUPERVISED CLASSIFICATION WITH GRAPH CONVOLUTIONAL NETWORKS&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://pdfs.semanticscholar.org/7e18/74986cf6433fabf96fff93ef42b60bdc49f8.pdf?_ga=2.51335209.1276923626.1587004438-1644601444.1584359006&quot;&gt;Weisfeiler-Lehman Graph Kernels&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzI2MDE5MTQxNg==&amp;amp;mid=2649687879&amp;amp;idx=1&amp;amp;sn=5b622fae52428b65c45e2d8433222723&quot;&gt;《Graph learning》 图传播算法（下）&lt;/a&gt;&lt;/p&gt;</content><author><name>ArchWalker</name><email>csarchwalker@gmail.com</email></author><category term="blog" /><category term="GNN" /><summary type="html">一、引言 此为原创文章，未经许可，禁止转载 前面的文章中，我们介绍了GNN的三个基本模型GCN、GraphSAGE、GAT，分析了经典的GCN逐层传播公式是如何由谱图卷积推导而来的。GNN模型现在正处于学术研究的热点话题，那么我们不经想问，GNN模型到底有多强呢？ 我们的目的是分析GNN的表达能力，我们需要一个模型作为衡量标准。比如说如果我们想衡量GBDT的分类能力的话，通常情况下我们会使用同样的数据集，采用不同的分类模型如LR, RF, SVM等做对比。对于GNN模型，我们采用的对比模型叫做Weisfeiler-Leman，其常被用做图同构测试(Graph Isomorphism Test)，图同构测试即给定两个图，返回他们的拓扑结构是否相同。图同构问题是一个非常难的问题，目前为止还没有多项式算法能够解决它，而Weisfeiler-Leman算法是一个多项式算法在大多数case上能够奏效，所以在这里我们用它来衡量GNN的表达能力，这篇博文详细介绍了Weisfeiler-Leman算法，作为我们分析GNN表达能力的基础。 二、Weisfeiler-Leman 算法介绍 2.1 动机 Graph 的相似性问题是指判断给定两个 Graph 是否同构。如果两个图中对应节点的特征信息（attribute）和结构信息（structure）都相同，则称这两个图同构。因此我们需要一种高效的计算方法能够将的特征信息及结构位置信息(邻居信息)隐射到一个数值，我们称这个数值为节点的ID(Identification)。最后，两个图的相似度问题可以转化为两个图节点集合ID的 Jaccard 相似度问题。 2.2 Weisfeiler-Leman 算法思路 一般地，图中的每个节点都具有特征（attribute）和结构（structure）两种信息，需要从这两方面入手，来计算几点ID。很自然地，特征信息（attribute）即节点自带的Embedding，而结构信息可以通过节点的邻居来刻画，举个例子，如果两个节点Embedding相同，并且他们连接了Embedding完全相同的邻居，我们是无法区分这两个节点的，因此这两个节点ID相同。由此，可以想到，我们可以通过 hashing 来高效判断是否两个节点ID一致。1维的Weisfeiler-Lehman正是这样做的。如果设 $h_i$ 表示节点 $v_i$ 的特征信息（attribute），那么 Weisfeiler-Leman 算法的更新函数可表示为： \[h_l^{(t)}(v)=\operatorname{HASH}\left(h_{l}^{(t-1)}(v), \mathcal{F}\left\{ h_l^{(t-1)}(u) | u \in N(v)\right\}\right)\] 在上式中，\(\mathcal{F}\)表示邻居Embedding的聚合函数，可以简单的将邻居Embedding排序后拼接起来(concatenate)。看到这里，有的读者可能产生了疑问，这个式子不是和之前GraphSAEG的跟新公式一样吗，那是不是意味着GraphSAGE具有和Weisfeiler-Leman算法相同的能力？确实这个式子在GraphSAGE中\(\mathcal{F}\)表示邻居节点的聚合(比如求和、Pooling等方式)，而$\text{HASH}$在GraphSAGE中是一个单层的感知机。这些差别实际上导致了GraphSAGE并没有完全的Weisfeiler-Leman算法的能力，在后一篇博文中我们会详细说明它。 下面我们通过一个形象的例子来说明Weisfeiler-Leman算法具体是如何操作的。 2.3 Weisfeiler-Leman 算法图形举例说明 给定两个图$G$和$G’$，其中每个节点的Embedding为这个节点的标签（实际应用中，有些时候我们并拿不到节点的标签，这时可以对节点都标上一个相同的标签如”1”，这个时候我们将完全用节点位于图中的结构信息来区分节点，因为他们的Embedding都相同） 给定图 $G$ 和 $G’$ 如何比较 $G$ 和 $G’$的相似性问题呢？Weisfeiler-lehman 算法的思路如下： step 1、对邻居节点标签信息进行聚合，以获得一个带标签的字符串（整理默认采用升序排序的方法进行排序）。 第一步的结果，这里需要注意，图中利用逗号将两部分进行分开，第一部分是该节点的ID，第二部分是该节点的邻居节点ID按升序排序的结构（eg：对于节点 5，他的邻居节点为2，3，4，所以他的结果为”5,234”） step 2、为了能够生成一个一一对应的字典，我们将每个节点的字符串hash处理后得到节点的新ID。 step 3、将哈希处理过的ID重新赋值给相应的结点，以完成第一次迭代。 第一次迭代的结果为：$G={6、6、8、10、11、13}，G’={6，7，9，10，12，13}$。这样即可以获得图中每个节点ID。接下去，可以采用 Jaccard 公式计算$G$ 和 $G’$的相似度。如果两个图同构的话，在迭代过程中$G$和$G’$将会相同。 至此Weisfeiler-Leman算法就介绍完了，作为下一篇博文的引文，我们简要得分析一下Weisfeiler-Leman算法和GCN逐层更新公式的关系。 三、Weisfeiler-Leman 算法与 GCN 间的转换 GCN逐层更新公式为： \[h_{i}^{(l+1)}=\sigma\left(\sum_{j \in \mathcal{N}_{i}} \frac{1}{c_{i j}} h_{j}^{(l)} W^{(l)}\right)\] 简单来说，GCN的逐层更新公式对Weisfeiler-Leman算法做了两点近似： 用单层感知机近似\(\text{HASH}\)函数，上式中\(\sigma, W^{(l)}\)即为单层感知机模型 用加权平均替代邻居信息拼接，上式中\(\frac{1}{c_{i,j}}\)表示节点$v_j$的Embedding聚合到节点$v_i$时需要进行的归一化因子 通过与 Weisfeiler-Lehman 算法的类比，我们可以理解即使是具有随机权重的未经训练的 GCN 模型也可以看做是图中节点的强大特征提取器。 四、后话 即使GCN、GraphSAGE、GAT和Weifeiler-Leman算法如此之像，但正如我们分析的那样，他们都做了一些近似，将$\text{HASH}$近似为单层感知机会导致一部分的精度损失，因为单层感知机不是单射函数。拼接邻居方式的近似引入了另一层精度损失，因为比如求和，pooling等邻居聚合方式可能作用于不同的邻居集合下而得到相同的结果，所以不管是哪个模型，都没有达到目前Weisfeiler-Leman算法在图同构问题上的能力。在下一篇博文中我们将会详细分析这些近似方法带来的损失，并给出如何解决这些问题。 参考资料 SEMI-SUPERVISED CLASSIFICATION WITH GRAPH CONVOLUTIONAL NETWORKS Weisfeiler-Lehman Graph Kernels 《Graph learning》 图传播算法（下）</summary></entry></feed>