图学习【参考资料2】-知识补充与node2vec代码注解


本项目参考:
https://aistudio.baidu.com/aistudio/projectdetail/5012408?contributionType=1

*一、正题篇:DeepWalk、word2vec、node2vec

其它相关项目:

关于图计算&图学习的基础知识概览:前置知识点学习(PGL)[系列一] https://aistudio.baidu.com/aistudio/projectdetail/4982973?contributionType=1

图机器学习(GML)&图神经网络(GNN)原理和代码实现(前置学习系列二):https://aistudio.baidu.com/aistudio/projectdetail/4990947?contributionType=1

1.1 DeepWalk算法流程

【图来源:网络,笔记由笔者添上】

算法流程:

【其中使用skip-gram模型是为了利用梯度的方法对参数进行更新训练】

1.2 Skip Gram算法流程

【图来源:网络,笔记由笔者添上】

算法流程:

【其中使用skip-gram模型是为了利用梯度的方法对参数进行更新训练】

此外,关于以上两个算法的定义和概念就不一一展示说明了,后边等有空了补上。下边说一下我学习的一些参考资料,希望对大家有帮助。

参考资料

1.【论文笔记】DeepWalk——陌上疏影凉

1.【网络图模型综述

1.【异构图神经网络简介--机器之心

1.【Graph Embedding之metapath2vec--圈圈_Master

1.【从Random Walk谈到Bacterial foraging optimization algorithm(BFOA),再谈到Ramdom Walk Graph Segmentation图分割算法


二 程序注解

【注解展示,是为了方便自己理解,同时也希望能帮到和自己一样在学习这块知识的小伙伴】

1.0 DeepWalk随机游走的实现



实现Graph类的random_walk函数--可参考

1.1 实现的参考代码--DeepWalk的随机游走算法实现

思路

  1. 理清successor,outdegree函数的输入输出
  2. 查看补全函数的返回类型--分析可能的结果--我最初的猜测:这walks应该是多个向量的集合,最后确实也是【[[],..]这样的结构多用于扩充,然后联想需要学习向量,所以想到向量递推的那种向量集合】
  3. 生成随机采样索引序列集 -- 这个需要匹配采样形状,以及索引阈值--我是参考一个示例修改的,感悟是: 利用随机数0~1,给rand传入指定shape,再乘以一个相同shape的数据,可以得到一个>=0, <=最大出度-1的值,这样就可以用来索引采样了
  4. 理清上下文变量关系,进行zip打包遍历到一起,然后进行聚合操作即可。

具体注解在代码中,可查阅,基本都是按照一行行注解的
from pgl.graph import Graph

import numpy as np

class UserDefGraph(Graph):
    def random_walk(self, nodes, walk_len):
        """
        输入:nodes - 当前节点id list (batch_size,)
             walk_len - 最大路径长度 int
        输出:以当前节点为起点得到的路径 list (batch_size, walk_len)

        用到的函数
        1. self.successor(nodes)
           描述:获取当前节点的下一个相邻节点id列表
           输入:nodes - list (batch_size,)
           输出:succ_nodes - list of list ((num_successors_i,) for i in range(batch_size))
        2. self.outdegree(nodes)
           描述:获取当前节点的出度
           输入:nodes - list (batch_size,)
           输出:out_degrees - list (batch_size,)
        """
        walks = [[node] for node in nodes]   # 首先获得当前节点列表对应的一个向量

        walks_ids = np.arange(0, len(nodes))  # 游走路径中节点对应id号
        cur_nodes = np.array(nodes)          # 当前节点情况
        for l in range(walk_len):   # 根据游走长度进行遍历--破出条件:1. range结束;2. outdegree==0【出度为零,没有可继续的节点】
            """选取有下一个节点的路径继续采样,否则结束"""
            outdegree = self.outdegree(cur_nodes)  # 计算当前节点的出度--也就是对应有哪些位置的邻近节点
            walk_mask = (outdegree != 0)           # 根据出度来确定掩码--True, False--将出度为0的部分复制为False,反之True
            if not np.any(walk_mask):              # 判断是否没有可继续的节点情况--出度为0
               break
            cur_nodes = cur_nodes[walk_mask]       # 根据掩码获取可继续前进的节点,作为后边讨论的当前可前行节点
            walks_ids = walks_ids[walk_mask]       # 获取掩码下,原节点id,组成新的work_ids用于后边讨论,但本身还是作为一个节点的标记,对应这是第几个节点
            outdegree = outdegree[walk_mask]       # 根据掩码获取相应的不为0的出度--用于后边计算前行的路径

            ######################################
            # 请在此补充代码采样出下一个节点
            '''
               [注解有点多,所以放外边了]
               PS:
                 1. successor 可获取当前节点的下一个相邻节点id列表,
                    那么successor 计算出下一节点的集合后,我们需要从中随机取出一个节点--所以我们要创建随机采样的index_list(索引序列集)
                 2. 创建index_list=>为了才到合适的index信息,采用np.floor与np.random,rand()实现:
                    eg: np.floor(np.random.rand(outdegree.shape[0]) * outdegree).astype('int64')
                        np.random.rand(outdegree.shape[0]): 根据出度集的形状来取得相应形状的随机数--这里体现游走的随机性
                        np.random.rand(outdegree.shape[0]) * outdegree:利用生成的随机数与出度集对应元素相乘——这里得到一些列的随机数,随机数范围在0~最大出度值--保证路径有效
                        np.floor(np.random.rand(outdegree.shape[0]) * outdegree)——实现向下取整,这样就得到了相应游走路径中接下来那个点的索引
                    具体实例:
                         np.floor(np.random.rand(20) * 3).astype('int64')
                         result: array([0, 1, 2, 1, 0, 0, 0, 0, 1, 1, 1, 2, 0, 2, 2, 2, 2, 1, 2, 0])
                 3. 既然知道了随机采样的序列集了,那么接下就是分配新的游走路径了
                    next_nodes = []  # 用于后边存放—— 装配有下一个节点的新路径
                    # 参数说明:
                        succ_nodes:相邻节点id列表
                        sample_index:对应出度生成的随即索引集
                        walks_ids:游走路径中节点对应id号
                    # 接下来的循环指的是,将节点列表、随机采样序列、游走路径中节点对应id号一一对应进行填充--得到一个游走情况
                    for s, ind, walk_id in zip(succ_nodes, sample_index, walks_ids):
                        walks[walk_id].append(s[ind])    # 注意: 从开始已经知道walks=>[[], [], []]是这种形式的,这样这里的append,就很容易理解成为相应节点添加可以继续前行的节点,形成一条路径
                        next_nodes.append(s[ind])        # 同时获取接下来要重新讨论游走时所需的新节点--即:如:从1走到了2,从3走到了7: [[1], [3]]=>[[1, 2], [3, 7]]
                                                         # 接下来自然就该考虑把新的2, 7 作为下一次游走时讨论出度的节点啦
            '''
            succ_nodes = self.successor(cur_nodes)  # 返回可继续的节点集合
            # next_nodes = ...
            sample_index = np.floor(np.random.rand(outdegree.shape[0]) * outdegree).astype('int64')
            next_nodes = []
            for s, ind, walk_id in zip(succ_nodes, sample_index, walks_ids):
               walks[walk_id].append(s[ind])
               next_nodes.append(s[ind])
            ######################################
            cur_nodes = np.array(next_nodes)  # 将节点转换为np类型,方便一些操作运算--同时保证前后数据类型
        
        # 遍历完游走长度的次数,就可以返回得到的随机游走路径啦
        return walks

