
1. 2024年还在聊对比学习聊的到底是什么对比学习这四个字这两年被提得越来越少但它其实已经悄悄钻进了你每天在用的每一个模型里。做RAG的人在调Embedding模型本质是在调对比学习的损失函数做图像检索的人在调DINOv2的特征用的还是自蒸馏里那套正负样本逻辑甚至连大模型的偏好对齐DPO那套公式拆开看就是一个没有负样本队列的对比损失。真正变的是SOTA的位置——三年前大家比的是ResNet-50上linear probe的Top-1精度现在比的是7B嵌入模型在MTEB榜单上的名次比的是SigLIP在图文检索上的零样本表现。这篇内容想做的事很具体把2024年前后对比学习这条技术线上还值得读的东西挑出来讲清楚每一个关键选择背后的为什么然后给出一份能直接照着跑的论文清单和代码落地路线。如果你是刚入门的算法工程师看完能知道从哪篇开始读、代码从哪抄如果你已经在做向量召回或者多模态对齐希望能帮你在损失函数、负样本构造、训练稳定性这几件事上少走几个月弯路。1.1 一句话说清这套范式的骨架对比学习的核心逻辑朴素到有点反直觉不给模型标注类别只告诉它这两个东西是一回事这两个东西不是一回事然后让它自己去学一个把语义压进向量空间的编码器。形式化的写法就是InfoNCEL -log( exp(sim(z_i, z_i) / τ) / Σ_j exp(sim(z_i, z_j) / τ) )z_i是锚点样本的向量z_i是它的正样本分母里那一堆z_j是这一批里所有其他的样本全都被当作负样本。这个损失做的事情就是让分子尽可能大让分母尽可能小。翻译成人话——正样本的相似度往上拉负样本的相似度往下压。真正让这套东西work的是2020年Wang和Isola那篇分析里提出的两个度量alignment对齐度和uniformity均匀度。对齐度衡量正样本对之间的平均距离越小越好均匀度衡量所有向量在超球面上的分布有多散越接近均匀分布越好。一个好的对比学习模型就是在这两件事上同时做好——拉近该近的同时不让所有点挤成一坨。理解了这两个词你后面看任何一篇对比学习的论文判断它改进了什么就有了统一标尺。1.2 从图像预训练挪到文本嵌入与多模态SOTA位置换了人2020到2022年对比学习的主战场在图像自监督预训练SimCLR、MoCo、BYOL、SwAV轮番上阵比的都是ImageNet线性探测。那个阶段的核心矛盾是怎么在没有负样本或者负样本不够多的情况下防止表示坍缩。到了2023、2024年重心明显挪了三个方向。第一是文本嵌入MTEB榜单成了事实上的竞技场E5、BGE、GTE、NV-Embed这些模型把对比学习从预训练技巧变成了检索系统的核心部件。第二是多模态对齐CLIP之后SigLIP用sigmoid损失把batch size的枷锁解开了各种VLM都换成了它的视觉塔。第三是大模型的偏好对齐DPO、SimPO这一批工作本质上是在用成对数据做对比只不过负样本从batch里的其他样本变成了被拒绝的那条回答。这个重心迁移带来一个很实际的后果如果你现在入行做对比学习从文本嵌入切入的性价比远高于从图像自监督切入。图像那条线的工程量太大动辄几百卡跑几周而文本嵌入在单机8卡上就能复现出有意义的结论业务价值也更直接。1.3 温度系数τ为什么成了最难调的参数几乎每个调过对比学习的人都有这个体会学习率还能靠经验蒙τ是真的要一格一格试。原因在于τ是直接乘在logits上的它控制的是softmax的锐度等价于在调节模型有多关注最难的负样本。τ调小了softmax变得极其尖锐梯度几乎全部由相似度最高的那几个负样本贡献。好处是模型被逼着去区分细粒度差异坏处是训练极不稳定稍微有一点标注噪声或者假负例整个批次的方向就被带偏了。τ调大了分布变平每个负样本都分到差不多的梯度训练稳但学不到细节最后收敛到一个大家都差不多的平庸解。更麻烦的是τ和batch size是耦合的。负样本数量越多分母里累加的项越多最优τ就得越小。CLIP用的是可学习温度初始化为log(1/0.07)让模型自己在训练中找而文本嵌入这边常见的做法是固定0.05因为batch通常只有几十到几百。我自己的经验是batch在256以内τ从0.05开始试batch到4096这种量级τ往0.02甚至0.01压。另外一点很多人忽略——τ最好不要和L2归一化同时被优化器顺手改掉把归一化和温度处理成独立的模块排查问题时好定位得多。2. 视觉自监督这条线的四代方案怎么选2.1 SimCLR、MoCo、BYOL、SimSiam的路线分歧这四篇放在一起看其实就是一部怎么绕开负样本困境的编年史。SimCLR最直接一个batch里的其他样本全当负样本用NT-Xent损失。它的贡献是把数据增强组合投影头大batch这三件套固定下来让自监督第一次在ImageNet上逼近了有监督。代价是它对batch size的依赖近乎变态原论文用4096小卡根本跑不动。MoCo的思路是把负样本从当前batch解放出来用一个动量更新的队列存历史的键向量。队列里的向量来自过去若干个batch容量可以开到65536理论上负样本管够。同时用一个动量编码器θ_k ← m·θ_k (1-m)·θ_q来保证队列里老向量的表示和新向量在同一个语义空间里——这一步是整个方法的关键动量系数通常取0.999。MoCo的出现让8卡甚至4卡也能做对比学习工程意义极大。BYOL更激进直接不要负样本。它用一个online网络和一个target网络online多接一个predictor MLPtarget用EMA更新然后只让online去预测target的输出。一开始大家觉得它一定会坍缩结果它没坍缩后来SimSiam给出了解释真正的防坍缩机制是predictor stop-gradient这对不对称结构EMA其实是可选的。SimSiam把BYOL继续简化去掉EMA去掉负样本只留一个encoder、一个predictor和stop-gradient在ImageNet上依然能跑出接近的水平。这篇论文的结论很重要表示坍缩的根源不是缺少负样本而是优化目标的对称性。理解了这一点你在设计自己的对比学习目标时就知道不对称结构是一个可以主动使用的工具。这四条路线怎么选我的建议很实在如果你的任务有明确的语义类别、需要细粒度区分走SimCLR/MoCo这条有负样本的路判别性更强如果你的下游任务更看重特征的通用性和迁移能力BYOL/SimSiam这条无负样本的路更省事也不用担心假负例问题。2.2 DINOv2之后视觉特征的下限被抬高了DINO系列是自蒸馏路线的集大成者。DINO用ViT做骨干teacher的输出经过centering和sharpening之后作为soft label去监督student本质上是一种软化版的聚类分配。它最惊艳的地方在于训出来的ViT在完全不加微调的情况下注意力图就能直接拿来做分割。DINOv2把这条路推到了工程可用的程度。几个关键改动值得记下来数据侧构建了LVD-142M这个经过自建检索管线清洗的数据集用图像相似度做去重和平衡而不是简单地堆规模目标侧把DINO损失和iBOT的掩码图像建模损失拼在一起前者管全局语义后者管局部patch级别的细节产出侧蒸馏出一系列小模型ViT-S/B/L/g让下游不用再背一个巨型骨干。2024年还有一个容易被忽略的改动是register token。ViT里会出现一些数值极大的异常token它们会抢走注意力、破坏特征质量。DINOv2的作者通过在输入序列里额外拼几个可学习的token把这些异常值吸走特征的干净程度明显提升。这个技巧后来被很多视觉模型抄走。实测下来DINOv2特征在细粒度检索、图像去重这类任务上几乎可以做到提取特征余弦相似度就能出结果不需要任何微调。这直接把视觉特征的工程下限抬高了一大截。2.3 2024年做视觉特征自监督预训练还是CLIP微调这是我被问得最多的一个问题答案取决于你的数据条件和任务类型。如果你手里有几十万到几百万的带文本描述的图像直接拿CLIP系模型微调效果上限更高因为文本监督天然带有语义标签的信息量。但如果你只有纯图像数据或者任务是工业质检、医学影像这类CLIP预训练数据覆盖很差的领域那自监督预训练或者直接用DINOv2当初始化再微调是更稳的路。还有一个折中方案我个人用得比较多用DINOv2冻结骨干提特征只在后面接一个轻量的投影头做对比学习微调。这样既借用了大规模预训练的先验又用几十万条领域数据把特征拉到自己的分布上训练成本比全量微调低一个数量级。注意冻结的时候BatchNorm和LayerNorm要不要一起冻这个细节很影响结果——通常LayerNorm建议放开因为它参数量小、对分布偏移敏感。3. 文本嵌入的对比学习从SimCSE到7B大嵌入3.1 无监督SimCSE的精髓是把dropout当噪声SimCSE这篇论文的价值在于它用最低的成本把对比学习搬到了NLP上。做法简单到令人发指同一句话丢进BERT两次两次forward因为dropout的随机性会产生稍微不同的表示把这两个表示当作正样本对batch里其他的句子当负样本。这个dropout即增强的思路背后有个很关键的判断在文本上最安全的数据增强是不改变token序列的增强。图像可以做裁剪、翻转、变色因为语义不变文本你要是删个词、换个词语义可能就变了。dropout是唯一一个只扰动表示层、不动输入的操作所以它天然不会引入假正例。有监督版本稍微复杂一点。SimCSE用NLI数据集构造样本对蕴含关系的句子对当正例矛盾关系的当硬负例。这里有个坑论文里专门讨论过——NLI里的中性关系句子很容易被错当成负例但语义上它们可能非常接近这就是典型的假负例。所以做有监督SimCSE时一定要把中性样本单独处理要么剔除要么降低权重。还有一个容易忽略的实现细节SimCSE用的是BERT的pooler_output带tanh的MLP不是last_hidden_state的均值池化。两者效果差别不小复现时千万别写错。import torch import torch.nn.functional as F def simcse_unsup_loss(model, input_ids, attention_mask, temp0.05): # 同一批数据 forward 两次dropout 产生两个不同表示 emb1 model(input_ids, attention_mask, return_dictTrue).pooler_output emb2 model(input_ids, attention_mask, return_dictTrue).pooler_output return info_nce(emb1, emb2, temp) def info_nce(z1, z2, temperature0.05): z1 F.normalize(z1, dim-1) z2 F.normalize(z2, dim-1) logits z1 z2.t() / temperature # [B, B] labels torch.arange(z1.size(0), devicez1.device) # 对称化两个方向的梯度都均衡 return 0.5 * (F.cross_entropy(logits, labels) F.cross_entropy(logits.t(), labels))3.2 难负例挖掘ANCE、RocketQA与假负例这堵墙In-batch负样本有个天然缺陷一个batch里随机凑出来的句子跟锚点大概率八竿子打不着模型区分它们不费吹灰之力学不到东西。真正能提升效果的是难负例——那些在向量空间里离锚点很近、但语义上完全无关的样本。ANCE是这条路线的起点想法很直白每隔若干步用当前模型重新对全库做一次编码刷新负样本索引。这样负样本始终是当前模型认为最难的随着模型变强负样本也跟着变难。代价是索引刷新非常耗资源工程上一般用异步的方式做。RocketQA补了两刀。第一刀是跨batch负样本把多个GPU上的样本汇总起来互相当负例成本几乎为零但效果明显。第二刀是去噪用一个单独的模型判断某个负例是不是其实是正例是的话就丢掉。假负例是对比学习里最隐蔽的性能天花板。它的危害在于模型被强行要求把一个语义上完全正确的样本推开这直接和任务目标矛盾最后模型只能学到不要把所有东西都推得太远这种软塌塌的表示。识别假负例的办法有几个实用的用BM25或者当前的cross-encoder给候选负例打分分数超过某个阈值的直接剔除或者对同一批负例做多次采样统计模型判断的一致性不一致的降权。# 难负例构造的一个实用片段 def build_hard_negatives(anchor, positive, bm25_topk, model, threshold0.85): negs [] for cand in bm25_topk: if cand positive: continue # 用当前模型估一个相似度过高视为疑似假负例 score cosine(model.encode(anchor), model.encode(cand)) if score threshold: negs.append(cand) return negs阈值这个数别拍脑袋。我的做法是先在一小部分人工标注的样本上画一条分数分布曲线看正例和负例的重叠区间在哪把阈值定在重叠区的上沿附近。3.3 7B嵌入模型的诱惑与工程成本2023年底到2024年嵌入模型开始往大参数走E5-Mistral-7B、GTE-Qwen2、NV-Embed这一批模型在MTEB上把7B以下的模型全面压制。它们的共同套路是用大模型当编码器用prompt区分任务类型用大量合成数据做对比训练。E5-Mistral的关键贡献是证明了合成数据大模型的组合在嵌入任务上可行。它的做法是让一个强模型为无标注文档生成任务描述和查询构造出一批高质量的训练对。NV-Embed则更进一步提出了一种latent attention pooling的方式来聚合token表示并且采用两阶段训练——先做对比学习再用指令微调数据做第二轮同时主动放弃in-batch negatives因为大批量下的假负例问题在大模型上反而更严重。但作为工程决策7B嵌入模型的成本必须算清楚。检索场景下你要在全库上做推理7B模型比0.1B模型的推理成本高两个数量级延迟也上不去。向量维度通常也更大4096对标768存储和内存开销同步翻几倍。我的判断标准很实际只有一个场景值得上7B嵌入——离线重排、查询量不大、对精度极度敏感。在线召回这一层BGE-base、GTE-base这个量级的模型加上好一点的难负例挖掘性价比高得多。真的想要大模型的精度可以用大模型离线生成训练数据蒸馏到小模型上这是目前最划算的路。3.4 Matryoshka一次训练多档维度Matryoshka Representation Learning解决的是一个非常实际的痛点向量维度是固定的但不同场景对精度和存储的需求不一样。你不可能为了省存储重新训一个模型。它的做法是让嵌套的前缀子向量都能独立使用。训练时同时对768维、512维、256维、128维分别算一遍对比损失加起来当总损失。学出来的效果很有意思——768维的前256维本身就构成一个可用的256维表示前128维又是一个可用的128维表示。def matryoshka_info_nce(emb_a, emb_b, dims(768, 512, 256, 128), tau0.05): loss 0.0 for d in dims: # 直接切前缀不需要额外的降维矩阵 loss loss info_nce(emb_a[:, :d], emb_b[:, :d], tau) return loss / len(dims)实际收益很直观768维降到256维MTEB上的分数通常只掉一到两个点而存储成本直接省到三分之一。对于千万级以上的向量库这是实打实的钱。需要注意的是各个维度的损失权重可以不均等如果你主要用256维就把256维的权重调大一点这是可以调的。4. 多模态侧的关键转向SigLIP把batch size解耦了4.1 CLIP全局softmax的两个硬伤CLIP的图文对比损失是对称的InfoNCE图像找文本、文本找图像分母都是整个batch。这个设计在小规模上没问题一到大规模就暴露两个硬伤。第一个是通信开销。分母要对整个batch求和在分布式训练里必须把所有GPU上的特征all_gather到一起算然后再把梯度散回去。batch越大这个通信占比越高卡在通信上的时间可能比计算还长。第二个是假负例。CLIP的训练数据是网络爬来的图文对同一个batch里很可能有多张图描述的是同一件事。全局softmax会把这些语义正确的样本当成负例推开而且batch越大、数据越脏这个问题越严重。CLIP原论文里其实提到过要用超大批量32768但这个规模下的假负例噪声也是最大的这是一个很难调和的矛盾。4.2 Sigmoid损失、可学习bias与局部批量训练SigLIP的核心改动只有一句话把全局softmax换成一堆独立的二分类sigmoid。def siglip_loss(img_emb, txt_emb, logit_scale, logit_bias): n img_emb.size(0) logits img_emb txt_emb.t() * logit_scale logit_bias # 对角线是正样本记 1其余记 -1 labels 2 * torch.eye(n, deviceimg_emb.device) - 1 loss -F.logsigmoid(labels * logits).sum() / n return loss每一对图像和文本单独做一个是不是匹配的二分类判断匹配的拉高logit不匹配的压低logit。这个改动带来三个直接好处不再需要跨设备all_gather因为每对样本的loss是独立的只要本地batch内的配对就够算。通信量骤降训练可以更自由地扩展规模。每个正样本只和一个文本配对负样本的权重是平的不会因为softmax的归一化把某些噪声样本放大。可学习bias这一手很漂亮。训练初期随机初始化的模型对所有pair都给出接近0的logit此时正负样本的损失是不平衡的负样本太多会把模型往全都判负的方向带。加一个初始化为约-10的bias可以让初始状态下的正负损失量级接近收敛明显更稳。这个小细节是SigLIP能训起来的关键之一。实测结论SigLIP在batch size小一个数量级的情况下能追平甚至超过CLIP这对算力有限的团队来说是决定性的。2024年大量VLM的视觉塔直接换成了SigLIP不是没有原因的。4.3 数据质量这条线比模型结构更值钱聊到多模态对比学习很多人把注意力全放在损失函数上但2023年之后真正的进展几乎都在数据侧。DFN那篇工作的结论非常反直觉用一个训练好的数据过滤网络去筛数据比把同样的算力花在训更大的模型上收益更高。具体做法是训一个小的CLIP模型用它给海量爬取的图文对打分把噪声对筛掉然后用筛过的干净数据训大模型。筛完之后数据量可能只剩十分之一但训出来的模型反而更强。另外一个方向是描述文本的质量。原始的alt-text往往只有几个词信息量极低。用强模型把短描述改写成完整的长句子加入颜色、位置、动作这些细节图文对比学习的信号质量会显著提升。这个方向的代表工作是DCIDense Captioning with Interactions用多个模型互相验证生成的描述过滤掉幻觉内容。从工程角度看如果你只有几十万条自己的图文数据别急着改损失函数先花时间把描述文本重写一遍收益比任何结构改动都大。这条我也踩过——曾经花了两个月调损失和温度最后发现把caption重写一遍检索指标直接涨了十几个点。5. 训练不崩的细节损失、精度、增强与评估5.1 投影头和stop-gradient的组合逻辑投影头这个东西看起来不起眼但它是对比学习能work的关键组件之一。SimCLR里做了个很经典的消融把投影头的输出拿去做linear probe效果反而不如投影头之前的那一层。解释是投影头起到了信息瓶颈的作用它把和增强方式相关的那些冗余信息比如颜色、亮度吸收掉了让主干学到更纯粹的语义特征。实践中的配置图像任务通常用两层MLP隐藏层2048输出128文本任务输出维度一般和主干一致或者减半。投影头只在训练时用推理时直接扔掉取主干的输出。stop-gradient是无负样本方法防坍缩的核心。它的作用可以这样理解如果两个分支互相追赶最省事的解就是把两边都变成常数坍缩因为常数之间的预测误差是0。加上stop-gradient之后只有一个分支在被优化另一个分支是固定的靶子坍缩这条捷径就被堵死了。这两者的组合有讲究。BYOL的风格是online分支 encoder projector predictortarget分支 encoder projectorEMA更新且梯度不回传。如果你把stop-gradient去掉再跑一遍通常几十个epoch内就会看到表示坍缩——所有向量的余弦相似度趋近于1。5.2 混合精度下的all_gather与数值稳定性分布式对比学习最容易出的问题在all_gather上。几个必须注意的点第一归约必须用fp32。混合精度训练时各卡的梯度是fp16的跨卡求和很容易溢出。正确做法是把特征转成fp32再做通信算完loss再转回去。第二跨卡特征要不要回传梯度。如果是真正的负样本比如SimCLR风格梯度要回传到对应的卡上如果只是提供负样本梯度MoCo风格可以detach掉省一半通信。第三用GradCache解耦batch size和显存。这篇论文的思路是把一个大batch拆成若干个micro-batch每个micro-batch正常前向但只保存特征不回传梯度等所有micro-batch的特征都拿到之后拼成一个大batch算对比损失再把梯度分别回传到对应的micro-batch。这样显存占用和一个micro-batch一样但享受的是大batch的负样本数量。# GradCache 的核心思路伪代码 feats [] for micro_batch in split(batch, chunk_size): with torch.no_grad(): feats.append(model(micro_batch)) # 不建图省显存 feats all_gather(torch.cat(feats)) # 拼成大 batch loss info_nce(feats[:half], feats[half:]) # 大 batch 的对比损失 loss.backward() # 梯度逐 chunk 回传 for i, micro_batch in enumerate(split(batch, chunk_size)): model(micro_batch).backward(feats[i].grad)这个技巧在单机多卡上非常实用能把有效batch从256拉到4096而显存只涨一点点。5.3 数据增强强度的拿捏数据增强的强度是对比学习里最玄学的部分但它有一条可以量化的原则增强后的两个视图在语义上必须仍然一致但表面特征要有明显差异。图像上常用的组合RandomResizedCropscale从0.08到1.0、ColorJitter强度0.4左右、GaussianBlur、Solarize。SimCLR的消融结论是裁剪颜色的组合最关键单用其中一个效果都会明显掉。裁剪太弱的话模型学不到尺度不变性太强的话正样本语义就变了把一只狗裁到只剩尾巴。文本上的增强空间小得多。除了dropout比较安全的有回译翻译成另一种语言再翻回来但要小心翻译模型引入语义漂移、同义词替换只替换低频词、以及用大模型改写这个2024年用得最多也最贵。一个实用的判断方法训练几个epoch之后看正样本对的平均相似度。如果一开始就接近0.99说明增强太弱模型没压力如果一直低于0.5说明增强过头了正样本对已经不像了。5.4 评估别只看一个指标linear probe、kNN、alignment/uniformity对比学习的评估有个特殊性它训出来的特征不是直接用来做分类的所以评估方式的选择很讲究。Linear probe是最标准的做法冻结主干在特征上训一个线性分类器。它能反映特征的线性可分性但对特征的整体质量不够敏感。kNN分类更轻量不训练直接找最近的k个邻居投票。这个指标和检索类任务的相关性更高如果你做的是召回优先看kNN。Alignment和uniformity是诊断指标。alignment是正样本对的平均距离别让它太小说明增强不够uniformity是所有向量对相似度的指数平均越接近-2左右越好。训练过程中如果uniformity一直往上冲说明表示在坍缩。还有一个专门的坑评估时的归一化方式必须和训练一致。训练用了L2归一化评估时忘了归一化余弦相似度会被模长污染指标看着能差出一大截。这个问题我见过不止一次。6. 14篇必读论文清单与代码复现路线6.1 论文清单与阅读顺序下面这14篇是我自己排过阅读顺序的建议按序号读前5篇建立直觉中间5篇进文本和多模态最后4篇解决工程和理论问题。序号论文年份核心贡献代码仓库关键词1A Simple Framework for Contrastive Learning (SimCLR)2020确立增强投影头大batch范式google-research/simclr2Momentum Contrast (MoCo)2020动量队列解耦batch sizefacebookresearch/moco3Bootstrap Your Own Latent (BYOL)2020无负样本predictorEMAdeepmind/byol4Exploring Simple Siamese Networks (SimSiam)2021证明不对称结构即可防坍缩facebookresearch/simsiam5Barlow Twins2021冗余消除跨相关矩阵趋近单位阵facebookresearch/barlowtwins6Emerging Properties in Self-Supervised ViT (DINO)2021自蒸馏centering/sharpeningfacebookresearch/dino7DINOv22023可迁移视觉特征蒸馏出小模型facebookresearch/dinov28SimCSE2021dropout即增强文本对比学习入门princeton-nlp/SimCSE9Text Embeddings by Weakly-Supervised Pre-training (E5)2022弱监督对大规模预训练microsoft/unilm10C-Pack (BGE)2023中文嵌入RetroMAE指令微调FlagOpen/FlagEmbedding11Sigmoid Loss for Language Image Pre-Training (SigLIP)2023解耦batch size可学习biasgoogle-research/big_vision12Matryoshka Representation Learning2022嵌套维度弹性存储各嵌入库已内置13Scaling Deep Contrastive Learning Batch Size (GradCache)2021用小显存吃大batchprinceton-nlp/GradCache14NV-Embed2024LLM当嵌入器latent attention poolingnvidia 官方仓库6.2 复现环境与高频报错复现这14篇里的大部分代码环境上其实很简单PyTorch 2.x CUDA 12.x Apex可选。但有几个坑几乎每个人都会踩。NCCL超时多卡跑对比学习时最常见的就是NCCL timeout。八成原因是某个进程OOM被杀了导致锁同步的其余进程一直等。解决方式是把NCCL的超时时间从默认的30分钟调短出现问题能快速失败同时打开TORCH_NCCL_ASYNC_ERROR_HANDLING。all_gather导致的显存翻倍把各卡特征gather到每张卡上是一个[world_size × B, D]的张量B大的时候显存直接爆。GradCache就是专门治这个的。DataLoader的worker数对比学习的数据增强比较重num_workers设小了GPU空转设大了内存吃满。通常每个GPU配4到8个worker比较合适注意persistent_workersTrue能省不少启动开销。fp16的logits溢出算对比损失之前一定要把相似度矩阵转成fp32尤其是当温度系数很小0.01量级的时候除以温度之后很容易超fp16的范围。6.3 从论文到业务的两条最短路径如果你读完这14篇想立刻用在业务上有两条路最省事。第一条路是召回场景拿BGE或者E5的中文/多语版做初始化用自己的业务数据构造正负例正例来自点击/购买/同簇负例用BM25挖难例用info_nce hard negatives做对比微调Matryoshka开起来控制存储。这条路从零到上线两周能出第一版。第二条路是图像特征直接用DINOv2官方的ViT-B/14提特征接FAISS做检索。如果领域差异大工业、医疗用冻结骨干轻量投影头的方式微调。这条路的上手成本几乎为零甚至不需要训练。两条路的共同点是别一开始就改损失函数。先把数据管好、把负例挖好、把评估做扎实这些的收益比调模型结构大得多。7. 我实际跑过的几个应用方案7.1 RAG召回与重排dual-encoder 硬负例RAG系统里对比学习最直接的应用就是召回模型。典型架构是双塔query塔和doc塔共享底座或者独立底座用对比损失训练。我踩过的一个大坑是负例比例。一开始我觉得负例越多越好用了1:30的比例结果模型学会了把所有东西都推开的平庸解。后来调到1:7左右1个正例配7个负例其中2到3个是硬负例其余是in-batch随机负例效果明显好转。这个比例不是定死的但硬负例和随机负例的比例比总数量更重要。第二个坑是查询侧和文档侧的不对称。query通常很短十几个字doc很长几百到上千字。如果两边用同一个池化方式短文本的表示会明显偏弱。常见的处理是query侧用带权重的池化或者直接取pooler_outputdoc侧用前N个token的均值池化两边各训各的。重排器这一层也可以用对比学习。把cross-encoder的输出做softmax正例一条、负例若干本质上就是一个listwise的对比损失。相比pointwise的交叉熵这种方式对排序位置更敏感。7.2 细粒度图像检索与去重图像侧我的经验是别从零训。直接用DINOv2的ViT-B/14输入分辨率拉到518提取的cls token特征做L2归一化后丢进FAISS的IVF-PQ索引百万级库的检索延迟能控制在几十毫秒。阈值选择是图像去重里最需要花时间的地方。不要用固定阈值而是先在小样本上画相似度分布找出正负样本的重叠区把阈值定在重叠区的中间偏保守的位置。工业场景下宁可漏掉一些重复也不要误删。如果领域差异确实大冻结骨干微调投影头的方案我用过几次只训一个两层的MLP用同一张图的不同裁剪作为正样本对几千张图就能训出明显的领域适配效果训练时间不到一小时。7.3 时序与图数据上的对比学习时序数据上的对比学习最大的难点在于什么算是不改变语义的增强。TS2Vec的做法比较有代表性同一条序列的两个随机时间片段互相作为正样本同时做timestamp masking和随机裁剪。我做过的一个设备异常检测项目里有效的增强是同一时间段但不同传感器的读数作为正样本因为不同传感器反映的是同一个物理过程。这个思路和图像上的多视图是一致的——正样本的本质是同一语义的不同观测角度而不是简单的数据扰动。图数据上GraphCL总结了四种增强节点丢弃、边扰动、属性掩码、子图采样。它们的效果高度依赖数据集节点分类任务上属性掩码通常最好图分类任务上子图采样更稳。2024年比较流行的一个简化是SimGCL干脆不用图增强直接在表示上加噪声当作增强在推荐场景里跑得又快又好。7.4 偏好对齐里的对比思想DPO与SimPO这一块很多人没意识到它也是对比学习。DPO的损失函数长这样让模型对偏好回答的log概率相对参考模型提升对拒绝回答的log概率相对参考模型下降。把它写开本质上就是一个只有一对正负样本的对比损失只是相似度换成了log概率比。SimPO在这个基础上做了两处简化去掉了参考模型直接优化模型自己的log概率差加了长度归一化因为长回答的log概率天然更低不归一化的话模型会学出越短越好的偏好。这两处改动让训练成本降了一半效果反而不差。这个方向给我的启发是对比学习的框架比我们想的更通用。只要有成对的正负信号不管信号来自数据增强、用户点击还是人类偏好都可以套进同一套数学结构里。7.5 线上落地的三个坑最后说三个我在上线阶段踩过的坑都是文档里不会写的。第一模型版本和索引版本必须强绑定。换了一版嵌入模型旧的向量索引就废了必须全量重建。重建期间如果查询走的是新模型、索引还是旧的召回质量会崩得很难看。务实的做法是双写双读一段时间用新版模型建一份新索引流量慢慢切过去。第二离线指标涨了不代表线上涨。对比学习很容易在离线评估上过拟合到某种特定的相似度分布。上线前最好做一次A/B至少看看点击率、召回覆盖率这些线上指标有没有同步变化。我见过离线涨了5个点、线上纹丝不动的情况原因是线上查询的分布和离线测试集差别太大。第三嵌入的归一化方式在整条链路上必须一致。训练时L2归一化写进索引时忘了归一化或者在服务端算余弦的时候又算了一次归一化——这两种情况都会让相似度的量纲错乱。最省事的办法是在向量写库之前统一做一次归一化服务端只做点积不再碰归一化。这个细节看着小但它导致的问题排查起来特别费劲因为指标只是略微变差很难定位。