Line:Large-scale Information Network Embedding
Github整理代码链接,欢迎讨论和交流,觉得有用的可以Star一下。
1.任务
论文方法主要用于将非常庞大的信息网络(顶点和边构成的图)映射到一个低维稠密的向量空间。
- 如可视化、节点分类、边预测等
- 无向图、有向图、有权边和无权边等
- 语言网络、社交网络、引文网络等
定义了两个目标函数用于保留全局和局部分网络结构;提出了一种edge-sample算法,解决由提出的目标函数引起随机梯度下降计算缺陷。
2.主要思想
一阶相似度:顶点6和顶点7存在直连边,则边权为1,认为一阶相似度很高
二阶相似度:顶点5和顶点6不存在直连边,但是存在顶点1、2、3、4相同的邻居顶点,定义顶点5和顶点6的二阶相似度很高
由于发现两个顶点的一阶相似度不能有效保留全局网络结构,所以提出了二阶相似度用于保留全局网络结构。
![](https://i-blog.csdnimg.cn/blog_migrate/8af37466ee7c3fdfa3529bda7a91fa48.png)
3.流程
预备知识KL-divergence(KL散度):
是一种量化两种概率分布p和q之间差异的方式,又叫相对熵
。在概率学和统计学上,我们经常会使用一种更简单的、近似的分布
来替代观察数据
或太复杂的分布
。KL散度能帮助我们度量使用一个分布来近似另一个分布时所损失的信息量。公式如下,
p
(
x
i
)
p(x_i)
p(xi)表示真实分布,
q
(
x
i
)
q(x_i)
q(xi)表示近似分布:
D
K
L
=
∑
i
=
1
N
p
(
x
i
)
l
o
g
p
(
x
i
)
q
(
x
i
)
D_{KL}=\sum_{i=1}^{N}p(x_i)log\frac{p(x_i)}{q(x_i)}
DKL=i=1∑Np(xi)logq(xi)p(xi)
3.1 一阶相似度
仅用于无向图
对每个无向边
(
i
,
j
)
(i,j)
(i,j),定义顶点
v
i
v_i
vi和 顶点
v
j
v_j
vj联合概率见公式2,其中
u
⃗
i
∈
R
d
\vec{u}_i\in{R^d}
ui∈Rd是顶点
v
i
v_i
vi低维空间向量表达
q
1
(
v
i
,
v
j
)
=
1
1
+
e
x
p
(
−
u
⃗
i
T
⋅
u
⃗
j
)
q_1(v_i,v_j)=\frac{1}{1+exp(-{\vec{u}_i}^T\cdot\vec{u}_j)}
q1(vi,vj)=1+exp(−uiT⋅uj)1
对每个无向的边
(
i
,
j
)
(i,j)
(i,j)定义顶点
v
i
v_i
vi和 顶点
v
j
v_j
vj经验概率见公式3,其中
w
i
j
w_{ij}
wij是
i
i
i和
j
j
j之间的权重,
W
∈
∑
i
,
j
∈
E
w
i
j
W\in\sum_{{i,j}\in{E}}w_{ij}
W∈∑i,j∈Ewij是所有边的权重和
p
1
(
v
i
,
v
j
)
=
w
i
j
W
p_1(v_i,v_j)=\frac{w_{ij}}{W}
p1(vi,vj)=Wwij
为了保留一阶相似度,使用KL散度最小化联合概率和经验概率,将公式2和公式3带入公式1中得公式4:
KaTeX parse error: No such environment: equation at position 8: \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲ \begin{split} …
3.2 二阶相似度
用于无向图、有向图
每个顶点可以看作两种表达,它自身和邻居顶点的上下文。定义 u ⃗ i ∈ R d \vec{u}_i\in{R^d} ui∈Rd为顶点 v i v_i vi自身表达, u ⃗ i ′ ∈ R d \vec{u}_i^{'}\in{R^d} ui′∈Rd为顶点 v i v_i vi邻居顶点的上下文表达。
对于每个有向边
(
i
,
j
)
(i,j)
(i,j),定义顶点
v
i
v_i
vi和其上下文顶点
v
j
v_j
vj的联合概率见公式5,其中
V
V
V为顶点
v
i
v_i
vi上下文顶点的集合(邻居顶点的集合)。
q
2
(
v
j
∣
v
i
)
=
e
x
p
(
u
⃗
j
′
T
⋅
u
⃗
i
)
∑
k
∈
V
e
x
p
(
u
⃗
k
′
T
⋅
u
⃗
i
)
q_2(v_j|v_i)=\frac{exp({\vec{u}_j^{'}}^T\cdot\vec{u}_i)}{\sum_{k\in{V}}exp({\vec{u}_k^{'}}^T\cdot\vec{u}_i)}
q2(vj∣vi)=∑k∈Vexp(uk′T⋅ui)exp(uj′T⋅ui)
对于每个有向边
(
i
,
j
)
(i,j)
(i,j),定义顶点
v
i
v_i
vi和其上下文顶点
v
j
v_j
vj的经验概率见公式6,其中
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=∑k∈N(i)wik,
N
(
i
)
N(i)
N(i)表示顶点
v
i
v_i
vi的邻居顶点集合
p
2
(
v
j
∣
v
i
)
=
w
i
j
d
i
p_2(v_j|v_i)=\frac{w_{ij}}{d_i}
p2(vj∣vi)=diwij
二阶相似度的先验假设:具有相同上下文分布的顶点,它们也相似。
为了保留二阶相似度,使用KL散度最小化联合概率和经验概率,将公式5和公式6带入公式1中得公式7,计算KL散度的时候为了区分每个顶点的重要程度,在目标函数中引入了一个参数
λ
i
\lambda_{i}
λi,为了优化计算令
λ
i
=
d
i
\lambda_{i}=d_i
λi=di:
KaTeX parse error: No such environment: equation at position 8: \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲ \begin{split} …
3.3 模型优化
3.3.1 负采样降低计算复杂度
在计算公式5的时候,分母需要计算整个上下文顶点的集合并求和,这增加了很大的计算量。为了解决该问题,使用负采样进行优化。**负采样:**为每个有向边 ( i , j ) (i,j) (i,j)的目标函数添加多个噪声负样本。
为了简化计算,将公式5的每个有向边
(
i
,
j
)
(i,j)
(i,j)进行负采样变换得到公式8,其中
σ
(
⋅
)
=
1
/
1
(
1
+
e
x
p
(
−
x
)
)
\sigma(\cdot)=1/1(1+exp(-x))
σ(⋅)=1/1(1+exp(−x))为常见的sigmod函数,第一部分为真实的边的概率,值越大越好,第二部分为从噪声分布中采样的负样本边的概率,值越大越好,由于整体
σ
(
⋅
)
\sigma(\cdot)
σ(⋅)是最大化优化,但是第二部分是负样本,负样本两个顶点的联合概率应该越小越好,所以在第二部分负采样中添加负号,
K
K
K为负采样边的个数。
q
2
(
v
j
∣
v
i
)
=
l
o
g
σ
(
u
⃗
j
′
T
⋅
u
⃗
i
)
+
∑
i
=
1
K
l
o
g
σ
(
−
u
⃗
n
′
T
⋅
u
⃗
i
)
q_2(v_j|v_i)=log\sigma({\vec{u}_j^{'}}^T\cdot\vec{u}_i)+\sum_{i=1}^{K}log\sigma(-{\vec{u}_n^{'}}^T\cdot\vec{u}_i)
q2(vj∣vi)=logσ(uj′T⋅ui)+i=1∑Klogσ(−un′T⋅ui)
对于一阶相似度的计算公式2,也采用负采样进行优化,只需将
u
⃗
j
′
\vec{u}_j^{'}
uj′换成
u
j
′
{u}_j^{'}
uj′即可,这样避免了多种不同复杂的解法。
得到优化目标公式8,最大优化目标函数求得最优解。一般情况下,求最大值问题不方便计算,通常会转化成求最小值,所以在公式8前面添加负号,看作模型的loss函数,最小优化。
3.3.2 边采样
本文采用更新方式是SGD(随机梯度下降),如对于有向边
(
i
,
j
)
(i,j)
(i,j),计算顶点
v
i
v_i
vi的向量表达
u
⃗
i
\vec{u}_i
ui的梯度
∂
L
2
∂
u
⃗
i
=
w
i
j
⋅
∂
l
o
g
(
q
2
(
v
j
∣
v
i
)
)
∂
u
⃗
i
\frac{\partial L_2}{\partial \vec{u}_i}=w_{ij}\cdot\frac{\partial log(q_2(v_j|v_i))}{\partial \vec{u}_i}
∂ui∂L2=wij⋅∂ui∂log(q2(vj∣vi))
由公式9可以看出,计算顶点梯度是需要乘以该采样边的权重,这给学习率的选择带来了一定的问题。比如在采样边权重很小的情况下选用了较大的学习率,可能会出现梯度爆炸;在采样边权重很大的情况下选用了较小的学习率,可能会出现梯度消失。为了解决该问题,提出了边采样,边的采样概率正比于边的权重。
3.3.3 时间复杂度为 O ( n ) O(n) O(n)的采样方法
首先将其概率分布按其概率对应到线段上,总共有4个事件A、B、C、D,如下图所示
![](https://i-blog.csdnimg.cn/blog_migrate/034bc61751aebb843afcf054b6db64f2.png)
接着产生 0 ∼ 1 0\sim1 0∼1之间的一个随机数,随机数对应到线段的的哪一段,就产生一个采样事件。比如落在 0 ∼ 1 / 2 0\sim1/2 0∼1/2之间就是事件A,落在 1 / 2 ∼ 5 / 6 1/2\sim5/6 1/2∼5/6之间就是事件B,落在 5 / 6 ∼ 11 / 12 5/6\sim11/12 5/6∼11/12之间就是事件C,落在 11 / 12 ∼ 1 11/12\sim1 11/12∼1之间就是事件D。在判断随机数属于哪个采样事件的时候事件复杂度为 O ( n ) O(n) O(n)。
3.3.4 时间复杂度为 O ( 1 ) O(1) O(1)的采样方法 (alias table method sample)
为了减少采样时间复杂度,引入了alias table method采样方式,将采样时间复杂度从 O ( n ) O(n) O(n)降低到 O ( 1 ) O(1) O(1)。总共分为两步:
- 绘制概率表(alias table)
将概率分别乘以事件总数N,得到变换后的概率分布图,见图3
![](https://i-blog.csdnimg.cn/blog_migrate/8f9496be56b2398a4eabdac7ecfba725.png)
图3总面积为N,可以看出某些位置面积大于1或者小于1,将面积大于1的事件多出的面积补充到面积小于1对应的事件中,以确保每一个小方格的面积为1,同时,保证每一方格至多存储两个事件,最后得到一个1*N的矩形,即绘制alias table,如图4所示。
![](https://i-blog.csdnimg.cn/blog_migrate/55c67bb57dc2d02f6125b194a52c5cc1.png)
表里面有两个数组,一个数组存储的是事件 i i i占第 i i i列矩形面积的比例,另一个数组存储第i列中不是事件 i i i的另一个事件编号。做表的时间复杂度是 O ( n ) O(n) O(n)。
例如图4存储数组:
accept= [ 2 3 1 1 3 1 3 ] \begin{bmatrix} \frac {2}{3} & 1 &\frac {1}{3} &\frac {1}{3} \end{bmatrix} [3213131]
alias= [ 1 N U L L 0 0 ] \begin{bmatrix} 1 & NULL & 0 & 0 \end{bmatrix} [1NULL00]
- 根据表采样
首先随机生成一个0到N间的随机整数 i i i,代表选择第 i i i列;再生成一个0到1间的随机数 p p p,若 p p p小于事件 i i i占第 i i i列矩形的面积的比例,则表示接受事件 i i i,否则,接收第 i i i列中不是事件 i i i的另一个事件。采样的时间复杂度为 o(1) 。
3.4 Alias Sample实现
3.4.1 Node Alias Sample
-
获得所有顶点归一化的概率分布列表
每个顶点的概率计算见公式10,其中 d i d_i di表示顶点 i i i的出度,出度的 3 4 \frac{3}{4} 43次幂的意义是平衡出度过大和过小的顶点,让它们之间的差距缩小,此参数是参照已有文献[1]设置。
p i = d i 3 4 ∑ i ∈ V d i 3 4 p_i=\frac{d_i^{\frac{3}{4}}}{\sum_{i\in{V}}{d_i}^{\frac{3}{4}}} pi=∑i∈Vdi43di43 -
根据概率分布创建alias table
3.4.2 Edge Alias Sample
-
获得所有边归一化的概率分布列表
每条边的概率计算见公式10,其中 w ( i , j ) w_{(i,j)} w(i,j)表示边 ( i , j ) (i,j) (i,j)的边权
p i = w ( i , j ) ∑ ( i , j ) ∈ V w ( i , j ) p_i=\frac{w_{(i,j)}}{\sum_{{(i,j)}\in{V}}{w_{(i,j)}}} pi=∑(i,j)∈Vw(i,j)w(i,j) -
根据概率分布创建alias table
4.代码实现和实验
4.1 模型代码(一阶和二阶相似度)
创建一个生成边 ( i , j ) (i,j) (i,j)中顶点 i i i的和顶点 j j j的Embedding的模型:
# _*_ coding: utf-8 _*_
"""
Time: 2020/9/10 10:47
Author: Cheng Ding(Deeachain)
Version: V 0.1
File: line.py
Describe: Write during the internship at Hikvison, Github link: https://github.com/Deeachain/GraphEmbeddings
"""
class Line(nn.Module):
def __init__(self, dict_size, embed_dim=128, order="first"):
super(Line, self).__init__()
assert order in ["first", "second", "all"], print("Order should either be [first, second, all]")
self.dict_size = dict_size
self.embed_dim = embed_dim
self.order = order
self.embeddings = nn.Embedding(dict_size, embed_dim)
self.context_embeddings = nn.Embedding(dict_size, embed_dim)
self.embeddings.weight.data.uniform_(-0.5, 0.5)
self.context_embeddings.weight.data.uniform_(-0.5, 0.5)
def forward(self, nodeindex, v_i, v_j):
# init embeddings
if self.order == 'first':
u_i = self.embeddings(torch.LongTensor(v_i))
u_j = self.embeddings(torch.LongTensor(v_j))
return u_i, u_j
elif self.order == 'second':
u_i = self.embeddings(torch.LongTensor(v_i))
u_j_context = self.context_embeddings(torch.LongTensor(v_j))
return u_i, u_j_context
elif self.order == 'all':
u_i = self.embeddings(torch.LongTensor(v_i))
u_j1 = self.embeddings(torch.LongTensor(v_j))
u_j2 = self.context_embeddings(torch.LongTensor(v_j))
return u_i, u_j1, u_j2
计算两个有直连边的邻居顶点一阶和二阶相似度,主要是根据公式8改写得到。公式8拆分成两部分,第一部分是正采样计算,第二部分是负采样计算。
v_i = batch[0] # v_i=[pos_i,neg_i1,neg_i2,neg_i3,neg_i4,neg_i5]
v_j = batch[1] # v_i=[pos_j,neg_j1,neg_j2,neg_j3,neg_j4,neg_j5]
loss = 0
for i in range(len(v_i)):
if args.order == 'all':
u_i, u_j1, u_j2 = model(node_index, v_i[i], v_j[i])
temp1 = torch.sum(torch.mul(u_i, u_j1), dim=1)
temp2 = torch.sum(torch.mul(u_i, u_j2), dim=1)
if i == 0: # postive
loss1 = -torch.mean(F.logsigmoid(temp1), dim=0)
loss2 = -torch.mean(F.logsigmoid(temp2), dim=0)
else: # negative
loss1 = -torch.mean(F.logsigmoid(-temp1), dim=0)
loss2 = -torch.mean(F.logsigmoid(-temp2), dim=0)
loss += (loss1 + loss2)
else:
u_i, u_j = model(node_index, v_i[i], v_j[i])
temp = torch.sum(torch.mul(u_i, u_j), dim=1)
if i == 0: # postive
temp = temp
else: # negative
temp = -temp
loss += -torch.mean(F.logsigmoid(temp), dim=0)
4.2 Alias Sample代码
根据公式10计算所有顶点的概率,创建Node Alias Sample Table;根据公式11计算所有边的概率,创建Edge Alias Sample Table
def get_alias_node(G):
'''
Get the alias node setup lists for a given node.
'''
node_degree = {}
index2node = {}
unnormalized_probs = []
for index, node in enumerate(sorted(G.nodes())):
index2node[index] = node
node_degree[node] = len((list(G.neighbors(node))))
unnormalized_probs.append(pow(node_degree[node], 0.75))
norm_const = sum(unnormalized_probs)
normalized_probs = [float(pow(u_prob, 0.75)) / norm_const for u_prob in unnormalized_probs]
return alias_setup(normalized_probs), index2node
def get_alias_edge(G):
'''
Get the alias edge setup lists for a given edge.
'''
index2edge = {}
unnormalized_probs = []
for index, edge in enumerate(sorted(G.edges())):
index2edge[index] = (edge[0], edge[1])
unnormalized_probs.append(G[edge[0]][edge[1]]['weight'])
norm_const = sum(unnormalized_probs)
normalized_probs = [float(u_prob) / norm_const for u_prob in unnormalized_probs]
return alias_setup(normalized_probs), index2edge
4.3 正负采样代码
def __getitem__(self, index):
# postive
edge_index_in_alias = alias_draw(J=self.J_edge, q=self.q_edge)
edge = self.index2edge[edge_index_in_alias]
node_i, node_j = edge[0], edge[1]
# negativate
neighbor_node_i = list(self.G.neighbors(node_i))
negative_sample = []
K = self.num_negative
while K:
# negative = random.choice(self.nodes)
negative_index_in_alias = alias_draw(self.J_node, self.q_node)
negative = self.index2node[negative_index_in_alias]
if negative not in neighbor_node_i and negative not in negative_sample:
negative_sample.append(negative)
K -= 1
elif negative in neighbor_node_i:
continue
# reduce dict size by change node number to index
node_i = self.node2index[node_i]
node_j = self.node2index[node_j]
i_list = []
j_list = []
i_list.append(node_i)
j_list.append(node_j)
for neg in negative_sample:
neg = self.node2index[neg]
i_list.append(node_i)
j_list.append(neg)
return i_list, j_list
4.4 实验
整理代码采用了另外的三个公开数据集,分别是:Cora数据集由机器学习论文组成,总共有2708篇论文,应用关系有5429个,论文总共七类(基于案例、遗传算法、神经网络、概率方法、强化学习、规则学习、理论);DBLP数据集也是引文网络组成的图,只选用了4类;BlogCatalog数据集是Blog用户之间关系构成的社交网络,相关详细参数见表1
数据集 | cora | dblp | BlogCatalog |
---|---|---|---|
V | 2708 | 17725 | 10312 |
E | 5429 | 105781 | 333983 |
Class | 7 | 4 | 39 |
实验参数:
- Deepwalk:词向量维度 d = 128 d=128 d=128、每个顶点游走路径数 γ = 50 \gamma=50 γ=50、游走路径长度 t = 20 t=20 t=20、 E p o c h = 5 Epoch=5 Epoch=5、SkipGram窗口大小 w = 10 w=10 w=10
- Line:词向量维度
d
=
128
d=128
d=128、二阶相似度生成128维词向量、
E
p
o
c
h
=
150
Epoch=150
Epoch=150、
l
r
=
0.005
lr=0.005
lr=0.005、
n
u
m
_
n
e
g
a
t
i
v
e
=
5
num\_negative=5
num_negative=5。(目前Line模型存在一定问题,不能复现论文效果。)
- Cora数据集有向图实验,一阶相似度:节点分类 f 1 = 0.44 f1=0.44 f1=0.44,可视化能区分一部分节点,;二阶相似度节点分类 f 1 = 0.52 f1=0.52 f1=0.52,,可视化能区分一部分节点
- COra数据集无向图实验,一阶相似度:节点分类 f 1 = 0.48 f1=0.48 f1=0.48,可视化能区分一部分节点,;二阶相似度节点分类 f 1 = 0.62 f1=0.62 f1=0.62,,可视化能区分一部分节点
- Node2vec:词向量维度 d = 128 d=128 d=128、返回参数 p = 0.25 p=0.25 p=0.25、输入输出参数 q = 0.25 q=0.25 q=0.25( p 、 q ∈ ( 0.25 , 0.5 , 1 , 2 , 4 ) p、q\in ({0.25,0.5,1,2,4}) p、q∈(0.25,0.5,1,2,4)文中实验证明 p p p和 q q q越小越好)、每个顶点游走路径数 γ = 50 \gamma=50 γ=50、游走路径长度 t = 20 t=20 t=20、 E p o c h = 5 Epoch=5 Epoch=5、SkipGram窗口大小 w = 10 w=10 w=10
Line通过计算二阶相似度,优化得到顶点的Embedding,最后使用Embedding设计顶点分类任务。数据集划分80%的数据用于训练,20%用于评估,分类任务使用SVM分类,得到顶点的分类指标f1-score如表2所示。
cora | dblp | |
---|---|---|
Deepwalk(f1-micro) | 0.8542 | 0.8327 |
Line(f1-micro) | 0.6218 | 0.6262 |
Node2vec(f1-micro) | 0.8561 | 0.8386 |
节点Embedding表达在空间维度上,相同类别的节点距离会越靠近,最后将学习得到的Embedding进行可视化展示。可视化使用的是TSNE实现,将向量降维到二维平面上。可视化效果如图5所示。
参考和引用
[1] Distributed Representations of Words and Phrases and their Compositionality
[2] 论文作者源码C++
[2] 参考tensorflow代码