百度图神经网络——论文节点比赛baseline代码注解

文章目录

  • 一、项目介绍
  • 二、BaseLine内容注解
    • 1.Config部分注解
    • 2. 数据读取与处理部分
      • 2.1 边数据的加载与处理
      • 2.2 数据的完整加载与处理
      • 2.3 数据读取与分割
    • 3. 模型加载部分
    • 4. 模型训练过程
  • 三、model.py的内容解析
    • 1 GCN模型代码讲解
    • 2. GAT模型代码讲解
    • 3. APPNP模型代码讲解
      • 3.1 SGC模型代码讲解——(APPNP模型补充)
    • 4. GCNII模型代码讲解
  • 四、build_model.py的内容解析
  • 五、总结

比赛地址:常规赛:论文引用网络节点分类
文章转载自:图网络笔记–论文节点比赛baseline代码注解,感谢红白黑大佬的笔记

一、项目介绍

  1. 数据分析处理部分–代码注解
  2. 模型加载–program空间配置部分–代码注解
  3. 训练部分–代码注解
  4. build_model.py 以及 model.py内容代码注解

二、BaseLine内容注解

  1. 注意,每一次调整使用的模型或者更管模型后,要记得重启notebook,避免运行失败或者运行上一次的模型等
    【ps: 重启运行时,可以跳过依赖下载,但请记得运行从import sys; sys.path.append(’/home/aistudio/external-libraries’)开始运行】

  2. 自定义模型时,要注意参考baseline重点例子,如何创建一个可用的模型【后边会说一下,大家不用担心,很简单的】

1.Config部分注解

from easydict import EasyDict as edict

'''模块说明:
    easydict这个模块下的EasyDict,可以使得创建的字典像访问属性一样
    eg:
    dicts = {'A': 1}
    print(dicts['A'])  => 1

    使用EasyDict之后:
    dicts = EasyDict(dicts)
    print(dicts.A)  => 1
'''

# 模型参数字典
config = {
    "model_name": "GCNII",
    "num_layers": 1,           # 网络层数--这个实现在模型类的forward里边,通过循环实现
    "dropout": 0.5,            # 训练时,参数drop概率
    "learning_rate": 0.0002,    # 训练优化的学习率
    "weight_decay": 0.0005,    # 权重正则化率
    "edge_dropout": 0.00,      # 边drop概率
}

config = edict(config)  # 利用EasyDict便利字典的读取

2. 数据读取与处理部分

2.1 边数据的加载与处理

# 加载边数据
def load_edges(num_nodes, self_loop=True, add_inverse_edge=True):
    '''
        input:
            num_nodes: 节点数
            self_loop: 是否加载自环边
            add_inverse_edge: 是否添加反转的边--我的理解是正反都添加--即对应无向图的情况
    '''
    # 从数据中读取边
    edges = pd.read_csv("work/edges.csv", header=None, names=["src", "dst"]).values
    
    # 反转边添加
    if add_inverse_edge:
        edges = np.vstack([edges, edges[:, ::-1]])  # vstack沿竖直方向拼接--如:A =[1, 2] , B = [2, 3]; vstack([A, B]) => [[1, 2], [2, 3]]
        # eg: edges=[[1, 3], [2, 5], [6, 7]] => edges[:, ::-1]=[[3, 1], [5, 2], [7, 6]]
        # 再拼接就得到了正反边的一个集合了

    # 自环边添加
    if self_loop:
        src = np.arange(0, num_nodes)          # 定义n和节点作为起点
        dst = np.arange(0, num_nodes)          # 定义n个节点作为终点--且与src一一对应
        self_loop = np.vstack([src, dst]).T    # 再将两个行向量拼接(此时shape:[2, num_nodes]), 然后再转置T=>得到shape:[num_node, 2]这是的数据0->0, 1->1 ...就得到了自环边的数据
        edges = np.vstack([edges, self_loop])  # 将自环边数据添加到本身的边数据中
     
    return edges

2.2 数据的完整加载与处理

