跳到主要内容
SUN / 笔记
← 全部文章

图神经网络与 PyTorch Geometric

1. 先分清图任务的预测对象

任务 输入 预测对象 源目录示例
节点分类 一张或多张图 节点标签 Cora GCN、PPI GAT、DGCN node
图分类 多张图 每张图的标签 MUTAG GIN、PROTEINS GMT、DASBM
链路预测 图和候选节点对 边是否存在/边类别 Cora link prediction、DGCN link
图表示学习 图结构与特征 节点或图 embedding GraphSAGE 无监督训练

任务类型决定数据划分、组 batch 方法、模型输出形状和指标,不能只替换最后一个分类层。

2. PyG 的 Data 对象

一张同构图通常表示为:

python
import torch
from torch_geometric.data import Data

x = torch.tensor(
    [[1.0, 0.0], [0.0, 1.0]],
    dtype=torch.float32,
)
edge_index = torch.tensor(
    [[0, 1], [1, 0]],
    dtype=torch.long,
)

data = Data(x=x, edge_index=edge_index)

主要属性:

属性 典型形状 含义
x (num_nodes, num_node_features) 节点特征
edge_index (2, num_edges) COO 形式的源、目标节点索引
edge_attr (num_edges, num_edge_features) 边特征,可选
y 依任务而异 节点级、边级或图级标签
pos (num_nodes, dimensions) 节点坐标,可选
batch (total_nodes,) 多图 mini-batch 中每个节点所属的图

图级任务中的 batch 不是普通样本维度。通常需要 global_add_pool、global_mean_pool 等把节点表示聚合成图表示。

3. 消息传递是 GNN 的共同骨架

一层消息传递可抽象为:

mij(l)=ϕ(hi(l),hj(l),eij) m_{ij}^{(l)}=\phi\left(h_i^{(l)},h_j^{(l)},e_{ij}\right)
mi(l)=AGG⁡j∈N(i)mij(l) m_i^{(l)}=\operatorname{AGG}_{j\in\mathcal{N}(i)}m_{ij}^{(l)}
hi(l+1)=γ(hi(l),mi(l)) h_i^{(l+1)}=\gamma\left(h_i^{(l)},m_i^{(l)}\right)
flowchart LR
    N[邻居节点特征] --> M[构造消息]
    E[边与边特征] --> M
    M --> A[sum / mean / max / attention 聚合]
    S[中心节点特征] --> U[更新节点状态]
    A --> U

聚合函数必须对邻居顺序保持不变。堆叠 LL 层后,节点理论上可以接收 LL 跳邻域信息,但过深还会带来过平滑、过压缩和计算成本问题。

4. GCN 的归一化传播

经典 GCN 层写为:

X′=D^−1/2A^D^−1/2XΘ X'=\hat{D}^{-1/2}\hat{A}\hat{D}^{-1/2}X\Theta

其中 A^=A+I\hat{A}=A+I 加入自环,D^\hat{D} 是对应度矩阵。对称归一化用于平衡不同度数节点的消息规模。

使用 PyG 内置 GCNConv:

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


class GCN(torch.nn.Module):
    def __init__(self, in_channels, hidden_channels, num_classes):
        super().__init__()
        self.conv1 = GCNConv(in_channels, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, num_classes)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = F.relu(x)
        x = F.dropout(x, p=0.5, training=self.training)
        return self.conv2(x, edge_index)

源 Notebook 还手写了继承 MessagePassing 的 GCNConv,有助于理解 add_self_loops -> 线性变换 -> 计算归一化 -> propagate -> message 的过程。工程代码优先使用官方层,除非确实需要修改消息函数或聚合规则。

5. Cora:单图节点分类

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

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

为什么 len(dataset) == 1:整个 Cora 引用网络是一张图,样本是图中的节点,不是一篇论文对应一个 Data。

源记录与 PyG 公共划分的关键形状:

内容 数值
图数量 1
节点数 2708
节点特征 1433 维词袋特征
类别数 7
PyG edge_index 条目 10556
训练/验证/测试 mask 140 / 500 / 1000 个节点

边数为什么有两个口径

原笔记开头写了 5429 条引用关系,后面 PyG 输出是 edge_index=[2, 10556]。两者不能直接互换:

  • 原始数据描述可能按引用关系或去重关系计数;
  • PyG 为无向消息传递通常存储两个方向的边条目,还会处理重复边;
  • 代码里真正参与传播的数量应以当前 data.edge_index.size(1) 为准;
  • 写实验报告时必须注明统计的是“关系数”还是“有向 COO 条目数”。

mask 训练

