Spaces:
Sleeping
Sleeping
| """ | |
| 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)] | |
| 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 | |
| ) |