☰
对比学习不收敛?从损失函数到温度系数全流程避坑指南
2026/10/2 1:03:54 网站建设 项目流程

跑对比学习模型,最常见的挫败感不是模型效果差,而是看着loss曲线半死不活地横在那儿,或者干脆冲高后跌到负数,最后所有样本的表征挤成一团,下游任务直接报废。这个“不收敛”问题背后,十有八九不是模型结构的问题,而是对比学习损失函数的细节没吃透。我前前后后踩了无数次坑,从SimCLR、MoCo到BYOL都折腾过,整理了这份避坑指南,希望能帮你少走弯路,直接定位到问题根源。

这篇内容重点讲清楚对比学习最常用的损失函数在想什么、哪些因素在暗地里影响收敛、以及工程实现里那些一不留神就出错的细节。适合正在训练自监督模型、或者准备在业务里用对比学习但老觉得效果不稳定的朋友。

1. 先搞懂对比学习损失函数到底在算账什么

对比学习不收敛,很多时候是“药不对症”——你不知道这个损失函数在乎什么,自然也不知道它为什么不下降。所以先把最核心的基础说透。

1.1 InfoNCE的前世今生:一个公式拆开揉碎

现在自监督对比学习最常碰到的损失函数是InfoNCE,SimCLR用的就是它。公式长这样:

[ L = -\log \frac{\exp(sim(z_i, z_j)/\tau)}{\sum_{k=1}^{2N} \mathbb{1}_{[k \neq i]} \exp(sim(z_i, z_k)/\tau)} ]

看着挺唬人,其实本质就是一句话:让正样本对之间的相似度尽量高,让所有负样本对之间的相似度尽量低。这里的( sim )通常用余弦相似度,( z_i )和( z_j )是同一张图片经过两次不同数据增强后、再通过编码器和投影头得到的特征向量。

用大白话讲:你拿一张猫的照片,经过两种不同的裁剪、颜色抖动、旋转,得到两张看起来不太一样的“猫”。理想情况下,模型应该认出这是同一只猫,把它们的表征拉近;同样的,它应该把这只猫和一张狗的照片推远。整个训练过程就是在做这个“拉近推远”的游戏。

InfoNCE还有一个更底层的身份——它是互信息的一个下界估计器。换句话说,最小化这个损失,本质上是在最大化增强视图之间共享的信息量。这也是为什么大家常说“对比学习是在学一个不变量”:要把那些在数据增强中被保留下来的核心语义给提取出来,把被扰动掉的那些细节给扔掉。

1.2 为什么一个损失函数能决定“收不收敛”

有读者可能会问:损失函数不就是个目标函数吗,我换成交叉熵为什么不行?问题就在这——交叉熵的目标是预测一个离散的类别标签,而对比学习的目标是构建一个连续的、具有判别力的特征空间。在这个空间里,属于同一语义的样本要聚在一起,不同语义的样本要尽量分开。

自监督模型的“收敛”和分类模型还不一样。分类模型loss降到低点,acc上去了,任务基本就成。对比学习loss降下去,只代表正负样本在损失层面分开了,但下游任务能不能用,还得看这个特征空间有没有保持良好的结构。更麻烦的是,如果损失函数里的某些项出了问题,比如温度系数过高、正样本对太强,模型会找到一个“偷懒”的解法:把所有样本的表征都映射到同一个点。这种情况下loss反而很低,但模型已经塌了。

所以,理解这个损失函数“如何分配梯度”“在什么时候给模型信号”“有哪些容易被忽视的边界条件”,才是定位不收敛问题的核心。

2. 温度系数是“命门”,不是随手设的旋钮

如果让我说对比学习里哪个超参数对收敛影响最大,我一定先投温度系数( \tau )一票。太多不收敛案例,最后定位来定位去,就是它没设对。

2.1 温度系数如何暗中改变梯度走向

在InfoNCE中,相似度会除以温度系数( \tau ),然后过softmax。这个操作直接决定了模型对“困难负样本”的惩罚强度。

  • ( \tau )很小(比如0.01):logits会被放大,softmax输出会变得更加“尖锐”。模型会极其敏感地把注意力集中在那些和正样本相似度较高的负样本上,强行把它们推开。它的好处是能学到更细粒度的区分,坏处是训练极不稳定,稍微有点噪声就会被放大,loss很容易震荡甚至直接飞掉。
  • ( \tau )很大(比如超过0.5):logits被压缩,softmax输出趋于均匀。每个负样本拿到的梯度都差不多,模型“一视同仁”,这会让训练变得稳定,但代价是正负样本的差距不够明显,模型学习动力不足,loss可能下降很慢,最后学到的表征也比较平庸。