python
model.train()
optimizer.zero_grad(set_to_none=True)
logits = model(data.x, data.edge_index)
loss = F.cross_entropy(logits[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()

评估时分别使用 val_mask 与 test_mask,不能把整张图上的所有标签都加入训练损失。

6. 不同图任务的代码结构

节点分类:PPI + GAT

  • 模型:GATConv;
  • 任务:多标签节点分类;
  • 指标:F1,而不是普通单标签 accuracy;
  • 数据:训练、验证、测试由多张图组成。

图分类:MUTAG + GIN

  • 每张分子图对应一个标签;
  • GIN 在节点层做消息传递;
  • global_add_pool(x, batch) 把节点表示变成图表示;
  • 图级 logits 与 data.y 计算交叉熵。

图分类:PROTEINS + GraphMultisetTransformer

源代码组合 GCNConv 与 GraphMultisetTransformer,说明图级 readout 不只可以使用简单 sum/mean pooling,也可以使用可学习的集合聚合。

链路预测:Cora

  • 使用 RandomLinkSplit 划分正边;
  • 训练时进行 negative sampling;
  • 编码器生成节点表示;
  • 解码器对节点对打分;
  • 使用 ROC-AUC 等排序指标。

必须避免把验证/测试正边泄漏进训练图结构或负采样集合。

无监督 GraphSAGE

源代码用 LinkNeighborLoader 生成局部训练样本,通过边目标训练编码器,再用 Logistic Regression 或 SGDClassifier 评估 embedding。这里要分开:

  • 表示学习阶段的自监督目标;
  • 下游线性评估阶段的标签使用;
  • 采样图与评估图的边界。

有向图:DGCN

PYGsgd 下的两个脚本使用 torch_geometric_signed_directed:

  • dgcn_node.py:半监督节点分类;
  • dgcn_link.py:链接方向/边分类。

有向图不能总是先无向化。入邻居、出邻居、方向特征和任务标签可能都携带信息。

7. 图分类 benchmark 的工程分层

本地 PDF《大模型辅助编程-图分类任务》记录了把已有训练脚本迁入统一 benchmark 的思路,可归纳为:

flowchart TB
    C[Config<br/>model / task / data 参数] --> T[Task]
    D[Dataset<br/>下载 / 处理 / 划分 / 缓存] --> T
    M[Model<br/>可选预处理] --> W[Task Wrapper<br/>图级 pooling + 分类输出]
    W --> T
    T --> R[direct run / hyperparameter tune]
    R --> E[按验证集选模并报告测试结果]

值得保留的工程原则:

  • 配置、数据、模型、任务流程分离;
  • 数据集负责 raw/processed 缓存和 split;
  • 图级任务用 wrapper 统一不同模型的输出;
  • 多次运行或调参要保证每个 trial 独立初始化模型;
  • 以验证集选择 checkpoint,再记录对应测试指标;
  • 记录随机种子、数据划分和调参空间。

PDF 中的代码仍有未完成标记和路径耦合,应视作设计记录,不是可直接复用的最终框架。

8. 两份 GNN 示例目录的关系

源目录同时存在:

  • GNN训练流程代码阅读-官方github-examples:作为官方示例基线;
  • GNN训练流程代码阅读:本地阅读/运行副本,并包含已下载的 Cora、PPI 数据。

逐文件哈希与 diff 显示:

  • mutag_gin.py、graph_sage_unsup_ppi.py、两个 DGCN 脚本完全相同;
  • 其余差异主要是加入 KMP_DUPLICATE_LIB_OK、类型检查注释、设备日志;
  • 本地副本没有形成大量算法改写。

因此笔记中把它们视为“基线 + 本地运行副本”,不重复归纳成两套知识,也不在未确认前删除任何一份。

9. 图任务验收清单

  • [ ] 预测对象是节点、边还是图,输出形状与标签一致。
  • [ ] edge_index 的方向、重复、自环和无向化策略明确。
  • [ ] 图切分或边切分没有泄漏。
  • [ ] mini-batch 图任务正确使用 batch 和图级 pooling。
  • [ ] 节点任务只在训练 mask 上计算训练损失。
  • [ ] 负采样不会采到真实正边。
  • [ ] 指标匹配任务:accuracy、F1、ROC-AUC 等口径清楚。
  • [ ] 数据预处理缓存与代码版本一致。
  • [ ] 大图训练说明是否使用邻居采样及其 fanout。
  • [ ] 有向图任务说明是否保留方向信息。

来源映射

相关文章

强化学习笔记 →上下文工程 →Mission: 掌握多模态 LoRA 与 QLoRA →