从零构建图神经网络:PyTorch Geometric实战Cora节点分类

第一次接触图神经网络时,我被它的独特魅力所吸引——不同于传统神经网络处理网格化数据(如图像)或序列数据(如文本),图神经网络直接处理非结构化的图数据。这种能力让我在处理社交网络、推荐系统等实际问题时有了全新武器。本文将带你用PyTorch Geometric(简称PyG)这个强大的图神经网络库,从零开始实现Cora学术论文引文网络的节点分类任务。

1. 环境准备与数据理解

在开始之前,我们需要确保环境配置正确。推荐使用Python 3.8+和PyTorch 1.10+版本,这是PyG稳定运行的基础。安装PyTorch Geometric只需一行命令:

pip install torch torch-geometric

Cora数据集是图神经网络领域的"MNIST"——一个经典的基准测试集。它包含2708篇机器学习领域的学术论文,每篇论文被表示为图中的一个节点。这些论文被分为7个类别:

  • 基于案例的推理
  • 遗传算法
  • 神经网络
  • 概率方法
  • 强化学习
  • 规则学习
  • 理论

节点之间的边代表引用关系——如果论文A引用了论文B,图中就有一条从A指向B的边。每个节点还有1433维的特征向量,表示论文中是否包含特定词汇。

提示:Cora数据集会自动下载到指定路径。如果遇到下载问题,可以手动下载并放到 root 参数指定的目录中。

2. 数据加载与预处理

让我们首先加载数据并查看其基本结构:

from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures

dataset = Planetoid(root='./data/Cora', name='Cora', transform=NormalizeFeatures())
data = dataset[0]

print(f'Number of nodes: {data.num_nodes}')  # 2708
print(f'Number of edges: {data.num_edges}')  # 10556
print(f'Number of features: {dataset.num_features}')  # 1433
print(f'Number of classes: {dataset.num_classes}')  # 7
print(f'Training nodes: {data.train_mask.sum().item()}')  # 140

关键数据结构说明:

  • x : 节点特征矩阵,形状为[节点数, 特征维度]
  • edge_index : 图的边信息,形状为[2, 边数]
  • y : 节点标签
  • train_mask/val_mask/test_mask : 划分训练、验证和测试集的布尔掩码

PyG的一个便利之处是它已经帮我们处理好了数据划分。Cora数据集按照如下比例划分:

数据集 节点数量 比例
训练集 140 5%
验证集 500 18%
测试集 1000 37%

3. 构建图神经网络模型

我们将实现一个经典的两层图卷积网络(GCN)。与普通神经网络不同,GCN在传播信息时会考虑节点之间的连接关系。

import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self, hidden_channels):
        super().__init__()
        torch.manual_seed(1234567)
        self.conv1 = GCNConv(dataset.num_features, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, dataset.num_classes)
        
    def forward(self, x, edge_index):
        # 第一层图卷积
        x = self.conv1(x, edge_index)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        
        # 第二层图卷积
        x = self.conv2(x, edge_index)
        return x

model = GCN(hidden_channels=16)
print(model)

这个简单的架构已经包含了GCN的核心思想:

  1. 图卷积层(GCNConv) : 聚合邻居信息来更新节点表示
  2. 非线性激活(ReLU) : 引入非线性变换能力
  3. Dropout : 防止过拟合,增强泛化能力

注意:GCNConv会自动处理自环边(节点到自身的边),这是GCN能够保留节点自身信息的关键。

4. 模型训练与评估

训练图神经网络与训练传统神经网络类似,但有几点需要特别注意:

model = GCN(hidden_channels=16)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
criterion = torch.nn.CrossEntropyLoss()

def train():
    model.train()
    optimizer.zero_grad()
    out = model(data.x, data.edge_index)
    loss = criterion(out[data.train_mask], data.y[data.train_mask])
    loss.backward()
    optimizer.step()
    return loss

def test():
    model.eval()
    out = model(data.x, data.edge_index)
    pred = out.argmax(dim=1)
    correct = pred[data.test_mask] == data.y[data.test_mask]
    acc = int(correct.sum()) / int(data.test_mask.sum())
    return acc

