图神经网络——GCN,GraphSAGE

1、应用

生物化学:分子指纹识别、药物分子设计、疾病分类
交通领域:对交通需求的预测、对道路速度的预测
计算机图像处理:目标检测、视觉推理等
自然语言处理:实体关系抽取、关系推理等

2、数据集介绍

CORA数据集由2708篇论文,及它们之间的引用关系构成的5429条边组成。这些论文被根据主题划分为7类,分别是神经网络、强化学习、规则学习、概率方法、遗传算法、理论研究、案例相关。每篇论文的特征是通过词袋模型得到的,维度为1433,每一维表示一个词,1表示该词在这篇文章中出现过,0表示未出现。

  • ind.cora.x  -> shape(140,1433)  训练集节点特征向量
  • ind.cora.tx  -> shape(1000,1433)  测试集节点特征向量
  • ind.cora.allx  -> shape(1708,1433)   有标签和无标签的训练节点特征向量
  • ind.cora.y  -> shape(140,7)  训练集节点标签  【one-hot编码】
  • ind.cora.ty  -> shape(1000,7)  测试集节点标签【one-hot编码】
  • ind.cora.ally -> shape(1708,7)   【one-hot编码】
  • ind.cora.graph 节点之间边的信息
  • ind.cora.test.index  测试集索引

3、案例

3.1 GCN

import scipy.sparse as sp
import torch 
import torch.nn as nn
import torch.nn.functional as F
import torch.nn.init as init
import torch.optim as optim
import matplotlib.pyplot as plt
from collections import namedtuple
import os.path as osp
import pickle
import urllib
import os
import itertools
import numpy as np

Data = namedtuple('Data',['x','y','adjacency',
                          'train_mask','val_mask','test_mask'])

#数据集
class CoraData(object):
    """
    通过网址下载cora数据集
    """
    download_url =  "https://github.com/kimiyoung/planetoid/raw/master/data" 
