ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

lm-evaluation-harness:把LLM评估变成可复现的工程化流水线

lm-evaluation-harness:把LLM评估变成可复现的工程化流水线 1. 评估不是跑分是你判断模型变好还是变坏的唯一尺子如果你最近在搞LLM评估八成绕不开EleutherAI维护的lm-evaluation-harness。我第一次用它是被逼的微调了一个小模型手工写了评测脚本结果发现同一个模型上午测和下午测分数能差出一大截。查了半天问题不是模型是我自己的脚本——prompt里多了一个空格、少了一个换行few-shot样例错位验证集切分也没固定。那一刻我意识到没有一套标准化的评估工具你根本分不清模型是真的在进步还是你碰巧测到了一个好天气。我先说结论lm-evaluation-harness不是只能用来打榜它是把模型评估这件看起来简单、做起来极脏的事拆成一条可复现流水线的工具箱。它能帮你做什么用统一接口加载模型用标准化任务配置评测模型输出可对比的指标并且保留每一条样本级别的结果。不管你是想验证一个微调版本是否值得上线还是想横向对比几个候选模型它都能派上用场。适合谁用适合那些真正在迭代模型、被我这个模型到底行不行反复折磨的人。1.1 手工评测脚本为什么撑不住场面很多人一开始都和我一样觉得评测不就是写个循环、喂几条问题、看答对多少吗对但也不对。等你把任务规模放大问题会一串串冒出来。第一是prompt不一致。同一个问题今天你写请回答明天改成Read the following and answer模型分数可能就差了两个点。第二是数据切分混乱。有人用测试集跑评测有人用验证集还有人同一个数据集反复出现最后结果全不可比。第三是few-shot样例的选取和顺序。同样是5-shot样例不同、顺序不同分数波动可以非常大。第四是随机性和解码参数。生成式任务里temperature、top_p、随机种子不固定你今天跑出来的exact_match明天就换一张脸。这些坑单个看都不大但它们叠加起来会让你的评估结论彻底失真。更关键的是手工脚本很难横向对比——你自己写的脚本测出来一个分数别人用另一个库测出来另一个分数你根本不知道差异来自模型还是来自评测代码。Harness解决的就是这个问题它把数据集、prompt模板、评测指标、few-shot逻辑全部固化到任务配置里同一个任务在不同模型之间是可复现的。1.2 harness的核心设计把评测拆成四层流水线用多了你会发现lm-evaluation-harness的设计思路其实非常朴素就四层模型层负责加载和推理任务层负责描述测什么、怎么出题、怎么判分调度层负责批处理、缓存、并发输出层负责汇总指标和沉淀样本。你在命令行里看到的--model、--tasks、--num_fewshot、--output_path分别对应着前面几层。这种分层带来的好处是换模型不用改任务换任务不用改模型代码。它的模型层最早主要支持HuggingFace Transformers加载的开源模型后来也接入了OpenAI兼容接口等在线API。任务层则是它真正的核心竞争力——EleutherAI社区维护了大量benchmark的配置从MMLU、HellaSwag、ARC到GSM8K、TruthfulQA每种任务都写清楚了用什么数据集、什么prompt、按什么指标算分。你在很多开源模型的技术报告里看到的我们在HellaSwag上跑了5-shot这句话背后大概率就是这同一套harness跑的。2. 安装到跑通第一个任务一份可以直接照抄的操作清单2.1 安装和版本选择的个人建议安装本身不复杂但版本问题值得留意。现在PyPI上的包名是lm-eval直接装就行pip install lm-eval如果你要改任务配置、加自定义数据集或者想跟踪最新功能我建议用源码方式安装git clone https://github.com/EleutherAI/lm-evaluation-harness.git cd lm-evaluation-harness pip install -e .我个人的建议是别直接在系统全局Python环境里装最好新建一个虚拟环境。原因是harness的依赖里包含torch、transformers、datasets这些重量级库版本冲突时你根本分不清是评测问题还是环境问题。我就踩过一次因为transformers版本太旧模型加载时trust_remote_code的警告被当成错误排查了两小时。还有些老教程会让你用lm-eval这个命令但新版本命令行入口已经统一成lm_eval。如果你搜索到的资料和实际命令对不上先确认harness版本。用lm_eval --version看一眼一切都对得上。2.2 第一次运行命令这样写省得掉进batch_size的坑装完之后先别急着跑大任务我建议你用一个1B左右的小模型选一个速度快的任务先跑通流程。下面这条命令我经常用lm_eval --model hf \ --model_args pretrainedQwen/Qwen2.5-0.5B,dtypefloat16 \ --tasks hellaswag \ --num_fewshot 5 \ --batch_size auto \ --output_path results/walkthrough拆开解释一下--model hf表示用HuggingFace模型接口。--model_args里传模型路径和精度dtypefloat16能省一半显存如果你机器支持也可以换dtypebfloat16。--tasks hellaswag指定任务这里用的是harness内置的HellaSwag配置。--num_fewshot 5表示5-shot评测。--batch_size auto让harness自己估算batch大小。这个参数很方便但它在某些任务上会贪心导致显存溢出。实测如果报OOM直接改成--batch_size 1虽然慢一点但一定能跑。不要迷信auto。--output_path指定结果输出目录。这个参数建议每次都写否则结果只在终端上显示不落盘后面想分析badcase就麻烦了。第一次跑可以用--limit 100限制样本数先验证流程没问题再放开全量。--limit 100的意思是每个任务只跑前100条样本跑完大概一两分钟适合冒烟测试。2.3 跑通之后先看什么终端表格和落盘文件跑完之后终端会打印一张表格列名大概是tasks、version、filter、n_shot、metric、value、stderr。每一行代表一个任务下的一个指标。我第一次看这个表格的时候第一反应是这也太简略了吧后来才发现真正的宝藏藏在输出目录里。--output_path results/walkthrough会生成一个带时间戳的JSON文件里面不仅有汇总指标更重要的是results和samples两个部分。samples里面是每条样本的完整记录包括prompt、gold答案、模型预测、是否答对。这是分析badcase的入口——想知道模型为什么错就去翻这里的具体样本。我曾经在某个小模型上看到HellaSwag分数只有0.39以为模型废了。后来打开samples一看发现有些样本的continuation被tokenizer截断了导致模型拿到的上下文不完整。这不是模型能力问题是加载参数里max_length没调对。如果没有samples我根本定位不到这个原因。3. 内置任务扫描harness到底替我们测了什么3.1 任务类型很多但别一把全跑harness内置了几百个任务你可以通过lm_eval --tasks list查看完整列表。列出来你就会发现它已经把很多通用benchmark整理成了开箱即用的状态。按用途我一般把常用任务分成几类类别代表任务说明知识理解mmlu、arc_easy、arc_challenge模型积累了多少知识能不能做选择题常识推理hellaswag、winogrande模型对语言常识和代词指代的理解事实性与安全性truthfulqa_mc1、truthfulqa_mc2模型会不会一本正经地胡说八道数学与逻辑gsm8k、bbh数值计算、多步推理和能力边界语言建模lambada_openai、wikitext预测下一个词的能力跟流畅度相关注意--tasks mmlu这种写法在很多版本里会展开成几十个子任务因为MMLU按学科细分成了几十个subject。跑起来时间很长。如果你只是快速验证优先选hellaswag、arc_easy、winogrande这几个速度快的任务。--tasks mmlu适合放在正式发版前的全量评估里不适合每次微调都跑。3.2 分数口径acc、acc_norm、exact_match、perplexity的区别很多新手看到结果表里的metric列会困惑为什么有的叫acc有的叫acc_norm有的叫exact_match。这背后其实是不同的评测口径。acc就是简单的准确率对于选择题来说就是答对的比例。acc_norm则是在计算概率时除以答案长度相当于做了归一化。为什么需要归一化因为有些模型倾向于给长答案更高概率如果不做处理长选项会占便宜。ARC、HellaSwag这类任务默认就偏向用acc_norm作为主指标。exact_match常用于生成式任务要求模型生成的内容和标准答案完全一致。这个指标很严格模型只要多写一个标点、多一个换行就算错。所以它衡量的是模型能不能稳定复现正确答案而不是模型是否理解。perplexity则是语言建模类任务的指标代表模型对文本连续性的拟合程度数值越低通常越好。但它对prompt措辞很敏感跨任务、跨模型对比时要小心别拿perplexity去跟准确率硬比。还有一个底层机制值得知道选择题类任务在harness里通常不是让模型生成答案而是计算每个选项作为后续文本的loglikelihood然后选概率最高的那个。这意味着它用的是模型内部的概率分布不是采样生成。所以这类任务跑起来快而且基本不受随机种子影响。4. 把业务场景做成自定义评测任务4.1 为什么能用官方任务不等于能落地官方benchmark只能回答模型在通用学术任务上什么水平回答不了你的业务问题。你是做客服意图识别那就得用自己的真实问法做评测你是做法律文书抽取那就得用自己标的答案做评测。如果把通用benchmark当作业务验收标准很容易出现打榜分数不错上线一塌糊涂的情况。自定义评测任务的核心就是把你业务里最关心的问答场景固化成harness认识的格式。一旦固化后面每次模型迭代都能用同一把尺子量这才是真正的价值。4.2 一个YAML任务配置的完整拆解harness自定义任务比很多人想象得简单不需要写Python类大多数场景一个YAML文件就够了。下面这个例子是一个业务多个选择题文件task: my_business_qa group: my_custom_bench output_type: multiple_choice dataset_path: json dataset_name: null dataset_kwargs: data_files: validation: ./my_data/validation.jsonl validation_split: validation doc_to_text: {{question}}\nA. {{option1}}\nB. {{option2}}\nC. {{option3}}\nD. {{option4}}\n答案 doc_to_choice: [A, B, C, D] doc_to_target: {{answer}} metric: acc num_fewshot: 0本地JSONL里每条数据至少要有question、option1到option4、answer这几个字段answer存的是正确答案的选项字母。逐行解释task任务名命令行--tasks里用的就是这个名字。group任务分组名方便你把多个任务归到一个组里统一运行。output_type: multiple_choice告诉harness这是一道选择题。dataset_path: json和dataset_kwargs指定数据来源是本地JSON文件。validation_split: validation指定评测用哪个split。doc_to_text用Jinja2模板拼接出模型看到的prompt。doc_to_choice四个候选项。doc_to_target正确答案字段。metric: acc用准确率评估。num_fewshot: 0零样本。如果要在自定义任务里用few-shot需要额外提供train_split和样例字段否则会报错。文件放在一个目录里比如eval_tasks/my_business_qa.yaml然后用--include_path挂载lm_eval --model hf \ --model_args pretrainedQwen/Qwen2.5-1.5B,dtypefloat16 \ --tasks my_business_qa \ --include_path ./eval_tasks \ --batch_size 8 \ --output_path results/custom如果任务没被识别先用lm_eval --tasks list | grep my_business_qa确认。较老版本的harness可能不认--include_path那就直接把YAML文件挪到仓库里的lm_eval/tasks目录下重新安装后再试。4.3 生成式任务的自定义思路如果你的业务不是选择题而是要求模型生成一段摘要、一个SQL或者一句回答那么output_type要改成generation指标大概率会用exact_match或者你自己写后处理逻辑计算关键词命中率。这种任务更接近真实使用场景但评估成本更高、稳定性更难控制。我的经验是能做成选择题就先做成选择题。因为选择题用loglikelihood评估速度快、无随机性、结果稳定生成式评测不仅要跑生成还要面对格式、长度、同义表述的判分难题。等选择题这条线稳定了再针对少数核心场景做生成式评测否则你会陷入模型明明答对了但脚本判它错的泥潭。5. 结果解读与翻车排查为什么跑出来的数跟我预期不一样5.1 先问三个问题prompt、数据版本、指标口径跑完一个分数先别急着下结论。我每次拿到结果都会先问自己三个问题。第一个问题这个任务的prompt合适吗国内很多中文团队拿英文benchmark直接测中文模型分数低得离谱然后开始怀疑模型。这不是模型不行是评测任务本身和模型的语言域不匹配。第二个问题数据版本固定了吗同一套benchmark不同harness版本的任务配置会变prompt会微调切分也可能不同。所以不同版本之间跑出来的分数不能直接比较。第三个问题我看的指标是主指标吗比如ARC任务如果你盯着acc而不是acc_norm看偶尔会得出相反的结论。这三件事不做分数再高也是虚的。5.2 一个伪差距的排查实例我有一次对比两个微调版本A模型的HellaSwag分数是0.58B模型是0.61看起来B模型赢了3个点。正准备开心地发报告结果随手用--limit 100复测了一把两个模型变成了0.55对0.54完全反过来。问题出在哪样本量太小。Harness结果里有一列stderr那是标准误。样本越少标准误越大。对于100条样本3个百分点的差异完全可能在噪声范围内。正确做法是要么跑全量要么多跑几个任务看趋势不要盯着单任务、小样本的零点几个点下判断。另一个更隐蔽的坑是few-shot样例顺序。同一个任务5-shot的样例如果来自训练集前5条和来自随机5条分数能差不少。所以我建议所有横评对比必须固定同一个harness版本、同一个任务版本、同一个few-shot配置最好连--seed都固定。5.3 环境导致的分数波动和几个我踩过的坑环境问题也能让分数失真这部分最容易被忽略。我列几个真实遇到过的症状真实原因处理方式显存溢出--batch_size auto对某些任务估算过大改成--batch_size 1或手动调小加载自定义模型报错模型仓库需要trust_remote_code在--model_args里加trust_remote_codeTrue中文模型分数奇低评测任务和prompt全是英文要么选适配任务要么构建中文自定义任务跑N次分数不一致生成式任务没固定解码参数检查temperature、seed尽量用loglikelihood类任务还有一个我反复提醒自己的点跑评测前先确认评测时用的模型参数和部署时是否一致。量化版本、float16和float32跑出来的分数会有细微差异这不代表模型变了只是精度变了。6. 把harness变成模型迭代的自动回归工具6.1 一个小脚本把评测收进日常链路评估不该是一次性动作而应当是迭代闭环里的一道自动检查。我现在每次微调完都会跑一个固定脚本#!/usr/bin/env bash set -euo pipefail MODEL_PATH$1 TS$(date %Y%m%d_%H%M%S) mkdir -p results lm_eval --model hf \ --model_args pretrained$MODEL_PATH,dtypebfloat16 \ --tasks hellaswag,arc_easy,winogrande,truthfulqa_mc1 \ --num_fewshot 0 \ --batch_size auto \ --output_path results/$TS echo 结果目录results/$TS先跑这四个速度较快的通用任务用30分钟得到一个大方向。等确认候选版本值得继续投入再跑--tasks mmlu,gsm8k这类大任务。日常快速验证时加个--limit 200就够了全量留到晚上跑。我的习惯是白天用limit版本筛掉明显变差的实验晚上不赶时间再跑全量作为存档。6.2 留档案、锁版本、调阈值我的三个实操习惯第一个习惯是留档案。每次评测结果我一定把模型路径、评测时间、harness版本、任务列表、few-shot数量记下来。可以用一个简单的eval_log.csv没有的话直接看--output_path里生成的JSON文件也能反推大部分信息。第二个习惯是锁版本。项目里用一个固定版本的harness升级harness之前先在同一批旧任务上跑一次基线确认没有异常波动再切换新版本。否则你很难分辨分数变化是模型变了还是评测工具变了。第三个习惯是设阈值优先看变差。别只盯着哪个模型分数更高更要看有没有任务明显变差。一个模型在MMLU上涨了2个点同时TruthfulQA掉了5个点这往往比全面涨0.5个点更值得警惕。Harness的好处就是让你同时看到多把尺子的读数而不是挑一把最顺眼的尺子自我安慰。老实说lm-evaluation-harness不是万能的它不能替你定义好模型是什么也不能替代线上业务指标。但它能让你在模型迭代过程中始终拿同一把尺子量不慌、不虚、不靠感觉。把评估标准化这件事早点做了后面所有模型对比都会变得高效很多。
返回列表