推荐系统(三)Graph Embedding之LINE

上一篇博客推荐系统(二)Graph Embedding之DeepWalk中讲到Graph Embedding的开山之作DeepWalk,该博客讲述了在图结构上进行RandomWalk获取训练样本,并通过Word2Vec模型来训练得到图中每个节点的Embedding向量。

但DeepWalk有两个比较大的问题:

  1. DeepWalk将问题抽象成无权图,这意味着高频次user behavior路径和低频次user behavior路径在模型看来是等价的。
  2. 所处环境相似但不直连的两个节点没有进行特殊的处理,这类节点的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 i v_i 来表示,节点自身Embedding向量用 u i \vec{u_i} 来表示,而当 v i v_i 被作为上下文节点时,则其Embedding向量用 u i \vec{u_i}' 表示,例如上图中2节点本身的Embedding向量为 u 2 \vec{u_2} ,而如果其作为5或者6的相邻节点时,其上下文Embedding向量为 u 2 \vec{u_2}' 。对于图的边 ( v i , v j ) (v_i,v_j) ,设边的权值为 w i , j w_{i, j}

1.1 First-order Proximity

对于图中直连边 ( v i , v j ) (v_i,v_j) ,模型预测概率为:
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})}

可以看出这个预测概率其实就是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 ( v i , v j ) p_1(v_i, v_j) p ^ 1 ( v i , v j ) \hat{p}_1(v_i, v_j) 的KL散度尽量小,不熟悉KL散度的小伙伴可以参考这里,这个大神讲的真的非常的清楚。而这里省略了 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)

1.2 Second-order Proximity

对于直连的边 ( v i , v j ) (v_i,v_j) ,直接给出模型预测的已知 v i v_i 时的条件概率为:
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})}

可以看出这个预测概率也是类似sigmoid函数,这条边的真实条件概率变为如下公式,其中 d i d_i 表示节点 v i v_i 的出度,
p ^ 2 ( v j v i ) = w i , j d i \hat{p}_2(v_j|v_i)=\frac{w_{i,j}}{d_i}

这时模型的目标和First-order Proximity类似,即使得 p 2 ( v j v i ) p_2(v_j|v_i) 尽量接近于 p ^ 2 ( v j v i ) \hat{p}_2(v_j|v_i) ,经过简化,模型的目标为:
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)

可能很多人在这里都会有所疑惑,为什么Second-order Proximity能够使环境相似但不直连节点的Embedding向量距离相近,而且原始论文中也没有提到。这里我的理解是这样的:假设 v i v_i v m v_m 是符合上述条件的两个节点,他们都连接着 v j v_j ,则 p 2 ( v j v i ) p_2(v_j|v_i) p 2 ( v j v m ) p_2(v_j|v_m) 两个式子只有 u i \vec{u_i} u m \vec{u_m} 是不同的,且 p ^ 2 ( v j v i ) \hat{p}_2(v_j|v_i) p ^ 2 ( v j v m ) \hat{p}_2(v_j|v_m) 都为1,则这样模型学习出来的结果必然会让 u i \vec{u_i} u m \vec{u_m} 比较相近。

2. Model Optimization

2.1 负采样更改损失函数

计算Second-order Proximity的损失函数时,可以看出模型有一个比较大的问题,就是每次求 p 2 ( v j v i ) p_2(v_j|v_i) 时都要遍历图中的所有节点,这样是非常耗时的,论文中引入负采样的方式,即在计算 p 2 ( v j v i ) p_2(v_j|v_i) 表示公式的分母的时候,并不需要遍历所有的节点,而是选取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})]

2.2 alias采样

2.1节中损失函数的计算效率问题已经解决,但是现在又有一个问题,即随机梯度下降过程中梯度不稳定的问题,因为 p 1 ( v i , v j ) p_1(v_i, v_j) p 2 ( v j v i ) p_2(v_j|v_i) 的计算公式中都有 w i , j w_{i,j} ,梯度计算公式中都含有 w i , j w_{i,j} ,拿 p 2 ( v j v i ) p_2(v_j|v_i) 的梯度计算举例,有如下公式, w i , j w_{i,j} 过大则会导致梯度爆炸, w i , j w_{i,j} 过小则会导致梯度消失。
O 2 u i = w i , j l o g p 2 ( v j v i ) u i \frac{\partial O_2}{\partial \vec{u_i}}=w_{i,j} \cdot \frac{\partial logp_2(v_j|v_i)}{\partial \vec{u_i}}

针对上述情况,论文提出一种解决方案,使得有权图变为无权图,即使所有的 w i , j w_{i,j} 都变为1,但是在采样的时候要根据原先每条边的权值大小调整采样概率,例如一个权重为5的边要比一个权重为1的边被采到的概率大,这时就需要选择一种合适的采样策略,使得采样后的数据和原先数据的分布尽量相似,这里就用到了大名鼎鼎的alias采样,不熟悉的小伙伴可以参考这里

3. 代码实现

和DeepWalk类似,这里依然援引知乎浅梦大神的github代码,这里分享下我对其LINE代码实现的两点见解。

  1. 实现负采样的代码,思路比较新颖,在line.py的函数batch_iter中,对于一个batch的正样本集合,与之搭配negative_ratio个batch的负样本集合来进行学习,整个过程只是通过mod这一个变量进行控制的,思路真的很棒。

  2. 针对负采样,我这边有一点个人的见解,原先负采样的思路是针对一个节点 v i v_i ,先随机采一个batch的与 v i v_i 直连的节点,与 v i v_i 拼接在一起构成正样本集合,之后随机采若干个batch的节点,与 v i v_i 拼接在一起构成负样本集合。这个做法的问题在于如果这个负样本集合中有与 v i v_i 直连的节点 v j v_j 构成的边 ( v i , v j ) (v_i,v_j) ,则会使模型的学习变得艰难,因为在采集正样本的时候已经采集过 ( v i , v j ) (v_i,v_j) 了,但是这里又将其作为负样本,这样会让模型变得困惑。在这里我自己新添加了几行代码来回避上述问题,如下所示,

    # 若干代码...
    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算法能够很好地兼顾这两个方面。

参考

  1. 【Graph Embedding】LINE:算法原理,实现和应用
  2. LINE- Large-scale Information Network Embedding
发布了36 篇原创文章 · 获赞 42 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_37688445/article/details/104106781