Keep K3 suite selection and report-schema scoring in bash, merge K3/vision dataset_args into dpv4 yamls, and pin EvalScope at 735d920ee911 with local patches. Co-authored-by: Cursor <cursoragent@cursor.com>
24 KiB
👍 贡献基准评测
EvalScope作为ModelScope的官方评测工具,其基准评测功能正在持续优化中!我们诚邀您参考本教程,轻松添加自己的评测基准,并与广大社区成员分享您的贡献。一起助力EvalScope的成长,让我们的工具更加出色!
下面将介绍如何添加通用文本推理和多项选择两种基准评测,主要包含上传数据集、注册数据集、编写评测任务三个步骤。
基础概念
您可以先跳过本节,直接从[准备基准评测数据集](#1-准备基准评测数据集)开始,遇到不理解的代码后再查看具体的实现。
EvalScope评测流程主要包含以下步骤:
- 数据准备:通过
DataAdapter加载和预处理数据集。 - 任务定义:通过
TaskConfig定义评测任务的配置,包括模型、数据集、评估指标等。 - 评测执行:通过
run_task函数执行评测任务,并输出评测结果。
其中DataAdapter是我们需要重点了解的类,它是基准评测的核心组件。
DataAdapter架构和调用流程
DataAdapter采用Pipeline架构,支持通过钩子方法自定义行为。以DefaultDataAdapter为例,完整的评测流程如下:
1. 数据加载阶段
load_dataset()
├── load()
│ ├── load_from_remote() / load_from_disk()
│ │ ├── load_subsets()
│ │ │ └── load_subset() / load_fewshot_subset()
│ │ │ └── record_to_sample() [用户实现]
│ │ └── _post_process_samples()
│ │ └── process_sample_input()
│ │ ├── sample_to_fewshot() [用户实现]
│ │ ├── format_fewshot_template() [用户可选实现]
│ │ └── format_prompt_template() [用户可选实现]
│ └── 返回 DatasetDict
2. 模型推理阶段(每个样本)
run_inference()
├── _on_inference_start() [钩子方法]
├── _on_inference() [钩子方法]
└── _on_inference_end() [钩子方法]
└── 返回 TaskState
3. 指标计算阶段(每个样本)
calculate_metrics()
├── filter_prediction()
│ └── extract_answer() [用户可选实现]
├── match_score() / score_with_judge_contracts()
└── 返回 SampleScore
4. 结果聚合阶段
aggregate_scores()
└── 返回 List[AggScore]
5. 报告生成阶段
generate_report()
├── _on_generate_report() [钩子方法]
└── _on_generate_report_end() [钩子方法]
└── 返回 Report
接入 LLM Judge Benchmark
使用者通过 Judge 参数 中的 typed judge 配置启用判别:
TaskConfig(
model='MODEL_UNDER_TEST',
datasets=['your_benchmark'],
judge={
'strategy': 'llm',
'models': {'model_id': 'JUDGE_MODEL', 'api_url': 'OPENAI_COMPATIBLE_URL', 'api_key': 'API_KEY'},
},
)
对于 adapter 开发者,judge 的 I/O 统一由 evalscope.api.judge 处理;不要在 adapter 中调用 self.llm_judge.judge(),也不要自行解析模型回复。
- 声明
scoring_policy:规则评分没有意义时使用JUDGE_ONLY;规则评分仍可用、但auto应使用 Judge 时使用JUDGE_DEFAULT;auto应保留规则评分时使用RULE_DEFAULT。 - 实现 adapter 唯一的入口
judge_definition(context)。普通的单 verdict 任务返回带有 Pydantic verdict schema 的JudgeDefinition.labels(...)或JudgeDefinition.numeric(...)。这些 helper 会追加 JSON 输出要求,并将 prompt、schema 与指标映射保持在同一处。 - 对 rubric、多 claim 或分阶段任务,定义 Pydantic verdict schema 和
OutputContract,然后返回JudgeDefinition.workflow(cases=..., request=..., reduce=...)。只有工作流需要时才传入expand=...、fallback=...或finalize=...。这些 callback 可以嵌套在judge_definition()中,也可以作为 adapter 的私有 helper,但必须由返回的 definition 持有,不能再作为 adapter hook 暴露。自定义 request 中应追加case.output_contract.instruction();仅当官方固定 JSON 要求与 schema 完全一致时才可保留官方格式。通过CaseVerdict.metadata传递上下文,不要把状态编码到case_id。 - 当确定性的规则无需调用模型即可判定样本时,返回
JudgeDefinition.skip(score, reason='...')。reason为必填字段,会以Score.metadata['judge_skip_reason']及Score.metadata['judge_skipped'] = True保存;Web review 面板会将其标为规则直接判分,而非 LLM verdict。 - 添加 scripted judge 测试,覆盖有效 JSON verdict、错误 JSON/自然语言回复和 transport error。无效 Judge 回复会从指标中排除,不会记为 0,executor 也不会自动纠错重试。
executor 负责请求调度、位置交换、重复次数、多 Judge quorum、聚合和 review 诊断;模型 transport 的重试策略由 generation_config 负责。
核心数据结构
1. Sample对象
表示单个评测样本,包含输入、目标答案和元数据:
@dataclass
class Sample:
input: Any # 输入内容(问题文本或聊天消息列表)
target: str # 目标答案(正确答案)
choices: Optional[List[str]] = None # 选择项(多选题使用)
subset_key: Optional[str] = None # 子集划分键(用于按类别分组)
metadata: Optional[Dict] = None # 元数据(推理过程、ID等)
tools: Optional[List] = None # 工具调用信息
2. TaskState对象
表示单次推理任务的完整状态:
@dataclass
class TaskState:
model: str # 模型名称
sample: Sample # 输入样本
messages: List[ChatMessage] # 聊天消息历史
output: ModelOutput # 模型原始输出
completed: bool # 任务是否完成
sample_id: Optional[str] = None # 样本ID
group_id: Optional[str] = None # 分组ID
metadata: Optional[Dict] = None # 任务元数据
3. ModelOutput对象
表示模型的原始输出:
@dataclass
class ModelOutput:
completion: str # 模型生成的文本
message: ChatMessage # 格式化的聊天消息
# 其他模型特定字段...
4. Score对象
表示单个样本的评分结果:
class Score(BaseModel):
value: Dict[str, int | float | bool] = Field(default_factory=dict) # 例如 {"accuracy": 1.0}
extracted_prediction: Optional[str] = None
prediction: Optional[str] = None
explanation: Optional[str] = None
metadata: Optional[Dict] = Field(default_factory=dict)
main_score_name: Optional[str] = None # 仅用于选择当前样本内的一个值
5. SampleScore对象
封装单个样本的完整评分信息:
class SampleScore(BaseModel):
score: Score
sample_id: Optional[str | int] = None
group_id: Optional[str | int] = None
sample_metadata: Optional[Dict] = None
6. AggScore对象
表示聚合后的评分统计:
class AggScore(BaseModel):
score: float = 0.0
metric_name: str = '' # 规范指标概念,例如 "accuracy"
aggregation: str = 'identity'
dimensions: Dict[str, str | int | float | bool] = Field(default_factory=dict)
num: int = 0
ids: Optional[List[str | int]] = None
metadata: Optional[Dict] = None
7. DatasetDict对象
管理多个数据集子集:
class DatasetDict(dict):
"""数据集字典,键为子集名称,值为Dataset对象"""
@classmethod
def from_dataset(cls, dataset, subset_list=None, limit=None, repeats=1):
"""从单个数据集创建多子集数据集字典"""
pass
DataAdapter核心方法详解
基于上述调用流程,以下是需要用户实现或可选重写的关键方法:
必须实现的方法
record_to_sample(record: Dict[str, Any]) -> Sample- 作用:将原始数据记录转换为标准Sample对象
- 输入:数据集中的原始记录字典
- 输出:标准化的Sample对象
- 示例:
def record_to_sample(self, record: Dict[str, Any]) -> Sample: return Sample( input=record['question'], target=record['answer'], metadata={'reasoning': record.get('explanation', '')} )
可选实现的方法
-
sample_to_fewshot(sample: Sample) -> str- 作用:将样本转换为few-shot示例字符串
- 输入:Sample对象
- 输出:格式化的few-shot示例文本
- 调用时机:构建few-shot提示时
-
extract_answer(prediction: str, task_state: TaskState) -> str- 作用:从模型原始输出中提取最终答案
- 输入:模型预测文本和任务状态
- 输出:提取的答案字符串
- 调用时机:计算指标前的答案清理
-
format_prompt_template(sample: Sample) -> str- 作用:格式化基础提示模板
- 输入:Sample对象
- 输出:格式化的提示文本
- 默认实现:使用
prompt_template.format(question=sample.input)
-
format_fewshot_template(fewshot: str, sample: Sample) -> str- 作用:格式化包含few-shot的提示模板
- 输入:few-shot示例字符串和Sample对象
- 输出:完整的few-shot提示
- 默认实现:使用
few_shot_prompt_template.format()
-
sample_filter(sample: Sample) -> bool- 作用:过滤数据集样本
- 输入:Sample对象
- 输出:是否保留该样本
- 默认实现:返回True(保留所有样本)
钩子方法系统
DataAdapter提供了钩子方法系统,支持在关键节点插入自定义逻辑:
推理阶段钩子
_on_inference_start(model, sample):推理开始前_on_inference(model, sample):执行推理_on_inference_end(model, sample, model_output, output_dir):推理结束后
报告生成钩子
_on_generate_report(scores, model_name):生成报告_on_generate_report_end(report, output_dir):报告生成后
适配器类型
EvalScope提供了两种主要的适配器基类:
-
DefaultDataAdapter:通用文本推理任务的基础适配器- 适用于开放式问答、数学推理、代码生成等任务
- 需要自定义答案提取逻辑
-
MultiChoiceAdapter:多项选择任务的专用适配器- 继承自
DefaultDataAdapter - 内置选择项格式化和答案提取逻辑
- 支持单选和多选模式
- 继承自
选择适配器类型的原则:
- 如果任务涉及从固定选项中选择答案 → 使用
MultiChoiceAdapter - 如果任务需要生成开放式答案 → 使用
DefaultDataAdapter
1. 准备基准评测数据集
您有两种方式准备基准评测数据集:
-
上传到ModelScope(推荐):将数据集上传到ModelScope平台,这样其他用户可以一键加载您的数据集,使用更加便捷,也能让更多用户受益于您的贡献。如需上传到ModelScope,可参考数据集上传教程。
-
本地使用:您也可以直接使用本地数据集进行评测,适合数据集尚在开发阶段或含有敏感信息的情况。
无论选择哪种方式,请确保数据的格式正确且可被加载。如使用本地数据集,可通过以下代码测试:
from modelscope import MsDataset
dataset = MsDataset.load("/path/to/your/dataset") # 替换为你的数据集
2. 创建文件结构
首先Fork EvalScope 仓库,即创建一个自己的EvalScope仓库副本,将其clone到本地。
git clone https://github.com/your_username/evalscope.git
cd evalscope
然后,在evalscope/benchmarks/目录下添加基准评测,结构如下:
evalscope/benchmarks/
├── benchmark_name
│ ├── __init__.py
│ ├── benchmark_name_adapter.py
│ └── ...
具体到GSM8K和MMLU-Pro,结构如下:
evalscope/benchmarks/
├── gsm8k
│ ├── __init__.py
│ ├── gsm8k_adapter.py
├── mmlu_pro
│ ├── __init__.py
│ ├── mmlu_pro_adapter.py
│ └── ...
3. 编写评测逻辑
下面将以GSM8K和MMLU-Pro为例,分别介绍通用文本推理和多项选择两种评测任务。
通用文本推理
通用文本推理任务通常要求模型对给定问题进行分析和推理,然后生成答案。以GSM8K(数学推理)为例:
我们需要在gsm8k_adapter.py中注册Benchmark并实现GSM8KAdapter类:
from typing import Any, Dict
from evalscope.api.benchmark import BenchmarkMeta, DefaultDataAdapter
from evalscope.api.dataset import Sample
from evalscope.api.evaluator import TaskState
from evalscope.api.registry import register_benchmark
from evalscope.constants import Tags
# 定义提示模板
PROMPT_TEMPLATE = """
Solve the following math problem step by step. The last line of your response should be of the form "ANSWER: $ANSWER" (without quotes) where $ANSWER is the answer to the problem.
{question}
Remember to put your answer on its own line at the end in the form "ANSWER: $ANSWER" (without quotes) where $ANSWER is the answer to the problem, and you do not need to use a \\boxed command.
Reasoning:
""".lstrip()
# 注册基准评测
@register_benchmark(
BenchmarkMeta(
name='gsm8k', # 基准测试名称
pretty_name='GSM8K', # 可读名称
dataset_id='AI-ModelScope/gsm8k', # 数据集ID 或 本地路径
tags=[Tags.MATH, Tags.REASONING], # 标签
description='GSM8K (Grade School Math 8K) is a dataset of grade school math problems, designed to evaluate the mathematical reasoning abilities of AI models.',
subset_list=['main'], # 子数据集列表
few_shot_num=4, # few-shot示例数量
train_split='train', # 训练集split名称
eval_split='test', # 评测集split名称
metric_list=['accuracy'], # 规范评估指标
prompt_template=PROMPT_TEMPLATE, # 提示模板
)
)
class GSM8KAdapter(DefaultDataAdapter):
def record_to_sample(self, record: Dict[str, Any]) -> Sample:
"""将原始数据记录转换为Sample对象"""
DELIM = '####'
question = record['question']
answer = record['answer'].split(DELIM)
target = answer.pop().strip() # 提取最终答案
reasoning = DELIM.join(answer) # 提取推理过程
return Sample(
input=question,
target=target,
metadata={'reasoning': reasoning.strip()}
)
def sample_to_fewshot(self, sample: Sample) -> str:
"""将样本转换为few-shot示例"""
if sample.metadata:
return (
f'{sample.input}\n\nReasoning:\n' +
f"{sample.metadata['reasoning']}\n\n" +
f'ANSWER: {sample.target}'
)
else:
return ''
def extract_answer(self, prediction: str, task_state: TaskState):
"""从模型预测中提取答案"""
from evalscope.filters.extraction import RegexFilter
# 使用正则表达式提取数字答案
regex = RegexFilter(regex_pattern=r'(-?[0-9.,]{2,})|(-?[0-9]+)', group_select=-1)
res = regex(prediction)
return res.replace(',', '').replace('+', '').strip().strip('.')
多项选择
多项选择任务要求模型从给定选项中选择正确答案。以MMLU-Pro为例,我们需要继承MultiChoiceAdapter:
from typing import Any, Dict
from evalscope.api.benchmark import BenchmarkMeta, MultiChoiceAdapter
from evalscope.api.dataset import Sample
from evalscope.api.registry import register_benchmark
from evalscope.constants import Tags
# 定义提示模板
USER_PROMPT_TEMPLATE = """Answer the following multiple choice question. The last line of your response should be of the following format: 'ANSWER: $LETTER' (without quotes) where LETTER is one of {letters}. Think step by step before answering.
Question:
{question}
Options:
{choices}
""".lstrip()
SUBSET_LIST = [
'computer science', 'math', 'chemistry', 'engineering', 'law', 'biology',
'health', 'physics', 'business', 'philosophy', 'economics', 'other',
'psychology', 'history'
]
@register_benchmark(
BenchmarkMeta(
name='mmlu_pro',
pretty_name='MMLU-Pro',
tags=[Tags.MULTIPLE_CHOICE, Tags.KNOWLEDGE],
description='MMLU-Pro is a benchmark for evaluating language models on multiple-choice questions across various subjects.',
dataset_id='modelscope/MMLU-Pro',
subset_list=SUBSET_LIST,
metric_list=['accuracy'],
few_shot_num=5,
train_split='validation',
eval_split='test',
prompt_template=USER_PROMPT_TEMPLATE,
)
)
class MMLUProAdapter(MultiChoiceAdapter):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.reformat_subset = True # 启用子集划分
def record_to_sample(self, record: Dict[str, Any]) -> Sample:
"""将原始数据记录转换为Sample对象"""
return Sample(
input=record['question'],
choices=record['options'], # 选择项列表
target=record['answer'], # 正确答案(如'A')
subset_key=record['category'].lower(), # 用于子集划分的key
metadata={
'cot_content': record['cot_content'],
'subject': record['category'].lower(),
'question_id': record['question_id'],
},
)
def sample_to_fewshot(self, sample: Sample) -> str:
"""将样本转换为few-shot示例"""
q_str = f"""Question:\n{str(sample.input)}"""
options = sample.choices if sample.choices is not None else []
# 格式化选择项
opt_str_list = []
for i, opt in enumerate(options):
opt_str_list.append(f"""{chr(65 + i)} {opt}""")
opt_str = f"""Options:\n{'\n'.join(opt_str_list)}"""
# 处理答案和推理过程
ans_str = sample.metadata['cot_content'] if sample.metadata is not None else ''
ans_str = ans_str.replace('The answer is', 'ANSWER:')
ans_opt = ans_str.split('ANSWER:')[-1].split('.')[0].strip().strip('(').strip(')')
ans_str = ans_str.replace(f'ANSWER: ({ans_opt})', f'ANSWER: {ans_opt}')
final_str = '\n'.join([q_str, opt_str, ans_str])
return final_str
关键差异说明
通用文本推理 vs 多项选择:
-
继承的基类:
- 通用文本推理:继承
DefaultDataAdapter - 多项选择:继承
MultiChoiceAdapter
- 通用文本推理:继承
-
Sample对象结构:
- 通用文本推理:主要包含
input和target - 多项选择:额外包含
choices(选择项列表)
- 通用文本推理:主要包含
-
答案提取方法:
- 通用文本推理:需要自定义
extract_answer()方法 - 多项选择:
MultiChoiceAdapter提供了标准的答案提取逻辑
- 通用文本推理:需要自定义
-
提示模板:
- 通用文本推理:更注重推理过程的引导
- 多项选择:专注于选择项的展示和答案格式
指标语义与主指标
报告不会猜测指标的含义。指标如何展示——名称、优化方向、单位、刻度与精度——统一来自
evalscope/metrics/semantics/catalog.py 中的集中目录;每个 benchmark 则声明自己的哪个指标承载结论。
大多数新 benchmark 无需改动目录。 复用已有规范指标名(accuracy、f1、
exact_match、pass_rate 等)时,语义已经声明好了:
metric_list=['accuracy'],
有两种情况值得你添一行:
-
产出多个指标或同一指标的多个变体时:明确声明哪个指标身份是主指标。Selector 可以约束 aggregation,也可以约束
k、scope、threshold等结构化维度:from evalscope.api.metric.semantics import MetricSelector metric_list=['precision', 'recall', 'f1', 'accuracy'], primary_metric=MetricSelector(name='f1', aggregation='mean'),仅产出一个非 diagnostic 指标身份时,它会被隐式选为主指标;若产出多个,则必须声明 selector, 报告生成不会按列表顺序猜测。Selector 必须恰好匹配一个实际产出的指标身份,并且其名称必须在
metric_list中声明。 -
引入新的规范指标名时:在
METRIC_DEFINITIONS中加一行,引用描述它的基线:# evalscope/metrics/semantics/catalog.py METRIC_DEFINITIONS['my_new_score'] = MetricEntry(baseline='quality.accuracy.ratio')
各命名层应保持分离:
metric_list、Score.value与自定义AggScore.metric_name使用accuracy这类规范名称。 少量旧别名会为兼容性自动迁移,但新 Adapter 不应继续增加别名。AggScore分别保存metric_name、aggregation和dimensions。不要把mean、pass@k、 threshold 或 scope 编码进指标名。- Catalog 以规范指标名为键;聚合方式导致的含义差异写入
AGGREGATION_SEMANTICS,benchmark 特有的同名冲突写入BENCHMARK_METRIC_OVERRIDES。 Score.main_score_name在单样本中选取一个值,BenchmarkMeta.primary_metric声明报告级 主指标身份,Report.primary_metric_identity持久化该身份。
修改 primary_metric 后,应运行 make docs-update BENCHMARK="<name>" FORCE=1 刷新自动生成的
元数据缓存,不要手工编辑 _meta/*.json。
未声明的指标会降级为 diagnostic,原样显示数值,不伪造方向与单位,并在日志中给出待补充的目录
条目。动态变体无需在目录中逐一枚举:k、问题类型、threshold、token range 等值应放入结构化
dimensions,并复用规范指标的语义。
4. 运行评测
调试代码,看看是否能正常运行。
GSM8K示例:
from evalscope import run_task, TaskConfig
task_cfg = TaskConfig(
model='Qwen/Qwen2.5-0.5B-Instruct',
datasets=['gsm8k'],
limit=10,
debug=True
)
run_task(task_cfg=task_cfg)
MMLU-Pro示例:
from evalscope import run_task, TaskConfig
task_cfg = TaskConfig(
model='Qwen/Qwen2.5-0.5B-Instruct',
datasets=['mmlu_pro'],
limit=10,
dataset_args={'mmlu_pro': {'subset_list': ['computer science', 'math']}},
debug=True
)
run_task(task_cfg=task_cfg)
输出示例:
+-----------------------+-----------+-----------------+------------------+-------+---------+---------+
| Model | Dataset | Metric | Subset | Num | Score | Cat.0 |
+=======================+===========+=================+==================+=======+=========+=========+
| Qwen2.5-0.5B-Instruct | gsm8k | Accuracy ↑ | main | 10 | 30% | default |
+-----------------------+-----------+-----------------+------------------+-------+---------+---------+
| Qwen2.5-0.5B-Instruct | mmlu_pro | Accuracy ↑ | computer science | 10 | 10% | default |
+-----------------------+-----------+-----------------+------------------+-------+---------+---------+
| Qwen2.5-0.5B-Instruct | mmlu_pro | Accuracy ↑ | math | 10 | 10% | default |
+-----------------------+-----------+-----------------+------------------+-------+---------+---------+
5. 基准评测文档生成
完成基准评测实现后,您可以使用EvalScope提供的工具生成标准文档。这将确保您的基准评测有一致的文档格式,并能够被其他用户轻松理解和使用。
要生成中英文文档,请运行以下命令,将根据注册信息生成文档:
pip install -e '.[docs]'
make docs
6. 提交PR
完成实现和文档生成后,请在提交 PR 前运行仓库的全部检查。该命令会先应用 Ruff 的安全修复和格式化,再验证其余 hooks:
make lint
检查通过后即可提交评审。完整开发流程请参考贡献指南,快来试一试吧🚀