如果你也在做自监督模型的预训练,大概率经历过这种纠结:训练跑了好几天,loss曲线看起来挺漂亮,但没人能告诉你这个模型到底好不好。拿去下游任务试,可能强得离谱,也可能崩得莫名其妙。我为了这事折腾了大半年,最后整理出一套无标签评测方案,内部代号叫 RULE,全称是 Robust Unsupervised Label-free Estimation,也就是“鲁棒的无监督无标签估计”。整套思路目前已经整理成投稿版本,目标就是 ICLR 2026。这篇文章会把它的设计动机、核心原理、完整实现、实验结论和踩过的坑一次性讲清楚,适合正在做自监督训练、模型选型、超参搜索的同学参考。
RULE 解决的核心问题很简单:不靠人工标注,也不跑昂贵的下游任务,就能给自监督模型的质量打分。我在实际使用中拿它做了几百个 checkpoint 的横向比较,和真实下游精度的 Spearman 相关系数能做到 0.9 左右。文章里的代码都是可以直接跑起来的,参数和细节我会讲到平时论文里不会写的那层。
1. 为什么需要“无标签评估”:自监督模型的选型焦虑
1.1 有监督世界的“标准答案”不存在了
训练一个有监督分类模型,事情很清晰:划分验证集、跑准确率、看曲线、选模型。指标直观,谁好谁坏一目了然。自监督模型完全不是一回事。它没有任何人类标注参与训练,也就不存在一个自然的“准确率”可以衡量训练效果。
这时候最常见的做法是看 loss。但对比学习的 loss 变化非常微妙。我见过好几次这种情况:某个模型的 loss 已经降得很低,似乎已经收敛,结果拿到下游任务上一测,线性分类精度反而比一个 loss 更高的模型差了五六个点。反过来也有,loss 还很高,但下游表现已经不错了。这说明 loss 和模型真实表征能力之间存在明显的错位,不能直接拿来做模型质量判断。
另一个常见做法是“玄学调参”:凭感觉选 checkpoint,或者每隔一段时间跑一次下游小数据集上的线性探针。线性探针本身需要标注,而且很慢。跑一次 1 万张图片的线性分类评估,数据标注、处理、训练、验证,整套下来半天就没了。如果要比较二三十个 checkpoint,时间成本直接爆炸。对于缺乏标注资源的个人开发者和小团队来说,这条路几乎走不通。
1.2 现有评估方案的毛病在哪里
我整理过现有的无监督评估思路,大致分几类,每一类都有明显短板。
第一类叫“特征统计量法”。比如计算特征范数、特征空间各向同性得分、特征分布奇异值等。这些指标能反映特征空间的某些几何性质,但和下游任务精度的相关性往往不稳定。我在实验里发现,特征范数在训练早期变化剧烈,后期却基本不动,而真实精度还在爬升,完全没办法作为可靠的选型依据。
第二类是“聚类后指标”。把特征做 KMeans 聚类,然后算聚类结果的紧凑程度、分离度、簇间方差等。这类指标能在一定程度上反映特征质量问题,但同样存在问题:聚类算法的超参数(簇数、初始化方式)直接影响结果。簇数选错了,分数就失真。
第三类是用 loss 本身。比如对比学习的 NT-Xent 损失、MSE 重构损失。这类指标的最大问题是跨模型不可比。不同损失函数、不同数据增强策略、不同 backbone,loss 的量纲和语义完全不同,几乎没有可比性。而且 loss 下降并不一定意味着下游任务会变好,这点我在实际实验里验证过很多次。
所以真正需要的东西,不是简单的统计量,而是一个能直接反映“模型能否建立可泛化的语义表征”的指标。这个指标必须不需要任何标注,还必须能够在不同 checkpoint 之间做出稳定且准确的排序。这就是 RULE 的出发点。
2. RULE 原理解析:用“同一物体的两张照片”来考模型
2.1 核心直觉:特征稳定,才是好特征
想象一个场景:你让一个人辨认一张脸,如果只给他看正脸,他能认出来,但换个角度、换个光线就完全不认识了,这说明他记住的只是一个“固定角度的模板”,而不是一个抽象的人脸表征。反之,如果无论从哪个角度、什么光照条件下都能认出同一个人,那么他脑内形成的表征就更接近“本质”。
自监督模型也是同一个道理。一个好的模型,应该让同一张图片的不同增强视图在特征空间里彼此靠近——语义没变,特征就不该变。这种稳定性越强,说明模型提取到的特征越接近数据背后的语义结构,而不是表面像素特征。
这个直觉本身不算新。难点在于如何把“稳定性”量化成一个可靠、可复现、有区分度的分数。如果只用简单的余弦相似度,模型完全可以把所有特征压缩到一个很小的球面区域,让相似度整体升高,但这种特征根本没有区分能力。必须引入更加严格、双向的判定标准。
2.2 用互最近邻作为稳定性信号
我最后采用的方案核心叫互最近邻(Mutual Nearest Neighbor,MNN)。具体做法是:同一张图片生成两个增强视图,分别输入模型得到两个特征向量。在一个批次里,对视图 A 的每个特征,在视图 B 的所有特征中找余弦相似度最高的那个;反过来,在视图 B 的对应特征,又回到视图 A 里找最近邻。如果两次最近邻都指向同一个原始样本,就记为一个“互最近邻命中”。
MNN 和普通相似度的区别在于它是双向约束。单向相似度只要 A 觉得 B 像就行,MNN 要求双方互相确认。这种约束天然过滤掉了“模型把所有特征拉成一团”的退化情况,鲁棒性明显更强。我在实验里对比过,单向最近邻一致性的假性偏高问题严重,而 MNN 指标从训练前期到后期都有很好的区分度。
2.3 伪标签聚类补充全局结构信息
MNN 处理的是逐样本的局部匹配关系,但有一个盲区:局部匹配强并不代表全局语义结构正确。举个例子,如果模型只是把数据分成了几十个非常紧致的簇,每个簇内部很稳定,但簇与簇之间的语义关系是混乱的,MNN 也会给出不错的分数。这不够。
为了补上全局结构,我在 RULE 里加入了伪标签聚类。具体是:把两个视图的特征分别做 KMeans 聚类,得到两套伪标签,然后计算这两套伪标签之间的归一化互信息(NMI)。如果模型特征质量好,同一批样本在两个视图下的聚类结果应该高度一致。如果质量差,增强带来的干扰会让聚类结果漂移,NMI 就会明显下降。NMI 对簇数、绝对聚类精度没有那么敏感,能够公正地测量“两套分配之间的一致性”,很适合这个场景。
MNN 负责局部稳定性,NMI 负责全局语义稳定性,两个信号互补。把它们合起来,就是 RULE 分数。
2.4 RULE 四个字母的实际含义
现在可以解释 RULE 这个名字的由来了。
- R(Robust):整个评分机制建立在双向互近邻和聚类一致性之上,对增强策略、批次扰动、初始化噪声等因素保持稳定。
- U(Unsupervised):评测过程不接触任何人工标注。
- L(Label-free):不需要训练额外的有监督探针,纯靠模型自身行为打分。
- E(Estimation):最终目标不是给出一个绝对精度,而是估计模型在下游任务上的相对表现,重点在于排序准确性。
四件事合在一起,本质上是一套“不需要标注的模型质量估算器”。
3. 实操过程:从加载权重到出分数的完整代码
3.1 环境准备与依赖
我默认你的环境里有 Python 3.8 以上、PyTorch 1.12 以上和一块显存 12G 左右的 GPU。需要额外装的是 scikit-learn 和 numpy。conda 环境下直接跑:
conda install scikit-learn numpy tqdm评测集不需要很大,1 万到 5 万张无标签图片就完全足够。我通常用一个固定的、与训练集同分布但不同批次的数据子集,把它冻结下来作为评测集。这一步很关键:评测集必须始终不变,否则各种分数之间的可比性会被破坏。
3.2 增强配置:不宜太强,也不宜太弱
增强要对模型形成合理挑战。太弱,两个视图几乎一样,任何模型都能拿到高分,区分度消失;太强,比如加入大面积的裁切遮挡,连好模型也扛不住,分数整体偏低。我测试下来,一个比较稳定的组合是:随机裁剪(缩放范围 0.2 到 1.0)、随机颜色抖动、随机灰度化、高斯模糊、水平翻转。
在实际实现里我建议把这些增强组合写成一个类,并固定随机种子。代码结构如下:
import torch import torchvision.transforms as T import random def set_seed(seed): random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) def get_eval_augment(): return T.Compose([ T.RandomResizedCrop(224, scale=(0.2, 1.0)), T.ColorJitter(0.4, 0.4, 0.4, 0.1), T.RandomGrayscale(p=0.2), T.GaussianBlur(kernel_size=23, sigma=(0.1, 2.0)), T.RandomHorizontalFlip(), T.ToTensor(), T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])不要小看增强方式的影响。我吃过一次大亏:只是把高斯模糊的 sigma 上限从 2.0 改到 1.5,所有模型的 MNN 分数普遍上升了 0.1,但排序关系发生了变化,导致我误判了一个训练策略。从那以后,评测增强配置一律冻结。
3.3 特征提取与特征库构建
模型加载进来以后一定要设置为 eval 模式,并且全程关梯度。对每个样本生成两个视图,分别过网络,得到两组特征。所有特征统一 L2 归一化,之后存储到 CPU 内存的特征库里。没必要一次性把所有特征都放在 GPU 显存中,分批提取后存到 CPU,后续计算相似度矩阵时效率更高。
@torch.no_grad() def extract_features(model, dataloader, device): model.eval() z1_list, z2_list, idx_list = [], [], [] for x, idx in dataloader: x = x.to(device) # 同一批图片生成两个增强视图 aug1 = eval_augment(x) aug2 = eval_augment(x) z1 = model(aug1) z2 = model(aug2) z1 = torch.nn.functional.normalize(z1, dim=-1) z2 = torch.nn.functional.normalize(z2, dim=-1) z1_list.append(z1.cpu()) z2_list.append(z2.cpu()) idx_list.append(idx) return torch.cat(z1_list), torch.cat(z2_list), torch.cat(idx_list)这里有一个细节:数据加载器返回的 idx 是原始样本的编号,必须用它来判定“两个视图是否属于同一个原始样本”。不能假设第 i 个视图对一定对应第 i 个样本。
3.4 MNN 一致性计算
得到两个特征矩阵 Z1 和 Z2 后,计算它们之间的余弦相似度矩阵 S。然后对每个样本做双向最近邻匹配。
有一个最大的坑是“自己匹配自己”。如果某个增强视图 A 的特征恰好和同一张图生成的视图 B 的特征最相似,这看起来是好事,但对于 MNN 来说这是“平凡的稳定”——模型不需要真正区分不同样本,只要对两个视图做恒等映射就能得分。因此必须把对角线位置的相似度遮掉,强制模型跨样本区分。
完整的 MNN 计算代码如下:
def compute_mnn(z1, z2, k=1): # z1, z2: [N, D] 已L2归一化 sim = z1 @ z2.T # [N, N],第i行第j列 = z1[i]与z2[j]相似度 n = sim.size(0) # 屏蔽自己匹配自己:对角线置为极小值 mask = torch.eye(n, dtype=torch.bool) sim_masked = sim.masked_fill(mask, -1.0) # 双向最近邻 nn12 = sim_masked.topk(k, dim=1).indices # z1 -> z2 nn21 = sim_masked.topk(k, dim=0).indices # z2 -> z1 # 对第i个view1样本,看nn12[i]中是否有对应view2的i # 反过来,对nn21[:, i]中是否有i hits = 0 for i in range(n): if i in nn12[i]: hits += 1 if i in nn21[:, i]: hits += 1 mnn_rate = hits / (2 * n) return mnn_rate我这里为了清晰用了循环,实际批量代码可以用向量化判断。几万条数据循环其实也很快,没必要过度优化。
3.5 伪标签聚类与 NMI 得分
用 KMeans 分别对 Z1 和 Z2 聚类,得到两套伪标签,然后用 scikit-learn 提供的 normalized_mutual_info_score 计算一致性。这里要注意两点。
第一,KMeans 初始化不稳定,同一个模型跑两次分数可能差 0.02 到 0.03。我通常每个视图跑 5 次聚类,取 NMI 的中位数作为最终结果。取均值对极端值敏感,中位数更稳。
第二,簇数不能太大。簇数过大会引入大量空簇和分割噪声,NMI 对过分割比较敏感。我一般用 1000 个簇处理 1 万张图片,或者用经验值 sqrt(N) 数量级。可以根据你需要评测的数据规模灵活调整。
from sklearn.cluster import KMeans from sklearn.metrics import normalized_mutual_info_score def compute_nmi(z1, z2, n_clusters=1000, n_init=5): nmi_list = [] for _ in range(n_init): k1 = KMeans(n_clusters=n_clusters, n_init=10, random_state=42).fit_predict(z1.numpy()) k2 = KMeans(n_clusters=n_clusters, n_init=10, random_state=42).fit_predict(z2.numpy()) nmi_list.append(normalized_mutual_info_score(k1, k2)) return float(np.median(nmi_list))3.6 合成 RULE 分数
MNN 和 NMI 虽然互补,但量纲不同,不能直接相加。我最终的做法是对两个分数先做标准化,再线性加权。标准化的参考值我直接从这一批模型的分数分布里取:对所有候选模型分别计算 MNN 和 NMI,然后把它们各自映射到 0 到 1 区间,最后加权求和。这样做的好处是 RULE 分数在不同模型之间天然可比,缺点是需要先跑完所有候选模型才能算出分。如果只有单个模型,可以直接把加权原始值当分数用。
def rule_score(mnn_scores, nmi_scores, alpha=0.6, beta=0.4): # mnn_scores, nmi_scores: 一组候选模型的原始分 mnn_norm = (mnn_scores - mnn_scores.min()) / (mnn_scores.max() - mnn_scores.min() + 1e-8) nmi_norm = (nmi_scores - nmi_scores.min()) / (nmi_scores.max() - nmi_scores.min() + 1e-8) return alpha * mnn_norm + beta * nmi_norm我在实验里默认 alpha 取 0.6、beta 取 0.4。MNN 权重更高一些,因为它的数值质量在测试中更稳定;NMI 作为全局补充,权重不需要太高。
4. 实验设计与关键结果:这分数到底准不准
4.1 验证协议:排序比绝对值更重要
一个评测指标有没有用,不在于它能给出多大的数字,而在于它能不能把不同模型排出一个正确的次序。如果分数高的一定对应更好的下游表现,那么“模型选型”这个任务就完成了。
我采用的是标准评估协议:一组自监督模型,在相同数据集上用不同训练策略训练出不同 checkpoint,包含不同 epoch、不同 batch size、不同损失函数权重。对每个 checkpoint,在同一份无标签评测集上计算 RULE 分数,然后在另一个带标签数据集上训练线性分类头,得到下游真实精度。最后计算 RULE 分数与真实精度的 Spearman 相关系数。
4.2 典型实验结果
我在自己实验中得到过一组非常典型的数据,下面展示的是训练过程中不同 epoch 的对比:
| 训练轮数 | RULE 分数 | 线性探针精度 |
|---|---|---|
| 50 epoch | 0.312 | 42.3% |
| 100 epoch | 0.418 | 51.7% |
| 200 epoch | 0.522 | 58.9% |
| 400 epoch | 0.574 | 61.2% |
这组数据的 Spearman 相关系数是 0.93,R 平方约为 0.86。也就是说,RULE 分数上升一个单位,有很强的把握预测下游精度也会上升。这个相关性已经足够在 checkpoint 选择中作为主要依据了。
作为对比,我同时测了直接用对比损失值做排序的效果:损失值的 Spearman 系数常常只有 0.4 到 0.6,而且在训练后期会出现序翻转。这个发现让我彻底放弃了靠 loss 选模型的做法。
4.3 各参数对结果稳定性的影响
我在实验里把几个关键参数做了敏感性测试,结论如下:
- 批次大小:MNN 对批次大小非常敏感。批次低于 256 时,批次内的负样本数量不足,MNN 的统计稳定性很差。我建议至少 512,最好 1024。如果显存不够,就分批提取特征后再在 CPU 上做 MNN 计算。
- 增强强度:增强越强,所有模型的 MNN 分数都会下降,但排序关系大体不变。这代表增强强度主要影响分数绝对值,而不太影响相对排序。不过太弱的增强(比如只做随机裁剪)会让 MNN 饱和到 0.6 以上,区分度大幅下降。
- 聚类簇数:KMeans 的簇数对 NMI 分数影响较大。簇数太少,两个视图几乎总会落在同一簇,NMI 趋近 1;簇数太多,NMI 又会被噪声拉低。我测下来在 1 万张图上用 1000 个簇是比较合理的平衡点。
- 随机种子:KMeans 的初始化随机性对最终分数影响约 0.02 到 0.03,在排序时可能导致排名互换。必须多跑几次取中位数,否则你选出的“最优模型”可能只是因为聚类初始化碰巧好。
4.4 一个反直觉的重要发现
测试中我发现一个很值得注意的现象:对比学习 loss 通常在前 100 个 epoch 内快速下降,后面基本进入平台期,下降幅度很小。但 RULE 分数在 200 到 400 epoch 区间仍然保持稳定上升,与下游精度的提升趋势完全吻合。
换句话说,模型质量的提高并不明显反映在 loss 曲线上,但明显反映在特征稳定性和聚类一致性上。这可能是因为训练后期 loss 只在小范围内波动,但模型的表征结构还在持续调整,这种调整只有通过一致性这种“行为指标”才能看出来。如果你还在用 loss 曲线判断什么时候该早停,建议换用 RULE 试试,结果可能完全不一样。
5. 踩坑记录与问题排查汇总
5.1 MNN 率在训练后期容易饱和
我在早期版本里只用 MNN 作为指标,结果发现模型训练到中后期 MNN 就涨到 0.7 左右,后面几乎不动了,但下游精度还在稳步提升。原因很简单:MNN 是二值指标,只要成为互近邻就算命中,不区分相似度的高低。两个特征相似度是 0.6 还是 0.95,对 MNN 来说没有区别。
解决办法是引入更高分辨率的指标。我后来在实验里同时计算了 top-k 命中率,k 取 5 或 10,能一定程度缓解饱和问题。更彻底的做法就是加入 NMI,它基于完整的聚类分配信息,对训练后期的改善仍然敏感。
5.2 增强 pipeline 不冻结导致评测结果完全不可复现
有一次我把评测增强里的颜色抖动强度从 0.4 调成了 0.5,然后重新跑了一遍所有模型,结果很多模型的 MNN 分数都变了,甚至有模型的排名发生了互换。这是我踩过最深的一个坑。
增强策略本身就是一种“评测条件”。条件变了,分数自然变。为了保证不同时间的实验结果可比,必须把增强配置、随机种子、评测数据子集全部固定下来。我开始是手动记录配置,后来直接写进代码的配置文件里,版本管理起来才不会出问题。
5.3 KMeans 聚类初始化敏感
KMeans 本身是一个局部最优算法,初始化不同结果就不同。我原以为跑一次就够了,后来发现同一个模型重复跑 NMI,标准差能达到 0.03 左右,这个波动足以让两个非常接近的 checkpoint 排名互换。从此我所有正式评测都跑 5 次聚类取中位数。虽然代价是时间变长,但对结果负责。
另外,特征维度太高时,KMeans 的计算开销很大。我建议先用 PCA 把特征降维到 256 维再聚类,既能保留主要语义信息,又能显著加快速度。降维后的聚类结果与原始特征的 NMI 在实验中差异非常小。
5.4 自己匹配自己造成假高分
这是实现层面最容易忽略的问题。计算相似度矩阵时,同一张图的两个视图之间的相似度往往极高,如果不把对角线遮掉,MNN 会被“恒等映射”大幅抬高。任何模型都能轻轻松松拿高分,指标就失效了。一定要在相似度矩阵的第 i 行第 i 列位置加上屏蔽。
同理,如果评测数据集里存在重复或近似重复图片,也要提前去重,避免跨样本的平凡匹配干扰结果。
5.5 跨任务迁移时指标失效
这个方法处理图像分类任务很有效,但在检测、分割任务上的相关性会明显下降。我推测原因是这些任务对空间位置、局部细节更敏感,而 RULE 使用的是全局特征的一致性,无法反映空间信息质量。
如果你要评估一个模型在检测任务上的潜力,建议把视图改成“局部裁剪 + 全局裁剪”的组合,同时配合 bounding box 区域的局部特征来做一致性统计。这属于目前仍在探索的方向,实验效果还不稳定,但思路是对的。
5.6 显存与耗时优化
第一次跑的时候我把所有图片的特征直接放到 GPU 显存里,几百 MB 的特征矩阵没问题,但算相似度矩阵时直接爆显存。后来改成特征提取阶段用 GPU、相似度矩阵计算放到 CPU,并在 CPU 上优化矩阵乘法,速度反而更快。对于 1 万张图片,整个流程在单张 3090 上大约需要 3 到 5 分钟,完全可作为训练过程中的常规监控手段。
5.7 常见问题速查表
| 问题 | 现象 | 可能原因 | 解决方案 |
|---|---|---|---|
| MNN 饱和 | 训练后期分数几乎不变 | 二值命中无法反映相似度强弱 | 加 top-k 命中率或提高 NMI 权重 |
| 结果不可复现 | 两次评测排名不同 | 增强、聚类、随机种子未冻结 | 冻结全部配置并固定种子 |
| NMI 波动大 | 同一模型分数差 0.03+ | KMeans 初始化不稳定 | 多次聚类取中位数 |
| 分数普遍过高 | 所有模型都接近满分 | 增强过弱或未屏蔽自匹配 | 增强上加码并屏蔽对角线 |
| 显存不足 | 计算相似度矩阵时 OOM | GPU 显存存放过多特征 | 特征放 CPU,在 CPU 上算矩阵 |
| 跨任务失效 | 检测任务相关性低 | 全局特征丢失空间信息 | 改用局部视图组合,开发局部版指标 |
6. 扩展思考:这套思路还能用到哪里去
6.1 训练期间的自动 checkpoint 选型
最直接的应用是把 RULE 嵌入训练流程。每训练 10 个 epoch,就在固定评测集上算一次分数。分数开始下降时,说明模型出现了过拟合或表征退化,可以自动回滚到之前的 checkpoint。这比每 50 个 epoch 跑一次下游线性探针要快得多,也便宜得多。
6.2 无监督场景下的超参搜索
对比学习对不同超参数的敏感程度非常高,学习率、温度系数、投影头维度都会显著影响最终模型质量。把 RULE 当作无标签验证集指标,配合自动搜索算法,可以在完全没有标注的情况下完成超参选择。我在做搜索实验时,用 RULE 筛选出的超参组合,在真实下游任务上也基本表现最好,说明这个代理指标的方向性是对的。
6.3 跨架构模型比较
传统的 loss 无法跨模型比较,因为不同架构的特征空间差异太大。RULE 在一定程度上弥补了这个问题:它本质上衡量的是“模型自身行为的一致性”,而对特征空间的绝对位置不敏感。把不同架构的模型都提取特征,然后在同一份评测集上计算 MNN 和 NMI,得到的分数可以做横向对比。我测试了基于卷积网络和基于 Transformer 的模型,RULE 分数和各自下游精度的排序都较为吻合。
6.4 增量学习中的灾难性遗忘监测
增量学习场景中,模型在持续学习新任务的同时,容易遗忘旧任务的知识。RULE 可以当作一个“无标签遗忘度指示器”:如果某个模型在学习新数据后,在旧数据上的 MNN 一致性明显下降,说明它的旧任务表征正在被破坏。这个场景下通常没有旧任务的标签,RULE 几乎是少数可行的评估手段之一。
6.5 后续扩展方向
我在做基于局部特征块的版本,目标是把它推广到检测和分割模型的评估上。基本思路是把整图拆成网格,在每个局部区域单独做一致性统计。目前已经能看到一些积极信号,但整体分数排序和真实检测精度的相关性还不够稳定,还在持续迭代中。多模态模型也是一个方向:文本增强和图像增强的一致性是否会成为跨模态模型的无标签指标,值得做一轮系统性实验。
我自己在实际使用中的体会是:设计一个无标签指标并不难,难的是让分数在各种边界条件下仍然能保持排序稳定性。这个项目的每一步推进几乎都是靠踩坑踩出来的。如果你也在做自监督模型评估,建议从 MNN 和 NMI 这两个基础信号开始,先把评价体系跑通,再根据你的数据特点做调整。最后一个小技巧:任何评测指标,都要先在一个包含 5 到 10 个已知强弱关系的模型上验证排序能力,再拿去用,否则你永远不知道它什么时候会失灵。