简介:基于GNN图神经网络预测的Python完整源码数据包,面向机器学习、图神经网络方向的研究者与开发者,旨在解决图结构数据上的节点分类与趋势预测任务,既适合入门学习,也可作为科研实验的基线参考。资源集成PPNP等经典GNN模型,涵盖数据预处理、模型训练与评估完整流程,并同时提供PyTorch与TensorFlow两种实现,方便按技术栈选用对照。包内共32个文件,以19个Python源码脚本为核心,另含4个Jupyter Notebook复现示例、4个npz图数据文件,以及依赖配置、环境说明等文档,压缩包仅8.34MB,占用小、部署快。目前已有2252人学习下载。使用该源码包可跳过环境搭建与数据准备环节,直接运行示例Notebook观察预测效果;深入阅读源码亦有助于理解GNN消息传递机制、图传播与标签传播等核心原理,还可基于现有模型修改结构和超参数,灵活适配其他图数据集,是图神经网络实战入门的实用资料。
1. GNN图神经网络预测:一份Python源码包真正要解决什么
一份标着“基于GNN图神经网络预测(Python完整源码数据包)”的资料,最常见的浪费方式是把环境配好、在自带数据集上跑通一次,然后就不知道怎么换到自己的数据上。实际上GNN图神经网络预测要做的只有一件事:把原本散落在“边”里的关系信息变成模型可学习的特征。社交网络里的关注关系、电商场景的用户购买序列、分子结构里的化学键,都是典型来源。用PyTorch Geometric写核心模型只要几十行代码,但数据拼装、任务划分和推理方式这三层,决定了同一个模型在不同业务场景下的效果差距。下面按一个可复现的最小工程结构推进:从关系表到Data对象,从两层GCN到GraphSAGE,从评估指标到对新节点的预测。
2. 数据准备:从原始关系表到GNN预测所需的邻接矩阵与节点特征
2.1 GNN的学习对象:为什么邻接矩阵是输入而不是预处理
传统机器学习对表格数据的默认假设是样本独立同分布,直接把这个假设搬到图上,等于丢掉了一半信息。GNN的做法是让每个节点的表示由自身特征和邻居特征共同决定。用最简化的图卷积公式来表达:
H^(l+1) = σ(Â · H^(l) · W^(l))
其中Â是加了自环并做归一化后的邻接矩阵,H^(l)是第l层的节点表示矩阵,W^(l)是本层可学习参数。一次卷积操作可以理解为“把直接邻居的特征加权求和后过一次非线性变换”。堆两层之后,每个节点的表示就包含了二阶邻居的信息。这也是GNN具备“预测”能力的来源:在节点自身属性之外,把图的连通性结构当成先验注入模型。理解了这一点,就能明白代码里不能丢弃邻接矩阵,也不能简单把边列表转成one-hot特征——聚合操作必须发生在图卷积层内部。
2.2 用PyTorch Geometric组装Data对象的最小代码
PyG(PyTorch Geometric)中所有图数据都统一为Data对象。下面这段代码把一个4节点4边的小图组装成标准输入:
import torch from torch_geometric.data import Data # 节点特征矩阵: 行数=节点数, 每一行是一个节点的属性向量 x = torch.tensor([ [0.1, 0.2], [0.3, 0.4], [0.5, 0.6], [0.7, 0.8] ], dtype=torch.float) # edge_index: 2行N列, 第一行是边的源节点索引, 第二行是目标节点索引 edge_index = torch.tensor([ [0, 1, 2, 3], [1, 2, 3, 0] ], dtype=torch.long) # y: 每个节点的类别标签, 用于有监督的节点分类预测 y = torch.tensor([0, 1, 0, 1], dtype=torch.long) data = Data(x=x, edge_index=edge_index, y=y) print(data.num_nodes, data.num_edges) # 输出: 4 4edge_index的格式是常见踩坑点:它必须是二维long型张量,第一行存边的起点,第二行存终点。如果原始边表是单向的,而业务场景不分方向,应先把图转成无向图,常见做法是调用data = data.to_undirected(),PyG会为每条边镜像补一条反向边。另外要注意节点编号必须是从0开始的连续整数,编号断档会导致索引越界。
2.3 从CSV边表构造GNN预测数据集的三个坑
源码包里最常见的失败往往不是模型问题,而是数据进不了模型。第一类坑是节点编号不连续,比如边表里节点编号是字符串“user_123”或从10000开始的稀疏id,直接塞进edge_index必然炸掉。第二类坑是特征尺度不统一,图神经网络的聚合操作本质是加权求和,一个特征取值在[0,1],另一个在[0,10000],聚合结果会被大数值特征主导。第三类坑是特征矩阵没有对齐节点顺序,原始关系表里的行顺序和节点编号顺序不一致,训练时标签就错位了。
处理方式我一般会先做一次完整的特征管线:
from sklearn.preprocessing import StandardScaler # 原始特征矩阵 raw_features: shape [N, feature_dim] scaler = StandardScaler() x = scaler.fit_transform(raw_features) # Z-score归一化 x = torch.tensor(x, dtype=torch.float) # 节点id连续化: 把原始id映射到0..N-1 node2idx = {old_id: i for i, old_id in enumerate(all_node_ids)} edge_src = [node2idx[s] for s in edge_src_raw] edge_dst = [node2idx[d] for d in edge_dst_raw] edge_index = torch.tensor([edge_src, edge_dst], dtype=torch.long)标准化的意义有两层:一是让不同量纲的特征在GCN的加权聚合中处于同一尺度;二是后续接入BatchNorm时,输入尺度过大会让归一化层的统计量漂移。特征对齐则建议在构造node2idx映射时同步完成,不要让特征矩阵的行顺序和编号映射各做一套。
3. GCN与GraphSAGE模型源码:从两层网络到训练循环
3.1 两层GCN的最小可运行代码
用在PyTorch工程里可直接嵌入的类实现两层GCN:
import torch.nn.functional as F from torch_gnn.nn import GCNConv class GCNNet(torch.nn.Module): def __init__(self, in_dim, hidden_dim, out_dim): super().__init__() # 第一层: 输入特征维度 -> 隐藏维度 self.conv1 = GCNConv(in_dim, hidden_dim) # 第二层: 隐藏维度 -> 输出类别数 self.conv2 = GCNConv(hidden_dim, out_dim) def forward(self, x, edge_index): x = self.conv1(x, edge_index) x = F.relu(x) x = F.dropout(x, p=0.2, training=self.training) x = self.conv2(x, edge_index) return xforward里有三个细节需要留意。第一,dropout必须传training=self.training,让它在eval阶段自动关闭;第二,最后一层不接softmax,因为PyTorch的CrossEntropyLoss内部会做log_softmax计算,提前softmax反而破坏数值稳定性;第三,不要在两层之间堆MLP,否则两阶邻居信息会被过深的参数化稀释,在中小规模图上更难训练。out_dim对节点分类就是类别数,对回归预测就是1。
3.2 图太大或邻居不均时换GraphSAGE
GCN在前向时要把整个邻接矩阵参与计算,当图规模超过百万节点,显存压力很大。这时常见的替换方案是GraphSAGE,代码改动极小:
from torch_gnn.nn import SAGEConv class SageNet(torch.nn.Module): def __init__(self, in_dim, hidden_dim, out_dim): super().__init__() self.conv1 = SAGEConv(in_dim, hidden_dim) self.conv2 = SAGEConv(hidden_dim, out_dim) def forward(self, x, edge_index): x = self.conv1(x, edge_index) x = F.relu(x) x = F.dropout(x, p=0.3, training=self.training) x = self.conv2(x, edge_index) return xSAGEConv与GCNConv的核心差异在聚合公式:GCN默认使用对称归一化聚合,节点度越大,单个邻居的贡献被摊薄得越厉害;SAGEConv先对邻居表示做聚合,再与节点自身表示拼接后过线性层,因此在度数差异大的图里通常更稳。替换模型后参数也要跟着调:
| 参数 | GCN推荐起始值 | GraphSAGE推荐起始值 | 说明 |
|---|---|---|---|
| hidden_dim | 64 | 32 | SAGE带拼接,等价信息下参数量更大 |
| 层数 | 2 | 2~3 | 超过3层必须加残差,否则过平滑 |
| dropout | 0.2 | 0.3 | SAGE对噪声更敏感,调高一点更稳 |
| 学习率 | 0.01 | 0.005 | 聚合方式不同,梯度尺度不同 |
如果图大到连edge_index都无法完整载入显存,可以用PyG的NeighborLoader对每批节点做邻居采样,固定采样数像num_neighbors=[10, 10]就表示每层采样10个邻居。GCN和SAGE都能配这个采样器,但SAGE在这种设置下更自然。
3.3 训练循环:约束loss只在掩码位置计算
节点分类预测的训练循环标准写法如下:
model = GCNNet(in_dim=16, hidden_dim=64, out_dim=3) optimizer = torch.optim.Adam(model.parameters(), lr=0.01) criterion = torch.nn.CrossEntropyLoss() for epoch in range(200): model.train() optimizer.zero_grad() out = model(data.x, data.edge_index) # 关键: 只对train_mask选中的节点算loss loss = criterion(out[data.train_mask], data.y[data.train_mask]) loss.backward() optimizer.step() if epoch % 20 == 0: model.eval() with torch.no_grad(): logits = model(data.x, data.edge_index) val_pred = logits[data.val_mask].argmax(dim=1) val_acc = (val_pred == data.y[data.val_mask]).float().mean().item() print(f"Epoch {epoch:03d} | loss {loss.item():.4f} | val_acc {val_acc:.4f}")out[data.train_mask]这一行是整个训练循环的关键。GCN前向天然会把所有节点的表示都算出来,如果不加mask,等于把未标注节点的信息也塞进了梯度,测试结果就有泄漏。eval时包在torch.no_grad()里,是为了不保留计算图,降低推理内存。200个epoch对中小规模图往往偏多,实际建议配早停:连续20个epoch验证集指标不提升就停。另一个常被忽略的点是weight_decay,Adam优化器配合5e-4左右的L2正则,能明显抑制过平滑。
4. 评估与调参:让GNN预测结果在验证集上更可信
4.1 节点分类和链路预测,数据划分方式完全不同
节点分类任务划分的对象是节点索引。随机抽即可,但要注意类别分布与原始数据保持一致,否则少数类可能整个消失在训练集里。这里使用分层划分:
from sklearn.model_selection import train_test_split all_idx = torch.arange(data.num_nodes).numpy() train_idx, temp_idx = train_test_split( all_idx, test_size=0.3, stratify=data.y.numpy(), random_state=42 ) val_idx, test_idx = train_test_split( temp_idx, test_size=0.5, stratify=data.y[temp_idx].numpy(), random_state=42 )链路预测则要在边集合上划分,训练时见过的节点,测试时依然会出现,只是那条边没见过。如果测试边两端的节点都不在训练图里,GNN无法构造有效的邻居聚合,预测结果不可信。实现时通常是先把边随机分成三份,再按“训练集边构建子图”的方式来组装Data对象。
4.2 指标选错了,调参方向就会偏
同一个模型,在类别均衡的节点分类任务上可以看Accuracy;类别不平衡时Accuracy会掩盖问题——全部预测成多数类也能得到很高的分数。此时要看macro-F1或ROC-AUC。链路预测固定用AUC和AP,但ROC-AUC对负样本采样方式很敏感:如果从全图随机采负边,AUC会虚高,更稳的做法是刻意挖一些与正边结构相似的负边。
| 任务类型 | 推荐指标 | 选用理由 |
|---|---|---|
| 节点分类(类别均衡) | Accuracy | 直接反映整体正确率 |
| 节点分类(类别不平衡) | macro-F1 | 对少数类更敏感 |
| 链路预测 | AUC、AP | 反映排序质量,AP更强调Top结果 |
| 图级回归预测 | MAE、RMSE | 连续值场景,RMSE放大离群误差 |
指标决定调参方向:看Accuracy时模型会往多数类偏,看macro-F1时模型会优先照顾少数类的召回。源码包里的默认评估代码往往是Accuracy,换成自己的业务数据前,先确认类别分布。
4.3 训练效果不对时,按三个顺序排查
GNN效果不好时,按频率从高到低排查三个点。第一是输入数据:edge_index有没有重复边、有没有加自环、特征是否归一化、train_mask和val_mask有没有重叠。这三项约占我方一半的调试时间。第二是模型容量:hidden_dim太小,train loss和val loss同时降不下去;hidden_dim过大,train loss持续下降但val loss震荡。第三才动学习率和正则项,多数场景下把lr从0.01降到0.005就能稳定收敛,dropout的调整效果远小于前两项。把参数集中放在一个配置字典里:
config = { "hidden_dim": 64, "num_layers": 2, "dropout": 0.2, "lr": 0.01, "weight_decay": 5e-4, "epochs": 200, "early_stop_patience": 20, }排查时严格按数据、容量、正则的顺序,避免同时动三个变量。
5. 推理阶段实战:新节点预测与模型保存的关键细节
5.1 如何对一个没见过的节点做预测
训练结束后,要对图里新出现的节点预测类别。最常见的错误是“只把新节点的特征喂给模型”。GNN的表示依赖邻居信息,单独喂新节点等于丢掉了全部上下文。正确做法是把新节点拼到原图上,组成增广图再做前向:
model.eval() with torch.no_grad(): # 新节点特征, 注意必须用训练时保存的scaler做同样归一化 new_feat = torch.tensor([0.3, 0.5, 0.2], dtype=torch.float).view(1, -1) data.x = torch.cat([data.x, new_feat], dim=0) # 拼接到特征矩阵末尾 new_node_idx = data.num_nodes - 1 # 新节点至少要有一条边连到旧图, 否则退化成MLP new_edges = torch.tensor([[new_node_idx, 3], [new_node_idx, 7]], dtype=torch.long) data.edge_index = torch.cat([data.edge_index, new_edges.t()], dim=1) logits = model(data.x, data.edge_index) pred = logits[new_node_idx].argmax(dim=0).item() print(f"新节点预测类别: {pred}")新节点必须至少有一条边连接到旧图节点,完全没有邻接信息时,GNN给它的表示只取决于自身特征,效果和MLP无异。所以实际工程里,对新节点没有边的情况,常见做法是先基于特征相似度做KNN找邻居,拼接出边后再进入GNN。
5.2 保存模型时把预处理参数一起存下来
保存GNN模型不要只存state_dict,至少要把特征归一化用的均值和标准差一起存:
torch.save({ "model_state": model.state_dict(), "feature_mean": scaler.mean_, "feature_std": scaler.scale_, "config": config, }, "gnn_ckpt.pth")加载恢复时,先把state_dict灌进模型,再用保存的mean和std对新节点特征做标准化。推理脚本与训练脚本必须共用同一个特征处理函数,把StandardScaler的fit状态原样序列化,这样新节点的数值口径和训练时完全一致,预测结果才可信。
本文还有配套的精品资源,点击获取