def load():
    # 从数据中读取点特征和边,以及数据划分
    node_feat = np.load("work/feat.npy")    # 读取节点特征--每个节点100个特征
    num_nodes = node_feat.shape[0]          # shape[0] 正好对应节点个数
    edges = load_edges(num_nodes=num_nodes, self_loop=True, add_inverse_edge=True)   # 根据实际传入的节点数,返回合理的边--这里包含自环边以及正向和反向的边

    graph = pgl.graph.Graph(num_nodes=num_nodes, edges=edges, node_feat={"feat": node_feat})  # 创建图:节点数、边数据、以及节点特征的字典
    
    indegree = graph.indegree()    # 计算当前图的所有节点的入度--返回一个list==>等价于graph.indegree(nodes=None),nodes指定,返回指定的入度

    norm = np.maximum(indegree.astype("float32"), 1)  # 取最大入度中的一个然后返回
    norm = np.power(norm, -0.5)   # 利用这个最大入读计算一个归一化参数
    graph.node_feat["norm"] = np.expand_dims(norm, -1) # 将归一化参数添加到节点的norm特征中, shape[1], 只含有一个元素的序列,但不算标量:如,a 和 [a]
    
    df = pd.read_csv("work/train.csv")      # 读取总的训练数据
    node_index = df["nid"].values           # 读取总的节点的索引序列(集)
    node_label = df["label"].values         # 读取总的节点的label序列
    train_part = int(len(node_index) * 0.8) # 划分训练数据集--80%--这里是计算一个训练集数目值
    train_index = node_index[:train_part]   # 利用训练集数目进行划分--0:train_part
    train_label = node_label[:train_part]   # 训练label划分
    valid_index = node_index[train_part:]   # 验证数据valid_index划分
    valid_label = node_label[train_part:]   # 验证valid_label划分

    test_index = pd.read_csv("work/test.csv")["nid"].values  # 读取测试集--也就是赛题提交数据--指定读取['nid']列数据
    
    # 这是一个可以使用名字来访问元素内容的tuple子类
    # 所以直接对应传入数据即可
    dataset = Dataset(graph=graph, 
                    train_label=train_label,
                    train_index=train_index,
                    valid_index=valid_index,
                    valid_label=valid_label,
                    test_index=test_index, num_classes=35)

    return dataset # 最后返回dataset数据

2.3 数据读取与分割

这一部分的分割和load中的命名索引元组有关!

dataset = load()  # 执行load函数获取完整的dataset(可命名索引的tuple)数据

# 从dataset中读取出相应数据
train_index = dataset.train_index                               # 读取训练索引序列
train_label = np.reshape(dataset.train_label, [-1 , 1])         # 读取训练label序列
train_index = np.expand_dims(train_index, -1)                   # 在最后一位添加一个维度,保证数据向量化[[a]]

val_index = dataset.valid_index                                 # 读取验证索引序列
val_label = np.reshape(dataset.valid_label, [-1, 1])            # 读取训练label序列
val_index = np.expand_dims(val_index, -1)                       # 在最后一位添加一个维度,保证数据向量化[[a]]

test_index = dataset.test_index                                 # 读取训练索引序列
test_index = np.expand_dims(test_index, -1)                     # 在最后一位添加一个维度,保证数据向量化[[a]]
test_label = np.zeros((len(test_index), 1), dtype="int64")      # 用于保存最终结果--提前用zeros创建一个空白矩阵,并指明数据类型

3. 模型加载部分

import pgl
import model      # model.py
import paddle.fluid as fluid
import numpy as np
import time
from build_model import build_model  # build_model.py

use_gpu = True
place = fluid.CUDAPlace(0) if use_gpu else fluid.CPUPlace()   # 工作环境--这里有修改--需要用cpu只需要把use_gpu设置为False即可


train_program = fluid.default_main_program()                # 创建主program  -- paddle静态图都是在相应的program中运行的 -- 通常为start
startup_program = fluid.default_startup_program()           # 创建start_program -- 是我们运行的开始


