👍 贡献基准评测#

EvalScope作为ModelScope的官方评测工具,其基准评测功能正在持续优化中!我们诚邀您参考本教程,轻松添加自己的评测基准,并与广大社区成员分享您的贡献。一起助力EvalScope的成长,让我们的工具更加出色!

下面将介绍如何添加通用文本推理多项选择两种基准评测,主要包含上传数据集、注册数据集、编写评测任务三个步骤。

基础概念#

小技巧

您可以先跳过本节,直接从准备基准评测数据集开始,遇到不理解的代码后再查看具体的实现。

EvalScope评测流程主要包含以下步骤:

  1. 数据准备:通过DataAdapter加载和预处理数据集。

  2. 任务定义:通过TaskConfig定义评测任务的配置,包括模型、数据集、评估指标等。

  3. 评测执行:通过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核心方法详解#

基于上述调用流程,以下是需要用户实现或可选重写的关键方法:

必须实现的方法#

  1. 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', '')}
        )
    

可选实现的方法#

  1. sample_to_fewshot(sample: Sample) -> str

    • 作用:将样本转换为few-shot示例字符串

    • 输入:Sample对象

    • 输出:格式化的few-shot示例文本

    • 调用时机:构建few-shot提示时

  2. extract_answer(prediction: str, task_state: TaskState) -> str

    • 作用:从模型原始输出中提取最终答案

    • 输入:模型预测文本和任务状态

    • 输出:提取的答案字符串

    • 调用时机:计算指标前的答案清理

  3. format_prompt_template(sample: Sample) -> str

    • 作用:格式化基础提示模板

    • 输入:Sample对象

    • 输出:格式化的提示文本

    • 默认实现:使用prompt_template.format(question=sample.input)

  4. format_fewshot_template(fewshot: str, sample: Sample) -> str

    • 作用:格式化包含few-shot的提示模板

    • 输入:few-shot示例字符串和Sample对象

    • 输出:完整的few-shot提示

    • 默认实现:使用few_shot_prompt_template.format()

  5. 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提供了两种主要的适配器基类:

  1. DefaultDataAdapter:通用文本推理任务的基础适配器

    • 适用于开放式问答、数学推理、代码生成等任务

    • 需要自定义答案提取逻辑

  2. MultiChoiceAdapter:多项选择任务的专用适配器

    • 继承自DefaultDataAdapter

    • 内置选择项格式化和答案提取逻辑

    • 支持单选和多选模式

选择适配器类型的原则:

  • 如果任务涉及从固定选项中选择答案 → 使用MultiChoiceAdapter

  • 如果任务需要生成开放式答案 → 使用DefaultDataAdapter

1. 准备基准评测数据集#

您有两种方式准备基准评测数据集:

  1. 上传到ModelScope(推荐):将数据集上传到ModelScope平台,这样其他用户可以一键加载您的数据集,使用更加便捷,也能让更多用户受益于您的贡献。如需上传到ModelScope,可参考数据集上传教程

  2. 本地使用:您也可以直接使用本地数据集进行评测,适合数据集尚在开发阶段或含有敏感信息的情况。

无论选择哪种方式,请确保数据的格式正确且可被加载。如使用本地数据集,可通过以下代码测试:

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
│   └── ...

具体到GSM8KMMLU-Pro,结构如下:

evalscope/benchmarks/
├── gsm8k
│   ├── __init__.py
│   ├── gsm8k_adapter.py
├── mmlu_pro
│   ├── __init__.py
│   ├── mmlu_pro_adapter.py
│   └── ...

3. 编写评测逻辑#

下面将以GSM8KMMLU-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 多项选择

  1. 继承的基类

    • 通用文本推理:继承DefaultDataAdapter

    • 多项选择:继承MultiChoiceAdapter

  2. Sample对象结构

    • 通用文本推理:主要包含inputtarget

    • 多项选择:额外包含choices(选择项列表)

  3. 答案提取方法

    • 通用文本推理:需要自定义extract_answer()方法

    • 多项选择:MultiChoiceAdapter提供了标准的答案提取逻辑

  4. 提示模板

    • 通用文本推理:更注重推理过程的引导

    • 多项选择:专注于选择项的展示和答案格式

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,可以查看我们的指南,快来试一试吧🚀