查看原文
其他

详解大模型评测工作流,以OpenCompass为例

OpenMMLab 2024-04-23

The following article is from NLP工作站 Author 邱震宇

写在前面

大家好,我是刘聪NLP。

目前很多机构都投身于基座模型训练、chat模型训练领域,要在这个领域中做到比别人更好的效果,需要相当多的财力、物力。所幸整个大模型领域中,除了微调训练外,还是有很多亟待解决的问题,例如大模型的应用研发、大模型的评测等。

今天给大家带来一篇来自邱震宇大佬(知乎@邱震宇)的大模型的评测分析。

知乎:https://zhuanlan.zhihu.com/p/652688939

有关大模型的评测,想必大家经常会看到相关新闻。因为每次有一个开源或者闭源的大模型发布出来,必然会发布自己在某某评测上的效果。或者有一些机构会专门做一些大模型的榜单,从多个不同维度来对大模型进行打分排名。目前大部分的企业机构受限于成本、数据隐私合规等因素,不会使用GPT4的服务,也无法自己训一个强力的基座模型,只能依赖于开源模型。然而当前模型的可选择面太多了,究竟选择哪个成了非常头疼的问题。通常来说,我们选择前会参考各大评测榜单的结果,因此了解市面上的大模型评测模式是很有必要的。另外我们也希望能够搭建一套自己的评测工具,目前市面上开源出来的评测工具似乎只有一家——OpenCompass。因此,本文先简要介绍大模型评测的模式,随后将详细介绍对OpenCompass的分析和一些看法,希望能给各位从事大模型方向的同僚提供一些参考。

大模型评测的模式

首先,目前常见的大模型评测模式可以大致总结为以下三种:

1、做题打分。主要是收集各种各样的评测数据集,然后把数据集分为不同的维度能力。通过设计一些prompt让大模型去做这些数据集的任务,与标准答案进行对照计算分数。典型的如OpenCompass,huggingface的openLLM leaderboard等。

2、让GPT-4做裁判。还是会收集评测用的数据集(一些不是公开开源的、不带标准答案的数据集也会包含在内),然后让GPT-4给大模型的生成结果进行评判。评判这块又有很多工作衍生出来,例如有直接让打分的,也有设计一些维度(例如事实性、准确性、安全合规性等),然后更细粒度地进行评测的。

3、竞技场模式。类似于竞技游戏里面的竞技场。每次拉两个大模型选手PK,由用户(有时候也会用GPT-4)来评测哪个模型更好,赢的大模型有加分,输的大模型有减分。当执行了足够多的PK轮次后,就会有一个大模型的得分排行榜,这个榜单相对来说还是比较公正的,能够较为客观得体现模型的能力强弱。典型的例子如UC伯克利发布的Chatbot Arena Leaderboard。

当然,除去上述模式外,还有很多针对单项能力的评测,例如针对数学能力、针对代码能力、针对推理能力等,评测这些能力一个是可以判断一个大模型是否真的具备类似人类得思考能力,另一方面其评测结果也是能够直接帮助在特定领域场合中选择大模型(例如代码助手)。

OpenCompass的内部机制浅析

今天我主要介绍的评测工具OpenCompass属于上一章节中提到的第一种模式,他是书生大模型(InternLM)的研发团队开源出来的大模型评测工具,也是当前市面上极少有的代码级开源评测项目。它包含了数据集处理、模型加载与生成、结果的评测与指标计算等流程,构成了一个完整的评测生命周期:另外,它最大的一个特点就是囊括了非常多的数据集以及对应数据集的预处理加工脚本。熟悉LLM的朋友应该清楚,传统的NLP数据集在输入大模型之前,要经过数据预处理加工,转换成instruction+input+output的文本生成任务格式。不同数据集的原始格式不尽相同,不同类型的任务如信息抽取与文本分类、语义理解与文本生成之间的区别还是比较大的,即使是同类型任务的不同数据集格式也有很大差异。因此要开发不同的预处理脚本,工作量非常大。而OpenCompass已经预置了相当多的国内外数据集处理脚本,即使不用OpenCompass,我们也可以参考其中的处理方式,快速移植到我们自己的项目中,以下是CMMLU数据集的加载和预处理示例(可以自定义自己的prompt模板):