# 以下是配置执行器执行空间(block)的操作部分--个人觉得,如果只是使用,记住使用规范即可
# program_guard接口配合使用python的 with 语句来将 with block 里的算子和变量添加进指定的全局主程序(main program)和启动程序(startup program)。
with fluid.program_guard(train_program, startup_program):   # 以下执行的算子等都会放入到train_program->startup_program的block中
    with fluid.unique_name.guard():   # 开启一个命名空间--常用program_guard一起使用
        # 这里使用到build_model.py中的函数,执行模型和参数的配置,并返回相关的data变量
        # 这个过程的算子都会被记录到(train_program, startup_program)对应的工作空间中
        gw, loss, acc, pred = build_model(dataset,   
                            config=config,
                            phase="train",
                            main_prog=train_program)


# 创建一个新的Program作为test_program
test_program = fluid.Program()
with fluid.program_guard(test_program, startup_program):   # 含义如上,这里是开启(test_program, startup_program)的工作空间,并记录相应的算子、变量
    with fluid.unique_name.guard():                        # 开启一个命名空间
        # 返回test的模型参数等 
        _gw, v_loss, v_acc, v_pred = build_model(dataset,
            config=config,
            phase="test",
            main_prog=test_program)

# 总结——program_guard确定工作环境--unique_name开启一个相应的命名空间,相辅相成。

test_program = test_program.clone(for_test=True)  # 克隆test_program

exe = fluid.Executor(place)   # 创建一个解释器

4. 模型训练过程

epoch = 4000                       # 训练轮次
exe.run(startup_program)           # 执行器运行-->优先执行

# 将图数据变成 feed_dict 用于传入Paddle Excecutor
# 图数据原型:graph = pgl.graph.Graph(num_nodes=num_nodes, edges=edges, node_feat={"feat": node_feat})  # 创建图:节点数、边数据、以及节点特征的字典
feed_dict = gw.to_feed(dataset.graph)    # 调用to_feed方法,将图数据转换为feed_dict,用于执行器的输入参数

# 训练开始
for epoch in range(epoch):
    # Full Batch 训练  ==  单batch_size训练--全数据一次投入
    # 设定图上面那些节点要获取
    # node_index: 训练节点的nid    
    # node_label: 训练节点对应的标签
    feed_dict["node_index"] = np.array(train_index, dtype="int64")   # 往feed_dict中添加键值对数据--每一个轮次数据都会重新赋值更新
    feed_dict["node_label"] = np.array(train_label, dtype="int64")
    
    train_loss, train_acc = exe.run(train_program,      # 执行器执行--执行train_program这个program空间内的算子和参数
                                feed=feed_dict,         # 传入的数据:graph..., node_index, node_label
                                fetch_list=[loss, acc], # 需要计算返回的数据
                                return_numpy=True)      # 返回numpy数据

    # Full Batch 验证  ==  单batch_size验证--全数据一次投入
    # 设定图上面那些节点要获取
    # node_index: 训练节点的nid    
    # node_label: 训练节点对应的标签
    feed_dict["node_index"] = np.array(val_index, dtype="int64")   # 往feed_dict中添加键值对数据--每一个轮次数据都会重新赋值更新
    feed_dict["node_label"] = np.array(val_label, dtype="int64")

    val_loss, val_acc = exe.run(test_program,            # 执行器执行--执行test_program这个program空间内的算子和参数
                            feed=feed_dict,              # 传入的数据:graph..., node_index, node_label
                            fetch_list=[v_loss, v_acc],  # 需要计算返回的数据
                            return_numpy=True)           # 返回numpy数据

    print("Epoch", epoch, "Train Acc", train_acc[0], "Valid Acc", val_acc[0])   # 打印训练数据

三、model.py的内容解析

1 GCN模型代码讲解

GCN需要用到的一个计算归一参数的方法

