详解Graph Embedding经典方法:算法原理、代码实现与应用样例(1)

 Random Walk


我们可以通过并行的方式加速路径采样,在采用多进程进行加速时,相比于开一个进程池让每次外层循环启动一个进程,我们采用固定为每个进程分配指定数量的num_walks的方式,这样可以最大限度减少进程频繁创建与销毁的时间开销。

deepwalk_walk方法对应上一节伪代码中第6行,_simulate_walks对应伪代码中第3行开始的外层循环。最后的Parallel为多进程并行时的任务分配操作。

def deepwalk_walk(self, walk_length, start_node):

walk = [start_node]

while len(walk) < walk_length:

cur = walk[-1]

cur_nbrs = list(self.G.neighbors(cur))

if len(cur_nbrs) > 0:

walk.append(random.choice(cur_nbrs))

else:

break

return walk

def _simulate_walks(self, nodes, num_walks, walk_length,):

walks = []

for _ in range(num_walks):

random.shuffle(nodes)

for v in nodes:

walks.append(self.deepwalk_walk(alk_length=walk_length, start_node=v))

return walks

results = Parallel(n_jobs=workers, verbose=verbose, )(

delayed(self._simulate_walks)(nodes, num, walk_length) for num in

partition_num(num_walks, workers))

walks = list(itertools.chain(*results))

 Word2vec


这里就偷个懒直接用gensim里的 Word2Vec 了。

from gensim.models import Word2Vec

w2v_model = Word2Vec(walks,sg=1,hs=1)

 DeepWalk 应用


这里简单的用 DeepWalk 算法在 wiki 数据集上进行节点分类任务和可视化任务。  wiki 数据集包含 2,405 个网页和17,981条网页之间的链接关系,以及每个网页的所属类别。

本例中的训练,评测和可视化的完整代码在下面的 git 仓库中:

https://github.com/shenweichen/GraphEmbedding

G = nx.read_edgelist(‘…/data/wiki/Wiki_edgelist.txt’,create_using=nx.DiGraph(),nodetype=None,data=[(‘weight’,int)])

model = DeepWalk(G,walk_length=10,num_walks=80,workers=1)

model.train(window_size=5,iter=3)

embeddings = model.get_embeddings()

evaluate_embeddings(embeddings)

plot_embeddings(embeddings)

 分类任务结果


micro-F1 : 0.6674

macro-F1 : 0.5768

 可视化结果



LINE


之前介绍过DeepWalk,DeepWalk使用DFS随机游走在图中进行节点采样,使用word2vec在采样的序列学习图中节点的向量表示。

LINE也是一种基于邻域相似假设的方法,只不过与DeepWalk使用DFS构造邻域不同的是,LINE可以看作是一种使用BFS构造邻域的算法。此外,LINE还可以应用在带权图中(DeepWalk仅能用于无权图)。

之前还提到不同的graph embedding方法的一个主要区别是对图中顶点之间的相似度的定义不同,所以先看一下LINE对于相似度的定义。

 LINE 算法原理


1. 一种新的相似度定义



✎  first-order proximity


1阶相似度用于描述图中成对顶点之间的局部相似度,形式化描述为若之间存在直连边,则边权即为两个顶点的相似度,若不存在直连边,则1阶相似度为0。如上图,6和7之间存在直连边,且边权较大,则认为两者相似且1阶相似度较高,而5和6之间不存在直连边,则两者间1阶相似度为0。

  second-order proximity


仅有1阶相似度就够了吗?显然不够,如上图,虽然5和6之间不存在直连边,但是他们有很多相同的邻居顶点(1,2,3,4),这其实也可以表明5和6是相似的,而2阶相似度就是用来描述这种关系的。形式化定义为,令表示顶点与所有其他顶点间的1阶相似度,则的2阶相似度可以通过的相似度表示。若之间不存在相同的邻居顶点,则2阶相似度为0。

2. 优化目标


  1st-order


对于每一条无向边,定义顶点之间的联合概率为:

为顶点的低维向量表示。(可以看作一个内积模型,计算两个item之间的匹配程度)

同时定义经验分布:

,

优化目标为最小化:

是两个分布的距离,常用的衡量两个概率分布差异的指标为KL散度,使用KL散度并忽略常数项后有:

1st order 相似度只能用于无向图当中。


  2nd-order