# 可增加轮次提高精度--epoch
# 当前参数精度大概在95%左右
!python my_deepwalk.py --use_my_random_walk --epoch 35 #  35 用自己实现的random walk训练DeepWalk模型,可在 ./tmp/deepwalk/walks/ 中查看构造的节点路径


[INFO] 2022-11-11 14:28:28,099 [my_deepwalk.py:  250]:	Step 1200 DeepWalk Loss: 0.198106  0.242671 s/step.
[INFO] 2022-11-11 14:28:30,539 [my_deepwalk.py:  250]:	Step 1210 DeepWalk Loss: 0.187183  0.309996 s/step.
[INFO] 2022-11-11 14:28:33,171 [my_deepwalk.py:  250]:	Step 1220 DeepWalk Loss: 0.189533  0.244672 s/step.
[INFO] 2022-11-11 14:28:35,537 [my_deepwalk.py:  250]:	Step 1230 DeepWalk Loss: 0.202293  0.232859 s/step.
[INFO] 2022-11-11 14:28:37,920 [my_deepwalk.py:  250]:	Step 1240 DeepWalk Loss: 0.189366  0.244727 s/step.
[INFO] 2022-11-11 14:28:40,450 [my_deepwalk.py:  250]:	Step 1250 DeepWalk Loss: 0.188601  0.254400 s/step.
[INFO] 2022-11-11 14:28:42,875 [my_deepwalk.py:  250]:	Step 1260 DeepWalk Loss: 0.191343  0.247985 s/step.
[INFO] 2022-11-11 14:28:45,286 [my_deepwalk.py:  250]:	Step 1270 DeepWalk Loss: 0.186549  0.255688 s/step.
[INFO] 2022-11-11 14:28:47,653 [my_deepwalk.py:  250]:	Step 1280 DeepWalk Loss: 0.188638  0.240493 s/step.


[INFO] 2022-11-11 14:29:45,898 [link_predict.py:  199]:	Step 180 Train Loss: 0.398023 Train AUC: 0.960870 
[INFO] 2022-11-11 14:29:46,023 [link_predict.py:  223]:			Step 180 Test Loss: 0.399052 Test AUC: 0.960234 
[INFO] 2022-11-11 14:29:48,816 [link_predict.py:  199]:	Step 190 Train Loss: 0.396805 Train AUC: 0.960916 
[INFO] 2022-11-11 14:29:48,951 [link_predict.py:  223]:			Step 190 Test Loss: 0.397910 Test AUC: 0.960275 
[INFO] 2022-11-11 14:29:51,783 [link_predict.py:  199]:	Step 200 Train Loss: 0.396290 Train AUC: 0.960936 
[INFO] 2022-11-11 14:29:51,913 [link_predict.py:  223]:			Step 200 Test Loss: 0.397469 Test AUC: 0.960292 

2.0 SkipGram模型训练

NOTE:在得到节点路径后,node2vec会使用SkipGram模型学习节点表示,给定中心节点,预测局部路径中还有哪些节点。模型中用了negative sampling来降低计算量。



参考 PGL/examples/node2vec/node2vec.py 中的 node2vec_model 函数

2.1 SkipGram模型实现代码参考--理解

  1. 这部分的话,官方代码已经给的很清晰了,这里主要是做一些解释补充--大都可以跟上边算法公式对应着看
  2. 这里采用组合损失--组合损失计算时,要注意在不必要的参数创建后,记得关闭梯度记录--否则会对他求梯度,这样不太好:

    如:ones_label,他只是一个中间量,用于存放结果的,不需要对他求梯度,因为不需要优化它
  3. 还有一点,静态图下,尽量使用layers下的运算方法,避免出现超出计算图的一些逻辑循环操作

    这一部分没什么好说的,大家理解就好--多看看源码哦!
import paddle.fluid.layers as l

