资讯详情

node2vec代码实现及详细解析

目录

  • 前言
  • 1.数据导入
  • 2.node2vecWalk
    • 2.1 转移概率归一化
      • 2.1.1 原理解析
      • 2.1.2 Alias采样
      • 2.1.3 代码实现
    • 2.2 node2vecWalk的实现
  • 3.LearnFeatures
  • 4.参数选择
  • 5.完整代码

前言

在KDD 2016 | node2vec:Scalable Feature Learning for Networks 我们详细讨论了node2vec但代码没有实现。本文将从原文出发,逐步详细讨论如何逐步实现node2vec。

1.数据导入

数据为《Les Misérables network》,也就是《悲剧世界》中的人物关系网络:这张图是一张无向图,图中有77个节点和254个边缘。节点表示《悲剧世界》中的人物,两个节点之间的边缘表示这两个人物出现在书的同一章,边缘的权重表示两个人物(节点)出现在同一章中的频率。

import networkx as nx G = nx.les_miserables_graph() 

原文中node2vec算法描述如下: 在这里插入图片描述 其中用于从节点生成 u u u开始的长为 l l l游走序列,使用所有节点产生的游行序列word2vec,得到每个节点的向量表示。

下面我将结合原文详细介绍这两种算法!

2.node2vecWalk

2.1 归一化转移概率

2.1.1 原理解析

node2vecWalk该算法的伪代码描述如下:

  1. 将初始节点 u u u加入到walk中
  2. 当前节点curr为walk最后一个节点
  3. 根据curr选择下一个节点,并选择周围节点的转移概率 s s s,并将其加入walk
  4. 当walk长度为 l l l收集结束,返回walk

描述模糊,我们再看原文:

当前节点是 v v v,下一个要收集的节点是 x x x,我们有以下概率公式: 我们可以找到

采样概率是一种归一化的转移概率: π v x Z \frac{\pi_{vx}} {Z} Zπvx​​,原文中 π v x \pi_{vx} πvx​的描述为: 观察上图:节点 t t t已经被采样了,紧接着我们采样了节点 v v v,现在需要做的是采样 v v v之后的下一次采样。根据前文所述,我们只会在节点 v v v的邻接节点中选择一个进行采样,也就是在三个 x x x中进行采样。这个时候 π v x = α p q ( t , x ) ⋅ w v x \pi_{vx}=\alpha_{pq}(t,x) \cdot w_{vx} πvx​=αpq​(t,x)⋅wvx​,其中 w v x w_{vx} wvx​表示两个节点间的权重(如果是无权图,则权重为1)

如果我们是进行第二次采样(第一次是初始结点 u u u),则有 v = u v=u v=u, x x x表示与 u u u相连的节点。这个时候我们就会发现没法利用 π v x = α p q ( t , x ) ⋅ w v x \pi_{vx}=\alpha_{pq}(t,x) \cdot w_{vx} πvx​=αpq​(t,x)⋅wvx​来计算两个节点间的转移概率,因为不存在节点 t t t,也就没法计算 α p q ( t , x ) \alpha_{pq}(t, x) αpq​(t,x)!!

此时的 π v x \pi_{vx} πvx​就是 w v x w_{vx} wvx​。

因此,第一步的思路就很明确了:

  1. 如果当前要进行第二次采样,我们就算出第一个节点 u u u到其所有节点的归一化转移概率:非归一化转移概率 π v x \pi_{vx} πvx​就是节点 u u u与其邻接节点相连边的权重(可能不在01间),归一化就是将所有概率变换到01之间:所有概率除以归一化常数 Z Z Z, Z Z Z为这些权重之和。
  2. 否则,我们则根据当前节点 v v v和上一个被采样的节点 t t t,算出 v v v到其所有邻接节点 x x x的非归一化转移概率: π v x = α p q ( t , x ) ⋅ w v x \pi_{vx}=\alpha_{pq}(t,x) \cdot w_{vx} πvx​=αpq​(t,x)⋅wvx​,然后同样除以它们的和来进行归一化。

2.1.2 Alias采样

当我们有了当前节点对其所有邻接节点的转移概率后,我们应该怎么选择?是

原文中算法描述如下: 可以看到作者采用了一种叫做AliasSample的采样方法,AliasSample,又名“别名采样”,是一种比较经典的采样算法。比如:游戏中经常遇到按一定的掉落概率来随机掉落指定物品的情况,例如掉落银币25%,金币20%, 钻石10%,装备5%,饰品40%。

在本文中可以描述为:我们已经有了待选节点集,也有了选择它们的概率,现在我们要进行下一次选择,要求该选择符合上述概率要求。

在Alias Sample中,我们输入一个概率列表,最后会得到两个数组:Prob和Ailas 然后:随机取某一列 k k k(即[1,4]的随机整数),再随机产生一个[0-1]的小数 c c c,如果 P r o b [ k ] > c Prob[k] > c Prob[k]>c,那么采样结果就是 k k k,反之则为Alias[k]。

关于Alias Sample的详细原理可以参考:Alias Sample,这里不再详细介绍。

2.1.3 代码实现

有了以上思路后我们就很容易编写代码了:

  1. 对于第一种情况:我们可以初始化一个字典nodes_info,nodes_info[node]表示与node有关的所有信息,
  2. 对于第二种情况,我们可以初始化一个字典edges_info,