下面我将从项目的运行机制、组成架构、本地化部署注意事项等方面详细分析OpenCompass。

运行机制

首先,我将从宏观整体的维度来说明OpenCompass的评测工作流程。下图为一个简易的流程图:

整个项目首先基于的是MMEngine框架,在此框架下,Partitioner模块会接收数据集与参数配置模块,并根据模型生成、结果评测等类型将整个评测任务拆分多个最小粒度的task元素。参数配置模块包括数据集的配置和模型的配置。数据集配置包括数据集的加载、预处理等配置,模型的配置包括模型的加载、预测生成等配置。最后我们有一个Runner模块负责管理和有序执行所有的task,完成最终的评测任务。下面我会详细的分析各个模块的原理。

项目结构

OpenCompass的项目结构还是比较清晰的,主要由以下核心模块组成:

  • (1)MMEngine

作为组成OpenCompass的基本框架,MMEngine是由OpenMMLab发布出来的一个基于pytorch的深度学习基础库,这个框架还是比较复杂的,封装得比较深,要了解其原理的话建议是参考官方文档:OpenMMLab:从 MMCV 到 MMEngine,架构升级,体验升级!由于OpenCompass并没有涉及到模型训练等复杂功能,因此它只是借助于MMEngine的部分特性构建了一个评测平台,用于管理数据、模型和评测任务。下面讲到的模块都用到了其中的基础功能。

OpenMMLab:从 MMCV 到 MMEngine,架构升级,体验升级!
https://zhuanlan.zhihu.com/p/571830155
  • (2)配置模块

OpenCompass中主要包含数据集配置与模型配置,可见代码项目中的configs/。

数据集配置(见configs/datasets/,以及opencompass/datasets/)主要包含了以下内容:

a.数据集加载方式。不同数据集的文件格式、字段名称都不同,因此需要针对性配置对应的文件读取方式。这个配置主要见opencompass/datasets/下。

上图代码截图中可以看到有个register_module(),这个就是MMEngine的一个基础特性。MMEngine 实现的注册器可以看作一个映射表和模块构建方法(build function)的组合。映射表维护了一个字符串到类或者函数的映射,使得用户可以借助字符串查找到相应的类或函数。上图中是已经创建了一个注册器LOAD_DATASET,通过注册将cmmlu的加载模块加入到注册器中。这样,后面,我们只需要通过类似

LOAD_DATASET.build(dict(type=“CMMLUDataset”))

的方式就能以配置的形式调用对应的方法。在OpenCompass中,调用的脚本可见:opencompass/utils/build.py中的build_dataset_from_cfg:说到注册器,OpenCompass中除了数据集的注册器外,还预置了其他核心模块的注册器,如模型、任务等,可见opencompass/registry.py。其中的注册器代表了项目中所需要用到的核心要素,下面会逐一讲解。

b.数据预处理。这里的预处理主要是将数据集原始格式转化为大模型能够支持的输入格式,即构造instruction+input+output的prompt数据,配置数据字段与prompt模版中需要填充的槽位对应关系

c.数据集的评测配置。OpenCompass支持多种不同的评测方式,均在opencompass/openicl/icl_evaluator路径下,例如sklearn中的roc评测、exact match评测,还有依赖huggingface的evaluate框架评测等,我们只需要在数据集配置文件中编写eval_cfg就可以配置上去了。eval_cfg中除了指标计算方式之外,还需要配置postprocessor,作用是负责对大模型生成的内容进行解析,将目标答案从里面抽取出来,便于做评测。(例如根据“答案:(\S)”的正则表达式,抽取答案选项)

模型配置(见configs/models/,以及opencompass/models/)主要包含了以下内容:

a.模型基本参数配置。在configs/models/中预置了不同大模型的基本参数配置,例如模型路径等加载参数。

b.模型的生成等与评测过程相关的方法定义。主要见opencompass/models/中的脚本,下图为HuggingFaceCausalLM的类定义。同样的,他也通过注册器将模型动作进行注册,方便后续进行配置型调用。

  • (3)retriever和inference模块

