关于算法:PGL图学习之基于UniMP算法的论文引用网络节点分类任务系列九

0次阅读

共计 31289 个字符,预计需要花费 79 分钟才能阅读完成。

我的项目链接:[PGL 图学习之基于 UniMP 算法的论文援用网络节点分类工作[系列九]](https://aistudio.baidu.com/ai…)

1. 常规赛:图神经网络入门节点分类介绍

(1)赛题介绍

图神经网络(Graph Neural Network)是一种专门解决图构造数据的神经网络,目前被广泛应用于举荐零碎、金融风控、生物计算中。图神经网络的经典问题次要有三种,包含 节点分类、连贯预测和图分类三种,本次较量是次要让同学们相熟如何图神经网络解决节点分类问题。

在过来的一个世纪里,迷信出版物的数量每 12 年减少近一倍,对每一种出版物的主题及畛域进行主动分类已成为当下非常重要的工作。本次工作的指标是预测未知论文的主题类别,如软件工程,人工智能,语言计算和操作系统等。较量所选 35 个畛域标签已失去论文作者和 arXiv 版主确认并标记。

本次较量选用的数据集为 arXiv 论文援用网络——ogbn-arixv 数据集的子集 。ogbn-arixv 数据集由大量的学术论文组成,论文之间的援用关系造成一张微小的有向图,每一条有向边示意一篇论文援用另一篇论文, 每一个节点提供 100 维简略的词向量作为节点特色。在论文援用网络中,咱们已对训练集对应节点做了论文类别标注解决。本次工作心愿参赛者通过已有的节点类别以及论文之间的援用关系,预测未知节点的论文类别。

(2)数据形容

本次赛题数据集由学术网络图形成,该图会给出每个节点的特色,以及节点与节点间关系(训练集节点的标注后果已给出)。

1.1 数据集简介

1. 学术网络图数据:

该图蕴含 164,7958 条有向边,13,0644 个节点,参赛者报名胜利后即可通过较量数据集页面提供 edges.csv 以及 feat.npy 下载并读取数据。图上的每个节点代表一篇论文,论文从 0 开始编号;图上的每一条边蕴含两个编号,例如 3,4 代表第 3 篇论文援用了第 4 篇论文。图结构能够参照 AiStudio 上提供的基线零碎我的项目理解数据读取办法。

2. 训练集与测试集:

训练集的标注数据有 70235 条,测试集的标注数据有 37311 条。训练数据给定了论文编号与类别,如 3,15 代表编号为 3 的论文类别为 15。测试集数据只提供论文编号,不提供论文类别,须要参赛者预测其类别。

具体数据介绍:

| 数据集 | 简介 |
| ——– | ——– |
| edges.csv | 边数据:用于标记论文间援用关系 |
| feat.npy | 节点数据:每个节点含 100 维特色 |
| train.csv | 训练集数据 |
| test.csv | 测试集数据 |

1. 图数据:

feat.npy 为 Numpy 格局存储的节点特色矩阵,Shape 为 (130644, 100),即 130644 个节点,每个节点含 100 维特色。其中 feat.npy 能够用 numpy.load(“feat.npy”) 读取。edges.csv 用于标记论文援用关系,为无向图,且由两列组成,没有表头。

字段 阐明
第一列 边的初始节点
第二列 边的终止节点

2. 训练集:train.csv

字段 阐明
nid 训练节点在图上的 Id
label 训练节点的标签(类别编号从 0 开始,共 35 个类别)

3. 测试集:test.csv

字段 阐明
nid 测试节点在图上的 Id

相干材料

飞桨 PGL 图学习框架飞桨 PGL 图学习框架根本可能笼罩大部分的图网络应用,包含图示意学习以及图神经网络。

节点分类实例分享蕴含多种援用网络模型,如 GCN、GAT、GCNII、APPNP 等。

UniMP 模型对立消息传递模型 UniMP,同时实现了节点的特色与标签传递,显著晋升了模型的泛化成果。

提交内容与格局

最终提交的 submission.csv 格局如下:

字段 阐明
nid 测试集节点在图上的 id
label 测试集的节点类别

特地提醒: 提交的后果必须是类别的 id,不能是概率。

提交样例:

nid label
2 34
3 1
4 15

留神:选手提交的 csv 文件的第一行为”nid,label”,程序调转将视为有效答案。接下来每一行的两个数字顺次示意测试集提供的节点 id 以及对应的预测论文类别,留神给出的预测类别 id 要属于给定范畴值且必须是整数。提交的数据行数要与 test.csv 统一。

2.BaseLine 代码解说

2.1 代码整体逻辑

  1. 读取提供的数据集,蕴含构图以及读取节点特色(用户可本人改变边的结构形式)
  2. 配置化生成模型,用户也能够依据教程进行图神经网络的实现。
  3. 开始训练
  4. 执行预测并产生后果文件

环境配置

该我的项目依赖飞桨 paddlepaddle==1.8.4, 以及 pgl==1.2.0。请依照版本号下载对应版本就可运行。

2.1.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",        #模型名称,能够参考 model.py、modelfied.py 文件外面有 gcn gat appnp unimp 等模型
    "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.1.2 数据读取与解决局部

创立一个命名索引元组

from collections import namedtuple
''' 模块阐明:collections 是一个 python 根底数据结构的裁减库:蕴含以下几种扩大:1. namedtuple(): 生成能够应用名字来拜访元素内容的 tuple 子类
        2. deque: 双端队列,能够疾速的从另外一侧追加和推出对象
        3. Counter: 计数器,次要用来计数
        4. OrderedDict: 有序字典
        5. defaultdict: 带有默认值的字典
       
    而当初用到的 namedtuple(): 则是生成能够应用名字来拜访元素内容的 tuple 子类
    eg:
        websites = [('math', 98, '张三'),
                    ('physics', 74, '李四'),
                    ('english', 85, '王二')
                   ]

        Website = namedtuple('Website', ['class', 'score', 'name'])

        for website in websites:
            website = Website._make(website)
            print website

        show:
            Website(class='math', score=98, name='张三')
            Website(class='physics', score=74, name='李四')
            Website(class='english', score=85, name='王二')
'''

#创立一个能够应用名字来拜访元素内容的 tuple-- 作为 Dataset 数据集
#蕴含图、类别数、训练索引集、训练标签、验证索引集、验证标签、测试索引集
Dataset = namedtuple("Dataset", 
               ["graph", "num_classes", "train_index",
                "train_label", "valid_index", "valid_label", "test_index"])

2.1.3 边数据的加载与解决


#加载边数据
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.1.4 数据的残缺加载与解决

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.1.5 数据读取与宰割

这一部分的宰割和 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 创立一个空白矩阵,并指明数据类型

2.1.6 模型加载局部

为了防止合成太多,这里也蕴含了执行器相干的局部代码正文!

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)   # 创立一个解释器

