数据截至 (上游 commit dd417662e5bd)
05 · 评测主循环与指标聚合
这一章讲什么: 全系统的总装线——
simple_evaluate/evaluate如何把「加载任务、执行请求、判分、聚合、出报告」串起来,以及一个分数后面的±是怎么算的。改框架本身、或在乎统计口径的人读这章。
1. 它要解决的小问题
逐题判分(第 3 章)产出的只是几千个 0/1 和 logprob。从它们到「MMLU = 62.5 ± 0.4」还有一串问题:
- 不同指标聚合方式不同:
acc求均值,perplexity要先平均 loglikelihood 再取 exp——换算放哪做? - 标准误怎么算?均值有闭式公式,F1/perplexity 没有。
- group(MMLU)分数是 57 个子任务分数的简单平均,还是按题量加权?stderr 又该怎么合并?
- 多 GPU 时各 rank 只有部分数据,汇总在哪做?
2. 主线全览:simple_evaluate → evaluate
simple_evaluate(lm_eval/evaluator.py:55)是用户入口,evaluate(lm_eval/evaluator.py:429)是执行核心。全流程:
simple_evaluate
① 固定四路种子(random / numpy / torch / fewshot)
② 建 LM(必要时包 CachingLM)
③ TaskManager.load(tasks) —— 第 1 章
④ 应用覆盖:gen_kwargs / num_fewshot / predict_only
│
▼
evaluate
⑤ task.build_all_requests() —— 第 2 章(按 rank 切片)
⑥ 按 request_type 归堆、执行 —— 第 3、4 章
⑦ apply_filters → process_results —— 第 3 章
⑧ 跨 rank gather_object 收集逐题值
⑨ _process_results:任务级聚合 → group 聚合
│
▼
rank 0:组装 results 字典(附 configs / versions / seeds / git hash)
2.1 种子四件套
lm_eval/evaluator.py:196-214 依次固定:random_seed(默认 0)、numpy_random_seed(默认 1234)、torch_random_seed(默认 1234)、fewshot_random_seed(默认 1234,默认值见 lm_eval/defaults.py)。few-shot 抽样单独一路,保证换 batch size、换卡数时 few-shot 样本不变(第 2 章)。四个种子最后都写进 results["config"](lm_eval/evaluator.py:402-417)。
2.2 命令行对任务配置的三类覆盖
simple_evaluate 在加载后、执行前统一改任务配置(lm_eval/evaluator.py:307-347):
| 覆盖 | 规则 |
|---|---|
gen_kwargs | 只作用于 generate_until 任务,update 进 generation_kwargs(lm_eval/evaluator.py:308-316) |
num_fewshot | 任务 YAML 写死 num_fewshot: 0 时不覆盖;否则覆盖并打 warning(lm_eval/evaluator.py:326-341) |
predict_only | 把任务指标换成 bypass,只出模型输出不算分(lm_eval/evaluator.py:318-324) |
执行前还有一道安全与兼容性校验(lm_eval/evaluator.py:517-533):任务标了 unsafe_code(如要执行生成代码的 HumanEval 类任务)而用户没传 confirm_run_unsafe_code=True,直接报错;多模态任务配非多模态模型同理。
3. 跨 rank 收集:谁算分,谁出报告
多 GPU 时,每个 rank 只建并执行自己那份请求(第 2、3 章),逐题判分也是各算各的。汇总在 evaluate 尾部(lm_eval/evaluator.py:671-701):
lm.gather_object(rank_metrics, dst=0)把各 rank 的(metric, filter) → [逐题值]收到 rank 0;- rank 0 用
itertools.chain.from_iterable按顺序拼回完整列表; - 只有 rank 0 继续往下做聚合(
lm_eval/evaluator.py:703-712),其他 rank 返回None。
值得记住:聚合发生在收集之后、单进程内。所以聚合函数不需要是分布式安全的——它就是普通 Python 函数,吃一个 list。
4. 指标注册表:三件套一次注册
指标不是孤立函数,是三件套:逐题度量函数、聚合函数、方向(越大越好吗)。注册一个装饰器搞定(lm_eval/api/registry.py:575 的 register_metric),看 acc 的定义(lm_eval/api/metrics.py:176-182):
# 摘自 lm_eval/api/metrics.py:157-163
@register_metric(
metric="acc",
higher_is_better=True,
output_type=["loglikelihood", "multiple_choice"],
aggregation="mean",
)
def acc_fn(items): # This is a passthrough function
return items
acc_fn 是 passthrough:逐题值(0/1)原样攒进列表,聚合端才 mean。任务没配 metric_list 时按 output_type 给默认指标(DEFAULT_METRIC_REGISTRY,lm_eval/api/registry.py:449-457):multiple_choice 默认 [acc, acc_norm],generate_until 默认 [exact_match],等等。
查指标的顺序(get_metric,lm_eval/api/registry.py:609):本地注册表 → 找不到则回落到 HF evaluate 库按名字加载(lm_eval/api/registry.py:631-640)——所以 YAML 里写 bleu、rouge 也能用,只要装了 evaluate。
5. 聚合:先任务级,再 group 级
5.1 任务级:passthrough + 聚合函数
_compute_task_aggregations(lm_eval/evaluator_utils.py:173)对每个 (metric, filter) 键:
- 从任务的三件套里取聚合函数,套到逐题值列表上(
lm_eval/evaluator_utils.py:192-203)——结果键形如acc,none、exact_match,strict-match。 - 配套算标准误,键形如
acc_stderr,none(lm_eval/evaluator_utils.py:206-217)。
passthrough 模式的代表是 perplexity 家族(lm_eval/api/metrics.py:46-58):
# 摘自 lm_eval/api/metrics.py:34-58
@register_aggregation("mean")
def mean(arr):
return sum(arr) / len(arr)
@register_aggregation("perplexity")
def perplexity(items):
return math.exp(-mean(items))
@register_aggregation("bits_per_byte")
def bits_per_byte(items):
return -weighted_mean(items) / math.log(2)
为什么必须坚持「聚合端换算」:perplexity 是 exp(-总ll/总词数),若逐题先算 exp 再平均,得到的是另一个量。第 3 章逐题端只攒 (ll, n_words) 原始量,就是为了这一刻。