这里对于每个顶点维护两个embedding向量,一个是该顶点本身的表示向量,一个是该点作为其他顶点的上下文顶点时的表示向量。

对于有向边,定义给定顶点条件下,产生上下文(邻居)顶点的概率为:

,其中为上下文顶点的个数。

优化目标为:

,其中为控制节点重要性的因子,可以通过顶点的度数或者PageRank等方法估计得到。

经验分布定义为:

是边的边权,是顶点的出度,对于带权图,使用KL散度并设,忽略常数项,有


3. 优化技巧


  Negative sampling


由于计算2阶相似度时,softmax函数的分母计算需要遍历所有顶点,这是非常低效的,论文采用了负采样优化的技巧,目标函数变为:

是负边的个数。

论文使用是顶点的出度。

  Edge Sampling


注意到我们的目标函数在log之前还有一个权重系数,在使用梯度下降方法优化参数时,会直接乘在梯度上。如果图中的边权方差很大,则很难选择一个合适的学习率。若使用较大的学习率那么对于较大的边权可能会引起梯度爆炸,较小的学习率对于较小的边权则会导致梯度过小。

对于上述问题,如果所有边权相同,那么选择一个合适的学习率会变得容易。这里采用了将带权边拆分为等权边的一种方法,假如一个权重为的边,则拆分后为个权重为1的边。这样可以解决学习率选择的问题,但是由于边数的增长,存储的需求也会增加。

另一种方法则是从原始的带权边中进行采样,每条边被采样的概率正比于原始图中边的权重,这样既解决了学习率的问题,又没有带来过多的存储开销。

这里的采样算法使用的是Alias算法,Alias是一种时间复杂度的离散事件抽样算法。具体内容可以参考:

Alias Method:时间复杂度O(1)的离散采样方法

=================================

https://zhuanlan.zhihu.com/p/54867139

4. 其他问题

  低度数顶点


对于一些顶点由于其邻接点非常少会导致embedding向量的学习不充分,论文提到可以利用邻居的邻居构造样本进行学习,这里也暴露出LINE方法仅考虑一阶和二阶相似性,对高阶信息的利用不足。

  新加入顶点


对于新加入图的顶点,若该顶点与图中顶点存在边相连,我们只需要固定模型的其他参数,优化如下两个目标之一即可:

若不存在边相连,则需要利用一些side info,留到后续工作研究。


 LINE核心代码


1. 模型和损失函数定义


LINE使用梯度下降的方法进行优化,直接使用tensorflow进行实现,就可以不用人工写参数更新的逻辑了~

这里的 实现中把1阶和2阶的方法融合到一起了,可以通过超参数order控制是分开优化还是联合优化,论文推荐分开优化。

首先输入就是两个顶点的编号,然后分别拿到各自对应的embedding向量,最后输出内积的结果。真实label定义为1或者-1,通过模型输出的内积和line_loss就可以优化使用了负采样技巧的目标函数了~

def line_loss(y_true, y_pred):

return -K.mean(K.log(K.sigmoid(y_true*y_pred)))def create_model(numNodes, embedding_size, order=‘second’):

v_i = Input(shape=(1,))

v_j = Input(shape=(1,))

first_emb = Embedding(numNodes, embedding_size, name=‘first_emb’)

second_emb = Embedding(numNodes, embedding_size, name=‘second_emb’)

context_emb = Embedding(numNodes, embedding_size, name=‘context_emb’)

v_i_emb = first_emb(v_i)

v_j_emb = first_emb(v_j)

v_i_emb_second = second_emb(v_i)

v_j_context_emb = context_emb(v_j)

first = Lambda(lambda x: tf.reduce_sum(

x[0]*x[1], axis=-1, keep_dims=False), name=‘first_order’)([v_i_emb, v_j_emb])

second = Lambda(lambda x: tf.reduce_sum(

x[0]*x[1], axis=-1, keep_dims=False), name=‘second_order’)([v_i_emb_second, v_j_context_emb])

if order == ‘first’:

output_list = [first]

elif order == ‘second’:

output_list = [second]

else:

output_list = [first, second]

model = Model(inputs=[v_i, v_j], outputs=output_list)

2. 顶点负采样和边采样


下面的函数功能是创建顶点负采样和边采样需要的采样表。中规中矩,主要就是做一些预处理,然后创建alias算法需要的两个表。