2.1.7. 模型训练过程

这个局部就绝对比较简单,就是利用执行器喂参数和算子就能够啦!

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])   # 打印训练数据

2.1.8 模型进阶

  1. 本人入手实现图神经网络

这里以想本人实现一个 CustomGCN 为例子,首先,咱们在 model.py 创立一个类 CustomGCN

import paddle.fluid.layers as L

class CustomGCN(object):
    """实现本人的 CustomGCN"""
    def __init__(self, config, num_class):
        # 分类的数目
        self.num_class = num_class
        # 分类的层数,默认为 1
        self.num_layers = config.get("num_layers", 1)
        # 中间层 hidden size 默认 64
        self.hidden_size = config.get("hidden_size", 64)
        # 默认 dropout 率 0.5
        self.dropout = config.get("dropout", 0.5)
        
    def forward(self, graph_wrapper, feature, phase):
        """ 定义前向图神经网络
        
        graph_wrapper: 图的数据的容器
        feature: 节点特色
        phase: 标记训练或者测试阶段
        """
        # 通过 Send Recv 来定义 GCN
        def send_func(src_feat, dst_feat, edge_feat):
            # 发送函数简略将输出 src 发送到输入节点 dst。return {"output": src_feat["h"] }
            
        def recv_func(msg):
            # 对音讯函数进行简略求均值
            return L.sequence_pool(msg["output"], "average")
        
        # 调用发送
        message = graph_wrapper.send(send_func,
                               nfeat_list=[("h", feature)])
                          
        # 调用接管
        output = graph_wrapper.recv(message, recv_func)
        
        # 对输入过一层 MLP
        output = L.fc(output, size=self.num_class, name="final_output")

而后,咱们只有改一下本 notebook 脚本的 config,把 model_type 改成 CustomGCN 就能跑起来了。

config = {"model_name": "CustomGCN",}
  1. 残差 -GAT 改写
