☰
自编码器约束聚类与自适应邻居:深度聚类方法核心解析
2026/10/22 14:07:12 网站建设 项目流程

看到这篇论文标题的时候,我第一反应是“又一个把自编码器接到K-means前面的工作”,但仔细读完核心思路之后,发现事情没那么简单。TNNLS 2020这篇《Autoencoder Constrained Clustering With Adaptive Neighbors》,本质上是在解决一个特别实际的问题:聚类过程里,怎么让特征表示、相似度关系、簇归属这三件事互相促进而不是各自为政。它把自编码器的重构能力、聚类的判别能力、还有邻居图的局部结构保持能力,揉进了一个统一的目标函数里,并且让邻居关系在训练过程中动态调整,而不是一开始就固定死。

这篇文章适合谁呢?如果你在做深度聚类相关的研究或工程落地,或者你手头有一批无标注的数据,想通过自编码器提取特征然后做聚类分析,又不想让特征提取和聚类完全脱节,那这篇论文的思路非常值得细品。本篇博文我从思路拆解、核心机制、复现要点、调参经验、常见坑几个角度完整过一遍,尽量把论文里没有明说的细节也补出来。

1. 从题目看门道:自编码器、聚类约束、自适应邻居到底解决了什么问题

1.1 一个核心需求:让特征表示主动配合聚类

传统做法通常分成两步走:先用自编码器把高维数据压成低维向量,然后再对低维向量跑K-means或谱聚类。听起来没什么问题,甚至很多项目都是这么落地的。但它的隐患很明显:自编码器训练的时候,只关心“重构误差最小”,它根本不知道你后面要聚类。自编码器在意的是尽可能保留原始数据的完整信息,而聚类其实恰恰相反,它希望把那些对区分簇没有帮助的细节扔掉,甚至希望特征空间里同类样本尽量挤在一起、异类样本尽量分开。

这两件事的目标并不一致。所以第二步聚类的时候,你拿到的特征虽然重构效果好,但未必是“聚类友好”的。有些样本在原始空间里离得近,但在聚类想要的意义下根本不应该进同一个簇;有些样本在原始空间里离得远,但本质上是同一类,只是被噪声和冗余维度拉远了。自编码器不懂这些,它只会老老实实地把信息全部保留,不加区分。

论文的核心动机就在这里:把聚类目标直接放进自编码器的训练过程里,让特征表示的学习被聚类任务“约束”住。这样一来,特征提取器不是自顾自地做压缩,而是边学表示、边迎合聚类需求。所谓“Autoencoder Constrained Clustering”,不是“用自编码器先提取特征再做聚类”这种线性串联,而是让聚类损失反向影响特征表示的学习。这个区别很关键,理解了这一点,后面的整个目标函数设计就顺理成章了。

1.2 为什么自适应邻居比固定相似度图更合理

很多聚类方法在构造图的时候,习惯设定一个固定的邻居数K,然后对每个样本找K个最近邻居,或者用高斯核函数计算两两相似度。这种做法的最大问题是:不同样本所在位置的密度不一样。有的样本处在簇的中心区域,周围同类样本很多,K取小了会丢掉大量有效连接;有的样本处在簇的边界甚至两个簇的交界处,K取大了会把异类样本也拉进来,污染相似度关系。

论文里“Adaptive Neighbors”的应对方案,简单说就是让相似度矩阵S不预先固定,而是在迭代优化过程中自动学出来。对于每个样本,算法会自己判断它跟其他样本之间的相似度该取多少,哪些连接该保留、哪些连接该趋近于零。这个思路其实从另一个角度解决了图聚类里一个长期存在的痛点:图的构造和最终的簇划分应该是耦合的,而不是先用启发式规则把图定死,再在图上做聚类。

更直白地说,自适应邻居让相似度矩阵本身成为了模型的一部分,而不是一个孤立的预处理步骤。这样,每次更新特征表示之后,样本间的距离关系发生变化,邻居关系也跟着更新,形成“特征更新 -> 相似度更新 -> 聚类约束更新 -> 特征再更新”这样一个闭环迭代。这正是这个方法名字里“With Adaptive Neighbors”的分量所在。

2. 方法整体框架:重构、聚类、图结构三股力量如何协同

2.1 统一目标函数:从单一损失到加权组合

