点击上方“AI公园”,关注公众号,选择加“星标“或“置顶”
作者:Vipra Singh
编译:ronghuaiyang
导读
通过检索增强生成(RAG)应用的视角来学习大型语言模型(LLM)。
在前几篇博文中,我们学习了面向RAG的数据准备,这包括数据摄入、数据预处理及分块。
由于在执行RAG期间需要搜索相关的上下文分块,我们必须将数据从文本格式转换为向量嵌入。
因此,我们将探索使用Sentence Transformers来转换文本的最有效方式。
让我们从一些最常用的嵌入模型开始。
1. 嵌入模型
嵌入是一种单词表示形式(通过数值向量),使得具有相似意义的单词具有相似的表示。
这些向量可以通过多种机器学习算法和大型文本数据集来学习。词嵌入的主要作用之一是为下游任务(如文本分类和信息检索)提供输入特征。
在过去十年中,已经提出了多种词嵌入方法,以下是其中的一些:
1.1. 独立于上下文的嵌入
独立于上下文的嵌入重新定义了词表示,为每个词分配唯一的向量,而不考虑上下文变化。这里这个简要的探讨侧重于同形异义词消歧方面。
独立于上下文的模型为每个词分配独特的向量,无论上下文如何变化。
像“duck”这样的同形异义词获得单一的向量,没有上下文线索的情况下混合了多种含义。
这种方法生成了一个全面的词向量映射,以固定的表示形式捕获多个意义。
独立于上下文的嵌入提供了效率,但在理解语言的细微差别时,尤其是在处理同形异义词时,也带来了挑战。这一范式转变促使我们更仔细地审视自然语言处理中的权衡。
一些常见的基于频率的独立于上下文的嵌入方法包括:
词袋模型(Bag of Words, BoW)
词袋模型会创建一个词汇表,包含所有句子中最常见的词汇,然后按照如下方式对句子进行编码:
Bag of WordsTF-IDF
TF-IDF是一种从句子中寻找特征的简单技术。在计数特征中,我们会统计文档中出现的所有单词/词组的数量,而TF-IDF只针对重要单词提取特征。我们是如何做到这一点的呢?想象语料库中的一个文档,对于文档中的任一单词,我们会考虑两个方面:
Term Frequency: 这个词在文档中有多重要?
Inverse Document Frequency: 该词在整个语料库中的重要性如何?
一些常见的基于预测的、与上下文无关的嵌入方法包括:
Word2Vec: Word2Vec中的词嵌入是通过一个两层的神经网络学习得到的,在训练过程中无意间捕获了语言上下文信息。这些嵌入作为算法主要目标的副产品出现,展示了这种方法的高效性。Word2Vec通过两种不同的模型架构——CBOW和连续skip-gram,提供了灵活性。
连续词袋模型(CBOW):
根据周围上下文单词的窗口来预测当前单词。
强调了上下文单词在预测目标单词时的协同影响。
连续Skip-Gram:
利用当前单词来预测周围的上下文单词窗口。
侧重于目标词在生成上下文词时的预测能力。
Word2Vec的双重模型架构提供了捕捉语言细微差别的多功能性,使得实践者可以根据自然语言处理任务的具体需求,在CBOW和连续skip-gram之间做出选择。理解这两种架构之间的相互作用增强了Word2Vec在多种情境下的应用效果。
Word-2-VecGloVe(全局向量词表示): GloVe的优势在于其在训练过程中利用了语料库中汇总的全局词-词共现统计信息。由此产生的表示不仅封装了语义关系,还在词向量空间中揭示了引人注目的线性子结构,加深了对词嵌入的理解。
FastText: 与GloVe不同,FastText采用了一种新颖的方法,将每个词视为由字符n-gram组成的。这一独特特性使FastText不仅能学习罕见词汇,还能巧妙处理词汇表外的词汇。对字符级嵌入的重视使FastText能够捕捉形态学上的细微差别,提供了对词汇更全面的表示。
1.2. 上下文依赖型嵌入
上下文依赖型方法根据同一个词所处的不同上下文环境,学习不同的词嵌入表示。
基于RNN的方法:
ELMO(来自语言模型的嵌入):基于具有字符级编码层和两个双向LSTM层的神经语言模型,学习上下文化的词表示。
CoVe(上下文化词向量):利用为机器翻译任务训练的带有注意力机制的序列到序列模型中的深层LSTM编码器来为词向量赋予上下文信息。
基于Transformer的方法:
BERT(来自变换器的双向编码器表示):基于Transformer的语言表示模型,使用大量跨领域的语料库进行训练。它应用了掩码语言模型来预测序列中随机遮盖的单词,并结合下一个句子预测任务来学习句子间的关联。
XLM(跨语言语言模型):通过下一个token预测、类似BERT的掩码语言模型目标以及翻译目标,使用Transformer进行预训练。
RoBERTa(稳健优化的BERT预训练方法):在BERT的基础上发展而来,调整了关键超参数,移除了下一个句子预训练目标,并使用更大的小批量尺寸和学习率进行训练。
ALBERT(用于语言表示自我监督学习的轻量BERT):提出参数缩减技术,以减少内存消耗并加快BERT的训练速度。
2. BERT
BERT(来自变换器的双向编码器表示)是谷歌AI开发的自然语言处理领域的一大突破,彻底改变了语言模型的格局。这一深入探讨将聚焦于其预训练方法论及其双向架构的精妙之处。
预训练: BERT通过两项无监督任务进行预训练——掩码语言建模(Masked Language Modeling, MLM) 和 下一句预测(Next Sentence Prediction, NSP)。在MLM任务中,大约随机遮盖输入中15%的词令牌,目标是仅凭上下文预测被遮盖词的原始词汇ID。
除了掩码语言模型外,BERT还采用NSP任务联合预训练文本对的表示。许多重要的下游任务,如问答(Question Answering, QA)和自然语言推理(Natural Language Inference, NLI),都是基于理解两个句子间的关系,这是单纯的语言模型所不能直接捕获的。
预训练数据: 预训练过程大体遵循现有的语言模型预训练文献。预训练语料库包括书籍语料库(8亿词)和英文维基百科(25亿词)。
双向性: 与从左到右的语言模型预训练不同,MLM目标使得表示能够融合左右上下文,从而使我们能够预训练一个深度的双向变换器。
BERT的架构由多层编码器组成,每层对输入应用自注意力并通过到下一层。即使是最小的变体BERT BASE,也拥有12层编码器、带有768隐藏单元的前馈神经网络块和12个注意力头。
2.1. 输入表示
BERT 接收的输入序列由句子或句子对(例如,对于问答任务中的<问题,答案>)组成,这些内容整合成一个token序列。
在输入模型之前,使用词汇量为3万的WordPiece分词器对输入序列进行预处理。该分词器通过将单词拆分成若干个子词(token)来工作。
特殊token包括:
[CLS] 作为每个序列的第一个token使用。与这个token对应的最终隐藏状态被用作分类任务中的序列整体表示。
[SEP] 句子对被打包成单一序列。我们通过两种方式区分这些句子。首先,我们用一个特殊token([SEP])将它们分开。其次,我们为每个token添加一个学习到的嵌入,以表明它属于句子A还是句子B。
[PAD] 用于表示输入句子中的填充部分(空token)。模型期望输入的是固定长度的句子。因此,根据数据集设定一个最大长度。较短的句子会被填充,较长的句子则被截断。为了明确区分真实token和[PAD]token,我们使用了注意力掩码。
引入了分割嵌入来指示给定token属于第一个句子还是第二个句子。位置嵌入表示句子中token的位置。与原始Transformer不同,BERT根据绝对序数位置学习位置嵌入,而不是使用三角函数。
对于给定的token,其输入表示是通过将相应的token嵌入、分割嵌入和位置嵌入相加构建的。
BERT的输入表示, 输入嵌入是token嵌入、分割嵌入和位置嵌入的总和。 句子编码使用BERT为了获取token嵌入,会在嵌入层使用一个嵌入查找表(如上图所示),其中行代表词汇表中所有可能的token的ID(例如,3万个行),列代表token嵌入的大小。
2.2. 为何选用Sentence BERT(S-BERT)而非BERT?
到目前为止一切顺利,但在构建句子向量时,这些Transformer模型存在一个问题:Transformer模型使用的是基于词或token级别的嵌入,而非句子级别的嵌入。
在句子Transformer出现之前,使用BERT计算句子相似度的方法是采用交叉编码器结构。这意味着我们将两个句子传递给BERT,并在BERT顶部添加一个分类头,以此输出相似度分数。
BERT交叉编码器架构包括一个消费句子A和B的BERT模型。两个句子在同一序列中被处理,中间由**[SEP]**令牌隔开。之后连接一个前馈神经网络分类器,输出相似度分数。
然而,这种方法的缺点是计算成本高,因为它需要对每一对句子进行编码,这在大型语料库或实时应用中是不切实际的。于是,Sentence BERT(S-BERT)应运而生,它采用了句向量方法,允许独立地编码句子,然后在向量空间中直接比较句子,极大地提高了效率。S-BERT通过微调BERT模型,使其在句子对相似度任务上进行训练,从而学习到有意义的句子级别嵌入。这样,每个句子都可以单独编码,然后通过计算这些固定长度句向量的余弦相似度等度量来快速估计句子间的相似性,无需像原始BERT那样对每对句子进行联合编码。
在左侧展示的是孪生(双编码器)架构,而右侧则是非孪生(交叉编码器)架构。两者的主要区别在于:左侧架构中,模型同时接受两个输入,它们作为一个整体被处理以生成表示;而右侧的交叉编码器架构中,两个输入是并行处理的,但模型的输出—即相似度评分—是基于两个输入联合处理的结果,因此输出彼此依赖。简而言之,孪生架构允许独立编码输入,然后比较它们的编码,而交叉编码器则直接基于联合输入给出最终的相似度判断。
左:交叉编码器,右:双编码器交叉编码器网络确实能产生非常精确的相似度分数(比SBERT还好),但它不具备扩展性。如果我们想在一个包含100K句子的小数据集中进行相似性搜索,就需要完成100K次的交叉编码器推断计算。
若要对句子进行聚类,我们必须比较数据集中所有的100K条句子,这将导致近5亿次的比较——这显然是不现实的。
理想情况下,我们需要预先计算句子向量,这些向量可以被存储并在需要时使用。 如果这些向量表示良好,我们只需计算每对向量之间的余弦相似度即可。使用原始的BERT(以及其他Transformer模型),我们可以通过平均BERT输出的所有token的嵌入值来构建句子嵌入(如果输入512个token,就会输出512个嵌入)。【方法1】
或者,我们可以使用第一个[CLS]token的输出(这是一个BERT特有的token,其输出嵌入用于分类任务)。【方法2】
利用这两种方法之一,我们可以获得句子嵌入,这些嵌入可以被更快地存储和比较,将搜索时间从65小时缩短到大约5秒。然而,准确性并不高,甚至不如使用平均化的GloVe嵌入(该方法于2014年开发)的表现。这就凸显了Sentence BERT(S-BERT)的重要性,它通过微调BERT来直接优化句子级别的相似度任务,从而在保持高效的同时显著提升了准确性。
Sentence BERT (Bi-Encoder)因此,使用BERT从10,000个句子中找到最相似的句子对需要65小时。而使用SBERT,创建嵌入仅需约5秒钟,并且通过余弦相似度进行比较仅需约0.01秒。
自SBERT论文发表以来,基于训练原始SBERT时采用的类似概念,已经构建了许多其他的句子Transformer模型。它们都在许多相似和不相似的句子对上进行训练。
通过使用如softmax损失、多负样本排序损失或均方误差边际损失等损失函数,这些模型被优化以对相似句子产生相似的嵌入,而对不相似的句子则产生不同的嵌入。
提取独立的句子嵌入是BERT面临的主要问题之一。为了解决这一问题,开发了SBERT。
3. Sentence Transformers
实际上,关于SBERT的描述需要一些澄清。SBERT实际上并没有完全摒弃分类头,而是采用了不同的策略来生成句子嵌入。SBERT的核心改进在于它通过微调BERT模型来优化句子对的相似度任务,这通常涉及到使用孪生网络(siamese)架构进行训练,而非简单地去掉分类头或一次处理一个句子。
在孪生网络架构中,确实有两个相同的网络(基于BERT)并行运行,它们共享相同的权重。这两个网络分别接收一对句子作为输入,各自产生句法表示,然后通过特定的损失函数(比如上述提到的softmax损失、多负样本排序损失或MSE边际损失)来优化它们的输出,以确保相似句子的嵌入更加接近,而不相似句子的嵌入则远离彼此。最后,SBERT通常使用平均池化(mean pooling)或其他方法(如[CLS]token的输出)来从这些句法表示中提取句子级别的嵌入。
因此,SBERT的设计旨在克服原始BERT在处理句子级别的任务时的局限性,通过专门针对句子相似度任务的微调,实现了更高效、更准确的句子表示。
在处理句子对(如句子A和句子B)时,SBERT模型的工作流程如下:首先,SBERT模型会分别对两个句子应用BERT模型,得到每个句子的token嵌入。这些嵌入是由512个768维向量组成的(这里纠正一下,BERT模型的输出维度通常是768,而非512)。之后,为了从这些丰富的token级别信息中提炼出句子级别的表示,我们会使用一个池化函数(如平均池化mean pooling、最大池化max pooling或[CLS]token策略等)来压缩这些数据,将其转换成单个768维的句子向量。这样,每个句子就被编码成了一个高维的向量,这些向量可以直接用于计算句子间的相似度,如通过计算余弦相似度来衡量两个句子的语义接近程度。
实际上,SBERT确实基于单个BERT模型进行操作。在训练过程中,尽管我们是依次处理句子A和句子B作为一对进行的,但这并不意味着存在两个物理分离的模型实体,而是同一模型在不同时间点分别处理两个句子,且该模型的所有参数(权重)在处理这对句子时保持一致。因此,提及“两个模型共享相同权重”是一种简化的表述方式,帮助理解在处理句子对时,SBERT确保了模型对句子的编码方式具有一致性和可比性,实质上是在利用BERT框架实现一种孪生网络(Siamese Network)的训练策略,从而优化句子间的相似度判断。
3.1. 孪生BERT预训练
在训练句子Transformer时,存在多种方法。我们将描述最初SBERT论文中突出介绍的、基于softmax损失进行优化的原始过程。
softmax损失方法采用“孪生(siamese)”架构,并在斯坦福自然语言推理(SNLI)和多体裁自然语言推理(MNLI)语料库上进行微调。
SNLI包含57万个句子对,而MNLI包含43万个。这两个语料库中的配对都包括一个前提和一个假设。每对配对被分配以下三个标签之一:
0 — 蕴含,即前提暗示假设。
1 — 中立,前提和假设都可能是真的,但它们不一定相关。
2 — 矛盾,前提和假设相互矛盾。
基于此数据,我们将句子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一起,被输入到一个具有三个输出的前馈神经网络(FFNN)中。
这三个输出对应于我们NLI相似度标签0(蕴含)、1(中立)和2(矛盾)。我们需要基于FFNN的输出计算softmax概率,这通常在交叉熵损失函数的计算过程中完成。softmax概率与标签一起,用于依据softmax损失进行优化。
这些操作在训练期间应用于两个句子嵌入,即u和v。需要注意的是,softmax-loss实际上指的是交叉熵损失(该损失默认包含softmax函数)。这导致对于标注为0(相似)的句子,其池化后的句子嵌入变得更加相似;而对于标注为2(不相似)的句子,嵌入则变得不那么相似。
记住我们使用的是孪生(siamese)BERT,而非两个独立的BERT(dual BERTs)。这意味着我们并非使用两个独立的BERT模型,而是用同一个BERT模型依次处理句子A和句子B。
这意味着,当我们优化模型权重时,它们会被推向这样一个方向:在识别到蕴含标签的情况下,模型输出的向量更加相似;而在识别到矛盾标签时,输出的向量则更加不相似。
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. 三元组损失
三元组目标引入了三元组损失,该损失基于三个句子计算,通常称为锚点、正例和负例。假设锚点和正例句子彼此非常接近,而锚点和负例则差异很大。在训练过程中,模型评估(锚点,正例)对相比(锚点,负例)对有多接近。
Triplet SBERT 结构接下来,让我们看看如何初始化和使用这些句子Transformer模型。
5. 上手使用Sentence Transformers
开始使用句子转换器最快捷简便的方式是通过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
Output:
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个token,以及是否将输入转换为小写(本例中,模型不进行小写转换)。我们还可以看到模型类别为BertModel。
池化(Pooling)操作,这里显示我们正在生成一个768维的句子嵌入,采用的是平均池化(mean pooling)方法。
一旦我们有了模型,就可以通过使用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
Output:
(5, 768)
6. 选择哪种嵌入模型?
然而,我们很快会发现,当前多数采用的嵌入模型属于Transformer类别。这些模型由不同的供应商提供,有些是开源的,有些则是专有的,每一款都针对特定目标进行了优化:
有些特别适合编码任务。
另一些则专门为英语设计。
还有能出色处理多语言数据集的嵌入模型。
最直接的方法是利用现有的学术基准。但重要的是要意识到,这些基准可能无法全面反映出AI应用中检索系统的真实世界应用场景。
作为替代方案,你可以尝试多种嵌入模型,并编制最终的评估表,以确定最适合你特定用例的模型。我强烈建议在此过程中加入重排序器(re-ranker),因为它能显著提升检索器的性能,最终达到最佳效果。
为了简化决策过程,Hugging Face提供了出色的大规模文本嵌入基准(Massive Text Embedding Benchmark, MTEB)排行榜。这个资源全面展示了所有可用嵌入模型及其在不同指标上的得分。
HuggingFace MTEB如果你选择第二种方法,这里有一篇优秀的Medium博客文章,展示了如何利用LlamaIndex中的检索评估模块。这个资源能帮助你高效地评估并从初始模型列表中识别出最优的嵌入模型及重排序器组合。通过实践指南和案例分析,文章指导用户如何设置实验、收集性能指标,并最终挑选出最适合特定应用场景的模型配置,从而提升检索和问答系统的整体效能。
我确信你现在更有信心为您的RAG架构选择最合适的嵌入及重新排序模型了!
结论
本文综述了多种用于生成文本向量表示的嵌入模型,涵盖了从词袋模型、TF-IDF、Word2Vec、GloVe、FastText到ELMO、BERT等。深入剖析了BERT的架构及预训练方法,介绍了SBERT在高效生成句子嵌入方面的应用,并通过sentence-transformers库的实例操作加以说明。结论部分突出了选取合适嵌入模型的挑战,并推荐利用诸如Hugging Face的“大规模文本嵌入基准(MTEB)排行榜”等资源进行评估,以便更科学地作出决策。
—END—
英文原文:https://medium.com/@vipra_singh/building-llm-applications-sentence-transformers-part-3-a9e2529f99c1
请长按或扫描二维码关注本公众号
喜欢的话,请给我个在看吧!