摘要
今天,我发布了六款全新的 Sentence Transformers CrossEncoder 重排序模型,它们在各自参数量级别上均达到业界领先水平。这些模型基于 Ettin ModernBERT 编码器构建,同时我还一并公开了用于训练它们的数据集和完整的训练方案:
- cross-encoder/ettin-reranker-17m-v1
- cross-encoder/ettin-reranker-32m-v1
- cross-encoder/ettin-reranker-68m-v1
- cross-encoder/ettin-reranker-150m-v1
- cross-encoder/ettin-reranker-400m-v1
- cross-encoder/ettin-reranker-1b-v1
这些模型采用知识蒸馏方案进行训练:在 cross-encoder/ettin-reranker-v1-data 数据集上,以 mixedbread-ai/mxbai-rerank-large-v2 的得分为目标,进行逐点均方误差(pointwise MSE)蒸馏。该数据集是 lightonai/embeddings-pre-training 的一个子集,并与 lightonai/embeddings-fine-tuning 的一个经过重排序的子集混合而成。
我们的六款重排序模型与 google/embeddinggemma-300m 搭配,在 MTEB(英文,v2 版)检索任务上的表现。更多结果请参见“五种其他嵌入模型搭配”部分。
如果你刚接触重排序模型,想先了解“为什么”,请跳转到“什么是重排序模型,以及为什么要将其与嵌入模型搭配使用?”部分。如果你只想直接使用模型,请跳转到“使用”部分。如果你想自行训练,请跳转到“训练”部分。
我使用 Sentence Transformers v5.5.0 中新增的 `train-sentence-transformers` 智能体技能(Agent Skill)引导了以下训练方案。通过 `hf skills add train-sentence-transformers [--global] [--claude]` 安装该技能,然后让你的 AI 编程智能体(Claude Code、Codex、Cursor、Gemini CLI 等)在你的数据上微调一个 SentenceTransformer、CrossEncoder 或 SparseEncoder 模型。
目录
什么是重排序模型,以及为什么要将其与嵌入模型搭配使用?
使用
- 端到端的检索-重排序流水线
架构细节
结果
- MTEB(英文,v2 版)检索
- 速度
训练
- 蒸馏方案
- 数据集
- 训练参数
- 评估
- 整体训练脚本
结论
致谢
什么是重排序模型,以及为什么要将其与嵌入模型搭配使用?
重排序模型(也称为逐点交叉编码器)是一种神经网络模型,它接收一个(查询,文档)对,并输出一个单一的相关性分数。与分别对查询和文档进行编码、然后从两个嵌入向量计算其相似度的嵌入模型不同,重排序模型让两个文本在每个 Transformer 层中相互关注。这种联合编码更准确,但代价也更高:模型必须为每个(查询,文档)对运行一次,而不是为每个文本运行一次。
由于交叉编码器在整个语料库上运行成本过高,常见的生产模式是“检索-然后-重排序”:一个快速的嵌入模型检索出前 K 个候选结果(成本低),然后一个交叉编码器以高精度仅对这 K 个结果进行重新排序。总成本保持可控,而最终的排序结果则更接近于穷举式交叉编码器通过所能产生的效果。
在本篇博文中,我将互换使用“重排序模型”和“交叉编码器”这两个术语。
使用方法
发布的模型是标准的 Sentence Transformers CrossEncoder 模型,因此您只需三行代码即可使用它们:
from sentence_transformers import CrossEncoder
model = CrossEncoder("cross-encoder/ettin-reranker-32m-v1")
scores = model.predict([
("Where was Apple founded?", "Apple Inc. was founded in Cupertino, California in 1976 by Steve Jobs, Steve Wozniak, and Ronald Wayne."),
("Where was Apple founded?", "The Fuji apple is an apple cultivar developed in the late 1930s and brought to market in 1962."),
])
print(scores)
# [11.393298 2.968891] <- larger means more relevant
对于一个查询和一个候选列表,您也可以使用 `rank` 来获取排序后的索引和分数:
ranked = model.rank(
query="Which planet is known as the Red Planet?",
documents=[
"Venus is often called Earth's twin because of its similar size and proximity.",
"Mars, known for its reddish appearance, is often referred to as the Red Planet.",
"Jupiter, the largest planet in our solar system, has a prominent red spot.",
"Saturn, famous for its rings, is sometimes mistaken for the Red Planet.",
],
top_k=4,
return_documents=True,
)
for r in ranked:
print(f"({r['score']:.2f}): {r['text']}")
# (10.82): Mars, known for its reddish appearance, is often referred to as the Red Planet.
# (9.86): Saturn, famous for its rings, is sometimes mistaken for the Red Planet.
# (8.55): Jupiter, the largest planet in our solar system, has a prominent red spot.
# (6.21): Venus is often called Earth's twin because of its similar size and proximity.
您可以将 `cross-encoder/ettin-reranker-32m-v1` 替换为任何其他尺寸的模型,以在质量与速度之间进行权衡。得益于 ModernBERT 的长上下文预训练,所有六个模型都支持最多 8K 个 token 的上下文(这对于长文档重排序非常有用)。
建议安装相关内核并设置 `model_kwargs={"dtype": "bfloat16", "attn_implementation": "flash_attention_2"}` 以获得最高吞吐量。更多详情请参见下方的“速度”部分,但总体而言,根据模型大小和序列长度,您可以预期相比默认加载方式获得 1.7 倍到 8.3 倍的速度提升。
from sentence_transformers import CrossEncoder
model = CrossEncoder(
"cross-encoder/ettin-reranker-32m-v1",
model_kwargs={"dtype": "bfloat16", "attn_implementation": "flash_attention_2"},
)
端到端的“检索-然后-重排序”流程
一个完整的示例,使用快速嵌入器进行检索,并使用重排序模型进行最终排序:
from sentence_transformers import SentenceTransformer, CrossEncoder
# Fast retrieval with a static embedder (sub-millisecond on CPU per query)
embedder = SentenceTransformer("sentence-transformers/static-retrieval-mrl-en-v1")
reranker = CrossEncoder("cross-encoder/ettin-reranker-68m-v1")
corpus = [
"Apple Inc. was founded in Cupertino, California in 1976 by Steve Jobs, Steve Wozniak, and Ronald Wayne.",
"The Fuji apple is an apple cultivar developed in the late 1930s.",
"Steve Jobs introduced the iPhone in 2007 at Macworld.",
"Macintosh computers were sold by Apple from 1984 onward.",
# ... thousands or millions more in production
]
query = "Where was Apple founded?"
# Step 1: encode + retrieve top-100
query_emb = embedder.encode_query(query, convert_to_tensor=True)
corpus_emb = embedder.encode_document(corpus, convert_to_tensor=True)
scores = embedder.similarity(query_emb, corpus_emb)[0]
top_k_idx = scores.topk(min(100, len(corpus))).indices.tolist()
# Step 2: rerank
top_k_docs = [corpus[i] for i in top_k_idx]
ranked = reranker.rank(query, top_k_docs, top_k=5, return_documents=True)
for r in ranked:
print(f"({r['score']:.2f}): {r['text']}")
# (11.63): Apple Inc. was founded in Cupertino, California in 1976 by Steve Jobs, Steve Wozniak, and Ronald Wayne.
# (4.71): Steve Jobs introduced the iPhone in 2007 at Macworld.
# (1.96): The Fuji apple is an apple cultivar developed in the late 1930s.
# (1.49): Macintosh computers were sold by Apple from 1984 onward.
这与大多数现代搜索系统使用的模式相同。检索器决定了哪些内容进入漏斗,而重排序模型则决定了最终胜出者。
架构细节
所有六个重排序器共享相同的架构,唯一的区别在于其骨干网络的大小。骨干网络来自约翰霍普金斯大学 Ettin 套件中的六个 Ettin 编码器之一。这些是 ModernBERT 风格的模型,采用无填充注意力机制、RoPE 位置编码、GeGLU 激活函数,并在 2T 个开放许可的模型 token 上进行了预训练,支持最多 8192 个模型 token 的上下文。
在每个编码器之上,重排序器使用了一个 4 模块的分类头,该分类头模仿了 ModernBertForSequenceClassification,但由 Sentence Transformers 的模块化组件构建而成。底层的 Transformer 是一个普通的 AutoModel,而非 AutoModelForSequenceClassification,这使我们能够对可变长度输入使用序列解填充,以适配 Flash Attention 2。在中等文档序列长度下,根据模型大小不同,这比 fp32+SDPA 实现了 1.7 倍到 8.3 倍的加速(完整基准测试请参见速度部分):
1. Transformer(FA2)
2. Pooling(cls)
3. Dense(H, H, bias=False, GELU)
4. LayerNorm(H)
5. Dense(H, 1, scores)
在我的消融实验中,CLS 池化表现优于均值池化。这有点令人惊讶。ModernBERT 每三层才使用一次全局注意力,而另外两层使用局部窗口注意力,无法从远端位置到达 CLS 标记。经验上,那少数几个全局层携带了足够的信号,使得 CLS 成为更好的池化选择。
| 模型 | 骨干网络 | 隐藏层大小 | 层数 | 参数量(含分类头) |
|---|---|---|---|---|
| cross-encoder/ettin-reranker-17m-v1 | jhu-clsp/ettin-encoder-17m | 256 | 7 | 17.6M |
| cross-encoder/ettin-reranker-32m-v1 | jhu-clsp/ettin-encoder-32m | 384 | 10 | 32.8M |
| cross-encoder/ettin-reranker-68m-v1 | jhu-clsp/ettin-encoder-68m | 512 | 19 | 68.6M |
| cross-encoder/ettin-reranker-150m-v1 | jhu-clsp/ettin-encoder-150m | 768 | 22 | 150.9M |
| cross-encoder/ettin-reranker-400m-v1 | jhu-clsp/ettin-encoder-400m | 1024 | 28 | 401.6M |
| cross-encoder/ettin-reranker-1b-v1 | jhu-clsp/ettin-encoder-1b | 1792 | 28 | 1.00B |
所有六个模型均根据 Apache 2.0 许可证发布,与 Ettin 编码器保持一致。
结果
MTEB(英文,v2)检索
我使用 MTEB 的两阶段重排序流程,将每个已发布的模型在完整的 MTEB(英文,v2)检索基准测试(10 个任务,对前 100 名进行重排序)上进行了评估,并将每个重排序器与六个覆盖不同速度/质量范围的嵌入向量模型配对:
| 嵌入向量模型 | 活跃参数量 | 仅检索器 NDCG@10 |
|---|---|---|
| sentence-transformers/static-retrieval-mrl-en-v1 | 0M | 0.3495 |
| sentence-transformers/all-MiniLM-L6-v2 | 23M | 0.4292 |
| BAAI/bge-small-en-v1.5 | 33M | 0.5149 |
| nomic-ai/nomic-embed-text-v1.5 | 1.37亿 | 0.5226 |
| google/embeddinggemma-300m | 3.08亿 | 0.5463 |
| jinaai/jina-embeddings-v5-text-small-retrieval | 5.96亿 | 0.5980 |
下图中每条虚线(仅检索器)是需超越的基准线。任何低于该线的结果都意味着重排序器平均而言会损害整个流程的性能:
完整结果表格(点击展开)在6组嵌入器配对上的平均NDCG@10,按降序排列。我们的六个模型以粗体显示,教师模型 mixedbread-ai/mxbai-rerank-large-v2 以下划线标出。
| 重排序器 | 参数量 | MTEB(英文,v2)检索 NDCG@10 |
|---|---|---|
| Qwen/Qwen3-Reranker-4B† | 40.2亿 | 0.6367 |
| mixedbread-ai/mxbai-rerank-large-v2 | 15.4亿 | 0.6115 |
| cross-encoder/ettin-reranker-1b-v1 | 10.0亿 | 0.6114 |
| cross-encoder/ettin-reranker-400m-v1 | 4.01亿 | 0.6091 |
| cross-encoder/ettin-reranker-150m-v1 | 1.51亿 | 0.5994 |
| Qwen/Qwen3-Reranker-0.6B | 5.96亿 | 0.5940 |
| mixedbread-ai/mxbai-rerank-base-v2 | 4.94亿 | 0.5920 |
| cross-encoder/ettin-reranker-68m-v1 | 6860万 | 0.5915 |
| jinaai/jina-reranker-m0 | 24.4亿 | 0.5856 |
| Alibaba-NLP/gte-reranker-modernbert-base | 1.50亿 | 0.5843 |
| cross-encoder/ettin-reranker-32m-v1 | 3280万 | 0.5779 |
| ibm-granite/granite-embedding-reranker-english-r2 | 1.50亿 | 0.5656 |
| cross-encoder/ettin-reranker-17m-v1 | 1760万 | 0.5576 |
| BAAI/bge-reranker-v2-m3 | 5.68亿 | 0.5526 |
| zeroentropy/zerank-2-reranker† | 40.2亿 | 0.5300 |
| BAAI/bge-reranker-large | 5.60亿 | 0.5098 |
| cross-encoder/ms-marco-MiniLM-L6-v2 | 2270万 | 0.5082 |
| cross-encoder/ms-marco-MiniLM-L12-v2 | 3340万 | 0.5066 |
| mixedbread-ai/mxbai-rerank-large-v1 | 4.35亿 | 0.5063 |
| cross-encoder/ms-marco-MiniLM-L4-v2 | 1920万 | 0.4979 |
| mixedbread-ai/mxbai-rerank-xsmall-v1 | 7080万 | 0.4968 |
| BAAI/bge-reranker-base | 2.78亿 | 0.4890 |
| mixedbread-ai/mxbai-rerank-base-v1 | 1.84亿 | 0.4865 |
† 限制为 max_seq_length=8192(基于4B Qwen3的重排序器在原生上下文长度下无法放入单张H100 80GB显卡)。原生上下文评估的结果可能更高。
完整 NanoBEIR 结果表格(点击展开)NanoBEIR 是 BEIR 的一个快速子集,包含13个数据集,每个数据集使用50个查询,针对最多5000个文档。NanoBEIR 是训练期间 metric_for_best_model 所设定的指标(参见评估部分),也是我用于指导实验的指标。
| 重排序器 | 参数量 | NanoBEIR 平均 NDCG@10 |
|---|---|---|
| mixedbread-ai/mxbai-rerank-large-v2 | 15.4亿 | 0.7318 |
| cross-encoder/ettin-reranker-1b-v1 | 10.0亿 | 0.7237 |
| jinaai/jina-reranker-m0 | 24.4亿 | 0.7197 |
| cross-encoder/ettin-reranker-400m-v1 | 4.01亿 | 0.7193 |
| mixedbread-ai/mxbai-rerank-base-v2 | 4.94亿 | 0.7162 |
| cross-encoder/ettin-reranker-150m-v1 | 1.51亿 | 0.7086 |
| Alibaba-NLP/gte-reranker-modernbert-base | 1.5亿 | 0.7017 |
| BAAI/bge-reranker-v2-m3 | 5.68亿 | 0.6971 |
| cross-encoder/ettin-reranker-68m-v1 | 6860万 | 0.6915 |
| ibm-granite/granite-embedding-reranker-english-r2 | 1.5亿 | 0.6909 |
| cross-encoder/ettin-reranker-32m-v1 | 3280万 | 0.6825 |
| cross-encoder/ettin-reranker-17m-v1 | 1760万 | 0.6746 |
| mixedbread-ai/mxbai-rerank-large-v1 | 4.35亿 | 0.6488 |
| BAAI/bge-reranker-large | 5.6亿 | 0.6379 |
| cross-encoder/ms-marco-MiniLM-L12-v2 | 3340万 | 0.6369 |
| cross-encoder/ms-marco-MiniLM-L6-v2 | 2270万 | 0.6312 |
| cross-encoder/ms-marco-MiniLM-L4-v2 | 1920万 | 0.6298 |
| mixedbread-ai/mxbai-rerank-base-v1 | 1.84亿 | 0.6231 |
| mixedbread-ai/mxbai-rerank-xsmall-v1 | 7080万 | 0.6136 |
| BAAI/bge-reranker-base | 2.78亿 | 0.6027 |
我发布的最小模型,即 1760 万参数的版本,在参数量大约只有一半的情况下,在 MTEB 上以 +0.051 NDCG@10(0.5576 对比 0.5066)的成绩击败了 3340 万参数的 ms-marco-MiniLM-L12-v2,在 NanoBEIR 上则以 +0.038(0.6746 对比 0.6369)的成绩胜出。3200 万参数的模型在 MTEB 上以 +0.025(0.5779 对比 0.5526)的成绩击败了 5.68 亿参数的 BAAI/bge-reranker-v2-m3,参数量差距达 17 倍。如果你一直在检索-重排序流程中使用某个旧版 MiniLM 重排序器作为默认选项,那么换成我们的 1760 万(或 3200 万)参数模型是一个低风险的直接替换方案,在两个基准测试上都能带来明显的质量提升。
继续看表格,我们的 1.5 亿参数模型是我在 MTEB 上测试过的 6 亿参数以下范围内最强的重排序器,以 +0.005(0.5994 对比 0.5940)的微弱优势击败了近期发布的 Qwen/Qwen3-Reranker-0.6B(5.96 亿参数),并且以 0.03 到 0.05 的差距击败了所有 BAAI bge-reranker 变体。6800 万参数的模型也值得一提:它的得分 0.5915 几乎与 Qwen3-Reranker-0.6B(0.5940)持平,而参数量仅为后者的九分之一。
在已发布模型范围的顶端,我们的 10 亿参数模型紧密跟随其教师模型。它在 MTEB 上的得分与 15.4 亿参数的 mxbai-rerank-large-v2 仅差 0.0001(0.6114 对比 0.6115),在 NanoBEIR 上相差 0.008,尽管它是从一个比自身大 54% 的模型中蒸馏而来。这种蒸馏有效地缩小了与教师模型之间的差距,这正是我在这次发布中所希望看到的结果。
在本次对比中,综合表现最强的重排序模型是 Qwen/Qwen3-Reranker-4B,其 MTEB 得分为 0.6367,比我们的 1B 模型高出 0.025。要缩小这一差距,按照当前的方案可能需要从更强的教师模型进行知识蒸馏(我们自己的教师模型本身表现就低于 Qwen3-Reranker-4B)。对于大多数“先检索再重排序”的工作负载,我们的 1B 模型参数量仅为前者的四分之一(参见“速度”部分),是更实用的选择。
速度
质量指标只是重排序模型重要性的一个方面。另一半则在于其延迟是否能够满足从检索到向用户展示结果之间的时间预算。下面我来介绍一下我的测量结果。
我在单张 NVIDIA H100 80GB 上,将全部六个已发布模型与十三个公开的重排序模型(均为约 1B 参数以内的强基线模型)进行了基准测试。查询和文档来自 sentence-transformers/natural-questions,并保持其自然的文档长度分布:大多数 NQ 答案较短,部分较长。文档被截断至 max_length=512,以避免给较老的模型带来不公平的优势。每个模型都使用其支持的最佳注意力实现:架构支持的地方使用 Flash Attention 2(BERT、XLM-RoBERTa、ModernBERT、Qwen2),不支持的地方使用 SDPA,而 DeBERTa-v2 则使用 eager 模式(该模型目前在 transformers 中既不支持 FA2 也不支持 SDPA)。
对于每个模型,自动批处理搜索从批大小 8 开始,并不断加倍,直到 GPU 内存耗尽。在每个批大小下,我运行三次计时测试并取中位数吞吐量,这样单次不理想的运行就不会影响最终数据。报告的吞吐量是获胜批大小下的结果。
表 1. 吞吐量(以每秒处理的对数计),全部采用 bfloat16 格式。我们的六个重排序模型以粗体显示。
| 模型 | 参数量 | 注意力机制 | 对/秒 |
|---|---|---|---|
| cross-encoder/ettin-reranker-17m-v1 | 17M | FA2 | 7517 |
| cross-encoder/ettin-reranker-32m-v1 | 32M | FA2 | 6602 |
| cross-encoder/ettin-reranker-68m-v1 | 68M | FA2 | 4913 |
| cross-encoder/ms-marco-MiniLM-L4-v2 | 19M | FA2 | 4029 |
| cross-encoder/ms-marco-MiniLM-L6-v2 | 22M | FA2 | 3817 |
| cross-encoder/ms-marco-MiniLM-L12-v2 | 33M | FA2 | 3311 |
| cross-encoder/ettin-reranker-150m-v1 | 150M | FA2 | 3237 |
| BAAI/bge-reranker-base | 278M | FA2 | 2858 |
| mixedbread-ai/mxbai-rerank-xsmall-v1 | 70M | eager | 2636 |
| mixedbread-ai/mxbai-rerank-base-v1 | 184M | eager | 1953 |
| cross-encoder/ettin-reranker-400m-v1 | 400M | FA2 | 1738 |
| BAAI/bge-reranker-large | 560M | FA2 | 1659 |
| BAAI/bge-reranker-v2-m3 | 568M | FA2 | 1569 |
| Alibaba-NLP/gte-reranker-modernbert-base | 150M | FA2 | 1418 |
| ibm-granite/granite-embedding-reranker-english-r2 | 150M | FA2 | 1404 |
| cross-encoder/ettin-reranker-1b-v1 | 1B | FA2 | 928 |
| mixedbread-ai/mxbai-rerank-large-v1 | 435M | eager | 867 |
| mixedbread-ai/mxbai-rerank-base-v2 | 494M | FA2 | 809 |
| mixedbread-ai/mxbai-rerank-large-v2 | 1.5B | FA2 | 387 |
我们的 17M 模型是整个对比中最快的重排序器,每秒可处理 7517 个句子对。这一吞吐量几乎是 ms-marco-MiniLM-L6-v2(3817)的两倍,甚至比更小的 ms-marco-MiniLM-L4-v2(4029)还要快。而且,正如你在之前的 MTEB 表格中所见,我们的 17M 模型在准确率上也优于所有 MiniLM 变体。如果你目前正在运行 MiniLM 交叉编码器,只需修改一行代码即可切换到我们的 17M 模型,这既能提升延迟表现,也能改善搜索质量。
我们的 150M 模型是一个更有趣的比较对象,因为恰好有两个架构完全相同的同类模型也是 150M 参数量:Alibaba-NLP/gte-reranker-modernbert-base 和 ibm-granite/granite-embedding-reranker-english-r2。两者都基于相同的 ModernBERT-base 骨干网络构建。我们的 150M 模型每秒可处理 3237 个句子对,而两个同类模型分别只有 1418 和 1404,存在 2.3 倍的速度差距。
这三款 150M 模型都使用了 Flash Attention 2,但两个同类模型通过 AutoModelForSequenceClassification 加载,这会导致输入保持填充状态。因此,注意力机制本身确实运行了 FA2 内核,但模型的其余部分仍然在对那些毫无贡献的填充 token 进行密集计算。我们的模块化 Transformer 模块(详见架构细节部分)会将未填充的输入一直传递到整个模型,因此每一层都只对真实 token 进行计算。这就是获得 FA2 部分收益与获得全部收益之间的区别。
在表格底部,我们的 1B 模型达到了每秒 928 对,比 1.54B 的教师模型 mxbai-rerank-large-v2(每秒 387 对)快 2.4 倍,同时 MTEB 分数差距在 0.0001 以内。教师模型基于 Qwen2,每对数据都有提示词模板开销,因此蒸馏后的学生模型继承了教师模型的校准能力和判断力,但省去了所有运行时负担。老实说,这是整个发布中我最满意的一个数字。
一个遗憾之处:基于 DeBERTa-v2 的 mxbai-rerank-{xsmall,base,large}-v1 系列最终比表格中其他模型慢得多,因为 DeBERTa-v2 目前在 transformers 中既不支持 Flash Attention 2,也不支持 SDPA。70M 的 mxbai-rerank-xsmall-v1 运行速度为每秒 2636 对,大约是参数量几乎相同的 68M 模型吞吐量的一半。这些模型本身完全没问题,只是无法使用现代注意力内核。
在消费级 GPU(RTX 3090,24 GB)上的相同基准测试 如果你在消费级显卡(而非数据中心 GPU)上自行部署,以下是 RTX 3090 上的相同吞吐量扫描。基准测试设置与表 1 相同:bfloat16,每个模型最佳支持的注意力机制,三次试验的中位数吞吐量,采用能容纳的最大批次。
| 模型 | 参数量 | 最佳注意力机制 | 每秒处理对数 |
|---|---|---|---|
| cross-encoder/ettin-reranker-17m-v1 | 17M | FA2 | 9008 |
| cross-encoder/ms-marco-MiniLM-L4-v2 | 19M | FA2 | 5071 |
| cross-encoder/ettin-reranker-32m-v1 | 32M | FA2 | 4497 |
| cross-encoder/ms-marco-MiniLM-L6-v2 | 22M | FA2 | 4234 |
| cross-encoder/ms-marco-MiniLM-L12-v2 | 33M | FA2 | 2847 |
| cross-encoder/ettin-reranker-68m-v1 | 68M | FA2 | 1916 |
| mixedbread-ai/mxbai-rerank-xsmall-v1 | 70M | eager | 1677 |
| BAAI/bge-reranker-base | 278M | FA2 | 1329 |
| cross-encoder/ettin-reranker-150m-v1 | 150M | FA2 | 982 |
| mixedbread-ai/mxbai-rerank-base-v1 | 184M | eager | 772 |
| ibm-granite/granite-embedding-reranker-english-r2 | 150M | FA2 | 598 |
| Alibaba-NLP/gte-reranker-modernbert-base | 150M | FA2 | 586 |
| BAAI/bge-reranker-large | 560M | FA2 | 448 |
| BAAI/bge-reranker-v2-m3 | 568M | FA2 | 436 |
| cross-encoder/ettin-reranker-400m-v1 | 400M | FA2 | 429 |
| mixedbread-ai/mxbai-rerank-large-v1 | 435M | eager | 266 |
| mixedbread-ai/mxbai-rerank-base-v2 | 494M | FA2 | 221 |
| cross-encoder/ettin-reranker-1b-v1 | 1B | FA2 | 189 |
| mixedbread-ai/mxbai-rerank-large-v2 | 1.5B | FA2 | 69 |
我们的 17M 模型在表中仍是最快的,每秒处理 9008 对,实际上比它在 H100 上的数字还要高,这表明在极小规模下,原始算力并非瓶颈,H100 的额外性能优势并未体现出来。表格中部排名略有变化,MiniLM 重排序器超过了我们的 32M 和 68M 模型,而 1B 模型则落后于 mxbai-rerank-base-v2(每秒 189 对 vs 221 对)。我们的 150M 模型仍然遥遥领先于两个基于 ModernBERT 的 150M 同类模型,教师模型替代的逻辑依然成立:我们的 1B 模型吞吐量是 1.5B mxbai-rerank-large-v2 的 2.7 倍(每秒 189 对 vs 69 对)。
在 CPU(Intel Core i7-13700K)上的相同基准测试
| 模型 | 参数量 | 最佳注意力机制 | 每秒处理对数 |
|---|---|---|---|
| cross-encoder/ettin-reranker-17m-v1 | 17M | SDPA | 267.4 |
| cross-encoder/ms-marco-MiniLM-L4-v2 | 19M | SDPA | 206.2 |
| cross-encoder/ms-marco-MiniLM-L6-v2 | 22M | SDPA | 143.9 |
| cross-encoder/ettin-reranker-32m-v1 | 32M | SDPA | 92.5 |
| cross-encoder/ms-marco-MiniLM-L12-v2 | 33M | SDPA | 75.9 |
| mixedbread-ai/mxbai-rerank-xsmall-v1 | 70M | eager | 38.9 |
| cross-encoder/ettin-reranker-68m-v1 | 68M | SDPA | 31.2 |
| BAAI/bge-reranker-base | 278M | SDPA | 19.2 |
| Alibaba-NLP/gte-reranker-modernbert-base | 150M | SDPA | 14.7 |
| ibm-granite/granite-embedding-reranker-english-r2 | 150M | SDPA | 14.5 |
| cross-encoder/ettin-reranker-150m-v1 | 150M | SDPA | 14.0 |
| mixedbread-ai/mxbai-rerank-base-v1 | 184M | eager | 13.4 |
| BAAI/bge-reranker-large | 560M | SDPA | 6.2 |
| BAAI/bge-reranker-v2-m3 | 568M | SDPA | 6.0 |
| cross-encoder/ettin-reranker-400m-v1 | 400M | SDPA | 5.2 |
| mixedbread-ai/mxbai-rerank-large-v1 | 435M | eager | 4.3 |
| mixedbread-ai/mxbai-rerank-base-v2 | 494M | SDPA | 3.5 |
| cross-encoder/ettin-reranker-1b-v1 | 1B | SDPA | 2.1 |
在 CPU 上,我们无法利用 bf16、Flash Attention 2 或 unpadding 的优势,因此延迟情况更为简单:参数量越高,模型越慢。17M 模型明显快于 ms-marco-MiniLM-L6-v2(每秒 267.4 对 vs 143.9 对),甚至比更小的 ms-marco-MiniLM-L4-v2(206.2)还要快。不出所料,由于 unpadding 不再适用,我们的 150M 模型与两个 150M 同类模型性能相当(每秒 14.0 对 vs 14.5 对和 14.7 对)。如果你的场景受 CPU 限制,我们的 17M 和 32M 模型是实用的选择。
为了解释速度提升的来源,下表使用相同的基准配置,对我们六个模型进行了 fp32+SDPA、bf16+SDPA 和 bf16+FA2 的对比测试。FA2 列分为两部分:一部分输入仍带有填充(封装模型所看到的情况),另一部分输入无填充(我们的模块化 Transformer 实际采用的方式)。最右侧一列是启用 FA2 时我们模型默认使用的配置。
表 2. 在自然 NQ 文档上,针对六个已发布尺寸的模型,在 max_length=512 条件下进行的精度与注意力消融实验。每个单元格显示每秒处理的样本数,括号内为相对于 fp32+SDPA 的倍数,第二行显示峰值 GPU 内存。最右侧一列(加粗)是启用 FA2 时我们模型默认使用的配置。
| 模型 | 参数量 | fp32+SDPA | bf16+SDPA | bf16+FA2(带填充) | bf16+FA2(无填充) |
|---|---|---|---|---|---|
| ettin-reranker-17m-v1 | 17M | 4402 (1.00x) 0.8 GB | 4523 (1.03x) 2.2 GB | 3744 (0.85x) 1.9 GB | 7517 (1.71x) 1.4 GB |
| ettin-reranker-32m-v1 | 32M | 3307 (1.00x) 1.2 GB | 4357 (1.32x) 1.6 GB | 3040 (0.92x) 2.9 GB | 6602 (2.00x) 1.1 GB |
| ettin-reranker-68m-v1 | 68M | 1364 (1.00x) 1.0 GB | 2861 (2.10x) 2.2 GB | 2003 (1.47x) 2.0 GB | 4913 (3.60x) 1.5 GB |
| ettin-reranker-150m-v1 | 150M | 671 (1.00x) 1.6 GB | 1942 (2.90x) 1.8 GB | 1396 (2.08x) 3.1 GB | 3237 (4.83x) 1.4 GB |
| ettin-reranker-400m-v1 | 400M | 266 (1.00x) 2.5 GB | 1113 (4.18x) 1.8 GB | 864 (3.25x) 2.7 GB | 1738 (6.53x) 2.2 GB |
| ettin-reranker-1b-v1 | 1B | 112 (1.00x) 4.6 GB | 630 (5.60x) 2.8 GB | 522 (4.64x) 3.6 GB | 928 (8.26x) 4.5 GB |
从 bf16+FA2(无填充)相对于 fp32+SDPA 基线的总加速比来看,随着模型尺寸增大而急剧增长,从 17M 模型的 1.71 倍提升到 1B 模型的 8.26 倍。其中大部分增长来自 bf16 本身:从 fp32+SDPA 到 bf16+SDPA 这一步,17M 模型仅获得 1.03 倍的加速,而 1B 模型则获得了完整的 5.60 倍加速,这也是因为内存成本降低使得批处理规模可以更大。简而言之,bfloat16 是整体加速的最大单一贡献因素。
出人意料的是,在输入仍处于填充状态时启用 FA2,实际上比该版本中任何尺寸下的 bf16+SDPA 都要慢。FA2 内核更偏好未填充的格式,当你向其提供填充后的输入时,你既要为格式转换付出簿记开销,同时还要在填充 token 本身的计算上消耗算力。因此,bf16+FA2 带填充列大致相当于你在 model_kwargs 中将 sdpa 替换为 flash_attention_2 且不改变模型加载器其他任何设置时所能测得的结果。表 1 中的 gte-reranker-modernbert-base 和 granite-embedding-reranker-english-r2 就处于这种情况。
最后,从 bf16+FA2 带填充切换到 bf16+FA2 无填充,可额外带来 1.78 倍(1B 模型)到 2.45 倍(68M 模型)的吞吐量提升,同时还能显著降低峰值内存,从而支持更大的批处理规模。
因此,我的建议很简单:同时启用 bf16 和 FA2。六个 Ettin 重排序器将默认使用未填充的输入,因为架构细节部分介绍的模块化 Transformer 模块正是为此设计的。完整代码片段与上方使用部分相同:
from sentence_transformers import CrossEncoder
model = CrossEncoder(
"cross-encoder/ettin-reranker-150m-v1",
model_kwargs={
"dtype": "bfloat16",
"attn_implementation": "flash_attention_2", # See tip below
},
)
使用 pip install kernels 安装 FA2。它内置了适用于多种 GPU 架构、CUDA 版本和操作系统的预编译内核。
其他 CrossEncoder 模型需要注意一点:完整的加速效果仅适用于像 Ettin 重排序器这样基于模块化 Transformer 构建的模型。将同样的两个标志应用于通过 AutoModelForSequenceClassification 加载的 CrossEncoder,结果只会落在表 2 中速度更慢的 bf16+FA2 带填充列。
训练
下面的训练脚本最初来自 Sentence Transformers v5.5.0 中附带的新版 train-sentence-transformers 智能体技能的输出。如果你使用 AI 编程智能体(Claude Code、Codex、Cursor、Gemini CLI 等),你可以安装该技能,并让它基于你的数据微调 SentenceTransformer、CrossEncoder 或 SparseEncoder 模型。该技能包含针对基础模型选择、损失函数与评估器选择、难负例挖掘、知识蒸馏、LoRA、Matryoshka、多语言训练以及静态嵌入向量的版本感知指导,此外还为每种模型类型提供了模板脚本。
hf skills add train-sentence-transformers --claude # symlinks into .claude/skills/
hf skills add train-sentence-transformers --global # under ~/.agents/skills/
像“基于我的数据集中的(查询,文档)对微调一个交叉编码器重排序模型,挖掘困难负样本,并推送到我的 Hub 仓库”这样的提示词,会生成一个可运行的脚本,你可以在此基础上迭代。我就是这样开始编写下面这个方案的。
所有六个重排序模型都使用相同的单阶段方案进行训练。只有学习率和每设备批次大小因模型规模而异。完整的训练脚本约 150 行,并使用了一个已发布的数据集。
该方案在一次跨模型规模的扫描后即收敛。每个规模的学习率通过在最终训练数据约 15% 的子集上进行小规模网格搜索来调整,得到的最终学习率可以干净地迁移到全量数据运行中,无需重新调整。除了学习率之外,不需要针对每个规模进行其他调整。
知识蒸馏方案
大多数已发表的重排序模型方案是在人工标注的相关性三元组(一个查询、一个正例文档,以及可选的困难负样本)上训练的,分别使用对比损失、逐点损失、成对损失或列表排序损失,例如 MultipleNegativesRankingLoss、BinaryCrossEntropyLoss、RankNetLoss 或 LambdaLoss。例如,可以参见我之前写的《使用 Sentence Transformers 训练和微调重排序模型》的博客文章。
但这种方法有一些实际和理论上的缺点。首先,正例需要人工标注,这在许多领域扩展时既昂贵又缓慢。其次,模型只能看到有人工标注的那一小部分(查询,文档)对的标签。尤其是在挖掘困难负样本之后,最终会产生大量假负例,例如《困难负样本,困难教训》一文所示。第三,这种标注的二元性质与现实不符,现实中有些文档只是比其他文档更相关。
我在这里采用了不同的路径:从现有的强教师重排序模型进行逐点均方误差知识蒸馏。这个设置简单到可以用三行来描述:
- 教师模型:mixedbread-ai/mxbai-rerank-large-v2(1.54B 参数)。
- 损失函数:对原始教师模型 logits(范围约 [−12, 22])使用 MSELoss,即不进行重新缩放。
- 训练数据:约 1.43 亿个(查询,文档,教师模型分数)三元组。
数据集
我已将训练数据作为单个 Hugging Face 数据集发布,名为 cross-encoder/ettin-reranker-v1-data,该数据集由两个来源整合而成。每个来源都保留为独立的分片,以确保来源透明可追溯。
- LightOn 预训练数据(lightonai/embeddings-pre-training,未经整理):包含 32 个分片,覆盖广泛领域的文本相似度信号(MTP、FW-EDU、Reddit、PAQ、S2ORC、Amazon、Wikipedia、MS MARCO 等)。我对部分分片的样本数量进行了限制,最终得到约 1.1 亿条(查询、文档、相似度)三元组。
- 来自 lightonai/embeddings-fine-tuning 的重新评分检索数据:包含 7 个分片(msmarco、hotpotqa、trivia、nq、squadv2、fiqa、fever)。源数据集中每个查询最多有 2048 个候选文档(最初由 Alibaba-NLP/gte-modernbert-base 评分),我使用 mixedbread-ai/mxbai-rerank-large-v2 对其重新评分,并上传为 cross-encoder/lightonai-embeddings-fine-tuning-reranked-v1。该数据集采用 Jang 等人的分位数锚点方法,将每个查询的 2048 个候选文档下采样至 256 个(所有正样本 + 前 16 个困难负样本 + 约 239 个分位数锚点分层样本)。在训练时,我从每个查询的 256 个候选中选取 64 个:其中 32 个来自按分数排序的头部(正样本加上最困难的负样本),另外 32 个中等难度负样本则从教师模型排序中更靠后的区间内采样。具体排名位置请参见数据集卡片。
总计:约 1.43 亿条(查询、文档、分数)三元组,外加一个保留的 5000 行评估分片(来自 quora 的尾部数据),用于驱动训练过程中的评估损失。
训练参数
大多数超参数在不同模型规模下保持不变:
CrossEncoderTrainingArguments(
num_train_epochs=1, # I chose more data over more epochs
per_device_train_batch_size=..., # global_batch_size // world_size (see table below)
gradient_accumulation_steps=1,
learning_rate=..., # per-size, see table
warmup_ratio=0.03, # ~3% linear warmup, then linear decay (default)
bf16=True, # FA2 + bf16 throughout
eval_strategy="steps",
eval_steps=0.05, # NanoBEIR every 5% of training
save_strategy="steps",
save_steps=0.05,
save_total_limit=5,
load_best_model_at_end=True,
metric_for_best_model="eval_NanoBEIR_R100_mean_ndcg@10",
seed=12,
)
只有学习率和全局批次大小会随模型规模变化。
| 规模 | 学习率 | 全局批次大小 |
|---|---|---|
| 1700 万 | 2.4e-4 | 1024 |
| 3200 万 | 1.2e-4 | 512 |
| 6800 万 | 3e-5 | 256 |
| 1.5 亿 | 1.5e-5 | 192 |
| 4 亿 | 7e-6 | 256 |
| 10 亿 | 3e-6 | 512 |
全局批次大小 = 每设备批次大小 × 世界大小 × 梯度累积步数。在单个 8-GPU 节点上,17m 模型的 1024 全局批次意味着每设备批次大小为 128。在 8 个节点上,则意味着每设备批次大小为 8。训练脚本通过 `global_batch_size // world_size` 计算每设备批次大小,因此同一脚本可在任意节点数量下运行。全局批次大小本可以做得更一致,但我发现上述数值效果良好,且不想仅仅为了追求一致性而重新调整它们。
评估
我在训练期间监控了 NanoBEIR 的平均 NDCG@10(每完成 5% 的训练步骤评估一次),并将其作为 `metric_for_best_model` 用于 `load_best_model_at_end`。NanoBEIR 速度很快,因此我可以在每次训练中运行 20 次评估。训练结束后,我在完整的 MTEB(英文,v2)检索基准上评估了最佳检查点(根据 NanoBEIR 确定)和最终检查点。最终发布的检查点是在 MTEB 上表现最好的那个。除 68m 模型外,NanoBEIR 偏好的检查点均胜出;对于 68m 模型,最终检查点略强。
整体训练脚本
完整脚本(每个已发布模型均使用此脚本训练)是一个单一文件。每次运行时只需更改 `ENCODER_SIZE`,其余一切均为自动处理:
from __future__ import annotations
import logging
import os
from pathlib import Path
import torch
import torch.nn as nn
from datasets import concatenate_datasets, get_dataset_config_names, load_dataset
from sentence_transformers import CrossEncoder
from sentence_transformers.base.modules import Dense
from sentence_transformers.cross_encoder import (
CrossEncoderModelCardData,
CrossEncoderTrainer,
CrossEncoderTrainingArguments,
)
from sentence_transformers.cross_encoder.evaluation import CrossEncoderNanoBEIREvaluator
from sentence_transformers.cross_encoder.losses import MSELoss
from sentence_transformers.sentence_transformer.modules import LayerNorm, Pooling, Transformer
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s", datefmt="%H:%M:%S")
logging.getLogger("httpx").setLevel(logging.WARNING)
# Per-size config. I swept the learning rates with these global (effective) batch sizes,
# also by incorporating accum_steps
CONFIGS: dict[str, dict] = {
"17m": {"base_model_name": "jhu-clsp/ettin-encoder-17m", "learning_rate": 2.4e-4, "global_batch_size": 1024},
"32m": {"base_model_name": "jhu-clsp/ettin-encoder-32m", "learning_rate": 1.2e-4, "global_batch_size": 512},
"68m": {"base_model_name": "jhu-clsp/ettin-encoder-68m", "learning_rate": 3e-5, "global_batch_size": 256},
"150m": {"base_model_name": "jhu-clsp/ettin-encoder-150m", "learning_rate": 1.5e-5, "global_batch_size": 192},
"400m": {"base_model_name": "jhu-clsp/ettin-encoder-400m", "learning_rate": 7e-6, "global_batch_size": 256},
"1b": {"base_model_name": "jhu-clsp/ettin-encoder-1b", "learning_rate": 3e-6, "global_batch_size": 512},
}
ENCODER_SIZE = "17m"
def main() -> None:
config = CONFIGS[ENCODER_SIZE]
encoder_id = config["base_model_name"]
learning_rate = config["learning_rate"]
global_batch_size = config["global_batch_size"]
world_size = int(os.environ.get("WORLD_SIZE", 1))
per_device_batch_size = global_batch_size // world_size
dataloader_workers = 0 if world_size > 8 else 4
run_name = f"ettin-reranker-{ENCODER_SIZE}-lr{learning_rate:.0e}"
# 1. Load a model to finetune with model card data
# The model mirrors ModernBertForSequenceClassification, but with a 'headless' Transformer that just loads
# AutoModel. This allows for unpadding with FA2, which isn't possible with AutoModelForSequenceClassification.
# This speeds up training considerably, while heavily reducing memory usage.
torch.manual_seed(12)
transformer = Transformer(encoder_id, model_kwargs={"attn_implementation": "flash_attention_2"})
transformer.model.config.num_labels = 1
embedding_dimension = transformer.get_embedding_dimension()
pooling = Pooling(embedding_dimension=embedding_dimension, pooling_mode="cls")
dense_inner = Dense(
in_features=embedding_dimension, out_features=embedding_dimension, bias=False,
activation_function=nn.GELU(),
module_input_name="sentence_embedding", module_output_name="sentence_embedding",
)
norm = LayerNorm(dimension=embedding_dimension)
dense_score = Dense(
in_features=embedding_dimension, out_features=1, bias=True,
activation_function=nn.Identity(),
module_input_name="sentence_embedding", module_output_name="scores",
)
model = CrossEncoder(
modules=[transformer, pooling, dense_inner, norm, dense_score],
num_labels=1,
activation_fn=nn.Identity(),
model_card_data=CrossEncoderModelCardData(
model_name=f"Ettin Reranker {ENCODER_SIZE} distilled from mxbai-rerank-large-v2",
language="en",
license="apache-2.0",
),
)
actual_attn = getattr(model[0].model.config, "_attn_implementation", None)
if not (actual_attn and "flash" in actual_attn.lower()):
logging.warning(f"FA2 may not be active (attn_impl={actual_attn!r}); training will be slower.")
# 2. Load the dataset. Each config is one source subset (32 lighton + 7 rerank retrieval
# domains). The held-out eval rows live as the 'validation' split of the 'quora' config.
dataset_repo = "cross-encoder/ettin-reranker-v1-data"
train_pieces = []
eval_dataset = None
for config_name in get_dataset_config_names(dataset_repo):
dataset = load_dataset(dataset_repo, config_name)
train_pieces.append(dataset["train"])
if "validation" in dataset:
eval_dataset = dataset["validation"]
train_dataset = concatenate_datasets(train_pieces)
print(train_dataset)
# 3. Define a loss function
loss = MSELoss(model)
# 4. Specify training arguments
args = CrossEncoderTrainingArguments(
output_dir=f"models/{run_name}",
num_train_epochs=1,
per_device_train_batch_size=per_device_batch_size,
per_device_eval_batch_size=per_device_batch_size,
gradient_accumulation_steps=1,
learning_rate=learning_rate,
warmup_ratio=0.03,
bf16=True,
eval_strategy="steps",
eval_steps=0.05,
save_strategy="steps",
save_steps=0.05,
save_total_limit=5,
logging_steps=0.025,
logging_first_step=True,
load_best_model_at_end=True,
metric_for_best_model="eval_NanoBEIR_R100_mean_ndcg@10",
dataloader_num_workers=dataloader_workers,
run_name=run_name,
seed=12,
)
# 5. Create an evaluator
evaluator = CrossEncoderNanoBEIREvaluator(
dataset_names=["msmarco", "nfcorpus", "nq", "fiqa2018", "touche2020", "scifact",
"hotpotqa", "arguana", "fever", "dbpedia", "climatefever", "scidocs",
"quoraretrieval"],
batch_size=per_device_batch_size,
always_rerank_positives=False,
show_progress_bar=False,
)
# 6. Create a trainer
trainer = CrossEncoderTrainer(
model=model,
args=args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
loss=loss,
evaluator=evaluator,
)
# 7. Evaluate before training
if trainer.is_world_process_zero():
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
evaluator(model)
# 8. Train
trainer.train()
# 9. Evaluate the final model
if trainer.is_world_process_zero():
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
evaluator(model)
# 10. Save the final model
final_dir = f"models/{run_name}/final"
model.save_pretrained(final_dir)
if __name__ == "__main__":
main()
对于多节点训练(17m/32m 以上的模型),使用 `torchrun` 启动同一脚本:
# Single-node (17m, 32m): defaults work
python train.py
# Multi-node 4n setup for 150m, preserves global_batch_size=192:
torchrun --nproc_per_node=8 --nnodes=4 ... train.py
结论
ettin-reranker-v1 系列模型,采用单一简单配方训练,在已发布的所有尺寸(最高至 1B 参数)中均达到业界领先水平。从强教师模型进行逐点 MSE 知识蒸馏,并应用于广泛领域与检索专用数据的混合数据集,这一方法可从 17M 参数干净地扩展到 1B 参数,不同尺寸之间仅需调整学习率和每设备批次大小。
每个 ettin-reranker-v1 模型在 MTEB 和 NanoBEIR 上均以显著优势超越了 ms-marco-MiniLM-L*-v2 系列。`cross-encoder/ettin-reranker-150m-v1` 是我在 600M 参数以下范围内测试过的最强中端重排序模型;`cross-encoder/ettin-reranker-400m-v1` 与 1.54B 参数的教师模型在 MTEB 得分上的差距仅为 0.0024;而 `cross-encoder/ettin-reranker-1b-v1` 与该教师模型的得分差距仅为 0.0001。
所有内容汇总如下:
模型:
- cross-encoder/ettin-reranker-17m-v1
- cross-encoder/ettin-reranker-32m-v1
- cross-encoder/ettin-reranker-68m-v1
- cross-encoder/ettin-reranker-150m-v1
- cross-encoder/ettin-reranker-400m-v1
- cross-encoder/ettin-reranker-1b-v1
数据集:cross-encoder/ettin-reranker-v1-data,包含约 1.43 亿条(查询、文档、标签)三元组,按 39 个命名拆分保存,因此每一行数据的来源均可追溯。
训练脚本:即上方“整体训练脚本”中约 150 行的代码,该脚本用于训练全部六个模型。
如果你在此基础上构建了任何成果,请务必告诉我!我真的很想看看大家会用它做什么,如果你能利用已发布的数据训练出更好的重排序器,那就更棒了。这个方案故意设计得比较简单,部分原因是为了给其他人留出充足的改进空间。训练一个更强的教师模型,同样的脚本就能持续产出更好的学生模型。
致谢
我要感谢 Ettin 团队(Orion Weller、Kathryn Ricci、Marc Marone、Antoine Chaffin、Dawn Lawrie 和 Benjamin Van Durme)构建了这些重排序器所基于的基础编码器,感谢 LightOn 团队(Antoine Chaffin、Raphael Sourty、Paulo Moura 和 Amélie Chatelain)在训练数据收集方面所做的工作,以及感谢 Mixedbread AI 团队(Xianming Li、Aamir Shakir、Rui Huang、Tsz-fung Andrew Lee、Julius Lipp、Benjamin Clavié 和 Jing Li)在教师模型方面所做的工作。
引用
如果你使用了 ettin-reranker-v1 系列模型或任何已发布的成果,请引用本篇博客文章。
@misc{aarsen2026ettin-reranker,
title = "Introducing the Ettin Reranker Family",
author = "Aarsen, Tom",
year = "2026",
publisher = "Hugging Face",
url = "https://huggingface.co/blog/ettin-reranker",
}