上一篇博客推荐系统(二)Graph Embedding之DeepWalk中讲到Graph Embedding的开山之作DeepWalk,该博客讲述了在图结构上进行RandomWalk获取训练样本,并通过Word2Vec模型来训练得到图中每个节点的Embedding向量。
但DeepWalk有两个比较大的问题:
- DeepWalk将问题抽象成无权图,这意味着高频次user behavior路径和低频次user behavior路径在模型看来是等价的。
- 所处环境相似但不直连的两个节点没有进行特殊的处理,这类节点的Embedding向量本应比较相似,而这类信息是没有办法通过深度优先遍历的方式来获取的,只能通过宽度优先遍历的方式来获取,使得这类节点的Embedding向量相去甚远。
上述两点缺陷正是本篇博客提到的LINE算法所关注的。本篇博客着重讲述LINE算法的模型构建和模型优化两个过程。在模型构建阶段,在图结构中随机提取指定条数的边作为训练样本集合,之后对于训练集合中的每条边,采用First/Second-order Proximity作为模型的两种构建手段,并利用相对熵作为损失函数。在模型优化阶段,采用负采样变更损失函数+alias边采样的方式进行加速运算。
关键字: First-order Proximity,Second-order Proximity,相对熵,Edge Sampling
如下是本篇博客的主要内容:
- First/Second-order Proximity
- Model Optimization
- 代码实现
- 总结
1. First/Second-order Proximity
LINE算法将user behavior 抽象为有权图,即每个边都带有权重,如下图所示。这样就能区分出高频次user behavior路径和低频次user behavior路径,相当于把原先DeepWalk中缺失的信息补充上。
而只将无权图变为有权图还远远不够,如何利用这部分信息才是关键。一般情况下如果图中两个节点有直接的边向量,且边的权值较大,则有理由相信这两个节点的Embedding向量的距离需要足够近,这就是论文中提到的First-order Proximity。而对于两个环境较为相似但不直连的节点,即他们共享很多相同的邻居,他们的Embedding向量也应该比较相似才对,就如下图中的节点5和节点6一样。对于这类节点的建模,即论文中提到的Second-order Proximity。如下会对这两种建模思想进行介绍。
设user behavior图结构为
(
V
,
E
)
(V,E)
(V,E),其节点用
v
i
v_i
vi来表示,节点自身Embedding向量用
u
i
⃗
\vec{u_i}
ui来表示,而当
v
i
v_i
vi被作为上下文节点时,则其Embedding向量用
u
i
⃗
′
\vec{u_i}'
ui′表示,例如上图中2节点本身的Embedding向量为
u
2
⃗
\vec{u_2}
u2,而如果其作为5或者6的相邻节点时,其上下文Embedding向量为
u
2
⃗
′
\vec{u_2}'
u2′。对于图的边
(
v
i
,
v
j
)
(v_i,v_j)
(vi,vj),设边的权值为
w
i
,
j
w_{i, j}
wi,j。
1.1 First-order Proximity
对于图中直连边
(
v
i
,
v
j
)
(v_i,v_j)
(vi,vj),模型预测概率为:
p
1
(
v
i
,
v
j
)
=
1
1
+
e
x
p
(
−
u
i
⃗
T
⋅
u
j
⃗
)
p_1(v_i, v_j)=\frac{1}{1+exp(-\vec{u_i}^T\cdot\vec{u_j})}
p1(vi,vj)=1+exp(−uiT⋅uj)1
可以看出这个预测概率其实就是sigmoid
函数,而这条边出现的真实概率为
p
^
1
(
v
i
,
v
j
)
=
w
i
,
j
W
,
W
=
∑
(
i
,
j
)
∈
E
w
i
,
j
\hat{p}_1(v_i, v_j)=\frac{w_{i,j}}{W}, W=\sum_{(i,j)\in E}w_{i,j}
p^1(vi,vj)=Wwi,j,W=(i,j)∈E∑wi,j
模型的目标是使预测概率和真实概率的分布尽量相近,即使得
p
1
(
v
i
,
v
j
)
p_1(v_i, v_j)
p1(vi,vj)和
p
^
1
(
v
i
,
v
j
)
\hat{p}_1(v_i, v_j)
p^1(vi,vj)的KL散度尽量小,不熟悉KL散度的小伙伴可以参考这里,这个大神讲的真的非常的清楚。而这里省略了
W
W
W这个常数项,模型的目标如下所示:
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}logp_1(v_i,v_j)
O1=−(i,j)∈E∑wi,jlogp1(vi,vj)
1.2 Second-order Proximity
对于直连的边
(
v
i
,
v
j
)
(v_i,v_j)
(vi,vj),直接给出模型预测的已知
v
i
v_i
vi时的条件概率为:
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(−uk′T⋅ui)exp(−uj′T⋅ui)
可以看出这个预测概率也是类似sigmoid
函数,这条边的真实条件概率变为如下公式,其中
d
i
d_i
di表示节点
v
i
v_i
vi的出度,
p
^
2
(
v
j
∣
v
i
)
=
w
i
,
j
d
i
\hat{p}_2(v_j|v_i)=\frac{w_{i,j}}{d_i}
p^2(vj∣vi)=diwi,j
这时模型的目标和First-order Proximity类似,即使得
p
2
(
v
j
∣
v
i
)
p_2(v_j|v_i)
p2(vj∣vi)尽量接近于
p
^
2
(
v
j
∣
v
i
)
\hat{p}_2(v_j|v_i)
p^2(vj∣vi),经过简化,模型的目标为:
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}logp_2(v_j|v_i)
O2=−(i,j)∈E∑wi,jlogp2(vj∣vi)
可能很多人在这里都会有所疑惑,为什么Second-order Proximity能够使环境相似但不直连节点的Embedding向量距离相近,而且原始论文中也没有提到。这里我的理解是这样的:假设 v i v_i vi和 v m v_m vm是符合上述条件的两个节点,他们都连接着 v j v_j vj,则 p 2 ( v j ∣ v i ) p_2(v_j|v_i) p2(vj∣vi)和 p 2 ( v j ∣ v m ) p_2(v_j|v_m) p2(vj∣vm)两个式子只有 u i ⃗ \vec{u_i} ui和 u m ⃗ \vec{u_m} um是不同的,且 p ^ 2 ( v j ∣ v i ) \hat{p}_2(v_j|v_i) p^2(vj∣vi)和 p ^ 2 ( v j ∣ v m ) \hat{p}_2(v_j|v_m) p^2(vj∣vm)都为1,则这样模型学习出来的结果必然会让 u i ⃗ \vec{u_i} ui和 u m ⃗ \vec{u_m} um比较相近。
2. Model Optimization
2.1 负采样更改损失函数
计算Second-order Proximity的损失函数时,可以看出模型有一个比较大的问题,就是每次求
p
2
(
v
j
∣
v
i
)
p_2(v_j|v_i)
p2(vj∣vi)时都要遍历图中的所有节点,这样是非常耗时的,论文中引入负采样的方式,即在计算
p
2
(
v
j
∣
v
i
)
p_2(v_j|v_i)
p2(vj∣vi)表示公式的分母的时候,并不需要遍历所有的节点,而是选取K
个负边进行计算,公式如下所示:
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σ(uj′T⋅ui)+i=1∑KEvn∼Pn(v)[logσ(un′T⋅ui)]
2.2 alias采样
2.1节中损失函数的计算效率问题已经解决,但是现在又有一个问题,即随机梯度下降过程中梯度不稳定的问题,因为 p 1 ( v i , v j ) p_1(v_i, v_j) p1(vi,vj)和 p 2 ( v j ∣ v i ) p_2(v_j|v_i) p2(vj∣vi)的计算公式中都有 w i , j w_{i,j} wi,j,而为了使一个batch中数据的分布和整体分布保持一致,需要通过一定的采样策略来完成。
针对上述情况,论文提出一种解决方案,使得有权图变为无权图,即使所有的 w i , j w_{i,j} wi,j都变为1,但是在采样的时候要根据原先每条边的权值大小调整采样概率,例如一个权重为5的边要比一个权重为1的边被采到的概率大,这时就需要选择一种合适的采样策略,使得采样后的数据和原先数据的分布尽量相似,这里就用到了大名鼎鼎的alias采样,不熟悉的小伙伴可以参考这里。
3. 代码实现
和DeepWalk类似,这里依然援引知乎浅梦大神的github代码,这里分享下我对其LINE代码实现的两点见解。
-
实现负采样的代码,思路比较新颖,在
line.py
的函数batch_iter
中,对于一个batch的正样本集合,与之搭配negative_ratio
个batch的负样本集合来进行学习,整个过程只是通过mod
这一个变量进行控制的,思路真的很棒。 -
针对负采样,我这边有一点个人的见解,原先负采样的思路是针对一个节点 v i v_i vi,先随机采一个batch的与 v i v_i vi直连的节点,与 v i v_i vi拼接在一起构成正样本集合,之后随机采若干个batch的节点,与 v i v_i vi拼接在一起构成负样本集合。这个做法的问题在于如果这个负样本集合中有与 v i v_i vi直连的节点 v j v_j vj构成的边 ( v i , v j ) (v_i,v_j) (vi,vj),则会使模型的学习变得艰难,因为在采集正样本的时候已经采集过 ( v i , v j ) (v_i,v_j) (vi,vj)了,但是这里又将其作为负样本,这样会让模型变得困惑。在这里我自己新添加了几行代码来回避上述问题,如下所示,
# 若干代码... if mod == 0: h = [] t = [] for i in range(start_index, end_index): if random.random() >= self.edge_accept[shuffle_indices[i]]: shuffle_indices[i] = self.edge_alias[shuffle_indices[i]] cur_h = edges[shuffle_indices[i]][0] cur_t = edges[shuffle_indices[i]][1] h.append(cur_h) t.append(cur_t) sign = np.ones(len(h)) else: sign = np.ones(len(h))*-1 t = [] for i in range(len(h)): negative_sampled_index = alias_sample(self.node_accept, self.node_alias) # 新添加代码,回避采样困惑问题 constructed_edge = (self.idx2node[h[i]], self.idx2node[negative_sampled_index]) if constructed_edge in self.graph.edges: sign[i] = 1 # ----------------------- t.append(negative_sampled_index) # 若干代码...
经过上述添加代码的改善后,经过50个epoch的训练后,loss由原先的0.0473变为0.0203,可以看出,新添加的代码确实有效果,如果大家有异议的话,可以尽管提~
4. 总结
本篇博客介绍了LINE算法整体思路、加速训练的手段以及代码实现的一些细节,希望能够给大家带来帮助。但LINE算法依然有其自己的缺点,即算法过分关注邻接特征,即只去关注邻接节点或相似节点,没有像DeepWalk一样考虑一条路径上的特征,而后面我们要讲述的Node2Vec算法能够很好地兼顾这两个方面。