【翻译注:本文章介绍了为什么要使用rerank模型,以及embedding模型和rerank的模型的特点,非常适合了解技术背后的原因。】
英语原文:www.pinecone.io/learn/serie…
检索增强生成(Retrieval Augmented Generation,RAG)似乎充满了无限可能。然而,许多人在构建RAG系统后,常常感到结果并不如预期。尽管RAG易于上手,但要真正掌握其精髓却颇有难度。实际上,建立一个有效的RAG系统不仅仅是将文档放入向量数据库并叠加一个大语言模型那么简单。这种方式有时有效,但并非铁板钉钉。
一、为什么需要重排序?
检索增强生成(Retrieval Augmented Generation,RAG)技术看似充满无限可能,但在实际应用中,许多人发现构建的RAG系统结果并不尽如人意。
尽管RAG相对容易入门,但要真正掌握其精髓却相当困难。实际上,建立一个有效的RAG系统远不止将文档存入向量数据库并叠加一个大语言模型那么简单。虽然这种方法有时会有效,但并非总能保证成功。
我们知道,RAG 通过在大量文本文档中进行语义搜索来工作,这些文档的数量可能达到数十亿。为了实现大规模搜索的快速响应,我们通常采用向量搜索技术。具体而言,就是将文本转化为向量后,放入一个向量空间内,再通过余弦相似度等度量标准来比较它们与查询向量的相似度。
向量搜索的前提是需要向量,这些向量通常将文本背后的意义压缩成768或1536维的形式,这一过程不可避免地会丢失一些信息。因此,我们常常会发现,即使是排名前三的文档,也可能遗漏了一些关键信息。
在此,我们关注的指标是召回率,即“我们检索到的相关文档的比例”。需要注意的是,召回率不考虑检索到的文档总数。
因此,理论上通过返回所有文档可以实现完美的召回率。然而,这在实际操作中是不可行的,因为大语言模型对可处理的文本量有限制,这个限制称为上下文窗口。
如果较低位置的文档包含了有助于大语言模型更好地形成回答的相关信息,该怎么办?一个简单的方法是增加返回的文档数量(即增加top_k值),并将它们全部传递给大语言模型。但是这样做有一定的条件:
第一,这样做的一个劣势,就是需要消耗更多的token,意味着成本的增加。
第二,尽管大模型拥有高达100K Token的巨大上下文窗口,理论上可以包含大量文档,但我们仍然不能返回所有文档并填满上下文窗口来提高召回率。
第三,当我们在上下文窗口中填充过多内容时,会降低大语言模型在该窗口中检索信息的能力。研究表明,当上下文窗口被过多Token填满时,大语言模型的回忆能力会受到影响。此外,过度填充上下文窗口还会使模型较难按指令执行,因此,这种做法是不可取的。
为了解决这一问题,我们可以通过检索尽可能多的文档来最大化检索召回率,然后通过尽量减少最终传递给大语言模型的文档数量。为此,我们重新排序检索到的文档,并只保留最相关的文档。
二、什么是重排序算法?
重排序模型(也称为Cross-Encoder)是一种能够针对查询和文档对输出相似度分数的模型。通过利用这些分数,我们可以根据文档与查询的相关性对它们进行重新排序。
一个包含两个阶段的检索系统通常在向量数据库(vector DB)阶段采用双编码器(bi-encoder,Bi-Encoder)或稀疏嵌入模型。搜索工程师长期以来在这种两阶段检索系统中使用重排序模型。第一阶段的模型(嵌入模型或检索器)负责从大数据集中提取一组相关文档。随后,第二阶段的模型(重排序器)对提取出的文档进行重新排序。
采用两阶段策略的原因在于,从大数据集中快速检索少量文档的速度远快于对大量文档进行重排序。简而言之,重排序器处理较慢,而检索器速度较快。我们将在后面详细解释其原因。
三、为何选择使用重排序器?
关键在于,重排序器的精确度远超过嵌入模型。
双编码器(bi-encoder)精度较低的根本原因在于,它必须将文档的所有潜在含义压缩成一个向量——这无疑导致了信息的丢失。此外,由于查询是在收到后才知道的,双编码器对查询的上下文一无所知(我们是在用户提出查询之前就已经创建了嵌入)。
而重排序器能够在大型Transformer中直接处理原始信息,这大大减少了信息丢失。由于重排序器是在用户提出查询时才运行,这让我们能够针对具体查询分析文档的含义,而非仅生成一个泛化的、平均化的含义。
重排序器避免了双编码器的信息丢失问题——但它也有代价,那就是时间。
双编码器模型将文档或查询的含义压缩成单一向量。值得注意的是,无论处理的是文档还是查询,双编码器的处理方式相同,都是在用户查询时进行。
使用双编码器和向量搜索时,所有繁重的Transformer计算都在创建初始向量时完成。这意味着,一旦用户发起查询,我们已经准备好了向量,接下来需要做的只是:
运行一个Transformer计算生成查询向量。
使用余弦相似度(或其他轻量度量)将查询向量与文档向量进行比较。
而对于重排序器,我们不进行任何预计算。相反,我们将查询和某个文档直接输入到Transformer中,进行完整的推理步骤,最终生成一个相似度分数。
重排序器通过一个完整的Transformer推理步骤,针对查询和单一文档生成一个相似度分数。请注意,这里的文档A实际上等同于我们的查询。
假设我们的系统有4000万条记录,使用像BERT这样的小型重排序模型在V100 GPU上运行,我们可能需要超过50小时来返回一个查询结果。而采用编码器模型和向量搜索,相同的查询结果可以在不到100毫秒的时间内完成。
四、重排序代码实现
现在我们了解了使用重新排序器进行两阶段检索背后的想法和原因,让我们看看如何实现它,首先,我们将设置我们的必备库:
!pip install -qU
datasets==2.14.5
openai==0.28.1
pinecone-client==2.2.4
cohere==4.27
在设置检索管道之前,我们需要检索数据!我们将使用 Hugging Face Datasets 中的 jamescalam/ai-arxiv-chunked 数据集。该数据集包含 400 多篇关于 ML、NLP 和 LLMs 的 ArXiv 论文,包括 Llama 2、GPTQ 和 GPT-4 论文。
关键代码如下:
from datasets import load_dataset
data = load_dataset("jamescalam/ai-arxiv-chunked", split="train")
data
import time
index_name = "rerankers"
existing_indexes = [
index_info["name"] for index_info in pc.list_indexes()
]
# check if index already exists (it shouldn't if this is first time)
if index_name not in existing_indexes:
# if does not exist, create index
pc.create_index(
index_name,
dimension=1536, # dimensionality of ada 002
metric='dotproduct',
spec=spec
)
# wait for index to be initialized
while not pc.describe_index(index_name).status['ready']:
time.sleep(1)
# connect to index
index = pc.Index(index_name)
time.sleep(1)
# view index stats
index.describe_index_stats()
def compare(query: str, top_k: int, top_n: int):
# first get vec search results
docs = get_docs(query, top_k=top_k)
i2doc = {docs[doc]: doc for doc in docs.keys()}
# rerank
rerank_docs = co.rerank(
query=query, documents=docs.keys(), top_n=top_n, model="rerank-english-v2.0"
)
original_docs = []
reranked_docs = []
# compare order change
for i, doc in enumerate(rerank_docs):
rerank_i = docs[doc.document["text"]]
print(str(i)+"\t->\t"+str(rerank_i))
if i != rerank_i:
reranked_docs.append(f"[{rerank_i}]\n"+doc.document["text"])
original_docs.append(f"[{i}]\n"+i2doc[i])
for orig, rerank in zip(original_docs, reranked_docs):
print("ORIGINAL:\n"+orig+"\n\nRERANKED:\n"+rerank+"\n\n---\n")
完整的代码:github.com/pinecone-io…
重新排名后,我们获得了更多相关信息。当然,这可以显着提高 RAG 的性能。这意味着我们最大化相关信息,同时最小化输入 LLM 的噪音。
重新排序是显着提高检索增强生成 (RAG) 或任何其他基于检索的管道中的召回性能的最简单方法之一。
五、Embeding Model的局限性
Embedding Model(嵌入模型)的局限性主要集中在信息表示、上下文依赖、动态交互以及计算资源等方面。以下是其核心局限性和详细分析:
1. 信息压缩导致语义丢失
Embedding Model将查询和文档的语义压缩成一个固定大小的向量(如768维)。在此过程中,存在以下问题:
- 语义信息丢失:复杂的文档语义或多义词的上下文含义可能无法完全表达。
- 多义性处理困难:当文档或查询具有多种潜在含义时,单一向量无法保留所有细节。
- 语义平均化:长文档的关键点可能被稀释,无法突出最相关的部分。
示例 文档:“Paris is the capital of France. It has a population of over 2 million people.”
- 查询1:“What is the capital of France?” -> 匹配正确
- 查询2:“What is the population of Paris?” -> 信息可能被平均化,难以准确匹配
2. 缺乏查询和文档之间的动态交互
- Embedding Model独立编码查询和文档,这意味着它们之间没有直接的语义交互。查询的语义无法影响文档的表示,文档的语义也无法根据查询动态调整。
- 在依赖查询上下文的场景中,这种交互的缺失会导致匹配错误。
示例
- 查询:“What is the population of France?”
- 文档1:“France has a population of 67 million people.” -> 应匹配
- 文档2:“France is famous for its wine.” -> 不相关 Embedding Model可能会因为“France”这一通用语义相似性匹配错误。
3. 长文档的处理困难
- 嵌入模型通常对输入长度有限制(例如 BERT 的限制为 512 个词)。对于超长文档,需要截断或进行分块处理。
- 截断:可能丢失关键信息。
- 平均化:将文档分块并取平均会稀释关键语义,降低匹配精度。
示例 文档:包含多个段落,只有一个段落与查询相关。Embedding Model可能无法识别相关段落的重要性。
4. 难以处理复杂的语义关系
-
Embedding Model通常依赖简单的向量相似性(如余弦相似度)度量查询和文档的关系。
-
它擅长表达语义相似性,但难以捕捉复杂的语义关系,例如:
- 同义替换(“created” vs. “painted”)
- 推理关系(“X 是 Y 的发明者” vs. “Y 是由 X 发明的”)
示例
- 查询:“Who painted the Mona Lisa?”
- 文档:“Leonardo da Vinci created the famous portrait Mona Lisa.” Embedding Model可能无法捕捉到“painted”和“created”的语义关系,导致匹配失败。
5. 对细粒度语义的捕捉能力有限
- 嵌入模型通常是全局语义匹配,难以捕捉查询和文档之间的细节匹配,例如词级匹配。
- 在查询需要精确答案(如问答系统)时,其表现往往不如细粒度匹配的模型。
示例
- 查询:“What year was the iPhone first released?”
- 文档:“The iPhone was first introduced by Apple in 2007.” Embedding Model可能因为语义过于泛化,而忽略了具体的时间“2007”。
6. 领域适配能力受限
- Embedding Model通常在通用语料上训练,难以适应特定领域(如医学、法律)的专有语义需求。
- 在新领域或稀有语料中,嵌入模型可能无法正确表示特定词汇或概念。
解决方案:需要对领域语料进行微调,但这增加了额外的训练成本。
7. 多义性和上下文依赖问题
- 嵌入模型在生成向量时,未考虑具体的查询语境,可能导致误匹配。
- 单一向量表示无法动态调整,以适应查询上下文的不同需求。
示例 文档:“The bank provides financial services.” 查询1:“What are the services offered by banks?” -> 匹配正确 查询2:“Where can I find a riverbank?” -> 匹配错误,因为“bank”有多个含义。
8. 偏向语义相似性,忽略用户意图
- Embedding Model更关注语义相似性,而非用户查询的实际意图。
- 这在需要显式回答问题或高精度匹配的任务中表现较差。
示例
- 查询:“Best budget laptops in 2024.”
- 文档:“Laptops with great features at an affordable price.” 嵌入模型可能会匹配这段文档,但用户可能希望得到具体的品牌和型号。
9. 缺乏透明性和可解释性
- 嵌入模型生成的向量是高维的、不可解释的数值表示。
- 在实际应用中,难以理解模型为什么选择某个文档作为查询的匹配结果。
总结
Embedding Model 的局限性可以概括为以下几个方面:
- 信息压缩:丢失复杂语义,尤其是长文档或多义词语境下的信息。
- 交互缺失:查询和文档无法动态影响对方的表示。
- 细粒度匹配困难:难以捕捉词级、句级或精确语义关系。
- 上下文依赖不足:多义性处理和领域适配能力受限。
这些局限性决定了Embedding Model适用于快速召回的第一阶段检索,而高精度匹配任务则需结合其他模型(如 Cross-Encoder)来弥补不足。
六、为什么 Cross-Encoder 的精度更高
Cross-Encoder 的精度更高,主要得益于它在计算过程中直接捕捉了查询和文档之间的交互关系,而这种交互在 Bi-Encoder 中是无法实现的。以下从多个角度分析其精度更高的原因:
1. 查询和文档的联合编码
-
Cross-Encoder 的方式:将查询和文档作为一个整体输入到模型中,模型会同时看到两者的内容,通过 Transformer 层捕获两者之间的细粒度语义关系。例如:
Input:[CLS]Query Tokens[SEP]Document Tokens[SEP]\text{Input}: [\text{CLS}] \text{Query Tokens} [\text{SEP}] \text{Document Tokens} [\text{SEP}]Input:[CLS]Query Tokens[SEP]Document Tokens[SEP]
- Transformer 的自注意力机制(Self-Attention)允许模型分析查询中的每个词与文档中每个词之间的相关性。
- 通过这种全局交互,模型能够更精确地理解查询与文档的语义匹配。
-
Bi-Encoder 的方式:分别独立对查询和文档编码为向量。这种独立编码导致查询和文档之间缺乏交互:
Query Encoding:fquery(Query)\text{Query Encoding}: f_\text{query}(\text{Query})Query Encoding:fquery(Query)
Document Encoding:fdoc(Document)\text{Document Encoding}: f_\text{doc}(\text{Document})Document Encoding:fdoc(Document)
- 结果是查询和文档的关系仅通过它们的向量相似度来衡量(如余弦相似度),而非基于它们的具体内容进行分析。
2. 细粒度的词级交互
Cross-Encoder 能够直接捕获查询和文档在词级别的相关性:
-
查询中的每个词可以与文档中的每个词进行关联。例如,对于问题
“What is the capital of France?”
和文档
“Paris is the capital city of France.”:
- Cross-Encoder 可以直接注意到“capital”和“capital city”的相关性。
- Bi-Encoder 则需要将这些信息压缩到低维向量中,可能丢失这种细粒度的匹配信息。
这使得 Cross-Encoder 在处理复杂语言现象(如同义关系、语序依赖等)时更有优势。
3. 利用上下文信息
在 Cross-Encoder 中,查询和文档的含义可以动态调整:
-
查询的词义可以因文档内容而变化。例如:
- 查询 “bank” 在看到文档内容涉及“money”时会被模型理解为“金融机构”,而在涉及“river”时会被理解为“河岸”。
- Cross-Encoder 可以灵活调整匹配策略,而 Bi-Encoder 中查询和文档是独立编码的,无法利用这种上下文信息。
4. 避免信息压缩的局限
Bi-Encoder 的一个关键问题是信息压缩:
- 每个文档的语义必须被压缩为一个固定大小的向量(例如 768 维)。
- 这个向量需要同时表示文档的所有潜在含义(无论是关于金融、地理还是其他主题),无法针对查询动态调整。
- Cross-Encoder 避免了这种局限,因为它直接对查询和文档的原始内容进行联合处理。
5. 学习到的匹配函数更复杂
- Bi-Encoder 使用简单的相似性度量(如余弦相似度)比较向量,无法捕捉更复杂的非线性关系。
- Cross-Encoder 中,Transformer 模型充当了一个复杂的匹配函数,能够学习任意复杂的查询-文档关系: Relevance Score=fCross-Encoder(Query,Document)\text{Relevance Score} = f_\text{Cross-Encoder}(\text{Query}, \text{Document})Relevance Score=fCross-Encoder(Query,Document) 这种灵活性使得 Cross-Encoder 能够更精确地判断相关性。
6. 实验结果支持
在多个自然语言处理任务(如问答、搜索排序)中,实验表明:
- Cross-Encoder 的结果优于 Bi-Encoder。例如,在排名任务中,Cross-Encoder 常常能够显著提升 NDCG(归一化折损累计增益)等评估指标。
7. 代价:效率问题
尽管 Cross-Encoder 的精度高,但它的计算效率低是不可忽视的:
- 推理复杂度:需要为每对查询和候选文档运行一次完整的 Transformer 推理。
- 时间开销:每次查询需要重新处理文档内容,导致时间成本显著增加。
因此,在实际应用中,Cross-Encoder 通常用于二阶段排序,而不是大规模初筛。
总结
Cross-Encoder 的精度更高,主要因为:
- 它直接对查询和文档进行联合编码,捕捉细粒度语义关系。
- 它能动态调整查询和文档的匹配,充分利用上下文信息。
- 它避免了 Bi-Encoder 的信息压缩问题,学习到更复杂的匹配函数。
但由于计算代价高,两阶段检索系统通常结合 Bi-Encoder 和 Cross-Encoder,以兼顾效率与效果。
八、Embeding和rerank 对比分析
1. 双编码器(Bi-Encoder)的特点
核心原理
- 独立嵌入生成:双编码器的设计基于两个独立的编码器,一个用于生成查询向量,另一个用于生成文档向量。
- 查询和文档解耦:查询和文档的嵌入可以分开预计算,大大提升了效率。
优点
-
高效检索:
- 文档嵌入提前计算并存储,查询时只需对比嵌入向量(如通过余弦相似度),速度非常快。
- 适用于大规模检索场景(如上亿条记录)。
-
计算可扩展性:
- 通过向量数据库(如 FAISS、Milvus)和 ANN(近似最近邻)算法进一步加速。
局限性
-
信息压缩的损失:
- 将文档压缩为单一向量会导致复杂语义关系的丢失,尤其是需要结合查询上下文的细粒度信息。
- 适合泛化语义检索,但不擅长处理查询与文档的深层语义匹配。
-
查询与文档缺乏交互:
- 查询和文档是分开编码的,无法实时利用查询的信息优化文档表示。
2. 重排序器(Cross-Encoder)的特点
核心原理
-
查询与文档联合处理:
- 查询和文档在同一个 Transformer 模型中联合输入,进行深度语义匹配。
- 输出的相似度分数基于 Transformer 模型分析两者之间的直接交互关系。
优点
-
高精度匹配:
- 查询和文档之间的所有上下文关系都可以被模型捕获,避免了双编码器中可能的语义信息丢失。
-
细粒度语义分析:
- 适合复杂查询(如问答系统)或需要精确匹配的任务(如文档重排序)。
局限性
-
计算成本高:
- 每个查询-文档对都需要运行一次完整的 Transformer 推理,随着候选文档数量的增加,计算成本呈线性增长。
- 不适用于直接处理大规模数据。
-
实时性受限:
- 查询发起后才能运行,不适合低延迟需求的场景。