论文分享 -- >Graph Embedding -- > LINE: Large-scale Information Network Embedding

博客内容将首发在微信公众号"跟我一起读论文啦啦",上面会定期分享机器学习、深度学习、数据挖掘、自然语言处理等高质量论文,欢迎关注!
在这里插入图片描述

本次要总结和分享的论文是 LINE: Large-scale Information Network Embedding,其链接 论文,所参考的实现代码 code,这篇论文某些细节读起来有点晦涩难懂,不易理解,下面好好分析下。

论文动机和创新点

  • information network 在现实世界中无处不在,例如最常见的社交网络图。而这种网络通常包含 百万以上的节点和数以十亿记的边,如果能将这种高维复杂的网络映射(降维) 到低维空间内,用于可视化、节点分类、预测、推荐等相关任务,则能产生巨大的商业和学术价值,而本论文就旨探讨如何将高维的信息网络 映射(降维)到低维空间内。

  • 现实中的信息网络十分复杂,通常包含 百万以上的节点和数以十亿记的边,对于如此复杂庞大的图结构,目前大多数的图嵌入方法都不可行。

  • 由此本文提出一种高效的网络表征学习方法,对于有向/无向、有权重/无权重的图都可适用,且可在单机器上、数小时之内完成对 包含百万以上的节点和数以十亿记的边的图网络的 embedding 学习。

  • 本论文所提方法 能保存 局部和全局的网络结构信息,并且提出了高效的优化技巧,使得能对包含百万节点的图进行学习。

     局部网络结构信息:节点间的一阶近似性(first-order proximity)
     全局网络结构信息:节点间的二阶近似性(second-order proximity)
     优化技巧:edge-sample,negative-sample
    
  • 与DeepWalk 类似深度优先搜索相比,Line更像一种广度优先搜索,对于取得二阶相似性,广度更加合理。并且Line适用于带权图,而DeepWalk不适用。

LINE主要内容

图结构的定义

G = ( V , E ) G=(V,E) G=(V,E)
上述公式中 V V V 表示 节点集合, E E E 表示边集合,对每天边上的权重 e = ( u , v ) ∈ E , w u v e=(u,v)\in E, w_{uv} e=(u,v)E,wuv 表示节点 u u u v v v关系强弱,若是无向图则 W u v = W v u W_{uv} = W_{vu} Wuv=Wvu,反正则不相等。注意本论文只探讨边权重 w > = 0 w >=0 w>=0的情况。 如上所描述的图G基本上可以囊括现实世界中的信息网络。

一阶相似性(first-order proximity)

对于图中任意两个节点 u u u, v v v 都可以由边进行连接,如果在图中两节点有边则 w u v > 0 w_{uv} > 0 wuv>0,否则等于0,这种定义也是符合现实逻辑的,在information network中,如果两个用户存在连接关系,则该两个用户的性格、兴趣等可能存在相似性、如果两个网页存在连向彼此的链接,则该网页内容可能存在相似性等等。
在这里可以假设有一条无向边 e = ( i , j ) e=(i,j) e=(i,j),其两节点 v i , v j v_i, v_j vi,vj 的联合概率可以定义如下:
p 1 ( v i , v j ) = s i g m o i d ( u i ⃗   ⋅   u j ⃗ ) = 1 1 + e x p ( − u i ⃗   ⋅   u j ⃗ ) (1) p_1(v_i, v_j) = sigmoid(\vec{u_i}\ \cdot \ \vec{u_j}) = \frac{1}{1+exp(-\vec{u_i}\ \cdot \ \vec{u_j})} \tag{1} p1(vi,vj)=sigmoid(ui   uj )=1+exp(ui   uj )1(1)
由此我们可以得到在 V ∗ V V*V VV 空间上的概率分布 p ( ⋅ , ⋅ ) p(\cdot, \cdot) p(,)
经验概率分布可以定义为 p 1 ^ ( i , j ) = w i , j W (2) \hat{p_1}(i, j) = \frac{w_{i,j}}{W} \tag{2} p1^(i,j)=Wwi,j(2)
其中 W = ∑ i , j ∈ E w i j W=\sum_{{i,j}\in{E}} w_{ij} W=i,jEwij ,由此可以的带 V ∗ V V*V VV 上的经验概率分布 p ^ 1 ( ⋅ , ⋅ ) \hat p_1(\cdot, \cdot) p^1(,)
论文提到要让 p 1 , p 1 ^ p_1, \hat{p_1} p1,p1^ 这两种分布KL散度距离越近越好,去掉一些常数,由此可得到 一阶相似的损失函数:
O 1 = − ∑ i , j ∈ E w i j l o g p 1 ( v i , v j ) (3) O_1 = - \sum_{{i,j} \in E} w_{ij}logp_1(v_i, v_j) \tag{3} O1=i,jEwijlogp1(vi,vj)(3)
论文里提到这种一阶相似性只能应用在无向图中,不适用有向图,这里也可理解,按照一阶相似度计算,不可能存在 v i v_i vi v j v_j vj 相似 不等于 v j v_j vj v i v_i vi 相似度。

