深度图聚类中的对比学习:组件选型、联合训练与避坑指南

发布时间:2026/10/28 11:38:10
深度图聚类中的对比学习:组件选型、联合训练与避坑指南 深度图聚类这词听着唬人拆开看就一件事手里有一张图节点特征和连边关系都摆在那儿但没有任何节点标签得把节点分成若干组让组内尽量像、组间尽量不像。传统套路是拿图卷积网络学一份节点表示再挂个K-Means收尾流程跑得通可这套东西有个软肋——无监督信号只有“重构邻接矩阵”这一条路学出来的表示更擅长还原拓扑连接而不是为聚类服务。对比学习恰好补上了这块短板它不需要标签只需要构造“谁和谁是同一个节点”的判断任务就能把表示空间往簇状结构上推。这篇内容聊的就是对比学习在深度图聚类里到底怎么落地、每个组件怎么选、哪些环节容易翻车、代码该长什么样适合已经跑过GCN、想往自监督聚类方向走的读者也适合做节点分类做腻了想换换口味的人。文中的思路和参数是基于公开研究与常见工程实践总结的具体到你的数据上还得自己动手调。1. 为什么深度图聚类要引入对比学习1.1 重构损失的“局部惰性”到底卡在哪先把问题说透。基于图自编码器的做法损失函数通常是重构邻接矩阵的内积误差形式上是让相邻节点的表示内积大、不相邻的小。这个目标本身没毛病但它有个隐含偏好它只关心“这条边在不在”不关心“这两个节点的表示在整体空间里该落在哪一坨”。结果就是表示空间呈现出一种弥散状态——邻居之间确实靠近了但不同簇之间没有明确的分离边界K-Means一刀切下去簇的轮廓全靠运气。更麻烦的是GCN的平滑特性。每一层聚合相当于一次低通滤波两三层之后同一连通区域内的节点表示会高度趋同。这对节点分类是好事对聚类却是双刃剑簇内紧致度够了簇间区分度反而被抹平了。重构损失对此毫无约束因为它压根不涉及“远距离节点之间应该拉开多远”这件事。我在早期做的一个实验里把重构损失单独跑到收敛然后用t-SNE看表示分布经常是一大团糊在一起轮廓系数低得可怜。对比学习改变的就是这一点。它显式地构造“正样本对”和“负样本对”损失函数逼着模型把正样本拉近、负样本推远。这个目标跟聚类的诉求是同构的——聚类本质上也是拉近同类、推远异类只不过对比学习用自监督的方式把这个过程提前到了表示学习阶段。换句话说对比损失给表示空间注入了一种“判别性”而这种判别性恰恰是无监督聚类最缺的东西。1.2 图数据做对比学习的三个天然契合点第一图结构本身就是正样本的富矿。在图像领域做对比学习你得靠裁剪、翻转、颜色抖动这些手段硬造出两个视图而图上一个节点的邻居、二跳邻居、不同增广视图下的同一个节点都能直接当正样本用。同质性假设相连节点倾向同类给这件事提供了理论支撑虽然现实图里同质性程度参差不齐但整体上这个先验是成立的。第二对比学习不需要标签这跟图聚类的场景完全对齐。半监督的方法要拿一部分标签迁移学习的方法要预训练模型而对比学习从零开始就能跑代价只是多几次前向传播。第三对比学习产出的表示可以直接喂给聚类头也能跟聚类目标联合优化。这一点很关键——很多早期工作是把“学表示”和“做聚类”当成两个独立阶段先训练编码器再离线跑K-Means中间没有任何反馈。联合训练之后聚类分配的置信度可以反过来指导对比学习挑正负样本形成一个闭环。1.3 三个常见的认知误区第一个误区是觉得增广越复杂越好。有人一上来就叠四五种增广边删一堆、特征遮一半、子图再采一遍结果两个视图的语义已经漂移了正样本对压根不正对比损失反而在教模型学噪声。图上增广的强度必须保守后面会细说。第二个误区是认为对比损失占的比重越大越好。对比损失是辅助目标不是主目标。它在前期帮你把表示空间撑开到了后期如果权重还压得很高聚类损失就推不动分配最后簇的数量和形状都不受控。第三个误区是把对比学习和聚类当成两个割裂的模块拼起来训练完编码器就扔掉对比分支。这种“两阶段硬拼”的做法在早期论文里常见但实测下来联合训练比两阶段普遍能高出两到三个点的ACC代价是显存和调参复杂度上升。2. 核心组件拆解与选型逻辑2.1 图增广最容易翻车的一环图增广的目的是造出同一张图的两个“视角”让模型学会对扰动不变。常见的有四类边扰动随机删边或加边、特征掩码随机把部分维度置零、节点丢弃随机去掉一部分节点及其连边、子图采样按随机游走取子图。每类都有强度参数边扰动通常控制在0.1到0.3特征掩码在0.2到0.4节点丢弃别超过0.2。为什么强度要压这么低因为图的信息密度比图像高得多。一张224×224的图片随机裁掉20%的面积剩下的部分还能认出是猫但一个节点如果丢掉一半邻居它在结构上的身份可能就变了——原本它属于A簇靠的是跟三五个核心节点的连接删掉两个它就跟B簇的核心节点更近了。这种“语义漂移”是图增广特有的坑正样本对变得不正对比学习就在学错误的对应关系。实践里我的偏好是特征掩码当主力边扰动做辅助。特征掩码改变的是节点自身的属性表达不会破坏拓扑身份相对安全边扰动虽然直接作用于结构但风险高得配合较小的强度。另外有个细节增广应该对两个视图独立采样而不是共用同一个mask——共用的话两个视图高度相关对比学习的难度太低学不到东西。还有一种被讨论很多的替代品不做显式增广直接用“节点本身 它的邻居”构造正样本对。这类方法比如用局部邻域和全局语义邻居生成正样本的思路绕开了增广强度调参的麻烦在异配图上表现更稳后面第5节会展开。2.2 编码器GCN、GAT还是MLP加传播编码器的选择取决于图的稀疏程度和同质性。两层GCN是最常用的基线参数少、训练快感受野刚好覆盖二跳邻居对大多数中等规模的图够用。层数往上加要谨慎三层以上过平滑会很明显表示趋于一致对比损失再怎么拉也拉不开。GAT适合邻居重要性差异大的图但它带来的额外参数量和计算开销在聚类任务上未必划算——聚类看的是整体结构注意力权重的细粒度区分能力在这个场景下收益有限。我试过在几个中等规模的引文类图上对比GAT比GCN平均只高零点几个点训练时间却翻倍。另一条路是MLP加邻接传播也就是把特征变换和结构聚合解耦先用MLP做特征映射再乘归一化邻接矩阵做传播。这种结构的好处是传播部分不参与梯度或者说梯度路径更短训练更稳而且在特征质量高的图上MLP的表现未必输给GCN。双塔结构里两个视图共享编码器权重是常规做法参数量小、不容易过拟合如果数据量足够大独立权重的双塔也能试但收益不明显。邻接矩阵的预处理有个细节容易被忽略必须做对称归一化$D^{-1/2}AD^{-1/2}$否则度数高的节点表示会爆炸。稀疏图用稀疏张量存别图省事转成稠密矩阵节点数过万之后显存直接吃满。2.3 对比目标InfoNCE及其图上的变体最常用的还是InfoNCE形式上是把正样本对的相似度在所有候选里做softmax$$L_i -\log \frac{\exp(\text{sim}(z_i, z_i^)/\tau)}{\sum_{j}\exp(\text{sim}(z_i, z_j)/\tau)}$$其中$z_i$和$z_i^$是同一节点在两个视图下的表示分母里的$j$遍历当前batch内的所有节点含自身$\tau$是温度系数。损失要对两个视图对称算一遍再取平均这样梯度更均衡。温度系数是个关键旋钮常取0.2到0.5。它的作用相当于控制softmax的锐度$\tau$小分布更尖锐模型会重点去区分那些“难负样本”跟正样本相似度已经很高的负样本$\tau$大分布更平滑模型对所有负样本一视同仁。$\tau$太小比如0.05训练会不稳梯度方差大$\tau$太大比如1.0判别性不足表示拉不开。我自己的经验是0.2起步观察到损失震荡就往上调到0.3。负样本的来源有两种batch内负采样和memory bank。batch内简单但batch size受显存限制负样本数量不够时对比效果会打折memory bank存历史表示负样本池大但存的东西可能过时需要动量更新。中等规模图几千到几万节点用batch内负样本就够全图训练的时候batch就是全图负样本自然充足。还有个值得注意的方向是难负样本挖掘。随机采的负样本里大部分跟正样本差异明显梯度贡献很小真正有用的是那些“看起来像正样本的负样本”。有工作用Beta分布去估计某个节点成为难负样本的概率按概率加权采样。这类技巧能提升性能但引入的额外超参也多建议先把基础版跑通再加。2.4 聚类头从K-Means到软分配最朴素的做法是训练完之后对表示跑一次K-Means。简单但问题是聚类结果不参与训练编码器不知道自己要为什么样的簇结构服务。稍微进一步是DEC那套软分配先初始化$K$个簇中心$\mu_j$用t分布算节点$i$属于簇$j$的概率$$q_{ij} \frac{(1|z_i-\mu_j|^2)^{-1}}{\sum_{j}(1|z_i-\mu_{j}|^2)^{-1}}$$然后构造一个锐化后的目标分布$p$把$q$平方再按列归一化用KL散度让$q$逼近$p$$$p_{ij} \frac{q_{ij}^2/\sum_i q_{ij}}{\sum_{j} q_{ij}^2/\sum_i q_{ij}}$$$$L_{cluster} \text{KL}(p | q)$$平方操作的意图是“奖励高置信度分配”让已经分得比较明确的节点进一步固化同时压低模糊分配的权重。这个设计有用但也埋了个雷如果早期某几个簇中心恰好吸走了大量节点锐化会把这个错误放大最后形成簇塌缩。所以$L_{cluster}$的权重不能一上来就给大得等表示结构基本成型再逐步加上去。簇中心怎么初始化随机从表示里挑$K$个点或者先跑一次K-Means取质心。后者更稳我一般用后者。簇数$K$在真实场景里往往未知只能靠先验或者用轮廓系数、间隔统计量去估这部分属于聚类本身的固有难题对比学习帮不上忙。2.5 三个损失怎么平衡完整的损失通常是三项之和$$L L_{con} \alpha L_{cluster} \beta L_{recon}$$$L_{con}$是对比损失$L_{cluster}$是上面的KL损失$L_{recon}$是可选的邻接重构损失加上它能防止表示偏离拓扑太远。权重上$\alpha$一般在0.1到1之间$\beta$在0.01到10之间跨度大是因为不同图的稀疏程度差异很大。调参的顺序建议是先把$\alpha$和$\beta$都设成0纯跑对比学习确认表示空间已经能拉出簇的轮廓用t-SNE肉眼看或者算一下轮廓系数然后固定$\alpha0.1$加上聚类损失观察分配熵的变化最后再考虑要不要加重构项。一次性把三个权重都开上去调基本等于盲调。3. 实操搭建一个可复现的训练流程3.1 数据与图结构的准备细节输入是三样东西节点特征矩阵$X$$N \times d$、边索引两行COO格式、簇数$K$。预处理包括三步特征按行做L2归一化避免某些维度数值范围过大主导相似度计算、邻接矩阵加自环、对称归一化。加自环这一步别省不然孤立节点的聚合结果会是零向量。import torch import torch.nn.functional as F def normalize_adj(edge_index, num_nodes): row, col edge_index # 加自环 loop torch.arange(num_nodes, deviceedge_index.device) row torch.cat([row, loop]) col torch.cat([col, loop]) deg torch.zeros(num_nodes, deviceedge_index.device) deg.index_add_(0, row, torch.ones_like(row, dtypetorch.float)) deg_inv_sqrt deg.pow(-0.5) deg_inv_sqrt[torch.isinf(deg_inv_sqrt)] 0. val deg_inv_sqrt[row] * deg_inv_sqrt[col] return torch.sparse_coo_tensor(torch.stack([row, col]), val, (num_nodes, num_nodes)).coalesce() def normalize_feat(x): return F.normalize(x, p2, dim1)增广函数单独写两个视图独立采样def augment(edge_index, x, p_e0.2, p_f0.3): # 边扰动随机删边 mask_e torch.rand(edge_index.size(1), deviceedge_index.device) p_e ei edge_index[:, mask_e] # 特征掩码随机置零 mask_f (torch.rand_like(x) p_f).float() xa x * mask_f return ei, xa注意边扰动之后邻接矩阵要重新归一化删边改变度数分布用旧的归一化系数会出错。这个细节我在第一次实现时漏掉了训练损失正常下降但聚类指标一直上不去排查了大半天才发现。3.2 预热阶段先让表示站稳预热阶段只跑对比损失不碰聚类头。epoch数取总轮数的20%到30%或者按损失曲线来判断——对比损失下降到趋于平缓就停。这个阶段的作用是让编码器先学到一个有判别性的表示空间簇的轮廓大致成形后面聚类头接手的时候不至于从一团乱麻开始。预热阶段有个小技巧初始阶段可以把温度系数设大一点比如0.5训练稳定后再逐步退火到0.2。这相当于先粗后细前期让模型关注整体分布后期再聚焦到难区分的样本上。def info_nce(z1, z2, tau0.2): z1 F.normalize(z1, dim1) z2 F.normalize(z2, dim1) logits z1 z2.t() / tau labels torch.arange(z1.size(0), devicez1.device) loss F.cross_entropy(logits, labels) loss2 F.cross_entropy(logits.t(), labels) return (loss loss2) / 2对称化这一步别省只单向算的话正样本对的梯度不对称表示会偏向某一个视图。3.3 联合训练聚类头接进来预热结束后用当前表示跑一次K-Means拿簇中心然后进入联合训练。每个epoch的流程是生成两个增广视图 → 编码得到$z_1$、$z_2$ → 算对比损失 → 用$z_1$算软分配$q$和目标分布$p$ → 算KL损失 → 反向传播。簇中心每若干个epoch用当前表示重新估计一次或者用动量方式缓慢更新。def target_distribution(q): weight q ** 2 / (q.sum(0) 1e-8) return (weight.t() / (weight.sum(1, keepdimTrue) 1e-8)).t() # 一个训练step的骨架 q soft_assign(z1, centers) # N x K p target_distribution(q).detach() loss_kl F.kl_div(q.log(), p, reductionbatchmean) loss loss_con alpha * loss_kl beta * loss_recon$\alpha$用线性升温比较稳从0.01起步在总轮数的前一半里线性升到目标值。一上来就给0.5以上簇塌缩的概率非常高。评估时用ACC需要标签对齐这一步只在测试时做训练过程中不能碰。用匈牙利算法做最优匹配import numpy as np from scipy.optimize import linear_sum_assignment def cluster_acc(y_true, y_pred): y_true y_true.astype(np.int64) y_pred y_pred.astype(np.int64) assert y_pred.size y_true.size d max(y_pred.max(), y_true.max()) 1 w np.zeros((d, d), dtypenp.int64) for i in range(y_pred.size): w[y_pred[i], y_true[i]] 1 row, col linear_sum_assignment(w.max() - w) return w[row, col].sum() / y_pred.sizeNMI和ARI不需要对齐直接算就行。报告结果的时候记得跑五到十个随机种子给均值和标准差单次结果在小图上波动能到两三个点没有统计意义的对比等于没做。3.4 关键超参的取值区间把常用参数的合理范围整理一下这些值来自公开实现和我的实际调试经验可以作为起点但别当教条参数含义建议区间调参备注p_e边扰动比例0.1 ~ 0.3稀疏图取下限p_f特征掩码比例0.2 ~ 0.4特征维度高时可取大tau温度系数0.2 ~ 0.5损失震荡就往上调alpha聚类损失权重0.1 ~ 1.0线性升温勿一步到位beta重构损失权重0.01 ~ 10稀疏图取大值K簇数由先验给定未知时用间隔统计量估编码器层数GCN层数2 ~ 3超过3过平滑明显预热比例占总轮数20% ~ 30%看损失曲线可提前结束表里最值得花时间的是$p_e$和$p_f$它们直接决定正样本对的质量其他参数的影响相对次要。4. 常见问题排查实录4.1 表征坍缩所有节点长得一样现象很直观随便抽两个节点算余弦相似度都在0.95以上t-SNE图上所有点挤成一个球。这种情况说明对比学习没起到拉开距离的作用模型找到了一个“偷懒解”——把所有输入映射到同一个向量损失里的正样本项虽然最优但负样本项也没被惩罚到因为softmax所有logits都一样。检测方法很简单算表示矩阵的平均成对余弦相似度超过0.8就该警惕或者看表示矩阵的奇异值分布如果只有一个奇异值显著大其余接近零基本可以确诊。成因通常是三个负样本数量不够batch太小、温度系数太大、或者损失里只有正样本对齐项没有负样本推远项。对应的解法是加大batch、把$\tau$降到0.1到0.2、检查损失实现有没有漏掉分母。还有一种情况是用了类似BYOL的非对称结构但没有加预测头和stop-gradient这种结构本来就容易坍缩实现细节必须严格对齐。4.2 聚簇塌缩某一个簇吞掉大半节点这个是聚类损失特有的坑。表现是分配矩阵$q$里某一列的概率接近1其他列接近0最终ACC看着不算太差但其实只分出了一个有效簇。根因是目标分布$p$的锐化操作有正反馈特性某个簇中心一旦在早期吸到更多节点锐化后它的权重更高KL损失会推着更多节点往那儿跑滚雪球。$\alpha$设太大、升温太快、簇中心初始化不合理都会触发这个。处理办法有几个把$\alpha$的升温曲线拉长前30%的轮数里保持很小的值在KL损失里加一个簇大小均衡的正则项惩罚各簇分配总数的方差或者定期用当前表示重跑K-Means重置簇中心打断正反馈。我在一个类别极不均衡的图上遇到过这个问题最后是靠“每10个epoch重置一次簇中心 $\alpha$上限压到0.3”解决的。4.3 过平滑与深度选择层数加到三层以上节点表示会趋同这时候对比损失的值可能还在降但聚类指标已经不动了甚至倒退。判断方法是对比不同层数下表示的平均相似度和聚类指标如果相似度随层数单调上升而指标单调下降就是典型的过平滑。缓解手段包括加残差连接每层输出跟输入相加、用跳跃知识网络把各层表示拼接、或者在聚合时给自环更大的权重。最省事的做法还是回到两层GCN两层的感受野对大多数中等规模的图已经够用多出来的层带来的收益远小于它引入的问题。4.4 排查速查表症状最可能的原因优先尝试的动作表示平均余弦相似度 0.9表征坍缩降温度、加batch、检查负样本项单簇占比超过80%聚类损失权重过大或升温过快延长升温、加均衡正则、重置簇中心对比损失下降但ACC不动表示判别性与聚类目标脱节加聚类损失、检查软分配实现训练损失震荡剧烈温度过小或增广过强温度上调、增广比例下调指标随种子波动超3个点初始化敏感多次运行取均值、固定簇中心初始化方式显存爆掉全图稠密邻接或大batch换稀疏张量、邻居采样、梯度累积5. 进阶方向与工程取舍5.1 不做显式增广的路子显式增广的麻烦在于强度难调调不好正样本对就失真。有一类做法绕开这个环节正样本直接从图结构里取比如一个节点的局部邻居加上通过表示相似度找到的全局语义邻居两者共同构成正样本集合。这样做的好处是不引入额外的扰动超参语义漂移的风险小在异配图相连节点未必同类上表现更稳。代价是正样本的质量依赖初始表示冷启动阶段可能不准通常要先跑几轮基础训练把表示预热一下。另一条路是簇级对比把簇中心当成原型做“节点—原型”的对比而不是“节点—节点”。这种思路跟自监督聚类里的原型学习是一脉相承的好处是负样本的数量从节点数降到簇数计算量小而且对比目标和聚类目标天然一致。风险是原型本身可能学偏需要配合均衡约束。5.2 大规模图上的采样与显存控制全图训练在几万节点以内还能扛再往上就得采样。邻居采样是最直接的办法每个batch选一批目标节点每层采样固定数量的邻居把子图拉出来算。这时候对比学习的负样本只能来自当前batchbatch size要给够不然负样本太少对比效果打折。折中方案是维护一个表示队列把历史batch的表示存下来当负样本池队列长度取几千用动量更新。还有个工程细节增广和编码是两遍前向显存占用差不多是单视图的两倍。显存紧张的时候可以用梯度累积把大batch拆成几个小batch分别算损失再累加梯度效果接近但更省。另外对比损失里的相似度矩阵是$B \times B$$B$取2048的时候这个矩阵就是四百万个数用半精度算能省一半显存。5.3 什么情况下不该用对比学习这话得说在前面对比学习不是万金油。节点数很少的图几百个节点没必要上这套负样本池太小对比学习退化成近似正样本对齐反而不如直接跑谱聚类或者普通GCN加K-Means。同质性极低的图也要谨慎邻居当正样本这个假设不成立硬做对比会把异类拉到一起指标还不如基线。还有一种情况是特征质量极差、几乎全靠结构信息的时候。对比学习依赖特征来算相似度特征本身没信息量增广出来的两个视图也提供不了额外信号这时候不如把精力花在特征工程或者结构嵌入上。判断要不要上对比学习我的经验是先跑一个纯重构损失的基线看表示的平均轮廓系数。如果已经在0.3以上说明结构本身够清晰加上对比学习的边际收益有限如果低于0.1表示空间很糊这时候对比学习的提升通常比较明显。最后分享一个我在实际调试里的体会这套方法里最不值当花时间的是编码器的花式改造最值当的是增广强度和损失权重的联合调试。我见过太多人一上来就换GAT、加注意力、堆残差结果调了两周不如把边扰动从0.5降到0.2带来的提升大。先把对比分支跑通、把坍缩和塌缩这两个坑填上再考虑结构上的创新顺序反了容易在错误的方向上越走越远。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询