1 概述
LINE是2015年微软发表的一篇论文,其全称为: Large-scale Information Network Embedding。论文下载地址:https://arxiv.org/pdf/1503.03578.pdf
LINE是一种基于graph产生embedding的方法,它可以适用于任何类型的graph,如无向图、有向图、加权图等,同时作者基于边采样进行了目标函数的优化,使算法既能捕获到局部的网络结构,也能捕获到全局的网络结构。
2 算法原理
2.1 新的相似度定义
该算法同时优化了节点的相似度计算方法,提出了一二阶相似度。
1、一阶相似度
一阶相似度用来描述的是两个顶点之间有一条边直接相连的情况,如果两个 u 、 v u、v u、v 之间存在直连变,则其一阶相似度可以用权重 w u v w_{uv} wuv来表示,如果不存在直连边,则一阶相似度为0。
上图中,顶点6、7之间是直接相连的,且权重比较大(边比较粗),则认为顶点6、7是相似的,且一阶相似度较高,顶点5、6之间并没有直接相连,则两者的一阶相似度为0。
2、二阶相似度
二阶相似度描述的是两个顶点之间没有直接相连,但是他们拥有相同的邻居。比如顶点 u 、 v u、v u、v 直接不存在直接相连,但是 顶点 u u u 存在其自己的一阶连接点, u u u 和 对应的一阶连接点 的一阶相似度可以形式化定义为: p ( u ) = ( w u , 1 , . . . , w u , ∣ V ∣ ) p(u) = (w_{u,1}, ..., w_{u, |V|}) p(u)=(wu,1,...,wu,∣V∣) ,同理可以得到顶点 v v v 和对应的一阶连接点的一阶相似度定义: p ( v ) = ( w v , 1 , . . . , w v , ∣ V ∣ ) p(v) = (w_{v,1}, ..., w_{v, |V|}) p(v)=(wv,1,...,wv,∣V∣) ,顶点 u 、 v u、v u、v 之间的相似度即为 p ( u ) 、 p ( v ) p(u)、p(v) p(u)、p(v) 之间的相似度。
上图中,顶点 5、6之间并没有直接相连,但是他们各自的一阶连接点是相同的,说明他们也是相似的。二阶相似度就是用来描述这种关系的。
2.2 优化目标
1、一阶相似度
对于每一条无向边
(
i
,
j
)
(i,j)
(i,j) ,定义顶点
v
i
,
v
j
v_i, v_j
vi,vj之间的联合概率为:
p
1
(
v
i
,
v
j
)
=
1
1
+
e
x
p
(
−
u
i
⃗
⋅
u
j
⃗
)
p_1(v_i,v_j) = \frac{1} {1+exp(- \vec{u_i} \cdot \vec{u_j})}
p1(vi,vj)=1+exp(−ui⋅uj)1
其中
u
i
⃗
∈
R
d
\vec{u_i} \in R^d
ui∈Rd 为顶点
v
i
v_i
vi 的低维向量表示(可以看作一个内积模型,计算两个item之间的匹配程度)。
同时定义经验分为:
p
^
1
(
i
,
j
)
=
w
i
,
j
W
\hat{p}_1(i,j) = \frac{w_{i,j}}{W}
p^1(i,j)=Wwi,j
其中
W
=
∑
i
,
j
∈
E
w
i
,
j
W = \sum_{i,j \in E} w_{i,j}
W=∑i,j∈Ewi,j。
为了计算一阶相似度,优化的目标函数为:
O
1
=
d
(
p
^
1
(
⋅
,
⋅
)
,
p
1
(
⋅
,
⋅
)
)
O_1 = d(\hat{p}_1(\cdot , \cdot), p_1(\cdot , \cdot))
O1=d(p^1(⋅,⋅),p1(⋅,⋅))
其中
d
(
⋅
,
⋅
)
d(\cdot , \cdot)
d(⋅,⋅) 是两个分布的距离,常用的衡量两个概率分布差异的指标为 KL 散度,使用 KL 散度并忽略常用项后有:
O
1
=
−
∑
(
i
,
j
)
∈
E
w
i
,
j
l
o
g
p
1
(
v
i
,
v
j
)
O_1 = - \sum_{(i,j) \in E} w_{i,j} log \, p_1 (v_i, v_j)
O1=−(i,j)∈E∑wi,jlogp1(vi,vj)
一阶相似度只能用于无向图中。
2、二阶相似度
和一阶相似度不同的是,二阶相似度既可以用于无向图,也可以用于有向图。二阶相似度计算的假设前提是:两个顶点共享其各自的一阶连接顶点,在这种情况下,顶点被看作是一种特定的「上下文」信息,因此每一个顶点都扮演了两个角色,即拥有两个embedding向量,一个是顶点本身的表示向量,一个是该点作为其他顶点的上下文顶点时的表示向量。
对于有向边
(
i
,
j
)
(i,j)
(i,j),定义给定顶点
v
i
v_i
vi 的条件下,产生上下文(邻居)顶点
v
j
v_j
vj 的概率为:
p
2
(
v
j
∣
v
i
)
=
e
x
p
(
u
j
⃗
T
⋅
u
i
⃗
)
∑
k
=
1
∣
V
∣
e
x
p
(
u
k
⃗
T
⋅
u
i
⃗
)
p_2(v_j|v_i) = \frac {exp(\vec{u_j}^T \cdot \vec{u_i})}{ \sum_{k=1}^{|V|} exp(\vec{u_k}^T \cdot \vec{u_i})}
p2(vj∣vi)=∑k=1∣V∣exp(ukT⋅ui)exp(ujT⋅ui)
其中
∣
V
∣
|V|
∣V∣ 为上下文顶点的个数。
二阶相似度定义的优化的目标函数为:
O
2
=
∑
i
∈
V
λ
i
d
(
p
^
2
(
⋅
∣
v
i
)
,
p
2
(
⋅
∣
v
i
)
)
O_2 = \sum_{i \in V} \lambda_i d(\hat{p}_2(\cdot|v_i), p_2(\cdot|v_i))
O2=i∈V∑λid(p^2(⋅∣vi),p2(⋅∣vi))
其中
λ
i
\lambda_i
λi 为控制节点重要性的因子,可以通过顶点的度数或者 PageRank等方法估计得到。
同样定义经验分为:
p
2
^
(
v
j
∣
v
i
)
=
w
i
j
d
i
\hat{p_2}(v_j | v_i) = \frac{w_{ij}}{d_i}
p2^(vj∣vi)=diwij
其中,
w
i
j
w_{ij}
wij 是边
(
i
,
j
)
(i,j)
(i,j) 的边权,
d
i
d_i
di 是顶点
v
i
v_i
vi 的出度,对于带权图,
d
i
=
∑
k
∈
N
(
I
)
W
i
k
d_i = \sum_{k \in N(I)} W_{ik}
di=∑k∈N(I)Wik
使用KL散度计算两个概率的差异,化简后有:
O
2
=
−
∑
(
i
,
j
)
∈
E
w
i
,
j
l
o
g
p
2
(
v
j
∣
v
i
)
O_2 = -\sum_{(i,j) \in E} w_{i,j} \, log \, p_2(v_j | v_i)
O2=−(i,j)∈E∑wi,jlogp2(vj∣vi)
2.3 优化技巧
1、Negative Sampling
二阶相似度计算中,在计算条件概率
p
2
(
⋅
∣
v
i
)
p_2(\cdot|v_i)
p2(⋅∣vi) 时,需要遍历所有的顶点,效率非常低下,论文中采用了负采样的技术,优化后,目标函数为:
l
o
g
σ
(
u
j
⃗
T
⋅
u
i
⃗
)
+
∑
i
=
1
K
E
v
n
∼
P
n
(
v
)
[
−
l
o
g
σ
(
u
n
⃗
T
⋅
u
i
⃗
)
]
log \, \sigma (\vec{u_j}^T \cdot \vec{u_i}) + \sum_{i=1}^{K} E_{v_n} \sim P_n(v)[-log \, \sigma(\vec{u_n}^T \cdot \vec{u_i}) ]
logσ(ujT⋅ui)+i=1∑KEvn∼Pn(v)[−logσ(unT⋅ui)]
其中
K
K
K 为负采样的个数。
论文中定义 p n ( v ) p_n(v) pn(v) 正比于 d v 3 / 4 d_v ^{3/4} dv3/4, d v d_v dv 是顶点 v v v 的出度(采样的个数为:出度的 3/4 幂)。
同时论文中使用 ASGD(Asynchronous Stochastic Gradient)算法进行优化。
2、Edge Sampling
在定义的一、二阶目标函数时,log之前还有一个权重系数 w i , j w_{i,j} wi,j ,在使用梯度下降方法优化参数时, w i , j w_{i,j} wi,j 会直接乘在梯度上。如果图中的边权方差很大,则很难选择一个合适的学习率。若使用较大的学习率那么对于较大的边权可能会引起梯度爆炸,较小的学习率对于较小的边权则会导致梯度过小。
对于上述问题,如果所有边权相同,那么选择一个合适的学习率会变得容易。这里采用了将带权边拆分为等权边的一种方法,假如一个权重为 w w w 的边,则拆分后为 w w w 个权重为1的边。这样可以解决学习率选择的问题,但是由于边数的增长,存储的需求也会增加。
另一种方法则是从原始的带权边中进行采样,每条边被采样的概率正比于原始图中边的权重,这样既解决了学习率的问题,又没有带来过多的存储开销。
这里的采样算法使用的是Alias算法,Alias是一种 O ( 1 ) O(1) O(1) 时间复杂度的离散事件抽样算法。具体内容可以参考:时间复杂度O(1)的离散采样算法—— Alias method/别名采样方法
2.4 其他讨论点
1、低度顶点的嵌入表示
由于低度顶点邻居数目极少,原网络中提供的信息有限,尤其在基于二阶相似度的LINE算法中是非常依赖于顶点的邻居数目的,那么如何确定低度顶点的向量表示呢?
一种直观的方法:添加更高阶的邻居(如邻居的邻居)来作为该低度结点的直接邻居。 与新添邻居边的权重如下:
w
i
j
=
∑
k
∈
N
(
i
)
w
i
k
w
k
j
d
k
w_{ij} = \sum_{k \in N(i)} w_{ik} \frac{w_{kj}} {d_k}
wij=k∈N(i)∑wikdkwkj
d
k
d_k
dk 是结点
k
k
k 的出边的权重总和(实际上,可以只添加与低度顶点
i
i
i 有边的,且边权最大的顶点
j
j
j 的邻居作为顶点i的二阶邻居)
2、如何找到网络中新添加顶点的向量表示
如果已知新添加的顶点i与现有顶点的联系(即存在边),则可得到其经验分布: p ^ 1 ( ⋅ , v i ) \hat{p}_1(\cdot, v_i) p^1(⋅,vi) 和 p ^ 2 ( ⋅ ∣ v i ) \hat{p}_2(\cdot | v_i) p^2(⋅∣vi)
之后通过最小化一、二阶相似度的目标函数可得到新加顶点i的向量表示:
$$
- \sum_{j \in N(i)} w_{ji} , log , p_1(v_j, v_i)
或 者 : 或者: 或者: - \sum_{j \in N(i)} w_{ji} , log , p_1(v_j | v_i)
$$
如果未能观察到新添顶点与其他现有顶点的联系,我们只能求助其他信息,比如顶点的文本信息,留待以后研究。
3 实验
算法在以下的数据集上进行了测试
对比了算法:
- Graph factorization (GF)
- DeepWalk
- LINE-SGD
- LINE
- LINE(1st+2nd)
使用的评估指标为:
- Micro-F1
- Macro-F1
在维基百科上的相关算法表现如下(更多看的是在语义、句法、覆盖率、运行时间上的对比分析):
在维基百科上进行的分类对比实验结果如下,可以看出LINE(1st+2nd)是要比其他算法效果明显的
在Flickr网络数据上进行的多目标分类LINE(1st+2nd)的效果也不错
将产出的embedding降维到2度,并进行可视化,可以看出,LINE的聚类效果更加明显。
4 代码实现
论文中给出了C 代码实现的地址
https://github.com/tangjianpku/LINE
「浅梦」也实现了很多的graph embedding算法,链接为:https://github.com/shenweichen/GraphEmbedding。当然我也看到有人基于tf实现了LINE算法,算是扩展了眼界吧,其主要代码如下,完整链接为:https://github.com/snowkylin/line。
import tensorflow as tf
import numpy as np
import argparse
from model import LINEModel
from utils import DBLPDataLoader
import pickle
import time
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--embedding_dim', default=128)
parser.add_argument('--batch_size', default=128)
parser.add_argument('--K', default=5)
parser.add_argument('--proximity', default='second-order', help='first-order or second-order')
parser.add_argument('--learning_rate', default=0.025)
parser.add_argument('--mode', default='train')
parser.add_argument('--num_batches', default=300000)
parser.add_argument('--total_graph', default=True)
parser.add_argument('--graph_file', default='data/co-authorship_graph.pkl')
args = parser.parse_args()
if args.mode == 'train':
train(args)
elif args.mode == 'test':
test(args)
def train(args):
data_loader = DBLPDataLoader(graph_file=args.graph_file)
suffix = args.proximity
args.num_of_nodes = data_loader.num_of_nodes
model = LINEModel(args)
with tf.Session() as sess:
print(args)
print('batches\tloss\tsampling time\ttraining_time\tdatetime')
tf.global_variables_initializer().run()
initial_embedding = sess.run(model.embedding)
learning_rate = args.learning_rate
sampling_time, training_time = 0, 0
for b in range(args.num_batches):
t1 = time.time()
u_i, u_j, label = data_loader.fetch_batch(batch_size=args.batch_size, K=args.K)
feed_dict = {model.u_i: u_i, model.u_j: u_j, model.label: label, model.learning_rate: learning_rate}
t2 = time.time()
sampling_time += t2 - t1
if b % 100 != 0:
sess.run(model.train_op, feed_dict=feed_dict)
training_time += time.time() - t2
if learning_rate > args.learning_rate * 0.0001:
learning_rate = args.learning_rate * (1 - b / args.num_batches)
else:
learning_rate = args.learning_rate * 0.0001
else:
loss = sess.run(model.loss, feed_dict=feed_dict)
print('%d\t%f\t%0.2f\t%0.2f\t%s' % (b, loss, sampling_time, training_time,
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())))
sampling_time, training_time = 0, 0
if b % 1000 == 0 or b == (args.num_batches - 1):
embedding = sess.run(model.embedding)
normalized_embedding = embedding / np.linalg.norm(embedding, axis=1, keepdims=True)
pickle.dump(data_loader.embedding_mapping(normalized_embedding),
open('data/embedding_%s.pkl' % suffix, 'wb'))
def test(args):
pass
if __name__ == '__main__':
main()
5 应用
LINE是与基于Graph构建的item embedding,在拿到item的embedding之后,我们可以进行的工作包括:
- 作为特征在粗排模型、精排模型进行使用
- 用来进行embedding的检索召回
- 分类
- 聚类
有一点需要注意的是,LINE算法可以应用到有向图、无向图、带权图中,相比DeepWalk等graph算法更加的灵活,但还是要看自己的需求而定,选用合适的算法做合适的事。
扫一扫关注「搜索与推荐Wiki」!号主「专注于搜索和推荐系统,以系列分享为主,持续打造精品内容!」