for epoch in range(1, 101):
    loss = train()
    if epoch % 10 == 0:
        test_acc = test()
        print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}, Test Acc: {test_acc:.4f}')

训练过程中有几个关键点:

  1. 只使用训练节点计算损失 :通过 data.train_mask 筛选
  2. 测试时关闭Dropout :通过 model.eval() 设置
  3. 权重衰减(weight_decay) :相当于L2正则化,防止过拟合

典型的训练输出可能如下:

Epoch: 010, Loss: 1.6862, Test Acc: 0.5730
Epoch: 020, Loss: 1.3971, Test Acc: 0.7040
Epoch: 030, Loss: 1.0893, Test Acc: 0.7610
...
Epoch: 100, Loss: 0.3124, Test Acc: 0.8150

5. 结果可视化与分析

理解模型学到了什么至关重要。我们可以使用t-SNE将高维节点嵌入降维到2D空间进行可视化:

import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

def visualize(h, color):
    z = TSNE(n_components=2).fit_transform(h.detach().cpu().numpy())
    plt.figure(figsize=(10,10))
    plt.xticks([])
    plt.yticks([])
    plt.scatter(z[:,0], z[:,1], s=70, c=color, cmap="Set2")
    plt.show()

model.eval()
out = model(data.x, data.edge_index)
visualize(out, color=data.y)

好的可视化结果应该显示:

  • 同类节点在嵌入空间中聚集在一起
  • 不同类节点之间有相对清晰的边界
  • 边界区域可能存在引文关系密切的不同类论文

如果发现某些类别难以区分,可以考虑:

  1. 增加网络深度(更多GCN层)
  2. 调整隐藏层维度
  3. 尝试更复杂的图神经网络架构(如GAT、GraphSAGE)

6. 进阶技巧与优化

当基本模型运行起来后,我们可以考虑以下优化策略:

学习率调度 :动态调整学习率可以提升模型性能

scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
    optimizer, mode='max', factor=0.5, patience=5, verbose=True)

早停(Early Stopping) :防止过拟合

best_val_acc = 0
patience = 20
counter = 0

for epoch in range(1, 1001):
    train()
    val_acc = evaluate(val_mask)
    
    if val_acc > best_val_acc:
        best_val_acc = val_acc
        counter = 0
    else:
        counter += 1
        if counter == patience:
            print("Early stopping!")
            break

模型集成 :结合多个模型的预测

models = [GCN(hidden_channels=16) for _ in range(5)]
for m in models:
    train_model(m)  # 分别训练每个模型

# 预测时取平均
preds = [model(data.x, data.edge_index).argmax(dim=1) for model in models]
final_pred = torch.mode(torch.stack(preds), dim=0).values

7. 常见问题排查

在实际应用中,你可能会遇到以下问题:

问题1:验证集准确率高但测试集准确率低

可能原因:

  • 数据泄露(验证集和测试集划分不合理)
  • 模型在验证集上过拟合

解决方案:

  • 检查数据划分
  • 增加正则化(更高的dropout率、更强的权重衰减)

问题2:训练损失不下降

可能原因:

  • 学习率设置不当
  • 梯度消失/爆炸
  • 模型容量不足

解决方案:

  • 尝试不同的学习率(0.1, 0.01, 0.001等)
  • 添加残差连接
  • 增加隐藏层维度

问题3:GPU内存不足

解决方案:

  • 减小批处理大小
  • 使用更小的模型
  • 尝试梯度累积技术
accumulation_steps = 4  # 累积4个batch的梯度再更新

for epoch in range(epochs):
    model.train()
    
    for i, batch in enumerate(train_loader):
        out = model(batch.x, batch.edge_index)
        loss = criterion(out, batch.y) / accumulation_steps
        loss.backward()
        
        if (i + 1) % accumulation_steps == 0:
            optimizer.step()
            optimizer.zero_grad()

在Kaggle等平台的GPU环境中,合理利用梯度累积可以在有限内存下训练更大模型。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