论文的损失函数不是单一的一项,而是把自编码器重构误差、聚类约束、自适应邻居图的局部结构保持组合在一起。大致形式可以理解为:

  • 自编码器重构项,让编码器E和解码器D学会低维表示,并且保证这个表示没有丢失原始输入的核心信息。
  • 聚类约束项,让编码器输出的特征在某种聚类目标下表现得好。常见做法是把特征与簇中心之间的距离作为惩罚,或者用分布匹配的思路让特征向簇中心靠近。
  • 自适应邻居图正则项,让特征空间的局部几何结构尽量平滑,也就是如果两个样本在原空间或者特征空间里被判定为相似,那么它们的编码表示也不应该差太远。

整体目标函数就好比一个有三股力量的拔河。重构项死死拉住“别丢掉原始信息”,聚类项使劲拽着“特征要适合分簇”,图正则项在旁边调节“局部结构别崩”。任何一股力量过强,都会出问题。重构权重太大,聚类约束就形同虚设;聚类权重太大,特征可能过度扭曲,甚至把所有样本挤到同一个簇中心附近;图正则权重太大,特征会变得过度平滑,失去判别性。

这种多损失叠加的思路在深度聚类里并不罕见,但论文的巧劲在于每一项都不过度复杂,组合起来却解决了特征提取和聚类脱节的冲突。

2.2 聚类的约束方式:为什么不能只依赖自编码器

如果只靠自编码器学习特征然后直接聚类,其实你是在赌自编码器碰巧学到了一个对聚类友好的表示空间。对于简单数据集,比如手写数字这类结构清晰的数据,运气好的时候确实能学到不错的特征。但数据一复杂,比如类别间有重叠、背景噪声大、类内差异大,自编码器学到的特征往往会把同一类的样本拉得很开,因为它们本身在像素空间里差异就大。

聚类约束的加入,相当于在训练过程中不断提醒特征提取器:有些样本你们必须在表示空间里靠近,因为它们是同一簇。反过来,簇中心又是由当前特征算出来的,特征提取器更新了,簇中心也跟随更新。这样,特征与聚类结构互相塑造,而不是特征学习完之后才被动地接受一次聚类。

这一点在实际编码中体现得很直接。每次迭代时,你不会先固定所有特征再把K-means跑满收敛,而是交替执行:更新网络参数 -> 重算特征 -> 更新簇分配和簇中心 -> 用新的聚类信息再指导网络更新。这种交替策略在实现时要注意收敛性的控制,后面实操部分我会详细展开。

2.3 自适应邻居与图拉普拉斯的连接

自适应邻居模块从数学上看,和经典的图拉普拉斯正则化是一脉相承的。只不过传统的图拉普拉斯使用固定权重矩阵,而这里的权重矩阵是优化变量。对于每个样本,相似度向量需要满足非负和归一化的约束,也就是每个样本对其余样本的相似度加起来等于一。同时目标函数中还带有一个二次正则项,防止某些样本把相似度均匀摊到所有样本头上,鼓励稀疏且集中的邻居选择。

这样一来,每个样本都会自适应地为自己挑选一小部分“亲密样本”。在特征空间里距离越近的样本,相似度越容易被分配得更高;距离越远,相似度趋近于零。这个操作和稀疏编码里稀疏性的作用异曲同工,都是通过强约束来迫使模型抓取关键结构。

放到整体框架里看,自适应邻居图不仅提供图正则项,还在特征空间变化之后自动调整连接关系。这是它和你自己去固定K近邻图的最大区别。固定K近邻图只需要算一次相似度,之后所有的更新都在这个图上做;自适应邻居则要求你在每轮迭代中都重新求解一次相似度矩阵,计算代价更高,但获得的结构信息更贴合当前的表示空间。

3. 核心机制与实现细节:逐项拆解,直到能上手写代码

3.1 损失函数的具体形态与参数含义

为了后面讲实现不悬空,我把几个关键模块的目标函数展开来讲。自编码器的重构损失最常用的是均方误差,如果输入做了标准化,也可以用其他距离度量。聚类约束项,论文采用的是类似K-means的分配形式,也就是每个样本的特征向量与它所属簇的簇中心之间的欧氏距离平方和。这个选择比较朴素但很稳,因为它和K-means天然兼容,迭代更新方便。

自适应邻居部分的约束,数学上是一个带约束的二次规划问题。每个样本的相似度向量需要满足求和等于一、每个分量大于等于零,同时目标函数里包含用特征空间距离加权的线性项和一个用于控制稀疏度的二次项。这个子问题虽然看起来复杂,但因为有闭式解或者非常成熟的快速求解方法,实际实现并不困难。

