一、為什么要重排序?

       重新排序是檢索過程中的一個步驟,根據某些標準對最初檢索到的結果進行進一步排序、細化或重新排序,以提高其相關性或準確性。

       這里有一個例子:想象一下,你正在搜索有關企鵝的信息。當你輸入搜索查詢時,系統會很快彈出幾篇關于不同類型企鵝的文章。這些結果是基于通用相關性檢索獲得的。

       現在,假設前幾篇文章是關于“南極企鵝”的,但你真正想要的是關于“動物園棲息地的企鵝”的信息,那么就需要對這幾篇文章進行重新排序了,比如使用用戶行為、特定關鍵字或更復雜的算法來進行該操作。這可能會使有關動物園棲息地的文章在列表中排名更高,降低有關南極洲的文章的排名。這一步驟確保最相關或最有用的信息出現在頂部,使您更容易找到您要查找的內容。

       本質上,重新排序微調了最初檢索到的結果,基于特定標準或用戶偏好提供了一組更具針對性和相關性的文檔或信息。

二、基于嵌入的檢索有什么問題?

使用基于嵌入的檢索有許多優點:

  1. 使用向量點積快速計算,并且在查詢推理期間不需要調用任何模型;
  2. 嵌入可以對文檔和查詢進行語義編碼,并可以根據查詢檢索出高度相關的文檔。

? ? ? ?然而,盡管有這些優點,基于嵌入的檢索有時準確性不高,并返回與查詢相關的無關上下文,這會大大降低RAG系統的整體質量。

? ? ? ?針對上述問題,可以借鑒推薦系統召回和排序兩個階段來解決。第一階段基于嵌入召回出相關性最高的top-k個文檔,該階段注重召回而非準確性。第二階段是對召回的top-k個文檔進行重新排序。

三、代碼實現

模型zephyr-7b-alpha

嵌入模型:hkunlp/instructor-large

3.1 加載數據

? ? ? ?LlamaIndex通過數據連接器Reader來加載數據,數據連接器可以加載不同數據源的數據,并將數據格式化為Document對象,Document對象會存儲文本和對應的元數據(未來會存儲圖像和音頻)。

PDFReader = download_loader("PDFReader")
loader = PDFReader()
docs = loader.load_data(file=Path("QLoRa.pdf"))

3.2 分塊

? ? ?我們將文本分割成512大小的分塊來創建節點Node。Node是LlamaIndex中的原子數據單元,表示源文檔的“塊”。節點包含元數據以及與其他節點的關系信息。

node_parser = SimpleNodeParser.from_defaults(chunk_size=512)
nodes = node_parser.get_nodes_from_documents(docs)

3.3 開源LLM和嵌入

       我們將使用開源大模型zephyr-7b-alpha,并對其進行量化,量化后的模型可以運行在Colab免費的T4 GPU上。

? ? ? ?在本例中,我們將使用hkunlp/instructor-large指令微調的文本嵌入模型,它可以通過簡單地提供任務指令來生成針對任何任務(例如,分類、檢索、聚類、文本評估等)定制的文本嵌入,而無需任何微調。在MTEB排行榜(https://huggingface.co/spaces/mteb/leaderboard)上排名第14!

from google.colab import userdata

# huggingface and cohere api token
hf_token = userdata.get('hf_token')

quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
)

def messages_to_prompt(messages):
prompt = ""
for message in messages:
if message.role == 'system':
prompt += f"<|system|>\n{message.content}</s>\n"
elif message.role == 'user':
prompt += f"<|user|>\n{message.content}</s>\n"
elif message.role == 'assistant':
prompt += f"<|assistant|>\n{message.content}</s>\n"

# ensure we start with a system prompt, insert blank if needed
if not prompt.startswith("<|system|>\n"):
prompt = "<|system|>\n</s>\n" + prompt

# add final assistant prompt
prompt = prompt + "<|assistant|>\n"

return prompt

# LLM
llm = HuggingFaceLLM(
model_name="HuggingFaceH4/zephyr-7b-alpha",
tokenizer_name="HuggingFaceH4/zephyr-7b-alpha",
query_wrapper_prompt=PromptTemplate("<|system|>\n</s>\n<|user|>\n{query_str}</s>\n<|assistant|>\n"),
context_window=3900,
max_new_tokens=256,
model_kwargs={"quantization_config": quantization_config},
# tokenizer_kwargs={},
generate_kwargs={"temperature": 0.7, "top_k": 50, "top_p": 0.95},
messages_to_prompt=messages_to_prompt,
device_map="auto",
)

