论文|LINE算法原理、代码实战和应用

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 uv 之间存在直连变,则其一阶相似度可以用权重 w u v w_{uv} wuv来表示,如果不存在直连边,则一阶相似度为0。

上图中,顶点6、7之间是直接相连的,且权重比较大(边比较粗),则认为顶点6、7是相似的,且一阶相似度较高,顶点5、6之间并没有直接相连,则两者的一阶相似度为0。

2、二阶相似度

二阶相似度描述的是两个顶点之间没有直接相连,但是他们拥有相同的邻居。比如顶点 u 、 v u、v uv 直接不存在直接相连,但是 顶点 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 uv 之间的相似度即为 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,jEwi,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)Ewi,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(vjvi)=k=1Vexp(uk Tui )exp(uj Tui )
其中 ∣ 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=iVλ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^(vjvi)=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=kN(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)Ewi,jlogp2(vjvi)

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σ(uj Tui )+i=1KEvnPn(v)[logσ(un Tui )]
其中 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=kN(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算法更加的灵活,但还是要看自己的需求而定,选用合适的算法做合适的事。


【技术服务】详情点击查看: https://mp.weixin.qq.com/s/PtX9ukKRBmazAWARprGIAg

在这里插入图片描述
扫一扫关注「搜索与推荐Wiki」!号主「专注于搜索和推荐系统,以系列分享为主,持续打造精品内容!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值