Note:
Click here to download the full example code
Author: Minjie Wang, Quan Gan, Yu Gai, Zheng Zhang
在本教程中,您将学习如何在小图上将不同级别的PageRank消息传递API。 在DGL中,消息传递和特征转换是用户定义的函数(UDFs)。
在PageRank的每次迭代中,每个节点(网页)首先将其PageRank值均匀地分散到其下游节点。 每个节点的新PageRank值是通过汇总从其邻居收到的PageRank值来计算的,然后通过阻尼因子进行调整:
P V ( u ) = 1 − d N + d × ∑ v ∈ N ( u ) P V ( v ) D ( v ) PV(u) = \frac{1-d}{N} + d \times \sum_{v \in \mathcal{N}(u)} \frac{PV(v)}{D(v)} PV(u)=N1−d+d×v∈N(u)∑D(v)PV(v)
此处, N N N表示图中的节点数目, v v v是节点 u u u的邻近节点, D ( v ) D(v) D(v)是节点 v v v可向外传递信息的边的数目, N ( u ) \mathcal{N}(u) N(u)是节点 u u u的邻近节点的数目。
使用networkx创建一个拥有100个节点的图,并将其转换为DGLGraph。
import networkx as nx
import matplotlib.pyplot as plt
import torch
import dgl
N = 100 # 节点数目
DAMP = 0.85 # 阻尼因子
K = 10 # 迭代次数
g = nx.nx.erdos_renyi_graph(N, 0.1)
g = dgl.DGLGraph(g)
nx.draw(g.to_networkx(), node_size=50, node_color=[[.5, .5, .5,]])
plt.show()
在该算法中,PageRank由两个典型的散布聚集模式阶段组成。首先将每一个节点的PageRank值初始化为 1 N \frac{1}{N} N1,随后会将每一个节点的出度作为节点的一个特征。
g.ndata['pv'] = torch.ones(N) / N
g.ndata['deg'] = g.out_degrees(g.nodes()).float()
定义message 函数,该函数将每个节点的PageRank值除以其出度,然后将结果作为消息传递给其邻居。
def pagerank_message_func(edges):
return {'pv' : edges.src['pv'] / edges.src['deg']}
在DGL中,message 函数 被表现为 Edge UDFs.Edge UDFs 具有一个单个的argument:edges,edges拥有三个成员:src 、 dst和data。src用于获取边初始节点起点的特征,dst用于获取边终止节点的特征,data用于获取边的特征。此处,函数用于计算的信息仅仅来源于边的起点。
接下来我们将定义reduce函数,该函数使用它的mailbox功能删除并且汇总信息,以计算新的PageRank值。
def pagerank_reduce_func(nodes):
msgs = torch.sum(nodes.mailbox['pv'], dim=1)
pv = (1 - DAMP) / N + DAMP * msgs
return {'pv' : pv}
类似于message 函数是边的UDFs,reduce 函数是节点的UDFs.。节点UDFs 具有一个单个的argument:nodes,nodes拥有两个成员:data 和 mailbox。data中包含节点的特征,mailbox包含所有传递过来的信息特征,并且沿着第二维度(dim=1)堆叠,即计算加和。
message UDF作用在一批边上,而reduce UDF作用在一批边缘上,但输出在一批节点上。 它们之间的关系如下:
图片地址:https://i.imgur.com/kIMiuFb.png
构建message 函数和reduce 函数,稍后DGL将调用它。
g.register_message_func(pagerank_message_func)
g.register_reduce_func(pagerank_reduce_func)
该算法很简单。 这是一个PageRank迭代的代码。
def pagerank_naive(g):
# 第一部分: 发出所有边的信息.
for u, v in zip(*g.edges()):
g.send((u, v))
# 第二部分: 收到信息并且计算新的PageRank值.
for v in g.nodes():
g.recv(v)
zip(*a)的用法可见:https://blog.csdn.net/qq_42707449/article/details/81122741
之前的代码无法缩放到大图,因为它会遍历所有节点。 DGL通过允许您在一批节点或边上进行计算来解决此问题。 例如,以下代码一次触发消息并减少多个节点和边缘上的功能。
def pagerank_batch(g):
g.send(g.edges())
g.recv(g.nodes())
我们仍在使用之前的reduce函数pagerank_reduce_func,其中*nodes.mailbox [‘pv’]*是单个张量,将传入的消息沿第二维堆叠。
您可能想知道是否有可能在所有节点上并行执行reduce,因为每个节点可能有不同数量的传入消息,并且您无法真正将不同长度的张量真正“堆叠”在一起。 通常,DGL通过按传入消息的数量对节点进行分组并为每个组调用reduce函数来解决该问题。
DGL提供了许多例程,这些例程以各种方式组合了基本的send和recv。 这些例程称为2级API。 例如,下面的代码示例演示如何使用此类API进一步简化PageRank示例。
def pagerank_level2(g):
g.update_all()
除了update_all,您还可以在此2级类别中使用pull,push和send_and_recv。 有关更多信息,请参阅API参考。
一些消息和归约功能经常使用。 因此,DGL还提供了内置函数。 例如,在PageRank示例中可以使用两个内置函数。
以下PageRank示例显示了此类功能。
import dgl.function as fn
def pagerank_builtin(g):
g.ndata['pv'] = g.ndata['pv'] / g.ndata['deg']
g.update_all(message_func=fn.copy_src(src='pv', out='m'),
reduce_func=fn.sum(msg='m',out='m_sum'))
g.ndata['pv'] = (1 - DAMP) / N + DAMP * g.ndata['m_sum']
在前面的示例代码中,您直接将UDF提供给update_all作为其参数。 这将覆盖先前构建的UDF。
除了更简洁的代码外,使用内置函数还使DGL有机会将操作融合在一起。 这样可以加快执行速度。 例如,DGL将把copy_src消息函数和sumreduce函数融合为一个稀疏矩阵向量(spMV)乘法。
以下部分描述了为什么spMV可以加快PageRank中的分散收集阶段。 有关DGL中内置函数的更多详细信息,请参见API参考。
您还可以下载并运行不同的代码示例以查看不同之处。
for k in range(K):
# Uncomment the corresponding line to select different version.
# pagerank_naive(g)
# pagerank_batch(g)
# pagerank_level2(g)
pagerank_builtin(g)
print(g.ndata['pv'])
out:
tensor([0.0106, 0.0114, 0.0139, 0.0099, 0.0080, 0.0023, 0.0098, 0.0091, 0.0072,
0.0113, 0.0081, 0.0073, 0.0121, 0.0096, 0.0098, 0.0105, 0.0082, 0.0063,
0.0091, 0.0073, 0.0131, 0.0139, 0.0064, 0.0130, 0.0124, 0.0097, 0.0147,
0.0075, 0.0100, 0.0083, 0.0073, 0.0091, 0.0124, 0.0114, 0.0100, 0.0090,
0.0107, 0.0169, 0.0106, 0.0090, 0.0066, 0.0105, 0.0090, 0.0090, 0.0147,
0.0098, 0.0088, 0.0107, 0.0138, 0.0146, 0.0098, 0.0140, 0.0065, 0.0048,
0.0125, 0.0113, 0.0121, 0.0066, 0.0066, 0.0097, 0.0090, 0.0149, 0.0090,
0.0112, 0.0113, 0.0108, 0.0098, 0.0066, 0.0066, 0.0122, 0.0064, 0.0113,
0.0099, 0.0081, 0.0081, 0.0100, 0.0131, 0.0107, 0.0065, 0.0106, 0.0071,
0.0106, 0.0123, 0.0097, 0.0130, 0.0122, 0.0079, 0.0090, 0.0065, 0.0106,
0.0113, 0.0106, 0.0082, 0.0098, 0.0129, 0.0123, 0.0096, 0.0097, 0.0131,
0.0090])
使用内置函数,DGL可以理解UDF的语义。 这使您可以创建有效的实现。 例如,对于PageRank,一种加速它的常用方法是使用其线性代数形式。
R k = 1 − d N 1 + d A ∗ R k − 1 \mathbf{R}^{k} = \frac{1-d}{N} \mathbf{1} + d \mathbf{A}*\mathbf{R}^{k-1} Rk=N1−d1+dA∗Rk−1
这里, R k \mathbf{R}^k Rk是迭代 k k k时所有节点的PageRank值的向量; A A A是图的稀疏邻接矩阵。 计算该方程式非常有效,因为存在用于稀疏矩阵矢量乘法(spMV)的高效GPU内核。 DGL通过内置函数检测这种优化是否可用。 如果可以将某种内置组合组合映射到spMV内核(例如PageRank示例),则DGL自动使用它。 我们建议尽可能使用内置函数。
Total running time of the script: ( 0 minutes 2.224 seconds)
下载代码::3_pagerank.py
下载代码::3_pagerank.ipynb