☰
2024对比学习实战:文本嵌入、多模态与工程落地论文清单
2026/10/1 8:12:57 网站建设 项目流程

1. 2024年还在聊对比学习,聊的到底是什么

"对比学习"这四个字这两年被提得越来越少,但它其实已经悄悄钻进了你每天在用的每一个模型里。做RAG的人在调Embedding模型,本质是在调对比学习的损失函数;做图像检索的人在调DINOv2的特征,用的还是自蒸馏里那套正负样本逻辑;甚至连大模型的偏好对齐,DPO那套公式拆开看就是一个没有负样本队列的对比损失。真正变的是SOTA的位置——三年前大家比的是ResNet-50上linear probe的Top-1精度,现在比的是7B嵌入模型在MTEB榜单上的名次,比的是SigLIP在图文检索上的零样本表现。

这篇内容想做的事很具体:把2024年前后对比学习这条技术线上还值得读的东西挑出来,讲清楚每一个关键选择背后的"为什么",然后给出一份能直接照着跑的论文清单和代码落地路线。如果你是刚入门的算法工程师,看完能知道从哪篇开始读、代码从哪抄;如果你已经在做向量召回或者多模态对齐,希望能帮你在损失函数、负样本构造、训练稳定性这几件事上少走几个月弯路。

1.1 一句话说清这套范式的骨架

对比学习的核心逻辑朴素到有点反直觉:不给模型标注类别,只告诉它"这两个东西是一回事""这两个东西不是一回事",然后让它自己去学一个把语义压进向量空间的编码器。形式化的写法就是InfoNCE:

L = -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 MLP,target用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, temp=0.05): # 同一批数据 forward 两次,dropout 产生两个不同表示 emb1 = model(input_ids, attention_mask, return_dict=True).pooler_output emb2 = model(input_ids, attention_mask, return_dict=True).pooler_output return info_nce(emb1, emb2, temp) def info_nce(z1, z2, temperature=0.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), device=z1.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, threshold=0.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), tau=0.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, device=img_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往往只有几个词,信息量极低。用强模型把短描述改写成完整的长句子,加入颜色、位置、动作这些细节,图文对比学习的信号质量会显著提升。这个方向的代表工作是DCI(Dense 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 + predictor,target分支 = encoder + projector(EMA更新,且梯度不回传)。如果你把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 数据增强强度的拿捏

数据增强的强度是对比学习里最玄学的部分,但它有一条可以量化的原则:增强后的两个视图在语义上必须仍然一致,但表面特征要有明显差异。

图像上常用的组合:RandomResizedCrop(scale从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/simclr
2Momentum Contrast (MoCo)2020动量队列解耦batch sizefacebookresearch/moco
3Bootstrap Your Own Latent (BYOL)2020无负样本,predictor+EMAdeepmind/byol
4Exploring Simple Siamese Networks (SimSiam)2021证明不对称结构即可防坍缩facebookresearch/simsiam
5Barlow Twins2021冗余消除,跨相关矩阵趋近单位阵facebookresearch/barlowtwins
6Emerging Properties in Self-Supervised ViT (DINO)2021自蒸馏+centering/sharpeningfacebookresearch/dino
7DINOv22023可迁移视觉特征,蒸馏出小模型facebookresearch/dinov2
8SimCSE2021dropout即增强,文本对比学习入门princeton-nlp/SimCSE
9Text Embeddings by Weakly-Supervised Pre-training (E5)2022弱监督对大规模预训练microsoft/unilm
10C-Pack (BGE)2023中文嵌入,RetroMAE+指令微调FlagOpen/FlagEmbedding
11Sigmoid Loss for Language Image Pre-Training (SigLIP)2023解耦batch size,可学习biasgoogle-research/big_vision
12Matryoshka Representation Learning2022嵌套维度,弹性存储各嵌入库已内置
13Scaling Deep Contrastive Learning Batch Size (GradCache)2021用小显存吃大batchprinceton-nlp/GradCache
14NV-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_workers=True能省不少启动开销。

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_output,doc侧用前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归一化,写进索引时忘了归一化,或者在服务端算余弦的时候又算了一次归一化——这两种情况都会让相似度的量纲错乱。最省事的办法是在向量写库之前统一做一次归一化,服务端只做点积,不再碰归一化。这个细节看着小,但它导致的问题排查起来特别费劲,因为指标只是"略微变差",很难定位。

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

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

立即咨询