直观类比:温度系数就像你去相亲时对外貌的挑剔程度。( \tau )很小时,你眼里容不得一粒沙子,长得稍微像前任的都被一票否决;( \tau )很大时,你看谁都差不多,最后也分不清谁是谁。太极端都不行,得找那个“清楚知道自己要什么但又不至于吹毛求疵”的分寸。

2.2 从实验数据看最佳范围

根据我自己的实验和论文里的经验值,InfoNCE的温度系数在0.05到0.2之间最常见,效果也最好:

  • SimCLR原文用的( \tau = 0.07 ),配合batch size 4096,在ImageNet上取得了当时最好的效果。
  • MoCo v2采用( 0.07 ),因为MoCo的负样本队列足够长,这个值比较合适。
  • BYOL虽然本身不含负样本,但它也用了温度系数处理预测头的相似度,一般也设在0.1附近。

如果是从零开始调,我建议直接以0.1为基准跑一到两个epoch,观察loss的初始值和波动情况。如果loss起飞,降到0.05;如果loss降得太慢,适当升到0.15~0.2,小步试错。

2.3 温度系数不能单独调,它和Batch Size强相关

很多人忽略一点:增大batch size时,负样本数量变多,模型对困难负样本的感知更强,此时如果还保持较小的( \tau ),梯度会被少数“极其相似”的负样本主导,训练容易被带偏。经验上,batch size翻倍时,( \tau )也可以适当往上调一点,比如从0.07调到0.1。

反过来,如果你的显卡只能支撑很小的batch size(比如128),却硬要套用论文里的0.07,大概率会出现训练极度不稳定的情况。这时候不是你的代码有bug,而是超参搭配不匹配。

3. 正负样本构造里的隐形陷阱:样本错了,损失函数再努力也白搭

损失函数本身写得没问题,但数据增强、负样本质量这些前置环节出了岔子,照样不收敛。这三类坑我几乎每次带新人都会讲一遍。

3.1 数据增强的强度:“正样本太难”和“太简单”都是坑

对比学习的效果极度依赖数据增强构造出的正样本对。如果增强太弱,两张“正样本”几乎一模一样,模型不需要理解语义就能轻易把它们对齐,学到的表征没有泛化性。如果增强太强,比如裁剪后只留了5%的像素,或者颜色抖动把色调完全改掉,正样本对可能连人都认不出是同一个物体,模型强行对齐它们,反而会学到错误的信息。

经验之谈:SimCLR的增强组合(随机裁剪+随机颜色抖动+灰度化+高斯模糊)是久经考验的组合。你可以在它的基础上按业务数据微调,但别一次性全删掉或全加满。判断增强强度是否合适的办法,是拿增强后的图片让一个没参与训练的人看一眼,如果人眼能轻松判别“这是同一张图的两个版本”,那基本是安全的。

3.2 负样本里的“假阴性”:语义重复的样本是个定时炸弹

对比学习默认把batch里除正样本外的所有其他样本都当成负样本。但现实数据里经常有语义相同但外观不同的图片:同一只狗的多个角度照片、同一款商品的多个角度的电商图。它们虽然不是由同一张图增强出来的,但语义上完全一样。把这类样本当负样本去推开,等于让模型把一个“本该靠近”的样本强行推远,梯度互相打架,训练就会不稳定。

解决方案有几条路:一是清洗训练集,把明显重复的图片去掉;二是采用无监督聚类,先给样本打伪标签,把同一聚类的样本从负样本中排除;三是在小batch下用MoCo这类队列方法,增加负样本的多样性,降低语义重复的概率。

3.3 Batch Size的真实下限:别只盯着显存看

SimCLR原文强调大batch size很重要,甚至用到4096,因为对比学习需要足够多的负样本去支撑梯度估计。如果你只有8张卡每张卡batch 32,整体batch不过256,那InfoNCE的负样本数量就偏少,模型很容易陷入“局部最优”,表象就是loss下降后不再变化,下游表征质量也上不去。

我自己在单卡实验时遇到过类似问题,一个比较实用的补救方案是用MoCo的queue机制,把历史batch的特征存成一个动态队列,作为额外的负样本来源。这样即使当前batch很小,负样本池也能有几千甚至几万个样本。代价是实现复杂度高一点,但训练稳定性肉眼可见地提升。

4. 工程实现里的隐蔽Bug:loss不收敛,先查代码再查超参

如果读到这里还没找到问题,那就要怀疑代码层面了。对比学习项目里,最容易出bug的环节往往是归一化、矩阵维度和数值稳定性这三类。

4.1 忘了做L2归一化:向量模长彻底毁了余弦相似度

