DROP 阅读理解任务的评估实现详解:离散推理指标、配置与源码解析
【免费下载链接】lm-evaluation-harnessA framework for few-shot evaluation of language models.项目地址: https://gitcode.com/GitHub_Trending/lm/lm-evaluation-harness
本指南以 lm-evaluation-harness 仓库中的 DROP 任务实现(lm_eval/tasks/drop/)为核心,系统讲解 DROP 基准的背景、任务配置(YAML)的每一个字段含义、数据预处理与答案解析逻辑,以及官方风格 EM/F1 指标的底层算法。读完本文,你将能够在本地复现 DROP 评估,并理解基于生成式(
generate_until)问答任务的完整评估链路。
DROP 基准概览:需要离散推理的阅读理解
DROP(DiscreteReasoningOverParagraphs)是 Dua 等人于 2019 年提出的问答基准,其论文标题为《DROP: A Reading Comprehension Benchmark Requiring Discrete Reasoning Over Paragraphs》,出处见 lm_eval/tasks/drop/README.md。该数据集的特点可以概括为三点:
- 全面理解段落:系统需要把问题中的多个指代(references)映射到段落中的相应实体;
- 离散操作:在映射完成后,模型还必须执行如加法(addition)、计数(counting)、排序(sorting)等离散运算才能得到答案;
- 众包 + 对抗性构建:数据由众包工人以对抗方式构造,规模约 96k 个问答对,专门用于暴露模型在"指代消解 + 数值/集合推理"上的短板。
因此,DROP 不是单纯的抽取式 QA——很多问题的答案无法直接从段落中摘录,必须经过数值或逻辑变换,这也是它区别于 SQuAD 等基准的核心之处。
该任务在 lm-evaluation-harness 中的完整支持代码只有三个文件,彼此职责清晰:
| 文件 | 职责 |
|---|---|
| lm_eval/tasks/drop/README.md | 数据集背景、论文引用、任务/分组注册说明 |
| lm_eval/tasks/drop/default.yaml | 任务配置:数据集源、输入输出格式、指标、生成参数 |
| lm_eval/tasks/drop/utils.py | 数据预处理、答案解析、EM/F1 指标计算 |
从任务注册表(lm_eval/tasks/README.md)可以看到,drop被归类为"需要数值推理、阅读理解与问答"的英语任务。
任务配置逐字段解析:default.yaml 全解
任务的运行行为完全由 default.yaml 定义。下面逐字段说明其含义与设计意图:
task: drop dataset_path: EleutherAI/drop output_type: generate_until training_split: train validation_split: validation process_docs: !function utils.process_docs doc_to_text: "{{passage}} {{question}}" doc_to_target: "{{ answer|join(',')}}" target_delimiter: "" process_results: !function utils.process_results should_decontaminate: true doc_to_decontamination_query: "{{passage}} {{question}}" generation_kwargs: until: - "." metric_list: - metric: em aggregation: mean higher_is_better: true - metric: f1 aggregation: mean higher_is_better: true metadata: version: 3.0| 字段 | 值 | 含义 |
|---|---|---|
task | drop | 任务唯一名称,CLI 中直接用它指定任务 |
dataset_path | EleutherAI/drop | Hugging Face Datasets 上的数据集标识,评测时会自动加载该数据集 |
output_type | generate_until | 输出类型为自由文本生成(直到遇到终止符),而非loglikelihood打分 |
training_split | train | 训练集划分名(本任务暂未用于少样本示例以外的场景) |
validation_split | validation | 验证集划分名,评测默认使用该划分 |
process_docs | !function utils.process_docs | 指向 utils.py 的数据预处理函数,将原始文档转换为评测所需格式 |
doc_to_text | "{{passage}} {{question}}" | 把文档字段渲染为输入提示:段落 + 问题 |
doc_to_target | "{{ answer|join(',')}}" | 参考答案渲染:多个答案以逗号连接 |
target_delimiter | "" | 提示与目标之间的分隔符为空 |
process_results | !function utils.process_results | 指向 utils.py 的结果后处理与指标计算函数 |
should_decontaminate | true | 启用去污染流程(防止评测样本泄漏进训练语料) |
doc_to_decontamination_query | "{{passage}} {{question}}" | 去污染查询串,与输入一致 |
generation_kwargs.until | ["."] | 生成在遇到句号.时停止 |
metric_list | em、f1 | 指标:Exact Match 与 F1,聚合方式均为取平均,数值越高越好 |
metadata.version | 3.0 | 任务配置版本号 |
几个关键设计点值得展开:
输出类型为什么是generate_until?DROP 答案可以是数字、日期或自由文本片段,无法通过固定选项的loglikelihood打分完成,因此任务采用自回归生成。配合until: ["."],模型生成的答案会在第一个句号处截断——这与 DROP 官方评估"答案通常不含句号"的假设一致,能显著缩短解码长度。
target_delimiter: ""表示提示(passage + question)与参考答案之间不加分隔符,直接拼接。这与常见完形填空类任务的空分隔符惯例一致。
metric_list中的em与f1是注册在框架中的聚合器名称。查看 lm_eval/api/metrics.py 可以看到f1已注册为全局聚合函数;而 DROP 的em/f1实际由process_results函数在单样本层面计算后返回,框架再按aggregation: mean对全部样本求平均——这就是"自定义指标 + 标准聚合"的组合方式。
数据预处理:多候选答案的提取与规范化
原始 DROP 数据集的每个样本包含一个主答案(answer)和一组人工验证的答案(validated_answers)。utils.py 中的process_docs负责把它们重构成统一的评测格式:
def process_docs(dataset): def _process(doc): return { "id": doc["query_id"], "passage": doc["passage"], "question": doc["question"], "answers": get_answers(doc), } return dataset.map(_process)处理后的每个文档包含id、passage、question和answers(答案列表)。get_answers的核心逻辑是:
- 将
validated_answers展平——原始结构是{"number": [...], "date": [...], "spans": [...]}的列表式字段,展平后变成一组"单答案"字典(见_flatten_validated_answers,其 docstring 给出了{"number": ['1','8'], ...}→ 两个独立答案的示例); - 把主答案与所有验证答案合并为候选集合;
- 用
parse_answer将每个候选解析为统一形式,并用set去重——同一答案的多个验证来源只保留一份。
parse_answer体现了 DROP 答案的三种类型,全部归一化为元组(保证可哈希、便于去重):
def parse_answer(answer): if answer["number"] != "": return (str(answer["number"]),) # 数值型答案 if answer["spans"] != []: return tuple(answer["spans"]) # 文本片段答案 return ( " ".join( [answer["date"]["day"], answer["date"]["month"], answer["date"]["year"]] ).strip(), ) # 日期型答案(日月年拼接)优先级为:数值 > 文本片段 > 日期。日期被拼成"day month year"的字符串并去除首尾空白。这套逻辑保证了"一个问题的多个合法答案"都能参与评分,也解释了为什么doc_to_target中需要用join(',')来渲染多个参考答案。
结果评估:max-over-gold 策略与官方 EM/F1
process_results是评分的入口:
def process_results(doc, results): preds, golds = results, doc["answers"] max_em = 0 max_f1 = 0 for gold_answer in golds: exact_match, f1_score = get_metrics(preds, gold_answer) if gold_answer[0].strip(): max_em = max(max_em, exact_match) max_f1 = max(max_f1, f1_score) return {"em": max_em, "f1": max_f1}关键策略是max-over-gold:模型预测会与每个黄金答案逐一比较,取 EM 和 F1 的最大值作为该样本得分。这避免了"多合法答案只取一个导致误判"的问题。同时,空白黄金答案(gold_answer[0].strip()为空)会被跳过,避免对无效答案求分。
get_metrics实现了与 DROP 官方评估一致的核心算法:
- Exact Match:先将预测与黄金答案分别转为"标准化片段 + 词袋"(
_answer_to_bags),若两边词袋的元素集合相同且长度相等,EM 记为 1.0,否则 0.0; - F1:通过
_align_bags计算多答案对齐下的 F1。
_align_bags是最精妙的部分——当预测答案和黄金答案都包含多个片段时,它把"哪个预测片段对齐哪个黄金片段"建模为线性指派问题,并借助 SciPy 的linear_sum_assignment(匈牙利算法)求解最优 1-1 对齐,从而在scores矩阵上取到最大化 F1 的组合。对齐前还会用_match_numbers_if_present做约束:若黄金片段含数字而预测片段不含,或数字集合无交集,则这对匹配的得分为 0,防止数字答案被随意对齐到非数字片段。
_compute_f1是经典的标准 F1:
precision = intersection / len(predicted_bag) # 空预测时 precision = 1.0 recall = intersection / len(gold_bag) # 空黄金时 recall = 1.0 f1 = 2 * precision * recall / (precision + recall) if not (precision == 0 and recall == 0) else 0.0最后,_normalize链(_remove_articles→_white_space_fix→_fix_number→_remove_punc→lower()→ 按空格/连字符分词)完成官方风格的正则化:去掉冠词(a/an/the)、规整空白、把数字统一为浮点字符串、去除标点(纯数字串除外,避免破坏1,000这类数字)、统一小写。这些细节决定了数字答案(如1.0vs1)和文本答案(大小写、标点差异)能否被正确判为匹配。
实际运行:在本地复现 DROP 评估
确认任务注册后,即可通过 lm-evaluation-harness 的 CLI 运行 DROP 评估。典型命令形态(以 Hugging Face 模型为例):
lm_eval --model hf \ --model_args pretrained=EleutherAI/pythia-70m \ --tasks drop \ --batch_size 8关键参数说明:
--tasks drop:指定任务名,与 default.yaml 中的task: drop对应;--model hf/--model_args:选择模型后端并传入加载参数(如pretrained、dtype、device等);--batch_size:推理批大小;- 其他常用开关还包括
--limit(只跑前 N 条样本快速验证)、--num_fewshot(少样本示例数)等。
评测默认使用validation划分(见配置中的validation_split: validation)。运行结束后,输出中会给出em与f1两个指标的平均值,它们正是metric_list中声明、由process_results逐样本计算后按mean聚合的结果。
需要注意:生成式任务会调用模型的真实解码流程(generate_until),耗时通常高于打分式任务;until: ["."]保证每个样本的解码在句号处及时停止,从而控制推理成本。若机器资源有限,建议先用--limit 20验证链路完整性,再运行完整集。
小结:从配置到源码的完整评估链路
DROP 任务在 lm-evaluation-harness 中的实现,是"生成式问答任务"的典型范例,其技术要点可归纳为:
- 配置驱动:全部行为由 default.yaml 声明,包括数据集源、
generate_until输出类型、输入输出模板与终止符; - 预处理可编程:
!function指令让数据清洗、多答案提取(数值/片段/日期三类)完全自定义; - 指标贴近官方:EM 与 F1 的计算(词袋比较、数字感知对齐、匈牙利算法最优匹配、标准化链)忠实复刻 DROP 官方评测逻辑;
- 去污染内置:
should_decontaminate与doc_to_decontamination_query使该任务可直接接入框架的污染检查流程。
无论是想要复现论文中的基准结果,还是为自定义的阅读理解任务设计类似实现,DROP 这套"YAML 配置 + utils 函数"的组合都是值得对照的参考模板。
【免费下载链接】lm-evaluation-harnessA framework for few-shot evaluation of language models.项目地址: https://gitcode.com/GitHub_Trending/lm/lm-evaluation-harness
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考