第一章:文本相似度与语义表示概述
在深入 sentence-transformers
框架之前,我们首先需要对文本相似度计算及其背后的核心概念——语义表示,有一个清晰且全面的理解。这构成了后续所有讨论的基础。
1.1 什么是文本相似度?
1.1.1 定义与重要性
文本相似度(Text Similarity)是指衡量两段文本(可以是词、短语、句子、段落或整个文档)在意义或内容上相近程度的指标。这种相近可以是字面上的(lexical similarity),也可以是语义上的(semantic similarity)。
在自然语言处理(NLP)领域,准确地计算文本相似度至关重要,因为它构成了许多高级应用的核心技术。能够理解并量化文本之间的关系,是机器理解人类语言的关键一步。随着信息爆炸式增长,快速有效地从海量文本数据中提取有价值信息、识别相关内容、避免冗余,都离不开文本相似度计算。
1.1.2 不同层级的文本相似度
文本相似度的衡量可以发生在不同的粒度层级:
词汇层相似度 (Lexical Similarity):
句法层相似度 (Syntactic Similarity):
语义层相似度 (Semantic Similarity):
sentence-transformers
主要致力于解决句子和段落级别的语义相似度问题。文档层相似度 (Document Similarity):
不同的应用场景可能侧重于不同层级的相似度。例如,拼写检查可能更关注词汇层相似度,而智能问答系统则高度依赖语义层相似度。
1.1.3 文本相似度的应用场景
文本相似度计算技术在现实世界中有非常广泛的应用,以下是一些典型的例子:
信息检索 (Information Retrieval) / 搜索引擎 (Search Engines):
sentence-transformers
可以将查询和文档都编码为向量,通过计算向量间的余弦相似度来排序搜索结果。智能问答 (Question Answering, QA) / 聊天机器人 (Chatbots):
抄袭检测 (Plagiarism Detection) / 重复内容识别 (Duplicate Content Detection):
推荐系统 (Recommendation Systems):
文本聚类 (Text Clustering) / 主题建模 (Topic Modeling):
文本摘要 (Text Summarization):
机器翻译评估 (Machine Translation Evaluation):
情感分析 (Sentiment Analysis) / 观点挖掘 (Opinion Mining):
理解这些应用场景有助于我们认识到为什么高质量的文本语义表示和相似度计算如此重要,这也是 sentence-transformers
这类工具应运而生的驱动力。
1.2 传统文本表示方法的局限性
在深度学习方法(尤其是基于Transformer的模型)兴起之前,NLP领域依赖于一些经典的文本表示方法。虽然这些方法在特定时期和特定任务上取得了一定的成功,但它们在捕捉深层语义信息方面存在显著的局限性。
1.2.1 词袋模型 (Bag-of-Words, BoW)
[2, 1, 1, 1, 1, 0, 0]
(对应词典顺序)[2, 1, 0, 0, 0, 1, 1]
1.2.2 TF-IDF (Term Frequency-Inverse Document Frequency)
1.2.3 N-gram 模型 (N-gram Models)
1.2.4 传统方法存在的共性问题总结
上述传统文本表示方法(BoW, TF-IDF, N-grams)虽然在NLP发展历程中扮演了重要角色,但它们共同面临一些难以克服的挑战,尤其是在追求深层语义理解的现代NLP任务中:
语义鸿沟 (Semantic Gap):
高维与稀疏性 (High Dimensionality and Sparsity):
缺乏对词序和句法结构的有效建模 (Lack of Effective Modeling of Word Order and Syntactic Structure):
无法处理未登录词 (Out-of-Vocabulary, OOV) / 新词:
这些局限性促使研究者们探索新的文本表示方法,能够将词语、句子乃至文档映射到低维、稠密的向量空间中,并且使得语义上相似的文本在向量空间中的距离也相近。这就是词嵌入(Word Embeddings)和后续句子/文档嵌入(Sentence/Document Embeddings)技术发展的动力。
1.3 词嵌入与文档嵌入思想的演进
为了克服传统文本表示方法的局限性,尤其是语义鸿沟问题,研究者们提出了分布式语义表示(Distributed Semantic Representations)的思想,其中最著名的成果就是词嵌入(Word Embeddings)。
1.3.1 Word2Vec (Skip-gram, CBOW)
提出者与时间: 由Google的Tomas Mikolov团队在2013年提出。
核心思想 (Distributional Hypothesis): “一个词的意义取决于它经常共同出现的词” (You shall know a word by the company it keeps - J.R. Firth)。Word2Vec基于这一思想,通过训练神经网络来学习词的向量表示(词嵌入)。
主要模型架构:
CBOW (Continuous Bag-of-Words):
Skip-gram:
训练技巧:
特点与贡献:
vector('king') - vector('man') + vector('woman')
的结果在向量空间中非常接近 vector('queen')
。局限性:
1.3.2 GloVe (Global Vectors for Word Representation)
1.3.3 FastText
1.3.4 从词嵌入到句嵌入的挑战
上述词嵌入方法(Word2Vec, GloVe, FastText)主要关注单个词的表示。然而,在许多NLP任务中,我们需要的是整个句子或段落的向量表示(句嵌入或文档嵌入)。如何从高质量的词嵌入有效地得到高质量的句嵌入,是一个重要且有挑战性的问题:
简单平均/加权平均 (Simple/Weighted Averaging of Word Embeddings):
SIF (Smooth Inverse Frequency) 权重:
基于RNN/LSTM/GRU的方法:
基于CNN的方法:
这些从词嵌入到句嵌入的方法各有优劣,但在Transformer模型出现之前,它们是主流的句表示学习技术。然而,它们在处理上下文依赖、复杂语义和长距离关系方面仍有提升空间。Transformer架构的出现,为解决这些问题提供了强大的新工具。
1.4 Transformer 模型与预训练语言模型
Transformer模型的提出是自然语言处理领域的一大突破,它彻底改变了序列建模的方式,并为后续大规模预训练语言模型(Pre-trained Language Models, PLMs)的成功奠定了基础。
1.4.1 Transformer 架构简介
提出者与时间: 由Google团队在2017年的论文 “Attention Is All You Need” 中提出,最初用于机器翻译任务。
核心思想: 完全摒弃了传统的循环(Recurrence, 如RNN)和卷积(Convolution, 如CNN)结构,仅依赖自注意力机制 (Self-Attention Mechanism) 来捕捉输入序列内部的依赖关系以及输入输出序列之间的对齐关系。
主要组成部分:
编码器 (Encoder): 由N个相同的层堆叠而成。每层包含两个主要的子层:
解码器 (Decoder): 也由N个相同的层堆叠而成。每层除了编码器层中的两个子层外,还插入了第三个子层:
输入嵌入 (Input Embedding):
自注意力机制 (Self-Attention) 详解:
自注意力机制是Transformer的核心。对于输入序列中的每个词,它会计算三个向量:
优点:
1.4.2 BERT (Bidirectional Encoder Representations from Transformers)
提出者与时间: 由Google AI Language团队的Jacob Devlin等人在2018年底提出。
核心思想: BERT利用Transformer的编码器部分,通过设计巧妙的预训练任务,在大规模无标签文本语料上进行训练,从而学习到深度的双向语言表示。这些预训练好的模型可以作为“基础模型”,通过在其上添加少量特定任务层并进行微调(fine-tuning),就能在广泛的NLP任务上取得SOTA(State-of-the-Art)或接近SOTA的性能。
架构: BERT本质上是一个多层双向Transformer编码器堆栈。常见的有 BERT-Base
(12层, 768维隐藏单元, 12个注意力头, 约1.1亿参数) 和 BERT-Large
(24层, 1024维隐藏单元, 16个注意力头, 约3.4亿参数)。
输入表示: BERT的输入是一个或两个句子序列,每个序列的开头会添加一个特殊的 [CLS]
标记,句子之间用 [SEP]
标记分隔。输入给模型的是三种嵌入的和:
预训练任务 (Pre-training Tasks): BERT的强大能力主要归功于其两个创新的预训练任务:
MLM (Masked Language Model, 掩码语言模型):
[MASK]
标记替换。[MASK]
,也学习对真实词的表示)。NSP (Next Sentence Prediction, 下一句预测):
[CLS]
标记的最终隐藏状态能够聚合整个输入序列对的表示,用于句子级别的预测。如何用于获取句子表示 (How BERT is used for sentence representation for downstream tasks):
[CLS]
标记的输出: 对于句子(对)分类任务(如情感分析、NLI),通常取 [CLS]
标记对应的最后一层Transformer的隐藏状态输出,然后在其上接一个简单的分类器(如Softmax层)进行微调。NSP任务的设计使得 [CLS]
的表示能够概括整个输入序列的信息。BERT 的影响: BERT的出现极大地推动了NLP领域的发展,开启了大规模预训练语言模型的新时代。它证明了通过在大规模数据上进行无监督预训练,然后针对特定任务进行微调的范式,可以显著提升模型性能并减少对大量标注数据的依赖。
1.4.3 其他重要的预训练模型 (Other important PLMs)
BERT之后,涌现了大量基于Transformer的优秀预训练语言模型,它们在BERT的基础上进行了各种改进和创新:
RoBERTa (Robustly Optimized BERT Pretraining Approach):
XLNet:
[MASK]
标记,避免了预训练和微调之间的不一致性。ALBERT (A Lite BERT for Self-supervised Learning of Language Representations):
ELECTRA (Efficiently Learning an Encoder that Classifies Token Replacements Accurately):
DistilBERT:
T5 (Text-to-Text Transfer Transformer):
这些模型以及更多其他的PLMs,都在不断推动自然语言理解能力的边界。sentence-transformers
框架正是构建在这些强大的预训练模型之上,通过特定的微调策略,使其更擅长生成高质量的句子嵌入。
1.5 sentence-transformers
框架的诞生与核心价值
尽管像BERT这样的预训练语言模型在许多NLP任务中表现出色,但直接使用它们(例如,通过平均其词嵌入或使用 [CLS]
输出)来获取句子嵌入以进行语义相似度比较或无监督任务(如聚类、信息检索)时,效果往往不尽如人意,有时甚至不如简单的GloVe词嵌入平均。
1.5.1 为什么需要 sentence-transformers
?
BERT/RoBERTa的原始输出不适合直接用于语义相似性:
sentence-transformers
框架的奠基性工作)中指出,未经微调的BERT输出的句子嵌入在向量空间中的分布并不理想。相似的句子在嵌入空间中可能并不靠近,导致余弦相似度等度量无法有效衡量语义相似性。[CLS]
输出主要针对分类任务进行优化,而平均池化策略也可能丢失过多信息或引入噪声。计算效率问题:
sentence-transformers
框架的出现,正是为了解决上述问题,使得基于Transformer的模型能够高效地生成用于语义相似度任务的高质量句子嵌入。
1.5.2 核心思想:孪生网络 (Siamese Networks) 和三元组网络 (Triplet Networks) 微调
sentence-transformers
的核心在于它采用了特定的网络结构和损失函数对预训练的Transformer模型(如BERT, RoBERTa)进行微调,使其输出的句子嵌入更适合语义相似性度量。
孪生网络 (Siamese Network) 结构:
(sentence_A, sentence_B)
,它们分别通过同一个BERT网络(权重共享)得到各自的句子嵌入 u
和 v
。u
和 v
被输入到一个特定的目标函数中进行优化。MEAN-Pooling
,有时也用 MAX-Pooling
或 CLS-Pooling
,但MEAN-Pooling
在SBERT论文中表现较好)来得到固定长度的句子嵌入。微调的目标函数 (Objective Functions):
根据下游任务的不同,可以选择不同的目标函数进行微调:
u
和 v
,以及它们的差的绝对值 |u-v|
拼接起来,然后通过一个Softmax分类器来预测它们之间的关系(如蕴含、矛盾、中立)。u
和 v
之间的余弦相似度,然后使用均方误差(MSE)损失函数来最小化预测相似度与真实人工标注相似度(0-1或0-5之间的分数)之间的差异。a
与一个正例句子 p
(相似) 的距离小于它与一个反例句子 n
(不相似) 的距离,并且两者之间至少有一个边界 epsilon
。通过在这些特定任务和目标函数上进行微调,sentence-transformers
使得基础的Transformer模型学会生成语义上更有意义、在向量空间中分布更合理的句子嵌入。
1.5.3 sentence-transformers
的优势
高质量的句子嵌入 (High-Quality Sentence Embeddings):
易用性 (Ease of Use):
丰富的预训练模型 (Variety of Pre-trained Models):
sentence-transformers
提供了大量针对不同语言(英语、中文、德语、多语言等)、不同任务(语义搜索、释义挖掘、NLI等)和不同性能/速度权衡(如基于BERT-large的高性能模型,基于DistilBERT的轻量级模型)的预训练句子嵌入模型。计算效率高 (Computationally Efficient for Similarity Comparison):
灵活性与可扩展性 (Flexibility and Extensibility):
sentence-transformers
集成到更复杂的NLP应用流程中。支持多种任务:
第二章:快速入门 sentence-transformers
2.1 安装 sentence-transformers
sentence-transformers
框架依赖于 PyTorch 和 Hugging Face Transformers 库。因此,在安装 sentence-transformers
之前,建议先根据您的环境(CPU 或 GPU,CUDA 版本)安装 PyTorch。
2.1.1 安装 PyTorch
您可以访问 PyTorch 官方网站 (https://pytorch.org/get-started/locally/) 获取适合您系统的安装命令。
例如,如果您的系统支持 CUDA 11.8,可以使用 pip 安装 PyTorch 的命令可能如下(请务必访问官网确认最新命令):
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
如果只需要 CPU 版本,命令通常更简单:
pip install torch torchvision torchaudio
2.1.2 安装 sentence-transformers
一旦 PyTorch 安装完成,就可以通过 pip 直接安装 sentence-transformers
:
pip install -U sentence-transformers
sentence-transformers
库的名称。这条命令会自动处理并安装其依赖项,包括 Hugging Face Transformers 等。
2.1.3 验证安装
安装完成后,您可以打开 Python 解释器并尝试导入该库来验证安装是否成功:
try:
from sentence_transformers import SentenceTransformer # 尝试从 sentence_transformers 包中导入 SentenceTransformer 类
print("SentenceTransformers SUCESSFULLY imported!") # 如果导入成功,打印成功消息
except ImportError:
print("SentenceTransformers FAILED to import. Please check your installation.") # 如果导入失败,打印失败消息
print("Ensure you have PyTorch installed correctly first (see https://pytorch.org/).") # 提示用户检查PyTorch安装
print("Then, install sentence-transformers using: pip install -U sentence-transformers") # 提示用户安装sentence-transformers的命令
如果看到 “SentenceTransformers SUCESSFULLY imported!” 的输出,说明安装成功。
2.2 sentence-transformers
的核心组件:SentenceTransformer
类
sentence-transformers
框架的核心是 SentenceTransformer
类。这个类负责加载预训练的句子嵌入模型,并将输入的文本句子转换为高质量的数值向量(嵌入)。
2.2.1 加载预训练模型
SentenceTransformer
类的构造函数接收一个模型名称或模型路径作为参数。该框架集成了大量在不同数据集和任务上预训练好的模型,这些模型托管在 Hugging Face Model Hub 上。
您可以在 sentence-transformers
的官方文档或 Hugging Face Model Hub (https://huggingface.co/models?library=sentence-transformers) 上找到可用的预训练模型列表及其特性。
一些常用的预训练模型包括:
'all-MiniLM-L6-v2'
: 一个非常流行且高效的模型,性能良好,速度快,嵌入维度为384。非常适合作为通用的起点。'all-mpnet-base-v2'
: 目前(截至我知识更新时)在语义文本相似度任务上表现最佳的通用模型之一,嵌入维度为768。性能优于 all-MiniLM-L6-v2
,但速度稍慢。'paraphrase-multilingual-MiniLM-L12-v2'
: 支持多种语言(超过50种)的释义识别模型。'clip-ViT-B-32'
: 一个多模态模型,可以为图像和文本生成嵌入,使得可以计算图文相似度。'shibing624/text2vec-base-chinese'
(由shibing624贡献)'DMetaSoul/sbert-chinese-general-v2'
(由DMetaSoul贡献)代码示例:加载一个预训练模型
from sentence_transformers import SentenceTransformer # 从 sentence_transformers 包中导入 SentenceTransformer 类
# 模型名称可以从 Hugging Face Model Hub 或 sentence-transformers 文档中找到
# 第一次加载模型时,它会自动从网上下载并缓存到本地
# 默认缓存路径通常是 ~/.cache/torch/sentence_transformers/
# 或者 ~/.cache/huggingface/hub/ 对于一些新模型
# 加载一个通用的英文模型 'all-MiniLM-L6-v2'
model_name_en = 'all-MiniLM-L6-v2' # 定义英文模型的名称
try:
model_en = SentenceTransformer(model_name_en) # 初始化 SentenceTransformer 类,传入模型名称以加载模型
print(f"成功加载英文模型: {
model_name_en}") # 打印成功加载的消息
except Exception as e:
print(f"加载英文模型 {
model_name_en} 失败: {
e}") # 如果加载失败,打印错误信息
# 加载一个常用的中文模型 'shibing624/text2vec-base-chinese'
# 请确保你的环境中可以访问 Hugging Face Hub,或者模型已经被下载到缓存中
model_name_zh = 'shibing624/text2vec-base-chinese' # 定义中文模型的名称
try:
model_zh = SentenceTransformer(model_name_zh) # 初始化 SentenceTransformer 类,传入模型名称以加载模型
print(f"成功加载中文模型: {
model_name_zh}") # 打印成功加载的消息
except Exception as e:
print(f"加载中文模型 {
model_name_zh} 失败: {
e}") # 如果加载失败,打印错误信息
print("请确保您可以访问Hugging Face Hub,或者该模型已下载。") # 提示可能的原因
print("对于一些社区模型,可能需要transformers库版本较新。尝试 `pip install -U transformers`") # 提示更新transformers库
# 你也可以指定设备(例如,如果你有GPU并想使用它)
# model = SentenceTransformer('all-MiniLM-L6-v2', device='cuda') # 'cuda' 表示使用第一个可用的GPU
# model = SentenceTransformer('all-MiniLM-L6-v2', device='cpu') # 明确指定使用CPU
# 检查模型使用的设备
if 'model_en' in locals() and model_en is not None: # 检查 model_en 是否已成功加载
print(f"英文模型 '{
model_name_en}' 运行在设备: {
model_en.device}") # 打印模型运行的设备
if 'model_zh' in locals() and model_zh is not None: # 检查 model_zh 是否已成功加载
print(f"中文模型 '{
model_name_zh}' 运行在设备: {
model_zh.device}") # 打印模型运行的设备
代码解释:
from sentence_transformers import SentenceTransformer
: 导入核心类。model_name_en = 'all-MiniLM-L6-v2'
: 这是一个字符串,指定了要加载的预训练模型的名称。这个名称对应于 Hugging Face Model Hub 上的一个模型标识符。model_en = SentenceTransformer(model_name_en)
: 创建 SentenceTransformer
类的实例。在实例化时,框架会自动处理以下事务:
sentence-transformers
的模型通常在 Transformer 层之上还有一个池化层(如平均池化),用于将词级别输出转换为句子级别嵌入。这个池化配置也会被加载。~/.cache/torch/sentence_transformers/
或 ~/.cache/huggingface/hub/
(对于通过 transformers
库加载的模型)。这意味着后续再次加载同一个模型时,会直接从本地缓存读取,速度会快很多。device
参数: SentenceTransformer
的构造函数可以接受一个 device
参数,用于指定模型应该加载到哪个设备上运行(例如 'cpu'
, 'cuda'
, 'cuda:0'
)。如果未指定,它通常会自动检测可用的GPU,否则使用CPU。try-except
块来捕获模型加载过程中可能发生的错误(例如网络问题、模型不存在、依赖库版本不兼容等)。2.2.2 编码句子:获取句子嵌入
一旦 SentenceTransformer
对象被成功加载,就可以使用其 encode()
方法将单个句子或句子列表转换为它们的嵌入向量。
encode()
方法的关键参数:
sentences
(str or List[str]): 需要编码的单个句子(字符串)或句子列表(字符串列表)。batch_size
(int, optional, defaults to 32): 如果输入是句子列表,编码过程会分批进行。此参数控制每批处理的句子数量。对于GPU,选择一个合适的批次大小可以充分利用其并行计算能力,提高效率。show_progress_bar
(bool, optional, defaults to None): 是否显示编码过程的进度条。如果为 None
且输入的句子数量较多(例如超过10个),则会自动显示。output_value
(str, optional, defaults to 'sentence_embedding'
): 指定输出值的类型。
'sentence_embedding'
: 返回句子嵌入(默认行为)。'token_embeddings'
: 返回每个词(token)的上下文嵌入。convert_to_numpy
(bool, optional, defaults to True): 如果为 True
,返回的嵌入是 NumPy 数组。如果为 False
,返回的是 PyTorch 张量 (torch.Tensor
)。convert_to_tensor
(bool, optional, defaults to False): 如果为 True
,返回 PyTorch 张量。如果 convert_to_numpy
也是 True
,则此参数被忽略。device
(str, optional, defaults to None): 指定用于编码的设备(例如 'cuda'
, 'cpu'
)。如果模型已加载到特定设备,则通常不需要再次指定,除非想临时切换。normalize_embeddings
(bool, optional, defaults to False): 如果为 True
,返回的句子嵌入会被归一化(L2范数归一化),使得它们的长度为1。这对于使用余弦相似度进行比较非常有用,因为归一化后的向量点积等价于余弦相似度。许多预训练的 sentence-transformers
模型在训练时就期望嵌入被归一化后使用。代码示例:编码单个句子和句子列表
from sentence_transformers import SentenceTransformer # 导入 SentenceTransformer 类
import numpy as np # 导入 numpy 用于后续操作
# 确保模型已加载 (我们使用之前加载的英文模型 'all-MiniLM-L6-v2' 为例)
# 如果没有,请先运行2.2.1中的加载代码
model_name_en = 'all-MiniLM-L6-v2' # 定义模型名称
try:
model = SentenceTransformer(model_name_en) # 加载模型
print(f"模型 {
model_name_en} 加载成功。") # 打印成功消息
except Exception as e:
print(f"加载模型 {
model_name_en} 失败: {
e}") # 打印失败消息
model = None # 将模型设为None,以便后续检查
if model: # 只有当模型成功加载后才执行编码操作
# 1. 编码单个句子
sentence_single = "This is an example sentence." # 定义一个英文句子
embedding_single = model.encode(sentence_single) # 使用模型的encode方法获取句子的嵌入向量
# 默认返回numpy数组
print(f"\n编码单个句子: '{
sentence_single}'") # 打印提示信息
print(f"嵌入向量 (前5个维度): {
embedding_single[:5]}") # 打印嵌入向量的前5个维度
print(f"嵌入向量的形状: {
embedding_single.shape}") # 打印嵌入向量的形状 (对于 'all-MiniLM-L6-v2' 应该是 (384,))
print(f"嵌入向量的数据类型: {
type(embedding_single)}") # 打印嵌入向量的数据类型 (应该是 numpy.ndarray)
# 2. 编码句子列表
sentences_list = [
"The weather is sunny today.",
"I enjoy reading books about science fiction.",
"Sentence embeddings are useful for semantic search."
] # 定义一个英文句子列表
embeddings_list = model.encode(sentences_list) # 使用模型的encode方法获取句子列表的嵌入向量
# 返回一个numpy数组的列表,或者一个二维numpy数组
print(f"\n编码句子列表:") # 打印提示信息
for i, sentence in enumerate(sentences_list): # 遍历句子列表和它们的索引
print(f"句子 {
i+1}: '{
sentence}'") # 打印当前句子
print(f" 嵌入向量 (前5个维度): {
embeddings_list[i, :5]}") # 打印对应嵌入向量的前5个维度
print(f"所有嵌入向量的形状: {
embeddings_list.shape}") # 打印所有嵌入向量的形状 (应该是 (3, 384) 对于3个句子和384维嵌入)
print(f"所有嵌入向量的数据类型: {
type(embeddings_list)}") # 打印嵌入向量的数据类型 (应该是 numpy.ndarray)
# 3. 编码时返回 PyTorch 张量
embedding_tensor = model.encode(sentence_single, convert_to_tensor=True) # 编码单个句子,并指定返回 PyTorch 张量
print(f"\n编码单个句子 (返回 PyTorch 张量): '{
sentence_single}'") # 打印提示信息
print(f"嵌入张量 (前5个维度): {
embedding_tensor[:5]}") # 打印嵌入张量的前5个维度
print(f"嵌入张量的形状: {
embedding_tensor.shape}") # 打印嵌入张量的形状
print(f"嵌入张量的数据类型: {
type(embedding_tensor)}") # 打印嵌入张量的数据类型 (应该是 torch.Tensor)
print(f"嵌入张量所在的设备: {
embedding_tensor.device}") # 打印嵌入张量所在的设备
# 4. 编码时对嵌入进行归一化
# 许多sentence-transformer模型在计算余弦相似度之前期望嵌入是归一化的
# 'all-MiniLM-L6-v2' 在其文档中通常建议使用归一化嵌入
embedding_normalized = model.encode(sentence_single, normalize_embeddings=True) # 编码单个句子,并进行归一化
print(f"\n编码单个句子 (归一化嵌入): '{
sentence_single}'") # 打印提示信息
print(f"归一化嵌入 (前5个维度): {
embedding_normalized[:5]}") # 打印归一化嵌入的前5个维度
# 验证归一化 (L2 范数应该接近 1)
l2_norm = np.linalg.norm(embedding_normalized) # 使用numpy计算L2范数
print(f"归一化嵌入的L2范数: {
l2_norm}") # 打印L2范数,它应该非常接近1
# 5. 编码中文句子 (需要加载中文模型)
model_name_zh = 'shibing624/text2vec-base-chinese' # 定义中文模型名称
try:
model_zh = SentenceTransformer(model_name_zh) # 加载中文模型
print(f"\n中文模型 {
model_name_zh} 加载成功。") # 打印成功消息
chinese_sentences = [
"今天天气怎么样?",
"我喜欢看科幻小说。",
"这是一个中文句子的例子。"
] # 定义一个中文句子列表
embeddings_zh = model_zh.encode(chinese_sentences, show_progress_bar=True) # 使用中文模型编码句子列表,并显示进度条
print(f"\n编码中文句子列表:") # 打印提示信息
for i, sentence in enumerate(chinese_sentences): # 遍历中文句子列表
print(f"句子 {
i+1}: '{
sentence}'") # 打印当前句子
print(f" 嵌入向量 (前5个维度): {
embeddings_zh[i, :5]}") # 打印嵌入向量的前5个维度
print(f"所有中文嵌入向量的形状: {
embeddings_zh.shape}") # 打印嵌入向量的形状 ('shibing624/text2vec-base-chinese' 通常是768维)
except Exception as e:
print(f"处理中文模型 {
model_name_zh} 时发生错误: {
e}") # 打印错误信息
else:
print("模型未能成功加载,跳过编码示例。") # 如果模型加载失败,打印此消息
代码解释:
model.encode(sentence_single)
: 将单个字符串输入 encode
方法,返回该句子的嵌入向量。model.encode(sentences_list)
: 将字符串列表输入 encode
方法,返回一个包含所有句子嵌入的二维数组(或张量)。每一行对应一个句子的嵌入。embedding_single.shape
: 对于 all-MiniLM-L6-v2
模型,单个句子的嵌入维度是384,所以形状是 (384,)
。对于 shibing624/text2vec-base-chinese
,维度通常是768。embeddings_list.shape
: 如果有 N
个句子,嵌入维度是 D
,则形状是 (N, D)
。convert_to_tensor=True
: 使 encode
方法返回 torch.Tensor
而不是 numpy.ndarray
。这在需要将嵌入直接用于 PyTorch 后续计算(如自定义损失函数、模型层)时很有用。normalize_embeddings=True
: 在返回嵌入之前对其进行L2归一化。归一化后的向量长度为1。
np.linalg.norm(embedding_normalized)
用于验证归一化效果。对于精确归一化的向量,其L2范数应为1.0。由于浮点数精度问题,实际值可能非常接近1,例如0.999999…。show_progress_bar=True
: 当处理大量句子时,显示一个进度条,方便用户了解编码进度。'paraphrase-multilingual-MiniLM-L12-v2'
)或针对每种语言使用特定的模型。2.2.3 获取词元嵌入 (Token Embeddings)
除了句子级别的嵌入,encode()
方法还可以返回句子中每个词元(token)的上下文嵌入。这在需要更细粒度分析或将句子嵌入与其他词级别信息结合时可能有用。
from sentence_transformers import SentenceTransformer # 导入 SentenceTransformer 类
# 假设 model 已经加载 (例如 model = SentenceTransformer('all-MiniLM-L6-v2'))
model_name_en = 'all-MiniLM-L6-v2' # 定义模型名称
try:
model = SentenceTransformer(model_name_en) # 加载模型
print(f"模型 {
model_name_en} 加载成功。") # 打印成功信息
except Exception as e:
print(f"加载模型 {
model_name_en} 失败: {
e}") # 打印失败信息
model = None # 将模型设为None
if model: # 只有当模型成功加载后才执行
sentence = "This is a short sentence." # 定义一个示例句子
# 获取词元嵌入,不进行归一化,返回numpy数组
token_embeddings_numpy = model.encode(sentence, output_value='token_embeddings', convert_to_numpy=True, normalize_embeddings=False) # 编码句子,指定输出词元嵌入
print(f"\n句子: '{
sentence}'") # 打印原始句子
print(f"词元嵌入的形状 (numpy): {
token_embeddings_numpy.shape}") # 打印词元嵌入的形状
# 形状通常是 (sequence_length, embedding_dimension)
# sequence_length 包括特殊标记,如 [CLS] 和 [SEP] (对于BERT类模型底座)
# 或者取决于模型的分词器如何处理
# 为了更好地理解,我们可以查看模型的分词器如何处理这个句子
tokens = model.tokenizer.tokenize(sentence) # 使用模型内部的分词器对句子进行分词
print(f"模型分词结果: {
tokens}") # 打印分词结果
# 注意: sentence_transformers 模型通常在其架构中包含了底层的Hugging Face Transformers模型。
# 我们可以访问其第一个模块(通常是底层的Transformer模型)的tokenizer
# 或者直接访问 model.tokenizer (如果 sentence_transformers 的版本提供了直接访问)
# 如果模型是 SentenceTransformer('bert-base-uncased') 这种直接指定transformers模型的情况,
# 则 model[0].tokenizer 是获取分词器的方式。
# 对于 'all-MiniLM-L6-v2' 这类已封装好的SBERT模型,其内部结构可能稍有不同,
# 但通常 model.tokenizer 应该是可用的,或者 model._first_module().tokenizer
# 打印每个词元及其嵌入的前几个维度 (示例)
# 这里的tokens数量可能与token_embeddings_numpy.shape[0]不完全对应,
# 因为sentence_transformers的encode输出的token_embeddings可能已经处理过特殊标记
# 或者直接对应于输入给pooling层的序列。
# 更准确的做法是依赖于token_embeddings_numpy.shape[0]作为序列长度。
# sentence_transformers的'token_embeddings'通常返回的是作用于pooling层之前的序列表示
# 其长度与输入给pooling层的序列长度一致。
# 示例:假设我们想知道原始词如何对应到这些嵌入
# 这是一个更复杂的问题,涉及到分词对齐。
# 对于简单展示,我们只看嵌入的维度。
print("词元嵌入 (每个词元一行,显示前3维):") # 打印提示信息
for i in range(token_embeddings_numpy.shape[0]): # 遍历每个词元的嵌入
# 我们无法直接将这里的索引i映射回原始单词,因为分词和特殊标记的存在
# 这里的token_embeddings_numpy的行数是经过模型内部处理后的序列长度
print(f" Token {
i}: {
token_embeddings_numpy[i, :3]}") # 打印第i个词元嵌入的前3个维度
# 获取词元嵌入,进行归一化,返回torch.Tensor
token_embeddings_tensor_normalized = model.encode(
sentence,
output_value='token_embeddings', # 指定输出类型为词元嵌入
convert_to_tensor=True, # 指定返回 PyTorch 张量
normalize_embeddings=True # 指定对嵌入进行归一化
) # 编码句子,获取归一化的词元嵌入张量
print(f"\n词元嵌入的形状 (torch.Tensor, 归一化): {
token_embeddings_tensor_normalized.shape}") # 打印词元嵌入张量的形状
l2_norm_token0 = torch.linalg.norm(token_embeddings_tensor_normalized[0]) # 计算第一个词元嵌入的L2范数
print(f"第一个词元嵌入的L2范数 (归一化后): {
l2_norm_token0.item()}") # 打印其L2范数,应接近1
else:
print("模型未能成功加载,跳过词元嵌入示例。") # 如果模型加载失败,打印此消息
代码解释:
output_value='token_embeddings'
: 这个参数告诉 encode
方法返回每个输入词元(经过模型分词器处理后)的上下文嵌入,而不是聚合后的句子嵌入。token_embeddings_numpy.shape
: 返回的词元嵌入是一个二维数组,形状为 (sequence_length, embedding_dimension)
。
sequence_length
: 指的是模型内部处理的序列长度,这通常包括由分词器产生的子词(subwords)以及模型可能添加的特殊标记(如BERT的[CLS]
和[SEP]
,尽管sentence-transformers
的池化层可能会在这些特殊标记上操作或忽略它们)。embedding_dimension
: 与句子嵌入的维度相同。model.tokenizer.tokenize(sentence)
: 可以用来查看模型是如何对输入句子进行分词的。注意,encode
方法内部会进行分词,这里的 tokenize
只是为了演示。实际的 sequence_length
取决于 encode
方法内部的完整处理流程,包括添加特殊标记。normalize_embeddings=True
同样可以应用于词元嵌入,对每个词元的嵌入向量进行L2归一化。2.3 计算句子相似度
获取句子嵌入后,最常见的应用之一就是计算它们之间的语义相似度。余弦相似度 (Cosine Similarity) 是衡量向量空间中两个向量方向相似性的常用指标,特别适用于 sentence-transformers
生成的归一化嵌入。
2.3.1 余弦相似度 (Cosine Similarity)
对于两个向量 (A) 和 (B),它们的余弦相似度计算公式为:
[ \text{CosineSimilarity}(A, B) = \frac{A \cdot B}{||A|| \cdot ||B||} = \frac{\sum_{i=1}^{n} A_i B_i}{\sqrt{\sum_{i=1}^{n} A_i^2} \cdot \sqrt{\sum_{i=1}^{n} B_i^2}} ]
其中:
余弦相似度的取值范围在 -1 到 1 之间:
如果向量 (A) 和 (B) 已经被L2归一化 (即 (||A|| = 1) 且 (||B|| = 1)),那么余弦相似度就简化为它们的点积:
[ \text{CosineSimilarity}(A, B) = A \cdot B \quad (\text{if } ||A||=1 \text{ and } ||B||=1) ]
这就是为什么在使用 sentence-transformers
时,通常建议对嵌入进行归一化(例如,通过 model.encode(..., normalize_embeddings=True)
或手动归一化),然后直接计算点积来获得余弦相似度,这样计算更高效。
2.3.2 使用 sentence_transformers.util.cos_sim
sentence-transformers
库提供了一个方便的工具函数 util.cos_sim(a, b)
来计算两个嵌入或两组嵌入之间的余弦相似度。
a
和 b
可以是单个嵌入向量(一维数组/张量)或嵌入矩阵(二维数组/张量,每行一个嵌入)。代码示例:计算余弦相似度
from sentence_transformers import SentenceTransformer, util # 导入 SentenceTransformer 和 util 模块
import torch # 导入 torch 用于创建张量示例
# 假设模型已加载 (例如 model = SentenceTransformer('all-MiniLM-L6-v2'))
model_name_en = 'all-MiniLM-L6-v2' # 定义模型名称
try:
model = SentenceTransformer(model_name_en) # 加载模型
print(f"模型 {
model_name_en} 加载成功。") # 打印成功消息
except Exception as e:
print(f"加载模型 {
model_name_en} 失败: {
e}") # 打印失败消息
model = None # 将模型设为None
if model: # 只有当模型成功加载后才执行
sentences1 = [
"The cat sits on the mat.",
"A dog plays in the garden."
] # 定义第一组句子
sentences2 = [
"A feline is resting on a rug.", # 与 sentences1[0] 语义相似
"The weather is nice today.", # 与 sentences1[0] 和 sentences1[1] 语义不相似
"A canine is having fun outdoors." # 与 sentences1[1] 语义相似
] # 定义第二组句子
# 编码所有句子,建议进行归一化以获得准确的余弦相似度(虽然util.cos_sim可以处理未归一化的)
# 许多 sentence-transformers 模型期望嵌入被归一化
embeddings1 = model.encode(sentences1, convert_to_tensor=True, normalize_embeddings=True) # 编码第一组句子,返回归一化张量
embeddings2 = model.encode(sentences2, convert_to_tensor=True, normalize_embeddings=True) # 编码第二组句子,返回归一化张量
print(f"\nEmbeddings1 shape: {
embeddings1.shape}") # 打印第一组嵌入的形状
print(f"Embeddings2 shape: {
embeddings2.shape}") # 打印第二组嵌入的形状
# 1. 计算两组句子嵌入之间的所有对的余弦相似度
# util.cos_sim(embeddings1, embeddings2) 会返回一个 (len(sentences1) x len(sentences2)) 的矩阵
cosine_scores_matrix = util.cos_sim(embeddings1, embeddings2) # 使用 util.cos_sim 计算余弦相似度矩阵
print("\n所有句子对之间的余弦相似度矩阵:") # 打印提示信息
print(cosine_scores_matrix) # 打印相似度矩阵
# cosine_scores_matrix[i][j] 是 sentences1[i] 和 sentences2[j] 之间的相似度
# 打印更易读的结果
for i in range(len(sentences1)): # 遍历第一组句子的索引
for j in range(len(sentences2)): # 遍历第二组句子的索引
print(f"相似度 ['{
sentences1[i]}' vs '{
sentences2[j]}']: {
cosine_scores_matrix[i][j]:.4f}") # 打印特定句子对的相似度,保留4位小数
# 2. 计算单个句子与一组句子的相似度
query_sentence = "A pet is on a carpet." # 定义一个查询句子
query_embedding = model.encode(query_sentence, convert_to_tensor=True, normalize_embeddings=True) # 编码查询句子
# query_embedding 的形状是 (1, embedding_dim) 或者 (embedding_dim)
# 如果是 (embedding_dim),util.cos_sim 会自动处理
# 如果想确保是 (1, embedding_dim) 可以用 query_embedding.unsqueeze(0)
# 计算 query_embedding 与 embeddings2 (一组嵌入) 的相似度
# util.cos_sim(query_embedding, embeddings2) 会返回一个 (1 x len(sentences2)) 的矩阵
cosine_scores_query_vs_set = util.cos_sim(query_embedding, embeddings2) # 计算查询嵌入与第二组嵌入的相似度
print(f"\n查询句子 '{
query_sentence}' 与第二组句子的相似度:") # 打印提示信息
for j in range(len(sentences2)): # 遍历第二组句子的索引
print(f" vs '{
sentences2[j]}': {
cosine_scores_query_vs_set[0][j]:.4f}") # 打印查询句子与当前句子的相似度
# 3. 如果手动处理NumPy数组并且嵌入已归一化,可以直接用点积
embeddings1_np = embeddings1.cpu().numpy() # 将PyTorch张量转换为NumPy数组 (如果之前是GPU张量,需要先移到CPU)
embeddings2_np = embeddings2.cpu().numpy() # 将PyTorch张量转换为NumPy数组
# 假设 embeddings1_np 和 embeddings2_np 已经是L2归一化的
# 计算第一个句子 (embeddings1_np[0]) 与 sentences2 中所有句子的相似度
manual_scores = np.dot(embeddings1_np[0], embeddings2_np.T) # 使用NumPy的点积计算相似度
# embeddings2_np.T 是转置,使得可以进行矩阵乘法得到所有对的相似度
print(f"\n手动计算 (点积) '{
sentences1[0]}' 与第二组句子的相似度 (假设已归一化):") # 打印提示信息
for j in range(len(sentences2)): # 遍历第二组句子的索引
print(f" vs '{
sentences2[j]}': {
manual_scores[j]:.4f}") # 打印相似度分数
# 验证 util.cos_sim 是否与手动点积(对于归一化向量)结果一致
print(f" (util.cos_sim 第0行: {
cosine_scores_matrix[0].cpu().numpy()})") # 打印使用util.cos_sim得到的结果,用于比较
else:
print("模型未能成功加载,跳过相似度计算示例。") # 如果模型加载失败,打印此消息
代码解释:
from sentence_transformers import util
: 导入包含 cos_sim
和其他实用函数的 util
模块。model.encode(..., convert_to_tensor=True, normalize_embeddings=True)
: 推荐在编码时就进行归一化并获取张量,这样后续使用 util.cos_sim
或直接点积更方便。cosine_scores_matrix = util.cos_sim(embeddings1, embeddings2)
:
embeddings1
是形状为 (N, D)
的矩阵,embeddings2
是形状为 (M, D)
的矩阵,则 cosine_scores_matrix
是一个形状为 (N, M)
的 PyTorch 张量。cosine_scores_matrix[i, j]
存储的是 embeddings1
中第 i
个句子与 embeddings2
中第 j
个句子之间的余弦相似度。np.dot(embeddings1_np[0], embeddings2_np.T)
embeddings1_np[0]
是一个形状为 (D,)
的向量。embeddings2_np.T
是形状为 (M, D)
的矩阵 embeddings2_np
的转置,变为 (D, M)
。(M,)
的向量,包含了 sentences1[0]
与 sentences2
中每个句子的相似度。2.3.3 其他相似度/距离度量
虽然余弦相似度是最常用的,但根据具体应用和嵌入的特性,有时也会考虑其他度量:
欧几里得距离 (Euclidean Distance):
[ D(A, B) = \sqrt{\sum_{i=1}^{n} (A_i - B_i)^2} ]
衡量向量空间中两个点的直线距离。距离越小,表示越相似。
与余弦相似度不同,欧几里得距离受向量长度的影响。对于L2归一化的向量,欧几里得距离 (D) 和余弦相似度 (S) 之间存在关系:(D^2 = 2(1 - S))。
曼哈顿距离 (Manhattan Distance / L1 Distance):
[ D(A, B) = \sum_{i=1}^{n} |A_i - B_i| ]
衡量向量在各个维度上差的绝对值之和。
点积 (Dot Product):
如前所述,对于归一化向量,点积等价于余弦相似度。对于未归一化的向量,点积会同时考虑向量的方向和长度。如果向量长度差异很大,点积可能会被长度主导。
sentence_transformers.util
模块也提供了计算这些距离的函数,例如:
util.pytorch_cos_sim
(与 util.cos_sim
类似,但明确是PyTorch实现)util.dot_score
(计算点积)torch.cdist
或 scipy.spatial.distance.cdist
。代码示例:使用其他距离度量 (以欧几里得距离为例)
from sentence_transformers import SentenceTransformer, util # 导入所需模块
import torch # 导入torch
import numpy as np # 导入numpy
# 假设 model 和 embeddings1, embeddings2 已按之前的方式准备好
# embeddings1 = model.encode(sentences1, convert_to_tensor=True, normalize_embeddings=True)
# embeddings2 = model.encode(sentences2, convert_to_tensor=True, normalize_embeddings=True)
model_name_en = 'all-MiniLM-L6-v2' # 定义模型名称
try:
model = SentenceTransformer(model_name_en) # 加载模型
print(f"模型 {
model_name_en} 加载成功。") # 打印成功消息
sentences1 = ["The cat sits on the mat."] # 定义第一组句子 (单个句子)
sentences2 = [ # 定义第二组句子
"A feline is resting on a rug.",
"The weather is nice today."
]
# 编码并确保是PyTorch张量
embeddings1 = model.encode(sentences1, convert_to_tensor=True) # 编码句子,返回张量
embeddings2 = model.encode(sentences2, convert_to_tensor=True) # 编码句子,返回张量
# 计算欧几里得距离 (使用 PyTorch)
# torch.cdist 计算两组点之间的成对距离
# p=2.0 表示 L2 范数 (欧几里得距离)
euclidean_distances = torch.cdist(embeddings1, embeddings2, p=2.0) # 计算欧几里得距离矩阵
print("\n欧几里得距离矩阵:") # 打印提示信息
print(euclidean_distances) # 打印距离矩阵
# euclidean_distances[i][j] 是 embeddings1[i] 和 embeddings2[j] 之间的欧几里得距离
# 距离越小,越相似
for i in range(embeddings1.shape[0]): # 遍历第一组嵌入
for j in range(embeddings2.shape[0]): # 遍历第二组嵌入
print(f"欧氏距离 ['{
sentences1[i]}' vs '{
sentences2[j]}']: {
euclidean_distances[i][j]:.4f}") # 打印距离
# 如果嵌入已经归一化,欧氏距离和余弦相似度有关系
# D^2 = 2 * (1 - cos_sim) => D = sqrt(2 * (1 - cos_sim))
# cos_sim = 1 - (D^2 / 2)
embeddings1_norm = model.encode(sentences1, convert_to_tensor=True, normalize_embeddings=True) # 编码并归一化
embeddings2_norm = model.encode(sentences2, convert_to_tensor=True, normalize_embeddings=True) # 编码并归一化
cos_sim_scores = util.cos_sim(embeddings1_norm, embeddings2_norm) # 计算归一化嵌入的余弦相似度
euclidean_dist_norm = torch.cdist(embeddings1_norm, embeddings2_norm, p=2.0) # 计算归一化嵌入的欧氏距离
print("\n对于归一化嵌入:") # 打印提示信息
print(f"句子1: '{
sentences1[0]}'") # 打印句子1
print(f"句子2: '{
sentences2[0]}'") # 打印句子2
print(f" 余弦相似度: {
cos_sim_scores[0,0]:.4f}") # 打印余弦相似度
print(f" 欧几里得距离: {
euclidean_dist_norm[0,0]:.4f}") # 打印欧氏距离
# 验证关系: D = sqrt(2*(1-S))
derived_D = torch.sqrt(2 * (1 - cos_sim_scores[0,0])) # 根据余弦相似度推导欧氏距离
print(f" 从余弦相似度推导的欧氏距离: {
derived_D:.4f}") # 打印推导结果
print(f"句子1: '{
sentences1[0]}'") # 打印句子1
print(f"句子2: '{
sentences2[1]}'") # 打印句子2
print(f" 余弦相似度: {
cos_sim_scores[0,1]:.4f}") # 打印余弦相似度
print(f" 欧几里得距离: {
euclidean_dist_norm[0,1]:.4f}") # 打印欧氏距离
derived_D_2 = torch.sqrt(2 * (1 - cos_sim_scores[0,1])) # 根据余弦相似度推导欧氏距离
print(f" 从余弦相似度推导的欧氏距离: {
derived_D_2:.4f}") # 打印推导结果
except Exception as e:
print(f"运行快速入门示例时发生错误: {
e}") # 打印错误信息
选择哪个度量?
sentence-transformers
和许多语义相似度任务中最常用的。它对向量的绝对大小不敏感,只关注方向,这通常更符合语义相似性的直觉。特别是当嵌入被归一化后,计算非常高效(点积)。对于大多数 sentence-transformers
预训练模型,推荐使用余弦相似度,并确保嵌入在使用前已L2归一化。在实践中,强烈建议遵循所使用的 sentence-transformers
预训练模型的特定建议。大多数模型都是针对优化余弦相似度进行训练的。
2.4 语义搜索/信息检索示例
一个常见的 sentence-transformers
应用场景是语义搜索。给定一个查询(query)和一组文档(corpus),目标是找出语料库中与查询语义最相关的文档。
基本流程:
代码示例:简单的语义搜索
from sentence_transformers import SentenceTransformer, util # 导入所需模块
import torch # 导入torch
# 假设模型已加载 (例如 model = SentenceTransformer('all-MiniLM-L6-v2'))
model_name_en = 'all-MiniLM-L6-v2' # 定义模型名称
try:
model = SentenceTransformer(model_name_en) # 加载模型
print(f"模型 {
model_name_en} 加载成功。\n") # 打印成功消息
except Exception as e:
print(f"加载模型 {
model_name_en} 失败: {
e}") # 打印失败消息
model = None # 将模型设为None
if model: # 只有当模型成功加载后才执行
# 1. 定义我们的文档语料库 (一些句子)
corpus_sentences = [
"A man is eating food.",
"A man is eating a piece of bread.",
"The girl is carrying a baby.",
"A man is riding a horse.",
"A woman is playing violin.",
"Two men pushed carts through the woods.",
"A man is riding a white horse on an enclosed ground.",
"A monkey is playing drums.",
"A cheetah is running behind its prey."
] # 定义文档语料库
print("正在编码语料库句子...") # 打印提示信息
# 编码语料库,推荐使用 convert_to_tensor=True 和 normalize_embeddings=True
# 如果语料库非常大,可能需要分批处理或考虑持久化存储嵌入
corpus_embeddings = model.encode(corpus_sentences, convert_to_tensor=True, normalize_embeddings=True, show_progress_bar=True) # 编码语料库
print(f"语料库嵌入形状: {
corpus_embeddings.shape}") # 打印语料库嵌入的形状
# 2. 定义用户查询
user_query = "A man is riding an animal." # 定义用户查询
print(f"\n用户查询: '{
user_query}'") # 打印用户查询
# 3. 编码查询
# 对于单个查询,可以不指定 batch_size,show_progress_bar 通常也不会显示
query_embedding = model.encode(user_query, convert_to_tensor=True, normalize_embeddings=True) # 编码用户查询
print(f"查询嵌入形状: {
query_embedding.shape}") # 打印查询嵌入的形状
# 4. 计算查询与所有语料库文档的余弦相似度
# util.cos_sim(query_embedding, corpus_embeddings) 返回形状 (1, len(corpus_sentences))
# 我们取第0行得到每个文档的相似度分数
cosine_scores = util.cos_sim(query_embedding, corpus_embeddings)[0] # 计算余弦相似度,并取第一行
# 5. 查找得分最高的 Top-K 个结果
top_k = 5 # 定义返回结果的数量
# 使用 torch.topk 找到最高得分的索引和值
# cosine_scores 是一维张量
# largest=True 表示找最大的k个,sorted=True 表示结果按得分排序
top_results = torch.topk(cosine_scores, k=min(top_k, len(corpus_sentences)), largest=True, sorted=True) # 找到top-k结果
print(f"\n'{
user_query}' 的 Top-{
top_k} 相似结果:") # 打印提示信息
# top_results.values 是得分张量
# top_results.indices 是对应原始语料库中的索引张量
for score, idx in zip(top_results.values, top_results.indices): # 遍历得分和索引
corpus_idx = idx.item() # 将索引张量转换为整数
sentence = corpus_sentences[corpus_idx] # 获取对应的句子
similarity_score = score.item() # 将得分张量转换为浮点数
print(f" - 得分: {
similarity_score:.4f} - 句子: '{
sentence}' (索引: {
corpus_idx})") # 打印结果
# 另一个查询示例
user_query_2 = "Someone is playing an instrument." # 定义第二个用户查询
print(f"\n用户查询: '{
user_query_2}'") # 打印用户查询
query_embedding_2 = model.encode(user_query_2, convert_to_tensor=True, normalize_embeddings=True) # 编码查询
cosine_scores_2 = util.cos_sim(query_embedding_2, corpus_embeddings)[0] # 计算相似度
top_results_2 = torch.topk(cosine_scores_2, k=min(top_k, len(corpus_sentences)), largest=True, sorted=True) # 找到top-k结果
print(f"\n'{
user_query_2}' 的 Top-{
top_k} 相似结果:") # 打印提示信息
for score, idx in zip(top_results_2.values, top_results_2.indices): # 遍历结果
print(f" - 得分: {
score.item():.4f} - 句子: '{
corpus_sentences[idx.item()]}' (索引: {
idx.item()})") # 打印结果
else:
print("模型未能成功加载,跳过语义搜索示例。") # 如果模型加载失败,打印此消息
代码解释:
corpus_sentences
: 代表我们的文档集合。在实际应用中,这可能来自文件、数据库或其他来源。corpus_embeddings = model.encode(...)
: 对整个语料库进行编码。对于大型语料库,这一步可能耗时较长,其结果(嵌入)通常会被保存下来以便复用。query_embedding = model.encode(...)
: 对用户查询进行编码。cosine_scores = util.cos_sim(query_embedding, corpus_embeddings)[0]
: 计算查询嵌入与语料库中所有文档嵌入的相似度。[0]
是因为 query_embedding
是单个嵌入,cos_sim
返回的是一个 (1, num_corpus_docs)
的矩阵,我们只需要第一行。torch.topk(cosine_scores, k=..., largest=True, sorted=True)
: 这是一个非常方便的 PyTorch 函数,用于从张量中找到最大(或最小)的 k
个值及其对应的索引。
k=min(top_k, len(corpus_sentences))
:确保 k
不超过语料库大小。largest=True
: 表示我们想要得分最高的(最相似的)。sorted=True
: 表示返回的结果会按照得分从高到低排序。values
(得分) 和 indices
(在 cosine_scores
张量中的索引,这也对应于 corpus_sentences
列表中的索引)。top_results
,打印出每个匹配文档的相似度得分和原文。这个简单的例子展示了 sentence-transformers
在语义搜索方面的强大能力。对于非常大的语料库(百万级甚至更多文档),直接计算查询与所有文档的余弦相似度可能会变得低效。在这种情况下,通常会结合使用近似最近邻 (Approximate Nearest Neighbor, ANN) 搜索技术(如FAISS, Annoy, ScaNN, HNSWlib等库)来加速检索过程。我们会在后续章节中更详细地探讨这一点。
2.5 选择合适的预训练模型
sentence-transformers
提供了大量预训练模型,选择哪个模型取决于多个因素:
语言 (Language):
'all-mpnet-base-v2'
(高性能), 'all-MiniLM-L6-v2'
(速度与性能平衡), 'multi-qa-MiniLM-L6-cos-v1'
(针对问答/搜索优化)。'shibing624/text2vec-base-chinese'
, 'DMetaSoul/sbert-chinese-general-v2'
, Hugging Face Hub上还有更多由社区贡献的中文模型,例如针对特定任务(如金融、法律)微调的模型。'paraphrase-multilingual-MiniLM-L12-v2'
, 'paraphrase-multilingual-mpnet-base-v2'
。这些模型通常在一种或多种语言的混合语料上训练,或者在翻译对上训练。任务类型 (Task Type):
'all-mpnet-base-v2'
, 'all-MiniLM-L6-v2'
, 'stsb-roberta-large'
在STS (Semantic Textual Similarity) 基准上表现良好。'multi-qa-mpnet-base-dot-v1'
(使用点积), 'multi-qa-MiniLM-L6-cos-v1'
(使用余弦相似度)。这些模型通常在问答数据集上进行微调。性能需求 (Performance Requirements):
sentence-transformers
模型(如 'all-mpnet-base-v2'
, 'stsb-roberta-large'
)提供最佳的语义表示质量,但计算成本也更高(编码速度慢,模型更大)。'all-MiniLM-L6-v2'
, 'all-distilroberta-v1'
)提供了较好的折中。它们比大型模型快得多,模型尺寸也小得多,同时在许多任务上仍能保持相当不错的性能。嵌入维度 (Embedding Dimension):
sentence-transformers
模型的嵌入维度各不相同,常见的有384 (如MiniLM系列), 512, 768 (如BERT-base, RoBERTa-base, MPNet-base), 1024 (如BERT-large, RoBERTa-large)。对称 vs 非对称语义搜索 (Symmetric vs. Asymmetric Semantic Search):
multi-qa-...
系列)在这种情况下可能表现更好。在哪里查找和比较模型?
sentence-transformers
官方文档 - 预训练模型: (https://www.sbert.net/docs/pretrained_models.html) 这是最重要的资源,列出了官方推荐的各种模型及其在关键基准(如STS benchmark)上的性能得分、速度、大小和适用场景。sentence-transformers
的模型,包括社区贡献的许多模型。每个模型页面通常有其详细描述、训练数据、评估结果和使用示例。经验法则:
'all-MiniLM-L6-v2'
是一个很好的通用起点,因为它快速且效果不错。如果需要更高质量,可以尝试 'all-mpnet-base-v2'
。'shibing624/text2vec-base-chinese'
或 Hugging Face Hub 上其他高下载量/高评价的中文句子模型开始。'paraphrase-multilingual-MiniLM-L12-v2'
是一个流行的选择。第三章:深入理解池化策略与模型结构
sentence-transformers
的核心是将预训练的 Transformer 模型(如 BERT, RoBERTa)的词级别输出转换为一个固定大小的句子级别嵌入向量。这个转换过程由池化层 (Pooling Layer) 完成。选择合适的池化策略对于生成高质量的句子嵌入至关重要。
3.1 为什么需要池化?
底层的 Transformer 模型(如BERT)在处理输入文本后,会为序列中的每个输入词元(token)生成一个上下文相关的嵌入向量。例如,对于一个包含 (L) 个词元的句子,BERT 的最后一层会输出一个形状为 ((L, D)) 的矩阵,其中 (D) 是隐藏层维度(例如,BERT-base 为 768)。
然而,对于许多下游任务,如句子相似度比较、句子分类、聚类等,我们需要一个单一的、固定长度的向量来代表整个句子的语义。池化层的作用就是将这个 ((L, D)) 的词元嵌入序列聚合(aggregate)或池化(pool)成一个 ((1, D)) 或 ((D,)) 的句子嵌入向量。
3.2 sentence-transformers
中常见的池化策略
sentence-transformers
框架在其模型定义中显式地包含了池化层。当你加载一个预训练的 sentence-transformers
模型时,其池化配置也一同被加载。这些模型通常是在特定池化策略下进行微调的。
以下是一些在 sentence-transformers
中常用的池化模块(它们都继承自 torch.nn.Module
):
3.2.1 平均池化 (Mean Pooling)
sentence_transformers.models.Pooling
(当 pooling_mode_mean_tokens=True
)attention_mask
来忽略填充词元 [PAD]
)。Pooling
模块在计算平均值时会正确处理填充词元。它使用词元的注意力掩码 (attention_mask
) 来确保只有真实的词元参与平均计算。sentence-transformers
的预训练模型(如 all-MiniLM-L6-v2
, all-mpnet-base-v2
)中被证明是非常有效的默认策略,尤其是在 SBERT 论文中被广泛推荐。代码示例:理解 Pooling
模块的平均池化
from sentence_transformers import SentenceTransformer, models # 导入所需模块
import to