def get_norm(indegree):
    '''
        入度归一化函数: 返回一个浮点数类型的最小值为1.0的入度值序列
        入度值:可以表示无向图中当前节点的邻边,而对于有向图则是指向当前节点的边数
    '''

    float_degree = L.cast(indegree, dtype="float32")  # data的类型转换后的值返回给float_degree,值返回
    float_degree = L.clamp(float_degree, min=1.0)     # 值裁剪--将其中小于1的值赋值为1.0 -->个人的考虑是,添加自环边的入度
    norm = L.pow(float_degree, factor=-0.5)           # 倒数开根号,获取归一化的入度
    # TODO: 度为float类型?
    # CALL: 为了后边方便用于计算,float数据更适合后边所需的运算

    return norm  # 返回一个归一化的度,用于公式计算

GCN模型代码注解

class GCN(object):
    """Implement of GCN
    """
    def __init__(self, config, num_class):
        self.num_class = num_class                          # 节点种类
        self.num_layers = config.get("num_layers", 1)       # 模型层数
        self.hidden_size = config.get("hidden_size", 64)    # 中间层输出大小--不一定只有一层中间层哈--跟num_layers有关
        self.dropout = config.get("dropout", 0.5)           # fc层的drop率
        self.edge_dropout = config.get("edge_dropout", 0.0) # 边drop率--为了获取一个忽略指定数目的随机子图(忽略部分边属性,然后生成一个新的子图用于训练--仅仅用于训练而已)

    def forward(self, graph_wrapper, feature, phase):
        '''
            graph_wrapper: 一个图容器,用于存储图信息,并可以迭代训练与预测
            feature:图的节点特征
            phase:指令--train or eval or test等

            功能:
                实现将输入的图进行一个简单的处理--通过n层图卷积实现特征提取,然后经过一个dropout层抑制过拟合;
                最后经过第二个图卷积获取类别数的输出,根据相应的处理得到需要的预测结果。

                如:softmax进行一个类别处理,利用argmax的到分类的类别【具体过程详见build_model.py】

                '
                    注意: 在GCN中,需要计算一个norm值,用于GCN的推断训练,以及后期的预测【详见GCN推导的公式】
                '
        '''
        
        # GCN这个layer的返回值:张量shape:(num_nodes,hidden_​​size)
        for i in range(self.num_layers):  # 根据层数进行迭代

            if phase == "train":  # 训练模式--才有边drop

                # 每次调用edge_drop(graph_wrapper, self.edge_dropout)结果可能不同
                ngw = pgl.sample.edge_drop(graph_wrapper, self.edge_dropout)   # 传入输入图,然后根据edge_dropout随机生成忽略某些边属性的新子图
                norm = get_norm(ngw.indegree())   # 归一化出度--得到计算参数
            else:  # eval/test模式

                ngw = graph_wrapper   # 新子图就是原始图
                norm = graph_wrapper.node_feat["norm"]  # 


            # 利用pgl自带的网络进行配置
            feature = pgl.layers.gcn(ngw,    # 传入图
                feature, # 相应的特征--训练过程中,最多只是对边有修改,并不涉及节点变化
                self.hidden_size,  # 输出大小
                activation="relu", # 激活函数
                norm=norm,         # 归一化值--用于gcn公式计算
                name="layer_%s" % i)  # 层名称

            # 在此后紧跟dropout进行,防止过拟合
            # 根据给定的丢弃概率,dropout操作符按丢弃概率随机将一些神经元输出设置为0,其他的仍保持不变。
            feature = L.dropout(   
                    feature,      # 上一级的输出
                    self.dropout, # drop率
                    dropout_implementation='upscale_in_train')  # drop配置upscale_in_train表示,仅在训练时drop,评估预测不实现drop
        
        # 将以上迭代部分做完后,再通过下边这个部分输出结果
        if phase == "train":   # 训练模式下

            ngw = pgl.sample.edge_drop(graph_wrapper, self.edge_dropout)  # 同前边一样的过滤一些边--基本效果同普通的dropout,这里作用于边而已
            norm = get_norm(ngw.indegree())
        else:
            ngw = graph_wrapper
            norm = graph_wrapper.node_feat["norm"]

        # 再通过一层图卷积层
        feature = conv.gcn(ngw,  
                     feature,
                     self.num_class,   # 输出结果就是我们实际节点训练或预测输出的类别情况:详见PS1
                     activation=None,
                     norm=norm,
                     name="output")    # 最后返回一个shape[-1]=num_class的数据,然后我们对数据处理只需要经过一个softmax,再argmax就得到了预测得到了节点的类别了

        return feature 