那么问题来了,为何按照公式(3) 的优化方向,得出来的节点 v i v_i vi 的表示向量 u i ⃗ \vec{u_i} ui 真的与节点 v j v_j vj 的表示向量 u j ⃗ \vec{u_j} uj 就真的如 如期的那样相似或者不相似呢? 这点论文中并没有解释,可能是个常识性知识,但是仍值得探讨下, 假设 w i j w_{ij} wij 很大,这说明实际中,节点 v i v_i vi v j v_j vj 很相似,那么按这公式(3) 优化, p ( v i , v j ) p(v_i, v_j) p(vi,vj)也应该很大,则 u i ⃗ ⋅ u j ⃗ \vec{u_i} \cdot \vec{u_j} ui uj 也应该很大,则按照余弦相似度计算,则向量 u i ⃗ \vec{u_i} ui u j ⃗ \vec{u_j} uj 的夹角很小,则该 u i ⃗ \vec{u_i} ui u j ⃗ \vec{u_j} uj 很相似,这就符合如期了。

这里还有一个问题:由公式(1)计算出的 p ( ⋅ , ⋅ ) p(\cdot, \cdot) p(,) 里面每个概率值都是有sigmoid计算出来的,整体并不是一个标准概率分布,因为 ∑ p 1 ( ⋅ , ⋅ ) ! = 1 \sum p_1(\cdot, \cdot) != 1 p1(,)!=1 很有可能性发生,严格上来说 p 1 ( ⋅ , ⋅ ) p_1(\cdot, \cdot) p1(,) 并不是一种分布。而 ∑ p ^ 1 ( ⋅ , ⋅ ) = 1 \sum\hat p_1(\cdot, \cdot) =1 p^1(,)=1 是一定的。这个问题不知如何解释?

二阶相似性(second-order proximity)

但是如果仅仅根据两节点是否存在直接相连的边来判断相似性,显然是不够的,例如两个用户虽然没有直接相连,但是他们却存在大量共同好友,由常识也知道认为这两用户是相似的,但是他们的一阶相似性却为0。由此定义二阶相似性:两节点各自相邻节点的一阶相似性。可解决一阶相似性的稀疏问题。

因为二阶相似性要在有向图上适用,故对每个节点,有两个角色要扮演:①自身角色 ②相对别的节点,节点作为上下文角色。
例如对于有向边 ( i , j ) (i,j) (i,j),在给定节点 v i v_i vi 的情形下, v j v_j vj 节点需要承担 v i v_i vi上下文角色。每个节点都有两个角色,每个角色都有两个表示向量。例如节点 v i v_i vi 的表示向量 u i ⃗ \vec{u_i} ui 和上下文表示向量 u i ⃗ ′ \vec{u_i}' ui 。由此可得在给定 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 ⃗ ) (4) 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})} \tag{4} p2(vjvi)=k=1Vexp(uk Tui )exp(uj Tui )(4)
上式中 V V V 表示所有节点数量。那么对于每个节点,都可以得到一个概率分布 p 2 ( ⋅ ∣ v i ) p_2(\cdot| v_i) p2(vi)
同样论文中也提出了二阶的经验分布:
p 2 ^ ( v j ∣ v i ) = w i j d i (5) \hat{p_2}(v_j|v_i) = \frac{w_{ij}}{d_i} \tag{5} p2^(vjvi)=diwij(5)
其中 d i = ∑ k ∈ N ( i ) w i k d_i = \sum_{k \in N(i)} w_{ik} di=kN(i)wik N ( i ) N(i) N(i) 表示节点 v i v_i vi 的出度,由此得到二阶经验分布 p 2 ^ ( ⋅ ∣ v i ) \hat{p_2}(\cdot|v_i) p2^(vi)
同样去掉一些常数,可得二阶相似性的损失函数:
O 2 = − ∑ ( i , j ) ∈ E w i j l o g p 2 ( v j ∣ v i ) (6) O_2 = -\sum_{(i,j) \in E} w_{ij}logp_2(v_j|v_i) \tag{6} O2=(i,j)Ewijlogp2(vjvi)(6)

