812 lines
31 KiB
Python
Executable File
812 lines
31 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Gene Family RAG 评估Pipeline
|
|
============================
|
|
|
|
功能:
|
|
1. 检索性能评估 (Recall@K, Precision@K, MRR, NDCG)
|
|
2. 生成质量评估 (Faithfulness, Relevance, Citation Accuracy)
|
|
3. 幻觉检测 (Hallucination Rate, Factual Precision)
|
|
4. 基因家族特有指标 (Gene ID Recognition, Cross-species Consistency)
|
|
5. 生成对比报告
|
|
|
|
使用方法:
|
|
python rag_metrics_evaluator.py --testset rag_benchmark_testset.json --output results/
|
|
|
|
作者: xialab
|
|
日期: 2026-04-26
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import re
|
|
import math
|
|
from typing import Dict, List, Tuple, Optional, Any
|
|
from dataclasses import dataclass, asdict
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
|
|
# ============== 数据模型 ==============
|
|
|
|
@dataclass
|
|
class RetrievalMetrics:
|
|
"""检索性能指标"""
|
|
recall_at_k: Dict[int, float] # {1: 0.8, 5: 0.9, 10: 0.95}
|
|
precision_at_k: Dict[int, float]
|
|
mrr: float # Mean Reciprocal Rank
|
|
ndcg_at_k: Dict[int, float] # Normalized DCG
|
|
hit_rate_at_k: Dict[int, float]
|
|
|
|
def to_dict(self) -> Dict:
|
|
return {
|
|
"recall@k": self.recall_at_k,
|
|
"precision@k": self.precision_at_k,
|
|
"mrr": round(self.mrr, 4),
|
|
"ndcg@k": self.ndcg_at_k,
|
|
"hit_rate@k": self.hit_rate_at_k
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class GenerationMetrics:
|
|
"""生成质量指标"""
|
|
faithfulness: float # 0-1, 与检索文档的一致性
|
|
answer_relevance: float # 0-5, LLM评分
|
|
completeness: float # 0-1, 回答覆盖度
|
|
citation_accuracy: float # 0-1, 引用准确性
|
|
fluency: float # 0-5, 流畅度
|
|
|
|
def to_dict(self) -> Dict:
|
|
return {
|
|
"faithfulness": round(self.faithfulness, 4),
|
|
"answer_relevance": round(self.answer_relevance, 2),
|
|
"completeness": round(self.completeness, 4),
|
|
"citation_accuracy": round(self.citation_accuracy, 4),
|
|
"fluency": round(self.fluency, 2)
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class HallucinationMetrics:
|
|
"""幻觉检测指标"""
|
|
hallucination_rate: float # 0-1, 包含幻觉的回答比例
|
|
factual_precision: float # 0-1, 事实正确的陈述比例
|
|
source_attribution: float # 0-1, 可追溯到来源的陈述比例
|
|
contradiction_rate: float # 0-1, 与文档矛盾的陈述比例
|
|
|
|
def to_dict(self) -> Dict:
|
|
return {
|
|
"hallucination_rate": round(self.hallucination_rate, 4),
|
|
"factual_precision": round(self.factual_precision, 4),
|
|
"source_attribution": round(self.source_attribution, 4),
|
|
"contradiction_rate": round(self.contradiction_rate, 4)
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class GeneFamilyMetrics:
|
|
"""基因家族特有指标"""
|
|
gene_id_recognition_rate: float # 基因ID识别准确率
|
|
functional_annotation_accuracy: float # 功能注释准确率
|
|
cross_species_consistency: float # 跨物种一致性
|
|
ortholog_mapping_accuracy: float # 同源基因映射准确率
|
|
go_term_accuracy: float # GO term匹配准确率
|
|
|
|
def to_dict(self) -> Dict:
|
|
return {
|
|
"gene_id_recognition_rate": round(self.gene_id_recognition_rate, 4),
|
|
"functional_annotation_accuracy": round(self.functional_annotation_accuracy, 4),
|
|
"cross_species_consistency": round(self.cross_species_consistency, 4),
|
|
"ortholog_mapping_accuracy": round(self.ortholog_mapping_accuracy, 4),
|
|
"go_term_accuracy": round(self.go_term_accuracy, 4)
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class EvaluationResult:
|
|
"""单个问题的评估结果"""
|
|
question_id: str
|
|
question: str
|
|
category: str
|
|
difficulty: str
|
|
|
|
# 检索结果
|
|
retrieved_docs: List[Dict]
|
|
relevant_doc_indices: List[int]
|
|
|
|
# 生成结果
|
|
generated_answer: str
|
|
citations: List[str]
|
|
|
|
# 各项指标
|
|
retrieval_metrics: Optional[RetrievalMetrics] = None
|
|
generation_metrics: Optional[GenerationMetrics] = None
|
|
hallucination_metrics: Optional[HallucinationMetrics] = None
|
|
genefamily_metrics: Optional[GeneFamilyMetrics] = None
|
|
|
|
def to_dict(self) -> Dict:
|
|
return {
|
|
"question_id": self.question_id,
|
|
"question": self.question,
|
|
"category": self.category,
|
|
"difficulty": self.difficulty,
|
|
"retrieved_docs_count": len(self.retrieved_docs),
|
|
"relevant_doc_indices": self.relevant_doc_indices,
|
|
"generated_answer": self.generated_answer[:500] + "..." if len(self.generated_answer) > 500 else self.generated_answer,
|
|
"citations": self.citations,
|
|
"retrieval_metrics": self.retrieval_metrics.to_dict() if self.retrieval_metrics else None,
|
|
"generation_metrics": self.generation_metrics.to_dict() if self.generation_metrics else None,
|
|
"hallucination_metrics": self.hallucination_metrics.to_dict() if self.hallucination_metrics else None,
|
|
"genefamily_metrics": self.genefamily_metrics.to_dict() if self.genefamily_metrics else None
|
|
}
|
|
|
|
|
|
# ============== 评估器实现 ==============
|
|
|
|
class RAGMetricsEvaluator:
|
|
"""RAG系统评估器"""
|
|
|
|
def __init__(self, k_values: List[int] = [1, 3, 5, 10]):
|
|
self.k_values = k_values
|
|
|
|
def evaluate_retrieval(
|
|
self,
|
|
retrieved_docs: List[Dict],
|
|
relevant_docs: List[str], # 相关文档ID列表
|
|
relevance_scores: Optional[Dict[str, int]] = None
|
|
) -> RetrievalMetrics:
|
|
"""
|
|
评估检索性能
|
|
|
|
Args:
|
|
retrieved_docs: 检索到的文档列表,每个文档包含id字段
|
|
relevant_docs: 相关文档ID列表
|
|
relevance_scores: 文档相关性评分 {doc_id: score}
|
|
"""
|
|
if relevance_scores is None:
|
|
relevance_scores = {doc_id: 1 for doc_id in relevant_docs}
|
|
|
|
retrieved_ids = [doc.get("id", doc.get("doc_id", str(i))) for i, doc in enumerate(retrieved_docs)]
|
|
relevant_set = set(relevant_docs)
|
|
|
|
# 计算Recall@K, Precision@K, HitRate@K
|
|
recall_at_k = {}
|
|
precision_at_k = {}
|
|
hit_rate_at_k = {}
|
|
|
|
for k in self.k_values:
|
|
retrieved_k = set(retrieved_ids[:k])
|
|
relevant_retrieved = len(retrieved_k & relevant_set)
|
|
|
|
recall_at_k[k] = relevant_retrieved / len(relevant_set) if relevant_set else 0
|
|
precision_at_k[k] = relevant_retrieved / min(k, len(retrieved_ids)) if retrieved_ids else 0
|
|
hit_rate_at_k[k] = 1 if relevant_retrieved > 0 else 0
|
|
|
|
# 计算MRR
|
|
mrr = 0
|
|
for i, doc_id in enumerate(retrieved_ids):
|
|
if doc_id in relevant_set:
|
|
mrr = 1 / (i + 1)
|
|
break
|
|
|
|
# 计算NDCG@K
|
|
ndcg_at_k = {}
|
|
for k in self.k_values:
|
|
retrieved_k = retrieved_ids[:k]
|
|
|
|
# 计算DCG
|
|
dcg = 0
|
|
for i, doc_id in enumerate(retrieved_k):
|
|
rel = relevance_scores.get(doc_id, 0)
|
|
dcg += (2 ** rel - 1) / math.log2(i + 2)
|
|
|
|
# 计算IDCG (理想情况)
|
|
ideal_rels = sorted(relevance_scores.values(), reverse=True)[:k]
|
|
idcg = sum((2 ** rel - 1) / math.log2(i + 2) for i, rel in enumerate(ideal_rels))
|
|
|
|
ndcg_at_k[k] = dcg / idcg if idcg > 0 else 0
|
|
|
|
return RetrievalMetrics(
|
|
recall_at_k=recall_at_k,
|
|
precision_at_k=precision_at_k,
|
|
mrr=mrr,
|
|
ndcg_at_k=ndcg_at_k,
|
|
hit_rate_at_k=hit_rate_at_k
|
|
)
|
|
|
|
def evaluate_generation(
|
|
self,
|
|
generated_answer: str,
|
|
question: str,
|
|
expected_answer: str,
|
|
citations: List[str],
|
|
retrieved_docs: List[Dict]
|
|
) -> GenerationMetrics:
|
|
"""
|
|
评估生成质量
|
|
|
|
注意:这里使用启发式方法,实际应用中可以使用LLM-as-judge
|
|
"""
|
|
# Faithfulness: 检查回答中的关键信息是否在检索文档中
|
|
faithfulness = self._calculate_faithfulness(generated_answer, retrieved_docs)
|
|
|
|
# Answer Relevance: 检查回答与问题的相关性
|
|
answer_relevance = self._calculate_relevance(generated_answer, question)
|
|
|
|
# Completeness: 检查回答覆盖预期答案的程度
|
|
completeness = self._calculate_completeness(generated_answer, expected_answer)
|
|
|
|
# Citation Accuracy: 检查引用的准确性
|
|
citation_accuracy = self._calculate_citation_accuracy(citations, retrieved_docs)
|
|
|
|
# Fluency: 简单检查回答长度和句子结构
|
|
fluency = self._calculate_fluency(generated_answer)
|
|
|
|
return GenerationMetrics(
|
|
faithfulness=faithfulness,
|
|
answer_relevance=answer_relevance,
|
|
completeness=completeness,
|
|
citation_accuracy=citation_accuracy,
|
|
fluency=fluency
|
|
)
|
|
|
|
def evaluate_hallucination(
|
|
self,
|
|
generated_answer: str,
|
|
retrieved_docs: List[Dict],
|
|
expected_answer: str
|
|
) -> HallucinationMetrics:
|
|
"""评估幻觉程度"""
|
|
# 简单启发式:检查回答中是否包含预期答案中没有的信息
|
|
# 实际应用中应使用NLI模型或LLM判断
|
|
|
|
# Hallucination Rate: 包含无法验证信息的概率
|
|
hallucination_rate = self._detect_hallucination(generated_answer, retrieved_docs)
|
|
|
|
# Factual Precision: 事实正确率
|
|
factual_precision = self._calculate_factual_precision(generated_answer, expected_answer)
|
|
|
|
# Source Attribution: 可溯源比例
|
|
source_attribution = self._calculate_source_attribution(generated_answer, retrieved_docs)
|
|
|
|
# Contradiction Rate: 矛盾率
|
|
contradiction_rate = self._detect_contradiction(generated_answer, retrieved_docs)
|
|
|
|
return HallucinationMetrics(
|
|
hallucination_rate=hallucination_rate,
|
|
factual_precision=factual_precision,
|
|
source_attribution=source_attribution,
|
|
contradiction_rate=contradiction_rate
|
|
)
|
|
|
|
def evaluate_genefamily_specific(
|
|
self,
|
|
generated_answer: str,
|
|
question_data: Dict
|
|
) -> GeneFamilyMetrics:
|
|
"""评估基因家族特有指标"""
|
|
# Gene ID Recognition Rate
|
|
gene_id_rate = self._check_gene_id_recognition(generated_answer, question_data)
|
|
|
|
# Functional Annotation Accuracy
|
|
func_accuracy = self._check_functional_annotation(generated_answer, question_data)
|
|
|
|
# Cross-species Consistency
|
|
cross_species = self._check_cross_species_consistency(generated_answer, question_data)
|
|
|
|
# Ortholog Mapping Accuracy
|
|
ortholog_accuracy = self._check_ortholog_mapping(generated_answer, question_data)
|
|
|
|
# GO Term Accuracy
|
|
go_accuracy = self._check_go_terms(generated_answer, question_data)
|
|
|
|
return GeneFamilyMetrics(
|
|
gene_id_recognition_rate=gene_id_rate,
|
|
functional_annotation_accuracy=func_accuracy,
|
|
cross_species_consistency=cross_species,
|
|
ortholog_mapping_accuracy=ortholog_accuracy,
|
|
go_term_accuracy=go_accuracy
|
|
)
|
|
|
|
# ============== 辅助计算方法 ==============
|
|
|
|
def _calculate_faithfulness(self, answer: str, docs: List[Dict]) -> float:
|
|
"""计算忠实度:回答内容有多少能在检索文档中找到支持"""
|
|
if not docs:
|
|
return 0.0
|
|
|
|
# 合并所有文档文本
|
|
doc_text = " ".join([doc.get("text", doc.get("content", "")) for doc in docs])
|
|
doc_text = doc_text.lower()
|
|
|
|
# 提取回答中的关键短语(简单实现:按句子分割)
|
|
sentences = re.split(r'[。!?.!?]', answer)
|
|
supported_count = 0
|
|
|
|
for sent in sentences:
|
|
sent = sent.strip().lower()
|
|
if len(sent) < 5: # 跳过太短的句子
|
|
continue
|
|
# 检查句子或其主要部分是否在文档中
|
|
if sent in doc_text or any(phrase in doc_text for phrase in self._extract_key_phrases(sent)):
|
|
supported_count += 1
|
|
|
|
total_sentences = len([s for s in sentences if len(s.strip()) >= 5])
|
|
return supported_count / total_sentences if total_sentences > 0 else 0.0
|
|
|
|
def _calculate_relevance(self, answer: str, question: str) -> float:
|
|
"""计算回答与问题的相关性 (0-5分)"""
|
|
# 简单启发式:检查问题关键词是否出现在回答中
|
|
question_keywords = self._extract_keywords(question)
|
|
answer_lower = answer.lower()
|
|
|
|
matched = sum(1 for kw in question_keywords if kw in answer_lower)
|
|
score = (matched / len(question_keywords)) * 5 if question_keywords else 3.0
|
|
return min(score, 5.0)
|
|
|
|
def _calculate_completeness(self, answer: str, expected: str) -> float:
|
|
"""计算回答覆盖预期答案的程度"""
|
|
expected_entities = self._extract_entities(expected)
|
|
answer_entities = self._extract_entities(answer)
|
|
|
|
if not expected_entities:
|
|
return 1.0
|
|
|
|
matched = len(set(expected_entities) & set(answer_entities))
|
|
return matched / len(expected_entities)
|
|
|
|
def _calculate_citation_accuracy(self, citations: List[str], docs: List[Dict]) -> float:
|
|
"""计算引用准确性"""
|
|
if not citations:
|
|
return 0.0
|
|
|
|
doc_ids = {doc.get("id", doc.get("doc_id", "")) for doc in docs}
|
|
valid_citations = sum(1 for c in citations if any(doc_id in c for doc_id in doc_ids))
|
|
return valid_citations / len(citations)
|
|
|
|
def _calculate_fluency(self, answer: str) -> float:
|
|
"""计算流畅度 (0-5分)"""
|
|
# 简单检查:句子数量、平均句子长度
|
|
sentences = re.split(r'[。!?.!?]', answer)
|
|
sentences = [s for s in sentences if len(s.strip()) > 5]
|
|
|
|
if not sentences:
|
|
return 1.0
|
|
|
|
avg_len = sum(len(s) for s in sentences) / len(sentences)
|
|
# 理想句子长度20-50字
|
|
if 20 <= avg_len <= 50:
|
|
return 5.0
|
|
elif 10 <= avg_len < 20 or 50 < avg_len <= 80:
|
|
return 4.0
|
|
else:
|
|
return 3.0
|
|
|
|
def _detect_hallucination(self, answer: str, docs: List[Dict]) -> float:
|
|
"""检测幻觉:回答中无法验证的信息比例"""
|
|
# 简化实现:检查数字、基因ID等具体信息
|
|
doc_text = " ".join([doc.get("text", "") for doc in docs]).lower()
|
|
|
|
# 提取回答中的数字和具体声明
|
|
numbers = re.findall(r'\d+', answer)
|
|
unverified = 0
|
|
|
|
for num in numbers:
|
|
if num not in doc_text:
|
|
unverified += 1
|
|
|
|
return unverified / len(numbers) if numbers else 0.0
|
|
|
|
def _calculate_factual_precision(self, answer: str, expected: str) -> float:
|
|
"""计算事实精确度"""
|
|
# 检查回答中的事实是否与预期答案一致
|
|
answer_facts = self._extract_facts(answer)
|
|
expected_facts = self._extract_facts(expected)
|
|
|
|
if not answer_facts:
|
|
return 1.0
|
|
|
|
correct = sum(1 for fact in answer_facts if any(self._fact_match(fact, ef) for ef in expected_facts))
|
|
return correct / len(answer_facts)
|
|
|
|
def _calculate_source_attribution(self, answer: str, docs: List[Dict]) -> float:
|
|
"""计算可溯源比例"""
|
|
sentences = re.split(r'[。!?.!?]', answer)
|
|
sentences = [s for s in sentences if len(s.strip()) > 10]
|
|
|
|
if not sentences:
|
|
return 0.0
|
|
|
|
doc_text = " ".join([doc.get("text", "") for doc in docs])
|
|
attributable = sum(1 for s in sentences if any(phrase in doc_text for phrase in self._extract_key_phrases(s)))
|
|
return attributable / len(sentences)
|
|
|
|
def _detect_contradiction(self, answer: str, docs: List[Dict]) -> float:
|
|
"""检测与文档矛盾的陈述比例"""
|
|
# 简化实现:检查否定词
|
|
doc_text = " ".join([doc.get("text", "") for doc in docs]).lower()
|
|
sentences = re.split(r'[。!?.!?]', answer)
|
|
|
|
contradictions = 0
|
|
for sent in sentences:
|
|
if "不" in sent or "没有" in sent or "not" in sent.lower():
|
|
# 检查否定是否与文档冲突
|
|
positive_version = sent.replace("不", "").replace("没有", "")
|
|
if positive_version in doc_text:
|
|
contradictions += 1
|
|
|
|
return contradictions / len(sentences) if sentences else 0.0
|
|
|
|
def _check_gene_id_recognition(self, answer: str, question_data: Dict) -> float:
|
|
"""检查基因ID识别准确率"""
|
|
key_ids = question_data.get("key_ids", [])
|
|
if not key_ids:
|
|
return 1.0
|
|
|
|
recognized = sum(1 for gid in key_ids if gid in answer)
|
|
return recognized / len(key_ids)
|
|
|
|
def _check_functional_annotation(self, answer: str, question_data: Dict) -> float:
|
|
"""检查功能注释准确性"""
|
|
key_concepts = question_data.get("key_concepts", [])
|
|
if not key_concepts:
|
|
return 1.0
|
|
|
|
answer_lower = answer.lower()
|
|
matched = sum(1 for concept in key_concepts if concept.lower() in answer_lower)
|
|
return matched / len(key_concepts)
|
|
|
|
def _check_cross_species_consistency(self, answer: str, question_data: Dict) -> float:
|
|
"""检查跨物种一致性"""
|
|
key_species = question_data.get("key_species", [])
|
|
if not key_species:
|
|
return 1.0
|
|
|
|
answer_lower = answer.lower()
|
|
mentioned = sum(1 for sp in key_species if sp.lower() in answer_lower)
|
|
return mentioned / len(key_species)
|
|
|
|
def _check_ortholog_mapping(self, answer: str, question_data: Dict) -> float:
|
|
"""检查同源基因映射准确性"""
|
|
# 简化:检查是否提到了同源关系
|
|
ortholog_terms = ["ortholog", "同源", "orthologous", "paralog"]
|
|
answer_lower = answer.lower()
|
|
return 1.0 if any(term in answer_lower for term in ortholog_terms) else 0.5
|
|
|
|
def _check_go_terms(self, answer: str, question_data: Dict) -> float:
|
|
"""检查GO term匹配"""
|
|
go_terms = question_data.get("go_terms", [])
|
|
if not go_terms:
|
|
return 1.0
|
|
|
|
matched = sum(1 for go in go_terms if go in answer)
|
|
return matched / len(go_terms)
|
|
|
|
# ============== 工具方法 ==============
|
|
|
|
def _extract_keywords(self, text: str) -> List[str]:
|
|
"""提取关键词"""
|
|
# 简单实现:提取长度>2的中文字符串或英文单词
|
|
words = re.findall(r'[\u4e00-\u9fa5]{2,}|[a-zA-Z]{3,}', text.lower())
|
|
return list(set(words))
|
|
|
|
def _extract_entities(self, text: str) -> List[str]:
|
|
"""提取实体(基因名、物种名等)"""
|
|
# 基因名模式:如 AtSUC1, WRKY33, CYP71
|
|
genes = re.findall(r'[A-Z][a-z]{1,4}[0-9]{1,3}', text)
|
|
# 物种名
|
|
species = re.findall(r'[A-Z][a-z]+ [a-z]+', text)
|
|
return genes + species
|
|
|
|
def _extract_key_phrases(self, sentence: str) -> List[str]:
|
|
"""提取关键短语"""
|
|
# 提取3-10个字符的子串
|
|
phrases = []
|
|
for i in range(len(sentence) - 2):
|
|
for j in range(3, min(11, len(sentence) - i + 1)):
|
|
phrases.append(sentence[i:i+j])
|
|
return phrases
|
|
|
|
def _extract_facts(self, text: str) -> List[str]:
|
|
"""提取事实陈述"""
|
|
# 提取包含数字的句子片段
|
|
return re.findall(r'[^。!?.!?]*\d+[^。!?.!?]*', text)
|
|
|
|
def _fact_match(self, fact1: str, fact2: str) -> bool:
|
|
"""判断两个事实是否匹配"""
|
|
# 简单实现:检查重叠度
|
|
return len(set(fact1) & set(fact2)) / max(len(fact1), len(fact2)) > 0.5
|
|
|
|
|
|
# ============== 批量评估 ==============
|
|
|
|
class BatchEvaluator:
|
|
"""批量评估器"""
|
|
|
|
def __init__(self, evaluator: RAGMetricsEvaluator):
|
|
self.evaluator = evaluator
|
|
self.results: List[EvaluationResult] = []
|
|
|
|
def evaluate_testset(
|
|
self,
|
|
testset_path: str,
|
|
rag_system_func, # 调用RAG系统的函数
|
|
output_dir: str
|
|
) -> Dict:
|
|
"""
|
|
评估整个测试集
|
|
|
|
Args:
|
|
testset_path: 测试集JSON文件路径
|
|
rag_system_func: 调用RAG系统的函数,接收question,返回(answer, retrieved_docs, citations)
|
|
output_dir: 输出目录
|
|
"""
|
|
# 加载测试集
|
|
with open(testset_path, 'r', encoding='utf-8') as f:
|
|
testset = json.load(f)
|
|
|
|
# 按类别评估
|
|
category_results = defaultdict(list)
|
|
|
|
for cat_key, cat_data in testset.get("categories", {}).items():
|
|
print(f"\n评估类别: {cat_data['name']}")
|
|
|
|
for q in cat_data.get("questions", []):
|
|
print(f" 问题 {q['id']}: {q['question'][:50]}...")
|
|
|
|
# 调用RAG系统
|
|
answer, retrieved_docs, citations = rag_system_func(q['question'])
|
|
|
|
# 评估检索
|
|
relevant_docs = q.get("pmid_reference", [])
|
|
rel_metrics = self.evaluator.evaluate_retrieval(
|
|
retrieved_docs, [relevant_docs] if isinstance(relevant_docs, str) else relevant_docs
|
|
)
|
|
|
|
# 评估生成
|
|
gen_metrics = self.evaluator.evaluate_generation(
|
|
answer, q['question'], q['expected_answer'], citations, retrieved_docs
|
|
)
|
|
|
|
# 评估幻觉
|
|
hal_metrics = self.evaluator.evaluate_hallucination(
|
|
answer, retrieved_docs, q['expected_answer']
|
|
)
|
|
|
|
# 评估基因家族特有指标
|
|
gf_metrics = self.evaluator.evaluate_genefamily_specific(answer, q)
|
|
|
|
# 保存结果
|
|
result = EvaluationResult(
|
|
question_id=q['id'],
|
|
question=q['question'],
|
|
category=cat_key,
|
|
difficulty=q.get('difficulty', 'medium'),
|
|
retrieved_docs=retrieved_docs,
|
|
relevant_doc_indices=[],
|
|
generated_answer=answer,
|
|
citations=citations,
|
|
retrieval_metrics=rel_metrics,
|
|
generation_metrics=gen_metrics,
|
|
hallucination_metrics=hal_metrics,
|
|
genefamily_metrics=gf_metrics
|
|
)
|
|
|
|
self.results.append(result)
|
|
category_results[cat_key].append(result)
|
|
|
|
# 生成汇总报告
|
|
report = self._generate_report(category_results)
|
|
|
|
# 保存结果
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 保存详细结果
|
|
with open(os.path.join(output_dir, "detailed_results.json"), 'w', encoding='utf-8') as f:
|
|
json.dump([r.to_dict() for r in self.results], f, ensure_ascii=False, indent=2)
|
|
|
|
# 保存汇总报告
|
|
with open(os.path.join(output_dir, "evaluation_report.json"), 'w', encoding='utf-8') as f:
|
|
json.dump(report, f, ensure_ascii=False, indent=2)
|
|
|
|
# 保存Markdown报告
|
|
self._save_markdown_report(report, os.path.join(output_dir, "evaluation_report.md"))
|
|
|
|
return report
|
|
|
|
def _generate_report(self, category_results: Dict) -> Dict:
|
|
"""生成汇总报告"""
|
|
report = {
|
|
"summary": {},
|
|
"by_category": {},
|
|
"by_difficulty": {}
|
|
}
|
|
|
|
# 汇总所有结果
|
|
all_results = []
|
|
for cat_results in category_results.values():
|
|
all_results.extend(cat_results)
|
|
|
|
# 总体指标
|
|
report["summary"] = self._aggregate_metrics(all_results)
|
|
|
|
# 按类别汇总
|
|
for cat, results in category_results.items():
|
|
report["by_category"][cat] = self._aggregate_metrics(results)
|
|
|
|
# 按难度汇总
|
|
difficulty_groups = defaultdict(list)
|
|
for r in all_results:
|
|
difficulty_groups[r.difficulty].append(r)
|
|
|
|
for diff, results in difficulty_groups.items():
|
|
report["by_difficulty"][diff] = self._aggregate_metrics(results)
|
|
|
|
return report
|
|
|
|
def _aggregate_metrics(self, results: List[EvaluationResult]) -> Dict:
|
|
"""聚合多个结果的指标"""
|
|
if not results:
|
|
return {}
|
|
|
|
# 检索指标
|
|
avg_recall = {k: sum(r.retrieval_metrics.recall_at_k[k] for r in results if r.retrieval_metrics) / len(results)
|
|
for k in self.evaluator.k_values}
|
|
avg_precision = {k: sum(r.retrieval_metrics.precision_at_k[k] for r in results if r.retrieval_metrics) / len(results)
|
|
for k in self.evaluator.k_values}
|
|
avg_mrr = sum(r.retrieval_metrics.mrr for r in results if r.retrieval_metrics) / len(results)
|
|
|
|
# 生成指标
|
|
avg_faithfulness = sum(r.generation_metrics.faithfulness for r in results if r.generation_metrics) / len(results)
|
|
avg_relevance = sum(r.generation_metrics.answer_relevance for r in results if r.generation_metrics) / len(results)
|
|
avg_completeness = sum(r.generation_metrics.completeness for r in results if r.generation_metrics) / len(results)
|
|
|
|
# 幻觉指标
|
|
avg_hallucination = sum(r.hallucination_metrics.hallucination_rate for r in results if r.hallucination_metrics) / len(results)
|
|
avg_factual = sum(r.hallucination_metrics.factual_precision for r in results if r.hallucination_metrics) / len(results)
|
|
|
|
# 基因家族指标
|
|
avg_gene_id = sum(r.genefamily_metrics.gene_id_recognition_rate for r in results if r.genefamily_metrics) / len(results)
|
|
avg_func = sum(r.genefamily_metrics.functional_annotation_accuracy for r in results if r.genefamily_metrics) / len(results)
|
|
|
|
return {
|
|
"count": len(results),
|
|
"retrieval": {
|
|
"recall@5": round(avg_recall.get(5, 0), 4),
|
|
"precision@5": round(avg_precision.get(5, 0), 4),
|
|
"mrr": round(avg_mrr, 4)
|
|
},
|
|
"generation": {
|
|
"faithfulness": round(avg_faithfulness, 4),
|
|
"answer_relevance": round(avg_relevance, 2),
|
|
"completeness": round(avg_completeness, 4)
|
|
},
|
|
"hallucination": {
|
|
"hallucination_rate": round(avg_hallucination, 4),
|
|
"factual_precision": round(avg_factual, 4)
|
|
},
|
|
"genefamily": {
|
|
"gene_id_recognition": round(avg_gene_id, 4),
|
|
"functional_annotation": round(avg_func, 4)
|
|
},
|
|
"rag_score": round(
|
|
0.3 * avg_recall.get(5, 0) +
|
|
0.3 * avg_faithfulness +
|
|
0.2 * (1 - avg_hallucination) +
|
|
0.2 * avg_factual, 4
|
|
)
|
|
}
|
|
|
|
def _save_markdown_report(self, report: Dict, output_path: str):
|
|
"""保存Markdown格式报告"""
|
|
lines = []
|
|
lines.append("# Gene Family RAG 评估报告")
|
|
lines.append("")
|
|
lines.append("## 总体性能")
|
|
lines.append("")
|
|
|
|
summary = report.get("summary", {})
|
|
rag_score = summary.get("rag_score", 0)
|
|
|
|
lines.append(f"**RAG综合评分: {rag_score:.4f}**")
|
|
lines.append("")
|
|
|
|
# 检索性能
|
|
retrieval = summary.get("retrieval", {})
|
|
lines.append("### 检索性能")
|
|
lines.append(f"- Recall@5: {retrieval.get('recall@5', 0):.4f}")
|
|
lines.append(f"- Precision@5: {retrieval.get('precision@5', 0):.4f}")
|
|
lines.append(f"- MRR: {retrieval.get('mrr', 0):.4f}")
|
|
lines.append("")
|
|
|
|
# 生成质量
|
|
generation = summary.get("generation", {})
|
|
lines.append("### 生成质量")
|
|
lines.append(f"- Faithfulness: {generation.get('faithfulness', 0):.4f}")
|
|
lines.append(f"- Answer Relevance: {generation.get('answer_relevance', 0):.2f}/5")
|
|
lines.append(f"- Completeness: {generation.get('completeness', 0):.4f}")
|
|
lines.append("")
|
|
|
|
# 幻觉检测
|
|
hallucination = summary.get("hallucination", {})
|
|
lines.append("### 幻觉检测")
|
|
lines.append(f"- Hallucination Rate: {hallucination.get('hallucination_rate', 0):.4f}")
|
|
lines.append(f"- Factual Precision: {hallucination.get('factual_precision', 0):.4f}")
|
|
lines.append("")
|
|
|
|
# 基因家族特有
|
|
genefamily = summary.get("genefamily", {})
|
|
lines.append("### 基因家族特有指标")
|
|
lines.append(f"- Gene ID Recognition: {genefamily.get('gene_id_recognition', 0):.4f}")
|
|
lines.append(f"- Functional Annotation: {genefamily.get('functional_annotation', 0):.4f}")
|
|
lines.append("")
|
|
|
|
# 按类别详细
|
|
lines.append("## 按类别详细")
|
|
lines.append("")
|
|
for cat, metrics in report.get("by_category", {}).items():
|
|
lines.append(f"### {cat}")
|
|
lines.append(f"- RAG Score: {metrics.get('rag_score', 0):.4f}")
|
|
lines.append(f"- Faithfulness: {metrics.get('generation', {}).get('faithfulness', 0):.4f}")
|
|
lines.append(f"- Hallucination: {metrics.get('hallucination', {}).get('hallucination_rate', 0):.4f}")
|
|
lines.append("")
|
|
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
f.write("\n".join(lines))
|
|
|
|
print(f"\n报告已保存: {output_path}")
|
|
|
|
|
|
# ============== 主函数 ==============
|
|
|
|
def main():
|
|
"""主函数 - 示例用法"""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description='Gene Family RAG 评估')
|
|
parser.add_argument('--testset', '-t', required=True, help='测试集JSON文件路径')
|
|
parser.add_argument('--output', '-o', default='./evaluation_results', help='输出目录')
|
|
parser.add_argument('--k-values', '-k', default='1,3,5,10', help='K值列表,逗号分隔')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# 解析K值
|
|
k_values = [int(k) for k in args.k_values.split(',')]
|
|
|
|
# 创建评估器
|
|
evaluator = RAGMetricsEvaluator(k_values=k_values)
|
|
batch_evaluator = BatchEvaluator(evaluator)
|
|
|
|
# 示例RAG系统函数(实际使用时替换为真实系统)
|
|
def mock_rag_system(question: str) -> Tuple[str, List[Dict], List[str]]:
|
|
"""模拟RAG系统调用"""
|
|
# 这里应该调用实际的RAG系统
|
|
return (
|
|
f"这是关于'{question}'的模拟回答。",
|
|
[{"id": "doc1", "text": "模拟文档内容"}],
|
|
["doc1"]
|
|
)
|
|
|
|
# 运行评估
|
|
print("=" * 60)
|
|
print("Gene Family RAG 系统评估")
|
|
print("=" * 60)
|
|
|
|
report = batch_evaluator.evaluate_testset(
|
|
testset_path=args.testset,
|
|
rag_system_func=mock_rag_system,
|
|
output_dir=args.output
|
|
)
|
|
|
|
# 打印汇总
|
|
print("\n" + "=" * 60)
|
|
print("评估完成!")
|
|
print(f"RAG综合评分: {report['summary'].get('rag_score', 0):.4f}")
|
|
print(f"详细结果保存至: {args.output}")
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|