RAG框架中的Retrieve算法评估

目录

什么是 RAG 中的 Retrieve?

Retrieve 算法评估

数据集构造

Retrieve 算法评估

BM25

Embedding Search

Ensemble Search

Ensemble + Rerank

指标可视化及分析

指标可视化

指标分析

总结


RAG框架(Re-ranking with Anchor Graph)是一种在语义相似度检索中用于优化召回率和准确率的算法。Retrieve步骤是在对大量文档进行初筛后,利用Anchor Graph对召回的文档进行重排序的过程。

在RAG框架中,Retrieve算法的评估主要关注以下几个方面:

  1. 召回率(Recall):召回率衡量的是系统能够找出相关文档的比例。一个高的召回率意味着系统能够从大量文档中找出更多的相关文档。
  2. 准确率(Precision):准确率衡量的是系统给出的相关文档中有多少是真正相关的。一个高的准确率意味着系统给出的相关文档更加可靠。
  3. F1分数:F1分数是召回率和准确率的调和平均数,用于综合考虑召回率和准确率。一个高的F1分数意味着系统在召回和准确方面都表现良好。
  4. 效率:Retrieve算法的效率也是评估的一个重要方面。如果一个算法虽然准确率高,但是运行时间过长,那么在实际应用中可能并不实用。

具体的评估过程通常如下:

  1. 数据准备:准备一个标注好的数据集,其中包含多个查询和对应的文档。这些文档应该被标记为相关或不相关。
  2. 实验设置:根据需要设定不同的参数,如Anchor Graph的大小和结构、阈值等。
  3. 运行实验:对每个查询,使用RAG框架进行检索,并记录每个指标的值。
  4. 分析结果:比较不同参数设置下各指标的值,分析Retrieve算法的性能。
  5. 优化:根据分析结果,对算法进行优化,提高性能。

注意,RAG框架是一种相对较新的技术,其具体的评估方法和标准可能还在发展和完善中。因此,具体的评估过程和方法可能会根据实际需求和实验条件有所不同。

什么是 RAG 中的 Retrieve?

RAG即 Retrieval Augmented Generation 的简称,是现阶段增强使用 LLM 的常见方式之一,其一般步骤为:

  1. 1. 文档划分(Document Split)

  2. 2. 向量嵌入(Embedding)

  3. 3. 文档获取(Retrieve)

  4. 4. Prompt 工程(Prompt Engineering)

  5. 5. 大模型问答(LLM)

大致的流程图参考如下:

25cb147b2313f6d9c730165db5b9511d.png

通常来说,可将RAG划分为召回(Retrieve)阶段和答案生成(Answer Generate)阶段,而效果优化也从这方面入手。针对召回阶段,文档获取是其中重要的步骤,决定了注入大模型的知识背景,常见的召回算法如下:

  • • BM25(又称 Keyword Search): 使用 BM24 算法找回相关文档,一般对于特定领域关键词效果较好,比如人名,结构名等;

  • • Embedding Search: 使用 Embedding 模型将 query 和 corpus 进行文本嵌入,使用向量相似度进行文本匹配,可解决 BM25 算法的相似关键词召回效果差的问题,该过程一般会使用向量数据库(Vector Database);

  • • Ensemble Search: 融合 BM25 算法和 Embedding Search 的结果,使用 RFF 算法进行重排序,一般会比单独的召回算法效果好;

  • • Rerank: 上述的召回算法一般属于粗召回阶段,更看重性能;Rerank 是对粗召回阶段的结果,再与 query 进行文本匹配,属于 Rerank(又称为重排、精排)阶段,更看重效果;

综合上述 Retrieve 算法的框架示意图如下:

57cbcbe6c96c54660b0d2a31c6e8ec7e.jpeg

上述的 Retrieve 算法更有优劣,一般会选择合适的场景进行使用或考虑综合几种算法进行使用。那么,它们的效果具体如何呢?

Retrieve 算法评估

那么,如何对 Retrieve 算法进行具体效果评估呢?

本文将通过构造自有数据集进行测试,分别对上述四种 Retrieve 算法进行实验,采用Hit Rate和MRR指标进行评估。

在LlamaIndex官方 Retrieve Evaluation 中,提供了对 Retrieve 算法的评估示例

这是现在网上较为权威的 Retrieve Evaluation 实验,本文将参考 LlamaIndex 的做法,给出更为详细的评估实验过程与结果。

