OpenCompass使用指北

OpenCompass 框架评测原理

注册器

Opencompass 是一个评测框架, 但是实际后台模型的导入, 执行, 数据集的处理与导入等, 都是基于另一个框架 MMEngine 的, 在 OpenCompass 中大量使用了注册器的机制, 在数据集处理与模型处理的过程中均使用了注册器的机制来管理我们评测的模型与数据集.

什么是注册器

简而言之, 注册器就是一个智能字典, 它把名字(字符串)和类(或函数)绑定在一起, 并帮你自动构建这些类或函数. 所以实际上包含两个步骤, 注册绑定与自动构建类的对象或者调用函数.

注册机制

在 opencompass 中, 实现了数据集的注册器与模型的注册器等各种注册器. 下面是这些注册器的初始化,

PARTITIONERS = Registry('partitioner', locations=['opencompass.partitioners'])
RUNNERS = Registry('runner', locations=['opencompass.runners'])
TASKS = Registry('task', locations=['opencompass.tasks'])
MODELS = Registry('model', locations=['opencompass.models'])
# TODO: LOAD_DATASET -> DATASETS
LOAD_DATASET = Registry('load_dataset', locations=['opencompass.datasets'])
TEXT_POSTPROCESSORS = Registry(
    'text_postprocessors', locations=['opencompass.utils.text_postprocessors'])
DICT_POSTPROCESSORS = Registry(
    'dict_postprocessors', locations=['opencompass.utils.dict_postprocessors'])

EVALUATORS = Registry('evaluators', locations=['opencompass.evaluators'])

ICL_INFERENCERS = Registry('icl_inferencers',
                           locations=['opencompass.openicl.icl_inferencer'])
ICL_RETRIEVERS = Registry('icl_retrievers',
                          locations=['opencompass.openicl.icl_retriever'])
ICL_DATASET_READERS = Registry(
    'icl_dataset_readers',
    locations=['opencompass.openicl.icl_dataset_reader'])
ICL_PROMPT_TEMPLATES = Registry(
    'icl_prompt_templates',
    locations=['opencompass.openicl.icl_prompt_template'])
ICL_EVALUATORS = Registry('icl_evaluators',
                          locations=['opencompass.openicl.icl_evaluator'])
METRICS = Registry('metric',
                   parent=MMENGINE_METRICS,
                   locations=['opencompass.metrics'])
TOT_WRAPPER = Registry('tot_wrapper', locations=['opencompass.datasets'])

我们可以看到 LOAD_DATASET 的注册器的位置在 opencompass.datasets 目录下, 这个目录下定义了各种数据集的格式, 并且将这些数据集注册到了 LOAD_DATASET 这个注册器中.

上述注册的注册器也就是 OpenCompass 的大致框架, 以及所有会使用到的功能单元, 例如 TASKS, MODELS, 这些功能单元之间存在着嵌套, 调用, 等复杂的关系, 也就组成了 OpenCompass. 这些复杂的关系可以从配置文件以及 OpenCompass 的执行过程得到, 后续我们会进行解析.

配置文件

Opencompass 的配置文件使用的是 OpenMMLab 风格的配置文件, 配置文件的特点是遵从 Python 格式, 使用 Python 语法, 如果感兴趣可以去阅读 MMeigine 的配置文件的说明文档, 有更加详细的说明. 我总结了一下配置文件的核心特点与思想, 默认大家熟悉 python 语法:

  1. 配置文件的格式上, 使用 python 中的字典的语法格式替代了传统的 json 以及 YAML 等格式.
  2. 配置文件允许继承, 继承直接使用 python 的 import 语法, 继承配置文件 A.py 的 B.py 文件可以直接读取 A.py 中的配置信息
  3. 可以在程序执行前, 或者在执行命令中修改配置文件中的配置(临时).

我们通过 example/eval_deepseek_r1.py 配置文件说明上述的特点, 下面我仅粘贴了部分配置文件.

