# 👍 贡献基准评测

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

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

## 基础概念

```{tip}
您可以先跳过本节，直接从[准备基准评测数据集](#1-准备基准评测数据集)开始，遇到不理解的代码后再查看具体的实现。
```

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对象
表示单个评测样本，包含输入、目标答案和元数据：

```python
@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对象
表示单次推理任务的完整状态：

```python
@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对象
表示模型的原始输出：

```python
@dataclass
class ModelOutput:
    completion: str              # 模型生成的文本
    message: ChatMessage         # 格式化的聊天消息
    # 其他模型特定字段...
```

#### 4. Score对象
表示单个样本的评分结果：

```python
@dataclass
class Score:
    value: Dict[str, float]      # 各指标的得分 {"acc": 1.0, "f1": 0.8}
    extracted_prediction: str    # 提取的预测答案
    prediction: str              # 原始预测文本
    metadata: Dict = None        # 评分元数据
```

#### 5. SampleScore对象
封装单个样本的完整评分信息：

```python
@dataclass
class SampleScore:
    score: Score                 # 评分对象
    sample_id: Optional[str]     # 样本唯一标识
    group_id: Optional[str]      # 分组标识
    sample_metadata: Optional[Dict] = None  # 样本元数据
```

#### 6. AggScore对象
表示聚合后的评分统计：

```python
@dataclass
class AggScore:
    metric: str                  # 指标名称
    value: float                 # 聚合值（如平均分）
    subset: str                  # 子集名称
    num_samples: int             # 样本数量
    agg_method: str              # 聚合方法（mean, median等）
    metadata: Dict = None        # 聚合元数据
```

#### 7. DatasetDict对象
管理多个数据集子集：

```python
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对象
   - **示例**：
   ```python
   def record_to_sample(self, record: Dict[str, Any]) -> Sample:
       return Sample(
           input=record['question'],
           target=record['answer'],
           metadata={'reasoning': record.get('explanation', '')}
       )
   ```

#### 可选实现的方法

2. **`sample_to_fewshot(sample: Sample) -> str`**
   - **作用**：将样本转换为few-shot示例字符串
   - **输入**：Sample对象
   - **输出**：格式化的few-shot示例文本
   - **调用时机**：构建few-shot提示时

3. **`extract_answer(prediction: str, task_state: TaskState) -> str`**
   - **作用**：从模型原始输出中提取最终答案
   - **输入**：模型预测文本和任务状态
   - **输出**：提取的答案字符串
   - **调用时机**：计算指标前的答案清理

4. **`format_prompt_template(sample: Sample) -> str`**
   - **作用**：格式化基础提示模板
   - **输入**：Sample对象
   - **输出**：格式化的提示文本
   - **默认实现**：使用`prompt_template.format(question=sample.input)`

5. **`format_fewshot_template(fewshot: str, sample: Sample) -> str`**
   - **作用**：格式化包含few-shot的提示模板
   - **输入**：few-shot示例字符串和Sample对象
   - **输出**：完整的few-shot提示
   - **默认实现**：使用`few_shot_prompt_template.format()`

6. **`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，可参考[数据集上传教程](https://www.modelscope.cn/docs/datasets/create)。

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

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

```python
from modelscope import MsDataset

dataset = MsDataset.load("/path/to/your/dataset")  # 替换为你的数据集
```

## 2. 创建文件结构

首先[Fork EvalScope](https://github.com/modelscope/evalscope/fork) 仓库，即创建一个自己的EvalScope仓库副本，将其clone到本地。

```bash
git clone https://github.com/your_username/evalscope.git
cd evalscope
```

然后，在`evalscope/benchmarks/`目录下添加基准评测，结构如下：

```text
evalscope/benchmarks/
├── benchmark_name
│   ├── __init__.py
│   ├── benchmark_name_adapter.py
│   └── ...
```
具体到`GSM8K`和`MMLU-Pro`，结构如下：

```text
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`类：

```python
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`：

```python
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对象结构**：
   - 通用文本推理：主要包含`input`和`target`
   - 多项选择：额外包含`choices`（选择项列表）

3. **答案提取方法**：
   - 通用文本推理：需要自定义`extract_answer()`方法
   - 多项选择：`MultiChoiceAdapter`提供了标准的答案提取逻辑

4. **提示模板**：
   - 通用文本推理：更注重推理过程的引导
   - 多项选择：专注于选择项的展示和答案格式

## 4. 运行评测

调试代码，看看是否能正常运行。

**GSM8K示例**：
```python
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示例**：
```python
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)
```

输出示例：

```text
+-----------------------+-----------+-----------------+------------------+-------+---------+---------+
| 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提供的工具生成标准文档。这将确保您的基准评测有一致的文档格式，并能够被其他用户轻松理解和使用。

要生成中英文文档，请运行以下命令，将根据注册信息生成文档：

```bash
pip install -e '.[docs]'
make docs
```

## 6. 提交PR
完成这些方法的实现和文档生成后，您的基准评测就准备就绪了！可以提交[PR](https://github.com/modelscope/evalscope/pulls)了。在提交之前请运行如下命令，将自动格式化代码：
```bash
make lint
```
确保没有格式问题后，我们将尽快合并你的贡献，让更多用户来使用你贡献的基准评测。如果你不知道如何提交PR，可以查看我们的[指南](https://github.com/modelscope/evalscope/blob/main/CONTRIBUTING.md)，快来试一试吧🚀
