通过GCN来实现对Cora数据集节点的分类

本文对用 GCN 可视化 cora 网络中节点的一个案例的代码,每一段或者每一行的作用进行详细的解释,解释文字为#后内容

本文代码、数据集来自https://blog.youkuaiyun.com/qq_37252519/article/details/121594170

需要的数据从https://github.com/kimiyoung/planetoid/tree/master/data下载、解压,放在哪都可以,但是在运行前,要改为自己电脑上的绝对路径

#!/usr/bin/env python
# coding: utf-8

# <h1>Table of Contents<span class="tocSkip"></span></h1>
# <div class="toc"><ul class="toc-item"><li><span><a href="#基于Cora数据集的GCN节点分类" data-toc-modified-id="基于Cora数据集的GCN节点分类-1"><span class="toc-item-num">1&nbsp;&nbsp;</span>基于Cora数据集的GCN节点分类</a></span><ul class="toc-item"><li><span><a href="#SetUp" data-toc-modified-id="SetUp-1.1"><span class="toc-item-num">1.1&nbsp;&nbsp;</span>SetUp</a></span></li><li><span><a href="#数据准备" data-toc-modified-id="数据准备-1.2"><span class="toc-item-num">1.2&nbsp;&nbsp;</span>数据准备</a></span></li><li><span><a href="#图卷积层定义" data-toc-modified-id="图卷积层定义-1.3"><span class="toc-item-num">1.3&nbsp;&nbsp;</span>图卷积层定义</a></span></li><li><span><a href="#模型定义" data-toc-modified-id="模型定义-1.4"><span class="toc-item-num">1.4&nbsp;&nbsp;</span>模型定义</a></span></li><li><span><a href="#模型训练" data-toc-modified-id="模型训练-1.5"><span class="toc-item-num">1.5&nbsp;&nbsp;</span>模型训练</a></span></li></ul></li></ul></div>

# # 基于Cora数据集的GCN节点分类

# <table align="left">
#   <td>
#     <a target="_blank" href="https://colab.research.google.com/github/FighterLYL/GraphNeuralNetwork/blob/master/chapter5/GCN_Cora.ipynb"><img src="https://www.tensorflow.org/images/colab_logo_32px.png" />Run in Google Colab</a>
#   </td>
# </table>

# 在Colab中运行时可以通过`代码执行程序->更改运行时类型`选择使用`GPU`

# ## SetUp

# In[1]:
#导入相关库

import itertools
import os
import os.path as osp
import pickle
import urllib
from collections import namedtuple

import numpy as np
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

# ## 数据准备

# In[2]:


#定义了一个名为 Data 的命名元组,其中包含字段 x、y、adjacency、train_mask、val_mask 和test_mask。命名元组类似于普通元组,但允许通过名称访问字段。

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



#这是一个函数,将 NumPy 数组 x 转换为 PyTorch 张量,并将其移动到指定的 device 上。
def tensor_from_numpy(x, device):
    return torch.from_numpy(x).to(device)