InfoNCE里用到的相似度通常是余弦相似度,也就是先对特征向量做L2归一化,再算点积。我见过不少实现把归一化漏了,直接用原始特征算相似度。这样一来,相似度不仅取决于方向,还取决于向量的模长。模型很容易走捷径:通过增加模长来“作弊”提升正样本相似度,而不是学习有意义的特征方向。

直观感受就是loss在下降,但你去看下游任务指标,基本没提升,甚至可能负优化。检查办法很简单:在算logits之前把feature打印出来看norm,如果norm不是1,说明归一化没做或者做错了位置。正确的做法是在投影头输出之后、计算余弦相似度之前,对每个样本的特征向量执行( z / |z|_2 )。

4.2 logits矩阵的形状搞错:相似度计算成“自己和自己的转置”

实现InfoNCE时,常见做法是concat正样本特征得到形状( (2N, D) )的向量,再算所有样本两两之间的相似度矩阵( (2N, 2N) )。这里容易出两处错误:

第一,对角线上相似度是样本与自身,需要在loss中mask掉。如果你忘了屏蔽对角线,相当于把“自己和自己的相似度”也当成正样本计算,模型很容易直接“作弊”学到恒等映射,而不是语义相似性。

第二,正样本对的索引对应关系搞错。在( (2N, 2N) )矩阵里,( i )和( i+N )(或者按实现约定的某个偏移)才是同一张原图的两个增强视图,如果索引对错了,模型看到的正样本对其实来自不同原始图片,那整场训练就是在自欺欺人。

建议自己写一个小batch的前向样例,比如batch size=2,用手算的方式验证一下logits矩阵里哪些位置应该是正样本,哪些应该是负样本,确认无误后再丢到完整训练流程里。

4.3 数值稳定性:float16和LogSumExp是个坎

在混合精度训练中,InfoNCE里的( \exp )很容易上溢出。假设温度系数( \tau = 0.07 ),那么相似度( 1.0 )处对应的( \exp(1.0/0.07) = \exp(14.28) ),在float16下已经接近上限了。如果某个负样本和正样本高度相似,相似度接近1,这个中间值在fp16下直接变inf,loss就变成NaN。

解决方式是用LogSumExp技巧:计算( \log \sum_j \exp(x_j) )时先减去最大值再还原。现实中,很多框架的交叉熵内部已经做了这个处理,但你要是手写InfoNCE,一定要用类似torch.logsumexp的实现,而不要手动写log(torch.sum(torch.exp(...)))。另一个保险措施是在训练早期用小扰动或降低温度来避免数值尖峰。

4.4 对称损失:别忘了两个方向都算

SimCLR的最终损失经常写成对称形式:除了计算从视图1到视图2的InfoNCE,还要计算从视图2到视图1的InfoNCE,两者取平均。有些简化实现只算了一个方向,虽然loss也会下降,但模型只学会了对“anchor是视图1”的情况做判别,表征泛化性差了一大截。

所以检查一下代码:loss = (loss_12 + loss_21) / 2是否写对了。这个小细节直接决定你学到的特征对输入顺序是否鲁棒,下游任务往往对此非常敏感。

5. 训练异常与排查速查表:从Loss不降到模型坍塌一次说清

训练过程中遇到的异常,表象千奇百怪,但根因通常集中在几个维度。我整理了一份排查顺序,按这个顺序走下来,大半问题能自己定位。

5.1 Loss完全不下降:先看数据,再看学习率

如果loss从第一步开始就不动,大概率不是损失函数写错,而是数据没喂对。检查数据增强是否生效、正样本对是否确实是同一原始样本、label或mask是否正确。然后是学习率,对比学习通常配合warmup使用,如果初始学习率太低,前几个epoch几乎看不出变化;如果太高,loss会瞬间冲到很大,然后在震荡中缓慢爬升,看起来也像“不收敛”。

我一般先用一个很小的子集(比如1000张图)过拟合训练,看loss能不能降到接近0。如果连过拟合都做不到,代码大概率有bug,和数据规模无关。

5.2 Loss在下降,但下游任务掉点:表征均匀性出了问题

对比学习有个经典现象:loss持续下降,表征却“退化”了。这说明模型可能在把特征推开时用力过猛,把所有样本均匀地分布在超球面上,正样本虽然靠近,但整体语义结构被破坏。可以画一下特征分布的直方图,或者算一下特征空间的“均匀性”指标。如果特征分布过于均匀,说明负样本推开的优先级过高,可以考虑降低负样本的重要性、调大温度系数,或者增加投影头的深度。

5.3 模型坍塌:Loss很低但所有表征都一样

坍塌是自监督里最经典也最让新手头疼的问题:所有样本的表征收敛到同一点,loss因为“正样本完全重叠”而显得很低,实际上模型啥也没学到。

