声明:本文写于2020-09-22,于2021-05-26修改。仅增加了一些模型细节和改了一些排版
概要
本文的内容主要是阅读Node2Vec论文《Node2Vec: Scalable Feature Learning for Networks》的阅读记录。
Node2vec的思想其实和DeepWalk差不太多,前者将随机游走的算法换成了深度优先(DFS)和广度优先(BFS)算法遍历图生成节点序列。如下图:
Node2Vec有以下几个步骤:
- 预处理计算出节点之间的转移概率(即从节点 u u u到节点 v v v的概率)
- 模拟随机游走
- 使用随机梯度下降(SGD)优化目标
以上三个步骤按顺序执行,而每个步骤中的运行可以并行计算。
Node2Vec模型
特征学习
node2vec将特征表示学习定义为一个最大似然优化问题,定义了一个目标优化函数: max f ∑ u ∈ V l o g P r ( N S ( u ) ∣ f ( u ) ) \max_f\sum_{u\in V}logPr(N_S(u)|f(u)) fmaxu∈V∑logPr(NS(u)∣f(u))其中 f f f为节点到特征表示的映射函数, u u u为节点, V V V为图节点集合, N S ( u ) N_S(u) NS(u)为利用采样策略 S S S从节点 u u u的邻接节点集合采样得到的节点集合。
为了简化优化问题,作了两个假设:
- 条件独立,在给定源节点的特征表示后,观察一个邻域节点的可能性独立于观察任何其他邻域节点。
- 特征空间对称性,源节点和近邻节点在特征空间中具有对称效应。
由上面的两个假设,目标优化函数可以等价为: max f ∑ u ∈ V [ ∑ n i ∈ N S ( u ) ( − l o g Z u ) + ∑ n i ∈ N S ( u ) f ( n i ) ⋅ f ( u ) ] \max_f\sum_{u\in V}\Big[\sum_{n_i \in N_S(u)}(-logZ_u)+\sum_{n_i\in N_S(u)}f(n_i)\space·f(u)\Big] fmaxu∈V∑[ni∈NS(u)∑(−logZu)+ni∈NS(u)∑f(ni) ⋅f(u)]其中 Z u = ∑ v ∈ V e x p ( f ( u ) ⋅ f ( v ) ) Z_u=\sum_{v\in V}exp(f(u)\space·f(v)) Zu=∑v∈Vexp(f(u) ⋅f(v))。
生成顶点序列
node2vec的特征学习是基于skip-gram模型的。而skip-gram模型一开始用来处理自然语言文本的,文本句子是线性的。在给定一段文本,某个词的上下文信息直接使用滑动窗口来获取。但是网络图是非线性的,需要为每个节点定义上下文信息的概念。这里采用的是随机过程采样。
随机过程采样的实现是运用在BFS和DFS的基础上设计有偏的随机游走的方式。假设源节点为
c
0
=
u
c_0=u
c0=u,模拟固定长度
l
l
l的随机游走的过程中,第
i
i
i个节点
c
i
c_i
ci的概率可以表示为:
π
v
x
π_{vx}
πvx为节点
v
v
v与
x
x
x之间的未归一化转移概率,
Z
Z
Z为归一化常数。
对于有偏变量
α
\alpha
α的计算,定义带有两个参数
p
p
p和
q
q
q的二阶随机游走。假目前一个随机游走走过边
(
t
,
v
)
(t,v)
(t,v),目前在节点
v
v
v出,现在需要考虑下一个节点往哪走,如图所示:
因此,需要评估随机游走的转移未归一化概率
π
v
x
π_{vx}
πvx(即从
v
v
v游走到下一个节点
x
x
x的概率)。定义
π
v
x
=
α
p
q
(
t
,
x
)
⋅
w
v
x
π_{vx} = \alpha_{pq}(t,x)\space·w_{vx}
πvx=αpq(t,x) ⋅wvx,其中
α
p
q
(
t
,
x
)
\alpha_{pq}(t,x)
αpq(t,x)为边
(
t
,
x
)
(t,x)
(t,x)的偏置项,
w
v
x
w_{vx}
wvx为边的权重,若没有权重则
w
v
x
=
1
w_{vx}=1
wvx=1。表达式如图:
d
t
x
d_{tx}
dtx表示
t
t
t到
x
x
x的最短距离。且取值范围为
0
,
1
,
2
{0,1,2}
0,1,2。从公式可以看出:
- 参数 p p p是控制下一步是否需要重新访问上一个节点。如果 p p p值设置的很大(大于 m a x ( q , 1 ) max(q,1) max(q,1)),则将基本上不会访问 t t t节点。相反,若 p p p值设置很小(小于 m i n ( q , 1 ) min(q,1) min(q,1)),则很大可能重新访问 t t t节点;
- 参数 q q q是控制下一步游走方向是靠近节点t的节点和是远离节点t的节点。如果 q q q设置为于1,则下一步将访问 x 1 x_1 x1,如果 q q q设置小于1,则下一步将访问 x 2 x_2 x2或 x 3 x_3 x3。也相当于控制随机游走的方式是按广度优先搜索还是深度优先搜索的方法进行。
采用随机游走的优点:随机游走在空间上和时间上的计算复杂度都非常高效;
最后,node2vec算法伪代码如下:
node2vec源码:
import numpy as np
import networkx as nx
import random
class Graph():
def __init__(self, nx_G, is_directed, p, q):
self.G = nx_G
self.is_directed = is_directed
self.p = p
self.q = q
def node2vec_walk(self, walk_length, start_node):
'''
Simulate a random walk starting from start node.
'''
G = self.G
alias_nodes = self.alias_nodes
alias_edges = self.alias_edges
walk = [start_node]
while len(walk) < walk_length:
cur = walk[-1]
cur_nbrs = sorted(G.neighbors(cur))
if len(cur_nbrs) > 0:
if len(walk) == 1:
walk.append(cur_nbrs[alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])])
else:
prev = walk[-2]
next = cur_nbrs[alias_draw(alias_edges[(prev, cur)][0],
alias_edges[(prev, cur)][1])]
walk.append(next)
else:
break
return walk
def simulate_walks(self, num_walks, walk_length):
'''
Repeatedly simulate random walks from each node.
'''
G = self.G
walks = []
nodes = list(G.nodes())
print 'Walk iteration:'
for walk_iter in range(num_walks):
print str(walk_iter+1), '/', str(num_walks)
random.shuffle(nodes)
for node in nodes:
walks.append(self.node2vec_walk(walk_length=walk_length, start_node=node))
return walks
def get_alias_edge(self, src, dst):
'''
Get the alias edge setup lists for a given edge.
'''
G = self.G
p = self.p
q = self.q
unnormalized_probs = []
for dst_nbr in sorted(G.neighbors(dst)):
if dst_nbr == src:
unnormalized_probs.append(G[dst][dst_nbr]['weight']/p)
elif G.has_edge(dst_nbr, src):
unnormalized_probs.append(G[dst][dst_nbr]['weight'])
else:
unnormalized_probs.append(G[dst][dst_nbr]['weight']/q)
norm_const = sum(unnormalized_probs)
normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs]
return alias_setup(normalized_probs)
def preprocess_transition_probs(self):
'''
Preprocessing of transition probabilities for guiding the random walks.
'''
G = self.G
is_directed = self.is_directed
alias_nodes = {}
for node in G.nodes():
unnormalized_probs = [G[node][nbr]['weight'] for nbr in sorted(G.neighbors(node))]
norm_const = sum(unnormalized_probs)
normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs]
alias_nodes[node] = alias_setup(normalized_probs)
alias_edges = {}
triads = {}
if is_directed:
for edge in G.edges():
alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
else:
for edge in G.edges():
alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
alias_edges[(edge[1], edge[0])] = self.get_alias_edge(edge[1], edge[0])
self.alias_nodes = alias_nodes
self.alias_edges = alias_edges
return
def alias_setup(probs):
'''
Compute utility lists for non-uniform sampling from discrete distributions.
Refer to https://hips.seas.harvard.edu/blog/2013/03/03/the-alias-method-efficient-sampling-with-many-discrete-outcomes/
for details
'''
K = len(probs)
q = np.zeros(K)
J = np.zeros(K, dtype=np.int)
smaller = []
larger = []
for kk, prob in enumerate(probs):
q[kk] = K*prob
if q[kk] < 1.0:
smaller.append(kk)
else:
larger.append(kk)
while len(smaller) > 0 and len(larger) > 0:
small = smaller.pop()
large = larger.pop()
J[small] = large
q[large] = q[large] + q[small] - 1.0
if q[large] < 1.0:
smaller.append(large)
else:
larger.append(large)
return J, q
def alias_draw(J, q):
'''
Draw sample from a non-uniform discrete distribution using alias sampling.
'''
K = len(J)
kk = int(np.floor(np.random.rand()*K))
if np.random.rand() < q[kk]:
return kk
else:
return J[kk]
同构性和同质性
在论文的实验部分,提出了图的同质性(homophily)和同构性(structural equivalence)。
同质性表示节点距离很接近的节点,如图1中的节点
u
u
u和节点
s
1
,
s
2
,
s
3
,
s
4
s_1,s_2,s_3,s_4
s1,s2,s3,s4为同质性节点。
同构性表示节点在图中的图结构相似的节点,如图1中的节点
u
u
u和节点
s
6
s_6
s6为同构性节点。以及节点结构相似生成节点序列的模型还有Struc2Vec模型
上面讲了,模型参数 p p p和 q q q可以控制随机游走方式,实验设置了 p = 1 , q = 0.5 p=1,q=0.5 p=1,q=0.5训练模型,得到的节点的向量表示更偏向节点的同质性。而当 q < 1 q<1 q<1相当于游走是按DFS的方式进行。设置 p = 1 , q = 2 p=1,q=2 p=1,q=2训练模型,得到的节点的向量表示更偏向节点的同构性,此时游走是按BFS的方式进行。
实验结果是DFS更能捕捉节点的同质性,而BFS更能捕捉节点同构性。(和本人一开始想的刚好相反)
总结
Node2Vec模型可以说是DeepWalk模型的改进版,通过超参 p , q p,q p,q控制有偏游走的方式,能更好的获取节点的在图中的结构信息。定义了特征学习的目标函数,使用随机梯度下降优化目标,得到最优的节点特征表示。
论文地址