到这来,可能有人要问,这种二阶相似性计算,如何体现 “两个节点拥有相似的相邻节点,则该两个节点肯定有相似性”,论文中对此也没有任何解释,可能是个比较常识性的问题,但是我认为仍值得深入解释一下,这里提出我个人的想法:试想一下,在公式(4)中 v i v_i vi v j v_j vj 相邻,并且 v i v_i vi 不断对与他相邻的节点 v j v_j vj 施加影响,也就是相邻节点间相互影响,并且不断沿着边向四周传播影响,如果两个节点拥有相似的邻接点,那么这两个节点受到的影响也是一致的,邻接点越相似,所受的影响就越一致。

可能又有人问,为啥二阶弄个上下文向量 u ′ ⃗ \vec{u'} u ,试想一下,在公式(4)中,如果 u j ′ ⃗ T \vec{u_j'}^{T} uj T u j ⃗ T \vec{u_j}^{T} uj T,那么 p 2 ( v j ∣ v i ) p_2(v_j|v_i) p2(vjvi) p 2 ( v i ∣ v j ) p_2(v_i|v_j) p2(vivj) 就相等了,那么就没有方向上的区别了。

优化技巧

边采样

论文里提到对于一个庞大复杂的带权有向图,其边权重的方差可能是很大的,难以训练优化,论文中提出可将每个带权边拆解成多个权重为 1 1 1 的边。但是这样做,图将变得非常复杂,对机器的memory要求将会剧增。 为了缓解这个问题,可以对边进行带概率(相同两节点边出现频次/ 总边数)的采样(即采样出的样本服从原始样本分布)。 论文中提出使用alias method方法进行采样。该方法查找时间复杂度O(1),但是建表时间复杂度O(n),但是只需建一次即可。详情可看 alia-method。这样就完成了对边的采样。

节点负采样

与NLP中的word2vec所遇到的问题类似,对于公式(4) 来说,分母需要遍历节点图中的所有节点,这样计算量可能非常巨大,因此需要采样部分负节点,来组成负样本,那么公式(4)可转化成如下:
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 ⃗ ) ] (7) 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})] \tag{7} log σ(uj Tui )+i=1KEvnpn(v)[log σ(un Tui )](7)

对于上面公式中的求和下标 i i i,个人感觉改成 n n n 应该较好理解些。上式相当于把节点 v j v_j vj v i v_i vi 作为一对正样本(label=1),而节点 v i v_i vi每个采样得到的节点组成的 p a i r w i s e pairwise pairwise 作为负样本(label=-1),故里面有个负号。有正有负,有变大有变小才能优化。

在实写代码时,只需要将每对pairwise做内积乘以label再做log_sigmoid,再对所有pairwise求和取负,再minimize即可

同理在公式(3) 里面,也需要对没有边相连的节点进行采样,作为负样本,这样 有正有负,有变大有变小才能优化。