class ResGAT(object):
    """Implement of ResGAT"""
    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)
        self.hidden_size = config.get("hidden_size", 8)
        self.feat_dropout = config.get("feat_drop", 0.6)
        self.attn_dropout = config.get("attn_drop", 0.6)
        self.edge_dropout = config.get("edge_dropout", 0.0)

    def forward(self, graph_wrapper, feature, phase):
        # feature [num_nodes, 100]
        if phase == "train": 
            edge_dropout = self.edge_dropout
        else:
            edge_dropout = 0
        feature = L.fc(feature, size=self.hidden_size * self.num_heads, name="init_feature")
        for i in range(self.num_layers):
            ngw = pgl.sample.edge_drop(graph_wrapper, edge_dropout) 
            
            res_feature = feature
            # res_feature [num_nodes, hidden_size * n_heads]
            feature = conv.gat(ngw,
                                feature,
                                self.hidden_size,
                                activation=None,
                                name="gat_layer_%s" % i,
                                num_heads=self.num_heads,
                                feat_drop=self.feat_dropout,
                                attn_drop=self.attn_dropout)
            # feature [num_nodes, num_heads * hidden_size]
            feature = res_feature + feature 
            # [num_nodes, num_heads * hidden_size] + [num_nodes, hidden_size * n_heads]
            feature = L.relu(feature)
            feature = L.layer_norm(feature, name="ln_%s" % i)

        ngw = pgl.sample.edge_drop(graph_wrapper, edge_dropout) 
        feature = conv.gat(ngw,
                     feature,
                     self.num_class,
                     num_heads=1,
                     activation=None,
                     feat_drop=self.feat_dropout,
                     attn_drop=self.attn_dropout,
                     name="output")
        return feature

更多进阶模型就在 2.2 节

2.2 模型代码解说:model.py 的内容解析

2.2.1 GCN 模型代码解说

  1. 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  # 返回一个归一化的度,用于公式计算
      
  2. 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.2.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     # 返回预测后果

2.2.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

2.2.4 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

2.2.5 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

2.3 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

3.Res_Unimp_Large 算法实现

# 导入相干包
!pip install --upgrade python-dateutil
!pip install easydict
!pip install pgl==1.2.1 
!pip install pandas>=0.25
# !pip install pyarrow==0.13.0
!pip install chardet==3.0.4

import sys 
import pgl
import paddle.fluid as fluid
import numpy as np
import time
import pandas as pd

3.1 图网络配置

这里曾经有很多弱小的模型配置了,你能够尝试简略的改一下 config 的字段。
例如,换成 GAT 的配置

config = {
    "model_name": "GAT",
    "num_layers":  1,
    "dropout": 0.5,
    "learning_rate": 0.01,
    "weight_decay": 0.0005,
    "edge_dropout": 0.00,
}
from easydict import EasyDict as edict

config = {
    "model_name": "res_unimp_large",
    "num_layers": 3,
    "hidden_size": 64,
    "heads": 2,
    "learning_rate": 0.001,
    "dropout": 0.3,
    "weight_decay": 0.0005,
    "edge_dropout": 0.3,
    "use_label_e": True
}

config = edict(config)

3.2 数据加载模块

这里次要是用于读取数据集,包含读取图数据构图,以及训练集的划分。

from collections import namedtuple

Dataset = namedtuple("Dataset", 
               ["graph", "num_classes", "train_index",
                "train_label", "valid_index", "valid_label", "test_index"])

def load_edges(num_nodes, self_loop=True, add_inverse_edge=True):
    # 从数据中读取边
    edges = pd.read_csv("work/edges.csv", header=None, names=["src", "dst"]).values

    if add_inverse_edge:
        edges = np.vstack([edges, edges[:, ::-1]])

    if self_loop:
        src = np.arange(0, num_nodes)
        dst = np.arange(0, num_nodes)
        self_loop = np.vstack([src, dst]).T
        edges = np.vstack([edges, self_loop])
    
    return edges

def load():
    # 从数据中读取点特色和边,以及数据划分
    node_feat = np.load("work/feat.npy")
    num_nodes = node_feat.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()
    norm = np.maximum(indegree.astype("float32"), 1)
    norm = np.power(norm, -0.5)
    graph.node_feat["norm"] = np.expand_dims(norm, -1)
    
    df = pd.read_csv("work/train.csv")
    # 打乱程序
    df.sample(frac=1.0) 
    node_index = df["nid"].values
    node_label = df["label"].values
    train_part = int(len(node_index) * 0.8)
    train_index = node_index[:train_part]
    train_label = node_label[:train_part]
    valid_index = node_index[train_part:]
    valid_label = node_label[train_part:]
    test_index = pd.read_csv("work/test.csv")["nid"].values
    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 = load()