# 使用 lazy_import 的导入方式继承上 import 文件中的配置信息
import os.path as osp
from itertools import product
from opencompass.models import OpenAISDK
from mmengine.config import read_base
from opencompass.utils.text_postprocessors import extract_non_reasoning_content
from opencompass.partitioners import NaivePartitioner, NumWorkerPartitioner
from opencompass.tasks import OpenICLInferTask, OpenICLEvalTask
from opencompass.runners import LocalRunner
from opencompass.models import(
    TurboMindModelwithChatTemplate,
)
# 数据集的配置信息, not lazy_import 模式, 需要真正的触发 import
with read_base():
    from opencompass.configs.datasets.aime2024.aime2024_llmverify_repeat8_gen_e8fcee import aime2024_datasets # 8 Run
    from opencompass.configs.summarizers.groups.OlympiadBench import OlympiadBenchMath_summary_groups

datasets = sum(
   (v for k, v in locals().items() if k.endswith('_datasets')),
    [],
)

配置文件与注册器配合使用

配置文件除了包含一些通用的配置信息, 另一个用法是配合注册器在程序中创建对象的实例. 与注册器配合使用时, 注册器的一大特点是可以在运行时根据配置文件的不同实例化不同的对象, 可以是子类或者父类, 实现了面向对象的多态机制. 在函数 build_from_cfg 中根据配置文件的 type 构造不同类的对象.

例如, 在模型推理的阶段, 我们定义的模型推理相关的属性如下:

infer = dict(
    partitioner=dict(
        type=NumWorkerPartitioner,
        num_worker=1
        # Similar with data-parallelism, how many workers for evaluation,
        # each worker will evaluate a part of the dataset. Total GPUs = num_worker * num_gpus_per_worker
        # For example, If you have 8 GPUs, for 7B model using 1 GPU for one instance, you can set num_worker=8
        # to max-utilize the GPUs.
        # If you have 8 GPUs, for 14B model using 2 GPUs for one instance, you can set num_worker=4
    ),
    runner=dict(
        type=LocalRunner,
        task=dict(type=OpenICLInferTask)
    ),
)

infer 推理过程中实际上定义了 partitioner 分片器的类型是 NumWorkerPartitioner, 它的 num_worker 属性为 1. 以及 runner 的类型是 LocalRunner, runner 执行的任务类型是 OpenICLInferTask. 在程序实际执行的过程中, 都是根据这些 type 以及对应的参数来创建实例对象的. 我们想要在 opencompass 中新引入一个模块, 我们就需要先将它注册到注册器中, 然后在需要的时候, 实例化一个该模块的对象.

opencompass 整体执行流程

我们先从宏观的角度看一下 opencompass 框架的评测流程以及运行机制, opencompass 在进行一次大模型对某个数据集的评测过程中, 使用了下列几种模块:

  1. 数据集: opencompass 提供了大量的内置数据集, 也可以自定义数据集, 数据集中还定义了数据集的读取方式, 推理时 prompt 的构造等, 后续再详细说明.
  2. 模型: 需要评测的模型, opencompass 提供了基于 HuggingFace 的模型, 基于 API 的模型, 也可以自定义模型, 后续具体说明.
  3. 任务划分器(Partitioner): OpenCompass 支持自定义评测任务的任务划分器, 它支持将一次评测流程划分为多个子任务并发执行, 例如不同模型分别评测不同的数据集, 任务划分器可以根据配置生成多个推理与评估的子任务.
  4. 任务(Task): Partitioner 可以将评测的推理与评估流程分解为各种子任务, 任务是 OpenCompass 中的一个基础模块, 本身是一个独立的脚本, 用于执行计算密集的操作, opencompass 支持两种不同的任务, OpenICLInferTaskOpenICLEvalTask, 分别执行评估与推理步骤.
  5. 执行器(Runner): 在 Partitioner 生成各种推理与评估任务后, 负责任务的启动与调度执行, 根据使用本地机器, 或者 Slurm 集群等, 使用不同的调度机制.
  6. 结果生成器(summarizer): 在执行推理与评估之后, 将结果进行总结输出为 csv 等可视化的文件.

我将这些模块之间的作用关系以及协作总结成下面的流程图:

img

接下来我们将要对上述的几个模块逐一介绍, 我们还会介绍 OpenCompass 的整个执行与分析的过程, 我们使用 OpenCompass 的强推理模型的流程为例, 这个强推理模型的配置文件为: example/eval_deepseek_r1.py.

数据集配置

eval_deepseek_r1 评测的数据集为 aime2024_datasets, 由于 python 配置文件的继承特性, 我们很容易看到这个数据集的配置信息如下:

aime2024_datasets = [
    dict(
        abbr=f'aime2024-run{idx}',
        type=Aime2024Dataset,
        path='opencompass/aime2024',
        reader_cfg=aime2024_reader_cfg,
        infer_cfg=aime2024_infer_cfg,
        eval_cfg=aime2024_eval_cfg,
        mode='singlescore',
    )
    for idx in range(8)
]

我将这些配置信息, 以及配置信息的继承关系画成了下面的图示:

img

这些属性解析如下:

  1. type: Aime2024Dataset 数据集的类型, 最重要的是数据集的读取方式, 从数据集中读取哪些列
  2. reader_cfg: 哪些列作为输入, 哪些列作为输出

数据集的推理步骤

OpenCompass 中根据数据集的格式不同, 支持两种推理模式, 典型的不同的数据集格式为选择题和问答题. 通常, 选择题使用 PPLInferencer 一种基于 Perplexity(困惑度) 的评估模式, 问答题使用 GenInferencer, 对应着生成式推理, 需要评估模型输出的文本质量.

PPLInferencer

PPLInferencer 对应判别式推理. 在推理时, 模型被要求计算多个输入字符串各自的混淆度(PerPLexity / PPL), 并将其中 PPL 最小的项作为模型的推理结果.

其中 PPL 的计算公式如下:

\[PPL = exp(-\frac{1}{N}\sum_{i=1}^{N}log \ p(x_i)) \]

其中 \(x_i\) 是第 \(i\) 个 token, \(p(x_i)\) 是模型预测极其概率, 其实本质上和计算测试文本上的平均对数似然的负指数, 因此 \(PPL\) 越低等价于极大似然结果越高, 模型对输出越有信心.

当使用 PPLInferencer 进行判别式推理的时候, prompt 的 template 是一个 dict, 表示每一句话所对应的模板, 例如:

prompt_template=dict(
    type=PromptTemplate,
    template=dict(
        "A": dict(
            round=[
                dict(role="HUMAN", prompt="Question: Which is true?\nA. {A}\nB. {B}\nC. {C}"),
                dict(role="BOT", prompt="Answer: A"),
            ]
        ),
        "B": dict(
            round=[
                dict(role="HUMAN", prompt="Question: Which is true?\nA. {A}\nB. {B}\nC. {C}"),
                dict(role="BOT", prompt="Answer: B"),
            ]
        ),
        "C": dict(
            round=[
                dict(role="HUMAN", prompt="Question: Which is true?\nA. {A}\nB. {B}\nC. {C}"),
                dict(role="BOT", prompt="Answer: C"),
            ]
        ),
        "UNK": dict(
            round=[
                dict(role="HUMAN", prompt="Question: Which is true?\nA. {A}\nB. {B}\nC. {C}"),
                dict(role="BOT", prompt="Answer: None of them is true."),
            ]
        ),
    )
)

实际推理模型的输入与输出与下面的格式类似:

