解锁RAG应用:利用FastRAG中的Dense Retrievers优化开放域问答
检索增强生成(RAG)近年来在自然语言处理领域声量不小。说白了,它就是把信息检索和文本生成这两项能力嫁接在一起,核心目标很简单:当用户抛出一个问题时,系统先去检索相关的信息段落,再基于这些素材生成更靠谱、更对路的回答。

这项技术潜力巨大,尤其在开放域问答这块。开放域问答研究的是如何构建一个能理解并回答各类问题的系统,关键一步就是从海量的非结构化数据中把有用的信息捞出来。
早期的信息检索技术,很大程度上依赖于基于单词统计的稀疏表示方法。那时候,文档通常用词袋模型来表示,一篇文档和一个查询的相关性,基本就靠关键词有没有出现来判定。
评分函数,比如我们熟悉的BM25和TF-IDF,就是通过关键词在文档里出现的频次,以及这个关键词在整个语料库里的普遍程度,来给文档打分。实践中最常用的工具之一是Elasticsearch,它基于Lucene搜索引擎,结合了单词统计和编辑距离(一种基于语法的方法)来做文本搜索。
下面这段代码展示了怎么用Python的Elasticsearch库执行基于TF-IDF和BM25的查询:
from elasticsearch import Elasticsearch
es = Elasticsearch()
index_name = "example_index"
query = "sample query"
bm25_query = {
"query": {
"match": {
"content": {
"query": query,
"operator": "and"
}
}
}
}
bm25_response = es.search(index=index_name, body=bm25_query)
print("BM25 Query Results:")
for hit in bm25_response['hits']['hits']:
print(hit['_source'])
tfidf_query = {
"query": {
"match": {
"content": {
"query": query,
"operator": "and"
}
}
},
"similarity": {
"default": {
"type": "classic"
}
}
}
tfidf_response = es.search(index=index_name, body=tfidf_query)
print("TF-IDF Query Results:")
for hit in tfidf_response['hits']['hits']:
print(hit['_source'])
不过,随着机器学习技术的发展,信息检索领域也迎来了一场变革,大家纷纷转向更强大的密集嵌入表示。这些嵌入技术能精准捕捉单词和短语的深层语义,实现对内容更深层次的理解。
相比于传统的统计方法,密集表示在匹配查询和文档时准确度更高,因为它能揭示那些微妙的语义相似性——恰恰是这些相似性,传统方法往往难以触及。
可以说,从稀疏到密集的这一跃迁,不仅大幅提升了检索系统的整体性能,也让检索结果的精度上了一个台阶。
如果对神经信息检索(Neural IR)感兴趣,可以查阅Mitra和Craswell在2018年的工作;想深入了解信息检索与问答的结合,Abbasiantaeb和Momtazi在2021年的综述文章也是很好的起点。
本文的重点是密集检索,这里介绍两种最具代表性的密集文档检索模型:密集段落检索(DPR,Karpukhin等人,2020)和基于BERT的情境化后期交互(ColBERT,Khattab和Zaharia,2020)。两者都依赖语义搜索来找到相关文档。
所谓语义搜索,就是用文本的密集表示来度量查询和文档之间的相似性。不过,这两种模型在如何存储文档向量、如何计算相似度上,思路截然不同。
我们将在一个公认的基准数据集——自然问题(NQ,Kwiatkowski等人,2019)上,从准确性和延迟两个维度对它们进行比较。NQ是一个由用户提交的问题组成的集合,这些问题的答案可以在维基百科文章中找到。
我们使用的工具是FastRAG——一个由英特尔实验室开发并最近开源的框架。它的目标是加速RAG应用的研发,也就是把检索和生成结合起来,用于问答、摘要、对话系统、内容创作等任务,同时利用外部的知识库来锚定大语言模型的输出。
在FastRAG中,应用程序被抽象为一个管道,通常由知识库、检索器、排序器和阅读器(通常是一个LLM)构成。阅读器负责“阅读”查询和检索到的文档,并生成输出。开发者可以自由尝试不同的架构和模型,并对性能和延迟进行基准测试。值得一提的是,FastRAG中提供的几种模型针对英特尔硬件做了优化,能在保持相当精度的前提下,实现更低的延迟。
近期的研究推动了基于Transformer的编码器模型在检索中的应用。这些模型的优势很明显:知识库中的文档被转换成向量并存储在索引里。
查询时,查询内容被编码成同一个向量空间中的向量,然后通过向量相似性搜索来召回最相关的文档。这个过程不仅可以高效地筛选文档,还能用来对召回结果进行重排,进一步优化顺序。这样一来,在运行时就能实现更精准、更实时的检索。
在密集检索领域,主要有几种不同的方法。一种是用单个token的嵌入来代表整个文档。DPR就是这种思路的典型代表。
在DPR中,编码器被训练成把整个文档的内容“总结”到第一个token(通常是[CLS])的嵌入里。这属于双编码器架构,因为它用了两个独立的编码器:一个处理查询,一个处理文档。查询和文档各自被编码成向量,然后进行匹配。
另一种方法是后期交互(late interaction),由ColBERT首次提出(Khattab和Zaharia,2020)。它的想法是保存(并索引)文档中所有单词的编码向量。在运行时,查询向量会跟所有文档单词的向量逐一比较——所以才叫“后期”交互。这种方法通常能检索到比DPR更相关的文档。不过,为每个token都建立索引(而不是每个文档只用一个token),索引大小自然就上去了。
后续的改进版本,ColBERT v2和PLAID(Santhanam等人,2022),通过两种方式大幅压缩了索引大小、降低了延迟:一是对索引中的向量进行量化和压缩,二是引入了一套启发式方法,用K-means对向量聚类,然后基于簇的质心,分层地为查询选择相关文档的token。使用PLAID索引的ColBERT v2,在保持低延迟的同时实现了顶尖的检索性能,延迟水平已经接近BM25、Lucene等稀疏检索方法,但精度却高出一大截。
在原代码块的位置,我们来看如何在FastRAG中使用ColBERT。首先,当然得安装FastRAG:
pip install fastrag
接下来,初始化文档存储:
from fastrag.stores import PLAIDDocumentStore
document_store = PLAIDDocumentStore(
index_path="data-index/",
checkpoint_path="ColBERT-model/",
collection_path="my-documents.tsv")
然后,基于这个文档存储定义一个检索器:
from fastrag.retrievers.colbert import ColBERTRetriever
retriever = ColBERTRetriever(document_store=document_store)
我们可以用Haystack的管道API来连接各个组件。下面这个例子只包含检索器:
from haystack import Pipeline
p = Pipeline()
p.add_node(component=retriever, name="Retriever", inputs=["Query"])
最后,执行查询:
p.run(query, params={"Retriever": {"top_k": k}})
结果是一个哈希映射,包含文档键和对应的结果列表,每个文档都带有相关性分数。
-
- 关于宇宙的好的网名有哪些
- 角色扮演 | 1
- 网名