简介:这是一份面向图神经网络学习与研究的完整代码资源,覆盖社交网络分析、生物信息学、推荐系统等常见图数据场景。代码包内含模型架构定义、数据预处理、节点嵌入生成与训练评估流程,可直接运行并用于节点分类、链接预测和图分类等典型任务。资源共384个文件,以322个json数据文件为主,辅以59个zbak备份文件、配置说明txt及备用zip,整体大小仅2.16MB,轻量且便于对照学习;json中保存图结构、节点特征与标签等关键数据,zbak可追溯实验调整过程。已有238人学习下载,适合入门阶段希望借助可运行代码理解GNN原理的技术人员和学生。通过阅读实现细节,不仅能掌握GCN等模型的消息传递与邻居聚合逻辑,还能学会处理边信息、设计嵌入层及应对大规模图常见问题,为后续开发高级图分析算法打下基础。
1. gnn图神经网络代码完整:为什么你的模型跑在网格数据上很强,换个场景就废了
做过图像和序列建模的都知道,CNN 靠卷积核滑动窗口,LSTM 靠时间步递归,它们的前提假设是「数据有规则的网格形状」。可现实里大量问题天生是图结构——社交网络、分子结构、知识图谱、甚至表情识别里把人脸关键点连成拓扑图。你把这些数据硬塞给 CNN,等于把一张点列表强行排成一行像素,空间关系全丢了。GNN(Graph Neural Network,图神经网络)就是为这类数据设计的:它让每个节点通过「消息传递」聚合邻居信息,把图结构建模成神经网络的前向过程。这篇笔记我直接给你一套能跑通的完整代码路径——从环境装库、数据加载、模型构建到训练评估,全部落成可复制的代码块,也把我在真实项目中踩过的坑一并讲清楚。适合刚接触图神经网络、被网上碎片化代码搞得不知道从哪起步的人,也适合已经跑通 Demo 但不知道参数怎么调、边界在哪的从业者。
2. 先把环境装对:PyG 还是 DGL,两类库的选型与安装
2.1 PyG 与 DGL 怎么选,一份对比
现在写 GNN 代码,主流选择就是 PyTorch Geometric(简称 PyG)和 Deep Graph Library(DGL)。两个库都实现了 GCN、GAT、GraphSAGE 这些经典模型,也都能跑在 PyTorch 后端上,但设计哲学差别很大。
PyG 的核心思路是「把图数据当成一种新的 Tensor 类型」,它提供了Data对象来统一存储节点特征、边索引、标签,配合MessagePassing基类,你自定义一个卷积层只需要写message和aggregate两个方法。上手写模型很直接,社区教程也多,Debug 的时候看中间张量形状很方便。
DGL 的图数据规范强调「显式区分节点和边上的计算」,它的update_all接口把消息计算和聚合分得更开,在超大图分布式训练上做得更早更成熟。如果你后面要上集群训练,DGL 有优势;如果只是本地学习、做原型验证,PyG 一般更顺手。
还有一类库值得提一下:DeepChem 和 NetworkX。DeepChem 面向分子图数据做药物研发,内置了大量化学特征转换工具;NetworkX 是纯 Python 图分析库,本身不提供神经网络层,但经常用来做图数据的预处理和可视化。我的建议是:入门用 PyG,主写模型逻辑;需要复杂图结构分析时配合 NetworkX 做预处理。
2.2 在本地装好 PyG 的完整命令与版本搭配
PyG 的安装是个经典翻车点——它的核心torch-scatter、torch-sparse、torch-cluster是编译型扩展,直接pip install torch-geometric通常能装上主包,但跑代码时一 import 就报ModuleNotFoundError或者 binary 不匹配。
正确做法是先确认你的 PyTorch 版本和 CUDA 版本,然后按对应版本安装配套预编译包。我一般用nvidia-smi看 CUDA 驱动版本,再用python -c "import torch; print(torch.__version__, torch.version.cuda)"看 PyTorch 实际用的 CUDA 版本。这两个版本号决定了后面所有 wheel 的下载路径:
# 以 CUDA 12.1 + PyTorch 2.3 为例,先装配套扩展 pip install torch-scatter torch-sparse torch-cluster -f https://data.pyg.org/whl/torch-2.3.0+cu121.html # 再装主包 pip install torch-geometric参数说明:-f指定了预编译 wheel 的索引地址,这里的torch-2.3.0+cu121要和你的本地环境严格对齐。如果 PyTorch 是 2.2.0 配 CUDA 11.8,就改成torch-2.2.0+cu118.html。装完一定要跑一遍 import 自检:
python -c "import torch_geometric; print(torch_geometric.__version__)"如果这一步报错,九成是扩展包版本不匹配。别急着重装 PyTorch——先看一眼你是不是用了 conda 环境,conda 的numpy版本偶尔会 和 PyG 冲突,最好在虚拟环境里重装numpy==1.26再试。
2.3 验证环境:用一段 20 行的最小图计算确认库能用
装好环境后我习惯先跑一个最小的张量级图计算,不让数据集下载这些环节干扰判断:
import torch from torch_geometric.data import Data # 手工构造一个 3 个节点、2 条边的有向图 # 边的起点和终点分开存储,索引从 0 开始 edge_index = torch.tensor([[0, 2], [1, 0]], dtype=torch.long) x = torch.tensor([[1.0], [2.0], [3.0]]) # 每个节点 1 维特征 data = Data(x=x, edge_index=edge_index) print(data)逻辑说明:edge_index是 PyG 的边存储格式,第一行是起点,第二行是终点,每条边对应一个起点-终点对。这里[0, 2]和[1, 0]分别表示0->2和1->0两条有向边。Data对象会自动校验节点特征数量和边的索引范围,如果边索引越过节点数会报错。
这个最小图能帮你快速确认三件事:Data对象能正常构造,edge_index维度是[2, num_edges],以及后续所有模型层的输入尺寸约定。跑通这段再往下走,后面模型报错时你就知道问题不在环境而在数据或模型本身。
3. 从零跑通一个完整的 GNN 分类代码:Cora 节点分类全流程
3.1 数据加载:拿到的不是一个 DataFrame,而是一个 Data 对象
图数据和表格数据的第一个分水岭就在这里——你用 pandas 读 CSV 是读不出边结构的。PyG 内置的Planetoid数据集封装了 Cora、Citeseer、Pubmed 这三个引用网络数据集,一行代码就能拿到已经处理好的 Data 对象:
from torch_geometric.datasets import Planetoid # 第一次运行会自动下载,只能下载官方源,路径要给有写权限的目录 dataset = Planetoid(root='/tmp/cora', name='Cora') data = dataset[0] # 看一眼数据结构 print(f'节点数: {data.num_nodes}') print(f'边数: {data.num_edges}') print(f'特征维度: {data.num_node_features}') print(f'类别数: {dataset.num_classes}') print(data.x.shape, data.edge_index.shape, data.y.shape) print(data.train_mask.sum(), data.val_mask.sum(), data.test_mask.sum())参数说明:root是本地缓存目录,数据集下载后解压存在这里,下次运行直接用缓存不会重复下载。name指定数据集名称。Cora 有 2708 个节点、5429 条边、每个节点 1433 维词袋特征,分为 7 类。
这里有个新手容易困惑的点:为什么dataset[0]而不是dataset直接用?Planetoid是数据集对象,里面只有一个图,取[0]拿到的才是Data对象。真正的图结构不在x里,而在edge_index里。train_mask、val_mask、test_mask是三个布尔数组,标记了每个节点属于训练、验证还是测试集——这是 PyG 官方提供的固定划分,用同一个划分才能和论文结果做公平对比。
如果 download 一直失败,常见原因是网络请求被限制。这种情况下不要反复重试,改成手动下载方案:用浏览器访问 PyG 官方数据源(https://github.com/kimiyoung/planetoid/raw/master/data/路径下的cora.tgz),下载后解压到root/planetoid/raw/目录,再把文件名改成 PyG 期望的cora.cites、cora.content,最后把processed目录删掉重新运行代码。
3.2 模型定义:GCN 的 forward 里到底做了什么
GCN 是最基础的图卷积模型,理解它的 forward 就理解了一半的 GNN。它的每一层做三件事:线性变换、邻居聚合、非线性激活。PyG 的GCNConv把这三步封装好了,但你要明白每一步在干什么:
import torch import torch.nn.functional as F from torch_geometric.nn import GCNConv class GCN(torch.nn.Module): def __init__(self, in_channels, hidden_channels, out_channels): super().__init__() # 第一层:把节点特征从 in_channels 维映射到 hidden_channels 维 self.conv1 = GCNConv(in_channels, hidden_channels) # 第二层:映射到输出维度,即类别数 self.conv2 = GCNConv(hidden_channels, out_channels) def forward(self, x, edge_index): # 第一层卷积 + ReLU + Dropout x = self.conv1(x, edge_index) x = F.relu(x) x = F.dropout(x, training=self.training) # 第二层卷积,输出 logits(未经过 softmax) x = self.conv2(x, edge_index) return x逻辑说明:GCNConv内部的核心操作是D^{-1/2} A D^{-1/2} X W,其中A是加了自环的邻接矩阵,D是度矩阵,X是节点特征,W是权重矩阵。PyG 在forward里做了对称归一化——它先用add_self_loops给每个节点加上指向自身的边,再计算归一化系数存到edge_weight里,最后执行 scatter 聚合。Dropout 层只在前向传播时生效,training=self.training这个写法让模型在eval()模式下自动跳过 Dropout。
这里反映了一个关键设计:GCN 不像 CNN 那样有「卷积核滑动」的概念,它的聚合范围完全由edge_index定义。同一个GCNConv层放在不同结构的图上,感受野会随图结构变化——这也是 GNN 和传统神经网络最本质的差异。
3.3 训练循环:mini-batch 与全图训练的取舍
Cora 只有 2708 个节点,全图训练无压力,直接整图进模型。代码结构上你会看到它和标准 PyTorch 训练几乎一样,但有两个细节不一样:optimizer.zero_grad()清梯度之后,forward 的时候要把data.edge_index传进去,而不是把 batch 的样本索引传进去;loss 只在train_mask对应的节点上计算。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = GCN(dataset.num_node_features, 16, dataset.num_classes).to(device) data = data.to(device) optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) def train(): model.train() optimizer.zero_grad() out = model(data.x, data.edge_index) # 整图一次前向 loss = F.cross_entropy(out[data.train_mask], data.y[data.train_mask]) loss.backward() optimizer.step() return loss.item() for epoch in range(200): loss = train() if epoch % 20 == 0: print(f'Epoch {epoch:3d}, Loss: {loss:.4f}')参数说明:lr=0.01配上weight_decay=5e-4是 Cora 上比较稳定的组合,但这个参数只在整图训练这种「数据量小、每步全量梯度」的场景下可靠。如果你换成大图,learning rate 要按 batch 大小重新调,一般会降到1e-3到3e-3区间。
全图训练的优势是梯度稳定,每次迭代看到全部节点的梯度;缺点是显存和计算代价随图规模线性涨。当图规模超过百万节点,全图训练就不可能了,需要切 batch。PyG 的NeighborLoader是常见方案,它按「采样邻居」的方式生成子图:
from torch_geometric.loader import NeighborLoader loader = NeighborLoader(data, num_neighbors=[10, 10], batch_size=128, shuffle=True)这个代码的作用是:每个 batch 取128个目标节点,每一层只采样10个邻居来聚合特征。num_neighbors列表的长度必须和网络层数一致。采样会显著降显存和计算量,代价是聚合信息不完整,精度通常会掉几个点,具体掉多少看图的密度。
3.4 评估与可视化:准确率不是唯一指标
评估 GNN 和评估普通分类模型有个重要差别:你不能把节点随机打乱再划分训练集测试集——图上的节点是有相关性的,随机划分会让训练集节点泄漏测试集的信息(邻居关系)。所以 PyG 提供了固定划分好的 mask,你用官方 mask 跑完才能和别人比:
def test(): model.eval() with torch.no_grad(): out = model(data.x, data.edge_index) pred = out.argmax(dim=1) accs = [] for _, mask in [('train', data.train_mask), ('val', data.val_mask), ('test', data.test_mask)]: acc = (pred[mask] == data.y[mask]).sum().item() / mask.sum().item() accs.append(acc) return accs train_acc, val_acc, test_acc = test() print(f'Train: {train_acc:.4f}, Val: {val_acc:.4f}, Test: {test_acc:.4f}')逻辑说明:model.eval()切换 BatchNorm 和 Dropout 的行为,torch.no_grad()关闭梯度追踪,省显存也加速推理。准确率按 mask 分别计算——这里强调的是 GNN 模型必须在图划分之后评估,不能用随机抽样验证。Cora 上这个简单 GCN 模型跑满 200 epoch,测试准确率大概在 80% 上下,这个数字是合理预期。
可视化建议用torch_geometric.utils.to_networkx把 Data 转成 NetworkX 图,再用nx.spring_layout做节点布局,按类别给节点上色,你就能直观看到学到的特征是不是按类聚在一起。这个操作对调试非常有用,后面会单独展开。
4. GNN 代码最常见的 5 个坑:从输入到输出逐个排查
4.1 自环缺失导致节点自身信息丢失
现象:模型训练 loss 忽高忽低,训练集准确率一直上不去,和论文结果差出一大截。
原因:GCN 的聚合公式里D^{-1/2} A D^{-1/2}中,A必须包含自环(即A[i][i] = 1)。如果不加自环,每个节点的更新只聚合邻居信息,丢掉了自身的原始特征,节点所在位置的信号会被反向稀释。PyG 的GCNConv默认帮你加自环(add_self_loops=True),但如果你手工构造edge_index且没有自环边,模型会静默地以「无自环」方式运行,不报错但效果差。
解决:手工构造边的逻辑里显式加上自环边:
import torch from torch_geometric.utils import add_self_loops edge_index, _ = add_self_loops(edge_index, num_nodes=num_nodes)这个函数的含义是把[0, n-1] -> [0, n-1]的 n 条自环边追加到edge_index中。返回值第一项是拼接后的边索引,第二项是边权重(没有的话是 None)。也可以在GCNConv初始化时显式传add_self_loops=False关闭,但一般不推荐这么做。
4.2 归一化方式错误导致特征分布异常
现象:训练时 loss 快速降到很低,但 val/test 表现极差,过拟合特征明显;或者不同类别的节点特征聚合后数值范围差异巨大。
原因:GCN 的归一化是「对称归一化」,每个节点聚合时按1 / sqrt(deg(i)) * 1 / sqrt(deg(j))缩放每条边上的消息。有人图省事直接用简单平均或者直接求和,这在度数分布不均匀的图上会让高热度节点(比如社交网络上粉丝上百万的账号)的特征值被放大,低热度节点的特征被淹没。GCNConv内部默认就是对称归一化,但如果你用MessagePassing自定义了卷积层,就要自己确认归一化系数的计算方式。
解决:最小复现路径里用GCNConv而不是自定义卷积。如果自定义,参考下面这个正确的归一化写法:
from torch_geometric.utils import degree row, col = edge_index deg = degree(col, num_nodes=x.size(0)) # 每个节点的度 deg_inv_sqrt = deg.pow(-0.5) deg_inv_sqrt[deg_inv_sqrt == float('inf')] = 0 norm = deg_inv_sqrt[row] * deg_inv_sqrt[col] # 之后的 message 计算里,把 x_j 乘以 norm这条代码里degree统计每条边的终点节点度数,pow(-0.5)计算平方根倒数,deg_inv_sqrt[row] * deg_inv_sqrt[col]就是每条边两端节点的归一化系数乘积。inf清零防的是孤立节点除零。
4.3 维度不匹配:消息传递 shape 报错怎么快速定位
现象:跑第一遍训练就报RuntimeError: size mismatch,堆栈信息指向模型的某一层。
原因:GCNConv的维度要求是in_channels等于输入特征数,out_channels是你想映射到的维数。最常见的维度错误发生在:dataset.num_node_features拿到的特征维度和实际数据不一致;或者自定义特征处理器里维度搞错了;还有edge_index的数据类型不对,必须是torch.long,如果从 numpy 转换没指定 dtype,默认是float64,会直接报类型错误。
解决:在模型 forward 里加 shape 打印,这是最快的远程定位方式:
class GCN(torch.nn.Module): def forward(self, x, edge_index): print(f'x shape: {x.shape}') print(f'edge_index shape: {edge_index.shape}') x = self.conv1(x, edge_index) print(f'after conv1: {x.shape}') ...观察点:x 的最后一维必须等于第一层 GCNConv 的 in_channels,edge_index 必须是[2, num_edges],并且 edge_index 里的最大值不能超过节点数减一。另外注意edge_index的 dtype 要处理成torch.long,而x一般是torch.float32,两者不统一也会报 dtype mismatch。
4.4 数据划分随机化导致指标无法复现
现象:每次跑出来的准确率波动很大,同一份代码两次运行结果差 5 个点以上,和论文对不上。
原因:如果直接用train_test_split或者随机打乱 index 来划分训练测试集,等于在破坏图的拓扑结构——相邻节点很可能被分到不同集合,测试集节点从训练集节点拿到邻居信息,这被称为「信息泄漏」。结果当然虚高,而且每次划分随机性不同,指标波动大。用固定种子治标不治本,因为划分逻辑本身就是错的。
解决:用官方划分或基于连通分量的方式做数据划分。PyG 内置 mask 已是最佳方案,自己处理数据时参考下面的划分思路:
# 取前 500 个节点做训练集,后 1000 个做验证集,其余测试集 train_mask = torch.zeros(num_nodes, dtype=torch.bool) val_mask = torch.zeros(num_nodes, dtype=torch.bool) test_mask = torch.ones(num_nodes, dtype=torch.bool) train_mask[:500] = True val_mask[500:1500] = True test_mask[:500] = False test_mask[500:1500] = False这个写法没有技术难度但概念很重要:按节点索引顺序划分比随机抽样更能保持图结构的完整性,因为索引分布和连通性相关。如果你的数据是异构图,务必按类型分组划分,不要跨类型混合划分。
4.5 显存溢出:大图怎么把模型塞进 GPU
现象:全图训练跑在百万级节点图上,CUDA out of memory在训练第一轮就出现,显存监控显示占用飙满。
原因:全图前向传播会同时维护所有节点的中间表示,在 GNN 里这个中间表示的大小是「节点数 × 隐藏层维数」。百万节点 × 256 维 × 4 字节 ≈ 1GB 起步,加上梯度传播需要保存中间变量,实际占用翻倍。另外 GNN 反向传播时邻居聚合需要回溯整棵计算子图,复杂度比普通 MLP 高很多。
解决:换成NeighborLoader采样训练。它每个 batch 只采样一个子图,子图规模被num_neighbors和batch_size两个参数控制,显存可以提前估算。从全图切到采样训练后,learning rate 一般要调小,收敛速度也会变慢——这是信息损失换显存的必然代价。另一个技巧是torch.cuda.empty_cache()在每轮迭代后清理缓存,虽然对峰值显存帮助有限,但能缓解显存碎片化。
5. 让 GNN 代码真正可用:自定义数据集、配置化与调试三板斧
5.1 为你的数据写一个自定义 Dataset
内置数据集只能用来学原理,到了真实项目——比如图神经网络表情识别、通信网络优化——数据长在你自己手里,哄 PyG 吃下它需要对Dataset协议做最小实现。PyG 的InMemoryDataset要求你实现两个方法:raw_file_names指定原始文件路径,process做数据变换并保存到processed目录。下面这个例子是读一个nodes.csv和edges.csv构造 Data 的骨架:
import torch import pandas as pd from torch_geometric.data import InMemoryDataset, Data class MyGraphDataset(InMemoryDataset): def __init__(self, root, transform=None): super().__init__(root, transform) self.data, self.slices = torch.load(self.processed_paths[0]) @property def raw_file_names(self): return ['nodes.csv', 'edges.csv'] @property def processed_file_names(self): return ['data.pt'] def process(self): nodes = pd.read_csv(self.raw_paths[0]) edges = pd.read_csv(self.raw_paths[1]) x = torch.tensor(nodes.iloc[:, :-1].values, dtype=torch.float) y = torch.tensor(nodes.iloc[:, -1].values, dtype=torch.long) edge_index = torch.tensor(edges[['src', 'dst']].values.T, dtype=torch.long) data = Data(x=x, edge_index=edge_index, y=y) torch.save((data, self.collate([data])), self.processed_paths[0])逻辑说明:process只在首次运行时执行,torch.load加载的是(data, slices)的元组,其中slices记录了多图 batch 时每个图的分隔边界。读节点表时把特征列和标签列拆开,最后collate([data])负责把 Data 对象打包成 PyG 内部格式。这里注意src、dst的索引必须是从 0 开始的连续整数,否则后面对齐会有坑。标签是分类问题就用torch.long,回归问题用torch.float。预处理永远只做一次、缓存到磁盘,这个约定能避免每次跑实验都重复转换。
5.2 批量实验的配置化写法
GNN 的超参数搜索比 CNN 更敏感,同一份数据换个 learning rate 可能差 10 个点。我建议用argparse或 YAML 配置把实验固定住,避免在代码里改数值。下面是一个极简配置骨架:
import argparse def get_config(): parser = argparse.ArgumentParser() parser.add_argument('--dataset', type=str, default='Cora') parser.add_argument('--hidden', type=int, default=16) parser.add_argument('--layers', type=int, default=2) parser.add_argument('--lr', type=float, default=0.01) parser.add_argument('--weight_decay', type=float, default=5e-4) parser.add_argument('--epochs', type=int, default=200) parser.add_argument('--seed', type=int, default=42) args = parser.parse_args() return args逻辑说明:args从命令行读取,每次运行时把参数追加到日志文件的文件名里,例如Cora_h16_lr0.01_seed42.log。这份配置的价值在于让实验可复现——图神经网络的随机性来自参数初始化和 dropout,固定 seed 至少能让每次运行在同环境下定住。批量实验时再套一个 shell 循环改参数,比手动改代码安全得多。
5.3 用学习曲线判断模型是否真的在学特征
最后这板斧是我最常用的一招:不看最终准确率,看训练集和验证集的 loss 曲线形态。GNN 训练正常时,train loss 和 train acc 单调上升,val acc 先升后平;如果 val acc 震荡剧烈,多半是学习率太大;如果 train acc 很高但 val acc 低于 train 一大截,是过拟合——减少层数或增加 dropout;如果 train acc 都上不去,问题在前面的数据处理和特征工程,不在模型。
顺带提一个常见误区:把model.eval()忘掉。在训练循环结束后直接评估不调用eval(),BatchNorm 会继续用训练集的统计量,Dropout 也会随机丢节点,测试准确率会上下跳动。你损失一晚上的调试时间可能就是为了查这一行代码。
自己写过不止一次这种情况:图数据预处理时节点索引没对齐,edge_index的起点终点在不存在的节点上,模型不报错但聚合结果全是垃圾。后来慢慢养成的习惯是,任何新数据集的第一件事不是直接塞模型,而是先把它画成图看一眼——节点数量、边密度、孤立点比例,这些用 NetworkX 几条命令就能查。图神经网络的黑匣子程度比 CNN 高不少,学会在代码里埋 shape、埋 loss 曲线,「黑匣子」也能拆成看得清的计算过程。希望这些踩过的坑和写顺手的代码骨架,能帮你少走几段弯路。
本文还有配套的精品资源,点击获取