def _gen_sampling_table(self):

create sampling table for vertex

power = 0.75

numNodes = self.node_size

node_degree = np.zeros(numNodes) # out degree

node2idx = self.node2idx

for edge in self.graph.edges():

node_degree[node2idx[edge[0]]

] += self.graph[edge[0]][edge[1]].get(‘weight’, 1.0)

total_sum = sum([math.pow(node_degree[i], power)

for i in range(numNodes)])

norm_prob = [float(math.pow(node_degree[j], power)) /

total_sum for j in range(numNodes)]

self.node_accept, self.node_alias = create_alias_table(norm_prob)

create sampling table for edge

numEdges = self.graph.number_of_edges()

total_sum = sum([self.graph[edge[0]][edge[1]].get(‘weight’, 1.0)

for edge in self.graph.edges()])

norm_prob = [self.graph[edge[0]][edge[1]].get(‘weight’, 1.0) *

numEdges / total_sum for edge in self.graph.edges()]

self.edge_accept, self.edge_alias = create_alias_table(norm_prob)

 LINE 应用


和之前一样,还是用LINE在wiki数据集上进行节点分类任务和可视化任务。wiki数据集包含 2,405 个网页和17,981条网页之间的链接关系,以及每个网页的所属类别。由于1阶相似度仅能应用于无向图中,所以本例中仅使用2阶相似度。

本例中的训练,评测和可视化的完整代码在下面的git仓库中

https://github.com/shenweichen/GraphEmbedding

G = nx.read_edgelist(‘…/data/wiki/Wiki_edgelist.txt’,create_using=nx.DiGraph(),nodetype=None,data=[(‘weight’,int)])

model = LINE(G,embedding_size=128,order=‘second’)

model.train(batch_size=1024,epochs=50,verbose=2)

embeddings = model.get_embeddings()

evaluate_embeddings(embeddings)

plot_embeddings(embeddings)

 分类任务结果


micro-F1: 0.6403

macro-F1:0.5286

结果有一定随机性,可以多运行几次,或者稍微调整epoch个数。

 可视化结果


node2vec



前面介绍过基于DFS邻域的DeepWalk和基于BFS邻域的LINE。

node2vec是一种综合考虑DFS邻域和BFS邻域的graph embedding方法。简单来说,可以看作是deepwalk的一种扩展,是结合了DFS和BFS随机游走的deepwalk。

 node2vec 算法原理


1. 优化目标


设 是将顶点 映射为embedding向量的映射函数,对于图中每个顶点,定义 为通过采样策略采样出的顶点 的近邻顶点集合。

node2vec优化的目标是给定每个顶点条件下,令其近邻顶点(如何定义近邻顶点很重要)出现的概率最大。

为了将上述最优化问题可解,文章提出两个假设:

  • 条件独立性假设

假设给定源顶点下,其近邻顶点出现的概率与近邻集合中其余顶点无关。

  • 特征空间对称性假设

这里是说一个顶点作为源顶点和作为近邻顶点的时候共享同一套embedding向量。(对比LINE中的2阶相似度,一个顶点作为源点和近邻点的时候是拥有不同的embedding向量的) 在这个假设下,上述条件概率公式可表示为:

根据以上两个假设条件,最终的目标函数表示为:

由于归一化因子:

的计算代价高,所以采用负采样技术优化。

2. 顶点序列采样策略


node2vec依然采用随机游走的方式获取顶点的近邻序列,不同的是node2vec采用的是一种有偏的随机游走。

给定当前顶点 ,访问下一个顶点的概率为:

是顶点 和顶点之间的未归一化转移概率, 是归一化常数。

node2vec引入两个超参数 来控制随机游走的策略,假设当前随机游走经过边 到达顶点 是顶点 之间的边权。

为顶点和顶点之间的最短路径距离。

下面讨论超参数p和 q对游走策略的影响:

  • Return parameter,p

参数p控制重复访问刚刚访问过的顶点的概率。注意到p仅作用于的情况,而 表示顶点x就是访问当前顶点 之前刚刚访问过的顶点。那么若 p较高,则访问刚刚访问过的顶点的概率会变低,反之变高。

  • In-out papameter,q

q控制着游走是向外还是向内,若,随机游走倾向于访问和t接近的顶点(偏向BFS)。若,倾向于访问远离t的顶点(偏向DFS)。

