lm-evaluation-harness自定义评估循环实战:从simple_evaluate到全流程定制
2026/8/21 15:42:48 网站建设 项目流程

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:指标不够用

框架自带accacc_normexact_matchf1perplexity等常用指标,但业务方往往关心的是"容错匹配":比如数学题答案写成 "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"。输出里你会看到每个指标的accacc_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_metricmetric参数是必填的,注册名就是你在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_evaluatemodel参数即可(它要求传入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_cacheSQLite缓存模型输出,路径自动拼_rank{rank}.db反复调参时能省一半时间
cache_requests缓存请求构建结果数据集大时显著加速
bootstrap_itersstderr计算的bootstrap迭代数,设0跳过不关心置信区间时设0提速
predict_only只出预测不评分(自动换成bypass指标)先落盘推理结果,后离线分析

避坑检查表 ⚠️

  1. limitsamples不能同时传,同时传会直接抛ValueError
  2. YAML里的转义字符必须用双引号until: ["\n"]是对的,单引号或裸写会解析成字面\n
  3. num_fewshot的覆盖规则:任务配置里num_fewshot: 0表示"强制0样本",此时你传的num_fewshot=5会被忽略(日志里有提示),需要去YAML里改。
  4. generate_until任务才接受gen_kwargs;loglikelihood类任务传了也会被忽略,别指望它生效。
  5. 多卡分布式时缓存文件按rank分片use_cache会自动拼_rank{rank}.db,避免多进程写同一文件。
  6. 多模态任务与模型必须匹配MULTIMODAL任务配非多模态模型会直接报错,目前多模态仅支持hf-multimodalvllm-vlm等类型。
  7. 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

六、延伸与资源

想继续深入,建议按这条路径走:

  1. 读源码:先啃 lm_eval/evaluator.py 的evaluate函数(请求构建→推理→评分三段式),再看 lm_eval/api/registry.py 的注册表实现。
  2. 抄作业:官方示例 examples/transformer-lens.py 是"非标准模型接入"的最佳模板;任务目录 lm_eval/tasks/ 下有几百个YAML,挑一个和你业务形态接近的改。
  3. 看文档:任务配置字段详解见 docs/task_guide.md,Python API说明见 docs/python-api.md,模型接入指南见 docs/model_guide.md,常见坑合集在 docs/footguns.md。
  4. 参与社区:给新任务提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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询