资讯详情

资讯详情

对比学习避坑:正负对构造、温度系数与负样本参数配置

对比学习里最容易让人翻车的从来不是网络结构写不出来而是正负对怎么构造、温度系数设多少、batch size 和负样本数怎么匹配这三件事没想清楚。我见过太多实验backbone 换成更深的、数据加到几百万张、训练跑了两天linear probe 一测只比随机猜高几个点回头一查负样本全是同类别样本τ 还照抄了别人的 0.07。这篇就围绕“正负对”和“参数设置”这两个最核心的变量把我在多个视觉与跨模态项目里踩过的坑、调过的参数、验证过的配置尽量讲透。适合已经跑通过一次对比学习、但结果不稳定或者复现不出别人指标的同学也适合刚要从零搭一套自监督预训练流程的读者照着抄。1. 先把正负对这层逻辑捋清楚1.1 对比学习优化的到底是什么讲参数之前必须先讲清楚损失在干什么否则所有“τ 调到 0.1 就好了”的经验都是玄学。对比学习的骨架非常简单给一个锚点样本让它和它的正样本在表示空间里靠近和一堆负样本远离。落到最常见的 InfoNCE 形式一个锚点的损失写成$$ \mathcal{L} -\log \frac{\exp(s_p / \tau)}{\exp(s_p/\tau) \sum_{i1}^{K}\exp(s_{n_i}/\tau)} $$其中 $s_p$ 是锚点与正样本的余弦相似度$s_{n_i}$ 是与第 $i$ 个负样本的相似度$K$ 是负样本数量$\tau$ 是温度系数。这一行公式里其实藏着三个可调的旋钮正样本怎么定义决定 $s_p$ 是什么、负样本从哪来决定 $K$ 有多大、$-s_{n_i}$ 有多难、$\tau$ 取多少决定 softmax 的锐利程度。很多人把它当成一个普通的分类损失在用只关心 loss 数值降不降结果模型学到的表示在 downstream 上完全不 work——原因往往就在这三个旋钮里至少有一个是错的。我更愿意把对比学习理解成一种“隐式的、样本量等于负样本数的分类任务”每个锚点都在做一次 $K1$ 类的分类正样本是唯一的正确类所有负样本都是错误类而且类别定义是随着 batch 动态变化的。这个视角非常重要因为它直接解释了两件事为什么 batch size 对对比学习这么敏感batch 变了类别数就变了任务难度就变了以及为什么负样本的质量比数量在后期更重要。1.2 正负对是被“构造”出来的不是数据自带的监督学习里标签是给定的对比学习里正负对完全由你构造这是它最大的自由也是最大的风险点。图像领域的标准做法是同一张图做两次随机增强得到两个视图这两个视图互为正值batch 内其他所有视图为负值。这里有个隐含假设——“同一张图的不同视图”在语义上是同一个东西“不同的图”在语义上是不同的东西。第一个假设靠数据增强的强度来保证第二个假设靠数据集本身的类别分布来保证。两个假设任意一个破了正负对就是错的。举个我实际遇到的例子做一个商品图像的表示学习数据里同一个 SKU 有几十张不同拍摄角度、不同背景的图如果只按“同一张图的两次增强”构造正值那同一个 SKU 的其他图片就被当成负样本推开了模型学出来的表示会把同一件商品的表示拉散。反过来如果贪心地按 SKU 分组把所有同 SKU 图都当正样本正样本之间差异过大有的图只拍了包装盒一角又会把表示拉塌。这两种失败模式对应的其实是完全不同的参数策略前者的正样本太窄需要扩大正样本的定义范围后者的正样本太杂需要收紧增强强度或者引入加权。所以我在动手写 dataloader 之前一定会先回答三个问题正样本的语义边界在哪里、负样本池里有多少“其实是同一个语义”的伪负样本、数据量级能不能撑起我想要的负样本数。这三个问题的答案决定了后面所有参数的选择。1.3 用对齐性和均匀性给自己做体检调参最怕的是“没有中间指标可看”只有最后的 linear probe 结果。我一般会额外算两个廉价指标来监控表示质量对齐性alignment和均匀性uniformity。对齐性衡量正样本对之间的表示距离越小越好均匀性衡量所有表示在超球面上的分布是否均匀越小越好。经验公式是$$ \mathcal{L}{align} \mathbb{E}| \bar{z}i - \bar{z}{i^} |^2,\quad \mathcal{L}{uniform} \log \mathbb{E}, e^{-2|\bar{z}_i-\bar{z}_j|^2} $$这两个指标的好处是几十个 batch 就能估出来不用等训练结束。我遇到过 loss 一直在降但 linear probe 不动的情况一看对齐性很好、均匀性极差说明模型把所有样本挤到了超球面的一个小区域里典型的表示坍缩早期征兆——这种情况光看 loss 是看不出来的因为 loss 里的负样本项已经被压得很低了看上去“拟合得很好”。注意均匀性指标要在一批随机抽样的样本上算不要在同一 batch 内的样本上算因为同 batch 内的样本经过 BN 或者分布式采样之后分布有偏算出来的均匀性会虚高。2. 正负对的具体构造方案与选型2.1 图像增强决定正样本的下限如果只做单一模态的图像自监督正样本的构造就是增强组合的构造。增强太弱两个视图几乎一样模型只需要学会恒等映射就能把 loss 压下去学不到任何不变性增强太强两个视图的语义可能已经不一致了比如裁剪到只剩背景正样本本身就变成了噪声。我做过的几组消融里增强强度对最终指标的影响比网络深度还大。比较稳的一套做法是四件套随机裁剪加缩放scale 范围取 0.2 到 1.0、随机颜色抖动亮度、对比度、饱和度、色相各给中等强度、随机灰度化概率 0.2 左右、随机高斯模糊概率 0.5核大小随图像分辨率定。裁剪的 scale 下界是关键参数取太小比如 0.08在小数据集上会大量产生“只拍到背景”的视图取太大0.5 以上则模型学不到尺度不变性。我的经验是数据类别越细粒度比如细分类、商品、工业缺陷scale 下界就要越保守0.3 到 0.5 比较合适数据是场景级的比如自然图像、遥感大图可以放到 0.2 甚至更低。2.2 负样本池的三种来源与取舍负样本从哪里来基本上决定了整个训练框架的形态。我把它归成三类各有明确的适用边界负样本来源典型实现负样本规模优点主要风险batch 内负样本对称损失相似度矩阵2N-2N 为 batch size实现最简单负样本随训练实时更新受显存限制规模上不去同类别伪负多队列负样本动量编码器加先进先出队列可到 6 万以上小 batch 也能有大量负样本队列内特征滞后一致性有损外部记忆库全量特征聚类或采样存储理论上等于数据集大小负样本最丰富更新策略复杂容易引入分布漂移batch 内负样本是最容易上手的也是绝大多数复现实验的起点。它的问题很直接负样本数量被显存卡死单卡 24G 跑 224 分辨率batch size 撑到 256 已经算不错可用的负样本也就 510 个左右。这时候如果照抄论文里 4096 的大 batch 配置去设学习率和 τ训练基本会崩。队列负样本是我在小显存场景下最常用的替代方案。核心思路是维护一个固定长度的特征队列每个 step 把当前 batch 的特征入队、最老的出队负样本从队列里取。队列长度和动量更新系数是两个必须一起考虑的参数量队列越长负样本越丰富但队列里的特征越旧和当前编码器的表示空间偏差越大动量系数越大越接近 1动量编码器更新越慢队列内特征的一致性越好但对新表示的响应也越迟滞。我一般的起点是队列长度 16384 到 65536、动量系数 0.999数据分布变化剧烈比如多来源混合数据时把动量降到 0.99 左右。2.3 难负样本能提点也能把训练带沟里负样本不是越多越好也不是越难越好。InfoNCE 对每个负样本的梯度权重正比于它被分错的概率所以那些“和锚点很像但语义不同”的难负样本会获得很大的梯度。适度挖掘难负样本能显著提升判别力尤其是细粒度任务。但有两个坑我必须提醒。第一个坑是伪负样本。难负样本里混着大量其实和锚点同类的样本同一个类别、同一个实例的不同视角、同一段文本的不同表述。把它们当难负样本使劲推开等于在教模型把同类拆开downstream 指标会掉得很明显。判断方法很土但有效把 top-20 的难负样本可视化出来人眼看一遍如果一半以上是同类就说明你的数据里同类样本太密集需要么换负样本采样策略比如同类内排除要么引入标签做监督对比。第二个坑是难负样本挖掘的时机。训练早期模型的表示还很乱此时挖出来的“难负样本”多半是噪声过早引入会让训练不稳定。我的做法是前若干个 epoch 用纯随机负样本等 loss 曲线进入平缓区之后再开启难负挖掘并且挖掘比例从 10% 逐步加到 50%不要一步到位。2.4 多正样本与跨模态配对的正负对构造一旦涉及多正样本构造逻辑就要改。监督对比学习里同一类别的所有样本都互为正值损失从“一正多负”变成“多正多负”形式上是把每个正样本的 log-prob 求平均再取负。这里有个实现细节很容易写错分母里要排除锚点自己但必须包含所有正样本和负样本不能只排除正样本否则正样本会被重复计算两次梯度尺度就偏了。跨模态场景图文、视频文本、音频文本的正负对构造又有不同。标准的做法是双向对比图到文的匹配为正batch 内其他文为负文到图的匹配为正batch 内其他图为负。这种构造天然带来一个好处——负样本数量翻倍且是跨模态的判别难度更高。但它对 batch 内配对的要求极严格一旦某个 batch 里出现两张图对应同一段文本数据去重没做好就会产生伪负样本。我在一个图文检索项目里就是因为训练集里存在大量重复文本导致双向对比的 loss 一直虚高、检索指标上不去后来在 dataloader 里加了按文本哈希的去重才恢复正常。3. 温度系数与关键参数的设置逻辑3.1 τ 到底在调什么温度系数 $\tau$ 是整套参数里最容易被低估的一个。从梯度看更清楚对正样本相似度求偏导得到 $\partial \mathcal{L}/\partial s_p -(1/\tau)(1-p_p)$对负样本得到 $\partial \mathcal{L}/\partial s_n (1/\tau), p_n$其中 $p$ 是 softmax 后的概率。这说明 $\tau$ 同时起两个作用一是整体缩放梯度幅度$1/\tau$ 是公共因子二是改变概率分布的锐利度从而改变梯度在各个负样本之间的分配。$\tau$ 小分布尖锐梯度集中在那几个最难的负样本上判别力强但训练容易抖对伪负样本也最敏感$\tau$ 大分布平缓梯度被均摊到所有负样本上训练稳但学到的表示区分度不足。这就是为什么同一套代码τ 从 0.07 换到 0.5linear probe 可能差十几个点。下面这张表是我在不同数据规模下实际用过、并且复现过的 τ 取值范围可以直接当起点场景负样本规模τ 建议起点备注小数据集自监督十万级以内几百0.1 到 0.2太小会明显过拟合到伪负样本中等规模自监督百万级几千0.07 到 0.1论文常用区间稳定大规模预训练千万级以上万级0.05 到 0.07负样本多可以更锐利监督对比有标签几千0.1伪负样本少可适度调小跨模态双向对比几千0.07通常对 τ 更敏感建议先搜这个参数调 τ 的正确姿势不是单独调而是和 batch size 一起调。我的经验法则是负样本数翻 4 倍τ 可以往下压 20% 到 30%负样本数砍到四分之一τ 要往上抬。原因是负样本变多之后softmax 的分母里项变多如果不压低 τ正样本的概率会被稀释梯度信号变弱。3.2 batch size、负样本数与学习率的三角关系这三者的关系可以用一句话概括batch size 决定了任务难度学习率要跟着任务难度走负样本数决定了你能承受多大的学习率。batch 变大每个锚点的负样本变多分类任务变难单步梯度噪声变小所以可以用更大的学习率但学习率一大小 batch 下就会过冲。线性缩放规则在这里依然适用但基准点要自己重设。我一般的做法是先固定一个“等效基准配置”比如 batch 256、lr 0.03、SGD momentum 0.9、weight decay 1e-4然后按 lr 0.03 × batch/256 往上放大超过 batch 1024 之后改用 LARS 或者干脆不放大得那么激进乘一个 0.5 到 0.7 的衰减因子。warmup 是必须的尤其是在大 batch 下我通常设 5 到 10 个 epoch 的线性 warmup让模型先在一个小学习率下把表示空间铺开再进入正常训练。跳过 warmup 直接上大 lr前几十个 step 的 loss 会直接炸掉或者坍缩。还有一个常被忽略的点batch size 变了BN 的统计量也变了。用梯度累积模拟大 batch 时每个 micro-batch 的 BN 统计量是基于小 batch 算的和真正的大 batch 不等价。要么改用同步 BN要么把 BN 换成 LayerNorm 或者 GroupNorm否则你会得到一个“看起来像大 batch 训练但其实不是”的模型指标对不上论文。3.3 投影头、优化器与训练轮次投影头的存在感很低但作用不小。训练时在 backbone 后面接一个两层 MLP隐层宽 2048 到 4096输出 128 到 256 维损失全部在投影空间里算训练结束后把投影头丢掉只用 backbone 的特征做下游任务。这么做的好处是让投影头承担“把表示压到适合对比损失的空间”的职责backbone 可以保留更多对下游有用的信息。我做过对比去掉投影头直接用 backbone 输出算损失linear probe 一般会掉 2 到 5 个点。投影头输出维度取 128 是常见起点取太小比如 32会明显损失信息取到 512 以上收益就很小了还费显存。优化器方面大 batch 用 LARS 或者带 warmup 的 SGD 都行中小 batch 我个人更倾向 AdamW超参不敏感、调试成本低。SGD 的 weight decay 一般 1e-4 到 1e-6AdamW 我用 0.05 到 0.1 的 decoupled weight decay。训练轮次从 200 到 1000 epoch 都有但经验上超过 400 epoch 之后收益递减得很厉害与其堆轮次不如把增强策略和负样本质量做好。4. 实操一份能直接跑的损失实现与调试流程4.1 InfoNCE 的正确写法与三个易错点先给一份我常用的对称 InfoNCE 实现输入是已经归一化的两个视图特征import torch import torch.nn.functional as F def nt_xent(z1, z2, tau0.1): # z1, z2: [N, D], 已做 L2 归一化 N z1.size(0) z torch.cat([z1, z2], dim0) # [2N, D] sim z z.t() # 余弦相似度, 范围 [-1, 1] sim sim / tau # 屏蔽对角线避免自己和自己算相似度 mask torch.eye(2 * N, dtypetorch.bool, devicez.device) sim.masked_fill_(mask, float(-inf)) # 正样本索引i 的正样本是 iN同一下标空间内循环 targets torch.arange(2 * N, devicez.device) targets (targets N) % (2 * N) loss F.cross_entropy(sim, targets) return loss第一个易错点是数值稳定性。千万不要自己写exp再除再log一定要用cross_entropy或者log_softmax因为相似度除以小 τ 之后数值可能到几十直接exp会溢出成inf。混合精度训练下这个问题更突出我在 fp16 下见过 loss 变成 nan改成先log_softmax就没事了。第二个易错点是相似度矩阵的屏蔽范围。上面的实现屏蔽了对角线但没有屏蔽“同一张图的两个视图之间的双向关系”以外的项这是对的。但如果你用的是多正样本版本屏蔽逻辑要相应调整我见过有人把整个正样本块都屏蔽掉结果 loss 一直是个常数。第三个易错点是归一化位置。特征必须在算相似度之前做 L2 归一化这一步写在模型 forward 的末尾或者损失函数开头都行但只能做一次。重复归一化不会出错但会浪费算力漏掉归一化则会让相似度尺度随特征范数漂移等价于 τ 在训练过程中不断变化这是很多人 loss 抖动的隐藏原因。4.2 显存不够时的三条务实路线单卡 24G 想跑 4096 的 batch 基本没戏我这里有三条实际用过的路线按推荐程度排序。第一条是小 batch 加队列负样本。batch 压到 128 或 256用 16384 的队列补足负样本规模。代价是多了一个动量编码器和一份队列显存但整体收益最稳也是我目前的主力方案。实现上只要注意动量编码器不参与梯度、更新只用动量公式θ_k ← m·θ_k (1-m)·θ_q以及队列里的特征要 detach 即可。第二条是梯度累积加同步 BN。累积步数取 4 到 8等效 batch 就上去了。但前面说过 BN 统计量的问题必须配同步 BN否则等效性打折。另外梯度累积下 warmup 的步数要按优化器 step 数算不是按前向次数算这个细节写错了会导致 warmup 期短了 4 到 8 倍训练前期直接发散。第三条是混合精度加激活检查点。这条主要省显存对等效 batch 没有帮助但能让你在同样的卡上把分辨率提上去。分辨率对对比学习的影响其实比 batch 更直接224 提到 288 带来的指标提升往往比 batch 从 256 加到 512 还明显。提示三条路线可以叠加但别一次性全上。我一般先加队列再开混合精度最后才考虑梯度累积因为每加一层出问题时的排查成本就翻一倍。4.3 训练过程中该盯哪些曲线我训练对比学习模型时会同时盯四条线总 loss、对齐性、均匀性、以及一个小的验证集上的 kNN 分类准确率用冻结特征做无参数的近邻分类几个 epoch 算一次成本很低。四条线的组合能定位绝大多数问题loss 降但对齐性不降说明正样本构造有问题loss 降、对齐性好、均匀性差说明表示在坍缩loss 早期就降到很低但 kNN 准确率不动八成是正负对构造出了 bug比如正样本和负样本搞反了或者相似度矩阵索引错位。kNN 这条线特别值得加它比 linear probe 便宜得多又能提前十几个 epoch 反映出趋势。5. 常见问题排查实录5.1 一份速查表现象最可能的原因排查动作处理方式loss 前期就掉到很低并卡住正负对构造错误正样本被当负样本抽一个 batch 打印 target 索引与相似度矩阵修正索引映射检查增强是否生效loss 正常下降但下游指标不动表示坍缩或伪负样本过多算均匀性指标可视化 top 难负样本调大 τ、缩小 batch、清洗重复样本训练中期 loss 突然飙升学习率过大或 warmup 不足看前 100 step 的 loss 曲线降 lr、延长 warmup、加梯度裁剪换大 batch 后指标反而变差τ 未随负样本数调整对比不同 τ 下的均匀性负样本变多时适度压低 τ多卡训练结果和单卡对不上BN 统计量不一致检查是否用了同步 BN换同步 BN 或改用 LayerNorm混合精度下出现 nan相似度先 exp 后 log检查损失实现改用 log_softmax 或 cross_entropy队列方案的指标随队列变长而下降动量系数过小队列特征过旧对比不同动量系数提高动量系数到 0.999 附近5.2 几个反直觉但真实存在的现象第一个现象τ 不是越小越好但也不是越大越稳。我在一个细粒度数据集上把 τ 从 0.1 扫到 0.03前 50 epoch loss 降得更快但 200 epoch 后的 linear probe 反而低了 4 个点。原因是过小的 τ 让模型过早地把注意力集中在少数几个难负样本上而这些样本里有相当比例是伪负样本等于在放大错误信号。细粒度任务上我更倾向把 τ 放在 0.1 到 0.15。第二个现象batch size 大到一定程度后继续加反而不如加负样本队列。batch 从 1024 加到 4096负样本数翻了 4 倍但训练时间涨了 3 倍多指标只涨了不到 1 个点。同样的算力拿去把队列从 16384 加到 65536收益更明显。这个结论在不同数据集上不完全一致但在算力有限的前提下优先扩负样本池而不是扩 batch是我这几年比较稳定的经验。第三个现象数据增强的强度要和训练轮次匹配。增强很强的时候模型需要更多轮次才能收敛如果你只跑 100 epoch强增强的模型看起来指标很差但跑到 300 epoch 就反超了。所以调增强的时候不要用短训练做判断至少给到 200 epoch 再比。5.3 几个我踩过的小坑数据去重这件事在对比学习里比在监督学习里重要得多。监督学习里重复样本只是让某些类别的权重变大对比学习里重复样本会直接产生伪负样本——同一个样本的两份拷贝在 batch 内互相成为负样本被强行推开。我在一个爬取数据集上就吃过这个亏去重前后 linear probe 差了将近 6 个点。去重的粒度建议做到“近似重复”用感知哈希或者特征聚类都行。还有一个是关于评测协议的。不同论文的 linear probe 设置差别很大有的用 SGD、有的用 L2 正则的 logistic regression学习率搜索范围、训练轮次、是否做数据增强都不一样这些细节能让同一个 checkpoint 的指标差出 3 到 5 个点。我现在的做法是自己在固定的验证集上跑一套统一的探针协议只用自己的数字做横向比较跨论文的数字只当一个粗略参考。这个习惯帮我省了很多次“以为方法不行、其实是对比方式不对”的误判。最后分享一个调参顺序上的经验先把正负对构造和增强策略定下来再调 batch 与学习率最后才精调 τ。顺序反过来做你会发现 τ 怎么调都不对因为前面的变量一直在动。我一般的节奏是先用小规模数据十分之一左右跑通全流程确认 loss 曲线形态正常、kNN 准确率在涨再放大到全量数据做正式的参数扫描每次只动一个变量扫三到五个取值就够了。τ 的扫描建议在 log 尺度上做0.05、0.07、0.1、0.15、0.2 这五个点基本能覆盖绝大多数场景没必要做更细的网格。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →