H2HMEM-Submit / evaluate.py
varib's picture
Update evaluate.py
f81ee9b verified
Raw
History Blame Contribute Delete
33.3 kB
"""
H2HMem Leaderboard 评估模块
包含两种评估方式:
1. 词法指标(Lexical Metrics):Precision, Recall, F1, BLEU-1
2. LLM-as-judge:使用大语言模型评估答案质量
支持按 main_type (Memory Recall/Reasoning/Application) 和 sub_type 分类统计
"""
import json
import re
import logging
import os
from pathlib import Path
from typing import Dict, List, Any, Set, Optional, Tuple
from collections import defaultdict
import numpy as np
# 英文 NLP 工具
try:
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction
NLTK_AVAILABLE = True
except ImportError:
NLTK_AVAILABLE = False
raise ImportError("请安装 nltk: pip install nltk")
# LLM-as-judge 依赖
try:
from openai import OpenAI
import tenacity
OPENAI_AVAILABLE = True
except ImportError:
OPENAI_AVAILABLE = False
print("警告: openai 未安装,LLM-as-judge 功能不可用")
# 下载 nltk 数据
try:
nltk.data.find('tokenizers/punkt')
except LookupError:
nltk.download('punkt', quiet=True)
try:
nltk.data.find('corpora/stopwords')
except LookupError:
nltk.download('stopwords', quiet=True)
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# =========================================================
# 常量定义:类型映射
# =========================================================
# Main type 到类别的映射
MAIN_TYPE_MAPPING = {
"Memory Recall": "recall",
"Memory Reasoning": "reasoning",
"Memory Application": "application"
}
# Sub type 全称到简称的映射
SUB_TYPE_TO_ABBR = {
# Memory Recall 子类型
"Unimodal Precise Recall": "UPR",
"Cross-modal Related Retrieval": "CRR",
"Knowledge Resolution": "KR",
# Memory Reasoning 子类型
"Temporal Reasoning": "TR",
"Multimodal Causal Inference": "MCR",
"Reference & Evolution Tracking": "RET",
# Memory Application 子类型
"Test-Time Learning": "TTL",
"Conflict Detection": "CD",
"Answer Refusal": "AR"
}
# 指标列表
METRICS = ["LLM", "P", "R", "F1", "BLEU-1"]
# 数据集列表
DATASETS = ["D", "M", "D&M"]
# =========================================================
# 第一部分:词法指标计算器
# =========================================================
class EnglishTextProcessor:
"""英文文本处理器"""
def __init__(self, use_stopwords: bool = True):
self.use_stopwords = use_stopwords
self.stopwords = self._load_stopwords() if use_stopwords else set()
def _load_stopwords(self) -> Set[str]:
stop_words = set(stopwords.words('english'))
extra = {'.', ',', '!', '?', ';', ':', '"', "'", '(', ')', '[', ']',
'{', '}', '-', '–', '—', '...', '..'}
stop_words.update(extra)
return stop_words
def tokenize(self, text: str, remove_stopwords: bool = True) -> List[str]:
if not text:
return []
text = text.lower()
text = re.sub(r'\s+', ' ', text).strip()
try:
tokens = word_tokenize(text)
except Exception:
tokens = text.split()
filtered = []
for t in tokens:
t = t.strip()
if not t:
continue
if remove_stopwords and self.use_stopwords and t in self.stopwords:
continue
if all(ch in "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" for ch in t):
continue
filtered.append(t)
return filtered
class LexicalMetricsCalculator:
"""词法指标计算器"""
def __init__(self):
self.text_processor = EnglishTextProcessor()
def calculate(self, prediction: str, reference: str) -> Dict[str, float]:
return {
'precision': self._precision(prediction, reference),
'recall': self._recall(prediction, reference),
'f1': self._f1(prediction, reference),
'bleu1': self._bleu1(prediction, reference),
}
def _precision(self, pred: str, ref: str) -> float:
p = set(self.text_processor.tokenize(pred))
r = set(self.text_processor.tokenize(ref))
if not p:
return 0.0
inter = p & r
return len(inter) / len(p)
def _recall(self, pred: str, ref: str) -> float:
p = set(self.text_processor.tokenize(pred))
r = set(self.text_processor.tokenize(ref))
if not r:
return 0.0
inter = p & r
return len(inter) / len(r)
def _f1(self, pred: str, ref: str) -> float:
p = set(self.text_processor.tokenize(pred))
r = set(self.text_processor.tokenize(ref))
if not p or not r:
return 0.0
inter = p & r
if not inter:
return 0.0
prec = len(inter) / len(p)
rec = len(inter) / len(r)
return 2 * prec * rec / (prec + rec) if (prec + rec) > 0 else 0.0
def _bleu1(self, prediction: str, reference: str, **kwargs) -> float:
pred_tokens = self.text_processor.tokenize(prediction, remove_stopwords=False)
ref_tokens = self.text_processor.tokenize(reference, remove_stopwords=False)
if not pred_tokens or not ref_tokens:
return 0.0
if pred_tokens == ref_tokens:
return 1.0
try:
smoothie = SmoothingFunction().method4
bleu_score = sentence_bleu(
[ref_tokens],
pred_tokens,
weights=(1.0, 0, 0, 0),
smoothing_function=smoothie
)
return float(bleu_score)
except Exception:
return 0.0
# =========================================================
# 第二部分:LLM-as-judge 评估器(支持批量评估)
# =========================================================
class LLMJudgeEvaluator:
"""LLM-as-judge 评估器 - 支持批量评估"""
SCORE_RUBRIC = """
**Score 0 (Incorrect / Miss):**
- The answer contradicts the Ground Truth.
- For Yes/No questions: The answer has the wrong polarity.
**Score 0.25 (Poor / Tangential):**
- Touches on topic but misses core entity.
- Contains significant hallucinations.
**Score 0.5 (Partial / Vague):**
- Technically correct but incomplete.
- Captures main entity but misses details.
**Score 0.75 (Good / Minor Imperfection):**
- Largely accurate, misses only minor details.
**Score 1 (Correct / Exact):**
- Accurate, precise, and complete.
"""
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = config or {}
self.api_key = self.config.get('api_key') or os.getenv('OPENAI_API_KEY')
self.base_url = self.config.get('base_url') or os.getenv('OPENAI_BASE_URL', 'https://api.openai.com/v1')
self.model_name = self.config.get('model_name', 'gpt-4o-mini')
self.temperature = self.config.get('temperature', 0)
self.batch_size = self.config.get('batch_size', 5)
self._client = None
def _get_client(self):
if self._client is None:
if not self.api_key:
raise ValueError("未设置OPENAI_API_KEY")
self._client = OpenAI(api_key=self.api_key, base_url=self.base_url)
return self._client
def _build_batch_prompt(self, qa_pairs: List[Dict[str, str]]) -> str:
"""构建批量评估的提示词,包含临时 ID"""
prompt = f"""You are an impartial judge evaluating an AI assistant's answers.
You will evaluate {len(qa_pairs)} questions and provide scores for each.
### Scoring Rubric
{self.SCORE_RUBRIC}
### Questions and Answers
"""
for item in qa_pairs:
prompt += f"""
--- Question ID: {item['temp_id']} ---
Question: {item['question']}
Ground Truth: {item['ground_truth']}
Assistant Answer: {item['assistant_answer'] if item['assistant_answer'] else "(No answer provided)"}
"""
prompt += """
### Output Format
Output strictly in JSON format as a list of objects, each containing the question_id and score:
[
{"question_id": 0, "score": 0.75, "reasoning": "<explanation>"},
{"question_id": 1, "score": 0.5, "reasoning": "<explanation>"},
...
]
Each score must be one of: 0, 0.25, 0.5, 0.75, or 1.
The number of objects must equal the number of questions.
The question_id must match the ID provided above.
"""
return prompt
def _parse_batch_response(self, response_text: str, qa_pairs: List[Dict[str, str]]) -> List[Dict[str, Any]]:
"""解析批量评估的响应,按临时 ID 匹配"""
expected_count = len(qa_pairs)
# 创建 ID 到结果的映射
result_map = {item['temp_id']: None for item in qa_pairs}
try:
# 尝试解析 JSON 数组
json_match = re.search(r'\[.*\]', response_text, re.DOTALL)
if json_match:
parsed_results = json.loads(json_match.group())
if isinstance(parsed_results, list):
for r in parsed_results:
if isinstance(r, dict) and 'question_id' in r:
qid = r['question_id']
score = float(r.get('score', 0))
valid_scores = [0, 0.25, 0.5, 0.75, 1]
if score not in valid_scores:
score = min(valid_scores, key=lambda x: abs(x - score))
result_map[qid] = {
'score': score,
'reasoning': r.get('reasoning', ''),
'success': True
}
# 为每个问题返回结果(未匹配的使用默认值)
results = []
for item in qa_pairs:
qid = item['temp_id']
if result_map[qid] is not None:
results.append(result_map[qid])
else:
logger.warning(f"问题 ID {qid} 未在响应中找到,使用默认值")
results.append({'score': 0, 'reasoning': 'Not found in response', 'success': False})
return results
except Exception as e:
logger.error(f"解析批量响应失败: {e}")
return [{'score': 0, 'reasoning': str(e), 'success': False} for _ in range(expected_count)]
@tenacity.retry(
stop=tenacity.stop_after_attempt(3),
wait=tenacity.wait_exponential(multiplier=1, min=1, max=10),
retry=tenacity.retry_if_exception_type(Exception),
)
def evaluate_batch(self, qa_pairs: List[Dict[str, str]]) -> List[Dict[str, Any]]:
"""批量评估多个问答对"""
if not qa_pairs:
return []
try:
client = self._get_client()
prompt = self._build_batch_prompt(qa_pairs)
response = client.chat.completions.create(
model=self.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=self.temperature,
timeout=120
)
response_text = response.choices[0].message.content.strip()
return self._parse_batch_response(response_text, qa_pairs)
except Exception as e:
logger.error(f"批量LLM评估失败: {e}")
return [{'score': 0, 'reasoning': str(e), 'success': False} for _ in range(len(qa_pairs))]
def evaluate_single(self, question: str, ground_truth: str, assistant_answer: str) -> Dict[str, Any]:
"""单个评估(向后兼容)"""
result = self.evaluate_batch([{
'temp_id': 0,
'question': question,
'ground_truth': ground_truth,
'assistant_answer': assistant_answer
}])
return result[0] if result else {'score': 0, 'reasoning': 'Failed', 'success': False}
# =========================================================
# 第三部分:评估结果收集器
# =========================================================
class ScoreCollector:
"""分数收集器,用于按类别统计加权平均"""
def __init__(self):
self.scores = defaultdict(list)
self.weights = defaultdict(int)
def add(self, category: str, score: float):
self.scores[category].append(score)
self.weights[category] += 1
def get_weighted_average(self, category: str = None) -> float:
if category:
scores = self.scores.get(category, [])
return np.mean(scores) if scores else 0.0
total_score = 0
total_weight = 0
for cat, scores in self.scores.items():
total_score += sum(scores)
total_weight += len(scores)
return total_score / total_weight if total_weight > 0 else 0.0
def get_all_averages(self) -> Dict[str, float]:
return {cat: np.mean(scores) if scores else 0.0
for cat, scores in self.scores.items()}
# =========================================================
# 第四部分:主评估器
# =========================================================
class SubmissionEvaluator:
"""提交文件评估器"""
def __init__(self, llm_config: Optional[Dict[str, Any]] = None,
enable_llm_judge: bool = True):
self.lexical_calculator = LexicalMetricsCalculator()
self.enable_llm_judge = enable_llm_judge and OPENAI_AVAILABLE
if self.enable_llm_judge and llm_config:
try:
self.llm_judge = LLMJudgeEvaluator(llm_config)
logger.info(f"LLM-as-judge 已启用,批量大小: {self.llm_judge.batch_size}")
except Exception as e:
logger.warning(f"LLM-as-judge 初始化失败: {e}")
self.enable_llm_judge = False
self.llm_judge = None
else:
self.llm_judge = None
def load_predictions(self, file_path: str) -> List[Dict]:
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
if 'predictions' not in data:
raise ValueError(f"文件格式错误: 缺少 'predictions' 字段")
return data['predictions']
def evaluate_single_prediction(self, pred: Dict) -> Dict[str, float]:
"""评估单个预测的词法指标(不包含 LLM)"""
system_answer = pred.get('system_answer', '').strip()
original_answer = pred.get('original_answer', '').strip()
lexical = self.lexical_calculator.calculate(system_answer, original_answer)
return {
'P': lexical['precision'],
'R': lexical['recall'],
'F1': lexical['f1'],
'BLEU-1': lexical['bleu1']
}
def evaluate_dataset(self, predictions: List[Dict], dataset_name: str, enable_llm: bool = True) -> Dict[str, Any]:
"""
评估单个数据集,按 main_type 和 sub_type 分类统计
"""
# 存储每个问题的词法指标
all_lexical_metrics = []
category_lexical_metrics = {cat: [] for cat in SUB_TYPE_TO_ABBR.values()}
# 按 main_type 和 sub_type 分类的收集器(词法指标)
main_type_lexical = {mt: [] for mt in ['recall', 'reasoning', 'application']}
sub_type_lexical = {st: [] for st in SUB_TYPE_TO_ABBR.values()}
# 存储每个问题的完整信息(用于后续分类)
questions_info = []
# 第一遍:计算词法指标,收集问题信息
for idx, pred in enumerate(predictions):
system_answer = pred.get('system_answer', '').strip()
original_answer = pred.get('original_answer', '').strip()
question_text = pred.get('question_text', '')
# 获取类型信息
main_type_full = pred.get('question_type', {}).get('main_type', '')
sub_type_full = pred.get('question_type', {}).get('sub_type', '')
main_type = MAIN_TYPE_MAPPING.get(main_type_full, '')
sub_type_abbr = SUB_TYPE_TO_ABBR.get(sub_type_full, '')
# 计算词法指标
lexical_scores = self.evaluate_single_prediction(pred)
# 存储信息
questions_info.append({
'index': idx,
'main_type': main_type,
'sub_type': sub_type_abbr,
'lexical_scores': lexical_scores,
'question_text': question_text,
'original_answer': original_answer,
'system_answer': system_answer
})
# 收集词法指标
all_lexical_metrics.append(lexical_scores)
# 按类别收集词法指标
if sub_type_abbr and sub_type_abbr in category_lexical_metrics:
category_lexical_metrics[sub_type_abbr].append(lexical_scores)
# 按 main_type 收集
if main_type and main_type in main_type_lexical:
main_type_lexical[main_type].append(lexical_scores)
# 按 sub_type 收集
if sub_type_abbr and sub_type_abbr in sub_type_lexical:
sub_type_lexical[sub_type_abbr].append(lexical_scores)
# =====================================================
# LLM-as-judge 批量评估
# =====================================================
llm_scores_by_index = {} # 索引 -> LLM分数
if self.enable_llm_judge and enable_llm and self.llm_judge:
# 准备批量评估数据
batch_size = self.llm_judge.batch_size
total_questions = len(questions_info)
total_batches = (total_questions + batch_size - 1) // batch_size
logger.info(f"开始批量LLM评估,共 {total_questions} 个问题,分成 {total_batches} 批(每批最多 {batch_size} 个)")
for batch_start in range(0, total_questions, batch_size):
batch_end = min(batch_start + batch_size, total_questions)
batch_questions = questions_info[batch_start:batch_end]
current_batch_num = batch_start // batch_size + 1
logger.info(f"评估第 {current_batch_num}/{total_batches} 批,包含 {len(batch_questions)} 个问题...")
# 准备批量数据,添加临时 ID
batch_data = []
for i, q in enumerate(batch_questions):
batch_data.append({
'temp_id': batch_start + i, # 使用原始索引作为临时 ID
'question': q['question_text'],
'ground_truth': q['original_answer'],
'assistant_answer': q['system_answer']
})
# 批量调用 LLM
batch_results = self.llm_judge.evaluate_batch(batch_data)
# 存储结果
for i, result in enumerate(batch_results):
original_idx = batch_start + i
if result['success']:
llm_scores_by_index[original_idx] = result['score']
else:
llm_scores_by_index[original_idx] = 0.0
success_count = len([r for r in batch_results if r['success']])
logger.info(f"第 {current_batch_num} 批完成,成功 {success_count}/{len(batch_questions)} 个")
logger.info(f"LLM批量评估完成,总成功 {len(llm_scores_by_index)}/{total_questions} 个")
# =====================================================
# 收集完整的分数(词法 + LLM)
# =====================================================
# 用于收集按类别的 LLM 分数
main_type_llm = {mt: [] for mt in ['recall', 'reasoning', 'application']}
sub_type_llm = {st: [] for st in SUB_TYPE_TO_ABBR.values()}
for q in questions_info:
idx = q['index']
llm_score = llm_scores_by_index.get(idx, 0.0)
# 按 main_type 收集 LLM 分数
if q['main_type'] and q['main_type'] in main_type_llm:
main_type_llm[q['main_type']].append(llm_score)
# 按 sub_type 收集 LLM 分数
if q['sub_type'] and q['sub_type'] in sub_type_llm:
sub_type_llm[q['sub_type']].append(llm_score)
# =====================================================
# 计算各类别的平均分
# =====================================================
def calc_avg(scores_list):
return np.mean(scores_list) if scores_list else 0.0
# 计算整体词法平均
overall_lexical = {
'P': calc_avg([m['P'] for m in all_lexical_metrics]),
'R': calc_avg([m['R'] for m in all_lexical_metrics]),
'F1': calc_avg([m['F1'] for m in all_lexical_metrics]),
'BLEU-1': calc_avg([m['BLEU-1'] for m in all_lexical_metrics])
}
# 计算整体 LLM 平均
overall_llm = calc_avg(list(llm_scores_by_index.values()))
# 构建结果
result = {
'total': len(predictions),
'main_type_counts': {mt: len(main_type_lexical[mt]) for mt in main_type_lexical},
'sub_type_counts': {st: len(sub_type_lexical[st]) for st in sub_type_lexical}
}
# 为每个指标构建结果
for metric in METRICS:
result[metric] = {
'overall': 0.0,
'recall': {'score': 0.0, 'sub_types': {}},
'reasoning': {'score': 0.0, 'sub_types': {}},
'application': {'score': 0.0, 'sub_types': {}}
}
if metric == 'LLM':
# LLM 指标
result[metric]['overall'] = overall_llm
for mt in ['recall', 'reasoning', 'application']:
result[metric][mt]['score'] = calc_avg(main_type_llm.get(mt, []))
for st, abbr in SUB_TYPE_TO_ABBR.items():
if abbr in ['UPR', 'CRR', 'KR']:
result[metric]['recall']['sub_types'][abbr] = calc_avg(sub_type_llm.get(abbr, []))
elif abbr in ['TR', 'MCR', 'RET']:
result[metric]['reasoning']['sub_types'][abbr] = calc_avg(sub_type_llm.get(abbr, []))
elif abbr in ['TTL', 'CD', 'AR']:
result[metric]['application']['sub_types'][abbr] = calc_avg(sub_type_llm.get(abbr, []))
else:
# 词法指标
metric_key = metric if metric != 'BLEU-1' else 'BLEU-1'
result[metric]['overall'] = overall_lexical.get(metric_key, 0.0)
for mt in ['recall', 'reasoning', 'application']:
scores = [m[metric_key] for m in main_type_lexical.get(mt, [])]
result[metric][mt]['score'] = calc_avg(scores)
for st, abbr in SUB_TYPE_TO_ABBR.items():
scores = [m[metric_key] for m in sub_type_lexical.get(abbr, [])]
if abbr in ['UPR', 'CRR', 'KR']:
result[metric]['recall']['sub_types'][abbr] = calc_avg(scores)
elif abbr in ['TR', 'MCR', 'RET']:
result[metric]['reasoning']['sub_types'][abbr] = calc_avg(scores)
elif abbr in ['TTL', 'CD', 'AR']:
result[metric]['application']['sub_types'][abbr] = calc_avg(scores)
return result
def evaluate_submission(self, dyadic_path: str, multiparty_path: str,
method_name: str = "", backbone: str = "",
category: str = "", organization: str = "",
paper_url: str = "", code_url: str = "") -> Dict[str, Any]:
"""
评估完整的提交,返回 pending_leaderboard 格式的数据
"""
try:
# 加载预测文件
dyadic_preds = self.load_predictions(dyadic_path)
multiparty_preds = self.load_predictions(multiparty_path)
logger.info(f"Dyadic: {len(dyadic_preds)} 个问题")
logger.info(f"Multiparty: {len(multiparty_preds)} 个问题")
# 分别评估两种对话类型
dyadic_results = self.evaluate_dataset(dyadic_preds, "D")
multiparty_results = self.evaluate_dataset(multiparty_preds, "M")
# 计算 D&M 的加权平均(按问题数量)
total_d = dyadic_results['total']
total_m = multiparty_results['total']
total_all = total_d + total_m
dm_results = {'total': total_all}
# 对于每个指标,计算加权平均
for metric in METRICS:
dm_results[metric] = {'overall': 0.0, 'recall': {'score': 0.0, 'sub_types': {}},
'reasoning': {'score': 0.0, 'sub_types': {}},
'application': {'score': 0.0, 'sub_types': {}}}
if total_all > 0:
# overall 加权平均
dm_results[metric]['overall'] = (
dyadic_results[metric]['overall'] * total_d +
multiparty_results[metric]['overall'] * total_m
) / total_all
# recall 加权平均
dm_results[metric]['recall']['score'] = (
dyadic_results[metric]['recall']['score'] * total_d +
multiparty_results[metric]['recall']['score'] * total_m
) / total_all
# reasoning 加权平均
dm_results[metric]['reasoning']['score'] = (
dyadic_results[metric]['reasoning']['score'] * total_d +
multiparty_results[metric]['reasoning']['score'] * total_m
) / total_all
# application 加权平均
dm_results[metric]['application']['score'] = (
dyadic_results[metric]['application']['score'] * total_d +
multiparty_results[metric]['application']['score'] * total_m
) / total_all
# 子类型加权平均
for sub in ['UPR', 'CRR', 'KR']:
dm_results[metric]['recall']['sub_types'][sub] = (
dyadic_results[metric]['recall']['sub_types'].get(sub, 0) * total_d +
multiparty_results[metric]['recall']['sub_types'].get(sub, 0) * total_m
) / total_all
for sub in ['TR', 'MCR', 'RET']:
dm_results[metric]['reasoning']['sub_types'][sub] = (
dyadic_results[metric]['reasoning']['sub_types'].get(sub, 0) * total_d +
multiparty_results[metric]['reasoning']['sub_types'].get(sub, 0) * total_m
) / total_all
for sub in ['TTL', 'CD', 'AR']:
dm_results[metric]['application']['sub_types'][sub] = (
dyadic_results[metric]['application']['sub_types'].get(sub, 0) * total_d +
multiparty_results[metric]['application']['sub_types'].get(sub, 0) * total_m
) / total_all
# 构建 pending_leaderboard 条目
pending_entry = {
"method": {
"name": method_name,
"backbone": backbone,
"category": category,
"organization": organization,
"paper_url": paper_url,
"code_url": code_url
},
"results": {
"D": {},
"M": {},
"D&M": {}
}
}
# 填充 D 结果
for metric in METRICS:
pending_entry["results"]["D"][metric] = {
"overall": dyadic_results[metric]['overall'],
"recall": dyadic_results[metric]['recall'],
"reasoning": dyadic_results[metric]['reasoning'],
"application": dyadic_results[metric]['application']
}
# 填充 M 结果
for metric in METRICS:
pending_entry["results"]["M"][metric] = {
"overall": multiparty_results[metric]['overall'],
"recall": multiparty_results[metric]['recall'],
"reasoning": multiparty_results[metric]['reasoning'],
"application": multiparty_results[metric]['application']
}
# 填充 D&M 结果
for metric in METRICS:
pending_entry["results"]["D&M"][metric] = {
"overall": dm_results[metric]['overall'],
"recall": dm_results[metric]['recall'],
"reasoning": dm_results[metric]['reasoning'],
"application": dm_results[metric]['application']
}
# 构建 dataset_scores(用于 pending_submissions 表格)
dataset_scores = {
"D": {},
"M": {},
"D&M": {}
}
for dataset in ["D", "M", "D&M"]:
for metric in METRICS:
dataset_scores[dataset][metric] = pending_entry["results"][dataset][metric]["overall"]
return {
'pending_leaderboard_entry': pending_entry,
'dataset_scores': dataset_scores
}
except Exception as e:
logger.error(f"评估失败: {e}")
import traceback
traceback.print_exc()
# 返回空结果
empty_entry = {
"method": {"name": method_name, "backbone": backbone, "category": category,
"organization": organization, "paper_url": paper_url, "code_url": code_url},
"results": {}
}
for dataset in DATASETS:
empty_entry["results"][dataset] = {}
for metric in METRICS:
empty_entry["results"][dataset][metric] = {
"overall": 0.0,
"recall": {"score": 0.0, "sub_types": {"UPR": 0, "CRR": 0, "KR": 0}},
"reasoning": {"score": 0.0, "sub_types": {"TR": 0, "MCR": 0, "RET": 0}},
"application": {"score": 0.0, "sub_types": {"TTL": 0, "CD": 0, "AR": 0}}
}
empty_scores = {d: {m: 0.0 for m in METRICS} for d in DATASETS}
return {
'pending_leaderboard_entry': empty_entry,
'dataset_scores': empty_scores
}
# =========================================================
# 全局函数
# =========================================================
_evaluator = None
def evaluate_submission(dyadic_path: str, multiparty_path: str,
method_name: str = "", backbone: str = "",
category: str = "", organization: str = "",
paper_url: str = "", code_url: str = "") -> Dict[str, Any]:
"""
评估提交的函数入口
Returns:
包含 pending_leaderboard_entry 和 dataset_scores 的字典
"""
global _evaluator
if _evaluator is None:
llm_config = {
'api_key': os.getenv('OPENAI_API_KEY'),
'base_url': os.getenv('OPENAI_BASE_URL', 'https://api.openai.com/v1'),
'model_name': os.getenv('LLM_JUDGE_MODEL', 'gpt-4o-mini'),
'temperature': 0,
'batch_size': 5 # 每批最多5个问题
}
_evaluator = SubmissionEvaluator(llm_config=llm_config, enable_llm_judge=True)
return _evaluator.evaluate_submission(
dyadic_path, multiparty_path,
method_name, backbone, category,
organization, paper_url, code_url
)