OpenCompass除了MMEngine以外,还依赖了上海AI Lab开发的另一个开源项目openICL,主要使用了其中的icl_inference和icl_retriever模块。这两个模块主要用于大模型从数据集中选择N个固定数量的样本加入到prompt中,然后通过In-context-learning的方式来"做题"。

a. retriever模块预置了多种不同的样本选择方式。其实跟目前大模型的典型应用——基于知识库的问答应用差不多,主要是通过一些方法选择最适合当前数据样本的上下文召回,帮助大模型更好得理解当前数据样本。典型的方法有:基于BM25的检索方式、基于KNN的检索方式、随机采样方式等,当然也有zeroshot方式,即不带任何先验样本。

(非常重要) b.Inference模块预置了两种主要的生成方式ppl和gen,分别对应当前大模型的两种基本形态base和chat。下面简要说明上面两种方式的原理和区别。

ppl:主要针对的是base版模型,即只经过无监督语料的预训练得到的模型,例如llama、baichuan-base等。这些模型他们没有经过指令微调和RLHF的对齐,因此并不擅长遵从人类指令来完成任务,他们更擅长的是进行文本的续写生成。因此,如果使用诸如"请回答以下问题:XXXX"的prompt,他们大概率是会续写问题或者与问题相关的内容,而不是给出问题的答案。那么这种模型如何在一些做题型数据集上进行评测呢?OopenCompass给出了这样的方案(这个方案不一定合理,但也有一定的说服力,各位可以自行斟酌):

对于选择题来说,假设由ABCD四个选项。对于一个样本,构造四个prompt,分别由问题+"答案:"+[ABCD]中的一个组成。然后将这四个prompt分别输入到大模型中,调用大模型的model()方法生成logits。

PS注意,这里不是用model.generate()方法,而是用model()

然后基于logits计算该prompt对应的ppl分数。实际上,ppl分数可以借由计算交叉熵损失而得到,两者的关系如下:

具体可以参考这篇博文:https://thegradient.pub/understanding-evaluation-metrics-for-language-models/

而huggingface的官方doc给出了大模型计算ppl的demo,参考:Perplexity of fixed-length models

Perplexity of fixed-length models:
https://huggingface.co/docs/transformers/perplexity

其中有个细节需要注意,就是官方doc中计算ppl遵循了滑窗的策略,即设置一个窗口,按照某个步长向右滑动,每次在窗口内计算ppl,直到窗口滑到结尾,然后将所有的ppl进行求平均得到最终的ppl:

然而在OpenCompass中,ppl的计算并没有使用滑窗策略,而是直接在整个input的范围上计算一个ppl的值作为最终结果。我也在github的discussion上进行了讨论,原因也比较好理解,就是我们这个评测任务不是去评测他的生成怎么样,而是评测他的答案有没有回答对,因此在生成完整的内容上计算ppl就可以了。

经过上述计算,我们相当于为ABCD每个选项都计算得到了一个ppl分数,最后我们需要选出最终的答案,只需要选择ppl最小的那个就可以了,参考下面的样例:

以下是关于法律的单项选择题,请直接给出正确答案的选项。\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

可以看到C选项对应的ppl最小,说明大模型认为选C时,对应的生成文本的不确定性最小。

gen:gen模式对应的就是chat版的大模型,即经过指令微调、RLHF等方式对齐过的大模型,如qwen-chat,internlm-chat,baichuan-chat,llama2-chat等。这些大模型能够理解并遵循人类的指令,完成人类指定的任务。因此我们只需要构建指令prompt,让他直接去生成答案,然后我们通过后处理方法将答案抽取出来就可以了,prompt样例如下:

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

关于区分ppl与gen两种生成模式,其实还有一个考量,就是评测任务的执行时间。对于base版本的模型来说,由于他是去做文本续写,所以除非生成内容长度达到预设的最大值,他有很大概率是不会很早就停止生成,因此如果用gen的方式评测,那么他跑一个样本的时间会非常长(亲测internlm的base版本,max_seq设为2048,跑一个样本需要分钟级),因为generate方法是要用一些解码方法来生成,无法并行。而直接用model()生成logits则很快,接近百毫秒级。

  • (4)Task和partitioner模块