以下是关于法律的单项选择题, 请直接给出正确答案的选项. \n题目: 甲偷割正在使用中的高速公路紧急信息警示屏的电源线, 价值 5000 元, 甲的行为应认定为: A 盗窃罪,B 故意毁坏财物罪,C 破坏交通设施罪,D 破坏交通工具罪\n 答案为: A 模型输出这一句话的 \(PPL=2.3\)
以下是关于法律的单项选择题, 请直接给出正确答案的选项. \n题目: 甲偷割正在使用中的高速公路紧急信息警示屏的电源线, 价值 5000 元, 甲的行为应认定为: A 盗窃罪,B 故意毁坏财物罪,C 破坏交通设施罪,D 破坏交通工具罪\n 答案为: B 模型输出这一句话的 \(PPL=2.5\)
以下是关于法律的单项选择题, 请直接给出正确答案的选项. \n题目: 甲偷割正在使用中的高速公路紧急信息警示屏的电源线, 价值 5000 元, 甲的行为应认定为: A 盗窃罪,B 故意毁坏财物罪,C 破坏交通设施罪,D 破坏交通工具罪\n 答案为: C 模型输出这一句话的 \(PPL=1.6\)
以下是关于法律的单项选择题, 请直接给出正确答案的选项. \n题目: 甲偷割正在使用中的高速公路紧急信息警示屏的电源线, 价值 5000 元, 甲的行为应认定为: A 盗窃罪,B 故意毁坏财物罪,C 破坏交通设施罪,D 破坏交通工具罪\n 答案为: D 模型输出这一句话的 \(PPL=1.9\)

在实际计算的时候, 会将上述的四个语句放入一个 batch 中, 如果 batch_size 太小, 就放入多个 batch, 计算每一个问答对的 \(PPL\) 分数.

GenInferencer

GenInferencer 对应生成式的推理. 在推理时, 模型被要求以输入的提示词为基准, 继续往下续写. GenInferencer 输出会被记录, 并可能用于进一步打分(例如后续会使用到的 GenericLLMEvaluator 中的裁判模型). GenInferencer 适合需要生成式回答 的任务, 如问答、摘要、对话、代码生成等可以评估生成内容的准确性、完整性、自然性等.

Aime2024Dataset 中使用的就是生成式推理, 它的配置信息如下:

aime2024_infer_cfg = dict(
    prompt_template=dict(
        type=PromptTemplate,
        template=dict(
            round=[
                dict(role='HUMAN', prompt='{question}\nRemember to put your final answer within \\boxed{}.'),
            ],
        )
    ),
    retriever=dict(type=ZeroRetriever),
    inferencer=dict(type=GenInferencer)
)

数据集的评估

OpenCompass 支持多种评估指标, 每一种指标都存在一个对应的可以实例化的评估类, 在 opencompass/openicl/icl_evaluator 文件夹下, 这里我们以 Aime2024Dataset 数据集在推理模型的评测中使用的 GenericLLMEvaluator 为例, 说明评估器的工作原理. 通常在强推理模型中, 在答案验证层面, 为了减少基于规则评测带来的误判, 我们统一使用基于LLM验证的方式进行评测, 我们可以看到评测器的定义如下:

aime2024_eval_cfg = dict(
    evaluator=dict(
        type=GenericLLMEvaluator,
        prompt_template=dict(
            type=PromptTemplate,
            template=dict(
            begin=[
                dict(
                    role='SYSTEM',
                    fallback_role='HUMAN',
                    prompt="You are a helpful assistant who evaluates the correctness and quality of models' outputs.")
            ],
                round=[
                dict(
                    role='HUMAN',
                    prompt = GRADER_TEMPLATE
                ),
            ]),
        ),
        dataset_cfg=dict(
            type=Aime2024Dataset,
            path='opencompass/aime2024',
            reader_cfg=aime2024_reader_cfg,
        ),
        judge_cfg=dict(),
        dict_postprocessor=dict(type=generic_llmjudge_postprocess),
    ),
    pred_role='BOT',
)

使用的是生成式的大模型评测器 GenericLLMEvaluator, 在这个评测器中有使用大模型评测的逻辑, 这部分如下:

  1. 加载并处理需要评测的数据集, load_and_preprocess_test_data()
  2. 加载被评测模型对评测数据集的预测结果, _load_predictions()
  3. 处理预测文本, _process_predictions(), 如果设置了 pred_role, 会提取指定角色的回答
  4. 评估, 首先获取参考答案(reference), 初始化评估器 Evaluator(通过 ICL_EVALUATORS.get(...) 动态构造类), 然后调用 evaluator.evaluate(k, n, test_set, **params) 执行打分, 若开启 dump_details, 还会格式化详细预测记录.

