👍 贡献基准评测#
EvalScope作为ModelScope的官方评测工具,其基准评测功能正在持续优化中!我们诚邀您参考本教程,轻松添加自己的评测基准,并与广大社区成员分享您的贡献。一起助力EvalScope的成长,让我们的工具更加出色!
下面将介绍如何添加通用文本推理和多项选择两种基准评测,主要包含上传数据集、注册数据集、编写评测任务三个步骤。
基础概念#
小技巧
您可以先跳过本节,直接从准备基准评测数据集开始,遇到不理解的代码后再查看具体的实现。
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() / llm_match_score()
└── 返回 SampleScore
4. 结果聚合阶段
aggregate_scores()
└── 返回 List[AggScore]
5. 报告生成阶段
generate_report()
├── _on_generate_report() [钩子方法]
└── _on_generate_report_end() [钩子方法]
└── 返回 Report
核心数据结构#
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对象#
表示单个样本的评分结果:
@dataclass
class Score:
value: Dict[str, float] # 各指标的得分 {"acc": 1.0, "f1": 0.8}
extracted_prediction: str # 提取的预测答案
prediction: str # 原始预测文本
metadata: Dict = None # 评分元数据
5. SampleScore对象#
封装单个样本的完整评分信息:
@dataclass
class SampleScore:
score: Score # 评分对象
sample_id: Optional[str] # 样本唯一标识
group_id: Optional[str] # 分组标识
sample_metadata: Optional[Dict] = None # 样本元数据
6. AggScore对象#
表示聚合后的评分统计:
@dataclass
class AggScore:
metric: str # 指标名称
value: float # 聚合值(如平均分)
subset: str # 子集名称
num_samples: int # 样本数量
agg_method: str # 聚合方法(mean, median等)
metadata: 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=['acc'], # 评估指标
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=['acc'],
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提供了标准的答案提取逻辑
提示模板:
通用文本推理:更注重推理过程的引导
多项选择:专注于选择项的展示和答案格式
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 | mean_acc | main | 10 | 0.3 | default |
+-----------------------+-----------+-----------------+------------------+-------+---------+---------+
| Qwen2.5-0.5B-Instruct | mmlu_pro | mean_acc | computer science | 10 | 0.1 | default |
+-----------------------+-----------+-----------------+------------------+-------+---------+---------+
| Qwen2.5-0.5B-Instruct | mmlu_pro | mean_acc | math | 10 | 0.1 | default |
+-----------------------+-----------+-----------------+------------------+-------+---------+---------+
5. 基准评测文档生成#
完成基准评测实现后,您可以使用EvalScope提供的工具生成标准文档。这将确保您的基准评测有一致的文档格式,并能够被其他用户轻松理解和使用。
要生成中英文文档,请运行以下命令,将根据注册信息生成文档:
pip install -e '.[docs]'
make docs
6. 提交PR#
完成这些方法的实现和文档生成后,您的基准评测就准备就绪了!可以提交PR了。在提交之前请运行如下命令,将自动格式化代码:
make lint
确保没有格式问题后,我们将尽快合并你的贡献,让更多用户来使用你贡献的基准评测。如果你不知道如何提交PR,可以查看我们的指南,快来试一试吧🚀