数据截至 (上游 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):
| 归一化 | 公式 | 解决的问题 |
|---|---|---|
LogProbCharNorm | logprob ÷ 字符数 | 长选项天然分低 |
LogProbTokenNorm | logprob ÷ token 数 | 同上,按 token 粒度 |
LogProbPMINorm | logprob − 无上下文 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 三态)」。三个子类:
| 指标 | 单样本语义 | 类 |
|---|---|---|
AvgAtN | n 个回答的平均分 | metrics_sample.py:1175 |
MajAtN | n 个回答多数投票后再对答案 | metrics_sample.py:1211 |
PassAtK | n 个回答里「至少 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 的传导链是:
LightevalTask.__init__发现指标是SamplingMetric,就把它的num_samples()收进self.num_samples(src/lighteval/tasks/lighteval_task.py:253-257);get_docs把最大值写进每个 Doc(lighteval_task.py:404,doc.num_samples = max(self.num_samples));- 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。语料级计算的例子:CorpusLevelPerplexityMetric 按 perplexity / 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):
- 任务级:每个任务的每个指标,先跑语料级聚合函数,再跑 stderr;
- 子任务平均:
mmlu:xxx|0这类带:的任务名,按冒号前缀合成mmlu:_average|0,子任务多于 1 个才算(info_loggers.py:374-396)——这就是 MMLU 总分的来源; - 全体平均:所有任务的所有指标再平均一遍,存进
"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.py | Metric、MetricGrouping、SampleLevelMetric、CorpusLevelMetric |
| 内置指标花名册 | src/lighteval/metrics/metrics.py | Metrics |
| 样本级指标实现 | src/lighteval/metrics/metrics_sample.py | LoglikelihoodAcc、ExactMatches、SamplingMetric、PassAtK、MajAtN、AvgAtN、JudgeLLM |
| 语料级指标实现 | src/lighteval/metrics/metrics_corpus.py | CorpusLevelComputation、CorpusLevelPerplexityMetric、CorpusLevelTranslationMetric、MatthewsCorrCoef |
| logprob 归一化 | src/lighteval/metrics/normalizations.py | normalize_log_probs、LogProbNormalization |
| 派发 | src/lighteval/metrics/__init__.py | apply_metric |
| stderr | src/lighteval/metrics/utils/stderr.py | get_stderr_function、bootstrap_stderr、mean_stderr |
| 登分与平均 | src/lighteval/logging/info_loggers.py | MetricsLogger.log、MetricsLogger.aggregate |
| 参数注入 | src/lighteval/tasks/registry.py | _update_task_configs(@ 语法部分) |