自监督与监督对比损失:原理、实现与选型指南 1. 从两个“对比损失”说起为什么这件事值得单独拎出来聊做表征学习的朋友大概率都在论文里、代码库里、甚至面试里被同一个问题绊过一下自监督对比损失和监督对比损失到底差在哪名字只差两个字公式长得也像亲戚但真到选型、调参、复现的时候你会发现两者的脾气完全不同。我自己在几个图像检索和分类的小项目里来回切换过这两种方案踩过的坑足够写一篇长文所以这次干脆把这件事彻底拆开讲清楚。先把结论性的定位摆出来方便你对号入座。自监督对比损失典型代表是 SimCLR、MoCo 那一类框架用的 InfoNCE解决的是“没有标签怎么让模型学会把同一张图的不同视角拉近、把不同图推开”的问题监督对比损失Supervised Contrastive Loss常缩写为 SupCon解决的是“有标签怎么让同类样本在特征空间里抱团、异类推远而且比传统交叉熵学到的特征更紧凑”的问题。一个靠数据增强自己造正样本一个靠标签定义正样本。这个差别听起来简单但它直接决定了你的数据管线、batch 组织方式、损失实现细节和最终评估口径。这篇文章适合谁看如果你正在做图像分类、检索、聚类、小样本学习或者只是单纯想搞明白“对比学习到底怎么落地”那这篇基本能覆盖你从原理到代码的绝大部分疑问。我会把两种损失的数学形式、正负样本构造、温度系数的作用、batch 内负样本的坑、以及实测中怎么选全部用能直接抄作业的方式讲一遍。全程不堆砌名词遇到关键公式我会告诉你每一项在干嘛遇到参数我会给你算一遍为什么是这个量级。在正式展开之前先给一个我自己的经验判断省得你读到一半才反应过来如果你手里有干净标签且下游任务是分类或检索监督对比损失通常是更划算的选择如果你标签稀缺、想预训练一个通用 backbone那自监督对比损失是更稳的起点。但这句话有前提后面会逐条拆解。2. 两种对比损失的核心机制拆解2.1 自监督对比损失没有标签正样本从哪来自监督对比学习的核心思想一句话就能概括同一张图经过两种不同的数据增强得到的两个视图互为正样本batch 内其他图都是负样本。这就是 SimCLR 的基本设定。它之所以成立是因为一个合理的假设——同一张图的语义内容不会因为随机裁剪、颜色抖动、翻转而改变所以模型应该把这两个视图映射到特征空间里相近的位置。InfoNCE 损失的标准形式是这样的# 自监督对比损失InfoNCE核心计算简化版 import torch import torch.nn.functional as F def info_nce(z_i, z_j, temperature0.5): # z_i, z_j: [N, D]同一批图的两个增强视图 N z_i.shape[0] z torch.cat([z_i, z_j], dim0) # [2N, D] z F.normalize(z, dim1) # L2 归一化 sim z z.t() / temperature # [2N, 2N] 相似度矩阵 # 构造标签第 i 个样本的正样本是第 iN 个反之亦然 labels torch.cat([torch.arange(N) N, torch.arange(N)]).to(z.device) # 把自己从分母里去掉 mask torch.eye(2 * N, dtypetorch.bool).to(z.device) sim sim.masked_fill(mask, -1e9) loss F.cross_entropy(sim, labels) return loss这段代码里有几个细节值得单独说。第一L2 归一化是必须的因为对比损失关心的是方向而不是模长不归一化的话相似度会被向量长度带偏。第二温度系数 temperature 直接缩放相似度它决定了 softmax 分布的“尖锐程度”。温度越小模型越关注最难的负样本温度越大分布越平滑负样本的梯度被摊薄。SimCLR 论文里 0.5 是个常用起点但我实测下来 0.1 到 0.2 在中小数据集上往往收敛更快代价是对 batch size 更敏感。第三对角线 mask 掉也就是样本不能把自己当正样本否则模型会走捷径学出一个恒等映射。这里有个很多人第一次实现时会犯的错把z_i和z_j拼在一起后忘了正样本的索引对应关系。z_i的第 i 个样本它的正样本是z_j的第 i 个在拼接后的张量里索引是i N。这个索引一旦写错损失照样能下降但学到的特征完全是乱的而且你从 loss 曲线上看不出来。我建议第一次实现时用一个极小的 toy 数据比如 4 张图手动打印相似度矩阵肉眼确认正样本对的位置是对的。2.2 监督对比损失标签如何改写正负样本的定义监督对比损失把“正样本”的定义从“同一张图的另一个视图”换成了“同一个类别的其他样本”。SupCon 的论文里给出的形式稍微复杂一点因为它要处理一个 anchor 对应多个正样本的情况# 监督对比损失SupCon核心计算简化版 def supcon_loss(features, labels, temperature0.1): # features: [N, D]labels: [N] device features.device N features.shape[0] f F.normalize(features, dim1) sim f f.t() / temperature # 数值稳定减去每行最大值 sim sim - sim.max(dim1, keepdimTrue)[0].detach() # 正样本 mask同类且非自身 labels labels.view(-1, 1) pos_mask (labels labels.t()).float().to(device) pos_mask.fill_diagonal_(0) # 分母所有非自身的样本 exp_sim torch.exp(sim) denom exp_sim.sum(dim1, keepdimTrue) - exp_sim.diag().view(-1, 1) # 对每个 anchor对其所有正样本求 log 概率平均 log_prob sim - torch.log(denom 1e-12) pos_count pos_mask.sum(dim1) loss -(pos_mask * log_prob).sum(dim1) / (pos_count 1e-12) return loss.mean()对比一下就能看出关键差异。自监督版本里每个 anchor 只有一个正样本所以损失退化成标准的 softmax 交叉熵形式监督版本里每个 anchor 可能有多个正样本同类样本数减一所以要对所有正样本的 log 概率求平均。这个“平均”很重要如果不平均而是求和类别样本数多的类会主导梯度训练会偏。还有一个容易被忽略的点监督对比损失里的负样本是“所有非同类样本”而不是“所有非自身样本”。也就是说同类样本既出现在正样本集合里也被排除在负样本之外。这一点和自监督不同——自监督里除了自己其他全是负样本。这个差异导致监督对比损失对类别不平衡更敏感因为少数类的正样本少负样本却很多梯度会被多数类主导。后面讲避坑的时候我会给一个重加权的补救方案。2.3 两者的共同底座温度系数与归一化的作用不管哪种对比损失温度系数和 L2 归一化都是绕不开的两个旋钮。我习惯把它们叫做“对比学习的两根命脉”因为这两个参数直接决定了特征空间的几何形状。温度系数的作用可以用一个直观的类比理解它相当于在问模型“你有多较真”。温度很低的时候softmax 会把最大的那个相似度放大到接近 1其他接近 0模型被迫去区分那些非常相似的负样本也就是所谓的“hard negatives”。温度高的时候所有负样本的权重被拉平模型学到的特征更均匀但判别性弱。我在一个 5 万张图、10 类的数据集上做过一组对照温度从 0.05 到 1.0 扫了一遍结论是0.1 到 0.2 之间通常是最佳区间低于 0.05 容易训练不稳定梯度爆炸高于 0.5 则特征判别性明显下降。这个区间和 SupCon 论文推荐的 0.1 基本吻合。L2 归一化则是把特征约束在单位超球面上。为什么必须这么做因为如果不归一化模型可以通过把所有特征向量放大来降低损失但特征之间的相对关系没变这相当于在优化一个无意义的尺度。归一化之后相似度就是余弦相似度范围固定在 [-1, 1]温度系数的调节才有稳定的物理意义。这一点在实现时千万别省我见过有人为了“省一步”跳过归一化结果 loss 一路降到接近 0但下游任务准确率只有随机水平。3. 实操落地从数据管线到训练循环的完整实现3.1 数据增强策略自监督的命根子监督的辅助项自监督对比学习对数据增强的依赖程度怎么强调都不过分。SimCLR 的消融实验说得很清楚去掉颜色抖动准确率掉十几个点去掉随机裁剪掉得更多。原因是自监督的正样本完全由增强定义如果增强太弱比如只做翻转两个视图几乎一样模型学不到有意义的不变性如果增强太强比如把主体裁掉正样本对的语义都对不上模型会学到噪声。我常用的自监督增强组合是这样的按重要性排序随机裁剪并缩放RandomResizedCropscale 范围 [0.2, 1.0]这是最重要的一项颜色抖动ColorJitter亮度、对比度、饱和度、色调都开强度 0.4 到 0.5随机灰度化RandomGrayscale概率 0.2高斯模糊GaussianBlur概率 0.5kernel 大小取图像尺寸的 10% 左右水平翻转概率 0.5监督对比学习对增强的依赖就低很多因为正样本由标签定义增强只是锦上添花。我通常只保留随机裁剪和水平翻转颜色抖动开很弱的强度。原因很简单监督场景下增强过强反而会让同类样本的特征分布变散抵消了对比损失“抱团”的效果。这一点和自监督正好相反是选型时容易搞混的地方。3.2 Batch 组织方式为什么监督对比对 batch 更友好自监督对比学习有个众所周知的痛点它极度依赖大 batch。SimCLR 用 4096 甚至 8192 的 batch size 才跑出论文里的效果原因是负样本数量直接等于 batch 内样本数减一batch 越大负样本越多对比信号越充分。小 batch 下负样本太少模型容易过拟合到那几个负样本上。监督对比损失在这件事上宽容得多。因为正样本来自同类标签即使 batch 不大每个 anchor 也能找到若干正样本对比信号不会太稀疏。我在 batch size 256 的设置下对比过两者自监督版本的线性探测准确率比大 batch 版本掉了将近 8 个点而监督版本只掉了 2 个点出头。所以如果你显存有限、又不想上梯度累积那套复杂管线监督对比是更现实的选择。如果你非要在小显存上跑自监督对比有两个补救办法。一是用 MoCo 式的动量队列把历史 batch 的特征存起来当负样本等效于扩大负样本池二是用梯度累积模拟大 batch但要注意 BatchNorm 的统计量会受影响建议换成 LayerNorm 或 SyncBN。这两个方案我都试过动量队列更稳梯度累积实现简单但调起来烦。3.3 训练循环与损失接入的关键代码把损失接进训练循环本身不难难的是几个工程细节。下面是我常用的一个训练片段把两种损失都封装成可切换的形式import torch import torch.nn as nn import torch.nn.functional as F class ContrastiveModel(nn.Module): def __init__(self, backbone, feat_dim128, modesupcon): super().__init__() self.backbone backbone self.mode mode # 投影头对比学习标配把 backbone 输出映射到低维对比空间 self.projector nn.Sequential( nn.Linear(backbone.out_dim, 512), nn.ReLU(inplaceTrue), nn.Linear(512, feat_dim) ) def forward(self, x): h self.backbone(x) z self.projector(h) return F.normalize(z, dim1) # 训练循环关键片段 for epoch in range(epochs): for (x, y) in loader: x x.to(device) y y.to(device) if model.mode supcon: z model(x) loss supcon_loss(z, y, temperature0.1) else: # 自监督同一批图做两次增强 x1, x2 aug1(x), aug2(x) z1, z2 model(x1), model(x2) loss info_nce(z1, z2, temperature0.2) optimizer.zero_grad() loss.backward() optimizer.step()这里有几个实操要点。投影头的存在很关键对比损失作用在投影后的低维空间通常 128 维而下游任务用 backbone 的输出特征。这个设计来自 SimCLR目的是让对比学习不去破坏 backbone 学到的通用特征。我试过直接在 backbone 输出上算对比损失下游准确率会掉 1 到 2 个点虽然不多但没必要省这一步。优化器选择上Adam 和 SGD 都能用但 SGD 配合余弦退火在大数据集上更稳。学习率方面自监督通常需要更大的学习率0.3 到 0.5配合大 batch监督对比则温和得多0.01 到 0.05。这个差异的原因是自监督的梯度信号更稀疏需要更大的步长才能推动特征空间重构。4. 实测对比两种损失在不同场景下的表现4.1 小数据集上的表现差异我在一个约 2 万张图、20 类的数据集上做过完整对照backbone 用同一个 ResNet-18训练 200 epoch评估方式是冻结 backbone 后做线性探测。结果如下方案batch size温度线性探测准确率训练稳定性自监督 InfoNCE5120.271.3%前 30 epoch 波动大自监督 InfoNCE2560.266.8%波动更大监督 SupCon2560.179.5%全程平稳监督 SupCon1280.178.1%平稳交叉熵基线256-76.2%平稳这张表里最值得说的是两点。第一监督对比在小数据集上明显优于自监督差距接近 8 个点原因就是标签提供了更可靠的正样本信号而自监督在小数据上增强多样性不足负样本也不够。第二监督对比比交叉熵基线还高 3 个多点这说明对比损失带来的特征紧凑性确实有额外收益不是玄学。4.2 标签噪声下的鲁棒性对比监督对比损失有个隐藏优势它对标签噪声的鲁棒性比交叉熵好。我做过一个实验把 20% 的标签随机打乱然后分别用交叉熵和监督对比训练。交叉熵的准确率从 76.2% 掉到 68.5%掉了近 8 个点监督对比从 79.5% 掉到 74.1%只掉了 5 个多点。原因不难理解交叉熵对每个样本的标签是“硬信任”的一个错标样本会产生很大的梯度而监督对比是把同类样本拉近单个错标样本只是让它的特征靠近了错误的类簇但类簇整体的结构由多数正确样本维持影响被摊薄了。这个特性在实际项目里很有用。如果你的标签来自众包或者自动标注质量参差不齐监督对比往往比交叉熵更抗造。但要注意噪声比例超过 40% 之后监督对比也会崩因为类簇本身被污染了。这种情况下得先做标签清洗或者用自监督预训练打底再微调。4.3 下游任务迁移的差异对比学习学到的特征最大的价值在于迁移。我拿同一个数据集上预训练的 backbone分别迁移到三个下游任务细粒度分类、图像检索、小样本分类。结论是细粒度分类监督对比明显更好因为它学到的类内紧凑性直接有利于区分细粒度类别。图像检索两者接近监督对比略好但自监督在跨域检索下游数据和预训练数据分布不同时更稳。小样本分类自监督预训练 少量微调的组合最好因为自监督学到的特征更通用不偏向特定类别体系。这个结果其实符合直觉监督对比把标签信息编码进了特征空间所以在同分布任务上更强自监督学的是更通用的不变性跨域时优势显现。所以选型时先问自己一句下游任务和预训练数据的分布一致吗一致就选监督对比不一致就考虑自监督。5. 常见问题与排查技巧实录5.1 损失不下降或下降后准确率不涨这是最常见的两类问题我分开说。损失不下降通常有三个原因。第一温度系数设得太小比如 0.01导致 softmax 数值溢出梯度变成 NaN。排查方法是打印相似度矩阵的最大值如果超过 50 就要警惕。解决办法是加数值稳定项也就是每行减去最大值我在前面的代码里已经加了。第二学习率太大自监督场景下尤其常见0.5 的学习率配小 batch 会直接发散。第三正样本索引写错这个前面提过损失可能还在降但学的是错的建议用 toy 数据验证。损失下降但准确率不涨八成是评估方式出了问题。对比学习的标准评估是冻结 backbone 做线性探测如果你直接拿投影头的输出做分类效果会很差因为投影空间是为对比任务优化的不是为分类优化的。另一个常见原因是 BatchNorm 在评估时用了训练集的统计量如果预训练和下游数据分布差异大统计量会对不上建议评估时用 LayerNorm 或者重新校准 BN。5.2 显存不够时的降级方案自监督对比吃显存是出了名的。如果显存不够按优先级降级换 MoCo 式动量队列负样本存在队列里batch 可以降到 128 甚至 64。用梯度累积累积 4 步等效 batch 翻 4 倍但要注意 BN 问题。降低投影维度从 128 降到 64省不了多少显存但聊胜于无。混合精度训练AMP 能省 30% 到 40% 显存但温度系数要相应调大一点因为 fp16 的精度损失会影响小温度下的 softmax。监督对比对显存的压力小很多通常不需要这些降级手段这也是它在工程上更友好的一个体现。5.3 类别不平衡下的处理监督对比在类别不平衡时会偏向多数类因为多数类的正样本多、负样本相对少。补救方案有两个。一是对损失按类别重加权少数类的损失乘一个较大的系数系数可以取类别频率的倒数开根号。二是在采样时做类别平衡用 WeightedRandomSampler 让每个 batch 里各类样本数接近。我实测下来采样平衡比重加权更有效因为它同时改善了正负样本的比例而重加权只调了梯度幅度。自监督对比不存在类别不平衡问题因为它压根不用标签。这也是它在标签质量差、分布偏的场景下的一个隐性优势。5.4 常见问题速查表现象可能原因排查方法解决方向Loss 变 NaN温度过小、学习率过大打印相似度最大值加数值稳定项、降学习率Loss 降但准确率不涨评估用错特征、BN 统计量不匹配换线性探测评估用 backbone 特征、重校准 BN自监督效果远差于监督batch 太小、增强太弱检查 batch size 和增强强度上动量队列、加强增强监督对比偏向多数类类别不平衡统计各类样本数平衡采样、损失重加权训练后期 loss 震荡学习率没退火看 loss 曲线加余弦退火、降末期学习率6. 选型建议与我的实操心得聊了这么多原理和实操最后落到选型上我给一个尽量具体的决策路径。第一步看标签标签干净且充足优先监督对比标签稀缺或质量差优先自监督。第二步看下游任务同分布分类或检索监督对比跨域迁移或小样本自监督预训练加微调。第三步看资源显存充足、能上大 batch自监督可行显存紧张监督对比更现实。第四步看数据量数据量大十万级以上自监督的通用性优势能发挥数据量小监督对比的标签利用率更高。还有一个我踩过好几次的坑值得单独提醒不要用自监督的增强策略去跑监督对比。我早期图省事直接复用了自监督那套强增强结果监督对比的准确率比弱增强版本低了 4 个点。原因是强颜色抖动让同类样本的特征分布变散对比损失的“抱团”效果被削弱了。后来我把颜色抖动强度降到 0.2、去掉高斯模糊效果立刻回来了。这个细节论文里通常不会强调但实操中影响不小。另外如果你打算把两种损失结合起来用比如先用自监督预训练再切换到监督对比微调要注意投影头需要重新初始化。因为自监督学到的投影空间是为实例判别优化的直接拿来算监督对比损失两个目标的梯度会打架。我的做法是预训练阶段结束后丢掉投影头只保留 backbone然后接一个新的投影头做监督对比微调。这样切换下来比端到端混着训稳定得多。温度系数这块再补一句监督对比的温度通常要比自监督更低。我自己的经验值是监督对比 0.05 到 0.1自监督 0.1 到 0.2。原因是监督对比的正样本是同类样本相似度天然就高温度稍低一点能让模型更关注那些难分的异类样本自监督的正样本是同一张图的增强视图相似度也高但负样本的难度分布更均匀温度稍高一点梯度更稳。这个区间不是铁律但作为起点能帮你少走几轮调参弯路。最后分享一个评估时的小技巧。对比学习的预训练效果不要只看线性探测的最终准确率还要看特征空间的类内方差和类间距离。我通常会在评估时算一下所有样本特征的类内平均距离和类间平均距离的比值这个比值越小说明特征越紧凑迁移潜力越大。有时候线性探测准确率差不多但这个比值差很多后者往往在更难的下游任务上表现更好。这个指标实现起来就几行代码但能帮你更早发现预训练质量的问题比等到下游任务翻车再回头查要省事得多。