系列5中讲到会讲解3个方面RAG的提升,它们可能与RAG的准确率有关系,但是更多的它们是有其它用途。本期来讲解第三部分:高级阶段。之所以说是高级阶段,可能是不好归一,而且实现起来相对于前面来说可能更为复杂。
目录
- 1 重排(Re-ranking)
- 1.1 RRF算法
- 1.2 Cohere Rerank模型
- 1.3 BGE-reranker模型
- 2 self-RAG
- 3 CRAG
- 4 总结
1 重排(Re-ranking)
在系列4中讲到问题优化的方法RAG-Fusion,里面提到了它与Multi-Query最后不同就是使用RRF对结果进行重排。其实重排是一种提高RAG问题准确率很好的方法,但是之所以没有放入RAG优化,而放入RAG提升,主要考虑2方面,其一是它其实算是一套流程中一个中间操作,在问题优化方法中也有提到过,不好在RAG优化单独拎出来;其二是它也并非只是简单使用RRF算法重排那么简单。这里就是使用一个大篇幅来讲一下重排。(注意:重排技术与问题优化等提高RAG准确度并不绑定,很多技术都是可以合着一起使用,效果会更好)
1.1 RRF算法
在系列4中讲到问题优化的方法RAG-Fusion中已经给出这个算法的代码,但是没有细说这个算法原理。这里来个大家举例说明一下。
RRF全称是Reciprocal Rank Fusion,也称为倒数排名融合。其实这个算法在日常生活中经常见到,比如足球、篮球等运动在评选MVP或者年度最佳球员时,经常会使用投票,这个投票一般会是一个排序,就是你排出你认为是MVP的三个人,注意:这里列出3个人是要排序,谁第一,谁第二,谁第三。然后再利用公式:1 / ( k + rank)。来计算每个人最终分数,然后通过最终分数排名。其中rank代表该球员在每张票的排名,k是一个常数。我们下面举个例子:
假如有3个人对本年度金球奖进行投票
A教练的投票结果:1.卡卡 2.C罗 3.梅西
B教练的投票结果:1.卡卡 2.梅西 3.C罗
C教练的投票结果:1.C罗 2.卡卡 3.梅西
假如k=0,那么:
卡卡的得分就是:1/1+1/1+1/2=2.5
C罗的得分就是:1/2+1/3+1/1=1.83
梅西的得分就是:1/3+1/2+1/3=1.17
因此最终排名就是:卡卡第一名、C罗第二名、梅西第三名。
代码实现如下:
# 定义RRF算法函数,代码来自:https://github.com/langchain-ai/langchain/blob/master/cookbook/rag_fusion.ipynb
def reciprocal_rank_fusion(results: list[list], k=60):
fused_scores = {}
for docs in results:
# Assumes the docs are returned in sorted order of relevance
for rank, doc in enumerate(docs):
doc_str = dumps(doc)
if doc_str not in fused_scores:
fused_scores[doc_str] = 0
# previous_score = fused_scores[doc_str]
fused_scores[doc_str] += 1 / (rank + k)
reranked_results = [
(loads(doc), score)
for doc, score in sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
]
return reranked_results
1.2 Cohere Rerank模型
Cohere Rerank是一个商业闭源的Rerank模型。它根据与指定查询问题的语义相关性对多个文本输入进行排序,专门用于帮助关键词或向量搜索返回的结果做重新排序与提升质量。它是一个在线模型,也就是你无法在本地部署,使用步骤:
- 在其官方网站注册一个API KEY
- 使用其客户端或者使用langchain第三方组件进行访问
下面就以langchain框架实现一个demo
前置条件:
- 申请一个Cohere的API KEY
- 下载m3e-base的embedding模型
- 准备一些文档作为查询使用
import os
import getpass
from langchain_community.llms import Cohere
from langchain.chains import RetrievalQA
from langchain_cohere import CohereRerank
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores.chroma import Chroma
from langchain_community.document_loaders import DirectoryLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
# 前置工作1:设置COHERE的APIKEY
os.environ["COHERE_API_KEY"] = getpass.getpass("Cohere API Key:")
# 前置工作2:文档存储,如果已经存储了文档,则不需要该步骤
encode_kwargs = {"normalize_embeddings": False}
model_kwargs = {"device": "cuda:0"}
embeddings = HuggingFaceEmbeddings(
model_name='/root/autodl-tmp/model/AI-ModelScope/m3e-base', # 换成自己的embedding模型路径
model_kwargs=model_kwargs,
encode_kwargs=encode_kwargs
)
if os.path.exists('VectorStore'):
db = Chroma(persist_directory='VectorStore', embedding_function=embeddings)
loader = DirectoryLoader("/root/autodl-tmp/doc") # 换成自己的文档路径
documents = loader.load()
text_spliter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
documents = text_spliter.split_documents(documents)
database = Chroma.from_documents(documents, embeddings, persist_directory="VectorStore")
database.persist()
# 第一步:初始化Cohere模型
llm = Cohere(temperature=0)
compressor = CohereRerank()
# 第二步:使用ContextualCompressionRetriever包装检索器
compression_retriever = ContextualCompressionRetriever(
base_compressor=compressor, base_retriever=database.as_retriever(search_kwargs={"k": 20})
)
# 第三步:使用RetrievalQA调用chain
chain = RetrievalQA.from_chain_type(
llm=Cohere(temperature=0), retriever=compression_retriever
)
query = "ChatGLM是什么?"
chain({"query": query})
1.3 BGE-reranker模型
cohere毕竟是闭源的,无法本地部署,因此国内推出了开源项目BGE-reranker模型。BGE-reranker模型是北京智源人工智能研究院(BAAI)推出的一种重排序模型,主要用于优化信息检索系统的性能,底层是基于BAAI General Embedding模型扩展混合检索能力。目前该已经开源,推出bge-reranker-base、bge-reranker-large等版本,在hugging face或者魔塔均可下载。
下面借助FlagEmbedding(一款用于微调/评估文本嵌入模型的工具)和FastAPI可以将BGE-reranker模型封装为API格式,提供和Cohere模型一样的效果。
前置条件:
- 下载BAAI/bge-reranker-base模型
- 安装FlagEmbedding和FastAPI工具
import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from operator import itemgetter
from FlagEmbedding import FlagReranker
# 定义FastAPI
app = FastAPI()
# 定义BGE-reranker模型
reranker = FlagReranker('你的BGE-reranker模型路径') # 修改为你模型文件路径
# 定义入参
class Query(BaseModel):
question: str
docs: list[str]
top_k: int = 1
# 提供访问入口
@ app.post('/bge_rerank')
def bge_rerank(query: Query):
scores = reranker.compute_score([[query.question, passage] for passage in query.docs])
if isinstance(scores, list):
similarity_dict = {passage: scores[i] for i, passage in enumerate(query.docs)}
else:
similarity_dict = {passage: scores for i, passage in enumerate(query.docs)}
sorted_similarity_dict = sorted(similarity_dict.items(), key=itemgetter(1), reverse=True)
result = {}
for j in range(query.top_k):
result[sorted_similarity_dict[j][0]] = sorted_similarity_dict[j][1]
return result
if __name__ == '__main__':
uvicorn.run(app, host='0.0.0.0', port=8000)
通过以上你就可以使用API 方式访问bge-reranker:
curl --location ‘http://localhost:50072/bge_base_rerank’
–header ‘Content-Type: application/json’
–data ‘{
“question”: “ChatGLM”,
“docs”: [“GLM3”, “Baichuan2”],
“top_k”: 2
}’
2 self-RAG
self-RAG是一种增强的RAG范式,其论文地址:https://arxiv.org/pdf/2310.11511。有兴趣的同学可以拜读,但是论文中更多的是底层和算法,如果研究应用的同学看起来会容易云里雾里。这里暂时摒弃数学公式,简单易懂给大家讲讲self-RAG的原理。
首先之所以提出self-RAG当然是RAG存在某些缺陷。RAG本身是借助外部知识,利用大模型推理得出答案,但是会存在几个问题
- 搜索知识过多,通过相似度检索很容易过多搜索出知识,哪怕进行top k的处理也还是很多且有可能不是真正相关,并且很多模型并不能支持过长的token数
- 大模型生成的结果,本身无法确保与搜出来的知识保持一致,哪怕你通过增加prompt,也难保本身搜出来的知识就存在错误
那么self-RAG如何解决这些问题?我们可以从下图看看self-RAG的工作流程
从上图,我们可以看到,self-RAG增加或者改进3方面的内容:
- 判断是否需要检索:可以解决搜索知识过多问题,因为有些问题其实不需要进行检索
- 搜索结果分别生成:这个可以解决大模型token不足的问题(相对于传统RAG是一起扔给大模型进行生成)
- 评估生成结果:这个可以解决生成结果与知识内容的一致性
如何做到以上3个点,self-RAG通过微调训练大模型,让大模型在推理过程中实现自我反省。为了让大模型具备自我反省,那么self-RAG在微调过程中,加入了有标志的“反省Token”,当大模型生成这些标志性的“反省Token”时,那么就要进行不同token的自我反省操作。如模型输出如下例子:
[Relevant] ChatGLM3 是北京智谱华章科技有限公司和清华大学 KEG 实验室联合发布的对话预训练模型。关于北京智谱华章科技有限公司[Retrieval]
我们能看到有2个特殊的token,分别是[Relevant]和[Retrieval]。当有这2个特别含义的token时,模型会根据这些token做出对应的反省操作,比如[Relevant]代表内容与问题相关,[Retrieval]代表这里需要进行搜索。至于如何评判是否需要检索以及评判生成的分数,Self-RAG共设计了四种类型的评判指标:
特殊token | 作用 | 取值 |
---|---|---|
Retrieve | 是否需要知识检索,表示LLM后续的内容生成是否需要做额外知识检索。 | [No Retrieval]:无需检索,LLM直接生成;[Retrieval]:需要检索;[Continue to Use Evidence]:无需检索,使用之前内容 |
IsRel | 知识相关性,表示检索出来的知识是否提供了解决问题所需的信息。 | [Relevant]:检索出来的知识与需要解决的问题足够相关;[Irrelevant]:检索出来的知识与需要解决的问题无关 |
IsSup | 响应支持度,表示生成的响应内容是否得到检索知识的足够支持。 | [Fully supported]:输出内容陈述被检索的知识完全支持;[Partially supported]:输出内容陈述只有部分被检索的知识所支持;[No support / Contradictory]:输出内容不被检索的知识所支持(即编造)。 |
IsUse | 响应有效性。表示生成的响应内容对于回答/解决输入问题是否有用 | [Utility : x]:按有效的程度x分成1-5分,即最高为[Utility:5] |
当然,这样的模型是需要特殊的训练,在Self-RAG的开源项目中提供了一个基于llama微调的模型selfrag_llama2_7b,希望将self-RAG应用于自身项目的同学,可以去看看这方面的内容。这就是self-RAG的流程原理,至于底层算法原理,有兴趣朋友也可以进行论文深入研读。
3 CRAG
在第二小节的self-RAG提出一种解决RAG痛点的流程框架,但是实际操作起来却是有些难度,特别是微调带有特殊token的模型,一说起微调就会是一个头疼的问题,不仅仅数据准备费时费力,并且微调后的模型稳定性也不一定能达到预期效果。那么有没有更为容易实现的简便方式呢,答案就是CRAG。
相比于传统的RAG,CRAG增加了一个用于矫正检索到的文档和用户问题之间知识相关性的可插拔模块。
从上图我们可以看到,CRAG就是增加了一个评估大模型,对结果进行评估。评估结果如下:
- Correct:表示文档与用户问题有一定相关性的,但是doc其中仍会存在噪音或者过大问题,因此对文档进行refine精简。这里会对文档进行叫做knowledge strips的操作,然后从中过滤出有用的 strip重新整合,最后得到矫正后的文档。
- Incorrect:表示文档对用户问题并没有相关性,需要引入新的知识源。这里引入 web searcher 从网上检索相关信息,并从中选出有用的内容,最后得到矫正后的文档。
- Ambiguous:表示评估大模型无法判断文档是否与用户问题有关,就会同时做 Correct 和 Incorrect 时的动作。
相对于self-RAG来说,CRAG强调它是一种可插拔的设计模式,其中只是增加评估大模型,这个模型可以是微调模型,也可以是当今最好的大模型,比如ChatGPT。这使得在实际应用中,可能更为容易,当然,还有一点就是refine精简,这也是CRAG论文的重要价值点之一。