Retrieve Evaluation 实验的步骤如下:

  1. 1. 文档划分:寻找合适数据集,进行文档划分;

  2. 2. 问题生成:对划分后的文档,使用 LLM 对文档内容生成问题;

  3. 3. 召回文本:对生成的每个问题,采用不同的 Retrieve 算法,得到召回结果;

  4. 4. 指标评估:使用Hit Rate和MRR指标进行评估

步骤是清晰的,那么,我们来看下评估指标:Hit Rate和MRR。

Hit Rate即命中率,一般指的是我们预期的召回文本(真实值)在召回结果的前 k 个文本中会出现,也就是 Recall@k 时,能得到预期文本。一般,Hit Rate越高,就说明召回算法效果越好。

MRR即 Mean Reciprocal Rank,是一种常见的评估检索效果的指标。MRR 是衡量系统在一系列查询中返回相关文档或信息的平均排名的逆数的平均值。例如,如果一个系统对第一个查询的正确答案排在第二位,对第二个查询的正确答案排在第一位,则 MRR 为 (1/2 + 1/1) / 2。

在 LlamaIndex 中,这两个指标的对应类分别为HitRate和MRR,源代码如下:

class HitRate(BaseRetrievalMetric):
    """Hit rate metric."""

    metric_name: str = "hit_rate"

    def compute(
        self,
        query: Optional[str] = None,
        expected_ids: Optional[List[str]] = None,
        retrieved_ids: Optional[List[str]] = None,
        expected_texts: Optional[List[str]] = None,
        retrieved_texts: Optional[List[str]] = None,
        **kwargs: Any,
    ) -> RetrievalMetricResult:
        """Compute metric."""
        if retrieved_ids is None or expected_ids is None:
            raise ValueError("Retrieved ids and expected ids must be provided")
        is_hit = any(id in expected_ids for id in retrieved_ids)
        return RetrievalMetricResult(
            score=1.0 if is_hit else 0.0,
        )


class MRR(BaseRetrievalMetric):
    """MRR metric."""

    metric_name: str = "mrr"

    def compute(
        self,
        query: Optional[str] = None,
        expected_ids: Optional[List[str]] = None,
        retrieved_ids: Optional[List[str]] = None,
        expected_texts: Optional[List[str]] = None,
        retrieved_texts: Optional[List[str]] = None,
        **kwargs: Any,
    ) -> RetrievalMetricResult:
        """Compute metric."""
        if retrieved_ids is None or expected_ids is None:
            raise ValueError("Retrieved ids and expected ids must be provided")
        for i, id in enumerate(retrieved_ids):
            if id in expected_ids:
                return RetrievalMetricResult(
                    score=1.0 / (i + 1),
                )
        return RetrievalMetricResult(
            score=0.0,
        )

数据集构造

以这个项目为基础,笔者采集了日本半导体行业相关的网络文章及其他文档,进行文档划分,导入至 ElastricSearch,并使用 OpenAI Embedding 获取文本嵌入向量。语料库一共为 433 个文档片段(Chunk),其中 321 个与日本半导体行业相关(不妨称之为领域文档)。

还差 query 数据集。这点是从 LlamaIndex 官方示例中获取的灵感:使用大模型生成 query!

针对上述 321 个领域文档,使用 GPT-4 模型生成一个与文本内容相关的问题,即 query,Python 代码如下:

# -*- coding: utf-8 -*-
# @place: Pudong, Shanghai
# @file: data_transfer.py
# @time: 2023/12/25 17:51
import pandas as pd
from llama_index.llms import OpenAI
from llama_index.schema import TextNode
from llama_index.evaluation import generate_question_context_pairs
import random
random.seed(42)

llm = OpenAI(model="gpt-4", max_retries=5)

