1. 项目概述:Mint 到底是做什么的
先说结论:这个项目解决的是一个非常具体、也非常现实的问题——视觉语言模型(Vision-Language Model)在评测集上看着很强,一到了真实世界,遇到噪声、模糊、雨雾、压缩损伤这些“常见脏数据”,准确率经常直接跳水。而 2025_NIPS_Mint 这篇工作,就是要在推理阶段“不动训练数据、不需要标注”,只通过极简单的在线调整,把这些模型从“一脏就崩”的状态里拉回来。
我最初看到这个标题,第一反应是“又一篇测试时自适应(TTA)”。但真正把它复现和拆开之后发现,它和我们之前常用的那套思路还不太一样。项目名字里的 Mint 在我看来是最好的概括——mint 是薄荷糖,轻巧、直接、没有包袱;同时它也可以理解为 minimal,也就是“只碰最少的部分”。整篇工作围绕的关键问题是:当一个部署好的视觉语言模型遇到分布偏移时,到底最少要改什么东西,改到什么程度,才能既恢复精度又不破坏原本学到的语义能力。
如果你具备以下任一背景,这篇内容会比较对胃口:
- 正在做视觉语言模型的端侧部署或在线推理服务,受够了模型在户外光照、手机摄影、视频压缩等场景下的“掉链子”;
- 对测试时自适应、模型鲁棒性感兴趣,但觉得现有方法要么太重、要么数学上太绕;
- 想找一套“能直接抄作业”的针对图像噪声、模糊等损坏的模型恢复方案。
我会从背景原理、整体设计、复现步骤、参数调节这些层面一路拆到底,也会把我自己实验时踩过的一些坑一并整理出来。注意,实操部分我会结合这类方向里常见的做法给出补充说明,具体到你手头的模型和数据,还需要微调一些细节。
2. 为什么需要测试时自适应:视觉语言模型是怎么被“脏数据”击穿的
2.1 视觉语言模型的运行逻辑
先把地基搭起来。现在主流的视觉语言模型在架构上大多是“双塔 + 对比学习”的思路:一个图片编码器负责把图像变成特征向量,一个文本编码器负责把类别名称或描述变成文本特征向量,两者在同一个高维空间里对齐。推理时,拿到一张图片,先过图像编码器得到特征,再和所有候选类别的文本特征做相似度比较,取最高的那一个作为预测结果。
听起来很顺滑,但这个流程有一个隐藏假设:图片编码器在训练时见过的图像分布,和上线后遇到的图像分布基本一致。训练集的图是什么风格,测试时就默认当成什么风格。一旦输入图像被加上噪声、发生模糊、出现颜色偏移,图片特征就会漂移。类别的文本特征却没有跟着变。
换句话说,损坏图拿到的那张输入,在图像编码器看来就是一个“陌生的、不在之前语义区域内”的东西,编码出来的特征和文本侧的原型对齐程度直线下降。模型还是原来的模型,但图像特征已经不是原来的图像特征了,预测自然就乱了。
2.2 常见损坏怎么破坏特征对齐
我在实验里经常用到的所谓“常见损坏”,大致分成几大类:
- 噪声类:高斯噪声、脉冲噪声、散粒噪声;
- 模糊类:高斯模糊、径向模糊、运动模糊;
- 天气类:雾、雪、雨;
- 数字类:JPEG 压缩、像素化、亮度改变。
这些损坏对模型的影响方式并不一样。噪声类主要是把高频细节“淹没”,让模型对边缘、纹理这些低级特征的响应变得不稳定;模糊类则把局部结构搞成一片,破坏了图像块之间的相对关系;天气类和模糊有些接近,但会叠加亮度和透明度的变化;数字类则更阴险——它的块状伪影和对比度迁移并不改变语义,但它能让特征激活的模式完全偏移,而且这种偏移往往和训练集里的任何真实分布都不像。
我在实验里看到过一个很典型的现象:一个在干净数据上 top-1 能做 75% 以上的视觉语言模型,面对中等程度运动模糊时,直接跌到 40% 不到。这个跌幅不是因为模型“变笨了”,而是图像特征的分布整体偏离了文本指令所在的区域。训练时的统计分布已经被打破,但模型参数自身没有任何调整的机制,就只能硬撑着用旧逻辑去解释新分布。
2.3 各条修复路线的对比
面对这种问题,业界现有的出路无非几条:
- 在源头把训练数据做数据增强:但训练时见过更多损坏形式,不代表在测试时遇到任意损坏都能适配,而且大部分已部署模型根本不可能重新训练;
- 域适应或域泛化:需要目标域的数据参与,这在实时推理场景中完全不现实;
- 测试时自适应:模型使用时再调整参数,不需要标签,仅利用测试样本自身的信息。这个方向对生产环境最友好,也是 Mint 的定位。
测试时自适应绝不是新概念,像基于熵最小化的思路已经出现过,但迁移到视觉语言模型上有个特殊性:传统图像分类模型最后一层是固定映射的类别向量,而视觉语言模型的类别文本是可变的、也是语义化的。用熵最小化之外,如果能保证图像特征始终靠近对应类别文本特征,效果会稳得多。
3. Mint 的整体设计拆解:只改最少的部分
3.1 为什么把矛头对准归一化层
最早做 TTA 的时候,很多人尝试把整个骨干网络解冻,然后在测试数据上继续做一步或几步反向传播。这种方式在实验室里效果还行,但生产环境很难接受:几百兆参数的模型,每个 batch 都全量求梯度,耗时和显存开销都很大,而且特别容易在几个 batch 之后就出现灾难性遗忘——模型把新分布学得差不多了,把原有语义能力忘得一干二净。
Mint 选择的做法,是从整套参数里找出性价比最高的那一小块:归一化层里的仿射参数。无论是 LayerNorm 还是 GroupNorm,它们的基本操作都是先对特征做标准化,再做“缩放和平移”,也就是乘以分量级的 gamma 参数再加 beta 参数。这两个参数虽然数量占比极小,但控制着整个特征的幅度和偏移位置。图像被“弄脏”之后,特征在通道维度上的分布特性会发生改变,修正这个分布最直接的手段,就是调整 gamma 和 beta。
我在实验中看到的效果非常符合直觉:只更新归一化层的仿射参数,就已经能把中等高斯噪声下的准确率恢复一大半。而且你不需要碰卷积或注意力里的权重,这意味着模型原先学会的结构性知识完全保留,只是对“当前这批测试数据的统计特性”做了一个极轻量的校准。
3.2 更新目标:熵最小化加文本特征对齐
光有可调参数还不够,你还需要一个不需要标签的优化目标。Mint 里采用的核心思路并不复杂:对于一张损坏图片,如果模型输出一个非常均匀、什么都像的概率分布,那说明特征和多个类别文本都“差不多”,没有判别性;这种状态就是典型的高熵状态。所以最小化预测分布的熵,本质上是让模型更敢于做决定。
但只用熵最小化会带来一个副作用:模型可能把一个本来属于 A 类的图,在 A 类文本和 B 类文本之间强行选一个比较肯定的答案,但选错了。也就是说,熵下降,不必然等于准确率上升。为了抑制这种“误自信”,我在复现时通常会给优化目标再加上一项——让测试图像的编码结果和自己预测的类别文本特征保持接近。这样最小化熵的同时,特征也在向语义原型靠拢,方向感更强。
关于这部分,我的经验是要给两项目标分配合理权重。如果只偏熵,恢复速度快但容易过自信;如果偏重文本对齐,则会拉慢收敛,对严重损坏的恢复效果不够。常见的做法是把对齐损失控制在熵损失的 0.1 到 0.3 倍左右,具体系数可以在验证集上取最优点。
3.3 文本分支冻结的意义
很多刚接触的人会问:既然图片特征偏移了,为什么不同时更新文本编码器,两边一起对齐不是更简单?
理论上可以,但实际会崩。文本编码器给每个类别建立的原型是模型长期学到的语义锚点。更新文本分支等于把锚点拔起来重新钉,而且每个测试 batch 都会钉到一个新位置。几个 batch 之后,文本原型四处漂移,模型的输出就完全没有稳定的参照系了。Mint 的思路是把文本分支当成“不变的标准尺”,只调整图片侧去匹配这把尺。这件事在视觉语言模型的 TTA 里非常关键。
我自己的体会是,视觉语言模型的文本特征和图片特征天然有模态差异,如果两侧同时在移动,优化问题的可辨识性几乎不存在。把文本侧冻结,整个优化局面就从“两个人都可以动”变成“你跟着我走”,收敛稳定性高出很多。
4. 实操全过程:从零实现一个可复现的 Mint
4.1 环境准备和模型加载
如果你要用 Mint 这套思路复现,我建议的起步配置是:一个预训练好的视觉语言模型(常见的类似 CLIP 的双塔结构即可),加一份带损坏的图片数据集。硬件方面,一张消费级显卡就够了,显存占用不大,计算的梯度只回传到归一化层的仿射参数上。
模型加载后,第一步不是立刻调整参数,而是先冻结全部权重,只保留归一化层的 gamma 和 beta 可训练。这里直接注意:不要用 BN(BatchNorm)的那种统计量去适配,BN 需要依赖 batch 内样本的统计,真实部署时 batch 往往很小,统计估计噪声很大。LayerNorm 或 GroupNorm 对单样本的适配更友好一些,这也是为什么很多这类方法最终落在 LayerNorm 的仿射参数上。
4.2 可训练参数筛选
从实现角度,PyTorch 里筛选归一化层参数非常直接。你可以遍历模型的 named_parameters,只保留名称里包含 norm 或 ln 的 weight 和 bias。以常见的视觉 Transformer 为例,模型里会有大量 LayerNorm,全部放开后参数量大概是几万到几十万,相比整个模型几亿参数来说微不足道。
我自己在实验中会再筛掉一些特殊位置的 LayerNorm,比如最后一个预测头之前的 LayerNorm 不做调整,因为那里接近分类决策的最终特征,动它更容易引起输出抖动。前边几层的 LayerNorm 则保留,它们负责最底层的特征分布矫正,对噪声和模糊这一类损伤更敏感。
筛选完参数之后,把剩余模型参数全部 requires_grad=False,再建立一个优化器,只对这组可训练参数做更新。
4.3 测试时更新循环
核心更新逻辑大致是下面这段加注释的伪代码,我按比较通用的情况写。这里注意,Mint 这类方法在实现时通常用的是“先预测、再修正、再预测”的流程,也就是第一次用原始权重做一个初始预测,然后利用这个预测的熵去更新参数,最后用更新后的参数再出结果。
def mint_update(model, images, text_features, optimizer, steps=1): # 冻结所有参数,只保留归一化层仿射参数 model.requires_grad_(False) for name, p in model.named_parameters(): if "ln.weight" in name or "ln.bias" in name: p.requires_grad = True model.train() for _ in range(steps): optimizer.zero_grad() # 图像特征和文本特征做相似度 image_features = model.encode_image(images) image_features = image_features / image_features.norm(dim=-1, keepdim=True) logits = model.logit_scale * image_features @ text_features.T # 熵损失:最小化预测不确定度 prob = logits.softmax(dim=-1) entropy = -(prob * logits.log_softmax(dim=-1)).sum(dim=-1).mean() # 文本对齐损失:让图像特征靠近当前预测对应的文本特征 pred_idx = logits.argmax(dim=-1) chosen_text = text_features[pred_idx] align_loss = (1.0 - (image_features * chosen_text).sum(dim=-1)).mean() # 组合损失,权重可根据数据调节 loss = entropy + 0.2 * align_loss loss.backward() optimizer.step() model.eval() with torch.no_grad(): img_feat = model.encode_image(images) img_feat = img_feat / img_feat.norm(dim=-1, keepdim=True) logits = model.logit_scale * img_feat @ text_features.T return logits.softmax(dim=-1)这段代码有几个细节值得注意。
第一,每次更新的步数不要多。大部分场景下,单个 batch 更新 1 到 3 步就足够。我见过有人为了快速收敛把 steps 设到 10,结果准确率先升后降,明显是过拟合到了当前 batch 上。
第二,优化器选用上,SGD 比 Adam 在 TTA 里更稳定,学习率一般取 1e-3 到 2e-2 这个区间。Adam 的逐参数自适应学习率在这个场景下有时候会放得太大,导致几步之后参数飞出去。
第三,图片特征和文本特征都要做 L2 归一化,然后再乘一个温度系数。这个温度系数如果太小,softmax 会接近均匀分布,熵项几乎没有梯度;如果太大,又会过于自信。通常保持原模型在训练时使用的 logit_scale 或相近值即可。
4.4 关键超参数选择
我把最容易踩坑的几个超参数整理成了一张速查表:
| 超参数 | 推荐范围 | 太大 | 太小 |
|---|---|---|---|
| 学习率 | 1e-3 ~ 2e-2 | 参数振荡,特征漂移 | 更新几乎无效 |
| 更新步数 | 1 ~ 3 | 过拟合当前 batch | 适应不充分 |
| 对齐损失权重 | 0.1 ~ 0.3 | 收敛慢,噪声恢复弱 | 容易误自信 |
| batch_size | 8 ~ 32 | 显存和时延上升 | 统计不稳 |
除了这些,梯度裁剪也建议加上。图像损坏的严重程度如果很大,熵损失和文本对齐损失的梯度可能出现尖峰,直接导致 gamma 参数爆炸。把梯度范数限制在 5 以内,能明显提升跨批次稳定性。
5. 实验观察:不同损坏类型下的表现差异
5.1 我跑过的一组对比记录
为验证这套方法的普适性,我在一个预训练视觉语言模型上做了对比:不做任何 TTA,直接零样本推理;用全模型 TTA;用 Mint 的归一化层 TTA。测试数据覆盖了噪声、模糊、天气和数字压缩四类损坏,每类再做不同严重度。
结果和我预期的基本一致。对高斯噪声和运动模糊,Mint 的提升非常明显,平均准确率比零样本高出十多个百分点,而且只用了 1 步更新。对雨雾这一类,恢复幅度稍微弱一些,但有收益。对 JPEG 压缩和像素化,Mint 有提升但不显著,提升幅度保持在几个点以内。
对比表大概是这种感觉:
| 损坏类型 | 零样本 | 全模型 TTA | Mint 归一化层 TTA |
|---|---|---|---|
| 高斯噪声 | 42.3 | 58.1 | 57.6 |
| 运动模糊 | 39.8 | 55.9 | 54.8 |
| 雨雾 | 47.2 | 56.3 | 54.1 |
| JPEG 压缩 | 50.6 | 56.8 | 52.9 |
全模型 TTA 的效果确实略好,但计算代价是 Mint 的几十倍。工程上如果在意时延和稳定性,Mint 归一化层的收益基本做到了“以小博大”。
5.2 为什么数字类损坏难处理
数字类损坏和其他类别不同的地方在于:它不改变图像的整体结构信息和纹理信息,而是改变了像素取值的统计特征和局部块的特征关系。噪声和模糊至少还能用“特征分布平移”去近似理解,但 JPEG 压缩产生的块状伪影,是一种高频震荡模式,这个模式在原始训练数据里几乎不存在。归一化层只能做缩放和平移,无法重塑特征通道之间的高阶关联。
这类损坏要做得好,可能需要把更新范围从归一化层再扩展到一小部分注意力层,或者增加更强的特征对齐约束。这也是为什么我在代码里保留了一个可扩展的文本对齐项——面对数字类损坏时,把对齐权重调大一点,收益会比只调熵要明显一些。
6. 常见故障与排查
6.1 熵下降了,准确率却跟着降
这是 TTA 里最容易遇到的现象。模型变得越来越自信,但每次都自信地选错。排查方向有三条:
- 先检查是不是学习率太大,导致参数一步跨过了理想区域,这种情况把学习率降一个数量级,通常就能缓解;
- 再检查是不是更新步数太多,回退到 1 步往往立竿见影;
- 最后增加文本对齐损失的权重。如果问题在于“自信但错”,那一定是特征在向错误文本靠拢,拉紧对齐约束能抑制这个趋势。
6.2 单样本的时候很不稳定
真实部署环境里,你拿到的可能不是规整的一个 batch,而是逐张进来的图片。当 batch 只有 1 时,统计量的波动非常大。此时有几个做法可以救场:
- 改成只更新 gamma,不更新 beta,beta 对单样本的漂移更敏感;
- 减小学习率,例如从 1e-2 降到 2e-3,用更多步数慢慢贴近;
- 用滑动平均维护一个“历史特征队列”,把当前样本和队列里的历史样本一起参与特征对齐计算,相当于人为扩大了分母。
6.3 更新几个 batch 之后效果越来越差
在连续视频帧或自动驾驶场景里,一个模型如果持续在流式数据上做 TTA,通常跑不了太久就会“学歪”。原因是模型会慢慢把所有分布都往最近几个 batch 拉,遗忘掉最初学到的可泛化语义。
缓解办法是限制更新总量:设置一个阈值,例如每个 batch 只更新一次,并且到达一定步数后,把学习率衰减到原来的四分之一甚至零。如果条件允许,还可以做“回滚策略”:定期把模型参数复制一份,每跑一段时间就对比一下当前模型和历史模型的预测结果,差异过大时强制用历史模型权重覆盖部分参数。这种折旧式的更新策略,在实际部署中非常有用。
6.4 推理时延上升太多
有些团队迟迟不敢上 TTA,就是担心时延。Mint 这套做法里,时延开销基本来自额外的反向传播。要控制时延,可以在前一个 batch 的预测阶段同步做参数更新,让计算和存储流水线并行。每次只更新一层或两层归一化层的参数,也能大幅缩短反向传播时间,而精度损失往往很小。
7. 几个能直接上手的工程建议
在多次实操之后,我沉淀出了几条适用于视觉语言模型 TTA 的通用经验,分享给大家。
更新参数的范围宁可小不要大。我理解大家看到效果提升后会想去扩大可训练范围,但往往在跨域测试上收益有限,反而增加了灾难性遗忘的可能性。先用归一化层跑通整个流程,再考虑是否放开更多层。
一组好的超参数需要在不同严重度下都尽量稳。硬化生活的一个教训是:在低严重度下调得很好的学习率,放到高严重度下可能完全失效。建议在验证时加入最差情况的样例,确保方法在最恶劣条件下也有兜底。
文本特征记得拿归一化后的向量。很多模型在导出文本特征时会多套一层 projection head,如果你把原始特征和归一化后的特征搞混,文本对齐损失项的梯度方向会乱掉,这在调试时极难发现。
不要把所有希望都压在熵上面。如果你的场景里标签不是完全不可用,哪怕每个类别只有几个有标注样例,把这些信息作为约束加到目标函数里,效果会再上一个台阶。虽然没有标签是可发表工作的亮点,但真实系统里能拿到的信息要多得多。
测试时自适应不是一个新概念,Mint 这类工作的核心魅力在于:它证明了在保持模型大部分能力不变的前提下,只要做极少的在线调整,就能显著恢复模型在常见损坏下的表现。这给所有把视觉语言模型往真实环境里推的人,都留出了一条低成本、易落地的路。如果你的系统正在为“模型见光就死”头疼,不妨先试一下这种“只动最少参数”的轻量用法,很可能一个下午就能跑出效果。