数据截至 (上游 commit 652c89d0ca9d)
第 2 章:判分的四种策略
这章讲全库的精华。模型吐出来的是一段自由文本,而分数要么是 0 要么是 1——中间这一步「文本 → 判定」就是评测最难、最容易出错、各 eval 最不一样的地方。simple-evals 用了四种由简到繁的办法。
2.0 为什么这一步难
问题的本质:模型给的「答案」和数据集里的「标准答案」几乎从不一字不差。 模型会说「答案应该是 C」「$\boxed{42}$」「Malia and Sasha Obama, born in 1998 and 2001」,而标准答案可能只是 C、42、Malia and Sasha。
每个基准的答案形态不同,于是判分策略也不同:
| 答案形态 | 例子基准 | 用什么策略 |
|---|---|---|
| 单个字母(ABCD) | MMLU、GPQA | 正则抽 + 精确匹配(§2.1) |
| 数学表达式 | MATH | 让另一个模型判等价(§2.2) |
| 短答案 / 多个 span | DROP、MGSM | 词袋 F1 + 数字归一(§2.3) |
| 开放短答 | SimpleQA、BrowseComp | LLM 当裁判(§2.4) |
| 可执行代码 | HumanEval | 跑测试用例(§2.5) |
2.1 策略一:正则抽答案 + 精确匹配(最便宜)
要解决的小问题: 从「…所以答案是 C」里抠出那个 C。
思路: 先用模板逼模型把答案放在固定格式里(「最后一行写 'Answer: $LETTER'」),再用正则去抓那一行。约束输出格式,抽取就稳。
两道防线:
- 归一化(
normalize_response,common.py:355-374):把**、\$\boxed{、}\$、\text{等 markdown/LaTeX 装饰统统删掉,免得它们挡住匹配。 - 多语言答案正则(
MULTILINGUAL_ANSWER_REGEXES,common.py:32-75):因为多语言 MMLU 里「答案」会写成答案:、Antwort:、정답:等几十种说法,库枚举了所有写法,逐个试到命中为止。
MMLU 的抽取循环(mmlu_eval.py:108-114):
# 示意,非源码 —— 提炼自 mmlu_eval.py 的抽取逻辑
for answer_regex in MULTILINGUAL_ANSWER_REGEXES: # 试遍所有语言的「答案:」写法
regex = MULTILINGUAL_ANSWER_PATTERN_TEMPLATE.format(answer_regex)
match = re.search(regex, response_text)
if match:
extracted_answer = normalize_extracted_answer(match.group(1)) # 把 أ/অ/A 等映射回 A-D
break
score = 1.0 if extracted_answer == row["Answer"] else 0.0
坑: normalize_extracted_answer(common.py:377-395)还要把阿拉伯文/孟加拉文/全角的 A-D 字母映射回拉丁 A-D,否则非英语题永远判错。这种「为多语言留的暗活」是评测库里最容易漏的细节。
GPQA 用的是同一策略,但答案正则更紧(ANSWER_PATTERN_MULTICHOICE = r"(?i)Answer[ \t]*:[ \t]*\$?([A-D])\$?",common.py:26),见 gpqa_eval.py:59-61。
2.2 策略二:让另一个模型判数学等价
要解决的小问题: \frac{3}{2} 和 1.5 是同一个答案吗?字符串比对会判错。
思路: 与其写一个数学表达式归一器(很难、很脆),不如再开一个 LLM 专门当「等价裁判」。MATH eval 持有一个 equality_checker(一个 sampler),判分时把标准答案和抽出的答案塞进一个模板问它「这两个表达式等价吗?只回 Yes/No」。
模板 EQUALITY_TEMPLATE(common.py:78-136)给了 一串示范,并刻意划定边界——只做平凡化简:3245/5 vs 649 要回 No(因为需要非平凡计算),但 72 degrees vs 72 回 Yes(对单位「给予宽容」)。
# 示意,非源码 —— 提炼自 common.check_equality
def check_equality(sampler, expr1, expr2):
prompt = EQUALITY_TEMPLATE % {"expression1": expr1, "expression2": expr2}
text = sampler([{"content": prompt, "role": "user"}]).response_text
return text.lower().strip() == "yes" # 只认整段就是 "yes"
真实实现 common.py:157-161;MATH eval 的调用 score = float(check_equality(self.equality_checker, row["Answer"], extracted_answer)) 见 math_eval.py:55。注意编排层给等价裁判单独配了一个便宜模型 gpt-4-turbo-preview(simple_evals.py:347),并注释「仅用于 math 的模糊匹配」。
2.3 策略三:DROP 的词袋 F1 + 匈牙利对齐(最重的纯算法)
要解决的小问题: 阅读理解题答案可能是「a list of spans」(多个短语),且词序、冠词、标点都不该影响判分。
思路三步走:
- 重度归一化(
_normalize_answer,drop_eval.py:58-67):小写、去标点、去冠词(a/an/the)、把数字标准化(12与12.0视同,_normalize_number,drop_eval.py:78-82)、压空白。 - 词袋 F1(
_compute_f1,drop_eval.py:119-134):把 答案拆成词的集合,算预测集合与标准集合的 token 级 F1。 - 多 span 最优对齐:当答案是多个 span 时,「预测的哪个 span 配标准的哪个 span」是个指派问题。库用
scipy.optimize.linear_sum_assignment(匈牙利算法,在二部图里求最大权完美匹配)找最优 1-1 配对,再取这套配对下的 F1(_align_bags,drop_eval.py:101-116)。
这是全库唯一一处经典组合优化算法。_match_numbers_if_present(drop_eval.py:137-148)还加了一道闸:如果标准答案含数字,预测必须命中那些数字才允许给 F1 分——防止文字蒙对而数字错的情况。
DROP 还顺带演示了少样本拼接:它把 3 个训练样例拼进 prompt 当示范(train_samples_per_prompt=3,simple_evals.py:373-377、drop_eval.py:260-281)——这是少数偏离「纯零样本」的 eval。
2.4 策略四:LLM 当裁判(SimpleQA / BrowseComp)
要解决的小问题: 开放式短答「Barack Obama 的孩子叫什么」——模型可能对、可能错、也可能回避不答,而且对错要看语义而非字面。
思路: 把判分整个外包给一个裁判模型(grader),用一个极其详尽的 prompt 教它怎么打标签。SimpleQA 的 GRADER_TEMPLATE(simpleqa_eval.py:13-92)给裁判三档标签,并列了大量正反例:
- CORRECT(A): 完整包含标准答案、不含矛盾信息(允 许带犹豫,只要事实对)。
- INCORRECT(B): 有任何事实与标准答案矛盾(哪怕带「可能」之类对冲也算错)。
- NOT_ATTEMPTED(C): 没给出关键信息、也没说错——即「没答」。
判分就是把裁判回复里的 A/B/C 抠出来(simpleqa_eval.py:112-126),默认兜底为 C。这就是 LLM-as-judge 的典型形态:把判分难题转成一次受控的分类提示。
SimpleQA 的指标设计很讲究(simpleqa_eval.py:164-189)——它不只报准确率,因为「不答」不该和「答错」同罚:
| 指标 | 含义 |
|---|---|
is_correct / is_incorrect / is_not_attempted | 三档各自占比 |
accuracy_given_attempted | 在它选择回答的题里答对率(衡量「敢答时准不准」) |
f1 | 上面两者的调和平均(同时奖励「敢答」和「答得准」) |
这套设计直击「事实性」评测的核心张力:一个只在有把握时才答、其余一律说「不知道」的模型,准确率会虚高;F1 把「回避」也计入代价,逼出诚实的画像。
BrowseComp 用同一套路(browsecomp_eval.py:26-45 的 GRADER_TEMPLATE、79-93 的 grade_sample),只是裁判输出 correct: yes/no 两档。
2.5 旁支:HumanEval 不判文本,直接跑代码
代码题没法靠匹配——唯一真相是「能不能跑过测试」。HumanEval 把模型生成的函数体抽出来(find_code 用正则抠 ```python 块并去掉签名,humaneval_eval.py:68-75),调上游 human-eval 包的 check_correctness 在沙箱里跑测试用例(humaneval_eval.py:20-43)。
它对每题采样 num_samples_per_task=5 份,用 estimate_pass_at_k 算 pass@k(k 次里至少一次通过的无偏估计,humaneval_eval.py:104-110)——这是代码生成评测的标准指标。
2.6 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 多语言答案正则 | common.py | MULTILINGUAL_ANSWER_REGEXES、MULTILINGUAL_ANSWER_PATTERN_TEMPLATE |
| 回答归一化 | common.py | normalize_response、normalize_extracted_answer |
| 数学等价裁判 | common.py | EQUALITY_TEMPLATE、check_equality |
| DROP 词袋 F1 + 匈牙利对齐 | drop_eval.py | _compute_f1、_align_bags、_match_numbers_if_present、get_drop_metrics |
| SimpleQA LLM 裁判 + F1 指标 | simpleqa_eval.py | GRADER_TEMPLATE、SimpleQAEval.grade_sample |
| BrowseComp LLM 裁判 | browsecomp_eval.py | GRADER_TEMPLATE、BrowseCompEval.grade_sample |
| HumanEval 执行判分 + pass@k | humaneval_eval.py | evaluate_functional_correctness、find_code、estimate_pass_at_k |
| MGSM 多语言答案解析 | mgsm_eval.py | parse_answer、score_mgsm |