def userdef_loss(embed_src, weight_pos, weight_negs):
    """
    输入:embed_src   - 中心节点向量 list (batch_size, 1, embed_size)
         weight_pos  - 标签节点向量 list (batch_size, 1, embed_size)
         weight_negs - 负样本节点向量 list (batch_size, neg_num, embed_size)
    输出:loss - 正负样本的交叉熵 float
    """
    
    ##################################
    # 请在这里实现SkipGram的loss计算过程
    
    ### 负采样计算部分——Multi Sigmoids
    # 分别计算正样本和负样本的 logits(概率)
    pos_logits = l.matmul(
        embed_src, weight_pos, transpose_y=True)  # [batch_size, 1, 1] -- matmul:矩阵相乘
    neg_logits = l.matmul(
        embed_src, weight_negs, transpose_y=True)  # [batch_size, 1, neg_num]

    # 设置正样本标签,并计算正样本loss
    ones_label = pos_logits * 0. + 1.
    ones_label.stop_gradient = True   # 关闭梯度记录
    pos_loss = l.sigmoid_cross_entropy_with_logits(pos_logits, ones_label)  # 交叉熵计算==对应公式2

    # 设置负样本标签,并计算负样本loss
    zeros_label = neg_logits * 0.
    zeros_label.stop_gradient = True
    neg_loss = l.sigmoid_cross_entropy_with_logits(neg_logits, zeros_label)  # 交叉熵计算==对应公式3

    # 总的Loss计算为正样本与负样本loss之和
    loss = (l.reduce_mean(pos_loss) + l.reduce_mean(neg_loss)) / 2   # 得到总的loss
    ##################################
    return loss


NOTE:Node2Vec会根据与上个节点的距离按不同概率采样得到当前节点的下一个节点。


参考; PGL/pgl/graph_kernel.pyx 中用Cython语言实现了节点采样函数node2vec_sample

3.1 Node2Vec采样算法转换代码注解

  1. 这部分代码,用于随机游走后得到的路径,然后对这些路径进行吸收学习,训练图结构
import numpy as np

# 随机节点的获取
def node2vec_sample(succ, prev_succ, prev_node, p, q):

    """
    输入:succ - 当前节点的下一个相邻节点id列表 list (num_neighbors,)
        prev_succ - 前一个节点的下一个相邻节点id列表 list (num_neighbors,)
        prev_node - 前一个节点id int
        p - 控制回到上一节点的概率 float
        q - 控制偏向DFS还是BFS float
    输出:下一个节点id int
    """
    ##################################
    # 请在此实现node2vec的节点采样函数
    # 节点参数信息
    succ_len = len(succ)                # 获取相邻节点id列表节点长度(相对当前)
    prev_succ_len = len(prev_succ)      # 获取相邻节点id列表节点长度(相对前一个节点)
    prev_succ_set = np.asarray([])   # 前一节点的相邻节点id列表

    for i in range(prev_succ_len):     # 遍历得到前一节点的相邻节点id列表的新list——prev_succ_set,用于后边概率的讨论
        # 将前一节点list,依次押入新的list中
        prev_succ_set = np.append(prev_succ_set,prev_succ[i])  # ? prev_succ_set.insert(prev_succ[i])
    
    # 概率参数信息
    probs = []     # 保存每一个待前往的概率
    prob = 0   # 记录当前讨论的节点概率
    prob_sum = 0.  # 所有待前往的节点的概率之和

    # 遍历当前节点的相邻节点
    for i in range(succ_len):    # 遍历每一个当前节点前往的概率
        if succ[i] == prev_node:  # case 1 : 采样节点与前一节点一致,那么概率为--1/q(原地)
            prob = 1. / p
        # case 2 完整的应该是: np.where(prev_succ_set==succ[i]) and np.where(succ==succ[i])
        # 但是因为succ本身就是采样集,所以np.where(succ==succ[i])总成立,故而忽略,不考虑
        elif np.where(prev_succ_set==succ[i]):   # case 2  : 采样节点在前一节点list内,那么概率为--1   ?cpython中的代码: prev_succ_set.find(succ[i]) != prev_succ_set.end()
            prob = 1.
        elif np.where(prev_succ_set!=succ[i]):   # case 3  : 采样节点不在前一节点list内,那么概率为--1/q
            prob = 1. / q
        else:
            prob = 0.       # case 4 : other

        probs.append(prob)  # 将待前往的每一个节点的概率押入保存
        prob_sum += prob    # 计算所有节点的概率之和
    
    RAND_MAX = 65535   # 这是一个随机数的最值,用于计算随机值的--根据C/C++标准,最小在30000+,这里取2^16次方
    rand_num = float(np.random.randint(0, RAND_MAX+1)) / RAND_MAX * prob_sum  # 计算一个随机概率:0~prob_sum. ?cpython中的代码: float(rand())/RAND_MAX * prob_sum

    sampled_succ = 0.   # 当前节点的相邻节点中确定的采样点

    # rand_num => 是0~prob_num的一个值,表示我们的截取概率阈值--即当遍历了n个节点时,若已遍历的节点的概率之和已经超过了rand_num
    # 我们取刚好满足已遍历的节点的概率之和已经超过了rand_num的最近一个节点作为我们的采样节点
    # 比如: 遍历到第5个节点时,权重概率和大于等于rand_num,此时第5个节点就是对应的采样的节点了
    # 为了方便实现:这里利用循环递减--判断条件就变成了————当rand_num减到<=0时,开始采样节点
    for i in range(succ_len):   # 遍历当前节点的所有相邻节点
        rand_num -= probs[i]    # 利用rand_num这个随机获得的概率值作为依据,进行一个循环概率检验
        if rand_num <= 0:   # 当遇到第一次使得rand_num减到<=0后,说明到这个节点为止, 遍历应该终止了,此时的节点即未所求的节点,【停止检验条件】
            sampled_succ = succ[i]   # 并把当前节点作为确定的节点
            return sampled_succ   # 返回待采样的节点--节点一定在succ中

