简介:C-EVAL是一个面向中文语境的多级多学科评估套件,专为自然语言处理与大型语言模型研究者设计,用于检验模型在中学、高中、大学和专业四个难度层级上的知识与推理能力。资源涵盖52个学科,从人文到科学工程,并包含专门衡量高级推理的C-EVAL HARD子集。实验显示,当时参与评测的模型中仅GPT-4平均准确率超过60%,说明现有大模型在中文高级任务上仍有明显提升空间。配套资料为1个PDF文件,共4.89MB,适合需要系统了解中文NLP评测体系、构建模型基准测试或开展跨学科能力对比的研究人员和技术从业者。已有76人学习下载。通过这份文档,读者可以掌握C-EVAL的题目构成、难度分层、评估协议以及作者对模型短板的分析思路,为后续开展中文大模型效果评测与改进提供参考文献和方法指引。
1. 为什么说C-EVAL是中文大模型绕不开的“考试卷”:从一道多选题说起
我见过不少模型在通用对话上表现惊艳,一到专业场景就露馅。C-EVAL就是专门用来戳破这种幻象的中文多级多学科评估套件,它把从初中到研究生阶段的52个学科知识压缩成近一万四千道选择题,用来测大模型在中国语境下的高级知识与推理能力。第一次跑完我有点意外:某个在开源榜单上排前面的模型,总分只有四十几,还不如一个更小的同行。但静下来想,这恰恰是C-EVAL的价值——它不是考“会不会聊天”,而是考“知不知道答案并且选得对”。
如果你正在做中文大模型的微调、对比选型或上线前的能力摸底,C-EVAL是绕不开的参照系。它不像开放式问答那样容易被“神回复”蒙混过关,也不像纯分类任务那样缺乏难度梯度。这套评测的底层逻辑、运行方式以及那些最容易让你分数翻车的小坑,就是本文想讲清楚的东西。
2. 先把C-EVAL拆明白:52个学科、四级难度与13948道选择题的构成逻辑
2.1 多级多学科到底怎么分级:从初中到研究生的知识坐标
C-EVAL把“多级”定义成了四个教育阶段:初中、高中、大学、研究生。每个学科下的题目按该阶段的知识要求标注难度,比如“高中数学”和“大学数学”不在一个量级上,研究生阶段的题目明显更考察抽象推理。这样做的好处是能顺着教育阶梯观察模型的认知能力分布,而不是只看一个笼统正确率。
学科覆盖上,C-EVAL一共有52个学科,既有数学、物理、化学、生物这类理科,也有历史、法律、文学、艺术这类文科,甚至还包括计算机科学、临床医学等专业领域。每个学科的题量并不平均,有些热门学科一百多道,有些冷门学科几十道。你在对比模型时,不能只看总分,要看模型在哪些学科拖了后腿。我习惯先把所有学科按“STEM”“人文社科”“其他专业”分组,再分别统计平均正确率,这样比一个孤零零的total acc信息量大得多。
2.2 为什么是选择题:评估推理能力的可复现性设计
C-EVAL全部采用四选一选择题,这是刻意为之的设计。开放生成式评测最大的问题就是解析困难,模型回答可能语义正确但表述不同,人工评测又不可大规模重复。选择题把“评估”简化为“选对选项”的概率问题,可以直接用模型的token概率来决定答案,无需采样生成,因此每个模型的得分都可在相同条件下复现。
C-EVAL的题目格式通常是题干加四个选项,选项以A、B、C、D开头。评测时,模型需要对每个候选答案拼接“题干 + 该选项”算概率,再比较四个概率大小,选概率最高的作为模型答案。这个“判别式”评估方式对模型很苛刻,因为如果你微调时只训练了生成式对话,没有专门训练“从四个选项中挑一个”的能力,得分大概率低于你的预期。这不是C-EVAL有问题,而是它测的就是这种结构化推理。
C-EVAL还额外提供了一个hard子集,专门筛选那些所有参与评测的模型都答错或者正确率极低的题目。这类题目属于“连AI都头疼”的样本,适合观察模型长尾能力。我通常把hard正确率当作区分模型上限的指标,它比总分更能暴露模型的推理天花板。
3. 用代码跑通C-EVAL:两种主流评测路径与最小命令
3.1 路径一:用eval-harness对你的模型做自动评测
如果你用的是Hugging Face格式的模型,最省事的办法是借助社区里的评估框架。常见做法是用支持C-EVAL的lm-evaluation-harness风格代码库,先安装依赖,再写一个最小评测命令。下面是我在本地环境实际用过的最小配置:
# 假设你已经安装了相关库,并下载了模型权重 python eval_harness.py \ --model hf-causal \ --model_args pretrained=your_model_path,dtype=float16 \ --tasks ceval-zh \ --num_fewshot 0 \ --batch_size 4 \ --output_path ./result_ceval.json这个命令的核心参数有几个:tasks指定评测任务为ceval-zh,num_fewshot设为0表示零样本,batch_size控制每次前向传播的样本数。output_path用来保存结果,方便后续分析。注意,不同版本的eval-harness对C-EVAL任务命名可能不一样,有的叫ceval_zh,有的按学科拆成多个子任务,你需要先查询任务列表确认。
另外要留心的是,框架在计算acc时会做选项概率归一化,但默认的归一化方式可能只比较四个选项对应的token概率。如果你的模型词表里有“A:”“A)”等不同变体,需要提前在代码里定义好选项模板。我一般会在跑评测前单独写一个极小的脚本,打印几条样本的预测选项目标格式,确认无误后再全量跑。
3.2 路径二:用官方脚本自己控制推理细节
如果你不想被框架约束,或者想深入看每个学科的细节,用官方原始的Python脚本是更稳的选择。官方脚本的思路很直接:读取json格式的题目文件,对每个题目构造出四个带前缀的候选文本,分别用模型计算perplexity或直接取最后一个token的概率,然后选最大者。
下面是一个简化版的最小复现思路:
import json import torch from transformers import AutoTokenizer, AutoModelForCausalLM # 加载模型 model_name = "your_model_path" tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True).half().cuda() choices = ["A", "B", "C", "D"] choice_prefixes = ["A.", "B.", "C.", "D."] with open("ceval_test.json", "r", encoding="utf-8") as f: items = json.load(f) for item in items: question = item["question"] options = item["options"] # 长度为4的列表 logprobs = [] for i, opt in enumerate(options): text = f"{question}\n{choice_prefixes[i]} {opt}" inputs = tokenizer(text, return_tensors="pt").to("cuda") with torch.no_grad(): outputs = model(**inputs) # 取最后一个token的logits last_logits = outputs.logits[0, -1, :] logprob = torch.log_softmax(last_logits, dim=-1) # 找到该选项文本末尾token的累积概率,简化处理只取首个token opt_tokens = tokenizer.encode(opt) score = 0 for tok in opt_tokens: score += logprob[tok].item() logprobs.append(score / len(opt_tokens)) pred = choices[int(torch.argmax(torch.tensor(logprobs)))] print(f"{item['id']}: {pred}")这段代码省略了归一化和边界处理,但逻辑能看出核心思路:把每个选项拼进题干,算整个序列的概率,概率最高的选项就是答案。logprobs里我用了平均token对数概率,可以平衡选项长度差异;实际评测更推荐使用每个选项精确的概率归一化,而不是简单对长度取平均。
跑这个脚本时要注意显存占用。如果模型较大,建议用batch_size=1逐条推理,或者用torch.cuda.amp做半精度。另一个坑是中文编码,要保证文件读取和终端输出都用UTF-8,否则题目里的中文变成乱码会直接拉低正确率。
4. 从“跑通”到“跑准”:C-EVAL的四个必调参数
4.1 上下文窗口:max_length不是越大越好
C-EVAL的题目长度差异很大,有些法律或医学题目题干很长,四个选项也都长,总序列可能超过2048个token。很多框架默认的max_length是2048,超出部分会被截断,导致模型看不到完整的条件,准确率自然下滑。但也不能盲目调大,因为生成式模型在计算概率时需要给整段文本做自回归,序列越长,每个位置的概率计算都依赖之前的全部内容,显存和时间成本都线性上升。
我的经验是统计评测集里题目token长度的95分位数,再按这个值设置max_length。例如,如果95%的题目都在1536以内,就把max_length设为2048留余量;如果发现有10%左右的题目超过2048,就提到3072。你可以先用tokenizer把所有题目编码一遍,统计长度序列,再决定参数。这是一个值得花的几分钟,能避免很多莫名其妙的低分。
4.2 few-shot的设置:0-shot还是5-shot,差一个身位
C-EVAL官方报告里给过不同few-shot设置下的分数。对大多数模型来说,0-shot是最朴素的测法,考察模型“看到题干就能答”的原始能力;5-shot则会在评测前插入5道示例,相当于给模型“考试前划重点”。你会发现,有些模型在5-shot下分数暴涨,另一些则不升反降,这通常和模型在预训练时看到过的题目模式有关。
如果你是想横向对比多个模型,就固定同一个few-shot数量。我一般会在主结果里跑5-shot,因为这是官方常用设置之一,能更好地反映模型“理解任务格式”后的水平;同时会另跑一个0-shot作为对照,看模型在没有样例时是否还能保持稳定。如果0-shot和5-shot的差距超过15个百分点,说明模型对任务格式非常敏感,这在部署时是个隐患。
4.3 答案归一化与选项前缀:别小看"A."和"A:"的区别
这是最容易翻车的地方。C-EVAL的每个选项在json里通常只存文本,比如“牛顿第一定律”。评测时需要把选项拼成“A. 牛顿第一定律”或“A:牛顿第一定律”。不同框架、不同版本,前缀模板可能不一样。如果你用的选项前缀和模型在预训练时见过的格式不一致,概率分布会受到干扰。
更细的问题出在“计算每个选项的概率”这一步。常见做法是:对每个候选文本,计算选项首token的概率,然后将四个首token概率归一化,作为四个选项的分数。但更严格的做法是计算整个选项文本每个token的条件概率之和。前一种效率高,但会丢信息,比如选项“人工智能”和“人工只能”首token可能相同,后者必须看完整token串才能区分。我一般会先用后一种方法跑一两个学科,对比和前一种方法的差距,如果差距稳定小于0.5个百分点,再切到快速模式。
4.4 随机种子与并行度:分数波动是怎么来的
理论上选择题评测是确定性的,但实际跑起来结果总会有微小波动,原因有三:一是不同batch size下padding的差异会影响注意力mask;二是某些模型用了dropout,评估模式下虽然应该关闭,但部分自定义模型没有真正禁用;三是多卡并行时数据划分的随机性。
要保证可复现,我通常会做三件事:固定torch.manual_seed(42),把model.eval()显式挂上,并用no_grad包住推理。如果使用多卡,设置CUDA_VISIBLE_DEVICES固定卡序,并且每次跑相同的batch size。即使这样,不同框架版本之间也可能有零点几分的波动,这是正常的。真正要注意的是波动是否超过1分——如果超过,你要排查代码里是否有随机采样逻辑。
5. C-EVAL避坑指南:5个翻车现场与对应解法
5.1 现象:模型一直选B,acc超过随机水平却不合理
某次我评测一个没做指令微调的底座模型,总分接近30%,而随机基线是25%。看起来不算差,但按学科展开后发现,这个模型在几乎所有题目上无脑选B,因为训练数据里选项B出现的频率稍高。它根本不是在“答题”,而是在猜先验分布。
原因与解决:模型没有真正理解“给出题干选答案”的任务格式,它对四个选项的位置有偏好。解决方法是检查每道题的预测答案分布,如果发现有选项占比超过40%,就说明模型没在推理。你可以在评测脚本里加入对选项分布的统计,一旦发现某选项占比异常,就要考虑给模型加few-shot示例,让它理解选项只是内容载体,而不是位置信号。这个检查应该作为每次评测后的第二件事,第一件事是看总分。
5.2 现象:hard子集分数暴跌,怀疑模型“背题”
C-EVAL的hard子集是专门筛选出来的高难度题目,很多模型的总分有50,hard的正确率只剩20多。这不是bug,而是数据集的正常设计。但如果你发现模型的hard分数接近0,甚至低于5%,则需要检查是否在评测hard子集时样本量太小。
原因与解决:hard子集的题量通常只有几百道,一道题的涨跌就会影响1到2个百分点。不要拿hard分数做模型间的细粒度对比,它的置信区间比总分宽得多。合理的用法是看相对趋势,比如模型A的总分比模型B低3分,但hard分数反而高7分,这说明A可能拥有更强的长尾推理能力,只是被中等难度的题目拉低。我在写模型对比报告时,会同时标注置信区间,避免被人盯着一分两分的差异过度解读。
5.3 现象:本地跑完的分数与官方报告对不上
这是最让人血压飙升的场景。同一个模型,本地跑出来的分数比官方低好几个点。我排查过很多次,最后发现都是细节差异导致的。
原因与解决:主要差异来源有三个。第一,官方报告里可能用的是某个特定tokenizer版本,而你本地换了新版本,词表变了。第二,官方可能用了不同的选项归一化策略,比如对token概率求和而不是求平均。第三,官方评测时用的是完整题目含所有选项的拼接方式,而你可能把题干和选项拆开分别输入。解决方法是尽量复用官方评测脚本,或先用官方发布的已知模型预测结果做一种“冒烟测试”,把你的输出和官方提供的样例输出对比,逐题定位差异。
5.4 现象:显存不够,无法直接跑完整评测
如果你的模型是13B甚至更大,跑满52个学科、近一万四千道题,对显存是很大的考验。我曾用一个13B模型全量跑,batch_size设为1,实际用了约20GB显存,一个流程跑了几小时。
原因与解决:显存占用主要来自模型权重和中间激活值。解决思路有几条:一是用半精度加载,能砍一半显存;二是用torch.utils.checkpoint检查点技术,用一点计算换显存;三是把评测拆开按学科跑,比如每次只评一个学科,最后合并结果,这样单次峰值显存可以降低。如果还不行,就只跑几个代表学科,比如数学、历史、法律、计算机,或者用hard子集做快速验证。这些方法的代价是分数不完整,但能让你在有限资源下先判断模型的大致水平。
5.5 现象:中文编码乱码导致分数异常
这个坑比较低级,但很致命。如果你在Windows下用默认编码读取json,或是在脚本里用了print输出中文,终端可能显示乱码,但这不影响计算。真正影响计算的是在tokenize之前,如果文本就变成了乱码字节,那么模型看到的完全是错误输入。
原因与解决:评测脚本必须百分百保持UTF-8编码。我在每个脚本开头都会写:
import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')另外,Hugging Face的tokenizer在编码中文时是逐字拆分的,乱码会让每个汉字变成一个或多个�,模型完全无法理解。排查方法是随机抽5道题,把tokenize后的id序列重新解码打印出来,看看是否和原文字符一致,如果已经是乱码就说明读取环节出了问题。
6. 把C-EVAL当体检报告用:从总分到单学科的进阶分析技巧
6.1 按学科聚类看短板:别只盯着平均分
总分的参考价值有限,我用C-EVAL的习惯是先看“学科分差”。把52个学科的正确率画成柱状图,你会立刻发现模型的偏科:有的模型理科强文科弱,有的恰恰相反。对实际部署来说,如果你的业务是客服,法律学科更重要;如果是教育辅导,数学和物理的权重更高。按学科聚类还能发现数据污染信号:如果某个冷门学科的正确率异常高,而其他学科都很低,那这个模型很可能在预训练时见过相关考试题,俗称“开卷考”。判断数据污染不能只看总分,必须结合学科分布。
6.2 用C-EVAL校验模型训练:一次测评的三种用法
C-EVAL不只在模型上线前用一次,我通常会把它嵌入训练循环里。第一种用法是做SFT前后的回归测试,微调后必须保证基础学科能力不下降;第二种用法是作为早停指标的一部分——如果训练损失在降但C-EVAL总分开倒车,说明模型开始过拟合某个任务格式;第三种用法是做多版本对比,比如不同基座模型、不同数据配比,用统一的评测配置来选型。
一个值得推荐的具体技巧是:把C-EVAL的题目按答案选项位置进行重排评测。官方题目里正确答案在A、B、C、D的位置分布不一定均匀,如果你怀疑模型有位置偏好,可以写一个小脚本,把四个选项的位置随机打乱后再测。如果打乱后分数明显下降,说明模型没在理解内容,而是依赖位置记忆。这个测试花不了多少时间,但对判断模型真实推理能力很有价值。
记得我第一次拿C-EVAL测模型时,看到总分不错就准备写报告,顺手点开“法律”学科发现只有30%,才意识到被平均分骗了。后来我养成了习惯:每个模型都要输出完整学科明细,并且至少做一次hard子集和选项重排测试。这些验证不是浪费时间,它们能让你在向团队汇报时少一些“玄学”争议。评测本身就是一门手艺,希望这篇文章能帮你少走弯路,也希望你能从C-EVAL里读出比分数更多的东西。
本文还有配套的精品资源,点击获取