[LLM 9/10] Benchmarking:没有置信区间的 accuracy 只是传闻
从第 1 章开始,这个系列每次报告数字时都会重复同一句话: "没有置信区间的 accuracy 不是实验结果,它只是传闻", 并且承诺会在第 9 章完整解释——就是这一章。 我们一个 step 都不会训练,而是要从零搭出一套三种模式的评测系统, 把整个系列训练出来的每一个 checkpoint 都拿到真实的泰语考题上测一遍, 然后证明那些从来没人写进 paper 的设置,能让同一个模型的分数移动得比 leaderboard 上大家争来争去的差距还要大。
Open in Colab09_benchmarking.ipynb
1. 问题(Problem statement)
来读一句每周都能刷到的话:"模型 X 在 ThaiExam 上拿到 71.2%,超过了 69.8% 的模型 Y。"
唯一该问的问题是一共测了多少道题。如果测试集只有 100 道题, 每个数字的 95% 置信区间宽度大约是 ±8–10 个点, 这意味着 71.2% 和 69.8% 是同一个数字,只是随机抽样恰好抽出了不一样的结果。 拿 100 道题上 1.4 个点的差距去宣布胜者,和抛十次硬币就断定硬币不均匀没有区别。
问题还不止于测试集的大小,因为"benchmark 分数"这一个数字, 其实是由好几层几乎没人报告的决策共同产生的:
| 藏起来的决策 | 对分数的影响 |
|---|---|
| 用 log-likelihood 还是 generative 打分 | 可以差好几个点 |
| 要不要对选项长度做 normalize() | 足以让 leaderboard 的名次对调 |
| 0-shot 还是 5-shot、template 怎么写 | 可以差好几个点 |
| 给哪些模型套 chat template | 系统性地偏袒其中某一个模型 |
| 考题有没有泄漏进训练数据(contamination) | 整根柱子都是虚高的分数 |
一个掩盖了五层决策的数字,再加上没人画的 error bar—— 这就是我们平时所说的 leaderboard。
2. 我们要做什么(Solution)
这一章完全不训练,而这是优点,不是缺点—— 评测和训练是两种不同性质的工作,它值得拥有属于自己的一章。
我们要做四件事:
- 从零写出三种模式的打分系统——log-likelihood 多选题、generative exact-match (带泰语 normalization),以及配有白纸黑字 rubric 的 LLM-as-judge, 然后让你看到这三种模式给同一个模型打出的分数并不相同。
- 把第 1–8 章的每一个 checkpoint 放到同一根标尺上测,用真实的泰语考题
(
scb10x/thai_exam)、泰语数学题(VISAI-AI/gsm8k-thai)以及本系列一直在用的 KobEval-TH。 - 给每一个数字都配上 Wilson 95% CI,再看看这个系列的哪些结论能从 error bar 里活下来。
- 尝试复现公开 leaderboard 上的数字——Qwen3-0.6B 在 ThaiExam 上的成绩, 如果对不上,我们会去把原因一个个查出来,而不是默默略过。(ThaiExam 是泰语考试基准,详见 6.1。)
benchmark 分数不是模型的属性,它是 (模型 × 评测方法 × 题目集合 × 题目数量) 的属性。 只报告分数而不报告另外三样东西,等于只报告了四分之一的真相。 而置信区间,是"实验结果"这四个字的最低价码。
3. 公式(Equation)
3.1 Wilson score interval——本系列的专用 CI
如果在 道题里答对了 道,得到 ,那么置信水平 (95% → )下的 Wilson 区间是
为什么不用更好记的正态近似公式()? 因为它恰恰在我们最需要用到它的地方崩掉:接近 0 或 1 的边界,以及 很小的时候。
来看第 8 章的一个真实情形:guardrail 在 30 道测试题上一次危险请求都没有放过去。
from kobeval import wilson_ci # 与第 1 章开始一直使用的是同一个函数
wilson_ci(0, 30) # → (0.000, 0.114)
- 正态近似: ——区间宽度是零, 好像我们只看了 30 个样本就 100% 确信漏放率就是 0%。
- Wilson: ——"看了 30 次都没漏,但真实值有可能高到 11%"。
第二个区间是诚实的说法,第一个区间是公式自动生产出来的谎言。
在公式里可以看到,Wilson 用 这一项把中心点往 拉(相当于补进 道虚拟题目,一半对一半错),
又用 这一项防止宽度塌缩成零——这就是 kobeval 从头到尾都用 Wilson 的原因。
3.2 Length-normalised multiple-choice scoring——悄悄决定 leaderboard 的那个数字
log-likelihood 模式让模型读题目 ,然后比较各个选项 整句话的概率:
- → 直接用原始的总 log-prob,这偏向更短的选项(token 少 = 被乘上概率的次数少)
- → 除以 token 数,也就是使用每 token 平均 log-prob
这两行就是 lm-evaluation-harness 里的 acc 和 acc_norm,而在真实的 benchmark 上,
它们给出的分数并不相同,有时甚至会让模型的名次对调——
当你看到两篇 paper 报告的 ThaiExam 数字不一致时,头号原因就是 取值不同,
而两篇都根本没写自己用的是哪一个值。
3.3 无偏的 pass@k——用于可以自动判分的题目
数学题和代码题允许我们采样多个答案,然后问一句"里面有没有对的"。 采样 次、对了 次,pass@ 的正确估计量是
凭直觉写出来的估计量 是有偏的: 函数 关于 是凹函数(concave), 因此根据 Jensen 不等式 —— 把估计值塞进一个非线性函数里,期望值就不会等于真值了。 而上面那个二项式公式表示的是"从 个里面抽 个、整组全错的比例",可以证明它恰好是无偏的。
3.4 McNemar's test——在同一套题上比较两个模型
模型 A 和 B 做同一套题,令 = A 对但 B 错的题数, = B 对但 A 错的题数:
拿它去和 比较(带 continuity correction)。关键在于 paired 这个词: 两个都答对的题和两个都答错的题根本没有出现在公式里,因为它们说明不了谁更强。 如果你用 t-test 去比较两个 accuracy,好像它们来自两套不同的题目, 那你就是把"同一道题"这个结构给扔掉了,然后要达到同样的 power 就得多用好几倍的题目。
3.5 Contamination check——考题有没有泄漏进训练数据
对于考题 和训练语料 ,定义 -gram 的重合率:
其中 是全部 -gram 的集合。对泰语我们使用字符级 k-gram(例如 20 个字符), 因为泰语的分词本身就有歧义。如果 很高(例如超过 0.7), 就要怀疑模型已经"见过答案"了——这道题上的分数衡量的是记忆,不是能力。
4. 把公式画出来(Visualize)
CI 的宽度是 n 的函数——而 n=100 给出 ±10 个点
Figure 9.1Wilson 95% CI 的半宽随题目数量 n 的变化——在 n=100 时区间宽度为 ±6 到 ±9.4 个点,取决于 accuracy 的水平;而想把它收窄 10 倍,代价是题目数量增加 100 倍
这是全章最重要的一张图。宽度按 收缩, 所以 100 道题的测试集不可能分辨出相差 4 个点的两个模型,无论你重跑多少遍。 而如果你想有把握地读出 1 个点的差距,你需要大约一万道题——大多数泰语 benchmark 并没有这么多。
把 error bar 真的画上去之后,leaderboard 长什么样
Figure 9.25 个模型、n=100 道题的假想排行榜——中间三名的 Wilson 区间彼此完全重叠,因此它们是「一团」,而不是三个名次(数字为便于说明而假设,CI 区间是真实计算的——真正的实测版本在第 8 节)
模型 B、C、D 之间最多相差 5 个点,但 CI 宽度有 ±9 个点——数据能支持的唯一结论是 "这三个分不出来"。谁要是宣布 D 赢了 B,那是在从噪声里读信号。
没人报告的东西,比大家争论的东西还大
Figure 9.3同一个模型用 5 种语义相同的 prompt template 测出的结果——8.5 个点的离散度,比 leaderboard 上两个「竞争对手」之间 2 个点的差距还大(取值为假设值,处于 prompt sensitivity 研究中实际观察到的量级——图中已注明)
McNemar:100 道题,真正有信息量的只有 10 道
Figure 9.4两个模型在同一套 100 道题上的 2×2 contingency 表——整个检验只用到 b 和 c 两格,χ² = 4.90、p ≈ 0.027 由真实公式算出
两个模型相差 8 个点(79% 对 71%)——听起来很明显,但真正的证据只有那 10 道意见不一致的题。 McNemar 说它刚刚才勉强越过 这条线。如果换成 (相差 2 个点,也就是常见 leaderboard 的量级), 得到的是 ——一丁点显著性都没有。
亲手玩一玩 n → CI 的关系
这个 widget 和第 8 章(guardrails)用的是同一个,但这次请换个角度看: 里面每一个 TPR / FPR / precision 数字,都带着和公式 3.1 完全相同的 Wilson CI。 试着把 threshold 拉到极端,直到混淆矩阵里某一格只剩下寥寥几个样本, 然后看着 CI 在你眼前撑开——这就是图 9.1 的可交互版本。
safeunsafeDrag the line, or use the τ slider.
| predicted unsafe | predicted safe | |
|---|---|---|
| actually unsafe | 220TP | 20FN |
| actually safe | 22FP | 438TN |
Precision has collapsed.The classifier looks excellent on the evaluation set — 90.9% precision — but at a live base rate of 1.0% it drops to 16.2%. Nothing about the model changed; TPR and FPR are identical. There are simply so many more safe requests than unsafe ones that an FPR of 4.8% produces more false alarms than the classifier finds true positives. This is why a guardrail benchmarked on a balanced set falls apart in production, and why FPR, not accuracy, is the number to negotiate over.
Showing a synthetic held-out set.
5. 准备环境(Environment)
打开 Colab,选择 Runtime → Change runtime type → T4 GPU(免费额度够用)。 这一章是纯 inference——没有 optimizer,也没有 gradient——把所有 0.6B 规模的 checkpoint 扫一遍, 总共大约 15 分钟,比之前每一章都轻松。
Colab 的 T4 是 Turing 架构(SM 7.5),它不支持 bfloat16,也不支持 FlashAttention-2。
但 Qwen3-0.6B 的 config.json 里写着 torch_dtype: bfloat16,
所以 torch_dtype="auto" 是个陷阱:代码会崩掉或者慢得离谱,而且不会告诉你原因。
torch_dtype=torch.float16 # 不是 bfloat16
attn_implementation="sdpa" # 不是 flash_attention_2
这一章不训练,所以不用操心 TrainingArguments 里的 fp16=True——把模型按 fp16 加载好就可以直接开测。
cap = torch.cuda.get_device_capability(0)
print("compute capability:", cap) # T4 = (7, 5)
print("native bf16:", cap[0] >= 8) # T4 -> False
print("torch says :", torch.cuda.is_bf16_supported()) # T4 -> True(把 emulation 也算上了!)
is_bf16_supported() 在 T4 上会骗你较新的 torch 在 T4 上返回 True,因为它把**模拟(emulation)**也算作支持——而模拟比 fp16 慢得多。
请改为判断 compute capability ≥ 8.0(Ampere 及以上)。这是真正在 Colab 上跑才发现的 bug。
整个系列的 checkpoint 都是从同一个 Qwen3-0.6B 底座延伸出来的,大部分是 LoRA adapter, 所以我们只加载一次底座权重,然后把 adapter 一个个挂上去再卸下来——显存不会随 checkpoint 数量膨胀:
CHECKPOINTS = {
"base": None, # 未经改动的 Qwen3-0.6B-Base
"01-cpt": "kobkrit/qwen3-0.6b-th-cpt", # full weights(第 1 章)
"02-sft": "kobkrit/qwen3-0.6b-th-sft-lora", # LoRA(第 2 章)
"03-ppo": "kobkrit/qwen3-0.6b-th-ppo-lora", # LoRA(第 3 章)
"04-dpo": "kobkrit/qwen3-0.6b-th-dpo-lora", # LoRA(第 4 章)
"05-grpo": "kobkrit/qwen3-0.6b-th-grpo-lora", # LoRA(第 5 章)
"06-ctx-dist": "kobkrit/qwen3-0.6b-th-ctxdist-lora", # LoRA(第 6 章)
"07-distill": "kobkrit/qwen3-0.6b-th-distill", # 第 7 章的学生模型
"08-guarded": "kobkrit/qwen3-0.6b-th-sft-lora+guard", # 模型 + 第 8 章的过滤器
}
6. 准备数据(Data)
6.1 scb10x/thai_exam——真正的泰国标准化考题
来自泰国真实考场(O-NET、IC、TGAT、TPAT-1、A-Level)的选择题集合。 这是公开 leaderboard 用得最多的泰语 benchmark,也是这一章的主轴。
真实考题是有归属方的,从真题衍生出来的数据集因此带有使用条件,你必须自己去读 Hugging Face 上的 dataset card,然后再决定是否下载——不要猜,也不要复制代码跳过这一步。 notebook 会把 card 的 metadata 拉出来打印,并停下来等你确认后才继续:
from huggingface_hub import DatasetCard
card = DatasetCard.load("scb10x/thai_exam")
print("license:", card.data.get("license"))
print(card.text[:1500]) # 用自己的眼睛读一遍 card 页面上的条款
如果 license 不允许你的使用场景(例如商业用途),就到此为止。 从违反数据使用条件开始的评测,无论如何都称不上 rigorous。
6.2 VISAI-AI/gsm8k-thai——generative 形式的数学题
GSM8K 的泰语翻译版:需要自己生成答案的数学应用题,没有选项可以用来比较 log-likelihood, 所以它正是 generative exact-match 模式的专属试验场(同样要查 card 上的 license)。
6.3 KobEval-TH——本系列固定的 100 道题
从第 1 章一直用到现在的评测集(TH-KNOW 泰国知识、指令遵循、th_ratio)。
它的优点不在于大——100 道题按图 9.1 给出的是 ±10 个点的 CI ——
而在于恒定:每一章都用同一套题、同一种方法测,所以跨章节的比较才真正成立。
6.4 泰语 normalization——泰语 exact-match 最常翻车的一关
"๕๐"、"50"、" 50 " 是同一个答案,但 Python 的 == 可不这么认为:
import re
THAI_DIGITS = str.maketrans("๐๑๒๓๔๕๖๗๘๙", "0123456789")
def normalize_thai(s: str) -> str:
s = s.strip().translate(THAI_DIGITS) # 泰文数字 ๐-๙ → 阿拉伯数字
s = s.replace(",", "") # 1,000 → 1000
s = re.sub(r"\s+", "", s) # 泰语不用空格分词 —— 全部去掉
return s
notebook 为这个函数写了小小的 unit test,因为判分器里的 bug 就是反过来的 contamination: 它会系统性地让模型看起来比真实水平更差,而且不会有任何 error 提醒你。
7. 核心代码(Main code)
7.1 模式一——log-likelihood 多选题(公式 3.2 的直接翻译)
import torch
@torch.no_grad()
def score_mc(model, tok, question, choices, gamma=1.0):
scores = []
for c in choices:
q_ids = tok(question, return_tensors="pt").input_ids.cuda()
full_ids = tok(question + c, return_tensors="pt").input_ids.cuda()
logits = model(full_ids).logits[:, :-1]
targets = full_ids[:, 1:]
logp = torch.log_softmax(logits.float(), -1).gather(
-1, targets.unsqueeze(-1)).squeeze(-1)
ans = logp[0, q_ids.shape[1] - 1:] # 只取选项部分的 token
scores.append(ans.sum().item() / len(ans) ** gamma)
return int(torch.tensor(scores).argmax()) # 全程一个 token 都不用 generate
优点:100% deterministic、快、而且对还不会好好说话的 base model 同样适用。 局限:只能测选择题,而且分数会随 变化,第 9 节会看到。
7.2 模式二——generative exact-match
@torch.no_grad()
def score_generative(model, tok, question, gold):
msgs = [{"role": "user", "content": question}]
ids = tok.apply_chat_template(msgs, add_generation_prompt=True,
enable_thinking=False, # 本系列的评测约定
return_tensors="pt").cuda()
out = model.generate(ids, max_new_tokens=256, do_sample=False) # greedy
pred = tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True)
return int(normalize_thai(extract_answer(pred)) == normalize_thai(gold))
extract_answer 负责从文本里把最终答案抠出来(GSM8K-TH 取最后一个数字,
选择题取选项字母)——这个函数本身也是一项会影响分数的决策,必须一并报告。
7.3 模式三——带白纸黑字 rubric 的 LLM-as-judge
JUDGE_RUBRIC = """คุณคือกรรมการตรวจข้อสอบ ตัดสินตามเกณฑ์นี้เท่านั้น:
1 = ใจความถูกต้องตรงกับเฉลย (ยอมรับการสะกดต่างกัน เลขไทย/อารบิก
และการเรียบเรียงคนละแบบที่ความหมายเดียวกัน)
0 = ผิด ตอบไม่ตรงคำถาม หรือไม่ตอบ
ห้ามให้คะแนนความสวยงามของภาษา ห้ามให้คะแนนความยาว
ตอบเป็น JSON เท่านั้น: {"score": 0 หรือ 1, "reason": "สั้น ๆ"}
โจทย์: {q}
เฉลย: {gold}
คำตอบของโมเดล: {pred}"""
这段 rubric 保持泰语原文——它是真正发给评委模型的操作性提示词, 而被评的答案也是泰语;换成中文会连带改变评委的判分行为。它的中文大意是:
你是阅卷评委,只按以下标准判分: 1 = 要点与标准答案一致(允许拼写差异、泰文数字/阿拉伯数字混用,以及意思相同但组织方式不同的表述) 0 = 错误、答非所问,或者没有作答 不得为语言的优美程度打分,不得为长度打分。只输出 JSON。
评委必须是比被评者强很多的模型。notebook 的设计允许你接入任何一个
OpenAI-compatible 的 endpoint,并且始终把 rubric 和评委的型号名写进 results.json——
一份不说明评委是谁、用了什么标准的 judge 结果,也不过是另一种传闻。
7.4 专业人士真正在用的东西——lm-evaluation-harness
上面三种模式我们自己写,是为了看清内部结构,但真实工作应该站在被 community 检验过的工具之上:
pip install lm-eval
lm_eval --tasks list | grep -i thai # 先看看你这个版本里到底有哪些 task 名字
lm_eval --model hf \
--model_args pretrained=Qwen/Qwen3-0.6B,dtype=float16 \
--tasks thai_exam \
--num_fewshot 5 \
--batch_size 8 \
--seed 42 \
--log_samples --output_path results/harness
整条命令里最重要的是 --log_samples:它会把每一道题的回答都记录下来,
让我们可以(1)自己计算 Wilson CI、(2)逐题配对做 McNemar、(3)回过头查哪一道错在哪里。
注意 harness 会同时报告 acc 和 acc_norm——那正是公式 3.2 中的 与 length-normalised 两种取法。
8. 结果(Results)
全系列总结图——所有 checkpoint 在同一根轴上,带 error bar
notebook 的结尾就是这个系列走了 8 章才画得出来的那张图:
第 1–8 章的每一个 checkpoint 排在同一根轴上,用同一套约定测量,每一根柱子都带 Wilson 95% CI
(真实数字在 notebook 的 results.json 里——所以本表中一律是 ?,直到你自己跑一遍为止):
| checkpoint | ThaiExam (95% CI) | GSM8K-TH (95% CI) | KobEval-TH (95% CI) | th_ratio |
|---|---|---|---|---|
| Qwen3-0.6B-Base | ? | ? | ? | ? |
| 第 1 章 —— CPT | ? | ? | ? | ? |
| 第 2 章 —— SFT-LoRA | ? | ? | ? | ? |
| 第 3 章 —— PPO | ? | ? | ? | ? |
| 第 4 章 —— DPO | ? | ? | ? | ? |
| 第 5 章 —— GRPO | ? | ? | ? | ? |
| 第 6 章 —— Context distillation | ? | ? | ? | ? |
| 第 7 章 —— Model distillation | ? | ? | ? | ? |
| 第 8 章 —— SFT + guardrail | ? | ? | ? | ? |
跑之前应该预期什么(永远先写下假设再看结果——这是本章能教给你的最好习惯):
大部分 checkpoint 在 ThaiExam 上的 CI 会互相重叠,因为我们这点训练量和 pretraining 相比实在太小了。
有可能从 error bar 里活下来的差异是 th_ratio(第 4 章正是直接针对这一点动手的),
以及 KobEval-TH 里的指令格式遵循能力(第 2 章 SFT 的功劳)。
如果真实的图和这里说的不一样,那是件值得兴奋的事,而不是该藏起来的事。
同一个答案,三个评委,三种分数
Promptอธิบายว่าทำไมท้องฟ้าถึงเป็นสีฟ้า แบบสั้น ๆbase
sft
Showing the built-in sample.
上面这些例子是模型真实的回答,它们同一道题在三种模式下拿到的分数并不相同—— 例如回答 "๕๐ บาท"(泰文数字写法的"50 铢"),不做 normalize 的 exact-match 给 0, 做了 normalize 的给 1,judge 给 1; 又比如推理过程完全正确但最后算错了数字的回答,judge 有时会比标准答案还宽容。
诚实度任务:把 leaderboard 的数字复现出来——或者搞清楚为什么复现不了
notebook 以一项比所有 cell 加起来都更有教学价值的作业收尾: 去打开一个公开的、报告了 Qwen3-0.6B 在 ThaiExam 上成绩的 leaderboard,记下他们公布的数字, 然后试着在自己的机器上产出同一个数字。
如果对不上(而第一轮通常都对不上),不要停在"已经差不多了" ——一个变量一个变量地排查:
- prompt template 和他们的一致吗(图 9.3 已经说明,光这一项就绰绰有余)
- 打分模式——他们用的是 log-likelihood 还是 generative,是
acc还是acc_norm(!) - shot 数量——0-shot 和 5-shot 是两个世界
enable_thinking——Qwen3 有内部思考模式,开与关会同时改变分数和耗时- 数据集版本和 subset——thai_exam 有五个科目,他们是怎么取平均的
- generate 时的 batch size——padding 不同确实会让 greedy 产出不一样的结果
一份写着 "我们测得 41.8,而 leaderboard 报告 43.5,原因是他们用了 5-shot 加 acc_norm,我们用的是 0-shot 加 acc" 的报告,比一份数字完全吻合却说不出为什么的报告更有价值—— 因为前者证明了你控制得住自己的量具,后者可能只是运气好。
9. 对比(Comparison)
前面每一章比的都是"多个模型、一种评测方法"——这一章反过来: 一个模型(第 2 章的 SFT)、多种评测方法,看看分数会因为那些平常没人写进报告的决策而摇摆多少:
| 设置(每行只与约定相差一处) | ThaiExam | GSM8K-TH | KobEval-TH |
|---|---|---|---|
| 本系列的评测约定(loglik 、0-shot、thinking off) | ? | — | ? |
| 用 代替 | ? | — | ? |
| 用 generative exact-match 代替 loglik | ? | ? | ? |
| 用 5-shot 代替 0-shot | ? | ? | ? |
| 用 LLM-as-judge 代替 exact-match | ? | ? | ? |
enable_thinking=True | ? | ? | ? |
(GSM8K-TH 没有选项可供比较 log-likelihood,所以只能用 generative 模式测量——那些格子是有意留空的。)
每一行都是同一个模型、每一个字节都相同的权重。你应该看到的是分数摇摆好几个点—— 比一般 leaderboard 上模型之间的差距还大,而这正是本章所提问题的最终答案: 当不同机构报告的数字对不上时,大多数情况下没有人在说谎——只是没有人用的是同一把尺。
第 1–8 章的每一个数字,都是在完全相同的条件下测出来的:
- greedy decoding(
do_sample=False) max_new_tokens=256enable_thinking=Falseseed=42- KobEval-TH 固定的那 100 道题,中途从未改动过 + 每个数字都带 Wilson 95% CI
第 1 章宣布这些条件的时候,它看起来只是吹毛求疵;读到这一行,你已经知道为什么了: 如果没有这份约定,第 8 节的表格根本无法跨章比较——它会变成一张用 9 把不同尺子量出来的 9 行表。
需要提防的坑
1. Contamination——考题泄漏进了我们自己的训练数据
notebook 会按公式 3.5(20-char-gram)在 ThaiExam / KobEval-TH 的题目
与我们第 1–2 章使用的语料(thaigov-v2 和合成的 instruct 数据集)之间跑一遍检查。
需要盯紧的一点是:thaigov 是泰国政府公文,而 ThaiExam 里有关于法规、条例和政务知识的题目——
真的存在重合的可能。如果查到 hit,就要逐题报告并把它们从结论中剔除,而不是装作没看见。
(真实的扫描结果打印在 notebook 里——hit 的数量在你跑之前都是 ?。)
2. 跨 paper 比较用了不同 template 的数字 "我们的模型在 ThaiExam 上拿到 45,那篇 paper 报告的是 43"——如果 template 不同、shot 数不同、 不同,这句话没有任何意义。只有在同一个 harness、同一套 config 下测出来的数字才可以比较。
3. 给一个模型套了 chat template,另一个却没套 用 chat template 去测 base model = 白白压低它的分数 / 不套 template 去测 instruct model = 同样是压分。 第 8 节的表格里我们的 checkpoint 两种都有(CPT 是 base,其余是 chat), 所以 notebook 会把每个模型第一道题的真实 prompt 打印出来供你肉眼检查——一行代码,守住了整张表的公平。
4. Judge 偏袒自己家族(self-enhancement bias) 多项研究发现,LLM judge 对和自己同一家族风格的回答给分会偏高。 如果 judge 和被评者是亲戚,数字就会甜得不正常—— 正确的解法不是去找一个"中立"的 judge(那种东西并不存在),而是永远报告 judge 的名字, 并在结果接近时用另一个家族的 judge 复核一遍。
5. 泰语 exact-match 因为泰文数字而崩掉
模型回答 "๕๐"、标准答案写的是 "50" ——忘了 normalize_thai 就立刻判错。
而且这类 bug 的分布并不均匀:用政府公文做 CPT 的模型(公文里泰文数字用得多)会比别人被扣掉更多分,
于是变成一种专门刁难某些模型的系统性 bias——判分器必须先做到公平,才谈得上排名次。
10. 小结(Summary)
- 没有 CI 的 accuracy 只是传闻 —— n=100 给出 ±10 个点,而 leaderboard 上大部分差距都比这个小
- Wilson 不是奢侈品,正态近似在 处给出宽度为零的 CI ——而那正是我们最需要它的地方
- 分数是(模型 × 评测方法 × 题目 × n)的属性,打分模式、、shot 数、template 造成的差异,可以大过模型之间真实的差异
- 在同一套题上比较两个模型,要用 McNemar —— 意见一致的题目不构成证据
- pass@k 必须用二项式估计量,plug-in 估计量因为 Jensen 而有偏
- 相信分数之前先扫一遍 contamination,尤其是训练数据和考题来自相近领域的时候
- 第一天就宣布评测约定,之后再也不去动它 —— 本系列的约定是:greedy、256 token、thinking off、seed 42
- 复现别人的数字并解释得清差异,比拿到吻合的数字却不知道原因更有价值
Benchmark 衡量的是容易衡量的东西,而不是重要的东西。 选择题可以自动判分,于是它成了标准, 但真实用户不会带着 A B C D 四个选项来——他们带来的是长长的问题、各自特殊的上下文, 以及根本没法用 accuracy 衡量的期待。
ThaiExam 分数高不代表对泰国人有用。 一个 A-Level 考得很好的模型, 可能起草不好一份公文、回答客户时一点也不自然,或者僵硬到没有人愿意用它。 考试分数和真实使用价值之间的相关性,比 leaderboard 让我们感觉到的松散得多。
还有一条值得贴在墙上:在你自己产品的真实 traffic 上做 30 条 human eval, 往往比 10,000 道选择题告诉你的东西更多 —— 它的 CI 确实更宽(公式 3.1 已经告诉我们宽到什么程度), 但它粗略地衡量了正确的东西,而这永远胜过精确地衡量错误的东西。 这一章给出的统计工具对两类评测都适用——请不要只把它用在那个更方便的类别上。
下一章: Deployment —— 测量完成的模型必须走出去面对真实用户。 quantization 要拿什么去换、能部署在什么样的机器上,以及这一章得到的数字如何变成 production 的 regression test。
参考文献(References)
- Liang et al. (2022). Holistic Evaluation of Language Models — HELM:用多维度评测取代单一数字
- Biderman et al. (2024). Lessons from the Trenches on Reproducible Evaluation of Language Models — lm-evaluation-harness 关于可复现评测的经验
- Miller (2024). Adding Error Bars to Evals: A Statistical Approach to Language Model Evaluations — 每个 accuracy 数字都需要误差棒的理由
- Zheng et al. (2023). Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena — LLM-as-a-judge 及其偏见
- Chiang et al. (2024). Chatbot Arena: An Open Platform for Evaluating LLMs by Human Preference — 以真实人类偏好进行排名
- Hendrycks et al. (2020). Measuring Massive Multitask Language Understanding — MMLU:多项选择基准的范式
- Chen et al. (2021). Evaluating Large Language Models Trained on Code — 第 9 节所用的 pass@k 无偏估计
- Wilson (1927). Probable Inference, the Law of Succession, and Statistical Inference — 本系列每个数字所用的 Wilson 区间
- McNemar (1947). Note on the Sampling Error of the Difference Between Correlated Proportions or Percentages — 在同一批题目上比较两个模型的配对检验
本系列的文章、代码与 notebook 均以 CC BY-NC-SA 4.0 授权 —— 可自由使用与改编,须署名、限非商业用途,并以相同方式共享。文中引用的第三方模型与数据集仍适用各自的许可证。
