搜索效果评估方法
本文介绍开放域搜索的常用的评估指标、方法,重点关注自动化评估方法。
背景介绍
在检索增强生成(RAG)场景中,对于频繁更新的知识、行业垂直API以及内部知识,需要使用检索来召回实时、可信的知识,大部分场景会使用一个通用的搜索引擎来提供开放域的信息检索。对于检索结果的评估是一个重要的课题。
业界对于RAG的评估,更多侧重于结合检索结果,大模型利用与处理检索结果的能力。如在RGB Benchmark中提出的Noise Robustness、Negative Rejection、Information Integration、Counterfactual Robustness指标;FreshLLMs与CRAG中给出的对大模型生成答案的阶梯评分:Perfect(1)、Acceptable(0.5)、Missing(0)、Incorrect(-1)。还有一些自动化评估的框架如RAGAS提供的分阶段评估:Context Precision、Context Recall、Noise Sensitivity、Faithfulness。上述的这些评估指标更多侧重评估模型能力,当然也可以端到端通过模型生成答案的准确性来评估不同检索引擎的准确性。本文会侧重检索结果的质量直接评估以及如何进行自动化评估。
评估维度
在使用通用搜索引擎检索场景,最重要的诉求是检索结果是否包含回答问题所需的知识。考虑到可操作性,可以分拆为两个维度:召回信息的语义相关性与召回站点的可信度。分拆开会包含:
检索结果的相关性(Contextual Relevancy):在没有答案(Ground Truth)情况下,可以通过问题与检索结果进行综合判断是否与问题相关。这个指标相比准确性更具可操作性,在很多搜索Query中,即使人工评估也很难给出一个Ground Truth,但是可以基于常识给出判断检索结果是否与问题相关。这个指标在有答案(GT)的情况下可以升级为准确性指标(Contextual Precision)。
检索结果的召回率(Contextual Recall):召回率度量对于问题回答所需要的信息点,是否都能够通过检索结果覆盖。
站点质量:包含网站的权威性、站点质量评分。在部分场景,对于官网、政府类权威网站、行业专业网站的信息检索有强诉求。在AIGC产出的内容越来越多的情况下,网站内容的可靠性尤为重要。
检索结果多样性:很多非单一准确答案场景,能检索到信息的多样性对于大模型多角度、全方位回答问题是重要的。
评估指标
基于上述评估维度中,综合考虑搜索排序场景,我们使用mAP与NDCG。mAP基于Contextual Precision/Relevancy并综合考虑排序结果,在实际使用中,对大模型token数量以及时延敏感场景会截取TopN检索结果时,有更好的衡量效果。NDCG能够对相关性给出多个阶梯的度量,而不仅是相关、不相关的两种取值,可以更真实的度量召回结果质量。
在实际的业务场景的真实query case,比较难以给出准确全面的Ground Truth。这导致站点质量、召回率、多样性比较难以全面评估。
mAP
mAP(Mean Average Precision)平均精度均值,计算规则如下:
对于每个Query计算Average Precision:其中Rk和Pk分别是第k个召回率和对应的精确率。n是相关结果的数量
数据集中的多个Query计算mAP:C是Query的总数,APi是第ii个类别的平均精度。
NDCG
NDCG(Normalized Discounted Cumulative Gain)可以同时考虑相关性强弱与位置信息,计算规则如下:
计算DCG:
是第i个位置的相关性评分,一般可以给出等级取值:强相关(3),中等相关(2),弱相关(1),不相关(0)。 对于位置的加权处理 计算IDCG:计算理想情况下的最佳排序的DCG取值。需要GT的结果进行计算
计算NDCG:对DCG进行归一化处理后的值。
评估方法
对于上述的两个评估指标,核心需要给出召回文档是否包含回答问题所需的知识,在实际评估中可以选择两种方法人工标注与LLM自动评估。
人工标注
相关性:标注每个召回文档与query是否相关,给出等级或者0/1的判定。
文档质量:点击具体的站点以及网页,判定网页质量,并对网页质量进行打分。
最终相关性可以对上述两个结果进行加权计算得到,再次不展开说明。
LLM自动评估
由于人工标注很难规模扩展,并且在实际的标注中,对于实际的query case比较难以得到一致性的标注结果。如果将召回准确性近似为语义上的相关性(Contextual Relevancy)大模型的评估可以解决前面的问题。在此,我们使用deep eval的Contxtual Relevancy指标来计算mAP或者DCG。实现逻辑如下:
deepeval==1.1.9
langchain-community==0.2.16
import json
import os
from typing import Optional, List
import numpy as np
from deepeval.metrics import BaseMetric, ContextualRelevancyMetric
from deepeval.metrics.contextual_relevancy.template import ContextualRelevancyTemplate
from deepeval.models import DeepEvalBaseLLM
from deepeval.test_case import LLMTestCase
from langchain_community.llms.tongyi import Tongyi
class ContextualRelevancyWrapper(BaseMetric):
def __init__(self):
qwen_model = QwenModel(
model_name="qwen2-72b-instruct",
temperature=0.2,
top_p=0.8
)
self.delegate = ContextualRelevancyMetric(
threshold=0.5,
model=qwen_model,
include_reason=True,
async_mode=False
)
def response_detail(self) -> dict:
verdicts = [
dict(verdict=verdict.verdict, reason=verdict.reason) for verdict in self.delegate.verdicts
]
ap = self.calculate_average_precision([verdict['verdict'] for verdict in verdicts])
detail = dict(
reason=self.delegate.reason,
average_precision=ap,
success=self.delegate.success,
verdicts=json.dumps(verdicts, indent=4, ensure_ascii=False)
)
return detail
def measure(self, test_case: LLMTestCase, *args, **kwargs) -> float:
return self.delegate.measure(test_case=test_case, *args, **kwargs)
async def a_measure(self, test_case: LLMTestCase, *args, **kwargs) -> float:
return await self.delegate.a_measure(test_case=test_case, *args, **kwargs)
def is_successful(self) -> bool:
return self.delegate.is_successful()
def calculate_average_precision(self, verdicts: List[str]):
"""
计算mAP
:param verdicts: ["yes", "no", "no"]
:return:
"""
verdict_list = np.array([1 if verdict == "yes" else 0 for verdict in verdicts])
denominator = sum(verdict_list) + 1e-10
numerator = sum(
[
(sum(verdict_list[: i + 1]) / (i + 1)) * verdict_list[i]
for i in range(len(verdict_list))
]
)
score = numerator / denominator
return score
class QwenModel(DeepEvalBaseLLM):
def __init__(
self,
model_name: str = "qwen2-72b-instruct",
dashscope_api_key: Optional[str] = None,
**kwargs
):
self.model_name = model_name
dashscope_api_key = dashscope_api_key or os.environ.get("dashscope_sk")
self.model = Tongyi(model=model_name, api_key=dashscope_api_key, **kwargs)
self.kwargs = kwargs
def load_model(self, *args, **kwargs):
return self.model
def generate(self, prompt_str: str) -> str:
return self.model.invoke(prompt_str)
async def a_generate(self, prompt_str: str) -> str:
try:
return await self.model.ainvoke(prompt_str)
except Exception as e:
print(f"Failed in invoke: {e}\nTraceback:\n{e}")
return ""
def get_model_name(self, *args, **kwargs) -> str:
return self.model_name
class CustomContextualRelevancyTemplate:
@staticmethod
def generate_reason(input, irrelevancies, score):
return f"""Based on the given input, reasons for why the retrieval context is irrelevant to the input, and the contextual relevancy score (the closer to 1 the better), please generate a CONCISE reason for the score.
In your reason, you should quote data provided in the reasons for irrelevancy to support your point.
**
IMPORTANT: Please make sure to only return in JSON format, with the 'reason' key providing the reason, reason field must return in **Chinese**.
Example JSON:
{{
"reason": "得分为 <contextual_relevancy_score>,因为 <YOUR_REASON>。"
}}
If the score is 1, keep it short and say something positive with an upbeat encouraging tone (but don't overdo it otherwise it gets annoying).
**
Contextual Relevancy Score:
{score}
Input:
{input}
Reasons for why the retrieval context is irrelevant to the input:
{irrelevancies}
JSON:
"""
@staticmethod
def generate_verdict(text, context):
return f"""Based on the input and context, please generate a JSON object to indicate whether the context is relevant to the provided input. The JSON will have 1 mandatory field: 'verdict', and 1 optional field: 'reason'.
The 'verdict' key should STRICTLY be either 'yes' or 'no', and states whether the context is relevant to the input.
Provide a 'reason' ONLY IF verdict is no. You MUST quote the irrelevant parts of the context to back up your reason.
**
IMPORTANT: Please make sure to only return in JSON format.reason field must return in **Chinese**
Example Context: "爱因斯坦因发现光电效应而获得诺贝尔奖。他在1968年获得了诺贝尔奖。那里有一只猫。"
Example Input: "爱因斯坦的一些成就是什么?"
Example:
{{
"verdict": "no",
"reason": "虽然上下文包含有关爱因斯坦获得诺贝尔奖的信息,但不相关地提到“那里有一只猫”,这与爱因斯坦的成就无关。"
}}
**
Input:
{text}
Context:
{context}
JSON:
"""
def patch_contextual_relevancy():
ContextualRelevancyTemplate.generate_reason = CustomContextualRelevancyTemplate.generate_reason
ContextualRelevancyTemplate.generate_verdict = CustomContextualRelevancyTemplate.generate_verdict
if __name__ == '__main__':
patch_contextual_relevancy()
cr_metric = ContextualRelevancyWrapper()
testcase = LLMTestCase(
input="小狗贫血的表现",
actual_output="",
retrieval_context=[
"狗狗贫血的主要症状包括:<br>1. 身体消瘦、毛发粗糙无光泽、体力较差,活动时间短、运动无力。<br>2. 皮肤和可视黏膜苍白,如舌头颜色变粉红或苍白,牙龈苍白。<br>3. 精神不振、嗜睡、经常出现走路摇晃、倒地后起立困难、卧地不起等症状。<br>4. 食欲不振、日常可能会晕倒、身体虚弱等。<br>如果发现狗狗出现以上症状,建议及时带狗狗去兽医院进行检查和治疗。在日常生活中,可以通过改善饮食结构、补充造血物质和营养品等方法来帮助狗狗改善贫血状况。",
"狗狗一直饿可能是由以下几种原因导致的:<br>1. 喂食量不足:如果狗狗每次的喂食量太少,即使一天喂5次,也可能导致狗狗吃不饱。狗狗一天的喂食总量一般是根据体重来确定的,一般按照狗狗体重的3%- 5%喂食。<br>2. 幼犬对吃饱没有太大概念,少量多次喂食,且对于好吃的食物没有抵抗力,此时并不是因为饿,只是馋。<br>3. 糖尿病问题:如果是中老年犬出现长期饥饿的情况,可能是糖尿病或其他疾病引起的多饮多食、体重减轻的症状,建议及时带狗狗去宠物医院检查。<br>4. 饮食不均衡或者不足:如果狗狗的饮食缺乏必要的营养成分,或者狗狗没有得到足够的食物,那么他们就会变得更饥饿。<br>5. 狗狗处于生长期:如果狗狗还很小,或者正处于生长期,那么他们需要更多的食物来支持他们的身体发育。<br>6. 狗狗在锻炼或者活动后饥饿:如果狗狗进行了大量的锻炼或者活动,那么他们会消耗更多的能量,因此可能会感到更饥饿。<br>如果狗狗一直处于饥饿状态,建议带它去看兽医,以确定是否存在任何潜在的健康问题。",
"狗狗如果出现了贫血,<em>它会看上去很没有精神,也没有食欲,身体变得消瘦而且毛发还非常的枯燥,</em>引起狗狗贫血有三方面的原因。 导致狗狗贫血的原因是多方面的,主人一旦发现狗狗没有食欲精神不好,就要及时采取措施给予狗狗营养的补充。",
"1、牙龈、眼睑、结膜等皮肤黏膜都会从粉红色、淡粉色,变淡变白。 2、在<em>贫血</em>初期出现嗜睡,出门散步<em>表现</em>得过度喘息、有气无力,容易疲倦。 3、食欲减退、废绝,体重下降快,毛发干枯发黄,无光泽。 4、<em>贫血</em>后期会出现运动无力、摇晃、起身困难、呼吸困难、全身衰竭。",
"小狗肚子饿了,<em>最多也就是发出比较轻微的呜咽声。</em>像个婴儿一样,声音虽不大,但是听上去很可怜。 幼犬的自理能力比较差,所以主人要做到准时喂食,一般3~5小时就要喂一次。 把食盆叼过来给主人 狗狗一大早起来就肚子饿了,但是看到主人还在睡,自己也不敢发出很大的声响。 把食盆叼过来给主人 狗狗一大早起来就肚子饿了,但是看到主人还在睡,自己也不敢发出很大的声响。 就会选择叼着一个狗碗去找主人,示意你该喂狗粮了,主人再想赖床也不行了。",
"<em>临床表现为呼吸加快、全身无力等特征。</em>狗狗贫血和发炎,通过血检的血球容积比、血色素可以判断。数值低下时会引起贫血,数值升高时就是正在发炎,不过当免疫力降低时,就算白血球没有增加,也有可能正在发炎。",
],
)
score = cr_metric.measure(testcase)
detail = cr_metric.response_detail()
print(f"score:{score}, mAP:{detail['average_precision']}")
print(f"detail_verdicts:{json.dumps(json.loads(detail['verdicts']), indent=4, ensure_ascii=False)}")
输出结果
score:0.6666666666666666, mAP:0.7708333333140625
detail_verdicts:[
{
"verdict": "yes",
"reason": null
},
{
"verdict": "no",
"reason": "上下文中没有提到小狗贫血的表现,而是详细描述了狗狗可能一直感到饥饿的原因,这与输入的小狗贫血的表现无关。"
},
{
"verdict": "yes",
"reason": null
},
{
"verdict": "yes",
"reason": null
},
{
"verdict": "no",
"reason": "上下文中没有提到小狗贫血的表现,而是提到了小狗肚子饿的行为,这与输入的问题无关。"
},
{
"verdict": "yes",
"reason": null
}
]
LLM自动评估分析
上述的自动化评估具有较强的稳定性,我们连续在1000条数据集上多次评估mAP的差异小于1%。基于上述的自动化评估方法,您可以对业务场景的Query做出快速的检索效果评估。在做搜索引擎选型、参数调整的评估都能提供辅助。自动化评估也存在一些限制,下面会简单分析下自动化评估的优劣点:
优势
Scalable:低成本,自动化的扩展到所有搜索引擎分析。
稳定性:给出的结果较稳定,多次评估的差异较小。
统计意义可用:在进行横向评测统计,给出的评估指标是有意义的,包括多搜索引擎评测、优化排序召回的对比测试等。
劣势
评估标准对齐问题:在部分场景上述Prompt会给出较为严苛的标准,导致评估错误。
不具备GT分辨能力:只能在语义上判定相关性,无法基于常识给出召回结果错误的判定。
异常CASE
query:Arbitrary
html_snippet
relevancy
reason
<em>Arbitrary</em> 单曲 20 专辑 13 播放歌手热门歌曲 关注 歌手简介 暂无简介 专辑 13 Chandeliers 2024-03-15 She FuxS 2024-02-09 Ep·i·glot·tis 2024-02-09 Dehumanize The Enemy(Explicit) 2024-01-26 Gr3ys Have S3x Slav3s 2023-12-29 查看更多内容,请下载客户端 立即下载 下载QQ音乐客户端 QQ音乐 PC版 QQ音乐 Mac版 QQ音乐 ...
no
上下文中提到了专辑和歌曲信息,但这与输入的'Arbitrary'没有直接关联,无法确定这些信息是否与问题相关。
Arbitrary <em>任意的;武断的;</em>专制的 If you describe an action, rule, or decision as arbitrary, you think that it is not based on any principle, plan, or system. It often seems unfair because of this. l Arbitrary and capricious 任意的和不正规的 用来形容行政机关或下级法院所作的决定或采取的措施,...
no
上下文中虽然提到了'arbitrary'这个词的定义,但是没有提供任何与输入'Arbitrary'相关的具体情境或例子,因此无法判断上下文是否完全相关。