train_index = dataset.train_index
train_label = np.reshape(dataset.train_label, [-1 , 1])
train_index = np.expand_dims(train_index, -1)

val_index = dataset.valid_index
val_label = np.reshape(dataset.valid_label, [-1, 1])
val_index = np.expand_dims(val_index, -1)

test_index = dataset.test_index
test_index = np.expand_dims(test_index, -1)
test_label = np.zeros((len(test_index), 1), dtype="int64")

3.3 组网模块

这里是组网模块,目前曾经提供了一些预约义的模型,包含 GCN, GAT, APPNP 等。能够通过简略的配置,设定模型的层数,hidden_size 等。你也能够深刻到 model.py 外面,去奇思妙想,写本人的图神经网络。

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

# # 应用 CPU
# place = fluid.CPUPlace()

# 应用 GPU
place = fluid.CUDAPlace(0)

train_program = fluid.default_main_program()
startup_program = fluid.default_startup_program()
with fluid.program_guard(train_program, startup_program):
    with fluid.unique_name.guard():
        gw, loss, acc, pred = build_model(dataset,
                            config=config,
                            phase="train",
                            main_prog=train_program)

test_program = fluid.Program()
with fluid.program_guard(test_program, startup_program):
    with fluid.unique_name.guard():
        _gw, v_loss, v_acc, v_pred = build_model(dataset,
            config=config,
            phase="test",
            main_prog=test_program)


test_program = test_program.clone(for_test=True)

exe = fluid.Executor(place)

3.4 开始训练过程

图神经网络采纳 FullBatch 的训练形式,每一步训练就会把所有整张图训练样本全副训练一遍。

import os
use_label_e = True
label_rate = 0.625
epoch = 1000
exe.run(startup_program)
max_val_acc = 0

# 这里能够复原训练
pretrained = False
if pretrained:
    def name_filter(var):
        res = var.name in os.listdir('./output')
        return res
    fluid.io.load_vars(exe, './output',predicate=name_filter)
    max_val_acc = 0.756

earlystop = 0
# 将图数据变成 feed_dict 用于传入 Paddle Excecutor
feed_dict = gw.to_feed(dataset.graph)
for epoch in range(epoch):
    # Full Batch 训练
    # 设定图下面那些节点要获取
    # node_index: 未知 label 节点的 nid    
    # node_label: 未知 label
    # label_idx: 已知 label 节点的 nid    
    # label: 已知 label
    
    if use_label_e:
        # 在训练集中抽取局部数据,其 Label 已知,并能够输出网络训练
        train_idx_temp = np.array(train_index, dtype="int64")
        train_lab_temp = np.array(train_label, dtype="int64")
        state = np.random.get_state()
        np.random.shuffle(train_idx_temp)
        np.random.set_state(state)
        np.random.shuffle(train_lab_temp)

        label_idx=train_idx_temp[:int(label_rate*len(train_idx_temp))]
        unlabel_idx=train_idx_temp[int(label_rate*len(train_idx_temp)):]
        label=train_lab_temp[:int(label_rate*len(train_idx_temp))]
        unlabel=train_lab_temp[int(label_rate*len(train_idx_temp)):]

        feed_dict["node_index"] = unlabel_idx
        feed_dict["node_label"] = unlabel
        feed_dict['label_idx']= label_idx
        feed_dict['label']= label
    else:
        feed_dict["node_label"] = np.array(train_label, dtype="int64")
        feed_dict["node_index"] = np.array(train_index, dtype="int64")
        

    train_loss, train_acc = exe.run(train_program,
                                feed=feed_dict,
                                fetch_list=[loss, acc],
                                return_numpy=True)

    # Full Batch 验证
    # 设定图下面那些节点要获取
    # node_index: 未知 label 节点的 nid    
    # node_label: 未知 label
    # label_idx: 已知 label 节点的 nid    
    # label: 已知 label
    
    feed_dict["node_index"] = np.array(val_index, dtype="int64")
    feed_dict["node_label"] = np.array(val_label, dtype="int64")
    if use_label_e:
        feed_dict['label_idx'] = np.array(train_index, dtype="int64")
        feed_dict['label'] = np.array(train_label, dtype="int64")
    val_loss, val_acc = exe.run(test_program,
                            feed=feed_dict,
                            fetch_list=[v_loss, v_acc],
                            return_numpy=True)
    print("Epoch", epoch, "Train Acc", train_acc[0], "Valid Acc", val_acc[0])
    
    # 保留历史最优验证精度对应的模型
    if val_acc[0] > max_val_acc:
        max_val_acc = val_acc[0]
        print(val_acc[0])
        fluid.io.save_persistables(exe, './output', train_program)
    
    # 训练精度继续大于验证精度,完结训练
    if train_acc[0] > val_acc[0]:
        earlystop += 1
        if earlystop == 40:
            break
    else:
        earlystop = 0

