☰
深度图聚类中的对比学习:组件选型、联合训练与避坑指南
2026/10/28 11:38:01 网站建设 项目流程

深度图聚类这词听着唬人,拆开看就一件事:手里有一张图,节点特征和连边关系都摆在那儿,但没有任何节点标签,得把节点分成若干组,让组内尽量像、组间尽量不像。传统套路是拿图卷积网络学一份节点表示,再挂个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肉眼看,或者算一下轮廓系数);然后固定$\alpha=0.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, device=edge_index.device) row = torch.cat([row, loop]) col = torch.cat([col, loop]) deg = torch.zeros(num_nodes, device=edge_index.device) deg.index_add_(0, row, torch.ones_like(row, dtype=torch.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, p=2, dim=1)

增广函数单独写,两个视图独立采样:

def augment(edge_index, x, p_e=0.2, p_f=0.3): # 边扰动:随机删边 mask_e = torch.rand(edge_index.size(1), device=edge_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, tau=0.2): z1 = F.normalize(z1, dim=1) z2 = F.normalize(z2, dim=1) logits = z1 @ z2.t() / tau labels = torch.arange(z1.size(0), device=z1.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, keepdim=True) + 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, reduction='batchmean') 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), dtype=np.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.size

NMI和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选一批目标节点,每层采样固定数量的邻居,把子图拉出来算。这时候对比学习的负样本只能来自当前batch,batch size要给够,不然负样本太少对比效果打折。折中方案是维护一个表示队列,把历史batch的表示存下来当负样本池,队列长度取几千,用动量更新。

还有个工程细节:增广和编码是两遍前向,显存占用差不多是单视图的两倍。显存紧张的时候可以用梯度累积,把大batch拆成几个小batch分别算损失再累加梯度,效果接近但更省。另外对比损失里的相似度矩阵是$B \times B$,$B$取2048的时候这个矩阵就是四百万个数,用半精度算能省一半显存。

5.3 什么情况下不该用对比学习

这话得说在前面:对比学习不是万金油。节点数很少的图(几百个节点)没必要上这套,负样本池太小,对比学习退化成近似正样本对齐,反而不如直接跑谱聚类或者普通GCN加K-Means。同质性极低的图也要谨慎,邻居当正样本这个假设不成立,硬做对比会把异类拉到一起,指标还不如基线。

还有一种情况是特征质量极差、几乎全靠结构信息的时候。对比学习依赖特征来算相似度,特征本身没信息量,增广出来的两个视图也提供不了额外信号,这时候不如把精力花在特征工程或者结构嵌入上。

判断要不要上对比学习,我的经验是先跑一个纯重构损失的基线,看表示的平均轮廓系数。如果已经在0.3以上,说明结构本身够清晰,加上对比学习的边际收益有限;如果低于0.1,表示空间很糊,这时候对比学习的提升通常比较明显。

最后分享一个我在实际调试里的体会:这套方法里最不值当花时间的是编码器的花式改造,最值当的是增广强度和损失权重的联合调试。我见过太多人一上来就换GAT、加注意力、堆残差,结果调了两周不如把边扰动从0.5降到0.2带来的提升大。先把对比分支跑通、把坍缩和塌缩这两个坑填上,再考虑结构上的创新,顺序反了容易在错误的方向上越走越远。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询