图网络学习报告

PGL图网络学习

1.1、异构图的定义:

简单来说就是:包含至少两种类型的节点或边的图结构。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在这里插入图片描述

图元:

​ 这里可以指异构图中按同构图等定义获得的一些系列的同构图,这些同构图就是异构图中的子图——也就是图元。对于图元,简单来说就是原图结构中的子图,不局限于异构图中的子同构图,也可以任意规则定义下的子图。而图中的黄色和紫色就可以组成至少两个图元。

1.2、同构图和异构图的数学表示:

同构图:G(V,E) —— V表示节点,E表示边(关系)

异构图:G(V, E, T) —— 相对同构图多了一个表示类别的维度

2.1、DeepWalk:

算法流程:
在这里插入图片描述

2.2、Skip Gram:

算法流程:

在这里插入图片描述

2.3、DeepWalk随机游走的实现:

在这里插入图片描述

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
2.4、SkipGram模型训练:

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

在这里插入图片描述

代码思考:

  1. 这里采用组合损失–组合损失计算时,要注意在不必要的参数创建后,记得关闭梯度记录–否则会对他求梯度,这样不太好:
    如:ones_label,他只是一个中间量,用于存放结果的,不需要对他求梯度,因为不需要优化它
  2. 还有一点,静态图下,尽量使用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
2.5、Node2Vec采样算法

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

node2vec引入两个超参数 p p p q q q来控制随机游走的策略,假设当前随机游走经过边 ( t , v ) (t,v) (t,v)到达顶点 v v v,设 π v x = α ( t , x ) ⋅ w v x \pi_{vx}=α(t,x)·w_{vx} πvx=α(t,x)wvx w v x w_{vx} wvx是顶点 v v v x x x之间的边权。

在这里插入图片描述

原理:

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

f ( u ) f(u) f(u)是将顶点 u u u映射为embedding向量的映射函数,对于途中每个顶点 u u u,定义 N s ( u ) N_s(u) Ns(u)为通过采样策略 S S S采样出的顶点 u u u的近邻顶点集合。

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

代码注解:

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

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中
4.1、图采样

在这里插入图片描述

1.GraphSAGE

在这里插入图片描述

2.PinSAGE
sampled_succ = succ[i]   # 并把当前节点作为确定的节点
        return sampled_succ   # 返回待采样的节点--节点一定在succ中

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值