整体的加权方式,论文里通过几个超参数来控制不同损失项的相对强度。这些超参数的选择直接决定模型行为。我做过一些实验,大致可以这样描述它们的作用:

  • 重构项的权重决定了特征保留原始信息的程度,权重越大,特征越偏向于“忠实压缩”,聚类约束的影响力会被削弱。
  • 聚类项的权重决定了特征对簇结构的响应强度,权重过大会让特征过度向簇中心收缩,甚至出现大量样本挤到同一个簇的退化情况。
  • 图正则项的权重决定了局部结构的平滑程度,权重过大容易让不同簇的边界变得模糊。

3.2 网络架构选择:从全连接到卷积

自编码器部分,原论文的实验主要基于图像数据,所以编码器和解码器都可以设计成多层结构。如果是全连接,常见配置是输入维度 -> 500 -> 500 -> 2000 -> 10这样锥形再扩大的结构,中间的2000维是为了让深层特征有足够容量,最后压到10维是为了聚类。这种中间宽、两头窄的设计在深度嵌入聚类类方法里很常见,它的好处是在压缩到低维之前先用宽层捕捉足够的抽象特征。

解码器基本是编码器的镜像。激活函数方面,中间层常用tanh,因为tanh的中心对称性在某些数据集上表现稳;输出层的激活函数要配合输入标准化方式,如果输入范围在0到1之间,输出层可以用sigmoid或者直接不加激活。这些细节论文里不会全写出来,但复现的时候逃不掉。

如果你手里的数据不是图像而是表格或序列,卷积结构可以换成全连接或者一维卷积。关键不是网络多深,而是编码器输出的低维表示维度要适合聚类的规模。一般来说,降到10到30维之间比较常见,维度太低会丢失太多信息,维度太高又让K-means在高维稀疏空间里不稳定。

3.3 自适应邻居子问题的求解逻辑

自适应邻居的更新,每轮迭代中给定当前特征表示,对每个样本单独求解自己的相似度向量。这个子问题规模是N×N的,N是样本数,直接对全体样本求解确实压力大。实际实现中,可以利用它的特殊结构:距离项和二次正则项的组合可以转化为一个带约束的最短路径问题,求解的开销并不大。

比较实用的做法是,对每个样本,先按距离排序,然后只计算前若干候选邻居的相似度,把距离太远的样本相似度直接置零。这样既保证了解的质量,又把复杂度限制在可控范围内。这个近似在实验中几乎不会影响最终聚类效果,因为真正对结构有贡献的本来就是那些相近的样本。

需要注意的是,自适应邻居的更新频率。理论上每一轮都应该更新,但如果网络参数更新频繁,相似度矩阵也跟着剧烈变化,训练会变得很不稳定。我个人的经验是,每隔几轮或者每完成一个epoch之后统一更新一次相似度矩阵,更稳定。论文里的交替优化策略也类似,并不是同一轮内所有参数同步更新,而是分模块交替迭代。

3.4 初始化与预训练:决定稳定性的第一步

这个方法的非凸性很强,初始化不好,模型很容易掉进糟糕的局部最优。最稳妥的启动方式是先只训练自编码器的重构部分,也就是不加入聚类约束和图正则项,让网络先学会一个有意义的低维表示。这个阶段可以跑足一些epoch,直到重构损失明显下降。

有了预训练的自编码器之后,再用它对原始数据生成特征,在这些特征上跑一次K-means,得到初始簇中心。值得注意的是,K-means的初始化也有讲究,用多次随机初始化取最优结果,或者用一些更稳定的初始化策略。比如先用层次聚类跑一遍,把得到的簇中心作为K-means的初始点,这样能大幅减少后面训练里簇中心被带到奇怪位置的概率。

预训练结束后,才把完整的带聚类约束的目标函数加载进来,开始联合优化。联合优化初期的前几个epoch会看到某个损失项突然上升,这很正常,因为模型正在从纯重构模式切换到聚类约束模式。

4. 实操复盘:从零复现这个框架的完整流程

4.1 数据准备与预处理:别在第一步埋雷

我常用两个数据集来验证这类深度聚类方法:手写数字类的小图数据集和带噪声的物体图像数据集。前者结构清晰,适合快速验证流程通不通;后者类别间有更多重叠和背景干扰,适合检验方法的鲁棒性。