2. GAT模型代码讲解

class GAT(object):
    """Implement of GAT"""
    def __init__(self, config, num_class):
        self.num_class = num_class   # 类别数
        self.num_layers = config.get("num_layers", 1)   # 层数
        self.num_heads = config.get("num_heads", 8)   # 多头注意力 -- 8*8尽量别改
        self.hidden_size = config.get("hidden_size", 8)  # 中间层输出大小--不一定只有一层中间层哈--跟num_layers有关 
        self.feat_dropout = config.get("feat_drop", 0.6)  # 特征drop率
        self.attn_dropout = config.get("attn_drop", 0.6)    # 参数drop率
        self.edge_dropout = config.get("edge_dropout", 0.0)    # 边drop率


	def forward(self, graph_wrapper, feature, phase):
	    '''
	        graph_wrapper: 一个图容器,用于存储图信息,并可以迭代训练与预测
	        feature:图的节点特征
	        phase:指令--train or eval or test等
	
	        功能:
	            首先根据运行模式,确定edge_drop率
	            然后进入网络叠加的循环中,进行pgl.sample.edge_drop后的子图获取,接着通过一个gat的layer--头尾8,输出大小为8,得到可叠加的特征输出
	            循环结束后,再通过一个头为1,输出大小为num_class的gat,得到输出结果
	
	            根据相应的处理得到需要的预测结果。
	            如:softmax进行一个类别处理,利用argmax的到分类的类别  【具体过程详见build_model.py】
	    '''
	    if phase == "train":   # 训练模式才会进行边drop
	        edge_dropout = self.edge_dropout
	    else:
	        edge_dropout = 0
	
	    # 在GAT中,只需要简单进行遍历层叠加即可
	    # GAT这个layer的返回值:张量shape:(num_nodes,hidden_​​size * num_heads)
	    for i in range(self.num_layers):   # 遍历num_layers层网络
	        ngw = pgl.sample.edge_drop(graph_wrapper, edge_dropout)   # 随机边drop
	        
	        # gat网络layer
	        feature = conv.gat(ngw,                             # 传入图容器--传入模型中的都不是简单的图,而是经过pgl中对应的图容器(不是pgl.Graph哦)
	                            feature,                        # 特征参数--节点特征
	                            self.hidden_size,               # 输出大小
	                            activation="elu",               # 激活函数
	                            name="gat_layer_%s" % i,        # nameed
	                            num_heads=self.num_heads,       # 头数
	                            feat_drop=self.feat_dropout,    # 特征drop率
	                            attn_drop=self.attn_dropout)    # 参数drop率
	
	    # 最后再通过一层实现结果输出
	    ngw = pgl.sample.edge_drop(graph_wrapper, edge_dropout) 
	    feature = conv.gat(ngw,        # 图
	                 feature,          # 特征参数--节点特征
	                 self.num_class,   # 输出大小为类别数--用于预测
	                 num_heads=1,      # 头数变为1
	                 activation=None,  # 不需要激活函数
	                 feat_drop=self.feat_dropout, # 特征drop率
	                 attn_drop=self.attn_dropout, # 参数drop率
	                 name="output")
	            
	    return feature     # 返回预测结果

3. APPNP模型代码讲解

