节点分类实战:从标签传播到GCN与GraphSAGE的图算法指南

📅 发布时间:2026/9/18 19:45:58
节点分类实战:从标签传播到GCN与GraphSAGE的图算法指南
1. 节点分类到底在解决什么问题1.1 从一个具体场景说起社交网络中的用户画像先说个最常见的场景。你在社交平台上注册了一个新账号平台怎么判断你是个真人、营销号还是机器人更复杂一点怎么判断你对数码、美妆还是游戏更感兴趣传统做法是看你发布的内容、填写的资料但如果你什么都不发、什么都不填呢这时候平台就会看你的“关系”——你关注了谁、谁关注了你、你和谁互动频繁。如果一个人关注了一堆数码测评博主、加入了几个硬件讨论群组、经常给装机帖点赞那这位用户大概率对数码感兴趣。这就是典型的图算法中的节点分类问题给定一张图图中每个节点都有一些属性关注列表、历史行为部分节点已经有明确标签比如一部分用户被人工标注成了“数码爱好者”任务是预测那些没有标签的节点属于哪一类。这个思路能解决的事情远比社交推荐广。在金融风控里已知一批欺诈账户和正常账户通过转账关系图预测新出现的账户是不是欺诈团伙的一员在生物信息里已知一部分蛋白质的功能类别通过蛋白质相互作用网络推测未知蛋白质的功能在学术网络中给定论文的引用关系预测某篇论文属于哪个研究方向。这些任务抽象到数学层面其实是一模一样的图上的半监督分类。1.2 为什么普通的机器学习分类搞不定这类任务你可能会想这不就是一个分类问题吗我把每个节点的特征提取出来喂给XGBoost或者逻辑回归不就行了问题在于很多场景下节点的自身特征非常稀疏甚至基本没有。一个刚注册的用户资料是空的行为是零你拿什么特征去做分类但在这个“特征缺失”的表象之下图结构本身携带了巨大的信息量。引用网络里一篇论文的引用关系已经决定了它的主题归属社交网络里一个账户的邻居构成已经暴露了它的真实身份。传统机器学习模型默认样本之间是独立同分布的它无法利用“样本之间的关系”。而图算法恰恰把这种关系当作核心信息源。我自己做过一个反欺诈项目初期只用用户属性和交易统计特征训练了一个XGBoost模型AUC大概在0.82左右看起来不错但一旦遇到新注册的、没有历史行为数据的账户就完全失效。后来在转账关系图上做节点分类只用图的拓扑结构信息甚至不用任何节点属性新账户冷启动场景下的识别准确率直接提升了十五个百分点以上。这就是图结构信息在发挥作用。还有一个本质区别值得点出来传统分类假设训练集和测试集是独立采样、同分布的但在图节点分类里训练节点和测试节点是相互连接的模型在预测一个节点时看到的图结构里天然包含测试节点的信息。这个特性既是优势——让模型能借力打力也是陷阱——数据划分如果不谨慎会造成严重的信息泄露和虚高的评估分数。后面实操部分我会专门展开讲这一块。1.3 节点分类与其他图学习任务的边界划分图机器学习领域有三大经典任务节点分类Node Classification、链接预测Link Prediction和图分类Graph Classification。很多人一开始容易搞混这里先帮你把边界划清楚。节点分类是给图里的每个节点分配一个类别标签关注的是“点”链接预测是判断两个节点之间是否存在或未来会出现一条边关注的是“边”图分类则是把整张图归类比如判断一个分子结构是否有毒性关注的是“整个图”。三者的模型设计和评估方式差异很大。本文聚焦节点分类但在实际项目里这三类任务经常组合使用比如先用链接预测补全缺失边再用节点分类做用户打标或者反过来用节点分类的结果去增强链路预测的特征。在技术选型上节点分类领域目前有三条主流路线基于标签传播的传统图算法、基于图嵌入的浅层方法以及基于图神经网络的深度学习方法。各有优劣适用场景完全不同。下面这一章我来详细拆解这三条路线的原理和选型依据。2. 核心技术路线从传统图算法到图神经网络2.1 传统派标签传播与结构先验提到节点分类业界最早用的、至今仍在工业界大量部署的其实是标签传播算法Label Propagation Algorithm简称LPA。它的核心思想朴素得像小区里的大爷大妈打听八卦先看看我的邻居们都是什么身份我大概率也是什么身份。具体执行起来非常粗暴初始化时给部分节点打上标签然后每一轮迭代每个未打标签的节点统计它所有邻居的标签分布把自己更新为邻居中出现次数最多的那个标签。重复这个过程直到收敛。整个过程不需要任何特征工程也不需要训练模型纯粹靠图的连接结构做事。LPA的优点极其明显实现简单、计算快、能处理超大图。我在处理千万级节点规模的反欺诈图时用LPA对全图做一遍社区标记只需要几分钟。但它的问题也很致命纯靠结构没有收敛保证迭代过程中标签可能在两个类别之间震荡对标签噪声非常敏感一个节点标错了会像谣言一样沿着边传播出去还有就是它完全无法利用节点自身的属性特征。针对LPA的缺陷工业界有一个折中方案把它改造为带约束的传播——每次迭代时已经置信的种子节点的标签保持不变或给种子节点的更新加上更高权重。这个变体被称为CopRA或半监督LPA在腾讯和阿里的一些风控场景里有实际应用。如果你处理的图规模巨大、节点特征稀疏且质量差但标签部分可信度较高LPA这条路值得先跑一遍作为基线。还有一种经典思路是用图的统计量做特征比如节点度数、聚集系数、PageRank值、K-core分解等然后丢给随机森林或逻辑回归。这条路本质上没有用真正的“图算法”而是把图结构“人工特征化”在一些特征工程搞得相当精细的传统机器学习pipeline里效果也不错。但它依赖领域专家对结构规律的手工提炼对不同类型图的可迁移性比较差。2.2 过渡派图嵌入与Node2VecLPA之后的重大突破是图嵌入Graph Embedding方法的出现。这类方法的思想可以概括为把每个节点映射成一个低维稠密向量使得图中结构相似的节点在向量空间里距离也相近然后把这个向量作为普通特征喂给任意分类器。其中最有代表性的当属DeepWalk和Node2Vec。DeepWalk的思路很有趣。它借鉴了NLP里Word2Vec的思想处理图结构先在图上做随机游走产生一系列节点序列这些序列被视作“句子”节点被视作“词”然后直接套用Word2Vec的Skip-gram模型去学习节点的向量表示。这样做的逻辑是随机游走路径上共现较多的节点在图结构上往往处于相似的角色位置或相近的社区。Node2Vec在DeepWalk基础上引入了一个重要的自由度通过两个参数p和q来控制随机游走是偏向深度优先DFS还是广度优先BFS。偏向BFS时学习到的向量更强调局部结构相似性同类节点在向量空间中更容易聚在一起偏向DFS时向量更强调社区归属适合发现不同社区的边界。这个调节旋钮非常实用我在处理不同业务图时一般会先在验证集上扫描p和q的取值通常能带来三到五个百分点的提升。图嵌入最大的优点是可以完全无监督地学习不需要任何标签数据。你先在一个大规模图上把向量学出来之后下游分类任务想用逻辑回归还是XGBoost都随你。缺点是首先它是在“图结构”这个静态视角上做文章没有把节点的属性特征融入学习过程其次Transductive的属性决定了它无法泛化到训练时没见过的节点每来一个新节点都要重新在全图上跑一遍嵌入这在工业界动态增长的场景里相当难受。它被GNN全面取代是时代必然。2.3 现代主力GCN、GraphSAGE与GAT图神经网络GNN近五年已经成为节点分类事实上的主流方案。它最大的特点是同时利用节点特征和图结构而且能通过多层堆叠实现多跳邻居的信息聚合。打个比方图嵌入像是你去派出所拿了一张全城的地图然后自己研究该去哪GNN则像是向身边每个邻居打听各自知道的消息然后综合判断自己该去哪。最早的经典工作是GCNGraph Convolutional Network。它的核心操作可以概括为聚合邻居信息并更新自己的表示。每一层的计算大致是把自己和邻居的特征向量做加权求和经过一个线性变换再过一个非线性激活函数。GCN有个漂亮的理论性质——它本质上是对图拉普拉斯矩阵做一阶近似谱卷积的简化版本但在工程实践里你完全可以把它当作一种特殊的特征传播和变换机制来理解。GraphSAGE解决了GCN的一个硬伤归纳学习问题。GCN在训练时学到的参数依赖于特定的图结构一张全新的图或新增节点后GCN一般需要重新训练。GraphSAGE则通过学习一个“聚合函数”——比如均值聚合、LSTM聚合、池化聚合——来对新节点生成嵌入。它的工作方式是采样一批邻居用聚合函数把邻居信息汇聚起来更新当前节点表示。这套机制让模型天生具备泛化能力新节点来了直接前向传播就能得到预测结果不需要重新训练。GATGraph Attention Network则引入了注意力机制。GCN对所有邻居一视同仁地加权平均GAT则通过学习一个注意力系数动态决定聚合时应该“更看重谁的声音”。这在噪声大、邻居相关性差异明显的图中特别有用。比如在一个社交网络里你的高中同学和你的偶像对你的兴趣偏好影响权重显然不同GAT里的注意力分数就扮演了这个区分权重的角色。2.4 三大路线怎么选很多读者问过我到底该用哪种方法我直接给一个可操作的决策框架如果你的图规模极大上亿节点、标签极少、不需要泛化到新节点且上线压力大那么LPA或带约束的LPA是你的首选跑得快、部署简单能快速建立基线。如果标签数据很少但图结构质量很高且你的任务是一次性的离线分析不要求泛化到新节点可以试Node2Vec 分类器的组合。如果你的节点有较丰富的属性特征、图规模在千万节点以内、模型需要部署到线上并面临新节点涌现的情况投入产出比最高的一定是GraphSAGE或GAT这类GNN模型。需要特别提醒的是图学习领域没有“银弹”。做节点分类项目的关键不是选最新最炫的模型而是先想清楚你的图数据长什么样、你的标签有多少、你的线上推理约束是什么。我用Node2Vec的时候曾经被新用户冷启动问题坑到脱发也有过在只有两千个带标节点的超大图上硬套GAT差点把显存跑爆的经历。选型比调参重要得多。3. 实操篇用PyTorch Geometric实现GCN节点分类3.1 环境准备与Cora数据集理论讲完了接下来直接跑一个完整的节点分类项目让你对全流程有一个立体的感知。我选择的数据集是Cora——图学习领域的“MNIST”几乎所有图神经网络论文的默认试验田。Cora数据集包含2708篇机器学习领域的论文每篇论文是一个节点每篇论文被一个1433维的0/1词向量表示词典大小是1433论文中出现了某个词对应位置为1节点之间有5429条引用边整图被划分为7个类别如神经网络、强化学习、概率方法等。每篇论文只属于一个类别我们的任务是利用少量已知类别的论文和图结构预测其余论文的类别。环境安装方面我建议使用PyTorch作为底层框架PyTorch Geometric是它的图学习扩展库。安装的关键是版本匹配这一步坑很多# 先安装PyTorch再去官网根据CUDA版本选对应的PyG版本 pip install torch pip install torch_geometric # 正确做法先查看CUDA版本 python -c import torch; print(torch.__version__, torch.version.cuda)我强烈建议你不要用pip install torch_geometric的默认方式直接装因为PyG对PyTorch版本和CUDA版本的匹配要求非常严格。正确姿势是先去PyG官网的安装页面选择你对应的PyTorch版本和CUDA版本复制官网给出的命令安装。这一步做错了安装完导入必然报错而不是运行时报错——体验很难受。数据加载更简单PyG已经内置了Cora数据集的下载和预处理from torch_geometric.datasets import Planetoid dataset Planetoid(root./data/Cora, nameCora) data dataset[0] print(f节点数: {data.num_nodes}) print(f边数: {data.num_edges}) print(f特征维度: {dataset.num_node_features}) print(f类别数: {dataset.num_classes})输出结果节点数: 2708 边数: 10556 特征维度: 1433 类别数: 7注意这里的边数是10556比前面说的5429多了一倍因为PyG默认存的是无向图每条原始引用关系会被对应成两条有向边。Cora原始数据本身是有向的引用网络但GCN的处理方式默认把它当无向图处理聚合信息时两个方向的引用都算。3.2 定义图卷积网络模型建完数据之后定义一个两层的GCN模型。这个结构的参数设置几乎是图神经网络论文里最经典的标配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__() 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, p0.5, trainingself.training) # 第二层图卷积输出每个类别的logits x self.conv2(x, edge_index) return x整体逻辑非常直观。第一层把1433维的原始词向量压缩到16维的隐层表示中间经过ReLU激活和Dropout防止过拟合第二层把16维映射到7维输出对应7个类别的分数。关于隐藏层维度的选择16这个数字并非拍脑袋定的。GCN原论文里使用了这个配置在Cora上效果很好后续大量论文沿用。实际项目中隐藏维度一般在16到256之间调原则是隐藏维度太小模型表达力不足欠拟合隐藏维度太大小数据集上很容易过拟合而且训练速度明显变慢。我知道很多读者会问“为什么就不能多堆几层GCN把效果做得更好”这是一个非常关键的问题。GCN堆到多层之后每个节点的感受野指数级扩大最终所有节点的表示会趋向一致这种现象叫过平滑Over-smoothing。在Cora这种规模的数据集上2层GCN的效果反而比3层、4层更好这是我在实验里反复验证过的结论。后面“常见问题”部分我还会详细展开。3.3 训练与评估全流程模型的训练代码和普通PyTorch分类任务非常相似唯一的区别是数据读取的方式变成了图结构索引。PyG内置了标准的数据划分每类固定拿出20个节点作为训练集500个节点作为验证集1000个节点作为测试集。具体训练过程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(), lr0.01, weight_decay5e-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() torch.no_grad() def test(): model.eval() out model(data.x, data.edge_index) pred out.argmax(dim1) accs [] for mask in [data.train_mask, data.val_mask, data.test_mask]: correct (pred[mask] data.y[mask]).sum().item() accs.append(correct / mask.sum().item()) return accs for epoch in range(200): loss train() train_acc, val_acc, test_acc test() if epoch % 20 0: print(fEpoch {epoch:3d} | Loss: {loss:.4f} | Train: {train_acc:.4f} | Val: {val_acc:.4f} | Test: {test_acc:.4f})这里要特别解释一个细节为什么训练时传给损失函数的只有训练节点的预测结果out[data.train_mask]但模型前向传播时用的是整张图因为GCN的卷积操作天然需要每个节点的邻居信息即使节点不属于训练集它的特征也会作为其它训练节点的邻居信息参与聚合。这个过程在训练时是必要的但它也埋下了一个隐患——如果不加控制地让所有节点的信息都参与传播模型在预测测试集时其实已经间接“看”过测试集的特征了。Cora的标准设置里训练阶段确实允许全图特征参与传播这是这个特定数据集的benchmark惯例。但在真实项目里你需要仔细设计信息流的边界这个问题我会在下一章展开。实际跑下来的训练过程大概是这样的初始loss在1.9左右前50轮快速下降测试准确率稳步上升100轮之后训练集准确率接近100%但测试集准确率停留在78%-82%之间不再明显上涨说明模型已经收敛。如果一切正常你的最终测试准确率应该在0.81左右这和GCN论文报告的水平是吻合的。3.4 核心参数与代码细节解读我建议你第一次跑通代码之后不要急着换数据集先把几个核心参数的敏感度都摸一遍这对建立对图模型的直觉非常有帮助。学习率是最先说参数。上面的代码用的是0.01这几乎是GCN在Cora上的默认最优值。如果你改成0.1训练loss会剧烈震荡模型很难收敛改成0.001训练速度会肉眼可见地变慢200轮不够得跑到1000轮以上才能达到相同水平。原因在于GCN的梯度传播路径比普通神经网络更深——梯度需要沿着图结构在多层之间回传学习率太大容易梯度爆炸。Dropout比例也很值得调。经典配置是0.5我试着改成0.1之后测试准确率掉到了0.75左右改成0.8准确率掉到了0.78左右。原因是Cora的训练节点太少只有140个节点有标签模型非常容易过拟合Dropout是主要的正则化手段。实践中如果你的带标节点多一些Dropout可以适当调低。再说一个新手经常忽略的点边的方向。前面提到PyG默认把Cora存成无向图边数翻倍。做真实项目时如果你的图是有向的比如转账关系、关注关系一定要思考清楚卷积时要不要保留边的方向。以漏斗欺诈检测为例欺诈者把钱转给多个分散账户如果只按“发出方向”聚合模型学到的是资金流出模式如果无向化处理模型学到的是双向关系模式。这两种模式的语义差异很大在不同业务里效果完全不同。PyG的GCNConv默认支持有向边的聚合是否需要无向化完全取决于业务语义。权重初始化方面PyG的GCNConv默认使用PyTorch的均匀分布初始化在Cora上工作良好。如果你自定义了更深的GNN结构建议显式换用Xavier或Kaiming初始化否则深层结构很容易出现梯度消失。4. 常见问题与排查技巧实录4.1 过平滑问题为什么GCN不能堆太深前面提到过GCN层数过多会出现过平滑。这里把这个现象讲透GCN每一层都会做一次邻居特征聚合一层GCN让每个节点看到它的一跳邻居两层看到二跳邻居如此类推。当层数不断加深每个节点接收的信息范围越来越大最终全图节点的表示趋于同一个方向彼此无法区分。举例来说Cora数据上我实际跑过不同层数的GCN层数测试准确率观察到的现象1层0.689表达能力不足欠拟合2层0.810效果最优经典配置3层0.766开始出现过平滑4层0.582过平滑严重接近随机水平这个结果在Cora这种规模的图上非常有代表性。那到底该不该堆深如果任务本身确实需要多跳信息可以考虑两个变通方案一是用残差连接把上一层的表示加到当前层输出上缓解信息丢失二是用JK-NetJumping Knowledge Network的设计把每一层的表示拼接或池化起来让模型自己决定各层信息的权重。这些技巧在真实项目中救过我很多次。4.2 数据划分不合理导致准确率虚高这是一个比算法本身更容易踩的坑。Cora数据集自带的标准划分是按类别随机抽取的每个类的训练节点均匀分布在整个图里。因为训练节点和测试节点在同一个连通分量里彼此之间通过边直接相连GCN的信息聚合会把测试节点的特征导入到模型训练过程中。严格来说这种“Transductive设置”下的准确率是被高估的。真实业务里你面对的往往是Inductive场景模型在上线时看到的节点是全新的它们在训练时的图里根本没出现过。如果你在离线评估时不做区分地随机划分上线的效果很可能大幅缩水。我在一个社交用户分类项目里就吃过这个亏——离线用随机划分测出F1有0.87上线一周后实际效果只有0.73差距巨大。正确的做法是如果线上推理会面临全新节点离线评估一定要模拟这个场景。具体做法是按子图划分训练集和测试集比如在图中随机选一部分社区作为测试集测试节点在训练阶段完全不参与任何信息传播。也就是说前向传播时要把测试节点从图中摘出去不能让它们的特征“泄漏”到邻接矩阵里。PyG里可以通过torch_geometric.transforms.RandomNodeSplit设置不同的划分模式或者自己手动构建子图模拟真实线上冷启动场景。4.3 类别不平衡的应对策略节点分类面临的另一个常见问题是类别极度不平衡。比如在一个电商风控图里正常用户可能有几千万恶意用户只有几万正负比例悬殊到几百比一。这种场景下你直接训练一个分类器模型会把所有节点都预测为多数类准确率依然很高但毫无业务价值。应对策略可以从数据、模型、损失函数三个层面同时下手。数据层面最常见的操作是对多数类节点进行负采样比如把正常用户的比例压到恶意用户的3到5倍降低训练时的类别偏向模型层面可以用GraphSAGE这种支持邻居采样的模型在聚合时对多数类邻居做随机采样本质上也是一种廉价的数据增强损失函数层面给少数类样本分配更高的交叉熵权重或者在训练时用Focal Loss让模型把注意力集中在难分样本上。有一件事要特别提醒既然是图数据不能只看节点层面的类别比例还要看边层面的类别连接偏好。比如恶意账户之间往往存在“抱团”现象——它们之间有大量互相关联而正常账户之间的连接相对稀疏。这种边层面的不平衡是图数据特有的处理方式也更多样比如可以对“异类边”连接不同类别节点的边做特殊加权让传播的信息更偏向类别分布。4.4 训练不收敛与显存不足的实战排查最后聊几个训练过程中最常见的工程问题。训练不收敛的现象常常表现为loss下降得很慢、甚至反复震荡准确率始终在随机水平附近挣扎。排查顺序我建议按照“数据 → 模型 → 超参数 → 实现”四个维度来先检查输入数据有没有问题——邻接矩阵是否包含了自环特征是否做了标准化标签是否从0开始连续编码我遇到过一个诡异的现象loss一直降不下去最后发现标签编码是从1开始的模型输出是7维实际标签却有“8”这个取值交叉熵直接就报错了。模型层面检查是否有梯度消失或梯度爆炸。可以打印每一层参数的梯度范数如果梯度接近0或者大到几百问题就很明显。GCN里一个典型的梯度爆炸原因是图里某个中心节点的度数特别大导致消息传递时聚合值非常大。解决办法是使用带归一化的卷积层或者对特征做标准化。超参数层面学习率过大是常见的原因尤其是GCN这种聚合操作多的模型。我个人习惯的做法是先用0.001跑一遍看loss曲线是否平滑下降如果loss震荡就调低学习率如果loss下降极其缓慢才往上调。在此基础上再用学习率衰减策略会比一直用固定学习率稳定得多。显存不足的问题主要发生在超大图上。整图卷积需要把全图的特征矩阵、邻接矩阵、中间激活值全部放进显存图规模大了之后对显存是巨大挑战。解决方案主要有两条路一是用GraphSAGE的邻居采样机制每个batch只采样固定数量的邻居训练时不再需要整个图显存占用大幅下降二是用Cluster-GCN的思路把图划分成多个子图簇每个batch只在一个簇上计算这样显存占用与图的规模解耦理论上可以处理几亿级别的图。我处理过一个差不多两千万节点的图用了GraphSAGE的邻居采样模式显存占用控制在8G以内效果和全图GCN差别不大。最后分享一个非常实用的排查工具torch_geometric.explain模块。当你怀疑模型学到了错误的模式时可以用它来可视化和解释每个节点预测时邻居的贡献度。我见过很多次模型预测一个节点为某类原因居然是通过一条长距离的间接连接和真正有业务逻辑的邻居无关。这种情况通常说明你的图上存在拓扑噪声或者聚合层数过多了。把解释结果拿出来看一眼往往比盲调参数更快定位问题。我自己做图算法项目的体会是节点分类的技术入门不算难难的是对图数据特性的深刻理解和对每一个细节的把控。从选模型到划分数据从调参数到排查问题每一步都决定了线上效果的最终走向。希望这篇内容能帮你少走一些弯路。如果后面有条件我还可以基于同样的框架展开讲讲链接预测和图分类的实战经验那些坑又是完全不一样的风景。