在OpenCompass中,将整个模型评测的工作流进行了拆分,以Task来定义一个最小粒度的工作节点。当然,Task也采用了注册器模式,将不同类型的task实现模块进行注册。目前项目中的Task类型包括openicl_eval,openicl_infer,llm_eval。前面两个之前已经讲过,基于openICL框架来实现对应的功能,最后一个则是自定义的对llm的评测方式,需要实现evaluate方法。详细代码可见opencompass/tasks/

那么谁来根据这个task定义,将整个评测工作流进行拆分呢?这就轮到partitioner出场了。它的主要作用就是对评测数据集进行拆分,并封装成一个一个的task对象。至于这些task对象如何有序的执行并串联起来呢?那么就需要我们最后一个模块runner登场了。

  • (5)Runner模块

Runner的主要作用就是串联全局,将所有的task安排的井然有序,直到完成最终的评测。OpenCompass预置了三种Runner模式,有基于阿里云集群的模式、基于Slurm的并行模式、基于本地python进程的local模式。我个人主要是用local模式比较多。local模式中也可以采用多进程模式,充分利用当前服务器上的多卡资源。对于在占用的卡,会采用lock操作将资源锁住,防止其他进程调用满功率使用的GPU时由于显存不够报OOM的情况发生。

OpenCompass的核心模块已经分析完了,其实还有一些很小的功能特性没有在本文中列出,例如collection功能,主要作用类似于数据集组的概念,可以将感兴趣的数据集用collection组成一个对象,方便进行数据集的管理;另外还有一个summarize的功能,主要作用类似于对评测后的结果按照一些维度进行总结,效果的话与OpenCompass官网上的几个评测维度是类似的。这些功能大家如果感兴趣可以自行查看源码,并不是非常复杂。

本地化部署注意事项

虽然OpenCompass的项目架构相对比较成熟,应用了很多成熟的框架,但是我在本地化部署的时候还是趟了一些坑。因此,下面我会列出在本地化部署时的注意事项,其他小伙伴可以作为参考。

1、存在不同的数据集类型,有的是简单的json格式的数据集文件,有的是HFDataset格式的文件。这两种类型的文件的文件路径它在dataset的cfg中是放在不同的字段的,一个是用”data_files“,一个使用”path“,因此需要针对性处理;另外,文件路径由于用的相对路径"./data",可能会有文件路径找不到的问题,我这边将其改成"opencompass/data"就可以了。具体修改方法为在opencompass/utils/build.py中,对build_dataset_from_cfg的方法进行修改:

# 以下为需要新增修改的部分
    if "data_files" in dataset_cfg:
        cur_data_path = dataset_cfg["data_files"]
        print(cur_data_path)
        cur_data_path = cur_data_path.replace("./data","opencompass/data")
        # cur_data_path = cur_data_path.replace("/dev.json","")
        print(cur_data_path)
        dataset_cfg["data_files"] = cur_data_path
        print("------------"+dataset_cfg['data_files'])
    else:
        cur_data_path = dataset_cfg["path"]
        print(cur_data_path)
        cur_data_path = cur_data_path.replace("./data","./opencompass/data")
        # cur_data_path = cur_data_path.replace("/dev.json","")
        print(cur_data_path)
        dataset_cfg["path"] = cur_data_path
        print("------------"+dataset_cfg['path'])

2、如果是在内网服务器进行部署,不能联网的条件下,需要手动从huggingface中下载evaluate的所有脚本,并将所有脚本复制到项目根路径下。

然后,在项目根路径/opencompass/openicl/icl_evaluator/icl_hf_evaluator.py, 将所有涉及到需要从hf下载的评测引用路径变为实际的路径,例如:

class RougeEvaluator(HuggingfaceEvaluator):
"""Rouge evaluator."""