因此,代码实现如下:

def init_transition_prob(self):
    """ :return:归一化转移概率矩阵 """
    g = self.G
    nodes_info, edges_info = { 
        }, { 
        }
    for node in g.nodes:
        nbs = sorted(g.neighbors(node)) # 当前节点的邻居节点
        probs = [g[node][n]['weight'] for n in nbs] # 概率就是权重
        # 归一化
        norm = sum(probs)  # 求和
        normalized_probs = [float(n) / norm for n in probs]  # 归一化
        nodes_info[node] = self.alias_setup(normalized_probs)  # 利用Alias采样得到Alias和Prob

    for edge in g.edges:
        # 有向图
        if g.is_directed():
            edges_info[edge] = self.get_alias_edge(edge[0], edge[1])
        # 无向图
        else:
            edges_info[edge] = self.get_alias_edge(edge[0], edge[1])
            edges_info[(edge[1], edge[0])] = self.get_alias_edge(edge[1], edge[0])

    self.nodes_info = nodes_info
    self.edges_info = edges_info

其中alias_setup函数就是Alias Sample的具体实现,代码直接参考了网上现成的:

def alias_setup(self, probs):
    """ :probs: v到所有x的概率 :return: Alias数组与Prob数组 """
    K = len(probs)
    q = np.zeros(K)  # 对应Prob数组
    J = np.zeros(K, dtype=np.int)  # 对应Alias数组
    # Sort the data into the outcomes with probabilities
    # that are larger and smaller than 1/K.
    smaller = []  # 存储比1小的列
    larger = []  # 存储比1大的列
    for kk, prob in enumerate(probs):
        q[kk] = K * prob  # 概率
        if q[kk] < 1.0:
            smaller.append(kk)
        else:
            larger.append(kk)

    # Loop though and create little binary mixtures that
    # appropriately allocate the larger outcomes over the
    # overall uniform mixture.

    # 通过拼凑,将各个类别都凑为1
    while len(smaller) > 0 and len(larger) > 0:
        small = smaller.pop()
        large = larger.pop()

        J[small] = large  # 填充Alias数组
        q[large] = q[large] - (1.0 - q[small])  # 将大的分到小的上

        if q[large] < 1.0:
            smaller.append(large)
        else:
            larger.append(large)

    return J, q

对于不是第二次采样的情况,需要利用get_alias_edge来得到一条边 ( t , v ) (t, v) (t,v)中 v v v到其邻居节点转移概率的Alias数组和Prob数组:

def get_alias_edge(self, t, v):
    """ Get the alias edge setup lists for a given edge. """
    g = self.G
    p = self.p
    q = self.q
    unnormalized_probs = []
    for v_nbr in sorted(g.neighbors(v)):
        if v_nbr == t:
            unnormalized_probs.append(g[v][v_nbr]['weight'] / p)
        elif g.has_edge(v_nbr, t):
            unnormalized_probs.append(g[v][v_nbr]['weight'])
        else:
            unnormalized_probs.append(g[v][v_nbr]['weight'] / q)
    norm_const = sum(unnormalized_probs)
    normalized_probs = [float(u_prob) / norm_const for u_prob in unnormalized_probs]

    return self.alias_setup(normalized_probs)

代码很容易理解:考虑v的邻居节点v_nbr:如果v_nbr就是t,则非归一化转移概率为 1 p × w \frac{1}{p} \times w p1​×w;如果v_nbr与t间有边,也就是图中 x 1 x_1 x1​,则 α p q ( t , x ) = 1 \alpha_{pq}(t, x)=1 αpq​(t,x)=1,即非归一化转移概率为 1 × w 1 \times w 1×w;否则就是图中 x 2 , x 3 x_2,x_3 x2​,x3​这种情况,非归一化转移概率为 1 q × w \frac{1}{q} \times w q1​×w。 w w w是两个节点间边的权重。

有了非归一化转移概率后,我们再进行归一化(除以和),最后再利用alias_setup函数获得Alias数组和Prob数组。

当我们有了各个节点间的转移概率时,我们在真正采样时需要做出决策,在这些相邻结点中选择一个作为下一个被采样的节点:随机取某一列 k k k(即[1,n]的随机整数,n为邻居节点的个数),再随机产生一个[0-1]的小数 c c c,如果 P r o b [ k ] > c Prob[k] > c Prob[k]>c,那么采样结果就是 k k k,反之则为Alias[k]。该采样函数实现较为简单:

def alias_draw(self, J, q):
    """ 输入: Prob数组和Alias数组 输出: 一次采样结果 """
    K = len(J)
    # Draw from the overall uniform mixture.
    kk = int(np.floor(npr.rand() * K))  # 随机取一列

    # Draw from the binary mixture, either keeping the
    # small one, or choosing the associated larger one.
    if npr.rand() < q[kk]:  # 比较
        return kk
    else:
        return J[kk]

其中kk或者J[kk]就是我们最终采样的结果。

2.2 node2vecWalk的实现

有了转移概率以及采样策略后,我们就能轻松实现node2vecWalk了: 实现代码如下:

def node2vecWalk(self, u):
    walk = [u]
    g = self.G
    l = self
        标签: vec2r7505qg超级电容

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

 锐单商城 - 一站式电子元器件采购平台  

 深圳锐单电子有限公司