ANCE — 让模型自己挖训练负例,对比学习的"自给自足"
是什么
ANCE 是一种让稠密检索模型自己挖出训练用的”难负例”的方法。日常类比:考研刷题时,最有用的不是随便抓 100 道题,而是专挑你上次模考做错的那种题——错题反复练才进步快。ANCE 干的就是这件事,只不过”考生”是 BERT 双塔模型,“错题”是它自己当前最容易混淆的段落。
它解决的是 DPR(dpr-2020)的硬伤:DPR 训练时的负例来源有两种,要么是同 batch 别人的正例(in-batch),要么是 BM25 召回的混淆段落。两者都是静态、外部的——模型练到一定程度后,这些负例对它来说”太简单了”,损失函数算出的梯度趋近于零,等于没在练。
ANCE 的解法:每隔 m 步就用当前模型自己的索引去 top-k 检索一遍全库,把分数最高但不是正例的段落当作下一轮的负例。这样负例难度永远跟着模型水平涨。
为什么重要
不理解 ANCE,下面这些事都没法解释:
- 为什么 2021 年之后稠密检索论文几乎全都在用”动态 hard negatives”——RocketQA / coCondenser / E5 / BGE 全是 ANCE 风格的衍生
- 为什么单向量稠密检索能追上 ColBERT(colbert-2020)的多向量精度——差距主要不在模型容量,而在训练时见过的负例分布
- 为什么训练大型检索器需要单独一台 GPU 当”负例挖掘机”——这是 ANCE 引入的标准做法
- 为什么对比学习里”负例难度”被当成一阶问题对待,而不是工程细节
核心要点
ANCE 的全部巧思可以拆成 三步:
-
观察:随机负例梯度衰减。论文给了一个理论结论——SGD 的梯度范数有一个下界,依赖负例的”难度”。如果负例随便选,梯度大小按
1/N衰减;只有让负例逼近模型当前的决策边界,梯度才能维持有效大小。这个推导把”为什么 in-batch negatives 不够用”上升到了数学层面。 -
方法:用模型自己的 ANN 索引挖。每 m 步,把当前 checkpoint 拿去编码整个段落库,重建 faiss 索引。然后对每个训练 query 检索 top-200,去掉已知正例,剩下的当作 hard negatives 喂给下一轮训练。
-
工程:异步双进程。训练 GPU 不停,专门一个 Inferencer 子进程后台刷索引。训练用的负例永远比当前模型滞后 m 步——这是个工程妥协,但论文证明了陈旧负例好过没有新负例。
三种负例策略对比
| 策略 | 难度 | 训练成本 | 何时停止有效 |
|---|---|---|---|
| in-batch(随机) | 低 | 零额外 | 模型一变强就失效 |
| BM25 hard | 中 | 一次性预算 | 模型超过 BM25 后失效 |
| ANCE (ANN) | 高 | 持续刷索引 | 永不失效(跟着模型涨) |
与”自蒸馏”的本质区别
ANCE 容易被误认为是某种”自蒸馏”——模型自己当老师。但有个关键差别:自蒸馏用模型自己的输出当正信号(让学生学老师的预测),而 ANCE 用模型自己的输出当负信号源(找出哪里它最容易混淆)。前者强化已有偏好,后者主动暴露盲区。这个差别让 ANCE 不会陷入”自己反复确认自己”的退化循环。
实践案例
案例 1:为什么 in-batch negatives 会”训不动”
batch=128,里面 128 对(query, 正例段落)。in-batch 负例是把别人的正例当我的负例。问题:随便抓一个别人的问题对应的段落,跟我这个问题在语义上通常风马牛不相及——模型轻易就分对了,loss 接近零,没梯度。
DPR 的解法是加 BM25 hard negatives 救场,但 BM25 是词法模型,它认为难的段落(关键词重合)跟稠密模型认为难的段落(语义相近)不是同一批。所以 BM25 hard negatives 只能填一部分坑。
案例 2:ANCE 在 MS MARCO 上的实测
| 模型 | MRR@10 | NDCG@10 (TREC DL19) |
|---|---|---|
| BM25 | 0.187 | 0.501 |
| DPR | 0.311 | 0.604 |
| ANCE | 0.330 | 0.648 |
| ColBERT (多向量) | 0.349 | 0.694 |
注意 ANCE 是单向量模型,但 MRR@10 比 DPR 涨了近 2 个点,NDCG@10 涨了 4.4 个点。论文进一步把 ANCE 做到 BERT-large,在某些指标上追平甚至超过当时的 ColBERT——核心差距确实在训练负例。
案例 3:异步刷索引的工程细节
刷一次 MS MARCO 全库(880 万段)的索引大概要 10 分钟(V100 GPU)。训练步频通常 2k-10k 步刷一次,所以训练用的负例总比当前模型滞后几千步。论文测过:m=2000 和 m=10000 几乎没差别——只要不要让索引旧到上一个时代就行。
实际部署时常用一台单独的 GPU 当 Inferencer,训练 GPU 完全不被打断。两边的同步靠共享文件系统:训练进程写出最新 checkpoint,Inferencer 读到后重建索引并把负例采样结果写回训练进程读取的目录。整个机制没有消息队列,纯靠”文件 mtime + 轮询”——非常朴素的工程实现。
这种”训练快、刷新慢”的非对称模式,后来在很多领域都被复用:RLHF 的 reference policy 刷新、知识图谱嵌入的负例采样、推荐系统的曝光样本生成——本质都是同一个图样。
案例 4:warm-start 的必要
ANCE 不能从零开始训。论文里都是先用 BM25 hard negatives 训一个 DPR 出来,再切换到 ANCE。原因:第一轮的 ANN 索引由”随机初始化的模型”产生,挖出来的”hard negatives” 其实是噪声。这是 ANCE 的隐式前提,后续工作(如 coCondenser)专门优化了无监督预训练这一步来弥补。
案例 5:损失函数没变,只换了负例来源
ANCE 的对比损失就是标准的 InfoNCE:
loss = -log( exp(sim(q, p+)) / [ exp(sim(q, p+)) + Σ exp(sim(q, p-_i)) ] )q 是问题向量,p+ 是正例段落向量,p-_i 是 N 个负例。唯一的不同是 p-_i 来源——DPR 从 in-batch 或 BM25 取,ANCE 从当前模型自己的 ANN 索引取。
模型架构、优化器、学习率全都不变。这个”最小改动 + 最大收益”的形式,是 ANCE 之所以被广泛采用的另一个原因——任何已有的 DPR 训练流水线都能改两行代码切到 ANCE。
踩过的坑
-
false negative 噪声:top-k 里可能藏着没被标注的真正例。训练时把它当 hard negative 推开,等于教模型”对的别选”——是反向监督。后续的 RocketQA 用交叉编码器做去噪过滤,就是为了治这个。
-
索引刷新成本高:百万段级别要 10 分钟一次,亿级段落库(如 web 规模)几乎不可能频繁刷。后来工作用蒸馏 / 缓存 / 局部更新等手段降低这个成本。
-
m 太小反而抖动:刷得太勤,每轮负例分布跳动剧烈,训练不稳。m 通常设得跟一个 epoch 量级相当,让模型先消化完上一批负例。
-
不能直接换检索任务用:ANCE 的训练循环依赖”有标注正例”,跨域迁移时(如医疗、代码)需要重新挖正例对,不是即插即用。
-
m 太大也有代价:刷得太慢,负例越来越接近”上一个时代的难”,等于又退化成静态 hard negatives。论文给的经验是 1-2 个 epoch 刷一次较稳。
-
batch 内重复:同一个 query 上一轮挖到的 hard negative,下一轮可能又被挖到。论文没特别处理,但实践中常加去重缓存避免梯度偏向少数难样本。
适用 vs 不适用场景
适用:
- 大规模文本检索的训练后期——模型已经超过 BM25 baseline,需要更难的负例
- 有充足 GPU 资源做异步 inference 的场景
- 需要单向量模型达到接近多向量精度的场景(推理快、存储省)
不适用:
- 训练初期 / 冷启动——必须先用 BM25 hard negatives 暖身
- 标注稀疏的场景——false negative 比例太高反而有害
- 段落库特别大且无法频繁重建索引(如 100 亿+ 文档)
历史小故事(可跳过)
2020 年微软 Bing 团队在 DPR 出来不久后就观察到一个现象:稠密检索器训到一半就停滞,损失曲线变平。他们试了很多方法——加大 batch、改学习率、换正则——都没用。最后从对比学习的梯度推导反过来想:是不是负例本身太弱?验证后发现:用模型自己挖的 top-k 段落当负例,损失曲线立即重新下降。这就是 ANCE 的起源——一篇方法源于”调不动”的论文。
ICLR 2021 接收时审稿意见的关键反馈也很有意思:审稿人最初质疑”这只是工程把戏,理论分析像后补的”。作者补了梯度范数下界证明后才被说服——这一段附录的数学,反过来让方法的”为什么有效”有了可推广的答案,而不仅是”我试了管用”。
之后两年,几乎所有稠密检索 SOTA(RocketQA、coCondenser、E5、BGE)都把 ANCE 当作底层训练循环,只是在”如何更快更准地挖负例”上做改进。RocketQA 加了交叉编码器去噪,coCondenser 改了预训练任务让 warm-start 更稳,E5 把规模拉到亿级文本对,BGE 把多任务训练塞进同一个循环——但”用模型自己的索引挖 hard negatives”这个核心循环始终没动。
学到什么
- 训练分布要追着推理分布走——静态外部负例迟早跟不上模型,自挖才能持续有梯度
- 梯度范数下界这个分析视角,把”难负例”从经验技巧变成可推导结论
- 异步双进程是检索领域的常见模式:一个进程算梯度,一个进程刷索引,两边松耦合
- warm-start 是 self-improvement 类方法的隐性门槛——从零开始挖出来的”难”通常是噪声
- 方法的廉价感:一个被广泛采用的方法常常并不”复杂”,而是”找对了关键变量”。ANCE 既没换模型也没换损失函数,只是换了负例来源——后续两年的稠密检索 SOTA 论文几乎都建立在这一改动之上
延伸阅读
- 论文 PDF:Xiong et al. 2020
- 微软官方代码:microsoft/ANCE
- dpr-2020 —— 前作,ANCE 解决的就是 DPR 弱负例的问题
- colbert-2020 —— 多向量对手,ANCE 证明单向量也能逼近
- anserini-2017 —— BM25 baseline 与 hard negative 来源
- 后续工作:RocketQA(去噪 hard negatives)、coCondenser(更稳的 warm-start 预训练)、E5(亿级合成数据规模化)—— 都是在 ANCE 训练循环上做局部改进
关联
- dpr-2020 —— ANCE 直接基于 DPR 双塔架构改造,只换了负例来源
- colbert-2020 —— 同期对手方案,多向量 vs 单向量+难负例两条路线
- anserini-2017 —— BM25 是 ANCE 的 warm-start 来源
- bert —— 双塔编码器的底座
- attention —— BERT 内部机制;ANCE 没改 attention 但所有 query/passage 表示都依赖它
一句话总结
让模型自己挖训练负例——这一个改动让稠密检索从”训不动”变成”训得越来越准”,并定义了之后两年所有稠密检索 SOTA 的训练循环。