跳到主要内容

第 2 章:判分的四种策略

这章讲全库的精华。模型吐出来的是一段自由文本,而分数要么是 0 要么是 1——中间这一步「文本 → 判定」就是评测最难、最容易出错、各 eval 最不一样的地方。simple-evals 用了四种由简到繁的办法。

2.0 为什么这一步难

问题的本质:模型给的「答案」和数据集里的「标准答案」几乎从不一字不差。 模型会说「答案应该是 C」「$\boxed{42}$」「Malia and Sasha Obama, born in 1998 and 2001」,而标准答案可能只是 C42Malia and Sasha

每个基准的答案形态不同,于是判分策略也不同:

答案形态例子基准用什么策略
单个字母(ABCD)MMLU、GPQA正则抽 + 精确匹配(§2.1)
数学表达式MATH让另一个模型判等价(§2.2)
短答案 / 多个 spanDROP、MGSM词袋 F1 + 数字归一(§2.3)
开放短答SimpleQA、BrowseCompLLM 当裁判(§2.4)
可执行代码HumanEval跑测试用例(§2.5)

2.1 策略一:正则抽答案 + 精确匹配(最便宜)

要解决的小问题: 从「…所以答案是 C」里抠出那个 C

思路: 先用模板逼模型把答案放在固定格式里(「最后一行写 'Answer: $LETTER'」),再用正则去抓那一行。约束输出格式,抽取就稳。

两道防线:

  1. 归一化(normalize_response,common.py:355-374):把 **\$\boxed{}\$\text{ 等 markdown/LaTeX 装饰统统删掉,免得它们挡住匹配。
  2. 多语言答案正则(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」(多个短语),且词序、冠词、标点都不该影响判分。

思路三步走:

  1. 重度归一化(_normalize_answer,drop_eval.py:58-67):小写、去标点、去冠词(a/an/the)、把数字标准化(1212.0 视同,_normalize_number,drop_eval.py:78-82)、压空白。
  2. 词袋 F1(_compute_f1,drop_eval.py:119-134):把答案拆成词的集合,算预测集合与标准集合的 token 级 F1。
  3. 多 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-377drop_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-45GRADER_TEMPLATE79-93grade_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_kpass@k(k 次里至少一次通过的无偏估计,humaneval_eval.py:104-110)——这是代码生成评测的标准指标。

2.6 代码地图

主题文件符号
多语言答案正则common.pyMULTILINGUAL_ANSWER_REGEXESMULTILINGUAL_ANSWER_PATTERN_TEMPLATE
回答归一化common.pynormalize_responsenormalize_extracted_answer
数学等价裁判common.pyEQUALITY_TEMPLATEcheck_equality
DROP 词袋 F1 + 匈牙利对齐drop_eval.py_compute_f1_align_bags_match_numbers_if_presentget_drop_metrics
SimpleQA LLM 裁判 + F1 指标simpleqa_eval.pyGRADER_TEMPLATESimpleQAEval.grade_sample
BrowseComp LLM 裁判browsecomp_eval.pyGRADER_TEMPLATEBrowseCompEval.grade_sample
HumanEval 执行判分 + pass@khumaneval_eval.pyevaluate_functional_correctnessfind_codeestimate_pass_at_k
MGSM 多语言答案解析mgsm_eval.pyparse_answerscore_mgsm