上篇谈到了deepwalk,整体流程就是按均匀分布从当前节点走到下一个节点,从而采样到一条条“句子”,但是这样采样方式一定是对的吗?边上是否有权重影响呢?走回头路的概率也是等于选择其他邻居节点的.为了使得walk的更合理,node2vec这篇论文给了一个通用的“游走”框架.同样是通过游走产生句子,然后通过w2v产出向量.

DOTA:大有可为的GNN:DeepWalk

Node2vec

从Deepwalk到Node2vec_python

BFS和DFS大家再熟悉不过,node2vec就是结合了这两种方式进行采样.为什么要结合这两种方式呢?因为对于图而言,我们希望每个节点最终学到的向量既表达了同质关系也表达了拓扑关系.比如上图中u和S1是相似的,因为他们之间空间上相近.又比如u和S6是相似的,虽然u和S6之间距离较远,但是因为u和S6周围都围绕了4个节点,所以他们从拓扑上看是类似的.所以如何才能让学到的向量能表达这种信息呢?node2vec就提出了一种随机游走的框架,如下图:

从Deepwalk到Node2vec_深度学习_02

每个边都有权重,当前节点的所有边权重归一化后就是当前节点到其他节点的概率.从上图中我们可以看到,一共有3种类型的边权重,第一种是<v,t>表示当前节点v和上一个节点之间的边权重为1/p,第二种是<v,x1>表示当前节点和临接上一个节点t的边权重为1,第三种就是<v, other>表示与其他节点的边权重为1/q.聪明的炼丹师已经发现,只要p=q=1,就是deepwalk.通过这种框架,如果p在0~1表示算法偏向于走回头路,如果p>1则算法偏向于远离上一个节点.同理如果q在0~1之间,则算法偏向于选择远离t,否则倾向于接近t.整个算法表示如下所示:

从Deepwalk到Node2vec_算法_03

虽然有了一个通用框架是好事,但是我们需要调的参数也多了,我们看看实验中调了哪些参数,如下所示:

从Deepwalk到Node2vec_深度学习_04

我们可以发现,参数对F1-score的影响较大,因此要用的炼丹师们要耐心去调.从实验结果来看,node2vec也表现较优.

从Deepwalk到Node2vec_python_05

代码

这里代码相较于deepwalk要稍微复杂一些,图用networkx进行构建,构建过程不再赘述.

# 核心代码
# 先由这个函数得到计算好的边权重
def preprocess_transition_probs(self):
    '''
    Preprocessing of transition probabilities for guiding the random walks.
    '''
    G = self.G
    is_directed = self.is_directed

    alias_nodes = {}
    for node in G.nodes():
      unnormalized_probs = [G[node][nbr]['weight'] for nbr in sorted(G.neighbors(node))]
      norm_const = sum(unnormalized_probs)
      normalized_probs =  [float(u_prob)/norm_const for u_prob in unnormalized_probs]
      alias_nodes[node] = alias_setup(normalized_probs)

    alias_edges = {}
    triads = {}

    if is_directed:
      for edge in G.edges():
        alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
    else:
      for edge in G.edges():
        alias_edges[edge] = self.get_alias_edge(edge[0], edge[1])
        alias_edges[(edge[1], edge[0])] = self.get_alias_edge(edge[1], edge[0])

    self.alias_nodes = alias_nodes
    self.alias_edges = alias_edges

# 再由这个函数找到路径
def node2vec_walk(self, walk_length, start_node):
    '''
    Simulate a random walk starting from start node.
    start_node 起点
    walk_length 要走的步数
    '''
    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 = sorted(G.neighbors(cur))
      if len(cur_nbrs) > 0:
        # 如果是root,则没有上一个节点
        # 两种情况下计算概率的方式不同
        if len(walk) == 1:
          walk.append(cur_nbrs[alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])])
        else:
          prev = walk[-2]
          next = cur_nbrs[alias_draw(alias_edges[(prev, cur)][0], 
            alias_edges[(prev, cur)][1])]
          walk.append(next)
      else:
        break

    return walk

从Deepwalk到Node2vec_人工智能_06