本系列文章目录
在之前的博客中,我们学习了为RAG(检索增强生成,Retrieval Augmented Generation)进行数据准备,包括数据摄取(Data Ingestion)、数据预处理(Data Preparation)和分块(Chunking)。
由于在RAG过程中需要搜索相关的上下文块,我们必须将数据从文本格式转换为向量嵌入(vector embeddings)。因此,我们将探索通过句子转换器(Sentence Transformers)实现文本转换的最高效方法。
让我们从一些最常用的嵌入模型(embedding models)开始。
1. 嵌入模型(Embedding Models)
嵌入(Embeddings)是一种单词表示形式(通过数值向量),允许具有相似含义的单词具有相似的表示。
这些向量可以通过各种机器学习算法和大型文本数据集学习得到。词嵌入(word embeddings)的主要作用之一是为下游任务(如文本分类和信息检索)提供输入特征。
在过去十年中,已经提出了几种词嵌入方法,以下是其中一些:
1.1 上下文无关嵌入(Context-independent Embeddings)
上下文无关嵌入(Context-independent embeddings)重新定义了单词表示,无论上下文如何变化,都为每个单词分配唯一的向量。这种简洁的探索侧重于对同形异义词(homonym)消歧的影响。
- 上下文无关模型为每个单词分配不同的向量,而不考虑上下文。
- 像"duck"这样的同形异义词会获得单个向量,在没有上下文线索的情况下混合了多种含义。
- 这种方法产生了一个全面的单词向量图,以固定表示形式捕捉多个含义。
上下文无关嵌入提供了效率,但在细微语言理解方面提出了挑战,特别是对于同形异义词。这种范式转变促使我们更仔细地研究自然语言处理中的权衡。
一些常见的基于频率的上下文无关嵌入:
词袋模型(Bag of Words)
词袋模型(Bag of Words)将创建一个包含所有句子中最常见单词的字典,然后对句子进行编码,如下所示。
TF-IDF
TF-IDF是一种从句子中查找特征的简单技术。在计数特征(Count features)中,我们计算文档中所有单词/ngrams的出现次数,而在TF-IDF中,我们只对重要单词提取特征。我们如何做到这一点?如果您考虑语料库中的一个文档,我们将关注该文档中任何单词的两个方面:
词频(Term Frequency):单词在文档中的重要性如何?
逆文档频率(Inverse Document Frequency):该术语在整个语料库中的重要性如何?
一些常见的基于预测的上下文无关嵌入:
Word2Vec:
Word2Vec中的词嵌入(word embeddings)是通过一个两层神经网络学习的,该网络在训练过程中无意中捕捉了语言上下文。这些嵌入作为算法主要目标的副产品展示,显示了这种方法的效率。Word2Vec通过两种不同的模型架构提供了灵活性:CBOW和连续skip-gram。
- 连续词袋模型(Continuous Bag-of-Words, CBOW):
- 从上下文单词的窗口中预测当前单词。
- 强调上下文单词在预测目标单词时的协作影响。
- 连续Skip-Gram:
- 使用当前单词来预测上下文单词的窗口。
- 侧重于目标单词在生成上下文单词时的预测能力。
Word2Vec的双模型架构在捕捉语言细微差别方面提供了多功能性,允许从业者根据其自然语言处理任务的具体要求在CBOW和连续skip-gram之间进行选择。理解这些架构之间的相互作用增强了Word2Vec在不同上下文中的应用。
GloVe(全局词向量表示,Global Vectors for Word Representation):
GloVe的优势在于其在训练过程中利用了从语料库中聚合的全局词-词共现统计信息。生成的表示不仅封装了语义关系,还揭示了词向量空间中有趣的线性子结构,为词嵌入的理解增添了深度。
FastText:
与GloVe相比,FastText采用了一种新颖的方法,将每个单词视为由字符n-gram组成。这一独特特性使FastText不仅能够学习罕见单词,还能优雅地处理词汇表外的单词(out-of-vocabulary words)。对字符级嵌入的强调使FastText能够捕捉形态学上的细微差别,提供了更全面的词汇表示。
1.2 上下文相关嵌入(Context-Dependent Embeddings)
上下文相关方法(Context-Dependent methods)根据上下文为同一个单词学习不同的嵌入。
基于RNN的模型:
- ELMO(来自语言模型的嵌入,Embeddings from Language Model):基于具有基于字符的编码层和两个BiLSTM层的神经语言模型学习上下文相关的词表示。
- CoVe(上下文相关的词向量,Contextualized Word Vectors):使用来自为机器翻译训练的注意力序列到序列模型的深度LSTM编码器来上下文化词向量。
基于Transformer的模型:
- BERT(双向编码器表示来自Transformers,Bidirectional Encoder Representations from Transformers):基于Transformer的语言表示模型,在大型跨领域语料库上训练。应用掩码语言模型(masked language model)来预测序列中随机掩码的单词,随后是用于学习句子之间关联的下一句预测任务(next-sentence-prediction task)。
- XLM(跨语言语言模型,Cross-lingual Language Model):这是一个使用下一词预测、类似BERT的掩码语言建模目标和翻译目标进行预训练的transformer。
- RoBERTa(鲁棒优化的BERT预训练方法,Robustly Optimized BERT Pretraining Approach):它建立在BERT基础上,修改了关键超参数,移除了下一句预训练目标,并使用更大的小批次和学习率进行训练。
- ALBERT(用于语言表示自监督学习的轻量级BERT,A Lite BERT for Self-supervised Learning of Language Representations):提出了参数减少技术,以降低内存消耗并提高BERT的训练速度。
2. BERT
BERT(Bidirectional Encoder Representations from Transformers)是由Google AI开发的自然语言处理领域的强大模型,重塑了语言模型的格局。本节将深入探讨其预训练方法和双向架构的复杂性。
预训练(Pre-training):BERT通过两个无监督任务进行预训练——掩码语言建模(MLM,Masked Language Modeling)和下一句预测(NSP,Next Sentence Prediction)。在MLM任务中,系统会随机掩盖输入中15%的标记(tokens),目标是仅根据上下文预测被掩盖单词的原始词汇ID。
除了掩码语言模型外,BERT还采用NSP任务联合预训练文本对表示。许多重要下游任务(如问答系统QA和自然语言推理NLI)都基于对两个句子关系的理解,而这是传统语言建模无法直接捕捉的。
预训练数据(Pre-training data):预训练流程基本遵循语言模型预训练的现有文献规范。使用的预训练语料库包括:
- BooksCorpus(8亿单词)
- 英文维基百科(25亿单词)
双向性(Bi-directional):与从左到右(left-to-right)的语言模型预训练不同,MLM目标使模型能够融合左右上下文信息,从而实现对深度双向Transformer的预训练。
BERT架构包含多个编码器层(encoder layers),每层对输入进行自注意力(self-attention)处理并传递到下一层。最小的BERT BASE变体包含:
- 12个编码器层
- 768个隐藏单元的FFNN块
- 12个注意力头(attention heads)
2.1 输入表示(Input representations)
对于问答任务,BERT将由句子或句子对(例如,<问题,答案>)组成的序列作为输入,以一个标记(token)序列的形式输入。
在将输入序列输入到模型之前,会使用词块分词器(WordPiece Tokenizer)进行处理,该分词器的词表大小为30000个标记。它的工作方式是将一个单词拆分成几个子词(标记)。
特殊标记如下:
- [CLS]:用作每个序列的第一个标记。与该标记对应的最终隐藏状态将用作分类任务的聚合序列表示。
- [SEP]:句子对被打包成一个单一序列。我们通过两种方式区分句子。首先,我们使用一个特殊标记([SEP])将它们分开。其次,我们为每个标记添加一个已学习的嵌入,以指示它属于句子A还是句子B。
- [PAD]:用于表示输入句子中的填充(空标记)。模型期望输入固定长度的句子。因此,根据数据集会固定一个最大长度。较短的句子会被填充,而较长的句子会被截断。为了明确区分真实标记和[PAD]标记,我们使用注意力掩码。
引入了段嵌入(Segmentation embedding)来指示给定标记属于第一个还是第二个句子。位置嵌入(Positional embedding)指示标记在句子中的位置。与原始的Transformer不同,BERT从绝对顺序位置学习位置嵌入,而不是使用三角函数。
对于给定的标记,其输入表示是通过将相应的标记嵌入、段嵌入和位置嵌入相加来构建的。
BERT输入表示:输入嵌入是标记嵌入、段嵌入和位置嵌入的总和。
使用BERT进行句子编码
为了获得标记嵌入,在嵌入层使用一个嵌入查找表(如上图所示),其中行表示词表中所有可能的标记ID(例如有30000行),列表示标记嵌入的维度。
2.2. 为什么选择句子BERT(S-BERT)而不是BERT?
到目前为止,一切都还不错,但这些Transformer模型在构建句子向量时存在一个问题:Transformer是使用词或标记级别的嵌入进行工作的,而不是句子级别的嵌入。
在句子Transformer出现之前,使用BERT计算准确句子相似度的方法是使用交叉编码器(cross-encoder)结构。这意味着我们会将两个句子输入到BERT中,在BERT的顶部添加一个分类头,然后使用它来输出一个相似度分数。
BERT交叉编码器架构由一个BERT模型组成,该模型处理句子A和句子B。两者都在同一个序列中进行处理,由一个[SEP]标记分隔。所有这些之后是一个前馈神经网络分类器,它输出一个相似度分数。
孪生(双编码器)架构在左侧显示,非孪生(交叉编码器)架构在右侧显示。主要区别在于左侧的模型同时接受两个输入。在右侧,模型并行接受两个输入,因此两个输出彼此不依赖。
交叉编码器(左)和双编码器(右)
交叉编码器网络确实能产生非常准确的相似度分数(比SBERT更好),但它不具有可扩展性。如果我们想要在一个包含10万个句子的小型数据集中进行相似度搜索,我们需要完成10万次交叉编码器推理计算。
为了对句子进行聚类,我们需要比较我们10万个句子数据集中的所有句子,这将导致接近5亿次比较——这根本不现实。
理想情况下,我们需要预先计算可以存储的句子向量,然后在需要时使用它们。如果这些向量表示良好,我们所需要做的就是计算它们之间的余弦相似度。使用原始的BERT(以及其他Transformer),我们可以通过对BERT输出的所有标记嵌入的值求平均来构建一个句子嵌入(如果我们输入512个标记,我们就会输出512个嵌入)。[方法一]
或者,我们可以使用第一个[CLS]标记的输出(一个特定于BERT的标记,其输出嵌入用于分类任务)。[方法二]
使用这两种方法中的任何一种都可以为我们提供句子嵌入,这些嵌入可以被存储并且比较速度要快得多,将搜索时间从65小时缩短到大约5秒。然而,准确性并不理想,甚至比使用平均的GloVe嵌入(2014年开发)还要差。
句子BERT(双编码器)
因此,使用BERT从1万个句子中找到最相似的句子对需要65个小时。使用SBERT,大约5秒内就能创建嵌入,并且使用余弦相似度进行比较大约只需0.01秒。
自从SBERT的论文发表以来,已经使用与训练原始SBERT类似的概念构建了更多的句子Transformer模型。它们都是在许多相似和不相似的句子对上进行训练的。
使用诸如softmax损失、多负例排名损失(multiple negatives ranking loss)或均方误差边际损失(MSE margin loss)等损失函数,这些模型被优化为对相似的句子生成相似的嵌入,对不相似的句子则生成不相似的嵌入。
推导独立的句子嵌入是BERT的主要问题之一。为了缓解这一方面的问题,开发了SBERT。
3. 句子Transformer
我们解释了使用BERT进行句子相似度计算的交叉编码器架构。SBERT与之类似,但去掉了最后的分类头,并且一次处理一个句子。然后,SBERT对最终输出层使用平均池化(mean pooling)操作来生成一个句子嵌入。
与BERT不同,SBERT使用孪生架构(siamese architecture)在句子对上进行微调。我们可以将其想象为有两个并行的相同BERT,它们共享完全相同的网络权重。
一个应用于句子对(句子A和句子B)的SBERT模型。请注意,BERT模型输出标记嵌入(由512个768维向量组成)。然后,我们使用一个池化函数将这些数据压缩成一个单一的768维句子向量。
实际上,我们使用的是单个BERT模型。然而,因为在训练期间我们将句子A和句子B作为对依次进行处理,所以将其想象为两个具有绑定权重的模型会更容易理解。
3.1. 孪生BERT预训练
训练句子Transformer有不同的方法。我们将描述原始SBERT论文中最突出的原始过程,该过程基于softmax损失进行优化。
softmax损失方法使用在斯坦福自然语言推理(Stanford Natural Language Inference,SNLI)和多体裁自然语言推理(Multi-Genre NLI,MNLI)语料库上微调的“孪生”架构。
SNLI包含57万个句子对,MNLI包含43万个句子对。这两个语料库中的句子对都包含一个前提和一个假设。每对句子被分配三个标签之一:
- 0 — 蕴含(entailment),例如前提暗示了假设。
- 1 — 中立(neutral),前提和假设都可能是真的,但它们不一定相关。
- 2 — 矛盾(contradiction),前提和假设相互矛盾。
给定这些数据,我们将句子A(假设是前提)输入到孪生BERT A中,将句子B(假设是假设)输入到孪生BERT B中。
孪生BERT输出我们池化后的句子嵌入。在SBERT论文中有三种不同池化方法的结果。它们是平均池化(mean)、最大池化(max)和[CLS]池化。对于NLI和STSb数据集,平均池化方法的性能最佳。
现在有两个句子嵌入。我们将嵌入A称为u,嵌入B称为v。下一步是连接u和v。同样,测试了几种连接方法,但性能最高的是一个(u, v, |u-v|)操作:
我们连接嵌入u、v和|u — v|。
|u-v|是通过计算得到的,用于给出两个向量之间的元素级差异。除了原始的两个嵌入(u和v)之外,这些都被输入到一个具有三个输出的前馈神经网络(FFNN)中。
这三个输出与我们的NLI相似度标签0、1和2相对应。我们需要计算来自FFNN的softmax,这是在交叉熵损失函数中完成的。softmax和标签用于对这个“softmax损失”进行优化。
在训练期间对两个句子嵌入u和v执行这些操作。请注意,softmax损失指的是交叉熵损失(默认情况下包含一个softmax函数)。
这使得我们对于相似句子(标签0)的池化句子嵌入变得更加相似,对于不相似句子(标签2)的嵌入变得更加不相似。
记住我们使用的是孪生BERT,而不是双BERT。这意味着我们不使用两个独立的BERT模型,而是使用一个先处理句子A再处理句子B的单个BERT。
这意味着当我们优化模型权重时,它们会朝着一个方向调整,使得模型在看到蕴含标签时输出更相似的向量,在看到矛盾标签时输出更不相似的向量。
4. SBERT目标函数
通过使用这两个向量u和v,下面讨论了优化不同目标的三种方法。
4.1. 分类
将三个向量u、v和|u-v|连接起来,乘以一个可训练的权重矩阵W,然后将乘法结果输入到softmax分类器中,该分类器输出与不同类别相对应的句子的归一化概率。使用交叉熵损失函数来更新模型的权重。
用于分类目标的SBERT架构。参数n表示嵌入的维度(对于BERT基础模型,默认值为768),而k表示标签的数量。
4.2. 回归
在这种公式化方法中,在得到向量u和v之后,它们之间的相似度分数直接通过一个选定的相似度度量来计算。将预测的相似度分数与真实值进行比较,并使用均方误差(MSE)损失函数来更新模型。
用于回归目标的SBERT架构。参数n表示嵌入的维度(对于BERT基础模型,默认值为768)。
4.3. 三元组损失
三元组目标引入了一个三元组损失,它是在通常被称为锚点(anchor)、正例(positive)和负例(negative)的三个句子上计算的。假设锚点和正例句子彼此非常接近,而锚点和负例句子则非常不同。在训练过程中,模型评估(锚点,正例)对与(锚点,负例)对相比的接近程度。
三元组SBERT架构
目前,让我们看看如何初始化和使用这些句子Transformer模型。
5. 实践句子Transformer
开始使用句子Transformer的最快、最简单的方法是通过由SBERT的创建者开发的sentence-transformers库。我们可以使用pip安装它。
!pip install sentence-transformers
我们将从原始的SBERT模型bert-base-nli-mean-tokens开始。首先,我们下载并初始化该模型。
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('bert-base-nli-mean-tokens')
model
输出:
SentenceTransformer(
(0): Transformer({'max_seq_length': 128, 'do_lower_case': False}) with Transformer model: BertModel
(1): Pooling({'word_embedding_dimension': 768, 'pooling_mode_cls_token': False, 'pooling_mode_mean_tokens': True, 'pooling_mode_max_tokens': False, 'pooling_mode_mean_sqrt_len_tokens': False})
)
我们在这里看到的输出是SentenceTransformer对象,它包含三个组件:
- Transformer本身,在这里我们可以看到最大序列长度为128个标记,以及是否将任何输入转换为小写(在这种情况下,模型不转换)。我们还可以看到模型类,BertModel。
- 池化操作,在这里我们可以看到我们正在生成一个768维的句子嵌入。我们使用的是平均池化方法。
一旦我们有了模型,使用encode
方法可以快速构建句子嵌入。
sentences = [
"the fifty mannequin heads floating in the pool kind of freaked them out",
"she swore she just saw her sushi move",
"he embraced his new life as an eggplant",
"my dentist tells me that chewing bricks is very bad for your teeth",
"the dental specialist recommended an immediate stop to flossing with construction materials"
]
embeddings = model.encode(sentences)
embeddings.shape
输出:
(5, 768)
6. 选择哪种嵌入模型?
然而,我们很快就会发现,目前使用的大多数嵌入模型都属于Transformer类别。这些模型由不同的提供商提供,有些是开源的,有些是专有的,每个模型都针对特定的目标进行了定制:
- 有些最适合编码任务。
- 有些专门为英语设计。
- 还有一些嵌入模型在处理多语言数据集方面表现出色。
最简单的方法是利用现有的学术基准。然而,必须记住,这些基准可能无法全面反映人工智能应用中检索系统在现实世界中的使用情况。
或者,你可以使用各种嵌入模型进行测试,并编制一个最终的评估表,以确定最适合你特定用例的模型。我强烈建议在此过程中加入一个重排器(re-ranker),因为它可以显著提高检索器的性能,最终产生最佳结果。
为了简化你的决策过程,Hugging Face提供了卓越的大规模文本嵌入基准(Massive Text Embedding Benchmark,MTEB)排行榜。这个资源提供了关于所有可用嵌入模型及其在各种指标上的相应分数的全面信息:
如果你选择第二种方法,有一篇优秀的博客文章展示了如何利用LlamaIndex的检索评估模块。这个资源可以帮助你有效地评估和识别从初始模型列表中最适合的嵌入和重排器组合。
我相信,现在你在为你的RAG架构选择最合适的嵌入和重排模型时,能更好地做出明智的决策了!
结论
这篇博客探讨了用于生成文本向量表示的各种嵌入模型,包括词袋模型(Bag of Words,BoW)、词频-逆文档频率(TF-IDF)、Word2Vec、GloVe、FastText、ELMO、BERT等。它深入研究了BERT的架构和预训练,引入了用于高效句子嵌入的句子BERT(SBERT),并提供了一个使用sentence-transformers库的实践示例。结论强调了选择合适嵌入模型的挑战,并建议利用Hugging Face大规模文本嵌入基准(MTEB)排行榜等资源进行评估。
致谢
在这篇博客文章中,我们从各种来源收集了信息,包括研究论文、技术博客、官方文档、YouTube视频等。每个来源都在相应的图片下方进行了适当的标注,并提供了来源链接。
以下是参考资料的综合列表:
- https://jina.ai/news/the-1950-2024-text-embeddings-evolution-poster/
- https://partee.io/2022/08/11/vector-embeddings/
- https://www.nlplanet.org/course-practical-nlp/01-intro-to-nlp/11-text-as-vectors-embeddings
- https://www.deeplearning.ai/resources/natural-language-processing/
- https://www.mygreatlearning.com/blog/word-embedding/#sh4
- https://mlwhiz.com/blog/2019/02/08/deeplearning_nlp_conventional_methods/
- https://vitalflux.com/bert-vs-gpt-differences-real-life-examples/
- https://d3mlabs.de/?p=1169
- https://www.linkedin.com/pulse/why-does-bert-stand-out-sea-sentence-embedding-models-bhaskar-t-bi6wc%3FtrackingId=RKK3MNdP8pugx6iyhwJ2hw%253D%253D/?trackingId=RKK3MNdP8pugx6iyhwJ2hw%3D%3D
- https://www.amazon.science/blog/improving-unsupervised-sentence-pair-comparison
- https://www.researchgate.net/figure/Sentence-BERT-model_fig3_360530243
- https://www.youtube.com/watch?app=desktop&v=O3xbVmpdJwU
- https://www.pinecone.io/learn/series/nlp/sentence-embeddings/
- https://towardsdatascience.com/sbert-deb3d4aef8a4
- https://huggingface.co/spaces/mteb/leaderboard
- https://medium.com/@vipra_singh/building-llm-applications-sentence-transformers-part-3-a9e2529f99c1