# Embedding
embed_model = HuggingFaceInstructEmbeddings(
model_name="hkunlp/instructor-large", model_kwargs={"device": DEVICE}
)

3.4 配置索引和檢索器

      首先,我們將設置ServiceContext對象,并使用它來構造索引和查詢。

索引是一種由Document對象組成的數據結構,大模型可以使用該索引進行查詢相關上下文。

? ? ? ?VectorStoreIndex是最常見和最易于使用的索引,支持在大規模數據語料庫上進行檢索,包括獲取“top-k”個最相似的節點,并將它們傳遞到“響應合成”模塊。

# ServiceContext
service_context = ServiceContext.from_defaults(llm=llm,
embed_model=embed_model
)

# index
vector_index = VectorStoreIndex(
nodes, service_context=service_context
)

# configure retriever
retriever = VectorIndexRetriever(
index=vector_index,
similarity_top_k=10,
service_context=service_context)

3.5 初始化重排序器

我們會評估以下3個重新排序器的性能:

# Define all embeddings and rerankers
RERANKERS = {
"WithoutReranker": "None",
"CohereRerank": CohereRerank(api_key=cohere_api_key, top_n=5),
"bge-reranker-base": SentenceTransformerRerank(model="BAAI/bge-reranker-base", top_n=5),
"bge-reranker-large": SentenceTransformerRerank(model="BAAI/bge-reranker-large", top_n=5)
}

3.6 檢索比較

       檢索器定義了在給定查詢時如何有效地從索引中檢索相關上下文。

       節點后處理器:節點后處理器接收一組檢索到的節點,并對它們進行轉換、過濾或重新排序操作。節點后處理器通常在節點檢索步驟之后和響應之前應用于查詢引擎中。

? ? ? ?讓我們創建一些助手函數來執行我們的任務:

# helper functions

def get_retrieved_nodes(
query_str, reranker
):
query_bundle = QueryBundle(query_str)

retrieved_nodes = retriever.retrieve(query_bundle)

if reranker != "None":
retrieved_nodes = reranker.postprocess_nodes(retrieved_nodes, query_bundle)
else:
retrieved_nodes

return retrieved_nodes

def pretty_print(df):
return display(HTML(df.to_html().replace("\\n", "<br>")))

def visualize_retrieved_nodes(nodes) -> None:
result_dicts = []
for node in nodes:
node = deepcopy(node)
node.node.metadata = None
node_text = node.node.get_text()
node_text = node_text.replace("\n", " ")

result_dict = {"Score": node.score, "Text": node_text}
result_dicts.append(result_dict)

pretty_print(pd.DataFrame(result_dicts))

讓我們可視化查詢的結果:

query_str = "What are the top features of QLoRA?"

# Loop over rerankers
for rerank_name, reranker in RERANKERS.items():
print(f"Running Evaluation for Reranker: {rerank_name}")

query_bundle = QueryBundle(query_str)

retrieved_nodes = retriever.retrieve(query_bundle)

if reranker != "None":
retrieved_nodes = reranker.postprocess_nodes(retrieved_nodes, query_bundle)
else:
retrieved_nodes

print(f"Visualize Retrieved Nodes for Reranker: {rerank_name}")
visualize_retrieved_nodes(retrieved_nodes)

沒有重新排序–頂部節點的相似性得分為0.87,這是baseline分數。

CohereRebank——頂部節點的相似性得分為0.988。

bge reranker base——頂部節點的相似性得分為0.72。

四、性能評價

       現在,我們將使用RetrieverEvaluator來評估我們的Retriever的質量。

       我們定義了各種評估指標,如命中率hit-rateMRR,這些指標根據每個特定問題的ground-truth文本來評估檢索結果的質量。為了簡化評估數據集的構造,我們采用了合成數據生成方法。

       MRR( Mean Reciprocal Rank)表示平均倒數排名,是一個國際上通用的對搜索算法進行評價的機制,并且只評估排名前10個的排名感知相關性得分。第一個結果匹配,分數為1,第二個匹配分數為0.5,第n個匹配分數為1/n,如果沒有匹配的句子分數為0。最終的分數為所有得分之和。

        hit-rate衡量檢索到的結果中至少包含一個與基本事實相關的項目的查詢的比例或百分比。例如,想象一個搜索引擎返回一個文檔列表來響應用戶的查詢。這里的基本事實是指該查詢的已知相關文檔,通常由人類判斷或標記數據確定。hit-rate計算搜索結果至少包含一個相關文檔的頻率。