# 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 university professor. Your task is to set {num_questions_per_chunk} questions for the upcoming Chinese quiz.
Questions throughout the test should be diverse. Questions should not contain options or start with Q1/Q2.
Questions must be written in Chinese. The expression must be concise and clear.
It should not exceed 15 Chinese characters. Words such as "这", "那", "根据", "依据" and other punctuation marks
should not be used. Abbreviations may be used for titles and professional terms.
"""

nodes = []
data_df = pd.read_csv("../data/doc_qa_dataset.csv", encoding="utf-8")
for i, row in data_df.iterrows():
    if len(row["content"]) > 80 and i > 96:
        node = TextNode(text=row["content"])
        node.id_ = f"node_{i + 1}"
        nodes.append(node)


doc_qa_dataset = generate_question_context_pairs(
    nodes, llm=llm, num_questions_per_chunk=1, qa_generate_prompt_tmpl=qa_generate_prompt_tmpl
)

doc_qa_dataset.save_json("../data/doc_qa_dataset.json")

原始数据doc_qa_dataset.csv是笔者从 Kibana 中的 Discover 中导出的,使用 llama-index 模块和 GPT-4 模型,以合适的 Prompt,对每个领域文档生成一个问题,并保存为 doc_qa_dataset.json,这就是我们进行 Retrieve Evaluation 的数据格式,其中包括 queries, corpus, relevant_docs, mode 四个字段。

我们来查看第一个文档及生成的答案,如下:

{
    "queries": {
        "7813f025-333d-494f-bc14-a51b2d57721b": "日本半导体产业的现状和影响因素是什么?",
        ...
    },
    "corpus": {
        "node_98": "日本半导体产业在上世纪80年代到达顶峰后就在缓慢退步,但若简单认为日本半导体产业失败了,就是严重误解,今天日本半导体产业仍有非常有竞争力的企业和产品。客观认识日本半导体产业的成败及其背后的原因,对正在大力发展半导体产业的中国,有非常强的参考价值。",
        ...
    },
    "relevant_docs": {
        "7813f025-333d-494f-bc14-a51b2d57721b": [
            "node_98"
        ],
        ...
    },
    "mode": "text"
}

Retrieve 算法评估

我们需要评估的 Retrieve 算法为 BM25, Embedding Search, Ensemble Search, Ensemble + Rerank,下面将分别就 Retriever 实现方式、指标评估实验对每种 Retrieve 算法进行详细介绍。

BM25

BM25 的储存采用 ElasticSearch,即直接使用 ES 内置的 BM25 算法。笔者在 llama-index 对 BaseRetriever 进行定制化开发(这也是我们实现自己想法的一种常规方法),对其简单封装,Python 代码如下:

# -*- coding: utf-8 -*-
# @place: Pudong, Shanghai
# @file: bm25_retriever.py
# @time: 2023/12/25 17:42
from typing import List

from elasticsearch import Elasticsearch
from llama_index.schema import TextNode
from llama_index import QueryBundle
from llama_index.schema import NodeWithScore
from llama_index.retrievers import BaseRetriever
from llama_index.indices.query.schema import QueryType

from preprocess.get_text_id_mapping import text_node_id_mapping


class CustomBM25Retriever(BaseRetriever):
    """Custom retriever for elasticsearch with bm25"""
    def __init__(self, top_k) -> None:
        """Init params."""
        super().__init__()
        self.es_client = Elasticsearch([{'host': 'localhost', 'port': 9200}])
        self.top_k = top_k

    def _retrieve(self, query: QueryType) -> List[NodeWithScore]:
        if isinstance(query, str):
            query = QueryBundle(query)
        else:
            query = query

        result = []
        # 查询数据(全文搜索)
        dsl = {
            'query': {
                'match': {
                    'content': query.query_str
                }
            },
            "size": self.top_k
        }
        search_result = self.es_client.search(index='docs', body=dsl)
        if search_result['hits']['hits']:
            for record in search_result['hits']['hits']:
                text = record['_source']['content']
                node_with_score = NodeWithScore(node=TextNode(text=text,
                                                id_=text_node_id_mapping[text]),
                                                score=record['_score'])
                result.append(node_with_score)

        return result

之后,对 top_k 结果进行指标评估,Python 代码如下:

# -*- coding: utf-8 -*-
# @place: Pudong, Shanghai
# @file: evaluation_exp.py
# @time: 2023/12/25 20:01
import asyncio
import time

import pandas as pd
from datetime import datetime
from faiss import IndexFlatIP
from llama_index.evaluation import RetrieverEvaluator
from llama_index.finetuning.embeddings.common import EmbeddingQAFinetuneDataset

from custom_retriever.bm25_retriever import CustomBM25Retriever
from custom_retriever.vector_store_retriever import VectorSearchRetriever
from custom_retriever.ensemble_retriever import EnsembleRetriever
from custom_retriever.ensemble_rerank_retriever import EnsembleRerankRetriever
from custom_retriever.query_rewrite_ensemble_retriever import QueryRewriteEnsembleRetriever


# Display results from evaluate.
def display_results(name_list, eval_results_list):
    pd.set_option('display.precision', 4)
    columns = {"retrievers": [], "hit_rate": [], "mrr": []}
    for name, eval_results in zip(name_list, eval_results_list):
        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()

        columns["retrievers"].append(name)
        columns["hit_rate"].append(hit_rate)
        columns["mrr"].append(mrr)

    metric_df = pd.DataFrame(columns)

    return metric_df


doc_qa_dataset = EmbeddingQAFinetuneDataset.from_json("../data/doc_qa_test.json")
metrics = ["mrr", "hit_rate"]
# bm25 retrieve
evaluation_name_list = []
evaluation_result_list = []
cost_time_list = []
for top_k in [1, 2, 3, 4, 5]:
    start_time = time.time()
    bm25_retriever = CustomBM25Retriever(top_k=top_k)
    bm25_retriever_evaluator = RetrieverEvaluator.from_metric_names(metrics, retriever=bm25_retriever)
    bm25_eval_results = asyncio.run(bm25_retriever_evaluator.aevaluate_dataset(doc_qa_dataset))
    evaluation_name_list.append(f"bm25_top_{top_k}_eval")
    evaluation_result_list.append(bm25_eval_results)
    cost_time_list.append((time.time() - start_time) * 1000)

print("done for bm25 evaluation!")
df = display_results(evaluation_name_list, evaluation_result_list)
df['cost_time'] = cost_time_list
print(df.head())
df.to_csv(f"evaluation_bm25_{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}.csv", encoding="utf-8", index=False)

BM25 算法的实验结果如下:

retrievers hit_rate mrr cost_time
bm25_top_1_eval 0.7975 0.7975 461.277
bm25_top_2_eval 0.8536 0.8255 510.3021
bm25_top_3_eval 0.9003 0.8411 570.6708
bm25_top_4_eval 0.9159 0.845 420.7261
bm25_top_5_eval 0.9408 0.85 388.5961

Embedding Search

BM25 算法的实现是简单的。Embedding Search 的较为复杂些,首先需要对 queries 和 corpus 进行文本嵌入,这里的 Embedding 模型使用 Openai 的 text-embedding-ada-002,向量维度为 1536,并将结果存入 numpy 数据结构中,保存为 npy 文件,方便后续加载和重复使用。

为了避免使用过重的向量数据集,本实验采用内存向量数据集: faiss。使用 faiss 加载向量,index 类型选用 IndexFlatIP,并进行向量相似度搜索。

Embedding Search 也需要定制化开发 Retriever 及指标评估,这里不再赘述,具体实验可参考文章末尾的 Github 项目地址。

Embedding Search 的实验结果如下:

retrievers hit_rate mrr cost_time
embedding_top_1_eval 0.6075 0.6075 67.6837
embedding_top_2_eval 0.6978 0.6526 60.8449
embedding_top_3_eval 0.7321 0.6641 59.9051
embedding_top_4_eval 0.7788 0.6758 63.5488
embedding_top_5_eval 0.7944 0.6789 67.7922

注意: 这里的召回时间花费比 BM25 还要少,完全得益于我们已经存储好了文本向量,并使用 faiss 进行加载、查询。

Ensemble Search

Ensemble Search 融合 BM25 算法和 Embedding Search 算法,针对两种算法召回的 top_k 个文档,使用 RRF 算法进行重新排序,再获取 top_k 个文档。RRF 算法是经典且优秀的集成排序算法,这里不再展开介绍,后续专门写文章介绍。

Ensemble Search 的实验结果如下:

retrievers hit_rate mrr cost_time
ensemble_top_1_eval 0.7009 0.7009 1072.7379
ensemble_top_2_eval 0.8536 0.7741 1088.8782
ensemble_top_3_eval 0.8941 0.7928 980.7949
ensemble_top_4_eval 0.919 0.8017 935.1702
ensemble_top_5_eval 0.9377 0.8079 868.299

Ensemble + Rerank

如果还想在 Ensemble Search 的基础上再进行效果优化,可考虑加入 Rerank 算法。常见的 Rerank 模型有 Cohere(API 调用),BGE-Rerank(开源模型)等。本文使用 Cohere Rerank API.

Ensemble + Rerank 的实验结果如下:

retrievers hit_rate mrr cost_time
ensemble_rerank_top_1_eval 0.8349 0.8349 2140632.4041
ensemble_rerank_top_2_eval 0.9034 0.8785 2157657.2871
ensemble_rerank_top_3_eval 0.9346 0.9008 2200800.936
ensemble_rerank_top_4_eval 0.947 0.9078 2150398.7348
ensemble_rerank_top_5_eval 0.9657 0.9099 2149122.9382

指标可视化及分析

指标可视化

上述的评估结果不够直观,我们使用 Plotly 模块绘制指标的条形图,结果如下:

994aa753beb034e374bc30558c7e0fe1.png

Hit Rate

82238138ce7309bb3e2a4e299f0c5fc9.png

MRR

指标分析

我们对上述统计图进行指标分析,可得到结论如下:

  • • 对于每种 Retrieve 算法,随着 k 的增加,top_k 的 Hit Rate 指标和 MRR 指标都有所增加,即检索效果变好,这是显而易见的结论;

  • • 就总体检索效果而言,Ensemble + Rerank > Ensemble > 单独的 Retrieve

  • • 本项目中就单独的 Retrieve 算法而言,BM25 的检索效果比 Embedding Search 好(可能与生成的问答来源于文档有关),但这不是普遍结论,两种算法各有合适的场景

  • • 加入 Rerank 后,检索效果可获得一定的提升,以 top_3 评估结果来说,ensemble 的 Hit Rate 为 0.8941,加入 Rerank 后为 0.9346,提升约 4%

转载文章网址为:​网页链接 

RAG框架(Reinforcement-And-Guided-Attention)是一种结合了强化学习和注意力机制的机器翻译方法。在RAG框架中,Retrieve算法是其中的一个关键组件,用于从语料库中检索与输入句子相关的上下文信息,以指导翻译过程中的注意力分配。

评估Retrieve算法的性能时,通常关注以下几个方面:

  1. 检索质量:评估Retrieve算法检索到的上下文信息的质量。可以通过比较检索到的上下文与实际相关的上下文之间的相似度来衡量。
  2. 翻译质量:评估使用Retrieve算法检索到的上下文信息对翻译质量的提升。可以通过比较使用和不使用Retrieve算法的翻译结果之间的差异来衡量。
  3. 效率:评估Retrieve算法的执行效率,包括检索时间、内存占用等。

为了进行客观的评估,可以使用一些常见的机器翻译评估指标,如BLEU(Bilingual Evaluation Understudy)分数、ROUGE(Recall-Oriented Understudy for Gisting Evaluation)分数等。这些指标可以用来衡量翻译结果的准确性和流畅性。

此外,还可以通过人工评估来进一步了解Retrieve算法的性能。人工评估可以提供更深入的见解,包括翻译结果在语义和表达方面的质量。

总之,评估Retrieve算法的性能需要综合考虑多个方面,包括检索质量、翻译质量、效率等。通过客观评估指标和人工评估的结合,可以更全面地了解Retrieve算法的性能和效果。

RAG (Retrieval-Augmented Generation) 是一种基于 Transformer 的自然语言处理框架,它结合了检索和生成的方法,以生成更具信息性和相关性的响应。在 RAG 框架中,Retrieve 算法用于从预处理的数据集中检索与给定查询最相关的文档或句子。

对 Retrieve 算法的评估通常关注以下几个方面:

  1. 相关性和准确性:这是最重要的评估标准。你希望 Retrieve 算法能够检索到与给定查询最相关和准确的信息。通常使用精确度 (precision)、召回率 (recall) 和 F1 分数来评估准确性。
  2. 效率:评估 Retrieve 算法的执行速度和资源消耗也很重要。在处理大规模数据集时,高效的 Retrieve 算法可以显著提高性能。
  3. 可扩展性:随着数据集的增长,好的 Retrieve 算法应具备良好的可扩展性。这意味着算法应该能够有效地处理更大规模的数据集,而不会显著增加计算时间和资源消耗。
  4. 鲁棒性:对于 Retrieve 算法来说,鲁棒性是指算法在不同类型的数据集和查询上的表现稳定性。理想情况下,一个鲁棒的 Retrieve 算法应该能够在不同的场景中保持稳定的性能。
  5. 可解释性:对于许多应用来说,理解 Retrieve 算法如何做出决策是至关重要的。可解释性有助于增强用户对算法的信任,并帮助开发人员调试和改进算法。

在评估 Retrieve 算法时,通常会使用一些基准测试数据集,如 TREC、MS MARCO 等,这些数据集提供了标准化的评估方法和指标。此外,为了全面评估 Retrieve 算法的性能,你可能需要在不同的数据集上运行多个实验,并使用多种不同的评估指标进行比较。

本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/9c46484a2f.html