局部后果展现:

Epoch 987 Train Acc 0.7554459 Valid Acc 0.7546095
Epoch 988 Train Acc 0.7537374 Valid Acc 0.75717235
Epoch 989 Train Acc 0.75497127 Valid Acc 0.7573859
Epoch 990 Train Acc 0.7611409 Valid Acc 0.75653166
Epoch 991 Train Acc 0.75316787 Valid Acc 0.75489426
Epoch 992 Train Acc 0.749561 Valid Acc 0.7547519
Epoch 993 Train Acc 0.7571544 Valid Acc 0.7551079
Epoch 994 Train Acc 0.7516492 Valid Acc 0.75581974
Epoch 995 Train Acc 0.7563476 Valid Acc 0.7563181
Epoch 996 Train Acc 0.7504627 Valid Acc 0.7538976
Epoch 997 Train Acc 0.7476152 Valid Acc 0.75439596
Epoch 998 Train Acc 0.7539272 Valid Acc 0.7528298
Epoch 999 Train Acc 0.7532153 Valid Acc 0.75396883

3.5 对测试集进行预测 - 生成提交文件

训练实现后,咱们对测试集进行预测。预测的时候,因为不晓得测试汇合的标签,咱们随便给一些测试 label。最终咱们取得测试数据的预测后果。

feed_dict["node_index"] = np.array(test_index, dtype="int64")
feed_dict["node_label"] = np.array(test_label, dtype="int64") #假标签
test_prediction = exe.run(test_program,
                            feed=feed_dict,
                            fetch_list=[v_pred],
                            return_numpy=True)[0]
submission = pd.DataFrame(data={"nid": test_index.reshape(-1),
                            "label": test_prediction.reshape(-1)
                        })
submission.to_csv("submission.csv", index=False)

4. 计划优化

次要改良思路嘛是在对预测后果进行简略投票和绝对多数投票下了点功夫,我只用了 10 个预测样本进行交融,预测的样本更多的话,提交后果的分数可能也越高,具体流程如下图所示。

留神图中简略投票和绝对多数投票的样本是任意数目

优良计划参考:

翻新点:
1. 对 node_feat 进行循环解决;
2. 减少标签节点

https://aistudio.baidu.com/ai…

https://aistudio.baidu.com/ai…

https://aistudio.baidu.com/ai…

4.2 简略投票(本地)

这里将训练进去的文件进行简略投票
import csv
from collections import Counter

def vote_merge(filelst):
    result = {}
    fw = open('D:/subexl/76/merge.csv', encoding='utf-8', mode='w', newline='')
    csv_writer = csv.writer(fw)
    csv_writer.writerow(['nid', 'label'])
    for filepath in filelst:
        cr = open(filepath, encoding='utf-8', mode='r')
        csv_reader = csv.reader(cr)
        for i, row in enumerate(csv_reader):
            if i == 0:
                continue
            idx, cls = row
            if idx not in result:
                result[idx] = []
            result[idx].append(cls)

    for nid, clss in result.items():
        counter = Counter(clss)
        true_cls = counter.most_common(1)
        csv_writer.writerow([nid, true_cls[0][0]])

