
图神经网络系列做到第 10 期今天把**归纳学习Inductive Learning**这块一次性讲透。前面几期如果一直在玩节点分类大概率用的都是转导学习Transductive Learning的套路——训练时整张图都在眼前测试节点也是训练阶段见过的。但真实业务里根本不是这么回事新用户不断注册新交易持续产生新论文每天都在入库。如果模型只能处理固定的一张图上线之后第一次遇到新节点就会失效。所以这次的内容重点非常明确让 GNN 学会“没见过也能推理”也就是归纳学习。核心模型选择是 GraphSAGE 和 Graph Attention NetworkGAT配合 PyTorch Geometric 进行节点分类实战实测从数据切分到模型训练、评估、新节点推理的完整链路。这篇实战文会先解释归纳学习和直推学习的根本差异再提供一套可以照着跑的本地部署和实验流程包括环境准备、数据集选择、模型结构对比、训练代码和效果验证方法。本身这套流程上手成本很低单张消费级显卡甚至纯 CPU 都能完成小规模实验关键是搞清楚归纳泛化是怎么发生的以及为什么 GraphSAGE 这类采样聚合模型天生适合动态图场景。1. 核心能力速览能力项说明学习范式归纳学习Inductive Learning支持未见节点与未见图推理对比对象直推学习Transductive Learning如 GCN 的标准节点分类核心模型GraphSAGE、Graph Attention NetworkGAT主要任务节点分类、图分类、链路预测中的增量推理典型数据集Cora、Citeseer、Pubmed、PPI蛋白质网络、OGB 系列硬件要求小规模数据集 CPU 可跑大规模图推荐 6G 以上显存部署方式Python 环境 PyTorch PyGPyTorch Geometric命令启动接口能力模型训练后可直接对新节点推理可封装为本地推理服务批量任务支持批量节点推理、多数据集批量实验适合场景动态社交网络、风控、推荐系统、论文分类、知识图谱增量更新从材料来看这套方案没有复杂的 WebUI也不是一键启动包本质是一个实验框架。和之前第 9 期如果做的是直推学习相比核心变化在于数据切分、邻居采样方式和模型聚合逻辑。2. 归纳学习与直推学习的本质区别2.1 直推学习训练时已经见过所有节点直推学习是 GCN 早期最常见的范式。训练时模型使用整张图的邻接矩阵和特征矩阵包括测试节点的特征和它们在图中的连接关系。问题是模型在训练阶段就已经偷看过测试节点的结构位置。当一个新的节点在推理阶段出现时它不在原来的邻接矩阵里模型压根不知道怎么给它计算表示。用公式化的语言描述直推学习的目标是给定整张图 G (V, E)训练节点集合 V_train 目标预测 V_test 的标签其中 V_test ⊆ V也就是说测试节点在训练时物理上存在于图中只是标签被遮住。这在固定图结构的研究设定下没问题但不适合真实系统。2.2 归纳学习训练时只见过子图推理时面向全新节点归纳学习的目标完全不同训练时只使用训练节点及其邻域子图 G_train 推理时模型要能泛化到未见过的节点 v_new以及可能是全新的图 G_new换句话说模型学习的不再是“某个节点在固定图中的位置”而是一个从局部邻居结构映射到节点表示的聚合函数。只要给定新节点的特征和它的邻居特征模型就能给它算出一个嵌入向量。这就是 GraphSAGE 论文里强调的核心思想学习聚合器Aggregator而不是学习固定的嵌入表Embedding Table。2.3 为什么 GraphSAGE 能实现归纳学习GraphSAGEGraph Sample and Aggregation的核心机制是采样 聚合对中心节点的邻居进行固定数量采样比如一阶采样 25 个二阶采样 10 个用聚合函数Mean、LSTM、Pooling把邻居信息聚合起来拼接中心节点自身特征和聚合结果经过全连接层和激活函数得到节点表示因为每一步都只依赖局部邻居特征不依赖全局邻接矩阵所以理论上可以把这套聚合逻辑用到任何新节点上。GAT 的归纳能力来自注意力机制它对邻居计算注意力权重不同邻居对中心节点的影响不同这种权重计算只依赖节点特征同样对未见节点有效。这直接带来工程上的优势线上模型可以持续服务新数据不需要每次有增量数据就重新训练整张图。3. 适用场景与使用边界3.1 适合什么场景动态社交网络新用户不断加入需要判断新用户的风险等级或兴趣偏好金融风控新的交易节点出现需要实时判断是否为欺诈行为推荐系统商品或用户不断更新需要快速生成新节点的 Embedding学术图谱新论文入库需要自动分类到研究领域蛋白质功能预测新测序的蛋白质需要预测功能标签图结构本身也是动态变化的3.2 不适合什么场景固定图结构的纯离线分析如果图的节点集合永远不会变化而且只需要做一次预测用直推学习如标准 GCN在准确率上通常更有优势因为它能利用全局结构信息超大规模图的实时在线推理如果单次推理需要采样的邻居数量很大比如三跳采样延迟可能达不到线上实时要求需要工程优化极端冷启动新节点完全没有特征也没有任何邻居连接这种情况下任何基于特征和结构的 GNN 都无法有效推理3.3 使用边界与合规问题如果涉及到真实用户数据社交关系、交易记录、行为日志必须注意合法授权用于训练的图数据必须符合数据来源方的使用协议隐私保护节点特征和标签可能包含敏感信息建议做脱敏处理公平性图模型容易放大某些群体的偏差发布前需要对不同子群的预测效果做评估测试环境验证先在隔离环境验证推理效果再考虑接入生产4. 环境准备与前置条件4.1 硬件和操作系统以本地开发机为例操作系统Windows 10/11、Ubuntu 18.04、macOS 均可GPU可选。Cora 这种小数据集纯 CPU 就能跑PPI 数据集单卡 8G 显存可以覆盖大多数配置如果上 OGB-Proteins 或更大规模的图建议 12G 以上显存内存16G 以上磁盘空间预留 10G 以上主要是 PyTorch、CUDA 依赖和数据集缓存4.2 软件依赖清单需要安装以下环境Python 3.8 - 3.11 PyTorch 2.0 PyTorch Geometric 2.3 numpy scikit-learnPyG 的安装经常容易出现版本匹配问题建议按照官方网站的安装命令来选择对应 CUDA 版本。例如 CUDA 11.8 的安装命令是pip install torch2.0.1 torchvision0.15.2 torchaudio2.0.1 --index-url https://download.pytorch.org/whl/cu118 pip install pyg-lib torch-scatter torch-sparse torch-cluster torch-spline-conv -f https://data.pyg.org/whl/torch-2.0.0cu118.html pip install torch-geometric如果不用 GPU可以把 PyTorch 替换成 CPU 版本训练速度会变慢但流程能跑通。4.3 数据集准备本文重点使用两个数据集数据集节点数边数类别数特点Cora270854297经典引文网络小规模适合快速验证PPI56944818716121多标签蛋白质交互网络官方划分多个图天然适合归纳学习Cora 的默认划分是直推学习标准的固定划分做归纳学习时需要自己重新划分。PPI 数据集有官方 train/val/test 图划分训练图和测试图在节点维度上完全不重叠是真正考验归纳能力的基准。建议两个都跑一遍。5. 模型设计与核心代码实现5.1 数据加载与归纳划分以 Cora 为例标准 PyG 加载方式是from torch_geometric.datasets import Planetoid dataset Planetoid(root./data, nameCora) data dataset[0]但注意这里默认的data.train_mask / val_mask / test_mask是给直推学习使用的。做归纳学习需要重新划分训练集节点和测试集节点在图中不共享邻居信息。一个相对简单但有效的划分策略是按节点 ID 随机切分并在邻居采样时对测试节点屏蔽训练集信息。更严谨的做法是直接使用 PPI 数据集from torch_geometric.datasets import PPI train_dataset PPI(root./data/PPI, splittrain) val_dataset PPI(root./data/PPI, splitval) test_dataset PPI(root./data/PPI, splittest)PPI 的训练集包含 20 张图测试集包含 2 张图图之间没有节点重叠。这符合真正归纳学习的定义训练时从未见过测试图的结构和节点特征。5.2 邻居采样器如果直接在完整大图上做 batch 训练内存和显存都会吃不消。PyG 提供了NeighborSampler来做小批量采样。它的工作方式随机选择一批中心节点对每个中心节点采样固定数量的邻居递归向外扩展采样更高阶邻居只把采样到的子图送入 GPU示例配置from torch_geometric.loader import NeighborLoader train_loader NeighborLoader( data, num_neighbors[10, 5], batch_size128, input_nodesdata.train_mask, shuffleTrue, )参数含义num_neighbors[10, 5]第一阶采样 10 个邻居第二阶采样 5 个邻居batch_size128每批次 128 个中心节点input_nodesdata.train_mask只从训练节点中采样中心节点把测试节点也各自做同样的采样就能在训练完成后对它们单独推理。5.3 GraphSAGE 模型实现GraphSAGE 在 PyG 里有现成实现也可以通过SAGEConv自己搭。推荐自己搭一层典型的 GraphSAGE 结构方便理解关键参数import torch import torch.nn.functional as F from torch_geometric.nn import SAGEConv class GraphSAGE(torch.nn.Module): def __init__(self, in_channels, hidden_channels, out_channels, num_layers2): super().__init__() self.convs torch.nn.ModuleList() self.convs.append(SAGEConv(in_channels, hidden_channels)) for _ in range(num_layers - 2): self.convs.append(SAGEConv(hidden_channels, hidden_channels)) self.convs.append(SAGEConv(hidden_channels, out_channels)) def forward(self, x, edge_index): for i, conv in enumerate(self.convs): x conv(x, edge_index) if i ! len(self.convs) - 1: x F.relu(x) x F.dropout(x, p0.2, trainingself.training) return x关键点SAGEConv内部实现的是 MeanAggregation对邻居特征求平均再与中心节点特征拼接过一次线性变换2 层模型即可聚合 2 阶邻居信息输出维度等于类别数配合交叉熵损失使用5.4 GAT 模型实现GAT 的核心是注意力聚合。PyG 里用GATConv来搭。from torch_geometric.nn import GATConv class GAT(torch.nn.Module): def __init__(self, in_channels, hidden_channels, out_channels, num_heads4, num_layers2): super().__init__() self.convs torch.nn.ModuleList() self.convs.append( GATConv(in_channels, hidden_channels, headsnum_heads, concatTrue) ) self.convs.append( GATConv(hidden_channels * num_heads, out_channels, heads1, concatFalse) ) def forward(self, x, edge_index): x self.convs[0](x, edge_index) x F.elu(x) x F.dropout(x, p0.2, trainingself.training) x self.convs[1](x, edge_index) return xGAT 与 GraphSAGE 的对比维度GraphSAGEGAT聚合方式固定加权平均/LSTM/Pooling注意力加权动态计算邻居权重表达力较强依赖聚合器设计更强适合邻居重要性不均的场景计算开销小较大归纳泛化强强适用场景大规模图、朴素聚合够用需要精确捕捉重要邻居5.5 训练脚本训练的核心逻辑如下import torch.nn.functional as F device torch.device(cuda if torch.cuda.is_available() else cpu) model GraphSAGE( in_channelsdataset.num_features, hidden_channels128, out_channelsdataset.num_classes, num_layers2, ).to(device) optimizer torch.optim.Adam(model.parameters(), lr0.01, weight_decay5e-4) def train(): model.train() total_loss 0 for batch in train_loader: batch batch.to(device) optimizer.zero_grad() out model(batch.x, batch.edge_index) loss F.cross_entropy(out[batch.train_mask], batch.y[batch.train_mask]) # 注意当 train_loader 中 input_nodes 限定为 train_mask 时 # 也可以直接用 out out[:batch_size] 与对应标签比较 loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(train_loader)需要注意如果用NeighborLoader它会自动把原始图上的全局train_mask映射到采样后的子图节点索引上。如果索引对应关系不好处理最简单的办法是在加载器里用input_nodes限定中心节点然后在损失计算时把输出截断到中心节点部分。5.6 评估方法评估时对测试节点执行同样的邻居采样流程计算准确率torch.no_grad() def test(loader): model.eval() correct 0 total 0 for batch in loader: batch batch.to(device) out model(batch.x, batch.edge_index) # 只取中心节点的预测结果 pred out[:batch.batch_size].argmax(dim-1) label batch.y[:batch.batch_size] correct (pred label).sum().item() total batch.batch_size return correct / total对 PPI 这种多标签任务把CrossEntropyLoss换成BCEWithLogitsLoss评估指标换成 micro-F1。6. 功能测试与效果验证6.1 测试目标验证模型能在训练子图上收敛验证模型能对完全未见过的新节点正确分类对比 GraphSAGE 和 GAT 在归纳任务上的效果差异观察不同邻居采样数量对效果的影响6.2 Cora 归纳学习测试步骤Cora 的标准做法是为了直推学习设计的。做归纳学习实验时按下面的流程操作import torch from torch_geometric.datasets import Planetoid from torch_geometric.loader import NeighborLoader dataset Planetoid(root./data, nameCora) data dataset[0] # 随机划分 train/val/test num_nodes data.num_nodes indices torch.randperm(num_nodes) train_len int(num_nodes * 0.6) val_len int(num_nodes * 0.2) train_idx indices[:train_len] val_idx indices[train_len:train_len val_len] test_idx indices[train_len val_len:] train_mask torch.zeros(num_nodes, dtypetorch.bool) val_mask torch.zeros(num_nodes, dtypetorch.bool) test_mask torch.zeros(num_nodes, dtypetorch.bool) train_mask[train_idx] True val_mask[val_idx] True test_mask[test_idx] True然后构造三个独立的采样器train_loader NeighborLoader( data, num_neighbors[10, 5], batch_size128, input_nodestrain_mask, shuffleTrue, ) val_loader NeighborLoader( data, num_neighbors[10, 5], batch_size128, input_nodesval_mask, shuffleFalse, ) test_loader NeighborLoader( data, num_neighbors[10, 5], batch_size128, input_nodestest_mask, shuffleFalse, )这里有个容易踩的坑验证集和测试集的中心节点虽然不在训练集中但它们仍然可能通过图结构连接训练节点。这会导致测试结果偏高不完全符合严格归纳学习的要求。如果想要更严格需要在采样时额外用edge_index的 mask 把训练集和测试集之间的边屏蔽掉。一个可行的做法是为每个数据集生成“子图”只保留训练集节点之间的边作为训练图测试集节点之间的边作为测试图。这在 PPI 中是天然成立的在 Cora 上需要手动处理。6.3 PPI 多图归纳测试PPI 是真正测试归纳学习能力的标准数据集from torch_geometric.datasets import PPI from torch_geometric.loader import DataLoader train_dataset PPI(root./data/PPI, splittrain) val_dataset PPI(root./data/PPI, splitval) test_dataset PPI(root./data/PPI, splittest) train_loader DataLoader(train_dataset, batch_size2, shuffleTrue) val_loader DataLoader(val_dataset, batch_size2, shuffleFalse) test_loader DataLoader(test_dataset, batch_size2, shuffleFalse)这里 PPTI 的每个节点都有 121 个二分类标签所以模型输出维度是121而不是num_classes。由于每张图之间互相独立放入同一个 batch 时会产生一个更大的图但训练图的节点不会泄漏到测试图。这是标准的归纳学习训练流程。6.4 预期效果与判断标准运行同样配置的训练循环后Cora 归纳学习准确率通常在 70% - 80% 之间直推学习 GCN 一般能到 80%归纳学习因为信息受限略低是正常的PPI 上的 micro-F1 在 GraphSAGE 2 层、隐藏层 256 维、50 轮训练后通常可以达到 0.75 以上如果你的效果明显低于这个范围优先检查数据划分、采样器配置和梯度是否收敛不用急着怀疑模型结构。6.5 失败排查方向现象可能原因处理方式训练 loss 不下降学习率过大/过小、特征未归一化调低学习率到 0.001检查输入特征分布验证准确率远低于训练邻居采样数过少邻居信息不够增大num_neighbors到[25, 10]推理时报节点索引越界采样器生成的 edge_index 与原始图不匹配检查是否将不同的子图数据错误混合PPI 效果特别差损失函数用了交叉熵而不是 BCE换成BCEWithLogitsLoss评估改用 F1GPU 显存不足batch_size 或邻居采样数过大减小 batch_size 或换成 CPU 训练7. 资源占用与性能观察7.1 显存占用观察方法训练过程中观察显存可以用nvidia-smiwatch -n 1 nvidia-smi在 Python 里也可以记录当前显存占用import torch def print_memory_usage(): if torch.cuda.is_available(): allocated torch.cuda.memory_allocated() / 1024**2 reserved torch.cuda.memory_reserved() / 1024**2 print(fAllocated: {allocated:.2f} MB, Reserved: {reserved:.2f} MB)Cora 这种小数据集显存占用通常不到 500MB即使是普通笔记本 GPU 也能跑。PPI 如果 batch_size2、隐藏层 256 维显存占用在 2GB 到 4GB 之间具体取决于邻居采样数量和批量大小。7.2 影响性能的关键因素邻居采样数量num_neighbors越大每个 batch 的子图越大显存占用和计算时间呈线性增长模型层数2 层到 3 层是性价比最高的区间超过 4 层容易出现过平滑oversmoothing隐藏层维度128 和 256 是常用选择维度翻倍显存占用也会增加batch_size增大 batch_size 提升 GPU 利用率但显存占用也会上升数据集规模边数量比节点数量对显存影响更大因为消息传递的计算量主要集中在边上7.3 显存优化手段降低邻居采样数量减小 batch_size使用混合精度训练torch.cuda.amp采样后把子图数据留在 CPU 上只把当前 batch 传到 GPU对超大图使用 GraphSAINT 或 Cluster-GCN 这类专门设计的采样策略7.4 进程残留问题训练结束后如果直接关闭终端Python 进程可能没有完全释放显存。建议# Linux 下查找残留训练进程 ps aux | grep python # 确认后按 PID 结束进程 kill -9 PIDWindows 下打开任务管理器结束对应的 Python 进程即可。8. 接口 API 与批量任务扩展8.1 模型导出与推理接口训练好的模型可以保存权重后续加载做推理# 保存模型 torch.save(model.state_dict(), graphsage_inductive.pth) # 推理时加载 model GraphSAGE(in_channels, hidden_channels, out_channels, num_layers2) model.load_state_dict(torch.load(graphsage_inductive.pth)) model.eval()8.2 简单推理服务示例如果要把模型封装成 HTTP 接口给内部系统调用可以用 FastAPI 写一个最小服务from fastapi import FastAPI, Request import torch import numpy as np app FastAPI() model GraphSAGE(...).to(device) model.load_state_dict(torch.load(graphsage_inductive.pth)) model.eval() app.post(/predict) async def predict(request: Request): payload await request.json() # payload 中包含 node_features 和 edge_index x torch.tensor(payload[node_features], dtypetorch.float32).to(device) edge_index torch.tensor(payload[edge_index], dtypetorch.long).to(device) with torch.no_grad(): out model(x, edge_index) pred out.argmax(dim-1).cpu().numpy().tolist() return {predictions: pred}这个例子只是提供一个服务化方向生产环境还要考虑鉴权、请求限流、特征校验和日志记录。本地调试验证阶段重点是先把接口跑通确认请求参数和返回格式。8.3 批量任务设计归纳学习模型在实际落地时经常需要对大量新节点做批量推理。推荐目录结构inputs/ batch_001.json batch_002.json outputs/ batch_001_pred.json batch_002_pred.json logs/ run_001.log批量脚本思路读取输入目录下的每个 JSON 文件对每个文件中的节点及其邻居特征执行推理结果写入输出目录记录每个 batch 的处理时间和失败原因失败任务单独重试不阻塞整个队列8.4 与真实生产系统的接入注意生产环境里的“新节点”往往不是独立出现的它会带着新的边进来。比如风控场景里一个新交易节点至少关联一个用户节点。推理前需要把新节点和已有子图拼接再做邻居采样。如果直接对孤立节点推理没有邻居信息模型效果会显著下降——因为 GNN 的核心就是邻居信息传递。这也是从实验到落地最容易忽略的点。9. 归纳学习模型的常见问题与排查方法9.1 模型在训练集上效果好测试集上效果差这是典型的过拟合。图模型中常见的原因是训练集和测试集节点在网络中高度关联而模型依赖了“作弊”信息。排查方法检查测试节点的邻居中是否包含训练节点如果包含尝试在采样阶段切断跨划分的边加入 Dropout0.2 - 0.5增强泛化降低隐藏层维度或模型层数9.2 新节点没有任何邻居推理结果极不稳定孤立节点没有邻域信息GNN 退化成对中心节点自身的特征变换。处理办法尽量把特征工程做足让孤立节点本身有可区分性尝试用虚拟边的策略把孤立节点与相似节点连接如果新节点出现时能够带上一批邻居节点一起入库推荐校验时优先使用这种批量增量方式9.3 采样器开销过大训练速度太慢如果num_neighbors设置过大、batch_size 过大每个 batch 的采样时间会超过 GPU 计算时间。解决方案降低采样阶数和邻居数量使用torch_geometric.loader.NeighborLoader的num_workers参数使用 Cluster-GCN 这类图划分方案替代逐节点采样9.4 显存不足优先尝试降低 batch_size减少隐藏层维度混合精度训练使用 CPU 训练小数据集通常没问题采样器加filter_per_workerTrue减少数据重复加载9.5 结果不可复现设置随机种子import random import numpy as np import torch def set_seed(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)注意在 GPU 上训练某些算子本身存在非确定性即使设置随机种子结果也可能有微小差异这属于正常现象。10. 最佳实践与使用建议10.1 从「最小的可运行实验」开始最开始不要上大规模数据集。先跑通 Cora 或 PPI 的完整流程确认模型结构、损失计算、采样逻辑没有问题再换更大规模的数据集。推荐首次实验配置数据集Cora模型GraphSAGE2 层隐藏层32邻居采样[5, 2]batch_size64学习率0.01训练轮数100这个配置在 CPU 上几分钟内就能完成一轮完整训练。10.2 归一化特征很重要图数据集里的节点特征分布差异很大Pubmed 的特征稀疏性非常强。建议对所有特征做标准化from sklearn.preprocessing import StandardScaler scaler StandardScaler() x_np data.x.numpy() data.x torch.tensor(scaler.fit_transform(x_np), dtypetorch.float32)这一步在很多实验里能稳定提升测试集准确率。10.3 训练日志要记录关键指标每次实验记录以下内容数据划分方式模型结构邻居采样数量学习率和优化器设置每个 epoch 的训练 loss 和验证集指标最终测试集结果方便后续复现和调参。10.4 每次只改一个变量GNN 调参调试容易同时改多个超参数导致无法判断是哪个改动影响了结果。推荐每次只修改一个维度要么调模型层数要么调邻居采样数量不要同时改两个。10.5 关注类不平衡问题Cora 各节点类别的数量相对均衡但真实业务里的图数据集往往严重不平衡。这时候单独看准确率没有意义需要记录每个类别的 Precision、Recall 和 F1。在 PyTorch 里可以通过sklearn.metrics.classification_report实现。11. 总结与下一步归纳学习解决的是 GNN 从“只会处理固定图”到“能给新节点做推理”的关键一步。核心手段是让模型学会局部邻居聚合函数而不是记住全局图的节点位置。工程上的直接收益是新增节点不需要重训模型只需要把新节点及其邻居特征喂给训练好的模型就能得到表示。这次实战的完整链路数据加载与划分 - 邻居采样器 - GraphSAGE / GAT 模型定义 - 训练与评估 - 增量节点推理。建议最先在自己熟悉的数据集上跑通 GraphSAGE因为它的聚合逻辑最简单最容易排查问题。跑通之后再换 GAT 看看注意力机制是否对当前任务有明显提升。最容易踩的三个坑测试节点仍然和训练节点共享边导致指标虚高没有真正体现归纳能力采样器的节点索引映射没处理好训练时 loss 计算到错误位置上PPI 类多标签任务误用交叉熵损失导致模型永远不收敛后续扩展方向可以考虑GraphSAINT 和 Cluster-GCN 用于更大规模图GraphSAGE 的 LSTM 聚合器用于序列邻居建模归纳式链路预测把边预测和节点分类联合训练。落地到生产环境前务必验证新节点通常的邻居数量和特征分布不要假设线上数据和训练数据来自同一个分布。建议把这篇归纳学习实战教程保存下来遇到配置问题回来对照排查表处理。# 快速验证环境好坏的完整命令示例 python -c import torch; print(torch.__version__) python -c import torch_geometric; print(torch_geometric.__version__)