# 新网络学习-APPNP
class APPNP(object):
    """Implement of APPNP"""
    def __init__(self, config, num_class):
        self.num_class = num_class                          # 类别数
        self.num_layers = config.get("num_layers", 1)       # 层数
        self.hidden_size = config.get("hidden_size", 64)    # 中间层输出大小--不一定只有一层中间层哈--跟num_layers有关
        self.dropout = config.get("dropout", 0.5)           # drop率——指的是fc层中用到的dopr率
        self.alpha = config.get("alpha", 0.1)               # alpha值---用于公式计算_论文中的超参数
        self.k_hop = config.get("k_hop", 10)                # k_hop值---网络传播次数
        self.edge_dropout = config.get("edge_dropout", 0.0) # 边drop率

    def forward(self, graph_wrapper, feature, phase):
        '''
            graph_wrapper: 一个图容器,用于存储图信息,并可以迭代训练与预测
            feature:图的节点特征
            phase:指令--train or eval or test等

            功能:
                首先根据运行模式,确定edge_drop率
                然后进入循环遍历叠加网络层,这里不同于之前的网络---这里叠加的是fc层和drop操作--先drop,后fc
                APPNP仅仅一个--并且由于APPNP层无法改变中间层大小,所以在传入前要把对应的feature转换为跟num_class相关的大小

                根据相应的处理得到需要的预测结果。
                如:softmax进行一个类别处理,利用argmax的到分类的类别  【具体过程详见build_model.py】
        '''
         
        if phase == "train":    # 训练模式才会进行边drop
            edge_dropout = self.edge_dropout
        else:
            edge_dropout = 0

        # APPNP比较特殊,这里的num_layers层数是指前层网络fc的深度,而不是直接叠加APPNP层
        for i in range(self.num_layers):
            feature = L.dropout(
                feature,        # 需要drop的特征
                self.dropout,   # drop率
                dropout_implementation='upscale_in_train')   # 训练时drop,非训练不drop
            feature = L.fc(feature, self.hidden_size, act="relu", name="lin%s" % i)

        # 完成上述操作后,再重复一次相同的操作,最后调整输出为num_class
        feature = L.dropout(
            feature,
            self.dropout,
            dropout_implementation='upscale_in_train')

        feature = L.fc(feature, self.num_class, act=None, name="output")  # 为appnp做好准备

        # APPNP这个layer的返回值:张量:shape(num_nodes,hidden_​​size)
        # 不能修改输出的new_hidden_​​size,只能使用传入的feature的数据形状hidden_​​size(num_class)
        feature = conv.appnp(graph_wrapper,   # 传入图容器
            feature=feature,                  # 特征--节点特征
            edge_dropout=edge_dropout,        # 边drop率
            alpha=self.alpha,                 # alpha值_论文中的超参数
            k_hop=self.k_hop)                 # 传播次数————这个部分太大,会显存爆炸哈

        return feature

3.1 SGC模型代码讲解——(APPNP模型补充)

# 单APPNP网络
class SGC(object):
    """Implement of SGC"""
    def __init__(self, config, num_class):
        self.num_class = num_class                      # 类别数
        self.num_layers = config.get("num_layers", 1)   # 层数

    def forward(self, graph_wrapper, feature, phase):
        '''
            graph_wrapper: 一个图容器,用于存储图信息,并可以迭代训练与预测
            feature:图的节点特征
            phase:指令--train or eval or test等

            功能:
                直接将图容器传入appnp层中,不经过任何处理,也不进行任何drop
                然后再经过fc层得到合适形状的输出

                根据相应的处理得到需要的预测结果。
                如:softmax进行一个类别处理,利用argmax的到分类的类别  【具体过程详见build_model.py】
        '''

        # APPNP这个layer的返回值:张量:shape(num_nodes,hidden_​​size)
        # 这里的hidden_​​size是输入feature的最低维度大小
        feature = conv.appnp(graph_wrapper,
            feature=feature,
            edge_dropout=0,   # drop为零
            alpha=0, # 论文中的超参数
            k_hop=self.num_layers)
        feature.stop_gradient=True  # 在这里停止梯度计算--也就是之后的运算计算相应的梯度,用于优化
        feature = L.fc(feature, self.num_class, act=None, bias_attr=False, name="output")  # 转换形状输出即可

        return feature

4. GCNII模型代码讲解