if __name__ == '__main__':
    vote_merge([
 #       "D:/subexl/75/0.76436.csv",
 #       "D:/subexl/75/0.7635.csv",
  #      "D:/subexl/75/0.75666.csv",
   #     "D:/subexl/75/0.75736.csv",
 #       "D:/subexl/75/0.75755.csv",
 #       "D:/subexl/75/0.75801.csv",
 #       "D:/subexl/75/0.75868.csv",
 #       "D:/subexl/75/0.75978.csv",
 #       "D:/subexl/75/0.76171.csv",
 #       "D:/subexl/75/0.76288.csv",
 #       "D:/subexl/75/0.76412.csv",
 #       "D:/subexl/75/0.759664.csv",
 #       "D:/subexl/75/0.75973517.csv",
 #       "D:/subexl/75/0.75980633.csv",
 #       "D:/subexl/75/0.76322347.csv",
 #       "D:/subexl/75/0.763223471.csv",
        "D:/subexl/76/0.75736.csv",
        "D:/subexl/76/0.75755.csv",
        "D:/subexl/76/0.75801.csv",
        "D:/subexl/76/0.75868.csv",
        "D:/subexl/76/0.75978.csv",
        "D:/subexl/76/0.76436.csv",
        "D:/subexl/76/0.759664.csv",
        "D:/subexl/76/0.75973517.csv",
        "D:/subexl/76/0.75980633.csv",
        "D:/subexl/76/0.76322347.csv",
        "D:/subexl/76/0.763223471.csv",
        "D:/subexl/76/submission.csv",
                ])

4.3 绝对多数投票(本地)

绝对多数投票法很简略,分类器的投票数超过半数便认可预测后果,否则回绝。将简略投票后的后果再与新训练进去的文件进行绝对多数投票。
在这里,我将所有提交文件的名称改为“测试精度.csv”,例如 0.76087.csv;而后依照精度大小排序,首先应用绝对多数投票法进行投票,若某一投票不过半数,间接取精度最高 csv 的预测后果。

import os
import numpy as np
from scipy import stats
import pandas as pd
#path 放的是你所有的提交文件
path = 'D:/subexl/75'
filelist = os.listdir(path)

#上面这行代码依照测试精度进行排序
filelist = sorted(filelist, key= lambda x:float(x[:-4]), reverse=True)
print(filelist)

#n 为测试集条目数
n = 37311
num_files = len(filelist)
a = np.zeros([num_files, n], dtype='i8')
for i, filename in enumerate(filelist):
    filepath = os.path.join(path, filename)
    a[i, :] = np.array(np.loadtxt(filepath, dtype=int, delimiter=',', skiprows=1, usecols=1, encoding='utf-8'))

res = np.zeros([n, 1])
for j in range(n):
    counts = np.bincount(a[:,j])
    maxnum = np.argmax(counts)
    
    # 判读最大投票数是否过半数
    if counts[maxnum] > num_files//2:
        res[j] = maxnum
    else:
        res[j] = a[0,j]

#写入文件
data=pd.read_csv('D:/subexl/75/0.75897.csv')
data['label'] = res
data.to_csv('E:/submission.csv',index=False,header=True)

5. 总结

  1. APPNP 这个网络,貌似间接应用体现不是很好 – 我这边的话是这样;当然改一改可能会有奇观

  2. 对于后边两个 model 中最初两个模型,k_hop 这个参数,设计的大的化可能会导致显存溢出哦?尽管对精度有些晋升然而也要看本身想要什么后果啦!

    【GCNII——理论两层, "k_hop", 64, "hidden_size", 64; 就会爆显存哦 --notebook:16G——GPU】
  3. 不如先跑最简略的那个模型,把学习率调小一点,层数 2~3,epoch 的话 4000 左右吧,最初还是要看学习率理论大小。

    【学习率太小可能在某些地位迭代不不上来,就是 loss 可能在一个陷阱里彷徨,下不去!】

  1. 学习了 easydict 库和 collections 库!
  2. 从官网数据处理局部,学习到利用 np 的 vstack 实现自环边以及晓得有向边如何增加反向边的数据——这样的一种代码实现边数据转换的形式!
  3. 从模型加载局部,学习了多 program 执行的操作,理清了 program 与命名空间之间的分割!
  4. 从模型训练局部,强化了执行器执行时,须要传入正确的 program 以及 feed_dict,在 pgl 中能够应用图 Graph 自带的 to_feed 办法返回一个 feed_dict 数据字典作为初始数据,后边再按需增加新数据!
  5. 从 model.py 学习了模型的组网,以及 pgl 中 conv 类下的网络模型办法的调用,不便组网!
  6. 重点来了:从 build_model.py 学习了模型的参数的加载组合,实现对立的解决和返回对立的算子以及参数!
正文完
 0