2019 年,Reimers 和 Gurevych 发表了论文《Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks》,解决了 BERT 在语义相似度任务上的一个根本性问题:直接用 BERT 计算两个句子的相似度,需要将它们拼接后送入模型,这在大规模检索场景下计算代价极高(O(n²) 次前向传播)。Sentence-BERT 通过孪生网络(Siamese Network)架构,为每个句子独立生成固定维度的语义向量,使语义搜索和聚类变得高效可行。
Sentence-BERT 是语义表示学习的里程碑,它启发了 SimCSE、E5、BGE 等一系列现代嵌入模型,至今仍是向量检索系统的技术基础。
本文要点
- BERT 句子表示的困境:CLS 与平均池化的局限
- 孪生网络(Siamese Network)架构设计
- 三种训练目标:分类、回归和三元组损失
- SNLI 和 MultiNLI 数据集上的训练策略
- 语义搜索与 FAISS 向量检索
- sentence-transformers 库的使用方法
- 从 SimCSE 到 E5、BGE、GTE 的演进历程
- 在 STS 基准上的性能对比
一、BERT 的句子表示困境
1.1 直接使用 BERT 的问题
BERT 虽然在 NLU 任务上表现卓越,但直接用于获取句子表示时存在两个核心问题:
问题一:计算代价过高
计算两个句子的语义相似度时,标准做法是将两个句子拼接后输入 BERT:
[CLS] 句子A [SEP] 句子B [SEP]要找到与查询最相似的句子,需要对候选集中每个句子都做一次完整的前向传播。对于 10,000 个候选句子,这意味着 10,000 次前向传播——在大规模检索场景下完全不可行。
问题二:表示质量不佳
即使单独提取 BERT 的句子表示(CLS Token 或平均池化),其语义质量也不理想:
# BERT 直接获取句子表示的两种方式from transformers import BertModel, BertTokenizer
model = BertModel.from_pretrained('bert-base-uncased')tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
inputs = tokenizer("The cat sits on the mat", return_tensors="pt")outputs = model(**inputs)
# 方式 1:CLS Tokencls_embedding = outputs.last_hidden_state[:, 0, :] # [1, 768]
# 方式 2:平均池化mean_embedding = outputs.last_hidden_state.mean(dim=1) # [1, 768]研究表明,BERT 的 CLS 表示和平均池化表示在语义相似度任务上的表现甚至不如 GloVe 词向量的简单平均。
1.2 根本原因分析
BERT 的预训练目标是 Token 级别的 MLM,并非针对句子级别的语义表示优化。NSP(Next Sentence Prediction)虽然是句子级别的任务,但过于简单(只是判断两个句子是否相邻),无法学到有意义的语义表示。
二、孪生网络架构
Sentence-BERT 的核心创新在于使用孪生网络(Siamese Network)架构来微调 BERT,使其能够生成高质量的句子嵌入。
2.1 架构设计
两个句子分别通过共享权重的 BERT 模型和池化层,得到固定维度的嵌入向量 u 和 v。由于权重共享,语义相似的句子会被映射到向量空间中相近的位置。
2.2 池化策略
Sentence-BERT 支持三种池化策略:
- CLS 池化:直接取 CLS Token 的输出
- 平均池化(MEAN):对所有 Token 的输出取平均(默认推荐)
- 最大池化(MAX):对所有 Token 的输出取最大值
实验表明,平均池化在大多数任务上表现最佳。
2.3 合并策略
根据不同任务,u 和 v 的合并方式不同:
# 分类任务的合并方式combined = torch.cat([u, v, |u - v|], dim=1) # |u-v| 是逐元素绝对差
# 回归任务的相似度计算similarity = cosine_similarity(u, v) # 或点积
# 三元组损失的距离计算distance = euclidean_distance(u, v)三、三种训练目标
Sentence-BERT 根据不同任务设计了三种训练目标:
3.1 分类目标(Classification Objective)
用于 NLI(自然语言推理)等分类任务:
# 分类目标# 输入:句子对 (A, B) + 标签(蕴含/矛盾/中立)# 合并向量:[u, v, |u-v|]# 输出:3 类分类
combined = torch.cat([u, v, torch.abs(u - v)], dim=1) # [batch, 3*dim]logits = classifier(combined) # [batch, 3]loss = CrossEntropyLoss(logits, labels)3.2 回归目标(Regression Objective)
用于 STS(语义文本相似度)等回归任务:
# 回归目标# 输入:句子对 (A, B) + 相似度分数(0-5)# 计算 cosine similarity
sim = cosine_similarity(u, v) # [-1, 1]# 缩放到 [0, 5] 范围pred = sim * 2.5 + 2.5loss = MSELoss(pred, gold_score)3.3 三元组损失(Triplet Objective)
用于学习相对相似度排序:
# 三元组损失# 输入:锚点句 a, 正样本 p, 负样本 n# 目标:使 a 与 p 的距离小于 a 与 n 的距离 + margin
dist_pos = euclidean_distance(emb_a, emb_p)dist_neg = euclidean_distance(emb_a, emb_n)loss = max(0, dist_pos - dist_neg + margin)四、训练数据与流程
Sentence-BERT 主要使用 SNLI 和 MultiNLI 数据集进行训练:
| 数据集 | 句子对数 | 标签类型 | 来源 |
|---|---|---|---|
| SNLI | 570K | 蕴含/矛盾/中立 | 图像描述 |
| MultiNLI | 433K | 蕴含/矛盾/中立 | 多领域文本 |
| STS Benchmark | 8,628 | 0-5 相似度分数 | 新闻标题 |
4.1 训练流程
关键发现:在 SNLI + MultiNLI 上用分类目标训练的模型,在 STS 任务上表现最好——即使训练数据不是相似度标注的,孪生网络的对比学习也能学到高质量的语义表示。
五、语义搜索与 FAISS
Sentence-BERT 最广泛的应用场景是语义搜索。通过预计算所有文档的嵌入向量,检索时只需计算查询向量与候选向量的余弦相似度。
5.1 基本流程
from sentence_transformers import SentenceTransformerimport faissimport numpy as np
# 1. 加载模型model = SentenceTransformer('all-MiniLM-L6-v2')
# 2. 编码文档库documents = [ "Python is a popular programming language", "Machine learning models require large datasets", "The weather is sunny today", # ... 数百万条文档]doc_embeddings = model.encode(documents) # [N, 384]
# 3. 构建 FAISS 索引dimension = doc_embeddings.shape[1]index = faiss.IndexFlatIP(dimension) # 内积索引faiss.normalize_L2(doc_embeddings) # L2 归一化(使内积 = cosine sim)index.add(doc_embeddings.astype('float32'))
# 4. 搜索query = "What programming languages are popular?"query_embedding = model.encode([query])faiss.normalize_L2(query_embedding)
scores, indices = index.search(query_embedding.astype('float32'), k=5)
# 5. 返回结果for i, (score, idx) in enumerate(zip(scores[0], indices[0])): print(f"Rank {i+1}: {documents[idx]} (score: {score:.4f})")5.2 FAISS 索引选择
| 索引类型 | 适合规模 | 搜索精度 | 内存占用 |
|---|---|---|---|
| IndexFlatIP | <100K | 精确搜索 | 高 |
| IndexIVFFlat | 100K-10M | 近似(可调) | 中 |
| IndexIVFPQ | >10M | 近似(可调) | 低 |
| IndexHNSW | 任意 | 高精度 | 中 |
六、语义表示的演进
Sentence-BERT 开创了句子嵌入的学习范式,此后出现了多个重要的后续工作:
6.1 SimCSE:Dropout 增强
SimCSE(Simple Contrastive Learning of Sentence Embeddings)是一个巧妙的改进:
- 无监督版本:同一个句子通过两次不同的 Dropout 路径,得到两个”正样本”
- 有监督版本:使用 NLI 数据的蕴含句作为正样本,矛盾句作为困难负样本
# SimCSE 无监督训练的核心思想# 同一个句子通过两次不同的 Dropoutsentence = "A cat sits on the mat"emb1 = model(sentence, dropout_mask_1) # 正样本对 1emb2 = model(sentence, dropout_mask_2) # 正样本对 2# 两个嵌入应该相似(正样本对)6.2 E5:预训练 + 精调两阶段
E5(EmbEddings from bidirectional Encoder rEpresentations)引入了两阶段训练:
- 预训练阶段:使用大规模弱监督文本对(Reddit 问答、StackExchange 等)
- 精调阶段:使用高质量标注数据(NLI、STS 等)
6.3 BGE:指令感知嵌入
BGE(BAAI General Embedding)的关键创新是指令感知(Instruction-aware)嵌入:
# BGE 的查询格式query_instruction = "Represent this sentence for searching relevant passages: "query_embedding = model.encode(query_instruction + query)doc_embedding = model.encode(document) # 文档不需要指令前缀
# 这样同一个查询可以有不同指令,适配不同任务6.4 现代 LLM 作为嵌入模型
最新的趋势是直接使用 LLM(如 Mistral-7B)作为嵌入模型,通过在最后几层添加池化得到句子表示。E5-Mistral 和 NV-Embed 是这一方向的代表工作。
七、性能对比
7.1 STS Benchmark 上的表现
| 模型 | STS12 | STS13 | STS14 | STS15 | STS16 | STS-B | SICK-R | 平均 |
|---|---|---|---|---|---|---|---|---|
| BERT CLS | 29.0 | 42.0 | 41.0 | 57.0 | 58.0 | 47.0 | 43.0 | 45.3 |
| BERT MEAN | 31.0 | 44.0 | 45.0 | 59.0 | 60.0 | 49.0 | 45.0 | 47.6 |
| SBERT (NLI) | 64.0 | 72.0 | 71.0 | 76.0 | 74.0 | 78.0 | 73.0 | 72.6 |
| SBERT (NLI+STS) | 67.0 | 74.0 | 73.0 | 79.0 | 76.0 | 81.0 | 76.0 | 75.1 |
| SimCSE (unsup) | 68.0 | 76.0 | 75.0 | 80.0 | 77.0 | 82.0 | 77.0 | 76.4 |
| E5-base | 72.0 | 80.0 | 78.0 | 84.0 | 80.0 | 85.0 | 79.0 | 79.7 |
| BGE-base | 73.0 | 81.0 | 79.0 | 85.0 | 81.0 | 86.0 | 80.0 | 80.7 |
可以看到,从 BERT 直接使用到 Sentence-BERT 有约 25 分的巨大提升,而后续的 SimCSE、E5、BGE 又进一步提升了约 5-8 分。
常见问题 FAQ
7.1 Q1: 为什么 BERT 的句子表示质量不如 GloVe 平均?
BERT 的预训练目标是 Token 级别的 MLM,不是句子级别的语义表示。各层的表示呈现出”各向异性”(anisotropic),即大部分向量集中在向量空间的一个狭窄区域,导致即使语义不同的句子也被映射到相近的位置。Sentence-BERT 通过对比学习缓解了这个问题。
7.2 Q2: sentence-transformers 库如何选择模型?
选择取决于场景:
- 通用语义搜索:
all-MiniLM-L6-v2(快速)或all-mpnet-base-v2(精度更高) - 多语言:
paraphrase-multilingual-MiniLM-L12-v2 - 中文:
shibing624/text2vec-base-chinese或 BGE 系列的中文模型 - 最新最优:BAAI/bge-large-en-v1.5 或 intfloat/e5-large-v2
7.3 Q3: 三元组损失中的 margin 如何选择?
margin 通常设为 1.0。太小的 margin 会导致正负样本距离不够分开,太大的 margin 会导致训练困难(模型难以满足约束)。在实际应用中,1.0 是一个经过验证的良好默认值。
7.4 Q4: FAISS 搜索和暴力搜索的精度差距大吗?
对于 IndexFlatIP(暴力搜索),精度是 100%。对于近似索引(IVFFlat、IVFPQ、HNSW),精度取决于参数设置。通常 nprobe=10-20 时,recall@10 可以达到 95% 以上,同时搜索速度快 10-100 倍。
7.5 Q5: Sentence-BERT 还适用于现代 LLM 时代吗?
Sentence-BERT 的范式(预训练到对比学习微调到向量检索)仍然是向量检索系统的基础。现代的改进主要是用更强的预训练模型(如 RoBERTa、DeBERTa 甚至 Mistral-7B)替换 BERT backbone,训练方法的核心思想没有根本变化。
7.6 Q6: 如何处理长文档的语义搜索?
Sentence-BERT 默认最大长度为 128-256 Token。对于长文档,常见策略有:
- 分段编码:将文档分成段落,分别编码后取平均或最大池化
- 层次化方法:先编码段落,再用 Cross-Encoder 精排
- 长文本模型:使用支持 8K+ Token 的嵌入模型(如 jina-embeddings-v2)
小结
Sentence-BERT 的核心贡献可以总结为:
- 孪生网络架构:解决了 BERT 句子对计算 O(n²) 的效率问题
- 三种训练目标:分类、回归、三元组损失,覆盖了不同类型的语义任务
- NLI 数据集的有效利用:证明分类任务也能训练出高质量句子嵌入
- sentence-transformers 生态:降低了语义搜索的技术门槛
从 Sentence-BERT 到 SimCSE、E5、BGE、GTE,语义表示的质量持续提升,但核心范式——对比学习 + 预训练微调——始终未变。Sentence-BERT 的思想已经深入到现代 RAG 系统、搜索引擎、推荐系统的底层架构中。
对于想要构建语义搜索系统的读者,建议:
- 使用 sentence-transformers 快速搭建原型
- 根据数据特点选择合适的预训练模型
- 在领域数据上进行微调以获得最佳效果
- 使用 FAISS 等向量数据库实现高效检索
参考资料
- Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks — Reimers & Gurevych, 2019
- SimCSE: Simple Contrastive Learning of Sentence Embeddings — Gao et al., 2021
- E5: Text Embeddings by Weak-Supervised Contrastive Pre-training — Wang et al., 2022
- BGE: C-Pack: Packaged Resources to Advance General Chinese Embedding — Xiao et al., 2023
- Making Monolingual Sentence Embeddings Multilingual using Knowledge Distillation — Reimers & Gurevych, 2020
- FAISS: A Library for Efficient Similarity Search and Clustering of Dense Vectors — Facebook Research
- sentence-transformers 官方文档
- MTEB Leaderboard: Massive Text Embedding Benchmark
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