[INFO] 2022-11-11 14:38:49,133 [link_predict.py:  199]:	Step 170 Train Loss: 0.454199 Train AUC: 0.954936 
[INFO] 2022-11-11 14:38:49,260 [link_predict.py:  223]:			Step 170 Test Loss: 0.454974 Test AUC: 0.954118 
[INFO] 2022-11-11 14:38:51,997 [link_predict.py:  199]:	Step 180 Train Loss: 0.452219 Train AUC: 0.955133 
[INFO] 2022-11-11 14:38:52,122 [link_predict.py:  223]:			Step 180 Test Loss: 0.453069 Test AUC: 0.954312 
[INFO] 2022-11-11 14:38:54,851 [link_predict.py:  199]:	Step 190 Train Loss: 0.450969 Train AUC: 0.955254 
[INFO] 2022-11-11 14:38:54,978 [link_predict.py:  223]:			Step 190 Test Loss: 0.451892 Test AUC: 0.954428 
[INFO] 2022-11-11 14:38:57,714 [link_predict.py:  199]:	Step 200 Train Loss: 0.450440 Train AUC: 0.955305 
[INFO] 2022-11-11 14:38:57,842 [link_predict.py:  223]:			Step 200 Test Loss: 0.451436 Test AUC: 0.954473 


1. 回顾并总结了图的基本概念。
2. 学习思考算法实现的代码思路--Node2Vec的实现以及RandomWalk的实现。
3. 对源码阅读能力的提升。
其它相关笔记:
关于图计算&图学习的基础知识概览:前置知识点学习(PGL)[系列一] https://aistudio.baidu.com/aistudio/projectdetail/4982973?contributionType=1 图机器学习(GML)&图神经网络(GNN)原理和代码实现(前置学习系列二):https://aistudio.baidu.com/aistudio/projectdetail/4990947?contributionType=1
* 如果我的项目对你有帮助不如点一个心,fork一下,以备可以常复习哦!