预处理阶段,我强烈建议做标准化,把像素值归一化到合理区间。如果数据维度很高,可以先做PCA降维,但注意保留足够多的主成分,一般保留能解释90%以上方差的维度。这个步骤不是为了替代自编码器,而是为了让预训练阶段更稳定,避免输入中的数值波动把梯度带偏。

数据加载方面,如果数据量不大,可以直接全量加载到显存里。深度聚类本身跟监督分类不太一样,它对batch处理的依赖方式比较特殊。因为自编码器部分可以用mini-batch,但聚类约束和自适应邻居部分需要在全体样本的特征上计算。所以实际实现里通常采用混合策略:网络参数和重构损失用mini-batch更新,簇中心和相似度矩阵在全量特征上更新。这个混合策略是实操中的关键设计。

4.2 核心模块的代码结构:伪代码级详解

下面我按自己的实现习惯,给出核心模块的组织方式。第一步,先定义自编码器的前向过程,其中包括编码器部分得到嵌入特征,解码器部分得到重构输出。第二步,聚类模块需要接收嵌入特征,计算每个样本到各簇中心的距离,并把聚类损失作为惩罚项。第三步,自适应邻居模块要对当前嵌入特征求解相似度矩阵S,并对特征做图正则化。第四步,写一个外层循环,交替执行网络更新、簇中心更新、相似度矩阵更新。

伪代码大致是:

# 1. 预训练自编码器(只优化重构损失) for epoch in range(pretrain_epochs): x_recon = autoencoder(x) loss = mse_loss(x_recon, x) optimizer.zero_grad() loss.backward() optimizer.step() # 2. 对特征做K-means初始化簇中心 features = encoder(x) centers = kmeans_initialize(features) # 3. 联合优化 for epoch in range(finetune_epochs): # 先更新网络参数 for batch in data_loader: x_recon = autoencoder(batch) features_batch = encoder(batch) cluster_loss = compute_cluster_loss(features_batch, centers) graph_loss = compute_graph_loss(features_batch, S) loss = recon_loss + lambda1 * cluster_loss + lambda2 * graph_loss optimizer.zero_grad() loss.backward() optimizer.step() # 再更新簇中心 features_all = encoder(whole_data) centers = update_centers(features_all) # 最后更新自适应邻居矩阵 S = solve_adaptive_neighbors(features_all)

这个结构里,每个模块都相对独立,方便调试。实际迭代时batch size不要太大,通常128到256就够了,太大反而让聚类损失的部分变得不稳定。簇中心的更新用全量特征来算,这是保证稳定性的关键。

4.3 关键超参数的取值范围与调参顺序

结合我个人的复现经验,几个核心超参数的敏感度不太一样。重构损失的权重一般设为1,不需要额外调节。聚类损失的权重是一个需要重点调的值,太小了约束不够,太大了特征坍塌。我一般从0.1开始尝试,然后二分调整。图正则项的权重反而可以相对大一点,因为它起到的是平滑作用,常见的稳定区间在0.01到1之间。

自适应邻居模块里还有两个参数需要设:一个是控制稀疏度的系数,影响每个样本的邻居数量;另一个是候选邻居数量,也就是求解时最多考虑多少个近邻。稀疏度系数如果太大,每个样本几乎只认最近的那一两个样本,图结构碎片化;太小的话,每个样本的相似度都摊得很平,失去邻居选择的“自适应”意义。一般来说,让每个样本平均保留5到15个有效邻居是个比较均衡的范围。

调参顺序也有讲究。先调聚类损失的权重,让ACC和NMI能稳定爬升;再微调图正则项的权重,观察特征可视化结果是否出现清晰的簇间分离;最后再动自适应邻居的稀疏度系数。不要一上来就把所有参数一起调,那样你很难判断是哪个参数引起了性能波动。

5. 常见问题与排查技巧实录:这些坑比论文里写的重要

5.1 聚类结果直接崩溃到某个单个簇

这是我见过最频繁的问题:联合训练到中途,几乎所有样本都被分到同一个簇,指标全面退化。根因通常出在聚类损失项权重太大。特征为了迎合簇中心的约束,会逐渐把所有样本拉到相近的位置,簇中心又根据这些过于集中的特征更新,不断互相迎合,最后整个表示空间坍缩成一个点簇。

