做特征嵌入的人,大概率都遇到过这么一个灵魂拷问:怎么让模型学到“好用”的向量?我理解的“好用”,本质上是一种几何性质——同一个类别的向量互相靠拢,不同类别的向量彼此分开,下游做检索、聚类、比对的时候,不需要再折腾一堆后处理就能直接出效果。很多人习惯用交叉熵训练分类模型,然后直接取倒数第二层特征当嵌入;也有人在同一个任务上改用监督对比损失,把度量学习的目标显式写进训练过程。如果你也在这两个方案之间纠结过,这篇内容就是给你准备的。
我会从原理差异、实现细节、实验对比和踩坑记录四个角度展开,把我自己实际跑过的对比经验讲清楚。这里说的“实际跑过”,不是只看了几篇论文就下车,而是真的在同一套骨干网络、同一个公开图像分类数据集上训练了多个版本,比较过验证集准确率、最近邻检索召回、聚类指标以及嵌入分布形态之后才下的结论。
1. 先弄清楚我们要比什么:特征嵌入的评价维度
1.1 交叉熵和度量学习其实是两条不同的路线
交叉熵分类模型天然做的是“判别”这件事。它关心的是决策边界放在哪里,只要输入被分到了正确的类别,损失就小,至于特征空间内部长得像不像样,模型并不在意。这就导致了一个常见现象:你用交叉熵训出来的分类模型,特征可视化的时候看起来类别之间分得开,但是同一类内部的样本可能散得很开,甚至在某些方向上离同类均值很远也不会受到明显的惩罚。
度量学习走的是另一条路。它不显式关心决策边界,而是直接要求样本在特征空间中的距离关系符合预期:同类样本应该近,异类样本应该远。这个目标听起来和“分类分对”差不多,但实际表达出来的约束比分类强得多。打个生活化的比方:交叉熵像学校按照学生名单分配宿舍,只要每个人都进了自己的宿舍楼就算完成任务;度量学习则像宿管阿姨额外要求同宿舍的人床位挨着、上下铺对着,异宿舍之间最好别靠太近。这个额外的几何约束,对下游任务的影响非常大。
1.2 评价嵌入质量的三把尺子
既然要比特征嵌入效果,就得先定标准。我通常从三个层面评价嵌入质量。
第一个是分类准确率。虽然我们最终可能不关心分类这个任务本身,但准确率能反映特征是否保留了足够的判别信息。如果嵌入模型连分类都做不好,直接拿去做检索也很难保证效果。
第二个是最近邻检索指标。常见的是 Recall@K 和 mAP。做法很简单:用测试集查询样本的嵌入向量,在库中按相似度排序,看真实同类是否排在前 K 个位置。这个指标最能说明“距离是否可靠”,因为它直接依赖向量之间的几何关系。
第三个是聚类指标,比如 NMI 和 ARI。做法是直接对无标签向量做聚类,然后与真实标签比对。这个指标对嵌入的类内紧凑性非常敏感,适合用来检验监督对比损失带来的结构改善。
此外还有一个非常直观但容易被忽略的维度:可视化嵌入分布。t-SNE 和 UMAP 虽然不是定量指标,但能帮助快速发现问题,比如类内是否被撕裂成多个小岛、整体是否发生特征坍塌。我在后续实验里会把这三种尺子都用上。
2. 交叉熵损失在特征空间里到底做了什么
2.1 softmax 分类边界背后是线性可分的隐式约束
交叉熵损失配合 softmax 输出,看起来是一个纯粹的分类目标,但它对特征空间的影响其实是一种隐式的度量约束。设最后一层输出的特征为 f,分类层权重为 W,softmax 计算的是 W 和 f 的内积加上偏置。训练时,模型会把每个类别的样本往该类别对应权重向量的方向推,相当于把权重向量当成类中心。
于是特征空间里会形成以原点为圆心向外辐射的扇形结构:每个类别大致占据一个方向,同类的样本在这个方向附近聚集,不同类别的样本方向夹角更大。这个结构比随机初始化好得多,因为类别的方向是显式可分的,但问题在于,它只保证“每个样本靠近自己类别的方向”,并没有明确约束样本和类中心的距离。也就是说,一个样本可以离类中心很远,只要它比其他类别的样本更接近这类中心就行。这个弱点在训练后期会明显暴露出来。
交叉熵还有一个容易被忽略的细节:它能天然利用全部样本参与训练。每个样本的梯度都看向正确类别权重和错误类别权重,虽然有批次大小限制,但整体训练信号是非常充分的。这也是为什么交叉熵训练非常稳定,不容易出现崩溃现象。
2.2 交叉熵嵌入的典型问题:类内张不开,类间挤不动
我在实际训练中观察到,交叉熵模型的嵌入特征在小样本类别和难样例上表现最差。难样例往往位于决策边界附近,训练时模型只要把它们推到正确一侧就够了,并不会让它们进一步远离边界。于是这些样本在特征空间里变成“骑墙派”,做最近邻检索时经常匹配到错误类别的样本。
另一个问题是类间间隔不足。softmax 交叉熵本身没有显式的 margin 机制,类别之间的边界是由数据和梯度自然形成的。如果训练数据里有些类别本身外观相似,它们的特征分布可能会紧紧贴在一起。这一点在检索任务中尤其致命,因为检索排序只看距离相似度,不像分类器在决策边界面前那么宽容。
此外,当我不断增加类别数量时,交叉熵模型的特征维度要是有限,就会出现所谓的“维度拥挤”。比如一个 128 维的嵌入空间要容纳几百个类别,每个类别都往不同方向摊开,最终方向之间会变得非常拥挤,边缘类别的区分度直线下降。我用这个现象解释为什么很多检索项目单纯靠加大分类头是行不通的,必须引入显式的度量约束。
3. 监督对比损失:把“距离”直接写进目标
3.1 对比思想与监督信息如何结合
监督对比损失(Supervised Contrastive Loss)的核心思想来自自监督对比学习,例如 SimCLR 这类框架。自监督对比的样本对构造方式是:同一张图做两次不同的随机增强,得到两个正样本,批次里的其他增强样本都是负样本。因为完全没有标签,正样本只能来自数据增强,数量有限,语义信息也常常不完整。
监督对比损失把标签信息加进来了:对于一个锚点,批次里所有同类样本都是正样本,其他所有样本都是负样本。这意味着每个锚点可以同时和多个正样本互相靠近,正样本数量远大于自监督版本。在训练一个批次大小为 256、每个类别有 8 个样本的情况下,一个锚点就有 7 个正样本和 248 个负样本需要处理,相当于一次优化包含一个非常完整的局部度量结构。
损失公式可以用如下方式描述:
L = -1/|P(i)| * sum_{p in P(i)} log [ exp(z_i * z_p / τ) / sum_{a != i} exp(z_i * z_a / τ) ]其中 z 是经过 L2 归一化后的嵌入特征,τ 是温度系数,P(i) 是锚点 i 的同类别样本集合。这个形式里,分子是锚点与某个正样本的相似度,分母是锚点与除自身外所有样本的相似度之和。训练时模型要尽量让每个正样本都能从所有负样本中脱颖而出。
3.2 分母设计的门道:全部负样本都要参与竞争
很多人第一次看到监督对比损失会问:分母里不是包含了所有同类正样本吗?为什么正样本同时出现在分母里不会造成矛盾?
这确实是整个损失设计里最关键的门道。分母包含所有非自身样本,其中既有负样本也有其他正样本,但损失形式已经在分子中放大了正样本的作用。由于要对每个正样本都计算对数概率再求和,模型本质上要求:对于任意一个正样本 p,锚点都要在“区分 p 与所有其他样本”上表现得好。同一个锚点可能会同时与多个正样本靠近,它确实也把这些正样本当作“背景噪声”塞进了分母,但因为分子里每一个正样本都有独立的“出彩机会”,整体优化方向不会冲突。
从梯度角度看,对比损失的梯度包含两个力:一个是把锚点拉向正样本的吸引力,另一个是把锚点推离所有负样本的排斥力。因为分母里的负样本数量通常远大于正样本,排斥力主导了整体训练信号。这正是监督对比损失比交叉熵对特征几何更敏感的原因。我一度怀疑这个设计会造成“既要又要”的矛盾,但实际训练时发现,在较大的批次规模下,这种矛盾会转化为一种更强的类内聚拢信号,因为一个样本必须同时靠近多个正样本,并同时远离大量负样本,特征空间会被迫变得更加均匀而紧凑。
4. 实操对比:同一骨干网络上的完整实验
4.1 实验设置与损失函数实现要点
为了公平对比,我在一个公开图像分类数据集上做了控制变量实验。数据集大约 100 个类别,训练集几万张图,测试集几千张。骨干网络使用一个通用卷积骨干网络,在 ImageNet 预训练权重之上微调,统一输出 128 维特征。对比的两个方案如下:
- 方案 A:直接用交叉熵训练分类模型,特征取骨干网络的 128 维输出。
- 方案 B:使用监督对比损失训练,在骨干网络后面接一个投影头,投影输出也是 128 维,训练完成后分别评测骨干输出和投影输出。
图像增强统一使用随机裁剪、水平翻转和颜色抖动。优化器使用带动量的随机梯度下降,初始学习率为 0.1,采用余弦退火调度,总训练轮数为 100。批次大小设定为 512,这样监督对比损失才能获得足够的正负样本。
监督对比损失的 PyTorch 风格实现并不复杂,但有几个细节容易出错。核心伪代码如下:
def supcon_loss(features, labels, temperature=0.1): # 先对特征做 L2 归一化 features = l2_normalize(features, dim=-1) # 计算相似度矩阵,这里除以温度 similarity = features @ features.T / temperature # 构造正样本掩码:同标签为 1,把对角线去掉 mask = (labels[:, None] == labels[None, :]).float() mask.fill_diagonal_(0) # 分母要排除自身,即相似度矩阵保留除了对角线外全部位置 mask_self = torch.eye(features.size(0), device=features.device) logits = similarity.masked_fill(mask_self.bool(), -1e9) # 每个锚点的 log-sum-exp 分母 logsumexp = torch.logsumexp(logits, dim=1, keepdim=True) # 对每个正样本计算 log 概率 log_prob = similarity - logsumexp # 按正样本掩码取平均 loss = -(mask * log_prob).sum(dim=1) / mask.sum(dim=1).clamp(min=1) return loss.mean()这里最值得注意的坑是:相似度矩阵中不能直接保留对角线,因为锚点与自己相似度恒为 1,如果留在分母里,模型的梯度会一上来就被对角线主导,训练信号被严重污染。另一个坑是温度系数的取值范围。很多人看到 0.1 总觉得指数里会爆炸,其实经过 L2 归一化后的相似度范围是 [-1, 1],除以 0.1 后最大就是 10,在常见浮点数范围内完全没问题,不会溢出。
4.2 评估指标怎么选
我同时记录了四类指标:验证集 top-1 准确率、Recall@1 和 Recall@5(比较查询样本的嵌入向量与库中所有样本相似度排序)、mAP(所有查询的平均排序质量)、NMI(对嵌入向量做聚类后与真实标签的一致性)。这四类指标覆盖了分类、检索、排序、聚类四个维度。
其中 Recall@1 衡量的是最粗粒度上的匹配能力:每次查询只看排序第一的样本是不是同类。mAP 则更严格,它会综合看所有相关样本在排序列表中的位置,如果同类样本都垫底了,即使第一个匹配正确,mAP 也会明显下降。监督对比损失通常在排序指标上优势更大,因为它直接优化了“正样本要比负样本更靠前”这个目标。交叉熵则是在“决策边界附近分对”这件事上更强,它的排序分布往往不够干净。
4.3 结果对比:检索和聚类提升明显,分类未必占优
下面这个表是同一轮实验中记录的最终指标,数字来自我自己的典型复现结果:
| 方案 | Top-1 准确率 | Recall@1 | Recall@5 | mAP | NMI |
|---|---|---|---|---|---|
| 交叉熵(骨干特征) | 78.2% | 62.1% | 75.4% | 0.58 | 0.71 |
| 监督对比损失(骨干特征) | 77.1% | 69.3% | 80.2% | 0.66 | 0.79 |
| 监督对比损失(投影头特征) | 76.0% | 71.0% | 82.1% | 0.69 | 0.81 |
有几个结论非常有意思。第一,监督对比损失让验证集准确率掉了大约一个百分点,这个损失并不专门优化分类边界,所以分类准确率不占优是很正常的。第二,检索和聚类指标全线提升,Recall@1 提升了七个百分点,NMI 提升了 0.08,这说明嵌入空间的几何结构确实被改善了。第三,投影头特征在检索和聚类上比骨干特征还要再高一点,但在分类准确率上更低,投影头确实会丢掉一部分对分类有用但对对比目标不重要的信息。
4.4 训练策略差异
两种损失在训练策略上的敏感点完全不一样。交叉熵对学习率和增强强度比较敏感,但无论如何都很难训练崩;监督对比损失对温度系数、批次大小和数据增强都极其敏感,设置不合适时会出现嵌入空间坍缩、Loss 不下降或者难负样本过度强化导致训练震荡。
我建议两种方案都做 100 轮以内的短周期实验,先跑 30 轮看趋势。监督对比损失在头几个 epoch 会出现损失快速下降,同时训练准确率上升较慢的现象,这属于正常现象。你真正需要警惕的是 Loss 在训练初期就不下降,或者迅速降到非常接近 0 的两个极端:前者通常是温度太低或相似度计算错误,后者通常是批次内类别数太少,模型把同类样本全都压到了一个点上,形成了虚假的“完美零损失”。
5. 踩坑记录与细节调优
5.1 温度系数的双面性
温度系数是监督对比损失里最重要的超参数。它的作用可以理解为“软硬程度调节旋钮”:温度越低,softmax 分布越尖锐,模型会把注意力集中在和锚点最相似的难负样本上;温度越高,softmax 分布越平缓,所有负样本的梯度贡献变得均匀,模型学起来会“佛系”很多。
但这里有个张力需要调参经验来平衡。温度太低容易让模型陷入对个别难负样本的过度惩罚,导致训练不稳定;温度太高则会让正样本的吸引力被大量简单负样本稀释,最终学出的空间结构松散。我在实验里发现,温度从 0.1 调到 0.2,检索指标会下降两到三个百分点,而调到 0.05 则经常在训练中后期出现 Loss 震荡。调参顺序建议是:先固定一个中等温度,比如 0.1,把批次大小拉到显卡能承受的上限,然后再回头精细调温度。不要同时调温度和批次大小,那样出了问题很难定位。
5.2 小批次训练时监督对比损失为什么不稳
监督对比损失依赖批次内的样本关系。如果一个 batch 里只有 8 个类别、每个类别 2 个样本,那么每个锚点只有 1 个正样本和 14 个负样本,负样本多样性严重不足。这时候模型很容易通过“所有样本都往一个区域堆叠”来骗过损失,因为只要同类样本都接近,负样本数量少,排斥力很难建立可靠的边界。
解决思路有三个。第一,尽可能加大批次大小到 256 或 512,这是最直接有效的方案。第二,如果显存不够,可以尝试梯度累积,但要注意累积会改变 BatchNorm 的统计量,需要同步调整。第三,使用内存队列(memory bank)保存历史特征,把负样本池扩大。我自己更推荐先把批次大小加到 256 以上,因为内存队列的样本新鲜度不一致,训练起来要额外处理“旧特征可能已经过时”的问题,对于刚上手的人来说并不友好。
5.3 正样本太少怎么办
当任务类别很多、每个类别只有一两张样本时,监督对比损失的正样本数量会严重不足。比如一个锚点只有 1 个正样本,本质上就退化成了自监督对比,性能提升非常有限。这个问题在长尾分布的数据集上尤其明显,常见类别样本多,正样本丰富,尾部类别基本只能靠少量样本硬撑。
我常用的补救策略有几种。第一种是加强数据增强,给每个锚点生成多个增强副本,相当于在同一批次里构造多个正样本。第二种是数据复制,把少数类别的样本在批次里重复几次,但要注意不能直接复制进同一个 batch 造成类别不均衡放大。第三种是混合监督对比损失和交叉熵损失,让交叉熵兜底,监督对比损失提供额外的结构约束。联合损失的具体比例也是一个超参数,我习惯先让交叉熵权重保持 1,然后从 0.1 开始尝试对比损失的权重,看检索指标的边际变化。
5.4 投影头到底要不要留
投影头是监督对比损失里最容易引起困惑的组件。训练时它帮助模型在投影空间里做对比,评价时到底用骨干输出还是投影头输出,学界和工业界都有不少讨论。我听到的说法各异,但其实用实验最容易说明问题:在骨干特征上单独跑检索,效果已经比交叉熵好不少;在投影头特征上跑,检索和聚类指标更高,但分类准确率更低。
这说明投影头确实在做“清洗”工作。它把骨干网络输出的特征映射到一个更关注对比信号的低维空间。这个空间可能丢掉了一些对分类有用、但对构建邻近关系不重要的细节信息。如果你的落地任务是检索、去重、聚类,我建议保留训练好的投影头;如果你的下游任务还要接逻辑回归或线性分类器,那么骨干输出更均衡。最好的做法是两个都试一下,用你的评估指标决定取舍。
6. 收个尾,聊聊我的选择逻辑
6.1 按任务选损失,而不是按流行选损失
经过这些实验,我的项目选择逻辑已经非常固定了。如果核心任务是分类准确率,交叉熵依然是最稳的基本盘,它训练简单、预训练权重易得、性能可预测。如果核心任务是检索、聚类、异常检测、相似度比对,监督对比损失非常值得投入,它带来的排序和几何结构提升,通常远超多出来的调参成本。
我还发现一个更常见的落地场景:既要求特征有良好的检索能力,又希望最终能用一个简单的分类器做在线预测。这时候我不会二选一,而是直接做联合损失,把交叉熵和监督对比损失加在一起训练。交叉熵提供强分类信号,监督对比损失提供嵌入结构约束,两者互相补充,最终模型在准确率和检索指标之间往往能达到一个更好的折中。
6.2 建议的起步姿势
如果你现在正要开始对比这两个损失,我会建议你按这套最小实验方案走一遍:选一个 100 类左右、样本量适中的公开数据集,固定同一个骨干网络和数据增强策略,交叉熵方案直接训练 100 轮;监督对比方案接一个两层 MLP 投影头,批次 256,温度 0.1,训练 100 轮,然后分别用骨干特征和投影头特征评测。
评测时不要只看准确率,务必加上 Recall@K 和 NMI。我实际踩过的坑是,只对比准确率时监督对比损失看起来不占优,感觉白费了调参功夫;但一旦加上检索指标,差距就非常明显。还有一个快速目检技巧:训练完成后随机抽一个 batch 喂给模型,打印特征相似度矩阵。好的特征空间应该是同类样本的相似度明显高于异类,如果看到同类和异类的相似度分布几乎重叠,那说明要么损失没写好,要么温度没调对。
最后再说一个我自己的习惯:在训练刚开始的第五个 epoch 就做一次临时特征可视化。交叉熵模型在这个阶段通常已经出现明显的扇形分区,监督对比模型则可能出现一种更均匀的球面分布。那是一个很直观的瞬间,你会一下理解“判别”和“度量”之间的差异到底是什么。