跳到主要内容

数据截至 (上游 commit 932e1f2f4c5a)

03 · 指标计算与聚合

这一章讲什么: 模型的 ModelResponse 怎么变成成绩表上那个带 ± 的数字。读完你能自己写一个自定义指标,也能回答「pass@k 到底让模型多干了什么」「stderr 是公式算的还是重采样出来的」。


1. 它要解决的小问题

打分这件事看似简单,拆开有四个真实难点:

  • 两级计算:有的指标逐样本算完再平均(accuracy),有的必须在全语料上一次性算(BLEU、困惑度)——两套机制要共存。
  • 指标反向约束上游:选 loglikelihood 准确率就需要后端给选项打分;选 pass@10 就需要模型对每题采 10 个回答。这些约束得从指标一路传回请求层。
  • 归一化见仁见智:选项概率按字符数归一?按 token 数归一?还是减去「无上下文概率」(PMI)?不同选择分数不同。
  • 分数要有误差线:均值的标准误有解析式,F1 的标准误没有——得 bootstrap 重采样。

2. 直觉:指标是一个五元组

Metric(src/lighteval/metrics/utils/metric_utils.py:32-40)把「一个指标」定义为五样东西:

字段含义
metric_name结果表里的列名"acc"
higher_is_better方向True
category需要哪种请求(SamplingMethod)LOGPROBS
sample_level_fn逐样本打分函数LoglikelihoodAcc()
corpus_level_fn语料级聚合函数np.mean

直觉一句话:指标不是一段评分代码,而是一份「上下游契约」——category 告诉 pipeline 怎么问模型,sample_level_fn 告诉它怎么批改每张答题卡,corpus_level_fn 告诉它怎么把一叠答题卡汇总成一个数。

内置指标集中在 Metrics 枚举(src/lighteval/metrics/metrics.py:146),每项是这个五元组的实例,任务配置里直接引用枚举成员(如 gsm8k 用 Metrics.expr_gold_metric,src/lighteval/tasks/tasks/gsm8k.py:81-83)。


3. 图示:分数怎么流到结果表

ModelResponse 列表(第 2 章的产物)


① _compute_metrics 按 (任务 × 采样方式) 把 (doc, response) 配对
pipeline.py:362 只把 category 匹配的指标喂给这组样本


② apply_metric batched 指标整批算一次,非 batched 逐样本算
metrics/__init__.py:29 → 每个样本得到一个 {指标名: 分数} dict


③ MetricsLogger.log 逐样本分数累积成 list
info_loggers.py:326


④ MetricsLogger.aggregate 语料级聚合 → stderr → 子任务平均 → 全体平均
info_loggers.py:330

怎么读这张图:② 是「逐题批改」,④ 是「登分 + 算全班平均」。中间所有分数都先以样本粒度存着,聚合是最后一步。


4. 原理演示:指标如何反向决定请求

下面这段演示「指标驱动请求」的完整回路(示意,非源码):

# 示意,非源码
class Metric:
category = SamplingMethod.LOGPROBS # 我要选项打分
sample_level_fn = LoglikelihoodAcc() # 逐样本:最高分的选项是 gold 吗?
corpus_level_fn = np.mean # 语料级:平均

# 任务侧:选了哪些指标,就决定 Doc 要进哪个桶
task.sampling_methods = {m.category for m in task.metrics} # → {LOGPROBS}
doc.sampling_methods.extend(task.sampling_methods)

# pipeline 侧:按桶发请求
for method, docs in sampling_docs.items():
outputs[method] = model.loglikelihood(docs) # 只在需要时才调

重点:任务作者从不需要写「请调用 loglikelihood」——选指标即选择了请求方式。真实代码在 LightevalTask.__init__(src/lighteval/tasks/lighteval_task.py:245)和 Pipeline._init_tasks_and_requests 的聚类循环(src/lighteval/pipeline.py:227-231)。


5. 真实实现

5.1 样本级:三种典型打分函数

loglikelihood 准确率——LoglikelihoodAcc.compute(src/lighteval/metrics/metrics_sample.py:243-294):从 ModelResponse.logprobs 取前 len(choices) 个分数,(可选)归一化,argmax 落在 gold_index 里就得 1 分。它还能识别「logprobs 数量是选项数两倍」的情形——后半截是无上下文( PMI 用)的分数(metrics_sample.py:275-276)。

归一化三选一——normalize_log_probs(src/lighteval/metrics/normalizations.py:504-538):

归一化公式解决的问题
LogProbCharNormlogprob ÷ 字符数长选项天然分低
LogProbTokenNormlogprob ÷ token 数同上,按 token 粒度
LogProbPMINormlogprob − 无上下文 logprob选项本身「顺口」程度不该加分

PMI 的「无上下文」题面来自 Doc.unconditioned_query(src/lighteval/tasks/requests.py:196-199 注释),通常是空串或 "Answer:"

sampling 家族——SamplingMetric(metrics_sample.py:1108-1172)是 pass@k / maj@n / avg@n 的共同基类,统一管「预处理(去空格/归一化)+ 单预测打分(exact match 的 prefix/suffix/full 三态)」。三个子类:

指标单样本语义
AvgAtNn 个回答的平均分metrics_sample.py:1175
MajAtNn 个回答多数投票后再对答案metrics_sample.py:1211
PassAtKn 个回答里「至少 k 个对」的概率估计metrics_sample.py:1263

PassAtK.pass_at_k(metrics_sample.py:1318-1324)用的是 Codex 论文(arXiv 2107.03374)的无偏估计量:1 - C(n-c, k)/C(n, k) 的数值稳定写法,代码里是一行连乘 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1))

5.2 反向回路:num_samples 一路传到引擎

