资讯详情 VAE+GMM深度聚类:原理、三阶段训练与避坑实战
📅 2026/11/3 3:38:50
拿 VAE 配 GMM 做深度聚类这几年我反复用过好几轮从早期的表格数据项目一直到现在的图像与序列特征分群这套组合的性价比一直很高。它的核心逻辑其实很朴素用变分自编码器VAE把原始样本压到一个低维、连续、概率化的隐空间再在这个隐空间里用高斯混合模型GMM去拟合多峰分布把“生成建模”和“分群判别”塞进同一个目标函数里联合优化。跟直接在原始维度上跑 K-Means 相比它抗维度灾难、能给出软划分、还能顺带生成新样本跟纯判别式的深度聚类相比它有明确的概率解释和完整的似然函数训练稳得多。这篇文章适合三类人手上有无标签数据想做结构探索的算法同学、被 K-Means 在高维上折磨过的工程同学、以及想搞清楚 VaDE、DGG 这一脉模型到底在优化什么公式的读者。我会把公式讲明白也会把代码和踩坑过程摊开讲。1. 从K-Means的硬伤说起为什么非要把生成模型和聚类绑在一起1.1 高维稀疏空间里欧氏距离本身就不太可靠很多人第一次做聚类默认动作就是标准化之后直接上 K-Means样本量几万、特征几百维跑出来的结果往往很“均匀”——每个簇的样本数差不多簇内却看不出任何业务含义。这不是 K-Means 实现有问题而是它的核心假设在这里失效了。K-Means 隐式假定各簇是各向同性、方差相同、且边界是超平面的球状分布一旦真实数据的簇是长条形、嵌套形或者各簇的密度差异很大它给出的划分就会严重跑偏。更麻烦的是高维情形下的距离集中现象当维度升高样本两两之间的距离会趋于相等最近邻和最远邻的差距被压缩距离这个度量本身就失去了分辨力。我做过一个很直观的小实验在同一个数据集上对比两种做法的轮廓系数原始维度直接 K-Means和先用自编码器压到 10 维再 K-Means后者在大多数场景下能高出 0.1 到 0.2。这背后的道理并不神秘——编码器是一个非线性降维器它把“重构信息量大”的方向保留下来把噪声方向压掉距离度量在低维稠密空间里重新变得有意义。但两步走也有明显问题降维阶段完全不知道聚类的存在它优化的是重构误差保留下来的低维结构未必是对分群最有区分度的结构。于是引出下一个问题——能不能让降维和聚类互相看着对方调整。1.2 VAE给聚类提供的不只是一个降维器自编码器AE压出来的隐向量是确定性的一个点VAE 压出来的是一个分布 q(z|x)通常建模成对角高斯输出均值 μ 和对数方差 log σ²。这个差别看起来只是多了一个头实际影响很大。第一概率化的隐空间让“簇”可以用概率密度来描述簇的形状可以不是球而是任意的高斯椭球甚至可以各自有不同的体积和朝向。第二有了显式的先验分布模型天然具备生成能力你可以从某个簇的高斯里采样再解码看看这个簇到底对应什么样的样本这对做业务解释非常有用。第三也是我认为最关键的一点q(z|x) 的存在让“软分配”变得自然——一个样本属于各个簇的概率可以严格地从后验密度算出来而不是像 K-Means 那样硬性划到最近中心。代价当然也有。VAE 训练里那个 KL 项是个磨人的东西权重没调好就容易出现后验坍塌编码器干脆忽略输入把 q(z|x) 退化成先验隐空间完全不带信息聚类自然一塌糊涂。后面第 4 节会专门讲怎么处理。1.3 GMM为什么刚好是隐空间里那个“合适的裁判”在隐空间里做聚类最直接的做法是 K-Means但它只给你硬标签和球状簇。换成 GMM你得到的是混合权重 π、每个成分的均值 μ_k 和协方差 Σ_k一个样本属于第 k 个簇的后验概率可以按贝叶斯公式算出来。这不只是“更精细”的问题而是参数估计的路径变了K-Means 靠距离分配再更新中心是一种交替优化的启发式GMM 靠期望最大化每一步都在抬高似然下界理论上有单调收敛保证。更妙的是VAE 的 ELBO 里本来就有一项 KL(q(z|x) || p(z))如果把先验 p(z) 从标准正态 N(0, I) 换成一个 GMM那么“让隐分布贴近先验”这个动作本质上就等价于“让隐向量聚到混合成分的高斯核附近”。原来那个纯正则化的 KL 项摇身一变成了聚类驱动力。这就是 VaDE 这一脉模型最漂亮的地方它没有外加一个聚类损失去和重构损失打架而是把聚类直接写进了生成模型的结构里。2. 模型骨架拆解从ELBO推到联合训练目标2.1 用GMM替换标准正态先验先把记号立清楚。输入 x隐变量 z簇指示变量 c 取 1 到 K。生成过程是先按 π 抽一个簇 c再从 N(μ_c, Σ_c) 里抽 z最后由解码器生成 x。在标准 VAE 里先验固定为 p(z)N(0,I)而在 GMM 先验的版本里p(z) Σ_k π_k N(z | μ_k, Σ_k)簇参数是可学习的。对应的变分下界对 q(z,c|x) 做因子化分解 q(z|x)q(c|x) 之后可以写成三项重构项 E[log p(x|z)]让解码器能还原样本隐变量项 -E_{q(c|x)}[KL(q(z|x) || p(z|c))]让每个样本的隐分布贴近它所属簇的高斯核簇先验项 -KL(q(c|x) || p(c))让簇的分配比例不要偏离混合权重太远。我第一次推这个式子的时候有点疑惑为什么 KL 项是对 c 取期望的形式理解方式是这样的——样本的簇归属本身是不确定的q(c|x) 给出一个软分配那么“这个样本的隐分布该贴近哪个高斯核”就应该按这个软分配做加权平均。这样一来梯度可以顺畅地从聚类项回传到编码器整个模型端到端可训。2.2 三项损失各自在管什么权重怎么摆落到代码里三项损失的量纲差别很大这是实操中最容易翻车的地方。重构项如果按像素求 MSE 再求和量级可能是几百甚至上千KL 项通常在个位数到几十之间簇先验项更小。直接相加重构项会彻底主导梯度聚类项几乎不起作用训练到后期隐空间还是散的。我的处理方式分两层。第一层是对重构损失做归一化比如按输入维度取平均而不是求和图像数据用 BCE 按像素平均连续特征用 MSE 按维度平均把量级压到 1 附近。第二层是给 KL 相关项一个系数 β这个 β 不是固定的用 warm-up 从 0 线性升到目标值通常在前 20% 到 30% 的迭代里完成爬坡。理由很实在训练早期编码器还没学到有意义的表示此时强行让隐分布去贴高斯核只会把所有样本压到一个核里直接触发簇坍缩等重构稳定了再逐步加聚类约束隐空间才有足够的结构去支撑多峰分布。提示如果你的数据重构难度很高比如高分辨率图像、长序列β 的终值不要设得太高0.1 到 0.5 之间往往比 1.0 更稳。重构和聚类之间是存在真实张力的强行两者都拉满结果通常是隐空间被压扁。2.3 从KL项到责任度的闭式更新VaDE 系列有个很实用的性质在给定隐向量之后GMM 的参数π、μ、Σ有闭式解不需要用梯度去学。具体做法是先算出每个样本对每个簇的责任度 γ_ik它正比于 π_k 乘以第 k 个高斯在 z_i 处的密度然后对所有样本做加权统计π_k (1/N) Σ_i γ_ikμ_k Σ_i γ_ik z_i / Σ_i γ_ikΣ_k 用同样的加权二阶矩减去均值平方得到这套更新和 EM 算法的 M 步完全一致只是把样本换成了编码器输出的隐向量。实践里我更倾向于在两套方案里选小数据集几万条以内用闭式更新稳定、快、不需要给 GMM 调学习率大数据集必须分 batch闭式更新会有 batch 间抖动此时改用梯度下降学 GMM 参数或者对闭式结果做指数滑动平均EMA衰减系数 0.9 到 0.99。我一般默认用 EMA它相当于把闭式解的方差压下来训练曲线会平滑很多。另外协方差的选择值得单独说。全协方差矩阵参数是 K·D²隐维度 D 稍大就容易过拟合而且求逆的数值稳定性堪忧。绝大多数场景用对角协方差就够甚至用所有簇共享一个标量方差都行。我在一个客户分群项目里试过全协方差最终 NMI 反而比对角版本低了将近 3 个点原因就是参数太多簇边界被噪声带跑。3. 实操落地一套能直接复现的训练流程3.1 数据预处理别小看这一步聚类对尺度极其敏感这句话听起来像废话但真正踩过坑的人才知道它有多致命。如果一个特征取值范围是 0 到 10000另一个是 0 到 1编码器在前几层几乎只会关注第一个特征隐空间被它单方面决定。我的标准动作是连续特征先做分位数截断1% 和 99% 分位把长尾极值压掉再做 z-score 标准化偏态严重的特征先取对数再标准化。类别特征要么做 one-hot类别数少要么做 embedding类别数多不要直接 LabelEncode 后当连续值喂进去那等于人为制造了一个虚假的序关系。缺失值也需要提前想清楚。用均值填充会把大量样本堆到分布中心形成一个大而假的簇这在聚类任务里比在监督任务里危害更大因为模型会把这个假簇当成真实结构去拟合。我通常的做法是缺失率低于 5% 的特征做填充并额外加一个缺失指示列缺失率超过 30% 的特征直接丢弃中间地带的特征视业务重要性决定。这个经验是从一次失败的分群里换来的——那次没加缺失指示列结果最大的簇几乎全是“某几个字段缺失”的样本业务方一看就摇头。3.2 网络结构与隐空间维度结构上不必追求复杂。表格数据两到三层全连接每层 256 到 512 单元激活用 ReLU 或 GELU 足够图像数据用几层卷积加反卷积序列数据用一维卷积或轻量 Transformer 编码器。真正需要花心思的是隐维度 D 的选择它直接决定了 GMM 能不能把簇分开。我的经验取值是 D 在 8 到 32 之间常用 10。太小的隐空间会强行把不同簇重叠在一起重构损失降不下去太大则隐空间过于宽松所有样本都变得“各自独立”GMM 找不到明显的多峰结构。有个粗略的判断方法训练完第一阶段之后画一下隐空间前两个主成分的散点图如果肉眼能看到分离的趋势说明 D 选得合适如果所有点糊成一团先试着减小 D 或者加大 β别急着调聚类数。关于簇数 K如果业务上没有先验可以用 BIC 或者轮廓系数在预训练隐空间上做一轮扫描K 从 2 扫到 20看指标曲线的拐点。注意深度聚类里的 K 一旦定下就不好中途改因为 GMM 参数是跟着 K 走的改 K 等于重训。3.3 三阶段训练预训练、初始化、联合微调这是整套流程里最影响成败的部分。一次性端到端训练几乎必然失败标准做法是拆成三段。第一阶段纯 VAE 预训练。先不管聚类只用重构损失加一个带 warm-up 的 KL 项训练到重构损失收敛。这一步的目的是让编码器学会一个有意义、连续、光滑的隐空间。有个细节值得提有些实现会先用纯 AE不加 KL预训练再切到 VAE理由是纯 AE 的隐空间更“舒展”KMeans 初始化更准。我两种都试过纯 AE 初始化在图像上确实略好一点但切换时要注意把编码器的方差头重新初始化得小一些否则一上来 KL 项会爆。第二阶段GMM 初始化。把训练集全部过一遍编码器取 μ 作为隐向量注意用均值而不是采样减少随机性在隐向量上跑一次标准 GMM 或者 K-Means把得到的中心、协方差、权重填进 GMM 先验。这一步相当于给联合训练一个靠谱的起点能省掉大量迭代也能明显降低簇坍缩的概率。第三阶段联合微调。打开完整的三项损失用很小的学习率预训练学习率的十分之一到五分之一继续训练。GMM 参数用闭式更新加 EMA编码器和解码器用梯度下降。这一段通常只需要预训练迭代数的 10% 到 20%训太久反而会过拟合到当前的软分配上。3.4 核心代码实现下面这套代码我用来做表格数据的原型验证结构上做了简化但关键部分都在。先定义编码器和解码器import math import torch import torch.nn as nn import torch.nn.functional as F class Encoder(nn.Module): def __init__(self, x_dim, h_dim, z_dim): super().__init__() self.backbone nn.Sequential( nn.Linear(x_dim, h_dim), nn.GELU(), nn.Linear(h_dim, h_dim), nn.GELU(), ) self.head_mu nn.Linear(h_dim, z_dim) self.head_logvar nn.Linear(h_dim, z_dim) def forward(self, x): h self.backbone(x) # 把 logvar 夹在合理区间避免 exp 溢出或方差趋零 return self.head_mu(h), self.head_logvar(h).clamp(-8.0, 6.0) class Decoder(nn.Module): def __init__(self, z_dim, h_dim, x_dim): super().__init__() self.net nn.Sequential( nn.Linear(z_dim, h_dim), nn.GELU(), nn.Linear(h_dim, h_dim), nn.GELU(), nn.Linear(h_dim, x_dim), ) def forward(self, z): return self.net(z)GMM 先验这一块把 π、μ、log σ² 都做成可学习参数但实际由闭式更新去写class GMMPrior(nn.Module): def __init__(self, n_clusters, z_dim): super().__init__() self.log_pi nn.Parameter(torch.zeros(n_clusters)) self.mu nn.Parameter(torch.randn(n_clusters, z_dim) * 0.1) self.log_var nn.Parameter(torch.zeros(n_clusters, z_dim)) torch.no_grad() def m_step(self, z, gamma, momentum0.9, eps1e-4): # z: [B, D] gamma: [B, K] nk gamma.sum(0).clamp_min(1e-3) # 每个簇的有效样本数 pi nk / nk.sum() mu (gamma.t() z) / nk[:, None] # 加权二阶矩减均值平方得到对角方差 var (gamma.t() (z ** 2)) / nk[:, None] - mu ** 2 var var.clamp_min(eps) self.log_pi.mul_(momentum).add_((1 - momentum) * torch.log(pi), alpha1.0) self.mu.mul_(momentum).add_((1 - momentum) * mu, alpha1.0) self.log_var.mul_(momentum).add_((1 - momentum) * torch.log(var), alpha1.0)这里用了 EMA 而不是直接覆盖momentum越大越平滑。注意log_pi用 EMA 之前不需要再归一化因为 softmax 会自动处理。然后是计算责任度和损失def log_gaussian(z, mu, log_var): # z: [B, D] mu/log_var: [K, D] - [B, K] inv_var torch.exp(-log_var) diff z[:, None, :] - mu[None, :, :] return -0.5 * (diff ** 2 * inv_var[None] log_var[None] math.log(2 * math.pi)).sum(-1) def vade_loss(x, x_rec, mu, logvar, gmm, beta1.0): # 1) 重构损失按维度取平均把量级压到 1 附近 rec F.mse_loss(x_rec, x, reductionnone).mean(-1) # 2) 重参数化采样 std torch.exp(0.5 * logvar) z mu std * torch.randn_like(std) # 3) 责任度 q(c|z)用 log-sum-exp 保持数值稳定 log_joint log_gaussian(z, gmm.mu, gmm.log_var) F.log_softmax(gmm.log_pi, 0)[None] log_gamma F.log_softmax(log_joint, dim-1) gamma log_gamma.exp() # 4) KL(q(z|x) || p(z|c))对 c 取期望 var torch.exp(logvar) diff z[:, None, :] - gmm.mu[None] kl_z -0.5 * (1 logvar[None] - gmm.log_var[None] - (diff ** 2 var[:, None, :]) / torch.exp(gmm.log_var[None])) kl_z (gamma * kl_z.sum(-1)).sum(-1) # 5) KL(q(c|x) || p(c)) kl_c (gamma * (log_gamma - F.log_softmax(gmm.log_pi, 0)[None])).sum(-1) return (rec beta * (kl_z kl_c)).mean(), gamma, z训练循环按三阶段走KL warm-up 用一个简单的线性爬坡def train(model_enc, model_dec, gmm, loader, epochs_pre50, epochs_ft20, devicecuda): params list(model_enc.parameters()) list(model_dec.parameters()) opt torch.optim.Adam(params, lr1e-3) for ep in range(epochs_pre): for x, in loader: x x.to(device) mu, logvar model_enc(x) std torch.exp(0.5 * logvar) z mu std * torch.randn_like(std) x_rec model_dec(z) rec F.mse_loss(x_rec, x, reductionnone).mean(-1).mean() kl (-0.5 * (1 logvar - mu ** 2 - logvar.exp()).sum(-1)).mean() beta min(1.0, ep / (0.3 * epochs_pre)) loss rec beta * kl opt.zero_grad(); loss.backward() torch.nn.utils.clip_grad_norm_(params, 5.0) opt.step() # ---- 第二阶段在全部隐均值上初始化 GMM ---- zs [] with torch.no_grad(): for x, in loader: mu, _ model_enc(x.to(device)) zs.append(mu) zs torch.cat(zs, 0) from sklearn.mixture import GaussianMixture gm GaussianMixture(n_componentsgmm.mu.shape[0], covariance_typediag).fit(zs.cpu().numpy()) with torch.no_grad(): gmm.log_pi.copy_(torch.log(torch.tensor(gm.weights_, dtypetorch.float32, devicedevice) 1e-8)) gmm.mu.copy_(torch.tensor(gm.means_, dtypetorch.float32, devicedevice)) gmm.log_var.copy_(torch.log(torch.tensor(gm.covariances_, dtypetorch.float32, devicedevice) 1e-4)) # ---- 第三阶段联合微调 ---- opt torch.optim.Adam(params, lr2e-4) for ep in range(epochs_ft): for x, in loader: x x.to(device) mu, logvar model_enc(x) std torch.exp(0.5 * logvar) z mu std * torch.randn_like(std) x_rec model_dec(z) loss, gamma, _ vade_loss(x, x_rec, mu, logvar, gmm, beta0.5) opt.zero_grad(); loss.backward() torch.nn.utils.clip_grad_norm_(params, 5.0) opt.step() gmm.m_step(z.detach(), gamma.detach(), momentum0.9)预测阶段直接取责任度最大的簇同时保留完整的概率向量供后续分析torch.no_grad() def predict(enc, gmm, loader): labels, probs [], [] for x, in loader: mu, _ enc(x.cuda()) log_joint log_gaussian(mu, gmm.mu, gmm.log_var) F.log_softmax(gmm.log_pi, 0)[None] p F.softmax(log_joint, dim-1) probs.append(p) labels.append(p.argmax(-1)) return torch.cat(labels), torch.cat(probs)3.5 关键超参数与取值参考调参的时候我一般先固定网络结构只动下面这几个。这张表是我在不同数据集上反复试出来的经验区间可以直接当起点超参数经验取值影响与调整方向隐维度 D8 ~ 32常取 10太小簇重叠太大簇松散看 PCA 散点图判断簇数 K业务先验或 BIC 扫描定下后不要中途改改 K 等于重训隐藏层宽度256 ~ 512数据量大可以加宽样本少于 5000 时收窄防过拟合预训练轮数损失的拐点后再多跑 20%预训练不足GMM 初始化会很糟微调轮数预训练的 10% ~ 20%训太久会过拟合当前软分配重建损失系数1.0本身已归一化归一化是关键别用求和KL 系数 β0.1 ~ 1.0warm-up 到终值重构难的任务取小值batch size256 ~ 1024用闭式更新时尽量大方差估计才稳GMM EMA 衰减0.9 ~ 0.99抖动大就调大方差下限1e-4防止某簇方差趋零导致数值爆炸还有一个隐性的重要参数经常被忽略随机种子。深度聚类的损失曲面非凸不同种子跑出来的 ACC 能差 5 到 10 个点。我的做法是固定跑 5 个种子用完整数据的平均 ELBO 挑最优的那个而不是用聚类指标挑因为用聚类指标挑等于在无标签数据上做监督选择容易过拟合。4. 避坑实录从簇坍缩到后验坍塌的排查路径4.1 簇坍缩所有样本挤进同一个簇这是最高频的故障表现是训练几十轮之后某个簇吞掉了 90% 以上的样本其余簇只剩零星几个点。原因通常有三类。第一类是初始化太差GMM 的初始中心全挤在隐空间的同一片区域责任度算出来天然偏向某一个簇。第二类是 β 升得太快编码器还没学会区分样本就被强行压向高斯核。第三类是簇先验项没起作用q(c|x) 完全跑偏也没人管。排查顺序我一般是这样的先打印每个簇的有效样本数 nk看是不是极度不均再把 β 的 warm-up 拉长一倍重新跑还不行的话检查 GMM 初始化时用的是不是 μ 而不是采样值——用采样值初始化方差大的样本会把初始中心拉偏。实在稳不住可以临时加一个均衡正则形如 KL(平均责任度 || 均匀分布)系数取 0.01 到 0.05等训练稳定了再撤掉。这招有点“作弊”的意味但在簇数明显小于真实簇数时确实管用。注意有些簇坍缩是数据本身的问题。如果真实数据里确实存在一个占比 80% 的主群体模型把它单独拎出来是对的不要一看不均衡就去改模型。判断方法是看这个大连的簇内部方差是否合理、解码出来的样本是否同质而不是只看比例。4.2 后验坍塌KL 掉到接近零后验坍塌的表现很典型KL 项在训练早期迅速掉到 0.01 以下重构损失看着还不错但隐空间里所有样本挤成一团聚类指标惨不忍睹。本质是解码器太强了它发现直接忽略 z、靠自身的学习能力也能把重构做下来于是编码器输出的 q(z|x) 就被优化成了先验隐变量彻底失效。处理手段有几个我一般组合使用。KL warm-up 是第一道防线把 β 从 0 缓慢升上去给编码器争取学习时间。第二道是 free bits给每个维度的 KL 设一个下限比如每维 0.05 nat低于这个值就不再产生梯度逼着模型至少用上这么多信息量。第三道是削弱解码器减小它的层数或宽度或者在解码器输入上加一点 dropout。第四道是针对性的如果你的重构目标是连续值且已经标准化试试把重构损失从 MSE 换成高斯负对数似然让解码器同时输出方差这会给模型一个更合理的概率目标。顺带提一句后验坍塌在隐维度很小比如 D2 或 3的时候反而不容易出现因为信息瓶颈本身就很紧编码器不得不把关键信息塞进去。所以如果你的 D 只有个位数而 KL 还是掉到零那问题多半不在模型结构而在数据预处理或者学习率。4.3 训练震荡与数值溢出联合微调阶段如果出现损失剧烈震荡甚至突然变成 NaN八成是数值问题。最常见的来源是方差太小某个簇的 σ² 在 EMA 更新中一路衰减到 1e-8 以下算高斯密度时 exp(-log_var) 直接溢出。所以log_var一定要 clamp下限设 1e-4 到 1e-3别嫌它影响精度稳定的训练比那点精度重要得多。第二个来源是责任度计算里的 log-sum-exp。如果你直接用概率相乘再归一化K 稍微大一点就会出现全零或者全 inf务必在 log 空间里算用F.log_softmax处理。第三个来源是梯度爆炸加梯度裁剪范数 5.0基本能解决如果是图像数据还要注意解码器最后一层的激活函数选择用 sigmoid 配 BCE、不加激活配 MSE别搞混。还有一个容易被忽略的点batch 内有簇空的时候。如果某个簇在当前 batch 里一个样本都没分到nk 会趋近于 0闭式更新算出来的均值是个 0/0。代码里clamp_min(1e-3)就是干这个用的同时可以给空簇加一个很小的伪计数或者干脆跳过这个簇的参数更新让它保持上一轮的值。4.4 评价指标怎么选以及一个容易自欺欺人的陷阱有标签的时候用 ACC需要匈牙利算法做标签匹配、NMI、ARI这三个我一般一起报。ACC 直观但只反映最好的匹配NMI 对簇数不敏感ARI 对随机划分更严格。无标签的时候只能看 ELBO、重构误差和簇的均衡度这时候指标的解释力很弱我的建议是同时观察隐空间的二维可视化作为定性判断的补充。这里有个陷阱必须提醒不要在没有验证集的情况下反复用聚类指标调超参数。聚类指标本质上是外部信息你用它调参就等于偷偷引入了标签报出来的数字会虚高。更靠谱的做法是留出一部分样本只看 ELBO或者用 bootstrap 重采样看划分的稳定性——把数据重采样多次计算不同次划分之间的 ARI稳定性高的模型通常泛化性也更好。我做过对比稳定性指标和真实 ACC 的相关性能到 0.7 左右比单纯看 ELBO 可靠得多。5. 选型对照这套方案适合什么不适合什么5.1 和其他深度聚类方案的横向对比把常见的几种放一起看选择逻辑会清楚很多。下表是我按实际使用体验整理的对照方案核心机制是否需要预训练能否生成样本主要短板原始数据 KMeans欧氏距离交替优化否否高维失效只能球形簇AE KMeans非线性降维后聚类是否降维与聚类目标脱节DEC / IDEC自训练 KL 锐化目标分布是否目标分布构造靠经验无似然解释VaDEGMM 作为隐空间先验是是依赖好的 GMM 初始化DGG隐空间 GMM 分 batch 闭式更新是是batch 小的时候参数估计抖本文这套VAE GMM 先验 EMA 更新是是超参数较多需要三阶段DEC 系列的优势是收敛快、实现简单缺点是它那个目标分布 p 是按当前软分配构造出来的本质上是一种自洽的锐化没有概率模型支撑遇到簇数估计错误时会一路错下去而且毫无提示。VaDE 这一脉的优势在于有完整的似然你能从 ELBO 的变化看出模型是不是“学到了东西”出问题时有明确的排查方向。5.2 什么数据量级、什么场景下值得上这套模型我的判断标准很直接样本量在几千到几百万、特征维度超过 50 维、或者数据结构明显是多峰的这套方案的优势才明显。如果数据只有几百条、十几维特征老老实实跑 GMM 或者层次聚类效果未必差还省掉一堆调参的功夫。如果数据是纯类别特征且维度很低先做频繁项集或者简单的共现分析收益可能比深度聚类更大。在场景上我踩过效果最好的几类用户行为分群特征维度高、群体结构明显、异常检测里的正常模式归纳GMM 的密度可以直接用来算异常分、生物或者化学的高维测量数据分型、以及图像数据的无监督预分组。效果一般的场景是文本嵌入的聚类——不是模型不行而是预训练句向量本身的几何结构已经被对比学习重塑过直接在上面对角高斯拟合簇形假设不太匹配这时候用球形 K-Means 或者谱聚类反而更稳。5.3 几个可以继续往下做的扩展方向如果你已经把这套跑通了有几个方向值得再往前推一步我自己也在陆续试。一个是把对角协方差换成低秩分解的形式Σ_k W_k W_k^T diag(d)参数量从 K·D² 降到 K·D·r既保留了一定的形状灵活性又不至于过拟合在隐维度较大时收益明显。另一个是把重构目标从逐点 MSE 换成对比式的重构让解码器不仅要还原自身还要和同 batch 内的其他样本拉开距离这在簇间差异细微的数据上能提升不少区分度。还有一个我最近在琢磨的方向把 GMM 的簇数和结构做成非参数化的用狄利克雷过程先验替代固定的 K让模型自己决定需要几个簇训完之后把权重接近零的成分剪掉。理论上很优雅实践里的坑是截断的簇数上限要设得足够大而且剪枝之后需要重新微调一轮否则隐空间的结构和剪枝后的簇数对不上。这条路我还没在真实项目里跑通全流程暂时只建议作为探索方向生产环境还是用固定 K 稳当。顺带记一个很实用的小技巧训完之后别急着交付簇标签先对每个簇做一次“解码重构探针”——从该簇的高斯里采若干个 z解码成样本看看它们长什么样。这一步经常能发现模型把两个不相干的群体合并了或者某个簇其实是缺失值堆出来的。可视化加上这个探针比看任何指标都更能帮你判断结果能不能用。