简化的对比学习很容易坍塌,因为只要把所有输入映射到同一个常数输出,正样本相似度必然是最大值,负样本的惩罚反而显得不重要。InfoNCE系方法能天然缓解坍塌,但你不一定用了InfoNCE。如果是BYOL或SimSiam这类不带负样本的方法,坍塌后通常表现为训练极不稳定。

排查坍塌时,核心看表征的方差:把所有样本的特征在某个维度上统计方差,如果方差趋近于0,基本就塌了。应对策略在InfoNCE下主要是调低温度系数、增大负样本数量;在非负样本方法下则是查预测头、动量更新、停止梯度(stop-gradient)这三个环节有没有写对。

5.4 排查速查表

现象优先检查项可能的解法
Loss完全不变数据增强、正样本对索引、mask用小子集过拟合验证
Loss炸成NaN数值稳定性、fp16溢出、温度过小使用LogSumExp,适当增大( \tau )
Loss下降慢学习率低、无warmup、( \tau )过大调整学习率,减小( \tau )
Loss下降但下游差特征均匀性、伪正样本清洗语义重复数据,调整( \tau )
训练不稳定、跳变batch太小、( \tau )过小、数据增强过强增大batch或增加负样本队列,调( \tau )
模型坍塌stop-gradient、负样本机制、投影头检查网络结构,确认负样本确实在参与训练

6. 实操调参经验:从“能跑”到“跑得好”的几点心得

最后这部分是我自己反复实验总结出来的调参顺序和一些小技巧,不一定适用所有场景,但可以作为起步参考。

6.1 投影头别小看,它决定了损失函数能多“挑剔”

SimCLR论文里一个重要发现是:把特征从编码器输出过一层MLP(通常是2层全连接+ReLU)再算损失,效果明显好于直接用编码器特征。投影头相当于把“用于对比的判别空间”和“用于下游任务的特征空间”解耦。模型可以在投影空间里更自由地做正负样本判别,而不必破坏主干特征的质量。

建议投影头输出维度设在128或256,不要太大。有人图省事把投影头去掉,结果loss怎么调都收敛不好,加上一个简单的两层MLP后一切回归正常。这个细节值不少精度。

6.2 优化器和学习率:LARS、AdamW各有各的使用前提

对比学习领域的一大特点是大batch训练,配合的是LARS优化器,它对大batch更友好。但如果你的batch很小,LARS的优势发挥不出来,还可能因为layer-wise自适应学习率带来额外的不稳定。我用单卡训练时更习惯用AdamW,学习率设置成1e-3到3e-3,再配合cosine learning rate schedule和warmup。如果在多卡大batch场景跑,LARS会顺畅很多,学习率可以按“batch size成比例放大”的方式粗调。

6.3 我的推荐调参顺序

拿一个新数据集跑对比学习时,我不会一开始就精调所有东西。我的顺序是:

  1. 先固定温度系数( \tau = 0.1 ),batch size设为显存能承受的上限;
  2. 用一个小子集过拟合,确认代码无bug;
  3. 跑完整数据集2~3个epoch,看loss下降趋势是否平滑;
  4. 如果loss曲线震荡或不降,实施最小改动排查:依次调整( \tau )、学习率、投影头隐藏层维度;
  5. 稳定后,再开始实验数据增强的强弱、batch size和负样本队列策略。

这个流程能最大程度减少多个变量同时变化带来的“调参鬼打墙”现象,也方便对比不同实验设置的真实收益。

6.4 最后的场景适配建议

自监督对比学习不是万能药,并非所有场景都适合一上来就套SimCLR。如果你的数据量很少(比如只有几千张图),负样本池太小,对比学习的优势发挥不出来,这时候更推荐先训练一个分类模型做初始化,再用对比学习做微调。如果数据量足够,但类别极度不均衡,清洗“假阴性”样本的优先级比调参高得多。

长期摸索下来,我觉得对比学习不收敛的问题,大部分都不是模型能力不足,而是“输入给损失函数的数据结构”和“损失函数自身的超参设置”没有匹配好。像搭积木一样,每一块都要对得上:数据增强温柔一点还是猛烈一点,负样本池够不够大,温度系数是鼓励困难样本还是均匀对待,这几件事想清楚了,训练自然就顺了。

我个人的体会是,对比学习调参非常像做化学实验,少加一滴反应物和多加一滴,产物完全不同。每次改动都尽量控制变量,做好实验记录,比凭感觉“试一下”高效得多。等你把温度系数和负样本数量摸出了手感,再去跑那些更大的模型,会发现自监督训练其实并没有传说中那么玄乎。

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

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

立即咨询