下面的图描述的是当从t访问到时,决定下一个访问顶点时每个顶点对应的

3. 学习算法


采样完顶点序列后,剩下的步骤就和deepwalk一样了,用word2vec去学习顶点的embedding向量。值得注意的是node2vecWalk中不再是随机抽取邻接点,而是按概率抽取,node2vec采用了Alias算法进行顶点采样。

Alias Method:时间复杂度O(1)的离散采样方法

=============================

https://zhuanlan.zhihu.com/p/54867139

 node2vec 核心代码


1. node2vecWalk


通过上面的伪代码可以看到,node2vec和deepwalk非常类似,主要区别在于顶点序列的采样策略不同,所以这里我们主要关注node2vecWalk的实现。

由于采样时需要考虑前面2步访问过的顶点,所以当访问序列中只有1个顶点时,直接使用当前顶点和邻居顶点之间的边权作为采样依据。当序列多余2个顶点时,使用文章提到的有偏采样。

def node2vec_walk(self, walk_length, start_node):

G = self.G

alias_nodes = self.alias_nodes

alias_edges = self.alias_edges

walk = [start_node]

while len(walk) < walk_length:

cur = walk[-1]

cur_nbrs = list(G.neighbors(cur))

if len(cur_nbrs) > 0:

if len(walk) == 1:

walk.append(cur_nbrs[alias_sample(alias_nodes[cur][0], alias_nodes[cur][1])])

else:

prev = walk[-2]

edge = (prev, cur)

next_node = cur_nbrs[alias_sample(alias_edges[edge][0],alias_edges[edge][1])]

walk.append(next_node)

else:

break

return walk

  1. 构造采样表

preprocess_transition_probs分别生成alias_nodes和alias_edges,alias_nodes存储着在每个顶点时决定下一次访问其邻接点时需要的alias表(不考虑当前顶点之前访问的顶点)。alias_edges存储着在前一个访问顶点为t,当前顶点为 时决定下一次访问哪个邻接点时需要的alias表。

get_alias_edge方法返回的是在上一次访问顶点 t,当前访问顶点为时到下一个顶点的未归一化转移概率:

def get_alias_edge(self, t, v):

G = self.G

p = self.p

q = self.q

unnormalized_probs = []

for x in G.neighbors(v):

weight = G[v][x].get(‘weight’, 1.0)# w_vx

if x == t:# d_tx == 0

unnormalized_probs.append(weight/p)

elif G.has_edge(x, t):# d_tx == 1

unnormalized_probs.append(weight)

else:# d_tx == 2

unnormalized_probs.append(weight/q)

norm_const = sum(unnormalized_probs)

normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs]

return create_alias_table(normalized_probs)def preprocess_transition_probs(self):

G = self.G

alias_nodes = {}

for node in G.nodes():

unnormalized_probs = [G[node][nbr].get(‘weight’, 1.0) for nbr in G.neighbors(node)]

norm_const = sum(unnormalized_probs)

normalized_probs = [float(u_prob)/norm_const for u_prob in unnormalized_probs]

alias_nodes[node] = create_alias_table(normalized_probs)

alias_edges = {}

for edge in G.edges():

alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])

self.alias_nodes = alias_nodes

self.alias_edges = alias_edges

return

 node2vec 应用


使用node2vec在wiki数据集上进行节点分类任务和可视化任务。wiki数据集包含 2,405 个网页和17,981条网页之间的链接关系,以及每个网页的所属类别。通过简单的超参搜索,这里使用p=0.25,q=4的设置。

本例中的训练,评测和可视化的完整代码在下面的git仓库中:

https://github.com/shenweichen/GraphEmbedding

G = nx.read_edgelist(‘…/data/wiki/Wiki_edgelist.txt’,create_using=nx.DiGraph(),nodetype=None,data=[(‘weight’,int)])

model = Node2Vec(G,walk_length=10,num_walks=80,p=0.25,q=4,workers=1)

model.train(window_size=5,iter=3)

embeddings = model.get_embeddings()

evaluate_embeddings(embeddings)

plot_embeddings(embeddings)

 分类任务


micro-F1: 0.6757 macro-F1: 0.5917

这个结果相比于DeepWalk和LINE是有提升的。

 可视化


这个结果相比于DeepWalk和LINE可以看到不同类别的分布更加分散了。

