为什么要对图进行嵌入?
直接在这种非结构的,数量不定(可能数目非常多),属性复杂的 图 上进行机器学习/深度学习是很困难的,而如果能处理为向量将非常的方便。但评价一个好的嵌入需要:
- 保持图属性不变,如图的拓扑结构、顶点连接、顶点周围节点等
- 嵌入速度应该与图的大小无关
- 合适的维度以方便做下游任务
图表示学习性质
- 如果没有标签。邻居节点的表示应该相似。
- 如果存在标签。同类节点的表示应该相似。
- 节点的顺序变化对表示没有影响。
Graph Embedding
图嵌入本身其实是属于表示学习。主要目的是将图中的节点/图表示成低维,实值,稠密的向量形式,使得到的向量能够做进一步的推理,以更好的实现下游任务。图嵌入包括顶点嵌入/图嵌入,嵌入的方法主要有矩阵分解,随机游走和深度学习。
- 矩阵分解。因为从某种程度上图中的各节点关系可以视为稀疏的矩阵,那么基于矩阵分解的方法就可以得到低维的向量。其中常用的矩阵包括普通的邻接矩阵,度矩阵,拉普拉斯矩阵,节点转移概率矩阵,节点属性矩阵等。
- DeepWalk。通过对图随机游走得到一些序列,把序列当句子,利用word2vec就可以得到每一个“词”的向量了。(前提是基于句子某写词的出现和随机游走访问到的节点都服从幂律分布)。
- Graph Neural Network: Graph+Neural Network都是图神经网络GNN,通过结合深度学习来得到图嵌入也是很不错的选择。(矩阵分解也是可以结合神经网络的)
Graph Factorization
和矩阵分解的思路一样,求得分解后的Z能够和原先的边权重Y有相同的效果:
f
(
Y
,
Z
,
λ
)
=
1
2
∑
(
i
,
j
)
∈
E
(
Y
i
j
−
<
Z
i
,
Z
j
>
)
2
+
λ
2
∑
i
∥
Z
i
∥
2
f(Y,Z,\lambda)=\frac{1}{2} \sum_{(i,j)\in E}(Y_{ij}-<Z_i,Z_j>)^2+\frac{\lambda}{2}\sum_i \|Z_i\|^2
f(Y,Z,λ)=21(i,j)∈E∑(Yij−<Zi,Zj>)2+2λi∑∥Zi∥2
本质上就是对邻接矩阵进行降维,给每个节点生成一个低维表示。
DeepWalk
出自2014的KDD,《DeepWalk: Online Learning of Social Representations》
而DeepWalk的思想如上图,具体就是从一个顶点出发,然后按照一定的概率随机移动到一个邻居节点,并将该节点作为新的当前节点,如此循环执行若干步,得到一条游走路径。然后把这个路径视为一个“句子”,用word2vec得到嵌入嵌入结果。
- 采样。通过随机游走对图进行采样。论文中作者建议从每个顶点执行32到64次随机游走就差不多了,具体游走代码如下。
- word2vec。将随机游走得到顶点路径当作word2vec中的句子。一般多使用skip-gram将随机游走顶点的one-hot向量作为输入,并最大化其相邻节点的预测概率。训练中通常预测20个邻居节点-左侧10个节点,右侧10个节点。
- 训练结束后的副产物,每个“词”的向量就是顶点的嵌入向量了。
def random_walk(self, path_length, alpha=0, rand=random.Random(), start=None):
""" Returns a truncated random walk.
path_length: Length of the random walk.
alpha: probability of restarts.
start: the start node of the random walk.
"""
G = self #图G
if start: #如果设置了开始节点
path = [start]
else:
# 如果没有就随机选择
path = [rand.choice(list(G.keys()))]
#只要没到最大长度
while len(path) < path_length:
cur = path[-1]
if len(G[cur]) > 0:
#按一定概率转移
if rand.random() >= alpha:
path.append(rand.choice(G[cur]))
else:
path.append(path[0])
else:
break
return [str(node) for node in path]
可扩展:处理新加入节点,由于有random walk,所以只需要学习新的结点的信息就可以了
可并行:同时从不同的结点处开始random walk
LINE
出自2015WWW,《LINE: Large-scale Information Network Embedding》。
作者开源code:https://github.com/tangjianpku/LINE
LINE也是一种基于邻域相似假设的方法(网络中相似的点在向量表示中的距离比较近),但DeepWalk可以视做是一个DFS,而LINE更加倾向于BFS。主要由一阶相似度和二阶相似度组成。First-order Proximity:一阶相似度用于描述图中成对顶点之间的局部相似度(两个顶点之间的相似),Second-order Proximity:二阶相似度是顶点的顶点之间的相似度,直观来说朋友的朋友也算是我们的朋友,这也同样应该是有效的,如上图的5和6之间虽然没有边,但是有4个相同的邻居节点,所以理论上我们认为5和6在二阶上是相似的。算法流程为:
- 模拟一阶相似度。顶点vi和vj之间的联合概率为 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(-u_i^T \cdot u_j)} p1(vi,vj)=1+exp(−uiT⋅uj)1u是节点对应的向量,向量越接近,点积就越大,联合概率也就越大。为了保持一阶相似性,将联合概率与经验概率(两点之间边的权值越大,经验概率越大)进行比较,即: p 1 ′ ( i , j ) = w i j W p_1'(i,j)=\frac{w_{ij}}{W} p1′(i,j)=Wwij O 1 = d ( p 1 , p 1 ′ ) = − ∑ w i j l o g p 1 ( v i , v j ) O_1=d(p_1,p_1')=-\sum w_{ij}log p_1(v_i,v_j) O1=d(p1,p1′)=−∑wijlogp1(vi,vj)d是衡量分布的函数,如果是KL散度来衡量则可以进一步展开,然后最小化该式子即可。另外,如果两节点之间无连接则直接设置为0。
- 模拟二阶相似度。也是定义一个vj是vi的邻居的概率为(二阶的度量是,如果vj和vi相似,那么对应向量点积越大,vj越有可能是vi的邻居)
p 2 ( v j ∣ v i ) = e x p ( u j T ⋅ u i ) ∑ e x p ( u j T ⋅ u i ) p_2(v_j|v_i)=\frac{exp(u_j^T \cdot u_i)}{\sum exp(u_j^T \cdot u_i)} p2(vj∣vi)=∑exp(ujT⋅ui)exp(ujT⋅ui)同样与经验概率(此处定义为Graph中所有节点可能是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,其中wij是边(i,j)的权重,di是顶点i的出度。同时考虑到节点的重要程度可能不一样,所以加入度数作为权重,度数越高越重要。
O 2 = − ∑ w i j l o g p 2 ( v j ∣ v i ) O_2=-\sum w_{ij} log p_2(v_j|v_i) O2=−∑wijlogp2(vj∣vi) - 分别训练一阶相似度模型和二阶相似度模型,然后拼接。
Trick:为了避免算二阶时需要遍历每个节点,LINE使用负采样的思想来简化计算。
Think:计算一阶相似度中改变同一条边的方向对于最终结果没有影响,所以一阶只适合无向图。而二阶有出度入度,能够实现有向图。
如何处理新加入的节点? 根据经验分布和连接,优化O就可以了,其中原节点向量不变。
Node2vec
出自2016KDD,《node2vec: Scalable Feature Learning for Networks》
Node2vec 可以看作是对 DeepWalk 的一种更广义的抽象,主要是改进DeepWalk的随机游走策略。由于普通的随机游走无法很好地保留节点的局部信息,所以Node2vec多做了两个参数P和Q来加以控制(完全随机时P=Q=1),以获取邻域信息和更复杂的依存信息。
- In-out,参数Q控制选择其他的新顶点的概率,偏广度优先,重视局部,即节点重要性。
- Return,参数P控制返回原来顶点的概率,偏深度优先,重视全局,即群体重要性。
两个参数控制以达到BFS和DFS的平衡,具体如上图此时在绿色节点上,返回到前一步红色节点的概率是 1 P \frac{1}{P} P1,到达未与先前红色节点连接的节点的概率为 1 Q \frac{1}{Q} Q1,到达红色节点邻居的概率为1。
#不同之处只有转移的概率不同
def node2vec_walk(self, path_length, start):
G = self
nodes = self.nodes
edges = self.edges
path = [start]
while len(path) < path_length:
cur = path[-1]
cur_n = list(G.neighbors(cur)) #邻居节点
if len(cur_nbrs) > 0:
if len(path) == 1:
path.append(cur_n[sample(nodes[cur][0], nodes[cur][1])])
else:
prev = path[-2]
edge = (prev, cur)
next_node = cur_n[sample(edges[edge][0],edges[edge][1])]
path.append(next_node)
else:
break
return walk
图嵌入的本质是什么?
实际上他们仍然还是矩阵分解!!具体如下图:
感兴趣可以参考下pointwise mutual information,因为word2vec本身也可以从PMI来解释。而后面LINE,PTE(处理异构)和node2vec都是对游走加上了限制。
下一篇文章将整理十分稳定的SDNE和对整张图进行嵌入的graph2vec。