#定义了一个名为 CoraData 的类,创建了一个包含与 Cora 数据集相关的文件名的列表。这些文件名对应于Cora数据集中使用的各种数据文件。
class CoraData(object):
    filenames = ["ind.cora.{}".format(name) for name in
                 ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index']]


#初始化 CoraData 类,具有可选参数 data_root,(记得改为自己电脑上的绝对位置,'D:/download/……'这种默认为 "./planetoid-master/data")和 rebuild(默认为 False)。
    def __init__(self, data_root="./planetoid-master/data", rebuild=False):
        """Cora数据,包括数据下载,处理,加载等功能
        当数据的缓存文件存在时,将使用缓存文件,否则将下载、进行处理,并缓存到磁盘

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

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

        """


#设置Cora数据集的数据根目录
        self.data_root = data_root


#构建用于缓存已处理数据的文件路径
        save_file = osp.join(self.data_root, "ch5_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))
#检查缓存文件是否已经存在且 rebuild 标志为 False。如果两个条件都为真,则从缓存文件加载数据。打印一条消息,指示正在使用缓存文件。从缓存文件加载数据到 _data 属性。如果缓存文件不存在或 rebuild 为 True,则调用 process_data() 方法(此处未显示)来处理数据。以二进制写入模式打开文件,用于缓存已处理的数据。将已处理的数据序列化并保存到缓存文件中。打印一条消息,指示已创建缓存文件


    @property
    def data(self):
        """返回Data数据对象,包括x, y, adjacency, train_mask, val_mask, test_mask"""
        return self._data
#将 data 方法转换为只读属性。它允许通过 CoraData 实例的 .data 属性直接访问 _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]
#使用列表推导式,读取 Cora 数据集的各个文件,并将它们分配给相应的变量

        train_index = np.arange(y.shape[0])
#创建一个包含训练集索引的 NumPy 数组

        val_index = np.arange(y.shape[0], y.shape[0] + 500)
#创建一个包含验证集索引的 NumPy 数组,从 y.shape[0] 开始,包括500个节点

        sorted_test_index = sorted(test_index)
#对测试集索引进行排序

        x = np.concatenate((allx, tx), axis=0)
#将 allx 和 tx 沿着行的方向拼接,形成节点特征矩阵 x

        y = np.concatenate((ally, ty), axis=0).argmax(axis=1)
#将 ally 和 ty 沿着行的方向拼接,然后对每行取最大值的索引,得到节点标签数组 y

        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、val_mask 和 test_mask,用于表示训练集、验证集和测试集的节点

        train_mask[train_index] = True
        val_mask[val_index] = True
        test_mask[test_index] = True
#将对应索引位置的布尔值设置为 True,表示这些节点属于相应的集合

        adjacency = self.build_adjacency(graph)
#调用 build_adjacency 方法构建邻接矩阵

        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)
#返回一个 Data 命名元组,包含处理后的节点特征、标签、邻接矩阵,以及训练集、验证集和测试集的掩码


    @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 中

    
        edge_index = list(k for k, _ in itertools.groupby(sorted(edge_index)))
#去除重复的边,通过排序和 groupby 操作实现

        edge_index = np.asarray(edge_index)
#将边索引列表转换为 NumPy 数组

        adjacency = sp.coo_matrix((np.ones(len(edge_index)),
                                   (edge_index[:, 0], edge_index[:, 1])),
                                  shape=(num_nodes, num_nodes), dtype="float32")
#使用 SciPy 中的 coo_matrix 创建邻接矩阵,其中 np.ones(len(edge_index)) 表示每条边的权重都是1

        return adjacency
#返回创建的邻接矩阵


    @staticmethod
    def read_data(path):
        """使用不同的方式读取原始数据以进一步处理"""
        name = osp.basename(path)
#获取文件名

        if name == "ind.cora.test.index":
#如果文件名是 "ind.cora.test.index",则使用 np.genfromtxt 读取数据

            out = np.genfromtxt(path, dtype="int64")
            return out
        else:
#否则,使用 pickle.load 读取数据。同时,如果数据具有 toarray 方法,将其转换为 NumPy 数组

            out = pickle.load(open(path, "rb"), encoding="latin1")
            out = out.toarray() if hasattr(out, "toarray") else out
            return out
#返回读取的数据

    @staticmethod
    def normalization(adjacency):
#计算规范化的邻接矩阵

        """计算 L=D^-0.5 * (A+I) * D^-0.5"""
        adjacency += sp.eye(adjacency.shape[0])  
# 增加自连接,增加自连接,即对角线上的元素加1

        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()
#返回规范化后的邻接矩阵


# ## 图卷积层定义

# In[3]:

#定义了一个图卷积层
class GraphConvolution(nn.Module):
    def __init__(self, input_dim, output_dim, use_bias=True):
        """图卷积:L*X*\theta
#初始化方法,接收输入特征的维度 input_dim,输出特征的维度 output_dim,以及是否使用偏置 use_bias。

        Args:
        ----------
            input_dim: int
                节点输入特征的维度
            output_dim: int
                输出特征维度
            use_bias : bool, optional
                是否使用偏置
        """
        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()
#调用 reset_parameters 方法初始化权重和偏置


#初始化参数的方法,采用 Kaiming 初始化权重,偏置初始化为零
    def reset_parameters(self):
        init.kaiming_uniform_(self.weight)
        if self.use_bias:
            init.zeros_(self.bias)

    def forward(self, adjacency, input_feature):
        """邻接矩阵是稀疏矩阵,因此在计算时使用稀疏矩阵乘法
#前向传播方法,接收邻接矩阵 adjacency 和输入特征 input_feature

        Args:
        -------
            adjacency: torch.sparse.FloatTensor
                邻接矩阵
            input_feature: torch.Tensor
                输入特征
        """
        support = torch.mm(input_feature, self.weight)
#计算支持(support)

        output = torch.sparse.mm(adjacency, support)
#使用稀疏矩阵乘法计算输出

        if self.use_bias:
            output += self.bias
#将偏置加到输出上

        return output
#返回计算得到的输出

    def __repr__(self):
        return self.__class__.__name__ + ' (' + str(self.input_dim) + ' -> ' + str(self.output_dim) + ')'
#返回图卷积层的字符串表示,包括输入和输出维度。


# ## 模型定义
#
# 读者可以自己对GCN模型结构进行修改和实验

# In[4]:


#定义了一个包含两层图卷积层的模型
class GcnNet(nn.Module):
    """
    定义一个包含两层GraphConvolution的模型
    """

    def __init__(self, input_dim=1433):
#初始化方法,接收输入特征的维度,默认为 1433

        super(GcnNet, self).__init__()
#调用父类的初始化方法

        self.gcn1 = GraphConvolution(input_dim, 16)
        self.gcn2 = GraphConvolution(16, 7)
#定义两个图卷积层

    def forward(self, adjacency, feature):
#前向传播方法,接收邻接矩阵 adjacency 和输入特征 feature

        h = F.relu(self.gcn1(adjacency, feature))
#使用 ReLU 激活函数对第一层图卷积的输出进行非线性变换

        logits = self.gcn2(adjacency, h)
#计算第二层图卷积的输出

        return logits
#返回模型的输出



# ## 模型训练

# In[5]:


# 超参数定义
LEARNING_RATE = 0.1
WEIGHT_DACAY = 5e-4
EPOCHS = 200
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
#定义了学习率 LEARNING_RATE,权重衰减 WEIGHT_DACAY,训练轮数 EPOCHS 和设备类型 DEVICE。如果 CUDA 可用,设备类型为 "cuda",否则为 "cpu"


# In[7]:


# 加载数据,并转换为torch.Tensor
dataset = CoraData().data
node_feature = dataset.x / dataset.x.sum(1, keepdims=True)  # 归一化数据,使得每一行和为1
tensor_x = tensor_from_numpy(node_feature, DEVICE)
tensor_y = tensor_from_numpy(dataset.y, DEVICE)
tensor_train_mask = tensor_from_numpy(dataset.train_mask, DEVICE)
tensor_val_mask = tensor_from_numpy(dataset.val_mask, DEVICE)
tensor_test_mask = tensor_from_numpy(dataset.test_mask, DEVICE)
normalize_adjacency = CoraData.normalization(dataset.adjacency)  # 规范化邻接矩阵

num_nodes, input_dim = node_feature.shape
indices = torch.from_numpy(np.asarray([normalize_adjacency.row,
                                       normalize_adjacency.col]).astype('int64')).long()
values = torch.from_numpy(normalize_adjacency.data.astype(np.float32))
tensor_adjacency = torch.sparse.FloatTensor(indices, values,
                                            (num_nodes, num_nodes)).to(DEVICE)
#加载数据并转换为 torch.Tensor:首先创建了一个 CoraData 的实例,并获取其中的数据。然后对节点特征进行归一化,确保每一行的和为1。接着将数据转换为 torch.Tensor 类型,并将邻接矩阵规范化为稀疏张量

# In[ ]:


# 模型定义:Model, Loss, Optimizer
#定义了一个 GcnNet 模型实例,损失函数为交叉熵损失,优化器选择 Adam
model = GcnNet(input_dim).to(DEVICE)
criterion = nn.CrossEntropyLoss().to(DEVICE)
optimizer = optim.Adam(model.parameters(),
                       lr=LEARNING_RATE,
                       weight_decay=WEIGHT_DACAY)


# In[8]:


# 训练主体函数
#进行模型训练的函数。在每个 epoch 中,进行前向传播、计算损失、反向传播和优化。同时计算训练集和验证集上的准确率,并记录损失和准确率的变化
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


# In[9]:


# 测试函数
#进行模型测试的函数。在测试阶段,使用测试集的数据进行前向传播,并计算准确率、预测标签和真实标签
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()


# In[13]:

#绘制训练过程中损失和验证准确率的变化图
def plot_loss_with_acc(loss_history, val_acc_history):
    fig = plt.figure()
    ax1 = fig.add_subplot(111)
    ax1.plot(range(len(loss_history)), loss_history,
             c=np.array([255, 71, 90]) / 255.)
    plt.ylabel('Loss')

    ax2 = fig.add_subplot(111, sharex=ax1, frameon=False)
    ax2.plot(range(len(val_acc_history)), val_acc_history,
             c=np.array([79, 179, 255]) / 255.)
    ax2.yaxis.tick_right()
    ax2.yaxis.set_label_position("right")
    plt.ylabel('ValAcc')

    plt.xlabel('Epoch')
    plt.title('Training Loss & Validation Accuracy')
    plt.show()


# In[ ]:

#调用 train() 进行模型训练,同时调用 test() 进行测试。打印测试集上的准确率
loss, val_acc = train()
test_acc, test_logits, test_label = test(tensor_test_mask)
print("Test accuarcy: ", test_acc.item())

# In[14]:

#绘制训练过程中损失和验证准确率的变化图
plot_loss_with_acc(loss, val_acc)

# In[ ]:


# 绘制测试数据的TSNE降维图
#使用 t-SNE 对测试数据进行降维并绘制图形,展示模型对不同类别的区分效果
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  # 标签为i的选出来
    x, y = out[indices].T
    plt.scatter(x, y, label=str(i))
plt.legend()
plt.show()

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值