# 新网络模型学习——GCNII
class GCNII(object):
    """Implement of GCNII"""
    def __init__(self, config, num_class):
        self.num_class = num_class                          # 类别数
        self.num_layers = config.get("num_layers", 1)       # 层数
        self.hidden_size = config.get("hidden_size", 64)    # 中间层输出大小--不一定只有一层中间层哈--跟num_layers有关
        self.dropout = config.get("dropout", 0.6)           # drop率——既是fc的,也是GCNII的drop
        self.alpha = config.get("alpha", 0.1)               # alpha值——论文中的超参数
        self.lambda_l = config.get("lambda_l", 0.5)         # labda_l值——论文中的超参数
        self.k_hop = config.get("k_hop", 64)                # 传播次数
        self.edge_dropout = config.get("edge_dropout", 0.0) # 边drop率

    def forward(self, graph_wrapper, feature, phase):
        '''
            graph_wrapper: 一个图容器,用于存储图信息,并可以迭代训练与预测
            feature:图的节点特征
            phase:指令--train or eval or test等

            功能:
                首先根据运行模式,确定edge_drop率
                然后进入循环遍历叠加网络层,这里不同于之前的网络---这里叠加的是fc层和drop操作--先fc,再drop
                GCNII仅仅一个--并且由于GCNII层无法改变中间层大小,所以计算后要把对应的feature转换为跟num_class相关的大小--又要再次利用fc来完成

                根据相应的处理得到需要的预测结果。
                如:softmax进行一个类别处理,利用argmax的到分类的类别  【具体过程详见build_model.py】
        '''

        if phase == "train":  # 训练模式才会进行边drop
            edge_dropout = self.edge_dropout
        else:
            edge_dropout = 0

        # GCNII也比较特殊,这里的num_layers层数指的是前层网络fc的深度,而不是直接叠加GCNII层
        for i in range(self.num_layers):
            feature = L.fc(feature, self.hidden_size, act="relu", name="lin%s" % i)   # 跟APPNP相比--GCNII先经过fc再通过dropout
            feature = L.dropout(
                feature,
                self.dropout,
                dropout_implementation='upscale_in_train')  # 训练时drop,否则不drop

        # GCNII这个layer的返回值:张量shape: (num_nodes, hidden_size)
        # GCNII也不能改变特征输出的大小
        feature = conv.gcnii(graph_wrapper,  # 图容器
            feature=feature,                 # 特征数据
            name="gcnii",                    # named
            activation="relu",               # 激活函数
            lambda_l=self.lambda_l,          # 论文中的超参数--用于内部公式计算
            alpha=self.alpha,                # 论文中的超参数
            dropout=self.dropout,            # drop率
            k_hop=self.k_hop)                # 传播次数

        feature = L.fc(feature, self.num_class, act=None, name="output")  # 再经过fc获得指定大小的输出
        return feature

四、build_model.py的内容解析

import pgl
import model
from pgl import data_loader
import paddle.fluid as fluid
import numpy as np
import time

'''build_model整个流程的说明:
        1. 首先明确传入参数(dataset, config, phase, main_prog)
                1. dataset: 一个简单的图
                2. config: 配置参数
                            1. 包括模型名称,以及相关的初始化参数--根据自己的模型配置就好
                3. phase: 工作指令--train-训练模式,其它为非训练模式
        
        2. 主要工作流程
                1. 首先将传入的图放入一个图容器,此时传入图和节点特征即可
                2. 利用python自带的getattr读取model.py中的类,并返回这个类
                3. 利用返回的类实例一个模型

                这后边就涉及静态的参数创建了:
                4. 将图容器传入以及其它对于模型的forward必须的参数--得到一个返回值--logits,这个输出信息用于预测等  -- logits是经过模型层返回的,也是一个data
                5. 创建一些训练和预测所必须的参数--node_index: 节点索引集【需回到notebook中对照理解】;node_label,用于计算acc,loss等
                【注意,这里涉及到模型返回的参数也好,其它的loss以及node_index、node_label都是一个静态图下的data,要通过build_model返回之后,经过执行器运行时才会有实际的值】
                6. 接着创建loss方法以及返回loss_data----以及添加acc方法,计算acc_data
                7. 添加一个softmax获取实际类别
                8. 接着平均化损失
                9. 如果是训练模式,还会单独添加优化器,返回优化器对象,并优化参数

                **: 切记,这里使用方法创建的变量都是静态图中的data,需要放入执行器中运行才有实际的意义
'''