分数计算的过程在每个评测器的 score 部分计算, 在使用大模型进行评测时, 打分机制如下:

def score(
    self,
    predictions,
    references: Optional[List] = None,
    test_set: Optional[Dataset] = None,
) -> Dict:
    """Apply to single-model scoring.

    Args:
        predictions: List of model predictions
        references: List of reference answers
        test_set: Optional Dataset containing additional
        context for evaluation
    """
  1. 获取被评测模型的推理输出, 也就是 prediction 信息, 以及评测数据集的 reference 信息, 也就是标准答案.
  2. 默认大模型评测数据集是 LMEvalDataset, 但是这其实是一个空数据集, 我们需要将测试数据集中每个样本对应的 predictionreference 拼接, 得到评测大模型的输入数据集, 构建评测输入数据集.
  3. 构建裁判模型, 裁判模型是在评测的配置文件中传入评测使用的大模型配置信息, 写入配置的 judge_cfg 部分, 例如, 在 example/eval_deepseek_r1.py 中配置信息如下:
verifier_cfg = dict(
        abbr='qwen2-5-32B-Instruct',
        type=OpenAISDK,
        path='Qwen/Qwen2.5-32B-Instruct', # You need to set your own judge model path
        key='sk-1234', # You need to set your own API key
        openai_api_base=[
            'http://172.30.56.1:4000/v1', # You need to set your own API base
        ],
        meta_template=dict(
            round=[
                dict(role='HUMAN', api_role='HUMAN'),
                dict(role='BOT', api_role='BOT', generate=True),
            ], 
        ),
        query_per_second=16,
        batch_size=1024,
        temperature=0.001,
        tokenizer_path='gpt-4o-2024-05-13',
        verbose=True,
        max_out_len=16384,
        # max_seq_len=32768,
        max_seq_len=49152,
)

for item in datasets:
    # item['infer_cfg']['inferencer']['max_out_len'] = 32768 # You can unset this line if you want to avoid length cutoff
    if 'judge_cfg' in item['eval_cfg']['evaluator']:
        item['eval_cfg']['evaluator']['judge_cfg'] = verifier_cfg
  1. 评测模型的 prompt 是一个特殊的 prompt, 它包含两部分, 分别是 you are a helpful assistant who evaluates the correctness and quality of models' outputs.GRADER_TEMPLATE, GRADER_TEMPLATE 用于提示大模型判断 prediction 是不是符合 answer 的, 主要部分如下:
    Here is your task. Simply reply with either CORRECT, INCORRECT. Don't apologize or correct yourself if there was a mistake; we are just trying to grade the answer.


    <Original Question Begin>: \n{question}\n<Original Question End>\n\n
    <Gold Target Begin>: \n{answer}\n<Gold Target End>\n\n
    <Predicted Answer Begin>: \n{prediction}\n<Predicted End>\n\n
    
    Judging the correctness of candidates' answers:
  1. 使用 GenInferencer.inference() 调用裁判模型打分

OpenCompass 还支持各种其他的评估器, 按照不同维度给输出结果打分, 例如 ACCEvaluator, EMEvaluator, BleuEvaluator 等.

模型配置

OpenCompass 支持多种不同模型的测试, 基础部分支持基于 HuggingFace 的模型以及基于 API 的模型, 还可以自定义模型结构, 以 eval_deepseek_r1.py 使用的模型为例, 如下:

models += [
    # You can comment out the models you don't want to evaluate
    # All models use sampling mode
    dict(
        type=TurboMindModelwithChatTemplate,
        abbr='deepseek-r1-distill-qwen-7b-turbomind',
        path='deepseek-ai/DeepSeek-R1-Distill-Qwen-7B',
        engine_config=dict(session_len=32768, max_batch_size=128, tp=1),
        gen_config=dict(
                        do_sample=True,
                        temperature=0.6,
                        top_p=0.95,
                        max_new_tokens=32768),
        max_seq_len=32768,
        max_out_len=32768,
        batch_size=64,
        run_cfg=dict(num_gpus=1),
        pred_postprocessor=dict(type=extract_non_reasoning_content)
    ),
]