最后补充一个node2vec在业界的应用介绍:

学习遇上复杂网络:解析微信朋友圈 Lookalike 算法当机器


SDNE


SDNE(Structural Deep Network Embedding )是和node2vec并列的工作,均发表在2016年的KDD会议中。可以看作是基于LINE的扩展,同时也是第一个将深度学习应用于网络表示学习中的方法。

SDNE使用一个自动编码器结构来同时优化1阶和2阶相似度(LINE是分别优化的),学习得到的向量表示能够保留局部和全局结构,并且对稀疏网络具有鲁棒性。

 SDNE 算法原理


相似度定义


SDNE中的相似度定义和LINE是一样的。简单来说,1阶相似度衡量的是相邻的两个顶点对之间相似性。2阶相似度衡量的是,两个顶点他们的邻居集合的相似程度。

2阶相似度优化目标



这里我们使用图的邻接矩阵进行输入,对于第个顶点,有,每一个都包含了顶点i的邻居结构信息,所以这样的重构过程能够使得结构相似的顶点具有相似的embedding表示向量。

这里存在的一个问题是由于图的稀疏性,邻接矩阵S中的非零元素是远远少于零元素的,那么对于神经网络来说只要全部输出0也能取得一个不错的效果,这不是我们想要的。

文章给出的一个方法是使用带权损失函数,对于非零元素具有更高的惩罚系数。修正后的损失函数为

其中为逐元素积,,若,则,否则


1阶相似度优化目标


对于1阶相似度,损失函数定义如下

该损失函数可以让图中的相邻的两个顶点对应的embedding vector在隐藏空间接近。

还可以表示为

其中L是图对应的拉普拉斯矩阵,,D是图中顶点的度矩阵,S是邻接矩阵, 

整体优化目标


联合优化的损失函数为

是正则化项,为控制1阶损失的参数,为控制正则化项的参数。

模型结构


先看左边,是一个自动编码器的结构,输入输出分别是邻接矩阵和重构后的邻接矩阵。通过优化重构损失可以保留顶点的全局结构特性(论文的图画错了,上面应该是Global structure preserved cost)。

再看中间一排,就是我们需要的embedding向量,模型通过1阶损失函数使得邻接的顶点对应的embedding向量接近,从而保留顶点的局部结构特性(中间应该是 Local structure preserved cost)

 实现


文章提出使用深度信念网络进行预训练来获得较好的参数初始化,这里我就偷个懒省略这个步骤了~

损失函数定义


l_2nd是2阶相似度对应的损失函数,参数beta控制着非零元素的惩罚项系数。y_true和y_pred分别是输入的邻接矩阵和网络重构出的邻接矩阵。

l_1st是1阶相似度对应的损失函数,参数alpha控制着其在整体损失函数中的占比。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

面试题文档来啦,内容很多,485页!

由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。

1111道Java工程师必问面试题

MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Elasticsearch 24 题 +Memcached + Redis 40题:

Spring 26 题+ 微服务 27题+ Linux 45题:

Java面试题合集:

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-ydhUs9EH-1712762044929)]
[外链图片转存中…(img-kHGeAI3m-1712762044930)]
[外链图片转存中…(img-e6QsjsZz-1712762044930)]
[外链图片转存中…(img-qT31MAGm-1712762044931)]
[外链图片转存中…(img-d7PrQkMg-1712762044931)]
[外链图片转存中…(img-YUnY2AkD-1712762044931)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-bfHIRI9k-1712762044932)]

最后

面试题文档来啦,内容很多,485页!

由于笔记的内容太多,没办法全部展示出来,下面只截取部分内容展示。

1111道Java工程师必问面试题

[外链图片转存中…(img-xNoUJfE5-1712762044932)]

MyBatis 27题 + ZooKeeper 25题 + Dubbo 30题:

[外链图片转存中…(img-Qz9H6hHm-1712762044933)]

Elasticsearch 24 题 +Memcached + Redis 40题:

[外链图片转存中…(img-kDZzIS6a-1712762044933)]

Spring 26 题+ 微服务 27题+ Linux 45题:

[外链图片转存中…(img-JclA76BI-1712762044934)]

Java面试题合集:

[外链图片转存中…(img-QpV2K1AH-1712762044934)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-We71Wn4s-1712762044935)]

  • 28
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值