所以对一阶相似与二阶相似做完负采样后,两者损失函数一致了,不同的是对于一阶相似,需将公式(7) 中的 u j ′ ⃗ \vec{u_j'} uj u j ⃗ \vec{u_j} uj ,实写代码时也是。

关键代码

class LINEModel:
    def __init__(self, args):
        self.u_i = tf.placeholder(name='u_i', dtype=tf.int32, shape=[args.batch_size * (args.K + 1)])
        self.u_j = tf.placeholder(name='u_j', dtype=tf.int32, shape=[args.batch_size * (args.K + 1)])
        self.label = tf.placeholder(name='label', dtype=tf.float32, shape=[args.batch_size * (args.K + 1)])
        self.embedding = tf.get_variable('target_embedding', [args.num_of_nodes, args.embedding_dim],
                                         initializer=tf.random_uniform_initializer(minval=-1., maxval=1.))
        self.u_i_embedding = tf.matmul(tf.one_hot(self.u_i, depth=args.num_of_nodes), self.embedding)
        if args.proximity == 'first-order':
            self.u_j_embedding = tf.matmul(tf.one_hot(self.u_j, depth=args.num_of_nodes), self.embedding)
        elif args.proximity == 'second-order':
            self.context_embedding = tf.get_variable('context_embedding', [args.num_of_nodes, args.embedding_dim],
                                                     initializer=tf.random_uniform_initializer(minval=-1., maxval=1.))
            self.u_j_embedding = tf.matmul(tf.one_hot(self.u_j, depth=args.num_of_nodes), self.context_embedding)

        self.inner_product = tf.reduce_sum(self.u_i_embedding * self.u_j_embedding, axis=1)
        self.loss = -tf.reduce_mean(tf.log_sigmoid(self.label * self.inner_product))
        self.learning_rate = tf.placeholder(name='learning_rate', dtype=tf.float32)
        # self.optimizer = tf.train.GradientDescentOptimizer(learning_rate=self.learning_rate)
        self.optimizer = tf.train.RMSPropOptimizer(learning_rate=self.learning_rate)
        self.train_op = self.optimizer.minimize(self.loss)

由上面代码可以看出

当选择一阶相似性时:节点 v j v_j vj 是从 e m b e d d i n g embedding embedding 矩阵里取表示向量,模型也只更新 e m b e d d i n g embedding embedding 矩阵;
当选择二阶相似性时:节点 v j v_j vj 是从 c o n t e x t _ e m b e d d i n g context\_embedding context_embedding 矩阵里取表示向量,模型会更新 e m b e d d i n g , c o n t e x t _ e m b e d d i n g embedding, context\_embedding embedding,context_embedding 矩阵;

class DBLPDataLoader:
    def __init__(self, graph_file):
        self.g = nx.read_gpickle(graph_file)
        self.num_of_nodes = self.g.number_of_nodes()
        self.num_of_edges = self.g.number_of_edges()
        self.edges_raw = self.g.edges(data=True)
        self.nodes_raw = self.g.nodes(data=True)

        self.edge_distribution = np.array([attr['weight'] for _, _, attr in self.edges_raw], dtype=np.float32)
        self.edge_distribution /= np.sum(self.edge_distribution)
        self.edge_sampling = AliasSampling(prob=self.edge_distribution)
        self.node_negative_distribution = np.power(
            np.array([self.g.degree(node, weight='weight') for node, _ in self.nodes_raw], dtype=np.float32), 0.75)
        self.node_negative_distribution /= np.sum(self.node_negative_distribution)
        self.node_sampling = AliasSampling(prob=self.node_negative_distribution)

        self.node_index = {}
        self.node_index_reversed = {}
        for index, (node, _) in enumerate(self.nodes_raw):
            self.node_index[node] = index
            self.node_index_reversed[index] = node
        self.edges = [(self.node_index[u], self.node_index[v]) for u, v, _ in self.edges_raw]

    def fetch_batch(self, batch_size=16, K=10, edge_sampling='atlas', node_sampling='atlas'):
        if edge_sampling == 'numpy':
            edge_batch_index = np.random.choice(self.num_of_edges, size=batch_size, p=self.edge_distribution)
        elif edge_sampling == 'atlas':
            edge_batch_index = self.edge_sampling.sampling(batch_size)
        elif edge_sampling == 'uniform':
            edge_batch_index = np.random.randint(0, self.num_of_edges, size=batch_size)
        u_i = []
        u_j = []
        label = []
        for edge_index in edge_batch_index:
            edge = self.edges[edge_index]
            if self.g.__class__ == nx.Graph:
                if np.random.rand() > 0.5:      # important: second-order proximity is for directed edge
                    edge = (edge[1], edge[0])
            u_i.append(edge[0])
            u_j.append(edge[1])
            label.append(1)
            for i in range(K):
                while True:
                    if node_sampling == 'numpy':
                        negative_node = np.random.choice(self.num_of_nodes, p=self.node_negative_distribution)
                    elif node_sampling == 'atlas':
                        negative_node = self.node_sampling.sampling()
                    elif node_sampling == 'uniform':
                        negative_node = np.random.randint(0, self.num_of_nodes)
                    if not self.g.has_edge(self.node_index_reversed[negative_node], self.node_index_reversed[edge[0]]):
                        break
                u_i.append(edge[0])
                u_j.append(negative_node)
                label.append(-1)
        return u_i, u_j, label

    def embedding_mapping(self, embedding):
        return {node: embedding[self.node_index[node]] for node, _ in self.nodes_raw}

上述代码中的图已假定边权重均为1,相当于已经对带权重的边进行了拆解了多条二元边;首先进行 边采样,然后再对节点进行了负采样,负采样得到 p a i r w i s e pairwise pairwise l a b e l label label 均为-1。

个人总结

  • 本论文刚刚读起来,有点晦涩难懂,总觉得有些地方不严谨。但是仔细推敲下,貌似没啥大问题
  • 论文读起来感觉很复杂的样子,但是代码真的挺简单的。
  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值