sampling 类指标需要模型对每题出 n 个回答,这个 n 的传导链是:

  1. LightevalTask.__init__ 发现指标是 SamplingMetric,就把它的 num_samples() 收进 self.num_samples(src/lighteval/tasks/lighteval_task.py:253-257);
  2. get_docs 把最大值写进每个 Doc(lighteval_task.py:404,doc.num_samples = max(self.num_samples));
  3. vLLM 后端把它赋给 SamplingParams.n(src/lighteval/models/vllm/vllm_model.py:435)。

参数的反向注入也走这条路:任务字符串 task@k=10&n=16 解析出的参数被 setattr 到指标的 sample_level_fn 上(src/lighteval/tasks/registry.py:232-241),Metric.__call__ 再把参数值拼进指标名,于是结果表里会出现 pass@k:k=10&n=16 这种自描述的列名(src/lighteval/metrics/utils/metric_utils.py:78-98)。声明了 attribute_must_be_set 而没传参的指标,会在任务加载期直接报错(registry.py:235-241)——fail fast,不让你跑完发现列名不对。

5.3 派发:apply_metric

apply_metric(src/lighteval/metrics/__init__.py:29-71)先做两个划分:

  • 按采样方式:pipeline 在 _compute_metrics(src/lighteval/pipeline.py:383-386)里只把 metric.category == sampling_method 的指标喂给对应那组样本——同一任务选了生成式和选项式两类指标时,两批样本各算各的,互不干扰。
  • 按是否 batched:batched_compute=True 的指标(典型是 LLM-as-judge,如 JudgeLLMSimpleQA)整批 (responses, docs) 一次性算;其余逐样本算(__init__.py:31-67)。

MetricGrouping(metric_utils.py:105-113)处理「几个指标共享昂贵预处理」的情形——一次计算出一个 dict 多个分数(如 BERTScore 同时出 P/R/F1),聚合阶段跳过已算过的(info_loggers.py:340-344:354-356)。

5.4 语料级:聚合与 stderr

聚合函数解析——Metric.get_corpus_aggregations(metric_utils.py:61-76)把 corpus_level_fn 统一成「名字 → 可调用」:普通函数直接用,CorpusLevelComputation 子类取它的 compute_corpus。语料级计算的例子:CorpusLevelPerplexityMetricperplexity / weighted_perplexity / bits_per_byte 三种口径归一化 logprob 总和(src/lighteval/metrics/metrics_corpus.py:164-192)。

stderr 分流——get_stderr_function(src/lighteval/metrics/utils/stderr.py:95-106):

  • 聚合函数名里含 mean → 用解析式 mean_stderr(标准差 ÷ √n,stderr.py:43-49);
  • 其余 → bootstrap_stderr:有放回重采样 1000 次、每次重算指标、取这批指标值的标准误(stderr.py:78-92)。重采样本身用 multiprocessing.Pool 并行(stderr.py:63-75)。

三级登分——MetricsLogger.aggregate(src/lighteval/logging/info_loggers.py:330-402):

  1. 任务级:每个任务的每个指标,先跑语料级聚合函数,再跑 stderr;
  2. 子任务平均:mmlu:xxx|0 这类带 : 的任务名,按冒号前缀合成 mmlu:_average|0,子任务多于 1 个才算(info_loggers.py:374-396)——这就是 MMLU 总分的来源;
  3. 全体平均:所有任务的所有指标再平均一遍,存进 "all" 键(info_loggers.py:398-402)。

聚合遇到 OverflowError 不崩,记 nan 并打 warning(info_loggers.py:348-352)——困惑度类指标在极端分布下会触发。


6. 坑

  • pass@k 不许多 gold。 PassAtK.compute 发现 len(golds) > 1 直接抛异常(metrics_sample.py:1290-1292);多答案任务要用别的指标。
  • pass@k 的 n 会在运行期被「自动补上」。 没传 n 时它假设 n = 实际回答数并打 warning(metrics_sample.py:1295-1297)——结果列名里的参数值可能和你预期不符。
  • stderr 只在样本数 >1 时计算(info_loggers.py:366-368);max_samples=1 调试跑没有误差线。
  • 返回 dict 的语料级指标没有 stderr。 代码里显式跳过(info_loggers.py:360-363)。
  • judge 类指标的「样本级」其实是批级。 JudgeLLM.compute 签名是 responses/docs 整批(metrics_sample.py:1005),走 batched_compute 通道;自定义 judge 指标记得把 batched_compute 置 True,否则 apply_metric 会按逐样本签名调它。
  • 指标名里的参数是运行期拼的。 两次 run 用了不同 k/n,结果表列名不同,直接 join 两张表会对不上列——这是特性(自描述),但对比脚本要感知。

7. 代码地图(本章)

主题文件路径符号名
指标五元组src/lighteval/metrics/utils/metric_utils.pyMetricMetricGroupingSampleLevelMetricCorpusLevelMetric
内置指标花名册src/lighteval/metrics/metrics.pyMetrics
样本级指标实现src/lighteval/metrics/metrics_sample.pyLoglikelihoodAccExactMatchesSamplingMetricPassAtKMajAtNAvgAtNJudgeLLM
语料级指标实现src/lighteval/metrics/metrics_corpus.pyCorpusLevelComputationCorpusLevelPerplexityMetricCorpusLevelTranslationMetricMatthewsCorrCoef
logprob 归一化src/lighteval/metrics/normalizations.pynormalize_log_probsLogProbNormalization
派发src/lighteval/metrics/__init__.pyapply_metric
stderrsrc/lighteval/metrics/utils/stderr.pyget_stderr_functionbootstrap_stderrmean_stderr
登分与平均src/lighteval/logging/info_loggers.pyMetricsLogger.logMetricsLogger.aggregate
参数注入src/lighteval/tasks/registry.py_update_task_configs(@ 语法部分)