SBERT
#2026/01/01 #ai
让句子比较变得又快又准
目录
- 一、为什么需要SBERT?先看看老方法的问题
- 二、SBERT 的聪明解决方案
- 三、SBERT的架构设计
- 四、训练方式详解
- 五、SBERT vs 交叉编码器
- 六、代码示例
- 七、SBERT的优势总结
- 八、应用场景
- 九、核心知识点总结
- 十、类比理解
一、为什么需要SBERT?先看看老方法的问题
1. BERT的尴尬处境
在SBERT出现之前,如果我们想用BERT来比较两个句子的相似度,会遇到一个大麻烦:计算开销太大了!
传统的做法叫做交叉编码器(Cross-Encoder):
句子A:"我喜欢猫"
句子B:"我爱小猫"
↓
把两个句子同时塞进BERT
↓
BERT处理后输出相似度分数

**图 :交叉编码器的架构。两个句子被连接在一起,用 <SEP> 词元分隔,然后作为一个整体输入模型
问题来了:如果你有10000个句子,想找出最相似的两个句子,需要比较多少次?
计算次数 = 10000 × 9999 / 2 = 近5千万次!
每次比较都要跑一遍BERT,这太慢了!
二、SBERT 的聪明解决方案
核心思想:先转换,再比较
SBERT(Sentence-BERT)的作者想到了一个聪明的办法:
老方法(交叉编码器):
句子A + 句子B → BERT → 相似度分数
每对句子都要重新计算
新方法(SBERT/双编码器):
句子A → BERT → 嵌入向量A
句子B → BERT → 嵌入向量B
然后比较向量A和向量B的距离
关键改进:
- 每个句子只需要处理 一次
- 得到的嵌入向量可以存起来反复使用
- 比较向量远比跑 BERT 快得多