测试的模型类别为 TurboMindModelwithChatTemplate, 测试的模型为 deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.
模型上面部分是类别, 后面的信息是实例化这个模型时的属性信息, 不同的模型配置不同, 可以传入的参数也不同. 例如, 在强推模型中, 在模型层面 OpenCompass 建议使用 Sampling 方式, 以减少因为Greedy评测带来的大量重复, 但是我们研发环境测试的 OpenAI 类型(基于 API) 的模型没有这个参数, 但是实际上可以通过修改源码传入这个参数来生效.

运行机制

OpenCompass 中一个评测任务的运行主要受到任务划分器(Partitioner), 任务(Task), 以及执行器(RUNNER) 这三个模块控制, 根据代码执行的步骤, 我们可以归纳 OpenCompass 执行一个评测任务的流程如下:

  1. run.py 开始执行, 首先解析配置文件, 解析配置文件时遵循我们之前提到的配置文件继承规则. 例如在解析数据集配置文件时会向前解析数据集使用的 GenInferencer 等配置文件, 也就是向前 import 的过程, import 的时候会将这些配置文件中使用的模块注册到注册器中, NumWorkerPartitioner 也是通过类似的方法在解析配置文件的时候注册到注册器中, 供后续实例化的时候使用.
  2. 推理阶段: 首先实例化一个任务划分器(Partitioner), 推理阶段默认的 Partitioner 是 NaivePartitioner, 也可以在配置文件中指定使用的 Partitioner, 不同的 Partitioner 的作用可以在它对应的源码中看到, 还是以 example/eval_deepseek_r1.py 配置文件为例, 这个配置文件中推理过程的配置如下:
# Inference configuration
infer = dict(
    partitioner=dict(
        type=NumWorkerPartitioner,
        num_worker=1
        # Similar with data-parallelism, how many workers for evaluation,
        # each worker will evaluate a part of the dataset. Total GPUs = num_worker * num_gpus_per_worker
        # For example, If you have 8 GPUs, for 7B model using 1 GPU for one instance, you can set num_worker=8
        # to max-utilize the GPUs.
        # If you have 8 GPUs, for 14B model using 2 GPUs for one instance, you can set num_worker=4
    ),
    runner=dict(
        type=LocalRunner,
        task=dict(type=OpenICLInferTask)
    ),
)

可以看到推理阶段还有两个重要的配置, 分别是 RUNNER 和 TASK, RUNNER 指的是执行器, 默认的 RUNNER 是 LocalRunner, 本地直接运行, TASK 是需要执行的任务, Partitioner 会根据配置文件将评测任务进行划分, 得到若干个 OpenICLInferTask 类型的推理任务. 最后 RUNNER 会执行这些任务, 这个步骤如下:

# 会根据主机中安装的 python 版本来选择 python3 或 python, 然后执行
if 'python3 ' in cmd or 'python ' in cmd:
    # If it is an infer type task do not reload if
    # the current model has already been loaded.
    if 'infer' in self.task_cfg.type.lower():
        # If a model instance already exists,
        # do not reload it.
        task.run(cur_model=getattr(self, 'cur_model',
                                    None),
                    cur_model_abbr=getattr(
                        self, 'cur_model_abbr', None))
        self.cur_model = task.model
        self.cur_model_abbr = model_abbr_from_cfg(
            task.model_cfg)
    else:
        task.run()

接下来执行 task.run(), 也就是配置文件中定义的推理任务 OpenICLInferTask.

  1. 评估阶段, 评估阶段和推理阶段的流程是类似的, 评估过程的配置如下:
# Evaluation configuration
eval = dict(
    partitioner=dict(
        type=NaivePartitioner, n=8
    ),
    runner=dict(
        type=LocalRunner,
        task=dict(
            type=OpenICLEvalTask)
    ),
)

NaivePartitioner 会生成多个评估任务 OpenICLEvalTask 并执行.

posted @ 2025-05-06 08:48  虾野百鹤  阅读(647)  评论(0)    收藏  举报