#     download_url =  "https://raw.githubusercontent.com/kimiyoung/planetoid/master/data"
    filenames = ["ind.cora.{}".format(name) for name in
            ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']]
    def __init__(self, data_root="cora", rebuild=False):
        """包括数据下载、处理、加载等功能
        当数据的缓存文件存在时,将使用缓存文件,否则将下载、处理,并缓存到磁盘
        Args:
        -------
        data_root: string, optional
        存放数据的目录,原始数据路径: {data_root}/raw 缓存数据路径: {data_root}/processed_cora.pkl
        rebuild: boolean, optional
        是否需要重新构建数据集,当设为True时,如果缓存数据存在也会重建数据
        """
        self.data_root = data_root
        save_file = osp.join(self.data_root, "processed_cora.pkl")
        if osp.exists(save_file) and not rebuild:
            print("Using Cached file: {}".format(save_file))
            self._data = pickle.load(open(save_file, "rb"))
        else:
#             self.maybe_download()
            self._data = self.process_data()
            with open(save_file, "wb") as f:
                pickle.dump(self.data, f)
            print("Cached file: {}".format(save_file))
    @property
    def data(self):
        """返回Data数据对象,包括x, y, adjacency, train_mask, val_mask, test_mask"""
        return self._data
    def maybe_download(self):
        save_path = os.path.join(self.data_root, "raw")
        for name in self.filenames:
            if not osp.exists(osp.join(save_path, name)):
                self.download_data(
                "{}/{}".format(self.download_url, name), save_path)
    @staticmethod
    def download_data(url, save_path):
        """数据下载工具,当原始数据不存在时将会进行下载"""
        if not os.path.exists(save_path):
            os.makedirs(save_path)
        
        data = urllib.request.urlopen(url)
        filename = os.path.split(url)[-1]        
        print(save_path,filename)
        with open(os.path.join(save_path, filename), 'wb') as f:
            f.write(data.read())
        return True

    def process_data(self):
        """ 处理数据,得到节点特征和标签,邻接矩阵,训练集、验证集以及测试集
        """
        print("Process data ...")
        _, tx, allx, y, ty, ally, graph, test_index = [self.read_data(
        osp.join(self.data_root, "raw", name)) for name in self.filenames]
        train_index = np.arange(y.shape[0])
        val_index = np.arange(y.shape[0], y.shape[0] + 500)
        sorted_test_index = sorted(test_index)
        x = np.concatenate((allx, tx), axis=0)
        y = np.concatenate((ally, ty), axis=0).argmax(axis=1)
        x[test_index] = x[sorted_test_index]
        y[test_index] = y[sorted_test_index]
        num_nodes = x.shape[0]
        train_mask = np.zeros(num_nodes, dtype=np.bool_)
        val_mask = np.zeros(num_nodes, dtype=np.bool_)
        test_mask = np.zeros(num_nodes, dtype=np.bool_)
        train_mask[train_index] = True
        val_mask[val_index] = True
        test_mask[test_index] = True
        adjacency = self.build_adjacency(graph)
        print("Node's feature shape: ", x.shape)
        print("Node's label shape: ", y.shape)
        print("Adjacency's shape: ", adjacency.shape)
        print("Number of training nodes: ", train_mask.sum())
        print("Number of validation nodes: ", val_mask.sum())
        print("Number of test nodes: ", test_mask.sum())
        return Data(x=x, y=y, adjacency=adjacency,
                train_mask=train_mask, val_mask=val_mask, test_mask=test_mask)
    @staticmethod
    def build_adjacency(adj_dict):
        """根据邻接表创建邻接矩阵"""
        edge_index = []
        num_nodes = len(adj_dict)
        for src, dst in adj_dict.items():
            edge_index.extend([src, v] for v in dst)
            edge_index.extend([v, src] for v in dst)
        # 由于上述得到的结果中存在重复的边,删掉这些重复的边
        edge_index = list(k for k, _ in itertools.groupby(sorted(edge_index)))
        edge_index = np.asarray(edge_index)
        adjacency = sp.coo_matrix((np.ones(len(edge_index)),
                                    (edge_index[:, 0], edge_index[:, 1])),
                                    shape=(num_nodes, num_nodes), dtype="float32")
        return adjacency
    @staticmethod
    def read_data(path):
        """使用不同的方式读取原始数据以进一步处理"""
        name = osp.basename(path)
        if name == "ind.cora.test.index":
            out = np.genfromtxt(path, dtype="int64")
            return out
        else:
            out = pickle.load(open(path, "rb"), encoding="latin1")
            out = out.toarray() if hasattr(out, "toarray") else out
            return out
    
def normalization(adjacency):
        """计算 L=D^-0.5 * (A+I) * D^-0.5"""
        adjacency += sp.eye(adjacency.shape[0]) # 增加自连接
        degree = np.array(adjacency.sum(1))
        d_hat = sp.diags(np.power(degree, -0.5).flatten())
        return d_hat.dot(adjacency).dot(d_hat).tocoo()
            
        

class GraphConvolution(nn.Module):    
    def __init__(self, input_dim, output_dim, use_bias=True):
        super(GraphConvolution,self).__init__()
        self.input_dim = input_dim        
        self.output_dim = output_dim
        self.use_bias = use_bias        
        self.weight = nn.Parameter(torch.Tensor(input_dim,output_dim))        
        if self.use_bias:
            self.bias = nn.Parameter(torch.Tensor(output_dim))
        else:
            self.register_parameter('bias',None)
        self.reset_parameters()
    def reset_parameters(self):        
        init.kaiming_uniform_(self.weight)        
        if self.use_bias:            
            init.zeros_(self.bias)

    def __repr__(self):        
        return self.__class__.__name__ + ' (' + str(self.in_features) + ' -> ' + str(self.output_dim) + ')'
        
    def forward(self, adjacency, input_feature):
        """邻接矩阵是稀疏矩阵,因此在计算时使用稀疏矩阵乘法
        Args:
        -------
        adjacency: torch.sparse.FloatTensor 邻接矩阵
        input_feature: torch.Tensor 输入特征
        """
        support = torch.mm(input_feature, self.weight)
        output = torch.sparse.mm(adjacency, support)
        if self.use_bias:
            output += self.bias
        return output
    
class GcnNet(nn.Module):
    """ 定义一个包含两层GraphConvolution的模型
    """
    def __init__(self, input_dim=1433):
        super(GcnNet, self).__init__()
        self.gcn1 = GraphConvolution(input_dim, 16)
        self.gcn2 = GraphConvolution(16, 7)
    def forward(self, adjacency, feature):
        h = F.relu(self.gcn1(adjacency, feature))
        logits = self.gcn2(adjacency, h)
        return logits
    
def train():
    loss_history = []
    val_acc_history = []
    model.train()
    train_y = tensor_y[tensor_train_mask]
    for epoch in range(epochs):
        logits = model(tensor_adjacency, tensor_x) # 前向传播
        train_mask_logits = logits[tensor_train_mask] # 只选择训练节点进行监督
        loss = criterion(train_mask_logits, train_y) # 计算损失值
        optimizer.zero_grad()
        loss.backward() # 反向传播计算参数的梯度
        optimizer.step() # 使用优化方法进行梯度更新
        train_acc,_,_ = test(tensor_train_mask) # 计算当前模型在训练集上的准确率
        val_acc,_,_ = test(tensor_val_mask) # 计算当前模型在验证集上的准确率
        # 记录训练过程中损失值和准确率的变化,用于画图
        loss_history.append(loss.item())
        val_acc_history.append(val_acc.item())
        print("Epoch {:03d}: Loss {:.4f}, TrainAcc {:.4}, ValAcc {:.4f}".format(
        epoch, loss.item(), train_acc.item(), val_acc.item()))
    return loss_history, val_acc_history
def test(mask):
    model.eval()
    with torch.no_grad():
        logits = model(tensor_adjacency, tensor_x)
        test_mask_logits = logits[mask]
        predict_y = test_mask_logits.max(1)[1]
        accuarcy = torch.eq(predict_y, tensor_y[mask]).float().mean()
    return accuarcy,test_mask_logits.cpu().numpy(),tensor_y[mask].cpu().numpy()
    

# 超参数定义
learning_rate = 0.1
weight_decay = 5e-4
epochs = 200
# 模型定义,包括模型实例化、损失函数与优化器定义
device = "cuda" if torch.cuda.is_available() else "cpu"
model = GcnNet().to(device)
print(model.parameters())
# 损失函数使用交叉熵
criterion = nn.CrossEntropyLoss().to(device)
# 优化器使用Adam
optimizer = optim.Adam(model.parameters(), lr=learning_rate, weight_decay=weight_decay)
# 加载数据,并转换为torch.Tensor
dataset = CoraData().data
x = dataset.x / dataset.x.sum(1, keepdims=True) # 归一化数据,使得每一行和为1
tensor_x = torch.from_numpy(x).to(device)
tensor_y = torch.from_numpy(dataset.y).to(device)
tensor_train_mask = torch.from_numpy(dataset.train_mask).to(device)
tensor_val_mask = torch.from_numpy(dataset.val_mask).to(device)
tensor_test_mask = torch.from_numpy(dataset.test_mask).to(device)
normalize_adjacency = normalization(dataset.adjacency) # 规范化邻接矩阵
indices = torch.from_numpy(np.asarray([normalize_adjacency.row,
normalize_adjacency.col])).long()
values = torch.from_numpy(normalize_adjacency.data.astype(np.float32))
tensor_adjacency = torch.sparse.FloatTensor(indices, values,(2708, 2708)).to(device)

loss,val_acc = train()
print(tensor_test_mask)
test_acc,test_logits,test_label = test(tensor_test_mask)
print("Test accuarcy: ", test_acc.item())

评估

from sklearn.manifold import TSNE
tsne = TSNE()
out = tsne.fit_transform(test_logits)
fig = plt.figure()
for i in range(7):
    indices = test_label == i
    x, y = out[indices].T
    plt.scatter(x, y, label=str(i))
plt.legend()

画图

def plot_loss_with_acc(loss_history, val_acc_history):
    fig = plt.figure()
    # 坐标系ax1画曲线1
    ax1 = fig.add_subplot(111)  # 指的是将plot界面分成1行1列,此子图占据从左到右从上到下的1位置
    ax1.plot(range(len(loss_history)), loss_history,
             c=np.array([255, 71, 90]) / 255.)  # c为颜色
    plt.ylabel('Loss')
    
    # 坐标系ax2画曲线2
    ax2 = fig.add_subplot(111, sharex=ax1, frameon=False)  # 其本质就是添加坐标系,设置共享ax1的x轴,ax2背景透明
    ax2.plot(range(len(val_acc_history)), val_acc_history,
             c=np.array([79, 179, 255]) / 255.)
    ax2.yaxis.tick_right()  # 开启右边的y坐标
    
    ax2.yaxis.set_label_position("right")
    plt.ylabel('ValAcc')
    
    plt.xlabel('Epoch')
    plt.title('Training Loss & Validation Accuracy')
    plt.show()
 
plot_loss_with_acc(loss, val_acc)

3.2 GraphSAGE

#GraphSAGE
Data = namedtuple('Data', ['x', 'y', 'adjacency',
                           'train_mask', 'val_mask', 'test_mask'])


class CoraData(object):
    filenames = ["ind.cora.{}".format(name) for name in
                 ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']]

    def __init__(self, data_root="cora/raw", rebuild=False):
        """Cora数据,包括数据下载,处理,加载等功能
        当数据的缓存文件存在时,将使用缓存文件,否则将下载、进行处理,并缓存到磁盘

        处理之后的数据可以通过属性 .data 获得,它将返回一个数据对象,包括如下几部分:
            * x: 节点的特征,维度为 2708 * 1433,类型为 np.ndarray
            * y: 节点的标签,总共包括7个类别,类型为 np.ndarray
            * adjacency_dict: 邻接信息,,类型为 dict
            * train_mask: 训练集掩码向量,维度为 2708,当节点属于训练集时,相应位置为True,否则False
            * val_mask: 验证集掩码向量,维度为 2708,当节点属于验证集时,相应位置为True,否则False
            * test_mask: 测试集掩码向量,维度为 2708,当节点属于测试集时,相应位置为True,否则False

        Args:
        -------
            data_root: string, optional
                存放数据的目录,原始数据路径: ../data/cora
                缓存数据路径: {data_root}/ch7_cached.pkl
            rebuild: boolean, optional
                是否需要重新构建数据集,当设为True时,如果存在缓存数据也会重建数据

        """
        self.data_root = data_root
        save_file = osp.join(self.data_root, "ch7_cached.pkl")
        if osp.exists(save_file) and not rebuild:
            print("Using Cached file: {}".format(save_file))
            self._data = pickle.load(open(save_file, "rb"))
        else:
            self._data = self.process_data()
            with open(save_file, "wb") as f:
                pickle.dump(self.data, f)
            print("Cached file: {}".format(save_file))

    @property
    def data(self):
        """返回Data数据对象,包括x, y, adjacency, train_mask, val_mask, test_mask"""
        return self._data

    def process_data(self):
        """
        处理数据,得到节点特征和标签,邻接矩阵,训练集、验证集以及测试集
        引用自:https://github.com/rusty1s/pytorch_geometric
        """
        print("Process data ...")
        _, tx, allx, y, ty, ally, graph, test_index = [self.read_data(
            osp.join(self.data_root, name)) for name in self.filenames]
        train_index = np.arange(y.shape[0])
        val_index = np.arange(y.shape[0], y.shape[0] + 500)
        sorted_test_index = sorted(test_index)

        x = np.concatenate((allx, tx), axis=0)
        y = np.concatenate((ally, ty), axis=0).argmax(axis=1)

        x[test_index] = x[sorted_test_index]
        y[test_index] = y[sorted_test_index]
        num_nodes = x.shape[0]

        train_mask = np.zeros(num_nodes, dtype=np.bool_)
        val_mask = np.zeros(num_nodes, dtype=np.bool_)
        test_mask = np.zeros(num_nodes, dtype=np.bool_)
        train_mask[train_index] = True
        val_mask[val_index] = True
        test_mask[test_index] = True
        adjacency_dict = graph
        print("Node's feature shape: ", x.shape)
        print("Node's label shape: ", y.shape)
        print("Adjacency's shape: ", len(adjacency_dict))
        print("Number of training nodes: ", train_mask.sum())
        print("Number of validation nodes: ", val_mask.sum())
        print("Number of test nodes: ", test_mask.sum())

        return Data(x=x, y=y, adjacency=adjacency_dict,
                    train_mask=train_mask, val_mask=val_mask, test_mask=test_mask)

    @staticmethod
    def build_adjacency(adj_dict):
        """根据邻接表创建邻接矩阵"""
        edge_index = []
        num_nodes = len(adj_dict)
        for src, dst in adj_dict.items():
            edge_index.extend([src, v] for v in dst)
            edge_index.extend([v, src] for v in dst)
        # 去除重复的边
        edge_index = list(k for k, _ in itertools.groupby(sorted(edge_index)))
        edge_index = np.asarray(edge_index)
        adjacency = sp.coo_matrix((np.ones(len(edge_index)),
                                   (edge_index[:, 0], edge_index[:, 1])),
                                  shape=(num_nodes, num_nodes), dtype="float32")
        return adjacency

    @staticmethod
    def read_data(path):
        """使用不同的方式读取原始数据以进一步处理"""
        name = osp.basename(path)
        if name == "ind.cora.test.index":
            out = np.genfromtxt(path, dtype="int64")
            return out
        else:
            out = pickle.load(open(path, "rb"), encoding="latin1")
            out = out.toarray() if hasattr(out, "toarray") else out
            return out
        
def sampling(src_nodes, sample_num, neighbor_table):
    """根据源节点采样指定数量的邻居节点,注意使用的是有放回的采样;
    某个节点的邻居节点数量少于采样数量时,采样结果出现重复的节点
    
    Arguments:
        src_nodes {list, ndarray} -- 源节点列表
        sample_num {int} -- 需要采样的节点数
        neighbor_table {dict} -- 节点到其邻居节点的映射表
    
    Returns:
        np.ndarray -- 采样结果构成的列表
    """
    results = []
    for sid in src_nodes:
        # 从节点的邻居中进行有放回地进行采样
#         print(neighbor_table,sid,neighbor_table.toarray())
        res = np.random.choice(neighbor_table[sid], size=(sample_num, ))
        results.append(res)
    return np.asarray(results).flatten()


def multihop_sampling(src_nodes, sample_nums, neighbor_table):
    """根据源节点进行多阶采样
    
    Arguments:
        src_nodes {list, np.ndarray} -- 源节点id
        sample_nums {list of int} -- 每一阶需要采样的个数
        neighbor_table {dict} -- 节点到其邻居节点的映射
    
    Returns:
        [list of ndarray] -- 每一阶采样的结果
    """
    sampling_result = [src_nodes]
    for k, hopk_num in enumerate(sample_nums):
        hopk_result = sampling(sampling_result[k], hopk_num, neighbor_table)
        sampling_result.append(hopk_result)
    return sampling_result

class NeighborAggregator(nn.Module):
    def __init__(self, input_dim, output_dim, 
                 use_bias=False, aggr_method="mean"):
        """聚合节点邻居

        Args:
            input_dim: 输入特征的维度
            output_dim: 输出特征的维度
            use_bias: 是否使用偏置 (default: {False})
            aggr_method: 邻居聚合方式 (default: {mean})
        """
        super(NeighborAggregator, self).__init__()
        self.input_dim = input_dim
        self.output_dim = output_dim
        self.use_bias = use_bias
        self.aggr_method = aggr_method
        self.weight = nn.Parameter(torch.Tensor(input_dim, output_dim))
        if self.use_bias:
            self.bias = nn.Parameter(torch.Tensor(self.output_dim))
        self.reset_parameters()
    
    def reset_parameters(self):
        init.kaiming_uniform_(self.weight)
        if self.use_bias:
            init.zeros_(self.bias)

    def forward(self, neighbor_feature):
        if self.aggr_method == "mean":
            aggr_neighbor = neighbor_feature.mean(dim=1)
        elif self.aggr_method == "sum":
            aggr_neighbor = neighbor_feature.sum(dim=1)
        elif self.aggr_method == "max":
            aggr_neighbor = neighbor_feature.max(dim=1)
        else:
            raise ValueError("Unknown aggr type, expected sum, max, or mean, but got {}"
                             .format(self.aggr_method))
        
        neighbor_hidden = torch.matmul(aggr_neighbor, self.weight)
        if self.use_bias:
            neighbor_hidden += self.bias

        return neighbor_hidden

    def extra_repr(self):
        return 'in_features={}, out_features={}, aggr_method={}'.format(
            self.input_dim, self.output_dim, self.aggr_method)
    

class SageGCN(nn.Module):
    def __init__(self, input_dim, hidden_dim,
                 activation=F.relu,
                 aggr_neighbor_method="mean",
                 aggr_hidden_method="sum"):
        """SageGCN层定义

        Args:
            input_dim: 输入特征的维度
            hidden_dim: 隐层特征的维度,
                当aggr_hidden_method=sum, 输出维度为hidden_dim
                当aggr_hidden_method=concat, 输出维度为hidden_dim*2
            activation: 激活函数
            aggr_neighbor_method: 邻居特征聚合方法,["mean", "sum", "max"]
            aggr_hidden_method: 节点特征的更新方法,["sum", "concat"]
        """
        super(SageGCN, self).__init__()
        assert aggr_neighbor_method in ["mean", "sum", "max"]
        assert aggr_hidden_method in ["sum", "concat"]
        self.input_dim = input_dim
        self.hidden_dim = hidden_dim
        self.aggr_neighbor_method = aggr_neighbor_method
        self.aggr_hidden_method = aggr_hidden_method
        self.activation = activation
        self.aggregator = NeighborAggregator(input_dim, hidden_dim,
                                             aggr_method=aggr_neighbor_method)
        self.weight = nn.Parameter(torch.Tensor(input_dim, hidden_dim))
        self.reset_parameters()
    
    def reset_parameters(self):
        init.kaiming_uniform_(self.weight)

    def forward(self, src_node_features, neighbor_node_features):
        neighbor_hidden = self.aggregator(neighbor_node_features)
        self_hidden = torch.matmul(src_node_features, self.weight)
        
        if self.aggr_hidden_method == "sum":
            hidden = self_hidden + neighbor_hidden
        elif self.aggr_hidden_method == "concat":
            hidden = torch.cat([self_hidden, neighbor_hidden], dim=1)
        else:
            raise ValueError("Expected sum or concat, got {}"
                             .format(self.aggr_hidden))
        if self.activation:
            return self.activation(hidden)
        else:
            return hidden

    def extra_repr(self):
        output_dim = self.hidden_dim if self.aggr_hidden_method == "sum" else self.hidden_dim * 2
        return 'in_features={}, out_features={}, aggr_hidden_method={}'.format(
            self.input_dim, output_dim, self.aggr_hidden_method)


class GraphSage(nn.Module):
    def __init__(self, input_dim, hidden_dim,
                 num_neighbors_list):
        super(GraphSage, self).__init__()
        self.input_dim = input_dim
        self.hidden_dim = hidden_dim
        self.num_neighbors_list = num_neighbors_list
        self.num_layers = len(num_neighbors_list)
        self.gcn = nn.ModuleList()
        self.gcn.append(SageGCN(input_dim, hidden_dim[0]))
        for index in range(0, len(hidden_dim) - 2):
            self.gcn.append(SageGCN(hidden_dim[index], hidden_dim[index+1]))
        self.gcn.append(SageGCN(hidden_dim[-2], hidden_dim[-1], activation=None))

    def forward(self, node_features_list):
        hidden = node_features_list
        for l in range(self.num_layers):
            next_hidden = []
            gcn = self.gcn[l]
            for hop in range(self.num_layers - l):
                src_node_features = hidden[hop]
                src_node_num = len(src_node_features)
                neighbor_node_features = hidden[hop + 1] \
                    .view((src_node_num, self.num_neighbors_list[hop], -1))
                h = gcn(src_node_features, neighbor_node_features)
                next_hidden.append(h)
            hidden = next_hidden
        return hidden[0]

    def extra_repr(self):
        return 'in_features={}, num_neighbors_list={}'.format(
            self.input_dim, self.num_neighbors_list
        )
    
def train():
    loss_history = []
    val_history = []
    model.train()
    for e in range(EPOCHS):
        for batch in range(NUM_BATCH_PER_EPOCH):
            batch_src_index = np.random.choice(train_index, size=(BTACH_SIZE,))
            batch_src_label = torch.from_numpy(train_label[batch_src_index]).long().to(device)
#             print(batch_src_index,dataset.adjacency)
            batch_sampling_result = multihop_sampling(batch_src_index, NUM_NEIGHBORS_LIST, dataset.adjacency)
            batch_sampling_x = [torch.from_numpy(x[idx]).float().to(device) for idx in batch_sampling_result]
            batch_train_logits = model(batch_sampling_x)
            loss = criterion(batch_train_logits, batch_src_label)
            optimizer.zero_grad()
            loss.backward()  # 反向传播计算参数的梯度
            
            #验证集
            val_acc,_,_ =  test(val_index)
            loss_history.append(loss.item())
            val_history.append(val_acc.item())
            optimizer.step()  # 使用优化方法进行梯度更新
            print("Epoch {:03d} Batch {:03d} Loss: {:.4f}".format(e, batch, loss.item()))
#         test()
        
    return loss_history,val_history


def test(test_index):
    model.eval()
    with torch.no_grad():
        test_sampling_result = multihop_sampling(test_index, NUM_NEIGHBORS_LIST, dataset.adjacency)
        test_x = [torch.from_numpy(x[idx]).float().to(device) for idx in test_sampling_result]
        test_logits = model(test_x)
        test_label = torch.from_numpy(dataset.y[test_index]).long().to(device)
        predict_y = test_logits.max(1)[1]
        accuarcy = torch.eq(predict_y, test_label).float().mean()
#         print("Test Accuracy: ", accuarcy)
        return accuarcy,predict_y.cpu().numpy(),test_label.cpu().numpy()

# 超参数定义
INPUT_DIM = 1433    # 输入维度
# Note: 采样的邻居阶数需要与GCN的层数保持一致
HIDDEN_DIM = [128, 7]   # 隐藏单元节点数
NUM_NEIGHBORS_LIST = [10, 10]   # 每阶采样邻居的节点数
assert len(HIDDEN_DIM) == len(NUM_NEIGHBORS_LIST)
BTACH_SIZE = 16     # 批处理大小
EPOCHS = 20
NUM_BATCH_PER_EPOCH = 20    # 每个epoch循环的批次数
LEARNING_RATE = 0.01    # 学习率

# 模型定义,包括模型实例化、损失函数与优化器定义
device = "cuda" if torch.cuda.is_available() else "cpu"
# model = GraphSage().to(device)
model = GraphSage(input_dim=INPUT_DIM, hidden_dim=HIDDEN_DIM,
                  num_neighbors_list=NUM_NEIGHBORS_LIST).to(device)
# print(model)
criterion = nn.CrossEntropyLoss().to(device)
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, weight_decay=5e-4)

# 加载数据,并转换为torch.Tensor
dataset = CoraData().data
x = dataset.x / dataset.x.sum(1, keepdims=True) # 归一化数据,使得每一行和为1
tensor_x = torch.from_numpy(x).to(device)
tensor_y = torch.from_numpy(dataset.y).to(device)

train_index = np.where(dataset.train_mask)[0]
train_label = dataset.y
val_index = np.where(dataset.val_mask)[0] 

test_index = np.where(dataset.test_mask)[0]

# tensor_train_mask = torch.from_numpy(dataset.train_mask).to(device)
# tensor_val_mask = torch.from_numpy(dataset.val_mask).to(device)
# tensor_test_mask = torch.from_numpy(dataset.test_mask).to(device)
# normalize_adjacency = normalization(dataset.adjacency) # 规范化邻接矩阵
# indices = torch.from_numpy(np.asarray([normalize_adjacency.row,
# normalize_adjacency.col])).long()
# values = torch.from_numpy(normalize_adjacency.data.astype(np.float32))
# tensor_adjacency = torch.sparse.FloatTensor(indices, values,(2708, 2708)).to(device)

loss_history,val_acc = train()
# loss,val_acc = train()
# print(tensor_test_mask)
test_acc,test_logits,test_label = test(test_index)
print("Test accuarcy: ", test_acc.item())

评估

def plot_loss_with_acc(loss_history,val_acc_history):
    fig = plt.figure()
    # 坐标系ax1画曲线1
    ax1 = fig.add_subplot(111)  # 指的是将plot界面分成1行1列,此子图占据从左到右从上到下的1位置
    ax1.plot(range(len(loss_history)), loss_history,
             c=np.array([255, 71, 90]) / 255.)  # c为颜色
    plt.ylabel('Loss')
    
#     坐标系ax2画曲线2
    ax2 = fig.add_subplot(111, sharex=ax1, frameon=False)  # 其本质就是添加坐标系,设置共享ax1的x轴,ax2背景透明
    ax2.plot(range(len(val_acc_history)), val_acc_history,
             c=np.array([79, 179, 255]) / 255.)
    ax2.yaxis.tick_right()  # 开启右边的y坐标
    
    ax2.yaxis.set_label_position("right")
    plt.ylabel('ValAcc')
    
    plt.xlabel('Epoch')
    plt.title('Training Loss & Validation Accuracy')
    plt.show()
 
plot_loss_with_acc(loss_history,val_acc)

参考:

https://mp.weixin.qq.com/s?src=11&timestamp=1715062231&ver=5245&signature=6tzq89bRWFC6uBU*uVU4ESPFrSPSRQbLZ80JQiyok-tW-R8*hAJj9sDpQGG2Mht6ZhqwMjju-EytjBed5KwwDfHs7TEcr-twqEOAQee*Ny4yozAr20Ik8ahW7*d9JuT*&new=1

深入浅出图神经网络: GNN 原理解析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/608506.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

影视极品转场音效大全,经典获奖通用音效素材

一、素材描述 本套音效素材,大小15.02G,16个压缩文件。 二、素材目录 01-华纳兄弟电影音效库合辑(2个压缩文件) 02-影视极品转场音效(2个压缩文件) 03-好莱坞经典综合音效(4个压缩文件&…

线程安全问题、同步代码块、同步方法

线程安全问题就是 用线程同步来解决线程安全问题 同步:一个线程接着一个线程等待执行 同步代码块: 通过锁来解决卖到重复票的问题:卖票问题和存钱取钱问题(见其他两篇文章) 同步方法:

杨辉三角的打印

题目内容: 在屏幕上打印杨辉三角。 思路: 首先我们通过观察发现,每一步的打印都与行列数有关,中间的数据由这一列和上一行的前一列数据控制。所以我们可以使用二维数组进行操作: (1&#xff…

用Rust解决鸡兔同笼问题

目录 一、什么是鸡兔同笼问题? 二、用Rust解决鸡兔同笼问题 三、鸡兔同笼问题在实际生活中的应用有哪些? 一、什么是鸡兔同笼问题? 鸡兔同笼问题是一种古代著名的数学问题,用于训练逻辑思维和解决方程的能力。 鸡兔同笼问题起…

API低代码平台介绍2-最基本的数据查询功能

最基本的数据查询功能 本篇文章我们将介绍如何使用ADI平台定义一个基本的数据查询接口。由于是介绍平台具体功能的第一篇文章,里面会涉及比较多的概念介绍,了解了这些概念有助于您阅读后续的文章。 ADI平台的首页面如下: 1.菜单介绍 1.1 O…

DNS 解析在网络传输中有什么意义?

首先我们先说说什么是DNS解析? DNS解析是将域名解析为对应的IP地址的过程。DNS它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS解析的过程就是寻找哪个IP地址对应你所输入的网址,然后将网页内容返回给用户…

GRS认证的优点和缺点是什么?

GRS认证(Global Recycled Standard)的优点主要体现在以下几个方面: 环保性:GRS认证鼓励和保证产品中使用更多的回收材料,从而减少对原始资源的需求和开采,有助于降低环境负荷,促进资源的循环利用…

案例研究|硬之城借助DataEase以数据驱动供应链精细化管理

深圳硬之城信息技术有限公司(以下简称为“硬之城”)成立于2015年,专注电子元件供应链领域,定位于电子产业供应链与智造平台。硬之城通过名为“Allchips”的集成式服务平台,为客户提供一站式的电子元件采购和供应链管理…

Redis + OpenResty 多级缓存

多级缓存 初识 OpenResty OpenResty - 开源官方站 基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。 具备Nginx的完整功能基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块允…

基士得耶CP 6303c速印机不制版故障检修

故障:基士得耶CP 6303c经常提示版纸卡纸,重设版纸 版纸还没下滚筒,卡在版纸仓里面,手动滚动裁纸刀可以解决,但第二天又是这个毛病; 版纸定位传感器的灵敏度调节一下,然后给机器全面做个清洁大保养,尤其是传…

python中w、r表示什么意思

文件读写就是一种常见的IO操作。 文件读写操作步骤 不同的编程语言读写文件的操作步骤大体都是一样的,都分为以下几个步骤: 1)打开文件,获取文件描述符; 2)操作文件描述符--读/写; 3&#…

电商核心内容揭秘50:个性化广告与投放策略

相关系列文章 电商技术揭秘相关系列文章合集(1) 电商技术揭秘相关系列文章合集(2) 电商技术揭秘相关系列文章合集(3) 电商技术揭秘四十一:电商平台的营销系统浅析 电商技术揭秘四十二&#…

[MQTT]Mosquitto的內網連接(intranet)和使用者/密碼權限設置

[MQTT | Raspberry Pi]Publish and Subscribe with RSSI Data of Esp32 on Intranet 延續[MQTT]Mosquitto的簡介、安裝與連接測試文章,接著將繼續測試在內網的兩台機器是否也可以完成發佈和訂閱作業。 同一網段的兩台電腦測試: 假設兩台電腦的配置如下: A電腦為發…

沉浸式翻译插件:打破语言障碍的革命性工具

在全球化的今天,语言障碍一直是人们获取信息和沟通的主要难题之一。Immersive Translate(沉浸式翻译)的出现,为这一问题提供了一种创新的解决方案。本文将深入介绍Immersive Translate的功能、使用场景以及它如何帮助用户克服语言…

SpringBoot自动配置源码解析+自定义Spring Boot Starter

SpringBootApplication Spring Boot应用标注 SpringBootApplication 注解的类说明该类是Spring Boot 的主配置类,需要运行该类的main方法进行启动 Spring Boot 应用 SpringBootConfiguration 该注解标注表示标注的类是个配置类 EnableAutoConfiguration 直译&#…

如何控制外部用户访问SAP表的权限

今天搞了一天,我就去找找找啊。我们是IDMC要访问BW的表。 Configure SAP user authorization (informatica.com) 这个informatica上面说要连SAP的数据的话,需要设置这些用户权限。 我也没具体看这两权限对象,这个别人已经设置好了。但是表权…

13 华三三层链路聚和

13 华三三层链路聚和 AI 解析 华三三层静态路由是指在华三交换机上配置的一种路由方式。它通过在交换机上手动配置路由表,将不同网络之间的数据进行转发。 华三三层静态路由的配置步骤如下: 1. 配置交换机接口的IP地址:在交换机上选择要配…

生产者与消费者 PV操作 与 阻塞队列

文章目录 普通方式 wait 与 notifyAll消费者生产者桌子测试类运行结果 阻塞队列Cook生产者Customer消费者测试类 普通方式 wait 与 notifyAll 消费者 package abc;public class Customer extends Thread{Overridepublic void run() {while (true) {synchronized (Desk.lock) {…

如何让加快OpenHarmony编译速度?

OpenHarmony 有两种编译方式,一种是通过 hb 工具编译,一种是通过 build.sh 脚本编译。本文笔者将提升 build.sh 方式编译速度的方法整理如下: 因为笔者只用 build.sh 脚本编译,没用过 hb 工具,好像下面的选项也可以用于…

可编程 IP 新星 Story Protocol 何以引领链上文艺复兴浪潮?

当前,随着 Web3 行业发展进入全新阶段,与生成式人工智能(AIGC)技术融合正在创造潜力新星项目。也是目前的互联网生态下,任何普通民众都有权利创作高质量的音乐、艺术、散文和视频内容,带来了用户生成内容&a…