图算法节点分类实战:从标签传播到GNN全解析
去年接手一个反欺诈场景数据是一张几百万节点的交易关系图每个节点是一个账户少数账户有是否涉诈的标签其余大部分是“沉默账户”。要快速评估这些账户的风险这是非常典型的图算法节点分类Node Classification问题利用图的结构关系和节点自身属性给没有标签的节点打标。节点分类几乎可以称作图算法里最实用、最经典的一类任务也是图神经网络、图表示学习这些方向的天然落地点。这篇文章我想把节点分类从问题定义到实战踩坑完整拆一遍覆盖三代建模路线、标签传播这类强基线以及工程里最常被忽略的数据划分和评估细节希望给你一套能直接拿去用的方法论。1. 节点分类到底在解决什么问题三种图场景的对比解读1.1 一个反直觉的场景朋友的朋友正在“替你”做决定先讲社交网络。你在朋友圈里连续出现了两个陌生人一个和你的朋友互关另一个和你的朋友没有任何交集。直觉告诉你前者你更有可能认识也更值得被推荐给你。如果把“认识”当成标签把人和人之间的关注关系看成边这个问题就可以被建模成节点分类——每个用户是一个节点某条边存在表示有关系节点标签是“认识/不认识”或者更细的兴趣分组。图算法在这里的假设是同质性homophily也就是“物以类聚”有边相连的节点标签倾向于相似。这个假设在很多场景里都成立也正是节点分类能work的根本原因。社交网络里物以类聚交易网络里风险账户也倾向于与风险账户抱团论文引用网络里同一个方向的论文互相引用得更多。理解了这一点你就能明白为什么图神经网络在节点分类上效果显著——它本质上是在用一种可学习的方式把这种“邻居相似”的先验注入模型。1.2 问题定义用一张图和部分标签学出一个分类函数正式一点节点分类的输入是一张图 G(V,E)V 是节点集合E 是边集合通常还带节点特征矩阵 X 和邻接矩阵 A。有一部分节点拥有标签 Y另一部分没有。任务目标是学一个映射函数 f: V→Y使得学到的 f 能对未标注节点输出正确的类别。图神经网络的解法是用邻域信息做卷积或消息传递把每个节点的表示不断聚合成包含局部结构信息的向量再接一个分类头。与图像分类、文本分类这类独立同分布数据最大的差异在于节点之间存在依赖样本之间不独立。所以传统机器学习里“打乱后随机切分”的做法在节点分类场景往往是错的这也是后面要重点讲的数据划分陷阱的源头。1.3 和链接预测、社团发现怎么区分节点分类、链接预测、社团发现是图算法里最容易混淆的三兄弟。节点分类是对“点”打标签链接预测是判断一条不存在的边是否应该存在社团发现则是把图划分成若干个密集连接的社区属于无监督。下面这个表格可以一眼看清区别任务预测对象监督类型典型应用节点分类节点标签有监督/半监督欺诈账户识别、用户兴趣预测、蛋白质功能预测链接预测边的存在性有监督好友推荐、知识图谱补全社团发现节点分组无监督社区运营、网络结构分析实务中三者经常混着用。比如做反欺诈先用社团发现做风险分群再用节点分类对未知节点精确打标最后用链接预测挖掘隐藏团伙关系这是很常见的组合套路。所以学习节点分类并不是孤立学一个算法而是在为整个图算法体系打地基。2. 三代建模路线为什么手工特征和图嵌入带不动节点分类2.1 手工特征时代度、聚类系数、PageRank 不够使最早的方法是把图结构转成手工特征。节点度数、邻居数量、三角形计数、聚类系数、PageRank、介数中心性这些都是经典的结构特征。把它们拼进普通机器学习模型比如逻辑回归或 GBDT就能干活。但这类方法的瓶颈很明显特征设计成本高表达碎片化一个节点的语义往往需要综合几十个特征才能勉强描述特征本质上是静态的难以捕获标签之间的高层关联多数特征描述的是“节点有多重要”或“局部有多稠密”而不是“节点属于哪个类”。我做第一版风险模型时用过 PageRank 做特征效果有提升但提升幅度很有限因为 PageRank 描述的是节点重要性不是节点类别相似性两者相关但不完全等价。真正带来质变的是把“邻居结构信息”当成自适应学习的对象而不是靠人去设计。2.2 图嵌入时代DeepWalk、Node2Vec、LINE 的建模思想图嵌入的思路是把节点映射成固定长度的稠密向量让向量之间的相似度近似图上结构的相似度。DeepWalk 的思想极其朴素在图上做随机游走得到的节点序列类比成自然语言里的句子然后用 word2vec 的 skip-gram 方法学习节点的向量表示。这个思路对搞 NLP 的同学来说会非常亲切本质上就是换了数据源的词嵌入。Node2Vec 在随机游走中引入了两个偏置参数 p 和 qp 控制回退概率q 控制游走偏向“深度”还是“广度”等效于在 BFS 和 DFS 之间做插值从而更灵活地捕捉局部或全局结构。LINE 则显式建模一阶相似度直接相连和二阶相似度拥有共同邻居。这批工作让节点分类第一次从“手工特征分类器”升级成“表示学习分类器”但局限也很明显学出来的嵌入是静态的新节点进来没法直接推断训练时没用上节点本身的属性特征图嵌入优化目标和下游分类任务存在不一致嵌入质量和分类效果不一定是正相关。虽然在直推式场景下效果不错放到归纳式场景就力不从心。2.3 图神经网络消息传递让特征和结构真正融合GNN 是现在处理节点分类的主流工具。它的核心是消息传递机制每个节点在每一层里聚合邻居节点的表示更新自己的表示。叠加多层后节点表示里就包含了多阶邻域的信息。其形式化表达为h_v^(k) UPDATE( h_v^(k-1), AGG({ h_u^(k-1) : u ∈ N(v) }) )GCN 是最经典的实现之一核心操作可以理解为对邻接矩阵做对称归一化后再和特征矩阵做乘法再经过线性变换和非线性激活。公式写出来是H^(l1) σ( \tilde{D}^{-1/2} \tilde{A} \tilde{D}^{-1/2} H^(l) W^(l) )其中 \tilde{A}AI 是加了自环的邻接矩阵\tilde{D} 是对应的度矩阵。这样每一层就是一次“对邻居特征取加权平均”权重由度决定度数低的节点得到的信息总量更少实现上自然做了归一化。GAT 在此基础上引入注意力权重每个邻居对中心节点的影响不是预先固定的而是由模型动态计算。GraphSAGE 则走向另一个方向用固定大小邻域采样来降低复杂度支持 inductive 学习新节点可以直接得到表示。GIN 则从理论上证明了在特定条件下GNN 的表达能力上限对应 Weisfeiler-Lehman 图同构测试。三代方法的核心差异在于手工特征用静态规则描述结构图嵌入把结构压成向量但忽略节点属性而 GNN 把结构和属性同时端到端学习这是它能统治复杂场景的根本原因。2.4 三代方法的实用选择方法是否用节点属性是否支持新节点表达能力实现成本手工特征可选是低低图嵌入否否多为直推中中GNN是GCN 直推GraphSAGE 等可归纳高中高如果只是要一个快速基线可以先跑标签传播如果数据量不大且追求效果直接上 GCN/GAT如果是工业级大规模动态图GraphSAGE 或者 Cluster-GCN 这类采样方案会更合适。选型不贪新关键看你的场景是直推式还是归纳式以及节点特征是否可靠。3. 标签传播一个被低估的强基线3.1 算法原理一句话就能说清楚标签传播Label Propagation AlgorithmLPA的原理朴素到令人怀疑每个节点不断查看邻居的标签然后把出现次数最多的标签作为自己的新标签不断迭代直到全局稳定。形式上就是C(v) argmax_c Σ_{u∈N(v)} 1[C(u)c]没有特征矩阵没有训练过程只靠图结构。我第一次用它时也觉得太简单但实验数据经常打脸在不少同质性较强的图数据上LPA 的准确率能逼近甚至超过一个没调好的 GCN。更关键的是它不需要太多标注数据几秒钟就能跑完几百万节点的图。所以在拿到一个新数据集时我强烈建议先跑一遍 LPA 当基线。如果 LPA 的表现已经很差说明这个图可能同质性不强后面再去调 GNN 也得先检查数据质量如果 LPA 表现很好但 GNN 反而更差那就要怀疑 GNN 实现或训练过程有问题。3.2 从 LPA 到约束版本迭代更新的细节与稳定性标准 LPA 有个毛病如果初始标签带错或者某个类标签扩散失衡结果可能震荡不够稳定而且它完全不利用节点特征。改进方向很多最常用的是加约束的标签传播变体核心是把已知标注节点的结果固定住不让它们被邻居“带偏”同时迭代时采用异步更新而非同步更新让标签传播方向更稳定。再往上有谱化的 Label Spreading考虑归一化图拉普拉斯更新时保留初始信息数学形式更优雅对噪声的鲁棒性也更好。实操中我很少裸用 LPA通常是给种子节点更高的置信度权重或者迭代若干次后人工检查几个高风险区域的标签是否合理再决定是否结束迭代。LPA 看似简单但它的收敛速度和结果在很大程度上取决于节点的更新顺序这也是很多人复现 LPA 结果不一致的根本原因。3.3 什么场景下标签传播反而比 GNN 更值得用第一是冷启动场景节点没有特征或特征噪音很大第二是超大规模图GNN 训练成本太高LPA 是线性复杂度第三是对可解释性要求高的场景LPA 的决策路径非常透明你可以直接说清楚某个节点为什么被打上这个标签。我遇到过一个图数据节点特征基本都是缺失值唯一可靠的信息是边关系这种情况下堆 GNN 纯属自找麻烦LPA 两行代码就出结果。但这不意味着 LPA 能替代 GNN一旦特征丰富、语义复杂LPA 的上限就明显不如 GNN。所以更准确的说法是LPA 是基线的第一选择GNN 是效果的上限来源。4. 完整实操链路从数据划分到模型评估4.1 数据集选型与预处理先学会跑通 Cora节点分类最经典的公开数据集是 Cora、CiteSeer、PubMed它们都是论文引用网络。Cora 有 2708 个节点、5429 条边、7 个类别、每个节点一个 1433 维的词袋特征CiteSeer 是 3327 个节点、6 类PubMed 规模稍大约 2 万节点、3 类。用这些数据做算法验证效率很高。如果是自建图数据预处理有几个关键点节点 ID 必须稳定且全局唯一边要去重、去掉自环除非业务上自环有意义节点特征要先做缺失值处理和标准化标签要检查类别分布。很多初学者忽略的是边的构建方式直接决定同质性假设是否成立。比如“共用一个手机号”建出来的边和“共同关注同一个账号”建出来的边语义完全不一样不要混在一张图里不加区分。同一个公司名下多个账号的聚合方式也会影响图结构这一步做错了后续模型再强也救不回来。4.2 train/val/test 划分的隐藏陷阱transductive 与 inductive节点分类的划分方式是最容易踩坑的地方。多数论文用的是 transductive 设定训练时所有节点的特征都可见只是测试节点的标签不可见。模型在训练过程中如果用了测试节点的特征和边严格说并没有“泄漏”标签但测试分布已经被引入了。这在学术上没毛病在工程上却常常导致模型上线后效果大打折扣——上线时新节点可能连特征都不完整。所以生产环境我建议优先使用 inductive 设定训练时完全看不到测试节点的输入。GraphSAGE、GraphSAINT、Cluster-GCN 等都支持 inductive 学习。另外划分时绝不能随机打乱后切分而要考虑按时间、按社区、按子图做划分。举个例子交易反欺诈里如果训练集是上个月的图测试集是下个月的图那模型能力才是真实可信的随机切分会让同一个用户的多个关联账户散布在训练集和测试集里指标虚高得离谱上线后立刻现原形。4.3 从零实现 GCNPyG 的完整流程与超参数细节用 PyTorch Geometric 实现一个两层 GCN 非常直接核心代码如下from torch_geometric.datasets import Planetoid from torch_geometric.nn import GCNConv import torch.nn.functional as F dataset Planetoid(root/tmp/Cora, nameCora) data dataset[0] class GCN(torch.nn.Module): def __init__(self): super().__init__() self.conv1 GCNConv(dataset.num_features, 16) self.conv2 GCNConv(16, dataset.num_classes) def forward(self, data): x, edge_index data.x, data.edge_index x self.conv1(x, edge_index) x F.relu(x) x F.dropout(x, trainingself.training, p0.5) x self.conv2(x, edge_index) return F.log_softmax(x, dim1)训练时用 Adam 优化器lr0.01、weight_decay5e-4、hidden16、dropout0.5这是 Cora 上最常见的一组配置。很多人直接抄但不知道含义weight_decay 在做 L2 正则对节点多、参数少的场景很重要dropout 防止模型过拟合到邻居信息层数超过 2~3 层后GCN 通常反而变差这是因为过平滑。我实测下来Cora 上两层 GCN 收敛后 test accuracy 大约 81%~82%。如果你跑出来低于 78%基本可以认为是数据划分或实现细节有问题而不一定是模型问题。训练时要用 Adam 而不是 SGD原因是图卷积的梯度方差较大Adam 的自适应学习率更稳。4.4 评估指标光看准确率不够节点分类最常见的评估指标是 Accuracy但类别不平衡时准确率会骗人。比如欺诈场景中正样本只占 2%全部预测成负样本准确率都有 98%。这种情况下要看 macro-F1 或 per-class 的 F1。micro-F1把所有类别的样本合并后算全局 F1对样本数多的类别更敏感macro-F1分别算每一类的 F1 后求平均对样本少的类别更敏感在欺诈检测这类长尾场景中更值得关注。另外一定要打印混淆矩阵看哪些类互相混淆这能直接告诉你模型到底学到了什么。比如在 Cora 上如果“规则学习”和“强化学习”两类经常互相误判说明它们的特征和邻居结构确实高度相似这不是模型 bug而是数据本身的性质。这种时候与其疯狂调参不如反过来想想是否该合并类别或者给模型补充更多可区分的特征。5. 踩坑总结数据泄漏、过平滑和大图训练的实操心得5.1 数据泄漏最隐蔽的三种情况如果把“随机划分的训练集/测试集”直接拿来做节点分类泄漏几乎是必然的。第一种是边泄漏训练集里有一个节点测试集另一个节点但两者之间有边。消息传递时测试节点的标签信息可以通过多层传播到训练节点模型等于在开卷考试中拿到了答案。第二种是特征泄漏你在特征工程阶段用全部节点的数据计算统计量比如均值、度分布这等于把测试集分布信息带进了训练过程。第三种是节点层面的样本重叠同一个真实实体被拆成多个节点分别进了训练集和测试集。应对思路是划分务必按子图或时间戳进行所有统计类特征只在训练子图上算再映射到其他部分如果数据里有实体去重后的唯一标识务必用它做分组。这些做法不需要高级模型但能避免指标和线上效果出现巨大落差。5.2 训练不收敛和过平滑GNN 调参的两大拦路虎GNN 训练和普通深度网络不太一样我踩过几个具体的坑。第一个是学习率过大。用 lr0.1 跑 GCN 时 loss 经常飞到 NaN后来改成 0.01 甚至 0.005 就好了。GCN 的梯度经过邻接矩阵乘法后会叠加邻居节点的影响对学习率更敏感不能照搬图像分类任务的默认值。第二个是初始化。用随机正态初始化不如用 Glorot/Xavier 初始化稳定因为图卷积涉及邻接矩阵乘法梯度传播对初始化方差很敏感。第三个是过平滑。堆到 5 层以上节点表示趋于一致分类准确率断崖式下降。常见缓解方法有 JK-Net跳跃连接、DropEdge训练时随机丢弃边、PairNorm对表示做归一化以及 GCNII 这类加残差和初始残差的模型。工程上我的建议是先拿 2 层 GCN 打通 baseline再按需加深绝大多数业务场景 2~3 层足够不要一上来就搞 10 层的花活。5.3 大图训练的工程化要点邻居采样与显存控制当图规模到千万节点级别直接全图训练必须计算所有节点的中间表示显存必然爆炸。工业界常见做法是 mini-batch 邻居采样。GraphSAGE 的做法是每一层采样固定数量的邻居把计算子图控制在可控范围。GraphSAINT 和 Cluster-GCN 则是随机抽子图或聚类子图做训练把一整张大图切成能放显存的小块。工程上还有一个容易被忽略的点邻接矩阵的存储。千万级节点的稀疏邻接矩阵用 CSR 格式存储几百 MB 就能搞定但如果不注意稀疏化直接跑稠密矩阵乘法内存规模是天文数字。分布式训练时图划分的质量直接影响通信开销选 METIS 还是随机划分要结合具体图密度和任务来测试。5.4 几点经验基线先行、结构优先、迭代验证结合这些年的项目经验我整理了节点分类的几个实操要点先跑 LPA 或两层 GCN 拿 baseline没有基线的调参都是耍流氓检查同质性系数看这个图到底适不适合用图算法数据划分先按时间或社区做不要随机切特征层面保留节点原始属性同时叠加简单的身份特征模型效果不好时先怀疑数据泄漏和划分方式再怀疑模型容量。这些东西看起来琐碎实际上每次项目里有超过一半的问题都出在这些地方而不是出在模型不够先进上。最后分享一个小技巧如果你在实验中发现 GNN 和 LPA 的效果差距不大别急着下结论说 GNN 没用先看看标注数据量。节点分类本质上是半监督任务在标注样本极少的情况下LPA 用纯结构传播本来就占优势GCN 的优势要到特征质量和标注数量都上来之后才会体现。这个判断能帮你避免很多无谓的调参和内耗。