def build_model(dataset, config, phase, main_prog):
    '''
        dataset: 就是一个图

        config: 来自以下代码
                from easydict import EasyDict as edict

                config = {
                    "model_name": "APPNP",
                    "num_layers": 3,
                    "dropout": 0.5,
                    "learning_rate": 0.0002,
                    "weight_decay": 0.0005,
                    "edge_dropout": 0.00,
                }

                config = edict(config)
        
        main_prog:执行器对象(program)
    '''
    gw = pgl.graph_wrapper.GraphWrapper(
            name="graph",
            node_feat=dataset.graph.node_feat_info())  # 创建图容器

    GraphModel = getattr(model, config.model_name)    # 获取model中关于config.model_name指定的模型配置--即在model.py中,getattr获取的对象属性就是相应的模型类
    m = GraphModel(config=config, num_class=dataset.num_classes)   # 利用返回的模型类,实例一个对象--传入配置信息,以及节点类别数(用于预测分类)
    logits = m.forward(gw, gw.node_feat["feat"], phase)   # 调用模型对象,进行前向计算--传入图,节点特征,执行指令--phase为train或者false

    # 补充说明:m.forward得到的是一个shape为[batch, num_class]的序列--后边用于softmax处理再进行类别获取

    # Take the last
    # 创建节点data
    node_index = fluid.layers.data(
            "node_index",
            shape=[None, 1],
            dtype="int64",
            append_batch_size=False)
    
    # 创建节点标签data
    node_label = fluid.layers.data(
            "node_label",
            shape=[None, 1],  # 【batch,1】
            dtype="int64",
            append_batch_size=False)

    # 根据索引 node_index 获取输入logits的最外层维度的条目,并将它们拼接在一起
    # 即: eg: node_index=[1, 2], logits=[[1, 2], [2, 3], [3, 4]]
    #    那么拼接的对象就是——[1, 2], [2, 3], [3, 4]
    #    然后根据node_index索引拼接,选择[2, 3], [3, 4]进行拼接,然后维数不变的返回[[2, 3], [3, 4]]
    pred = fluid.layers.gather(logits, node_index)        # node_index是一个data,暂时是不作用的,要等exe执行器运行时才会传入信息---这里最后作用的结果就是根据index索引相应的值

    loss, pred = fluid.layers.softmax_with_cross_entropy(
        logits=pred, label=node_label, return_softmax=True)    # 输入pred, 与node_label,进行交叉熵计算--并返回通过sortmax的pred数据和loss

    acc = fluid.layers.accuracy(input=pred, label=node_label, k=1)   # 准确率计算
    pred = fluid.layers.argmax(pred, -1)   # 利用argmax确定具体的类别
    loss = fluid.layers.mean(loss)         # 计算平均损失

    if phase == "train":   # 训练模式才进行优化

        # Adam优化器:利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率
        adam = fluid.optimizer.Adam(
            learning_rate=config.learning_rate,   # 学习率
            regularization=fluid.regularizer.L2DecayRegularizer(    # L2权重衰减正则化
                regularization_coeff=config.weight_decay))          # 正则化系数--就是我们在notbook中config设置的weight_decay

        adam.minimize(loss)   # 通过minimize获取实际损失--即loss_all / batchsize

    return gw, loss, acc, pred  # 返回训练后的图、损失、精度、预测data

五、总结

训练少许经验:

  1. APPNP这个网络,貌似直接使用表现不是很好–我这边的话是这样;当然改一改可能会有奇迹,谁知道呢?对吧!
    【炼丹就要无所畏惧——时间这东西,那是来着不拒(●ˇ∀ˇ●)】
  2. 关于后边两个model中最后两个模型,k_hop这个参数,设计的大的化可能会导致显存溢出哦?虽然对精度有些提升但是也要看自身想要什么结果啦!
    【我这里的话,GCNII——实际两层, “k_hop”, 64, “hidden_size”, 64; 就会爆显存哦–notebook:16G——GPU】

你可能感兴趣的:(图神经网络,神经网络)