lm-evaluation-harness自定义评估循环实战:从simple_evaluate到全流程定制
【免费下载链接】lm-evaluation-harnessA framework for few-shot evaluation of language models.项目地址: https://gitcode.com/GitHub_Trending/lm/lm-evaluation-harness
你是否遇到过这样的场景:想用自家训练的内部模型跑一次标准评测,但模型权重不在HuggingFace上,也不方便下载;或者团队自研的指标(比如"回答是否包含指定实体""数值误差是否在5%以内")在框架自带的指标表里根本找不到?再或者,你只是想在提交版本前快速用10%的数据验证一遍流程,却被迫全量跑上几个小时?
lm-evaluation-harness的自定义评估循环正是为解决这些问题而生的。它把"加载模型—构造请求—推理—算指标—出报告"这条链路拆成了可插拔的模块,你不需要改框架源码,就能替换掉任何一个环节。读完本文,你将掌握三件事:用simple_evaluate精确控制评估参数、用注册机制注入自己的指标、把非标准模型接入评估流程,并拿到一份可直接套用的避坑清单。
一、先别急着写死流程:三个真实痛点
痛点1:模型不在HuggingFace上
很多公司的内部模型走的是私有推理服务或自定义权重格式。标准教程里--model hf --model_args pretrained=xxx的路子直接走不通,于是一批人就手写脚本"复刻"评估逻辑,结果指标口径和社区对不上,结果没法横向比较。
痛点2:指标不够用
框架自带acc、acc_norm、exact_match、f1、perplexity等常用指标,但业务方往往关心的是"容错匹配":比如数学题答案写成 "42.0" 和 "42" 应当算对。这种差异化的打分逻辑,官方指标里没有。
痛点3:重复建设严重
每个任务都写一遍"读数据→拼prompt→调模型→算分"的胶水代码,改一个字段就要动一大片。而框架的评估循环把这些固化成了一套约定,你只需要"填空"。
核心思路:评估循环是流水线,不是黑盒。你要做的是替换流水线上的零件,而不是重造整条产线。
二、3分钟快速上手:最小的自定义评估
先克隆仓库并安装依赖(建议Python 3.10+):
git clone https://gitcode.com/GitHub_Trending/lm/lm-evaluation-harness cd lm-evaluation-harness pip install -e .然后打开一个Python终端,运行下面这段代码——这就是"自定义评估循环"的最小形态:
from lm_eval import evaluator results = evaluator.simple_evaluate( model="hf", # 模型类型,注册名 model_args="pretrained=gpt2", # 模型参数,传给模型构造函数 tasks=["arc_easy"], # 任务名,来自 lm_eval/tasks/ 下的YAML num_fewshot=0, # few-shot 示例数 limit=10, # 每个任务只跑10条,快速验证 ) print(results["results"]["arc_easy"])如果机器上有GPU,加一行device="cuda:0"。输出里你会看到每个指标的acc、acc_norm以及对应的stderr(标准误差):
{'acc,none': 0.4, 'acc_stderr,none': 0.1549, 'acc_norm,none': 0.5, 'acc_norm_stderr,none': 0.1581}命令行等价写法(适合脚本化):
lm_eval --model hf --model_args pretrained=gpt2 \ --tasks arc_easy --num_fewshot 0 --limit 10到这里你已经跑通了框架的默认流水线。接下来我们要做的,是把流水线的每一节都换成自己的。
三、黑盒变白盒:评估循环内部是如何转起来的
三大核心组件
- Evaluator(评估器):lm_eval/evaluator.py 中的
simple_evaluate负责"初始化模型+加载任务",真正干活的是evaluate函数。 - LM(模型接口):lm_eval/api/model.py 中的抽象类
LM,只规定三个核心方法:loglikelihood(计算对数似然)、loglikelihood_rolling(滑动窗口困惑度)、generate_until(生成到终止符)。 - Task(任务定义):lm_eval/api/task.py 中的
Task类,负责"读数据→构造请求→算指标"。
协作流程
注册表机制:框架的"零件仓库"
框架用一套注册表(Registry)管理所有可替换零件,定义在 lm_eval/api/registry.py:
| 注册表 | 存什么 | 常用API |
|---|---|---|
model_registry | 模型类 | register_model/get_model |
metric_registry | 指标函数 | register_metric/get_metric |
aggregation_registry | 聚合函数 | register_aggregation/get_aggregation |
filter_registry | 输出过滤器 | register_filter/get_filter |
你写的自定义指标、自定义模型,本质就是往这些注册表里"挂一个名字",评估循环按名字查找并调用。这就是为什么你不需要改动 evaluator.py 一行代码。
四、分步实战:为"中文关键词摘要评测"定制完整评估流程
下面我们围绕一个完整案例:评估模型做中文摘要时,回答是否覆盖了标准答案中的关键实体。这个指标官方没有,我们一步步把它接进评估循环。
4.1 注册自定义指标:实体命中率
指标分两层:逐样本打分(metric)和跨样本聚合(aggregation)。参照 lm_eval/api/metrics.py 里的写法,创建一个my_metrics.py:
import re from lm_eval.api.registry import register_metric, register_aggregation @register_aggregation("entity_hit_rate") def entity_hit_rate(items): """对逐样本的命中率列表取均值""" return sum(items) / len(items) @register_metric( metric="entity_hit", higher_is_better=True, # 越高越好 output_type="generate_until", # 适用于生成类任务 aggregation="entity_hit_rate", # 关联上面的聚合函数 ) def entity_hit(items): # 逐样本打分:items 里是每条样本的 (预测, 参考) 结构, # 具体解包方式由下方任务YAML的 process_results 决定 return items注意:
register_metric的metric参数是必填的,注册名就是你在YAML里引用的名字。higher_is_better若未注册,运行时is_higher_better()会打警告。
4.2 自定义任务YAML:定义数据、提示词与指标
在lm_eval/tasks/下新建目录my_summary/,写一个my_summary.yaml(参考 lm_eval/tasks/arc/arc_easy.yaml 的结构):
task: my_summary dataset_path: cnn_dailymail # 数据集路径(HuggingFace datasets) dataset_name: 3.0.0 # 数据集子集名 output_type: generate_until # 生成式任务 test_split: test doc_to_text: "请用一句话概括新闻:{{article}}" doc_to_target: "{{highlights}}" generation_kwargs: until: ["\n", "."] # 生成到换行或句号停止 metric_list: - metric: entity_hit # 引用4.1注册的指标 aggregation: entity_hit_rate higher_is_better: true metadata: version: 1.0同目录放一个_my_summary.py,实现逐样本打分逻辑(YAML里的process_results通过!function引用):
def process_results(doc, results): pred = results[0] # 模型生成的第一段文本 refs = doc["highlights"] # 参考摘要 hit = 0 for ref in refs: # 简化版实体命中:检查预测文本是否包含参考中的非停用词 for token in re.findall(r"[\u4e00-\u9fa5]{2,}", ref): if token in pred: hit += 1 total = max(len(refs), 1) return {"entity_hit": hit / total} # 返回 {指标名: 得分}然后在YAML中加入一行让框架加载它:
process_results: !function _my_summary.process_results运行前建议用--check_integrity校验任务配置是否正确,避免跑到一半才发现字段名写错。
4.3 接入自定义模型:实现LM抽象类
如果模型不在HF上,实现LM的子类即可。官方示例 examples/transformer-lens.py 演示了如何把TransformerLens模型包一层HF适配器后直接喂给HFLM。更通用的做法是实现三个核心方法:
from lm_eval.api.model import LM class MyPrivateModel(LM): def __init__(self, endpoint: str, **kwargs): super().__init__() self.endpoint = endpoint # 内部推理服务地址 def loglikelihood(self, requests, disable_tqdm=False): # 输入: requests 列表,每个含 (context, continuation) # 输出: 每项返回 (对数似然, 是否是greedy解码结果) return [(0.0, False) for _ in requests] def loglikelihood_rolling(self, requests, disable_tqdm=False): # 滑动窗口困惑度场景才需要,用不到可抛 NotImplementedError raise NotImplementedError def generate_until(self, requests, disable_tqdm=False): # 输入: requests,每个含 (context, gen_kwargs) # 输出: 每项返回生成文本列表 return [self._call_inference(r[0]) for r in requests] def _call_inference(self, prompt: str) -> list[str]: # 调用私有服务,这里替换成你的HTTP请求逻辑 return ["示例输出"]把MyPrivateModel实例直接传给simple_evaluate的model参数即可(它要求传入LM子类实例):
results = evaluator.simple_evaluate( model=MyPrivateModel(endpoint="http://10.0.0.8:8080/infer"), tasks=["my_summary"], limit=20, )4.4 运行与结果解读
lm_eval --model hf --model_args pretrained=Qwen/Qwen2-1.5B-Instruct \ --tasks my_summary --num_fewshot 0 --limit 50 \ --apply_chat_template --fewshot_as_multiturn注意这里开启了apply_chat_template——如果你的模型是对话模型,务必开启,否则提示词不带聊天格式,loglikelihood/生成结果都会失真(框架会在检测到inst/chat字样但未开启时打警告)。输出结果中results.my_summary下就会出现我们自定义的entity_hit指标及entity_hit_stderr。
五、进阶技巧与避坑清单
关键参数速查表
| 参数 | 作用 | 推荐用法 |
|---|---|---|
limit | 每任务样本数或比例(<1为比例) | 联调阶段设0.1,上线全量 |
samples | 精确指定要跑的样本下标,如{"arc_easy": [0,3,6]} | 复现某个失败case |
batch_size | "auto"自动探测 +max_batch_size兜底 | 大模型务必设置上限防OOM |
num_fewshot | 覆盖任务默认的少样本数 | 注意YAML里写死0的任务不会被覆盖 |
use_cache | SQLite缓存模型输出,路径自动拼_rank{rank}.db | 反复调参时能省一半时间 |
cache_requests | 缓存请求构建结果 | 数据集大时显著加速 |
bootstrap_iters | stderr计算的bootstrap迭代数,设0跳过 | 不关心置信区间时设0提速 |
predict_only | 只出预测不评分(自动换成bypass指标) | 先落盘推理结果,后离线分析 |
避坑检查表 ⚠️
limit和samples不能同时传,同时传会直接抛ValueError。- YAML里的转义字符必须用双引号:
until: ["\n"]是对的,单引号或裸写会解析成字面\n。 num_fewshot的覆盖规则:任务配置里num_fewshot: 0表示"强制0样本",此时你传的num_fewshot=5会被忽略(日志里有提示),需要去YAML里改。generate_until任务才接受gen_kwargs;loglikelihood类任务传了也会被忽略,别指望它生效。- 多卡分布式时缓存文件按rank分片:
use_cache会自动拼_rank{rank}.db,避免多进程写同一文件。 - 多模态任务与模型必须匹配:
MULTIMODAL任务配非多模态模型会直接报错,目前多模态仅支持hf-multimodal、vllm-vlm等类型。 - stderr开销很大:默认
bootstrap_iters=100000,数据量大时bootstrap会成为主要耗时,调试阶段建议设小或设0。
常见问题Q&A
Q:自定义指标总报 "Could not find registered metric"?A:先确认你的my_metrics.py被 import 过了。注册是"执行即注册",你需要在调用simple_evaluate之前import my_metrics,或者把文件放进lm_eval/api/下由框架自动加载。
Q:override_metric和 YAML里的metric_list有什么区别?A:task.override_metric("my_metric")是运行时动态替换单个任务的指标(框架内部predict_only就是这么干的,见 lm_eval/api/task.py);而metric_list是声明式配置,写在YAML里、可配多个指标。日常优先用metric_list,需要临时改指标时再用override_metric。
Q:模型生成了带多余前缀的文本,指标全为0?A:先检查generation_kwargs.until是否设置合理;再考虑加一个Filter(如正则提取答案段),过滤器在apply_filters阶段生效,会直接改写送入指标的filtered_resps。
六、延伸与资源
想继续深入,建议按这条路径走:
- 读源码:先啃 lm_eval/evaluator.py 的
evaluate函数(请求构建→推理→评分三段式),再看 lm_eval/api/registry.py 的注册表实现。 - 抄作业:官方示例 examples/transformer-lens.py 是"非标准模型接入"的最佳模板;任务目录 lm_eval/tasks/ 下有几百个YAML,挑一个和你业务形态接近的改。
- 看文档:任务配置字段详解见 docs/task_guide.md,Python API说明见 docs/python-api.md,模型接入指南见 docs/model_guide.md,常见坑合集在 docs/footguns.md。
- 参与社区:给新任务提PR时记得跑
--check_integrity,并遵守任务目录的命名与版本规范。
一句话总结:自定义评估循环的核心价值,是把"评估"从一次性脚本变成可组合、可复用、可复现的基础设施——你只负责定义"测什么、怎么算",剩下的流水线交给框架。
【免费下载链接】lm-evaluation-harnessA framework for few-shot evaluation of language models.项目地址: https://gitcode.com/GitHub_Trending/lm/lm-evaluation-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考