def __init__(self) -> None:
super().__init__(metric='./metrics/rouge'

3、debug模式下,原始的opencompass会对每个task都重新加载模型权重(即使当前只有一种模型),为了加快评测速度,不重复加载,需要对opencompass/runners/local.py 的launch函数进行修改,具体修改方法为如果是infer类型任务,如果当前模型已经加载过了,则不重复加载。

if self.debug:
            for task in tasks:
                print("+++++++++++++++++")
                print(self.task_cfg)
                task = TASKS.build(dict(type=self.task_cfg.type, cfg=task))
                task_name = task.name
                if "infer" in self.task_cfg.type.lower():
                    # 若已经有model实例,则不重复加载
                    task.run(self.cur_model)
                    self.cur_model = task.model
                else:
                    task.run()

还需要修改对应的opencompass/tasks/openicl_infer.py OpenICLInferTask的run方法:

def run(self,cur_model=None):
        self.logger.info(f'Task {task_abbr_from_cfg(self.cfg)}')
        # print("--------------")
        # print(self.model_cfgs)
        # print(self.dataset_cfgs)
        for model_cfg, dataset_cfgs in zip(self.model_cfgs, self.dataset_cfgs):
            self.max_out_len = model_cfg.get('max_out_len', None)
            self.batch_size = model_cfg.get('batch_size', None)
            # 有模型实例则不重复加载
            if cur_model:
                self.model = cur_model
            else:
                self.model = build_model_from_cfg(model_cfg)
                cur_model = self.model

非debug模式下,我还没找到比较好的修改的方法,因为他是通过get_cmd调用python进程的方法来执行。如果有知道修改方法的伙伴可以在评论区发出来讨论讨论

4、最后一个注意事项与上一章节提到的ppl和gen两种模式有关,在项目预置的internlm的demo中,引入了很多数据集的配置,按理说对于internlm的评测应该都是基于ppl的,但是预置的demo并不是这样做的,导致一开始我的评测任务显示要40-50个小时才能做完。因此如果是对base版本模型评测,建议把collections/base_medium.py中的所有配置都改为ppl,或者自己新建一个collection,把需要评测的数据集配置上ppl模式。

小结——大模型评测的问题现状

至此,通过上述的分析,相信大家对OpenCompass这个项目应该有一定的了解了,虽然他是一个开源的项目,但整体的完成度还是比较高的。然而,细看其核心的模型评测方法,仍然有一些亟待讨论的问题,而这些问题也是目前整个业界对大模型评测任务缺少统一标准的原因,个人认为可以总结为如下几点:

1、不同评测榜单使用的数据不同。目前仅是国内的几家榜单,如SuperClue, Opencompass,智源的Flageval等等使用的数据集虽然有部分重合(如c-eval,cmmlu,mmlu等),但也有很多不同的数据集。各家还都给出了分数排名,这样的分数就很容易给用户开发者造成一定的困扰。

2、即使是相同的数据集,不同的评测榜单由于评测方式的不同(例如prompt写的不一样,大模型的生成方式不同等)也会得到不一样的分数,甚至有的分数差别还特别大。例如OpenCompass中对于Qwen-chat的理解评测和智源的Flageval对于Qwen-chat的中文理解评测就差的很多。

3、有些评测分数虚高。为什么这么说呢?因为很多评测题目都是应试题目类型,例如单项选择题、多项选择题等。大模型在这些题目上做的好,不一定就代表他们理解了这些题目背后的知识,有可能是在预训练阶段已经包含了相关的语料,也有可能是仅仅记住ABCD选项的偏序关系,又或者是类似我们学生时代耳熟能详的”三短一长选一长“等应试型技巧。

4、基于竞技场的评测方式虽然相对来说比较公平,但是还是极度依赖人工评测。人的判断是具有主观性的,因此不可避免得也会对评测结果带来主观上的偏移。

我个人多上述部分问题有过一些思考,例如对于问题3,我想过评测的时候,建议加个任务,就是让大模型基座模型去续写评测题,看看跟评测题是否相似或者完全还原,如果是,则对于它的评测分数需要做一些降权,尤其是对那些提供选项的题目。但是后来我又转念一想这个问题是不是也说明了大模型真的注入了知识?只不过还不能说明他具备与人类类似的思考能力?总之,我打算后续有时间的会尝试下这种做法,看看会有什么效果。

对于其他的问题目前我还没有比较好的想法,如果各位伙伴有相关的idea,欢迎来找我讨论。大模型的评测对整个大模型领域来说是一个非常重要的工作,无论是在学术界评测大模型的好坏,还是在工业界应用大模型时对业务方的”交代与解释“,都少不了一个合理且让人信服的评测方式,同时好的大模型评测也能诊断出大模型的缺陷,从而推动大模型技术的发展,希望有越来越多的人重视这块工作。

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存