简介这份资源是《深入浅出图神经网络GNN原理解析》一书的配套代码包面向正在学习图神经网络、希望把理论落到实处的读者尤其适合具备一定Python与深度学习基础、想动手复现GCN等经典模型的学习者。包内共26个文件以py脚本、md说明文档和ipynb交互式笔记为主另含Cora数据集相关文件与勘误PDF压缩包约306KB体量轻便便于随书逐章对照运行。内容覆盖图卷积、图采样、自注意力池化、自编码器等章节示例并针对5.4节图滤波器描述偏差提供了勘误更正还附带本地Cora数据以规避下载失败问题。目前已有3483人学习下载读者可借此搭建可运行的实验环境理解模型实现细节与常见排错思路适合作为入门到进阶的实践参考。1. 从消息传递视角拆开 GNN这套配套代码到底能跑出什么如果你正在找一份能把 GCN、GAT、GraphSAGE 从公式落到 Jupyter Notebook 里逐行跑通的材料这套《深入浅出图神经网络GNN原理解析》配套代码值得花时间拆一遍。它不是那种只丢几个.py文件让你自己猜入口的仓库而是按书的章节组织成一组 Notebook每个文件对应一个可独立运行的知识点从图的邻接矩阵表示、拉普拉斯矩阵、消息传递范式到 GCN 的对称归一化、GAT 的注意力系数、GraphSAGE 的邻居采样聚合再到节点分类和图表征的下游任务。关键词里出现的 gcn、gnn、graph-neural-network、JupyterNotebook 基本覆盖了它的全部内容形态。适合两类人一类是刚看完公式想验证自己理解对不对的另一类是已经会用 PyTorch 但没系统写过图卷积、想拿一份能改能调的结构做二次开发的。下面按“先立住原理、再动手复现、最后说坑”的顺序拆。2. 环境与数据准备把 Notebook 跑起来的前置动作2.1 依赖版本与安装顺序这套代码的 Notebook 依赖 PyTorch 和 PyTorch GeometricPyG版本不匹配是新手翻车最集中的地方。PyG 的 wheel 和 PyTorch、CUDA 版本强绑定直接pip install torch-geometric有时会拉到和本地 torch 不兼容的构建。我一般会先固定 torch再按官方给的对应关系装 PyG 的四个组件。# 先确认本地 torch 版本后面所有安装都围绕它 python -c import torch; print(torch.__version__, torch.version.cuda) # 以 CPU 版为例先装 torch版本按你实际环境替换 pip install torch2.1.0 --index-url https://download.pytorch.org/whl/cpu # 再装 PyG 的四个依赖注意 torch 版本号要和上面一致 pip install torch-scatter torch-sparse torch-cluster torch-spline-conv \ -f https://data.pyg.org/whl/torch-2.1.0cpu.html # 最后装主包 pip install torch-geometric逻辑说明torch-scatter、torch-sparse这些是 PyG 的底层稀疏算子必须和 torch 的 ABI 对齐所以要用-f指定和 torch 版本一致的 wheel 源。参数上torch-2.1.0cpu里的cpu表示 CPU 构建如果你有 GPU 就换成对应的cu118之类。装完用python -c import torch_geometric; print(torch_geometric.__version__)验证能打印版本号才算过。2.2 数据集加载与格式确认配套代码里节点分类任务常用 Planetoid 系列Cora、Citeseer、Pubmed这些通过 PyG 的Planetoid类可以直接下载。但下载走的是外网源网络不稳时会卡在Downloading。常见做法是提前把raw目录准备好或者手动指定root到本地已有数据的路径。from torch_geometric.datasets import Planetoid # root 指向本地数据目录name 选数据集 dataset Planetoid(root./data/Cora, nameCora) data dataset[0] # 确认图的基本维度这是后面所有操作的基准 print(节点数:, data.num_nodes) print(边数:, data.num_edges) print(特征维度:, dataset.num_features) print(类别数:, dataset.num_classes) print(训练/验证/测试掩码:, data.train_mask.sum().item(), data.val_mask.sum().item(), data.test_mask.sum().item())逻辑说明Planetoid返回的data是一个Data对象x是节点特征矩阵edge_index是 COO 格式的边索引2×Ey是标签三个 mask 决定哪些节点参与训练、验证、测试。参数上root建议放在项目内固定目录避免每次换工作目录重复下载name大小写敏感写错会报找不到数据集。跑通这段说明环境和数据链路都通了可以进模型部分。3. GCN 与 GAT 的实现细节从公式到可运行层3.1 GCN 层的对称归一化与实现GCN 的核心是把邻接矩阵做对称归一化再和特征矩阵相乘。公式是 $\tilde{A} D^{-1/2}(AI)D^{-1/2}$其中 $AI$ 是加了自环的邻接矩阵$D$ 是度矩阵。配套代码里通常用GCNConv直接封装但理解内部计算对调参和排错很关键。import torch import torch.nn.functional as F from torch_geometric.nn import GCNConv class GCN(torch.nn.Module): def __init__(self, num_features, hidden, num_classes): super().__init__() # 第一层特征 - 隐藏 self.conv1 GCNConv(num_features, hidden) # 第二层隐藏 - 类别数 self.conv2 GCNConv(hidden, num_classes) def forward(self, data): x, edge_index data.x, data.edge_index # 第一层卷积 ReLU 激活 x self.conv1(x, edge_index) x F.relu(x) # 训练时加 dropout 防过拟合 x F.dropout(x, p0.5, trainingself.training) # 第二层输出 logits x self.conv2(x, edge_index) return F.log_softmax(x, dim1)逻辑说明GCNConv在内部完成了自环添加和对称归一化你不需要手动构造 $\tilde{A}$。参数上hidden一般取 16 或 32Cora 这种小图 16 就够dropout的p0.5是书里常用值图越大可以适当调低。注意forward里trainingself.training让 dropout 只在训练时生效推理时自动关闭这是 PyTorch 的标准写法漏掉会导致测试结果偏低。3.2 GAT 的注意力系数与多头机制GAT 和 GCN 的区别在于聚合邻居时用注意力权重代替固定的归一化系数。每个邻居的权重由节点对的特征计算得到再 softmax 归一化。多头注意力就是把多组独立的注意力结果拼接或平均。from torch_geometric.nn import GATConv class GAT(torch.nn.Module): def __init__(self, num_features, hidden, num_classes, heads8): super().__init__() # 第一层多头注意力输出 hidden*heads self.conv1 GATConv(num_features, hidden, headsheads, dropout0.6) # 第二层单头输出类别数 self.conv2 GATConv(hidden * heads, num_classes, heads1, concatFalse, dropout0.6) def forward(self, data): x, edge_index data.x, data.edge_index x F.dropout(x, p0.6, trainingself.training) x F.elu(self.conv1(x, edge_index)) x F.dropout(x, p0.6, trainingself.training) x self.conv2(x, edge_index) return F.log_softmax(x, dim1)逻辑说明第一层heads8表示 8 组独立注意力输出维度是hidden*heads因为默认concatTrue会拼接。第二层heads1, concatFalse是为了输出和类别数对齐如果这里还拼接维度就对不上了。参数上dropout0.6是 GAT 原论文在 Cora 上的设置比 GCN 高因为注意力机制本身参数更多更容易过拟合。激活函数用ELU而不是ReLU这是 GAT 的惯例换成 ReLU 在小图上掉点比较明显。3.3 训练循环与评估指标模型定义好之后训练循环要处理 mask 的选择。节点分类不是所有节点都参与 loss只有train_mask为 True 的节点算损失。def train(model, data, optimizer): model.train() optimizer.zero_grad() out model(data) # 只用训练掩码内的节点计算损失 loss F.nll_loss(out[data.train_mask], data.y[data.train_mask]) loss.backward() optimizer.step() return loss.item() torch.no_grad() def test(model, data): model.eval() out model(data) pred out.argmax(dim1) accs [] # 分别算训练/验证/测试准确率 for mask in [data.train_mask, data.val_mask, data.test_mask]: correct (pred[mask] data.y[mask]).sum() accs.append(correct.item() / mask.sum().item()) return accs逻辑说明nll_loss配合前面的log_softmax使用等价于交叉熵。torch.no_grad()装饰器在评估时关闭梯度省显存也更快。参数上优化器一般用Adam(lr0.01, weight_decay5e-4)这是 GCN 原论文在 Cora 上的配置weight_decay相当于 L2 正则。跑 200 个 epochCora 上 GCN 测试准确率大概在 0.81 上下GAT 能到 0.83 左右如果明显低于这个区间先查 mask 有没有用对、dropout 有没有在推理时误开。4. 避坑与排查跑这套代码最容易卡住的五个地方4.1 现象PyG 导入报 undefined symbol原因torch-scatter等底层算子的 wheel 和当前 torch 版本 ABI 不匹配通常是先装了 PyG 再升级 torch 导致的。解决先pip uninstall torch-scatter torch-sparse torch-cluster torch-spline-conv torch-geometric确认torch.__version__后严格按 2.1 节的顺序重装wheel 源里的 torch 版本号必须和本地一致。4.2 现象数据集下载卡住或报 URLError原因Planetoid默认从外网拉数据网络不通时卡在下载。解决手动下载cora的raw文件放到root/raw目录下或者换用本地已有的图数据。注意root路径要写绝对路径或相对项目根目录的路径写错会重复触发下载。4.3 现象训练 loss 不降、准确率停在随机水平原因常见是 mask 用错比如把train_mask写成了全 True或者y的维度对不上。解决打印data.train_mask.sum()确认训练节点数Cora 是 140 个再检查out[data.train_mask]和data.y[data.train_mask]的 shape 是否一致。另一个可能是学习率太大lr0.01是安全值调到 0.1 容易震荡。4.4 现象GAT 第二层维度报错原因第一层多头拼接后输出是hidden*heads第二层输入没对应上。解决第二层GATConv的in_channels写成hidden*heads并且设concatFalse否则输出维度是num_classes*heads和标签对不上。4.5 现象测试准确率比预期低好几个点原因评估时忘了model.eval()dropout 还在生效或者torch.no_grad()漏了导致 batch norm 统计量被更新虽然这套代码一般不用 BN。解决评估函数开头强制model.eval()结束再model.train()确认 dropout 的training参数跟着模型状态走。5. 进阶玩法把单图节点分类扩到多图与自定义数据跑通 Cora 之后这套代码的价值在于它的层实现可以迁移到别的图任务。一个具体技巧是把GCNConv和GATConv用到多图场景比如分子图或社交网络子图这时数据组织方式从单个Data变成DataLoader里的Batch。from torch_geometric.loader import DataLoader from torch_geometric.datasets import TUDataset # 加载多图数据集每个样本是一张独立的图 dataset TUDataset(root./data/ENZYMES, nameENZYMES) loader DataLoader(dataset, batch_size32, shuffleTrue) # 多图分类时forward 里对每张图做全局池化 from torch_geometric.nn import global_mean_pool class GraphClassifier(torch.nn.Module): def __init__(self, num_features, hidden, num_classes): super().__init__() self.conv1 GCNConv(num_features, hidden) self.conv2 GCNConv(hidden, hidden) self.fc torch.nn.Linear(hidden, num_classes) def forward(self, data): x, edge_index, batch data.x, data.edge_index, data.batch x F.relu(self.conv1(x, edge_index)) x F.relu(self.conv2(x, edge_index)) # 按 batch 把节点特征聚合成图级表示 x global_mean_pool(x, batch) return F.log_softmax(self.fc(x), dim1)逻辑说明DataLoader会把多张图拼成一个大图batch向量标记每个节点属于哪张图global_mean_pool按这个标记做图级平均。参数上batch_size32对 ENZYMES 这种小图合适图更大要调小。这里的关键差异是节点分类用train_mask选节点图分类用batch做池化两者不能混。另一个技巧是自定义数据。如果你手头有边列表和节点特征可以自己构造Data对象不用依赖内置数据集。import torch from torch_geometric.data import Data # 假设有 4 个节点、5 条边 edge_index torch.tensor([[0, 1, 2, 3, 0], [1, 2, 3, 0, 2]], dtypetorch.long) x torch.randn(4, 16) # 4 个节点每个 16 维特征 y torch.tensor([0, 1, 0, 1]) # 每个节点的标签 data Data(xx, edge_indexedge_index, yy) # 手动加训练掩码 data.train_mask torch.tensor([True, True, False, False]) data.test_mask ~data.train_mask逻辑说明edge_index是 2×E 的 COO 格式第一行是源节点第二行是目标节点无向图要正反都写一遍。x的行数必须等于节点数y的长度也要等于节点数。掩码是布尔张量长度和节点数一致。构造完直接丢进前面的GCN类就能跑这是把配套代码用到自己数据上最短的路径。从那以后我每次拿到一份图神经网络代码都强制先跑一遍数据维度检查确认num_nodes、num_features、num_classes和 mask 数量对得上再动模型。这套配套代码的结构清晰适合当模板改但环境版本和 mask 这两处最容易出玄学问题提前查一遍能省不少时间。希望帮到你。本文还有配套的精品资源点击获取