LM Evaluation Harness标准化评测:让不同模型的分数真正可比

📅 发布时间:2026/7/16 18:36:31
LM Evaluation Harness标准化评测:让不同模型的分数真正可比 LM Evaluation Harness标准化评测让不同模型的分数真正可比一、模型评测中的度量的度量问题LLM领域存在一个悖论每周都有新模型声称在某个benchmark上达到SOTA但用户在实际使用中却难以感知这些领先带来的差异。问题的根源之一在于评测协议的不统一——不同团队在同一个benchmark如MMLU、HellaSwag上使用了不同的提示模板、少样本示例选择、答案解析规则和评分归一化方式。这些看似微小的协议差异累积起来可以造成数个百分点有时甚至超过5%的分数差异。EleutherAI的LM Evaluation Harnesslm-eval项目试图解决这一问题通过统一且可复现的评测框架使得不同模型在同一benchmark上的分数真正具有可比性。它的核心价值不在于提供新的benchmark而在于标准化了如何评测的协议细节。二、lm-eval的架构设计与任务注册lm-eval的核心架构由三个抽象层次组成模型接口Model API、任务定义Task和评测流水线Evaluation Pipeline。模型接口层通过统一接口适配不同的模型类型。lm_eval.api.model.LM是所有模型的基类子类包括HuggingFaceLM、OpenAILM、VLLM等。这一层封装了tokenization、generation和loglikelihood计算的差异。任务定义层每个benchmark被定义为一个继承自Task的类包含数据集加载、prompt构造、答案提取和指标计算的完整逻辑。评测流水线层lm_eval.evaluator编排整个评测过程包括任务分发、批处理、结果聚合和报告生成。from lm_eval.api.task import Task from lm_eval.api.registry import register_task from datasets import load_dataset from typing import List, Dict, Optional register_task(custom_qa) class CustomQATask(Task): 自定义问答任务的lm-eval注册示例。 继承Task基类并实现以下关键方法 - has_training_docs/has_validation_docs: 数据集划分 - training_docs/validation_docs: 数据迭代器 - doc_to_text: 文档→prompt文本转换 - doc_to_target: 文档→参考答案转换 - construct_requests: 构建模型请求 - process_results: 处理模型输出并计算指标 通过register_task装饰器将任务自动注册到全局任务表。 VERSION 1.0 # 任务版本确保可复现性 DATASET_PATH custom/qa_dataset def __init__(self): super().__init__() self.dataset None def has_validation_docs(self) - bool: return True def has_training_docs(self) - bool: return False def validation_docs(self): 返回验证集的数据迭代器。 懒加载数据集在首次访问时从HuggingFace/Disk加载。 if self.dataset is None: self.dataset load_dataset( self.DATASET_PATH, splitvalidation ) return self.dataset def doc_to_text(self, doc: dict) - str: 将数据集文档转换为模型输入的prompt文本。 这是评测标准化最关键的步骤之一 不同prompt格式会显著影响模型行为。 Args: doc: 数据集中的单条数据包含question等字段 Returns: 格式化后的prompt字符串 return f问题{doc[question]}\n答案 def doc_to_target(self, doc: dict) - str: 提供参考答案用于loglikelihood评测模式。 Args: doc: 数据集中的单条数据 Returns: 参考答案字符串 return f {doc[answer]} def construct_requests(self, doc: dict, ctx: str): 构造对模型的评测请求。 支持两种评测模式 1. generate_until: 开放式生成用于代码、翻译等 2. loglikelihood: 计算答案的对数似然用于选择题 Args: doc: 数据集文档 ctx: doc_to_text生成的prompt前缀 Returns: 模型请求列表 from lm_eval.api.instance import Instance # loglikelihood模式计算候选答案的概率 continuation self.doc_to_target(doc) return [ Instance( request_typeloglikelihood, docdoc, arguments(ctx, continuation), idx0 ) ] def process_results(self, doc: dict, results: list) - dict: 处理模型输出并计算评测指标。 Args: doc: 原始数据文档 results: 模型对construct_requests的响应 Returns: {指标名: 指标值} 的字典 # 对于loglikelihood模式results[0]是(ll_true, ll_false)或(ll, is_greedy) ll_true, _ results[0] # 提取预测答案选择对数似然更高的候选 # 此处简化处理实际任务可能需要更复杂的解析 return { accuracy: 1.0 if ll_true -float(inf) else 0.0 }三、Few-shot示例的标准化采样Few-shot评测中示例的选择和排列顺序对结果有显著影响。相同的5个示例按不同顺序排列可能导致1-3%的精度波动。lm-eval通过固定seed和系统化的采样策略来控制这一变量。import random from typing import List, Dict, Iterator class FewShotSampler: 标准化的少样本示例采样器。 关键设计 1. 使用固定seed确保采样可复现 2. 从训练集中采用系统化采样非简单随机 3. 支持示例数量的梯度设计0-shot, 1-shot, ..., n-shot 系统化采样 vs 简单随机采样的优势 系统化采样保证了示例在训练集中的均匀覆盖 避免了随机采样可能导致的分布偏差。 def __init__(self, seed: int 1234, num_fewshot: int 5): self.rng random.Random(seed) self.num_fewshot num_fewshot def sample( self, docs: List[dict], current_doc: dict ) - List[dict]: 从训练文档中采样少样本示例。 采样策略 1. 排除当前文档避免数据泄露 2. 从剩余文档中系统化采样 3. 保持示例顺序的一致性通过seed控制 Args: docs: 训练集文档列表 current_doc: 当前评测文档需排除 Returns: 采样出的few-shot示例列表 # 排除当前文档防止in-context leakage candidates [ d for d in docs if d.get(question) ! current_doc.get(question) ] if len(candidates) self.num_fewshot: return candidates # 系统化采样计算步长均匀选取 step len(candidates) / self.num_fewshot sampled [ candidates[int(i * step)] for i in range(self.num_fewshot) ] return sampled def format_fewshot_prompt( self, examples: List[dict], current_doc: dict, doc_to_text_fn, doc_to_target_fn ) - str: 将采样示例和当前文档格式化为完整prompt。 格式 [Example 1 text target] [Example 2 text target] ... [Example n text target] [Current doc text] ← 模型需补全 Args: examples: 少样本示例列表 current_doc: 当前评测文档 doc_to_text_fn: 文档→输入文本的转换函数 doc_to_target_fn: 文档→目标答案的转换函数 Returns: 完整的few-shot prompt字符串 prompt_parts [] for ex in examples: prompt_parts.append( doc_to_text_fn(ex) doc_to_target_fn(ex) ) prompt_parts.append(doc_to_text_fn(current_doc)) return \n\n.join(prompt_parts)四、答案解析的协议标准化在生成式评测generate_until模式中模型的原始输出需要经过答案提取才能与参考答案比较。以数学推理任务GSM8K为例模型可能输出因此答案是42。、答案是42或 42——需要一个鲁棒的提取器从这些变体中抽出最终数值答案。lm-eval为此提供了一系列可配置的解析器并通过版本化的任务定义确保解析逻辑的一致性。import re from typing import Optional class AnswerExtractor: 标准化的答案提取器集合。 不同任务需要不同的提取策略 - 数学题提取最后一个数值表达式 - 多选题提取括号中的选项字母 - 开放式QA提取第一句话或整个生成 staticmethod def extract_math_answer(text: str) - Optional[str]: 从数学推理输出中提取最终数值答案。 匹配模式优先级 1. 答案是 X / answer is X 模式 2. X 模式算式末尾的数值 3. 文本中最后一个数值 Args: text: 模型的原始生成文本 Returns: 提取出的数值字符串若无法提取则返回None # 模式1显式的答案声明 pattern_answer_tag r(?:答案是|答案|answer is)\s*([\d,\.\-]) match re.search(pattern_answer_tag, text, re.IGNORECASE) if match: return match.group(1).replace(,, ) # 模式2等式末尾的数值 pattern_equal r\s*([\d,\.\-])\s*(?:$|\n) matches re.findall(pattern_equal, text) if matches: return matches[-1].replace(,, ) # 模式3文本中最末尾的数值 pattern_number r[\d,\.\-] matches re.findall(pattern_number, text) if matches: return matches[-1].replace(,, ) return None staticmethod def extract_multiple_choice(text: str) - Optional[str]: 从多选题输出中提取选项字母。 支持格式 - (A) xxx / A. xxx / A) xxx - 单独的括号包裹: (A) Args: text: 模型生成文本 Returns: 提取的选项字母如A、B # 括号括起来的单个大写字母 pattern r\(([A-D])\) match re.search(pattern, text) if match: return match.group(1) # 行首或空格后的大写字母后跟句点或括号 pattern2 r(?:^|\s)([A-D])[\.\)] match re.search(pattern2, text) if match: return match.group(1) return None五、总结LM Evaluation Harness的价值不在于它的技术复杂度——它本质上是一个编排框架——而在于它通过标准化协议消除了模型评测中的隐性变量。当我们在论文中引用MMLU 85.2%时这个数字的意义完全取决于评测协议的每个细节prompt模板、few-shot示例、答案解析规则。lm-eval将这些细节固化在版本化的任务定义中使得数字的复现和比较成为可能。落地建议(1) 使用lm-eval时始终在论文或报告中注明任务版本号如mmlu:1.0和commit hash(2) 自定义任务时继承框架的Task基类而非从头实现以确保与生态的互操作性(3) 在比较两个模型的分数前确认它们使用的是相同的lm-eval版本和任务配置——不同版本之间的分数不具有直接可比性。