简介:这份资源是《深入浅出图神经网络: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 Geometric(PyG),版本不匹配是新手翻车最集中的地方。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 torch==2.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.0+cpu.html # 最后装主包 pip install torch-geometric逻辑说明:torch-scatter、torch-sparse这些是 PyG 的底层稀疏算子,必须和 torch 的 ABI 对齐,所以要用-f指定和 torch 版本一致的 wheel 源。参数上,torch-2.1.0+cpu里的+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', name='Cora') 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×E),y是标签,三个 mask 决定哪些节点参与训练、验证、测试。参数上,root建议放在项目内固定目录,避免每次换工作目录重复下载;name大小写敏感,写错会报找不到数据集。跑通这段,说明环境和数据链路都通了,可以进模型部分。
3. GCN 与 GAT 的实现细节:从公式到可运行层
3.1 GCN 层的对称归一化与实现
GCN 的核心是把邻接矩阵做对称归一化,再和特征矩阵相乘。公式是 $\tilde{A} = D^{-1/2}(A+I)D^{-1/2}$,其中 $A+I$ 是加了自环的邻接矩阵,$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, p=0.5, training=self.training) # 第二层输出 logits x = self.conv2(x, edge_index) return F.log_softmax(x, dim=1)逻辑说明:GCNConv在内部完成了自环添加和对称归一化,你不需要手动构造 $\tilde{A}$。参数上,hidden一般取 16 或 32,Cora 这种小图 16 就够;dropout的p=0.5是书里常用值,图越大可以适当调低。注意forward里training=self.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, heads=8): super().__init__() # 第一层多头注意力,输出 hidden*heads self.conv1 = GATConv(num_features, hidden, heads=heads, dropout=0.6) # 第二层单头,输出类别数 self.conv2 = GATConv(hidden * heads, num_classes, heads=1, concat=False, dropout=0.6) def forward(self, data): x, edge_index = data.x, data.edge_index x = F.dropout(x, p=0.6, training=self.training) x = F.elu(self.conv1(x, edge_index)) x = F.dropout(x, p=0.6, training=self.training) x = self.conv2(x, edge_index) return F.log_softmax(x, dim=1)逻辑说明:第一层heads=8表示 8 组独立注意力,输出维度是hidden*heads,因为默认concat=True会拼接。第二层heads=1, concat=False是为了输出和类别数对齐,如果这里还拼接维度就对不上了。参数上,dropout=0.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(dim=1) 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(lr=0.01, weight_decay=5e-4),这是 GCN 原论文在 Cora 上的配置,weight_decay相当于 L2 正则。跑 200 个 epoch,Cora 上 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 是否一致。另一个可能是学习率太大,lr=0.01是安全值,调到 0.1 容易震荡。
4.4 现象:GAT 第二层维度报错
原因:第一层多头拼接后输出是hidden*heads,第二层输入没对应上。解决:第二层GATConv的in_channels写成hidden*heads,并且设concat=False,否则输出维度是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', name='ENZYMES') loader = DataLoader(dataset, batch_size=32, shuffle=True) # 多图分类时,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), dim=1)逻辑说明:DataLoader会把多张图拼成一个大图,batch向量标记每个节点属于哪张图,global_mean_pool按这个标记做图级平均。参数上,batch_size=32对 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]], dtype=torch.long) x = torch.randn(4, 16) # 4 个节点,每个 16 维特征 y = torch.tensor([0, 1, 0, 1]) # 每个节点的标签 data = Data(x=x, edge_index=edge_index, y=y) # 手动加训练掩码 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 这两处最容易出玄学问题,提前查一遍能省不少时间。希望帮到你。
本文还有配套的精品资源,点击获取