上一篇博客推荐系统(二)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图结构为,其节点用来表示,节点自身Embedding向量用来表示,而当被作为上下文节点时,则其Embedding向量用表示,例如上图中2节点本身的Embedding向量为,而如果其作为5或者6的相邻节点时,其上下文Embedding向量为。对于图的边,设边的权值为。
1.1 First-order Proximity
对于图中直连边,模型预测概率为:
可以看出这个预测概率其实就是sigmoid
函数,而这条边出现的真实概率为
模型的目标是使预测概率和真实概率的分布尽量相近,即使得和的KL散度尽量小,不熟悉KL散度的小伙伴可以参考这里,这个大神讲的真的非常的清楚。而这里省略了这个常数项,模型的目标如下所示:
1.2 Second-order Proximity
对于直连的边,直接给出模型预测的已知时的条件概率为:
可以看出这个预测概率也是类似sigmoid
函数,这条边的真实条件概率变为如下公式,其中表示节点的出度,
这时模型的目标和First-order Proximity类似,即使得尽量接近于,经过简化,模型的目标为:
可能很多人在这里都会有所疑惑,为什么Second-order Proximity能够使环境相似但不直连节点的Embedding向量距离相近,而且原始论文中也没有提到。这里我的理解是这样的:假设和是符合上述条件的两个节点,他们都连接着,则和两个式子只有和是不同的,且和都为1,则这样模型学习出来的结果必然会让和比较相近。
2. Model Optimization
2.1 负采样更改损失函数
计算Second-order Proximity的损失函数时,可以看出模型有一个比较大的问题,就是每次求时都要遍历图中的所有节点,这样是非常耗时的,论文中引入负采样的方式,即在计算表示公式的分母的时候,并不需要遍历所有的节点,而是选取K
个负边进行计算,公式如下所示:
2.2 alias采样
2.1节中损失函数的计算效率问题已经解决,但是现在又有一个问题,即随机梯度下降过程中梯度不稳定的问题,因为和的计算公式中都有,梯度计算公式中都含有,拿的梯度计算举例,有如下公式,过大则会导致梯度爆炸,过小则会导致梯度消失。
针对上述情况,论文提出一种解决方案,使得有权图变为无权图,即使所有的都变为1,但是在采样的时候要根据原先每条边的权值大小调整采样概率,例如一个权重为5的边要比一个权重为1的边被采到的概率大,这时就需要选择一种合适的采样策略,使得采样后的数据和原先数据的分布尽量相似,这里就用到了大名鼎鼎的alias采样,不熟悉的小伙伴可以参考这里。
3. 代码实现
和DeepWalk类似,这里依然援引知乎浅梦大神的github代码,这里分享下我对其LINE代码实现的两点见解。
-
实现负采样的代码,思路比较新颖,在
line.py
的函数batch_iter
中,对于一个batch的正样本集合,与之搭配negative_ratio
个batch的负样本集合来进行学习,整个过程只是通过mod
这一个变量进行控制的,思路真的很棒。 -
针对负采样,我这边有一点个人的见解,原先负采样的思路是针对一个节点,先随机采一个batch的与直连的节点,与拼接在一起构成正样本集合,之后随机采若干个batch的节点,与拼接在一起构成负样本集合。这个做法的问题在于如果这个负样本集合中有与直连的节点构成的边,则会使模型的学习变得艰难,因为在采集正样本的时候已经采集过了,但是这里又将其作为负样本,这样会让模型变得困惑。在这里我自己新添加了几行代码来回避上述问题,如下所示,
# 若干代码... 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算法能够很好地兼顾这两个方面。
参考
来源:CSDN
作者:LightYoungLee
链接:https://blog.csdn.net/weixin_37688445/article/details/104106781