搜索效果評估方法
本文介紹開放域搜索的常用的評估指標、方法,重點關注自動化評估方法。
背景介紹
在檢索增強生成(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'相關的具體情境或例子,因此無法判斷上下文是否完全相關。