看到这篇论文标题的时候我第一反应是“又一个把自编码器接到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流程相比分群的稳定性明显更好而且调参时间没有增加多少。这就是这个方法最大价值所在——它不追求花哨的结构而是实打实地把特征学习和聚类目标拧在了一起。你如果手头也有类似的无标注数据聚类需求完全可以照着这个思路搭一套自己的流程然后在你自己的数据上反复打磨那几个权重参数。