4.1 構建(查詢、上下文)對的評估數據集

      我們可以手動構建問題+Node id的檢索評估數據集。Llamaindex通過generate_question_context_pairs函數在現有文本語料庫上提供合成數據集生成。使用大模型根據每個上下文塊自動生成問題,可以參閱:https://docs.llamaindex.ai/en/stable/module_guides/evaluating/usage_pattern_retrieval.html。

? ? ? ?這里,我們使用Zephr-7B在現有的文本語料庫上構建一個簡單的評估數據集,返回的結果是一個EmbeddingQAFinetuneDataset對象(包含queries、relevant_docs和corpus)。

# Prompt to generate questions
qa_generate_prompt_tmpl = """\
Context information is below.

---------------------
{context_str}
---------------------

Given the context information and not prior knowledge.
generate only questions based on the below query.

You are a Professor. Your task is to setup \
{num_questions_per_chunk} questions for an upcoming \
quiz/examination. The questions should be diverse in nature \
across the document. The questions should not contain options, not start with Q1/ Q2. \
Restrict the questions to the context information provided.\
"""

# Evaluator

qa_dataset = generate_question_context_pairs(
nodes, llm=llm, num_questions_per_chunk=2, qa_generate_prompt_tmpl=qa_generate_prompt_tmpl
)
# helper function for displaying results
def display_results(reranker_name, eval_results):
"""Display results from evaluate."""

metric_dicts = []
for eval_result in eval_results:
metric_dict = eval_result.metric_vals_dict
metric_dicts.append(metric_dict)

full_df = pd.DataFrame(metric_dicts)

hit_rate = full_df["hit_rate"].mean()
mrr = full_df["mrr"].mean()

metric_df = pd.DataFrame({"Reranker": [reranker_name], "hit_rate": [hit_rate], "mrr": [mrr]})

return metric_df

?Llamaindex提供了一個函數,用于在批處理模式下對數據集運行RetrieverEvaluator。

query_str = "What are the top features of QLoRA?"

results_df = pd.DataFrame()
# Loop over rerankers
for rerank_name, reranker in RERANKERS.items():
print(f"Running Evaluation for Reranker: {rerank_name}")

query_bundle = QueryBundle(query_str)

retrieved_nodes = retriever.retrieve(query_bundle)

if reranker != "None":
retrieved_nodes = reranker.postprocess_nodes(retrieved_nodes, query_bundle)
else:
retrieved_nodes

retriever_evaluator = RetrieverEvaluator.from_metric_names(
["mrr", "hit_rate"], retriever=retriever
)

eval_results = await retriever_evaluator.aevaluate_dataset(qa_dataset)

current_df = display_results(rerank_name, eval_results)
results_df = pd.concat([results_df, current_df], ignore_index=True)

4.2 結果

WithoutReranker:為每個嵌入提供了baseline;

CohereRerank:是SOTA結果;

bge-reranker-base:結果比CohereRerank差,也許是因為該模型對這種重新排序無效;

bge-reranker-large:結果比bge-reranker-base差,也許是因為該模型對這種重新排序無效。

     結果表明重新排序在優化檢索過程中的重要性,尤其是CohereRebank模型。

五、結論

      為搜索選擇適當的嵌入至關重要;即使是最有效的重新排序也無法彌補較差的基本搜索結果(比如bge-rerankers)。

? ? ? 最大限度地提高檢索器的性能取決于發現嵌入和重新排序的最佳組合。這仍然是一個活躍的研究領域,來確定最有效的組合。

本文章轉載微信公眾號@ArronAI

上一篇:

開源| 伯克利AI分布式框架Ray,兼容TensorFlow、PyTorch與MXNet

下一篇:

Open-R1 技術解密:HuggingFace 如何完整復現 DeepSeek 推理模型
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費