PageRank算法简单实现
PageRank with DGL Message Passing
- 使用DGL实现PageRank算法
PageRank算法
PageRank让链接来"投票"
一个页面的“得票数”由所有链向它的页面的重要性来决定,到一个页面的超链接相当于对该页投一票。一个页面的PageRank是由所有链向它的页面(“链入页面”)的重要性经过递归算法得到的。一个有较多链入的页面会有较高的等级,相反如果一个页面没有任何链入页面,那么它没有等级。
假设一个由4个页面组成的小团体:A,B,C和D。如果所有页面都链向A,那么A的PR(PageRank)值将是B,C及D的Pagerank总和。
继续假设B也有链接到C,并且D也有链接到包括A的3个页面。一个页面不能投票2次。所以B给每个页面半票。以同样的逻辑,D投出的票只有三分之一算到了A的PageRank上。
由于“没有向外链接的页面”传递出去的PageRank会是0,所以,给每个页面一个最小值:
- is the number of nodes in the graph.
- is the out-degree of a node .
- is the neighbornodes.
- is damping factor.
简单实现
- 创建一个包含有100个节点(网页)的网络,use networkx
- erdos_renyi_graph(),return a random graph
- Parameters:
- n (int) – The number of nodes.
- p (float) – Probability for edge creation.
- seed (integer, random_state, or None (default)) – Indicator of random number generation state.
- directed (bool, optional (default=False)) – If True, this function returns a directed graph.
import dgl
import torch
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
N = 50 # number of nodes
DAMP = 0.85 # damping factor,阻尼系数
K = 10 # number of iterations
g = nx.nx.erdos_renyi_graph(N, 0.1)
g = dgl.DGLGraph(g)
plt.figure(figsize=(15, 8))
nx.draw(g.to_networkx(), node_size=50, node_color=[[.5, .5, .5,]])
step 1
PageRank consists of two phases in a typical scatter-gather pattern.
- 初始化每个节点的PR值为 .
- 把每个节点的出度(out-degree)作为图中节点的特征.
# pr : init pagerank value
# od : out degree
g.ndata['pr'] = torch.ones(N) / N
g.ndata['od'] = g.out_degrees(g.nodes()).float()
# g.ndata['pr']
g.nodes[:].data['pr']
tensor([0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200,
0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200,
0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200,
0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200,
0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200, 0.0200,
0.0200, 0.0200, 0.0200, 0.0200, 0.0200])
# g.nodes[:].data['od']
g.ndata['od']
tensor([ 5., 4., 4., 6., 5., 5., 5., 9., 5., 8., 9., 10., 4., 4.,
1., 6., 6., 6., 4., 7., 5., 8., 7., 6., 1., 5., 5., 5.,
5., 6., 4., 7., 1., 7., 7., 2., 4., 5., 2., 6., 7., 6.,
6., 2., 3., 7., 2., 5., 5., 6.])
UDFs : works on a batch of edges, nodes
- message function : 计算每个节点传递给它的邻接节点的PR值(message)
- 传入参数为edges,deges{src, dst, data}
- edges.src[‘pr’] : 访问边src端节点的pr
- edges.src[‘od’] : 访问边src端节点的出度
- 传入参数为edges,deges{src, dst, data}
- reduce function : 通过一个节点收到的来自周围邻接几点发送的(message|pr),计算并更新自己的PageRank value.
- 传入参数为nodes,nodes {data, mailbox.data}
- nodes.mailbox[‘pr’] : 指向该节点的邻接节点传递给它的message(pr)
def pagerank_message_func(edges):
return {'pr' : edges.src['pr'] / edges.src['od']}
def pagerank_reduce_func(nodes):
msgs = torch.sum(nodes.mailbox['pr'], dim=1)
pv = (1 - DAMP) / N + DAMP * msgs
return {'pr' : pv}
## register
g.register_message_func(pagerank_message_func)
g.register_reduce_func(pagerank_reduce_func)
one PageRank iteration:
# 便利每条边还节点,进行信息的传递和pr的更新
def pagerank_naive(g):
# Phase #1: send out messages
for u, v in zip(*g.edges()):
g.send((u, v))
# Phase #2: compute, update new PageRank values.
for v in g.nodes():
g.recv(v)
批处理
compute on a batch of nodes or edges. For example
DGL如何并行处理多个边和节点:
- 由于每个节点的出入度不同,所以不能将不同长度的张量’对叠’在一起
- DGL按照每个node入度的数量分组,然后对每一组调用pagerank_reduce_func
def pagerank_batch(g):
g.send(g.edges())
g.recv(g.nodes())
higher level APIs
level-2 APIs 包含了所有的send
和recv
操作
def pagerank_level2(g):
g.update_all()
DGL也提供了内置函数
- dgl.function.copy_src(src, out) : an edge UDF
- dgl.function.sum(msg, out) : a node UDF
import dgl.function as fn
def pagerank_builtin(g):
g.ndata['pr'] = g.ndata['pr'] / g.ndata['od']
g.update_all(message_func=fn.copy_src(src='pr', out='m'),
reduce_func=fn.sum(msg='m',out='m_sum'))
g.ndata['pr'] = (1 - DAMP) / N + DAMP * g.ndata['m_sum']
如果使用了DLG的builtin function,这将会覆盖之前的操作,即注册的两个函数:
- pagerank_message_func
- pagerank_reduce_func
使用内置的函数,除了代码更清晰,还会更快,这是应为:DGL将融合copy_src消息函数和函数sum简化为一个稀疏矩阵向量(spMV)乘法.CPU可以为这种计算进行加速.
Using spMV for PageRank
- :第次迭代,所有nodes的PR值向量
- :图的稀疏邻接矩阵
for k in range(K):
# pagerank_naive(g)
# pagerank_batch(g)
# pagerank_level2(g)
pagerank_builtin(g)
print(g.ndata['pr'])
tensor([0.0107, 0.0221, 0.0219, 0.0181, 0.0269, 0.0255, 0.0267, 0.0193, 0.0179,
0.0150, 0.0176, 0.0068, 0.0179, 0.0143, 0.0142, 0.0191, 0.0330, 0.0277,
0.0393, 0.0153, 0.0266, 0.0327, 0.0185, 0.0207, 0.0177, 0.0178, 0.0255,
0.0315, 0.0177, 0.0219, 0.0357, 0.0281, 0.0251, 0.0214, 0.0188, 0.0142,
0.0144, 0.0075, 0.0106, 0.0139, 0.0183, 0.0226, 0.0210, 0.0115, 0.0181,
0.0223, 0.0070, 0.0171, 0.0213, 0.0109])