排查时先看特征空间的方差。如果特征的方差在训练过程中持续下降,基本可以确认是坍缩。应对方案有两种:一是降低聚类损失权重,二是给聚类损失加入某种防坍缩机制,比如对簇分配熵做惩罚,强迫样本不要过度集中在一个簇。我自己更常用的是前者,简单直接,而且论文的方法本身没有额外防坍缩的复杂设计,权重调好就够。

5.2 相似度矩阵更新的稳定性问题

自适应邻居模块在特征每轮都变化的情况下,相似度矩阵也跟着跳。如果你发现loss曲线呈现锯齿状波动,而且波动幅度很大,多半是相似度矩阵更新得太频繁。一般我建议相似度矩阵不要每轮都重新算,而是隔几轮更新一次,给网络一点时间适应当前的图结构。

另一个容易踩的坑是候选邻居数量设置得太小。样本某些区域本来密集,候选邻居少了,真实有用的连接被截断,自适应邻居的效果直接退化成K近邻图。反过来,候选邻居数量太大,求解相似度向量的计算开销暴涨,而且容易被远处样本干扰。根据规模调整,几千个样本的数据集候选邻居取50左右,几万样本的可以取100左右,通常够用。

5.3 评估指标的迷惑性:ACC、NMI、ARI各有各的偏见

深度聚类论文里最常见的三个评估指标是ACC、NMI和ARI。这三个指标各有偏向。ACC简单粗暴,把预测簇和真实簇做最优匹配之后算准确率,它非常依赖簇的数量设置和匹配算法的结果。NMI对簇数量不均衡更敏感。ARI对随机划分惩罚更合理,但两个聚类结果如果簇数差距大,ARI的表现会偏低。

实操中不要只看ACC高就下结论,因为如果真实类别数本身不平衡,ACC可能会虚高。我一般会把NMI和ARI也打出来,三个指标放在一起看。如果ACC高但NMI低,说明簇的纯度虽然高,但簇结构可能不完整,存在簇被切碎或者合并的情况。这时候优先检查簇数设置和特征维度是否合理。

5.4 小批量训练与全局结构损失的矛盾

这个方法的聚类损失和图正则项本质上是全局性质的目标,它需要所有样本之间的相互关系。但网络参数更新又不可能每次都用全量数据,于是容易出现小批量更新和全局目标之间的冲突。我的解决方案是,在更新网络参数时,簇中心和相似度矩阵都使用固定值,这样聚类损失和小批量内的样本之间仍然是全局一致的。等到一个epoch结束,再统一更新簇中心和相似度矩阵,而不是在每个batch内部同步更新。

这种异步更新的代价是目标函数和网络参数之间存在滞后,但实际经验表明,这种滞后反而让训练更稳定,有点像动量更新。如果你用同步方式,每个batch内部都更新簇中心,损失波动会非常剧烈,甚至出现簇中心来回震荡导致整个过程始终难以收敛。

5.5 特征维度过高或过低的取舍

低维特征维度是很多人容易忽视的参数。论文里的实验设置通常降到一个较低的维度,比如10维左右。维度太低,自编码器能够保留的判别信息有限;维度太高,K-means在欧氏距离上的区分力会减弱。我建议根据数据集的真实类别数和复杂度来定。类别数少的取10到20维,类别数多的可以取30到50维。

有个比较实用的信号:查看聚类特征的可视化结果。如果你在二维可视化里能看出明显的簇结构,那么低维特征的质量大概率是够用的。如果二维图上一片混沌,先不要急着加维度,先检查是不是聚类损失权重太小导致约束不够。

6. 个人经验与扩展方向

这套方法我实际使用下来的最大体会是,它的框架灵活性比论文表面看起来要强得多。自编码器部分可以替换成变分自编码器或者别的表示学习网络;聚类约束部分可以替换成别的聚类损失形式;自适应邻居模块甚至可以单独拿出来叠加到其他聚类算法上。这意味着你不需要完全照搬论文的结构,完全可以把它当成一个组合件来用。

我后来在一个业务场景里尝试过类似的思路:大规模无标注日志数据,先用序列自编码器提取行为表示,然后用带自适应邻居的聚类框架做用户分群。和之前的先特征提取再K-means流程相比,分群的稳定性明显更好,而且调参时间没有增加多少。这就是这个方法最大价值所在——它不追求花哨的结构,而是实打实地把特征学习和聚类目标拧在了一起。你如果手头也有类似的无标注数据聚类需求,完全可以照着这个思路搭一套自己的流程,然后在你自己的数据上反复打磨那几个权重参数。

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

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

立即咨询