图:原始 sentence-transformers 模型的架构,它采用了孪生网络(也称为双编码器)的结构
三、SBERT的架构设计
1. 从分类头到池化层
旧 BERT的做法:
输入句子 → BERT → 分类头 → 输出类别
但研究人员发现,如果直接用BERT的输出层做平均(或用[CLS]词元)来生成句子嵌入,效果竟然还不如简单的词向量平均(比如GloVe)!
SBERT的改进:
- 移除分类头
- 在BERT的最终输出层上使用平均池化
- 通过池化生成固定大小的句子嵌入向量
关于 分类头 和 池化 ,详见 4. 分类头、池化、CLS
2. 孪生网络架构(Siamese Network)
SBERT使用了一个巧妙的孪生架构:
句子A 句子B
↓ ↓
BERT (模型1) BERT (模型2)
↓ ↓
词元嵌入 词元嵌入
↓ ↓
池化层 池化层
↓ ↓
句子嵌入u 句子嵌入v
↓ ↓
合并:(u, v, |u-v|)
↓
softmax分类
关键点:
- 两个BERT模型 共享权重(实际上是
同一个模型) - 每个句子独立通过BERT处理
- 最后比较两个句子的嵌入向量
四、训练方式详解
1. 孪生网络的实际使用
虽然图上画了两个BERT,但实际上只有一个模型:
# 伪代码示意
bert_model = load_bert_model()
# 处理句子A
embedding_a = bert_model.encode("句子A")
# 处理句子B(用同一个模型)
embedding_b = bert_model.encode("句子B")
# 计算相似度
similarity = cosine_similarity(embedding_a, embedding_b)
因为是同一个模型,所以:
- 节省内存
- 保证一致性
- 可以分别处理句子(不需要同时输入)
2. 训练目标
SBERT通过对比学习训练:
训练数据:
配对句子 + 标签(相似/不相似)
例如:
句子A:"我的狗很可爱"
句子B:"我有一只狗"
标签:相似 ✅
句子A:"我的狗很可爱"
句子C:"今天天气不错"
标签:不相似 ❌
训练过程:
- 两个句子分别通过BERT
- 得到句子嵌入
u和v - 将
(u, v, |u-v|)拼接起来 - 通过 s
oftmax判断是否相似 - 根据预测结果调整
BERT参数
五、SBERT vs 交叉编码器
对比表格
| 特性 | 交叉编码器 | SBERT(双编码器) |
|---|---|---|
| 处理方式 | 两个句子同时输入 | 两个句子分别输入 |
| 速度 | 慢 🐢 | 快 🚀 |
| 准确率 | 略高 | 略低但可接受 |
| 缓存 | 不能缓存 | 可以缓存嵌入向量 |
| 适用场景 | 精排(小规模) | 粗排(大规模) |
具体数字对比
假设要在10000个句子中找最相似的:
交叉编码器:
- 需要比较:50,000,000次
- 每次都要跑BERT
- 时间:几小时 ⏰
SBERT:
- 先生成10000个嵌入向量(一次性)
- 然后比较向量(超快)
- 时间:几分钟 ⚡
六、代码示例
使用sentence-transformers库
from sentence_transformers import SentenceTransformer
# 1. 加载SBERT模型
model = SentenceTransformer('all-mpnet-base-v2')
# 2. 准备句子
sentences = [
"我喜欢猫",
"我爱小猫",
"今天天气不错"
]
# 3. 生成嵌入向量(每个句子只需处理一次)
embeddings = model.encode(sentences)
print(embeddings.shape) # (3, 768) - 3个句子,每个768维
# 4. 计算相似度
from sklearn.metrics.pairwise import cosine_similarity
similarity_matrix = cosine_similarity(embeddings)
print(similarity_matrix)
# 前两个句子(都是关于猫)相似度很高
# 第三个句子(天气)与前两个相似度低
七、SBERT的优势总结
1. 速度提升
假设BERT处理一个句子需要10ms
交叉编码器比较1000个句子:
(1000 × 999 / 2) × 10ms = 约1.4小时
SBERT:
生成嵌入:1000 × 10ms = 10秒
比较向量:几乎瞬间完成
总时间:约10秒 ⚡
速度提升了约500倍!
2. 可扩展性
因为嵌入向量可以预先计算并存储:
✅ 可以建立向量数据库
✅ 支持实时搜索
✅ 方便增量更新
3. 灵活性
生成的嵌入向量可以用于:
- 语义搜索
- 文本聚类
- 分类任务
- 推荐系统
八、应用场景
场景1:语义搜索(第8章)
# 用户查询
query = "如何修复漏水的水龙头"
# 文档库(已预先生成嵌入)
documents = [...] # 海量文档
doc_embeddings = [...] # 预先计算好的嵌入
# 实时搜索(只需处理查询)
query_embedding = model.encode(query)
similarities = cosine_similarity([query_embedding], doc_embeddings)
# 返回最相似的文档
top_docs = get_top_k(similarities, k=10)
场景2:文本聚类(第5章)
# BERTopic就使用SBERT作为默认嵌入模型
from bertopic import BERTopic
topic_model = BERTopic(
embedding_model="all-mpnet-base-v2" # 使用SBERT
)
topics = topic_model.fit_transform(documents)
九、核心知识点总结
三个关键创新
- 池化替代分类头:
- 旧:BERT → 分类头
- 新:BERT →
池化层→ 固定维度嵌入
- 孪生架构:
- 两个BERT共享权重
- 句子可独立处理
- 对比学习训练:
- 相似句子嵌入靠近
- 不相似句子嵌入远离
与word2vec的联系
SBERT的训练思想其实源自word2vec的对比学习:
word2vec:
- 正例:句子中相邻的词
- 负例:随机采样的词
SBERT:
- 正例:语义相似的句子对
- 负例:不相似的句子对
十、类比理解
把句子比较比作找相似的人:
交叉编码器方法:
每次要比较两个人时,都要重新观察两个人的所有特征
→ 很准确,但太慢
SBERT方法:
先给每个人拍一张"特征照片"(嵌入向量)
以后比较只需要看照片
→ 快得多,准确度也不错
总结一句话:SBERT通过先嵌入、后比较的方式,把句子相似度计算的速度提升了数百倍,同时保持了不错的准确率,是现代语义搜索和文本匹配系统的基础!