简介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 pretrainedyour_model_path,dtypefloat16 \ --tasks ceval-zh \ --num_fewshot 0 \ --batch_size 4 \ --output_path ./result_ceval.json这个命令的核心参数有几个tasks指定评测任务为ceval-zhnum_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_codeTrue) model AutoModelForCausalLM.from_pretrained(model_name, trust_remote_codeTrue).half().cuda() choices [A, B, C, D] choice_prefixes [A., B., C., D.] with open(ceval_test.json, r, encodingutf-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_tensorspt).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_size1逐条推理或者用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 现象模型一直选Bacc超过随机水平却不合理某次我评测一个没做指令微调的底座模型总分接近30%而随机基线是25%。看起来不算差但按学科展开后发现这个模型在几乎所有题目上无脑选B因为训练数据里选项B出现的频率稍高。它根本不是在“答题”而是在猜先验分布。原因与解决模型没有真正理解“给出题干选答案”的任务格式它对四个选项的位置有偏好。解决方法是检查每道题的预测答案分布如果发现有选项占比超过40%就说明模型没在推理。你可以在评测脚本里加入对选项分布的统计一旦发现某选项占比异常就要考虑给模型加few-shot示例让它理解选项只是内容载体而不是位置信号。这个检查应该作为每次评测后的第二件事第一件事是看总分。5.2 现象hard子集分数暴跌怀疑模型“背题”C-EVAL的hard子集是专门筛选出来的高难度题目很多模型的总分有50hard的正确率只剩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, encodingutf-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里读出比分数更多的东西。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?