C-EVAL中文大模型评测实战:从数据构成到可复现分数
简介:C-EVAL 是面向中文语境下基础模型评测的多层级、跨学科评估套件,适合自然语言处理与大模型评估方向的研究人员及技术从业者使用,用于系统分析各类大语言模型在中文环境中的知识储备与推理能力短板。套件按中学、高中、大学、专业四个难度层级组织,覆盖人文到理工共 52 个学科,并附有高难子集 C-EVAL HARD 专门考察高级推理;实验显示仅 GPT-4 平均准确率超过 60%,说明现有模型仍有较大提升空间。资源包共 1 个 PDF 文件,约 4.89MB,完整呈现该评测套件的设计思路、题目构成与多模型对比结果,并说明数据集与评估代码的开源发布方式及在线排行榜机制。已有 76 人学习,读者可据此了解中文评测基准的构建逻辑、各模型表现差异,以及后续在安全性与偏差等维度继续探索的方向。
1. 中文大模型评测绕不开的那套卷子:C-EVAL 到底测什么
手里有个微调完的中文模型,想证明它“确实变强了”,最尴尬的不是没数据,而是拿什么尺子量。拿英文 MMLU 直接机翻成中文去测,分数看着漂亮,一问中国法制史、注册消防工程师的题就露馅——这不是模型不行,是尺子本身偏了。C-EVAL 就是冲着这个痛点来的:13948 道四选一中文题,横跨中学、高中、大学、专业四个难度层级,覆盖 52 个学科,从初中物理到临床医学、从马克思主义基本原理到离散数学,全部按中国语境出题。它要回答的问题很直接:这个基础模型在中国人关心的知识体系上,到底站在哪一档。适合谁?做中文 LLM 预训练、指令微调、领域适配的团队,以及需要给模型能力做横向对比的评测岗。下面按“这套卷子怎么构成 → 怎么跑起来 → 分数怎么读 → 坑在哪”一路拆。
2. 拆开这套卷子:52 个学科、四个难度层与 C-EVAL HARD 的设计逻辑
2.1 为什么是四选一,而不是开放生成
C-EVAL 全量题目统一为四选一单选,只有一个正确答案。这个选择不是偷懒,是评测工程上的取舍。开放生成的评分要么靠人(贵、慢、不可复现),要么靠另一个模型当裁判(引入裁判偏差),而四选一只有一个指标——准确率,谁跑都是同一个数,横向可比。更关键的是,四选一被验证为高级能力的“代理指标”:一个模型如果连大学化学的选项都选不对,很难相信它在真实下游任务里能推理对。题目来源刻意避开了高考真题、国家级职业资格考试原题这类网上到处流传的题,改用模拟卷、地方性小规模考试、部分高校期末题,就是为了降低预训练数据污染的概率——这一点在做基准时经常被忽略,但直接决定分数可不可信。
2.2 难度分层与学科分布怎么读
四个难度层的题量分布并不均匀,这决定了你读分数时不能只看总平均。按公开统计,中学约 1409 题、高中约 1594 题、大学约 6249 题、专业约 4696 题,大学层占比最大。学科按主题聚成四类:STEM 20 科 4495 题、人文 11 科 2676 题、社科 10 科 2845 题、其他 11 科 3932 题。这个结构意味着:一个模型总分高,很可能是大学层理工题撑起来的;要判断它的人文社科底子,得单独拉子集看。C-EVAL HARD 是从中挑出的高难学科子集(高等数学、大学物理一类),专门卡推理能力,GPT-4 在这个子集上也只有 53.3% 左右,是少数能让顶级模型“掉下神坛”的中文基准。
2.3 数据划分:dev / val / test 各管什么
每个学科内部按比例切成三份:dev 集每科 5 条带解析的示例,专门给 few-shot 和 few-shot CoT 用;val 集与 test 集约 1:9,val 用来调超参,test 是最终报分的那份。全量约 260 条 dev、1346 条 val、12342 条 test。这里有个容易踩的点:dev 的 5 条示例是带人工校验解析的,它的作用是“告诉模型答题格式和推理风格”,不是让你拿去训练。把 dev 混进训练集再报 test 分数,等于开卷考试还提前看了答案,分数没有意义。常见做法是固定 dev 示例、只改 prompt 模板和 few-shot 数量,把变量控制住。
3. 把评测跑起来:从拉代码到出分的完整链路
3.1 环境与数据准备
C-EVAL 的评测代码和数据在公开仓库里,典型结构是数据按学科分目录存放,每条样本含题干、四个选项、答案、解析字段。先确认 Python 环境和依赖,再核对数据完整性——52 个学科目录一个不能少,缺一个学科总分就会偏。
# 建独立环境,避免和训练环境串包 conda create -n ceval python=3.10 -y conda activate ceval pip install torch transformers datasets pandas tqdm # 拉取评测代码与数据(仓库地址以公开页面为准) git clone <ceval-repo-url> ceval && cd ceval # 核对学科目录数量,应为 52 ls data/ | wc -l这段的重点不在命令本身,而在最后那行校验:学科目录数对不上,后面所有分数都不可信。依赖里transformers版本要和你的模型权重匹配,老版本加载新架构会直接报 key 不匹配,别急着怀疑数据。
3.2 构造 prompt:answer-only 与 chain-of-thought 两条路
C-EVAL 支持两种答题模式。answer-only 是直接让模型输出选项字母,快、省 token,适合大批量粗筛;chain-of-thought 是先让模型写推理再给答案,分数通常更高但成本翻几倍。prompt 模板的核心是把题干、四个选项、dev 示例按固定格式拼好,并明确要求“最后一行只输出答案字母”,否则解析器抓不到结果。
# 简化示意:answer-only 的 prompt 拼装 def build_prompt(question, options, dev_examples, cot=False): head = "以下是中国考试单选题,请选出唯一正确答案。\n\n" # dev 示例:固定 5 条,保证 few-shot 一致性 for ex in dev_examples: head += f"题目:{ex['q']}\nA. {ex['A']}\nB. {ex['B']}\nC. {ex['C']}\nD. {ex['D']}\n答案:{ex['ans']}\n\n" body = f"题目:{question}\nA. {options['A']}\nB. {options['B']}\nC. {options['C']}\nD. {options['D']}\n" if cot: body += "请逐步推理,最后一行输出「答案:X」。" else: body += "只输出答案字母。" return head + body参数上要盯三处:dev 示例数量(固定 5 条,别随手加减,否则和公开分数不可比)、max_new_tokens(CoT 模式要给够,比如 512,给少了推理被截断,答案行根本没生成)、温度(评测一律贪心解码,temperature=0,任何采样都会让分数抖动且不可复现)。解析环节用正则抓最后出现的“答案:X”或单独字母,抓不到就记为解析失败,单独统计——解析失败率高说明 prompt 格式或生成长度有问题,不是模型不会。
3.3 批量推理与结果落盘
全量 test 一万两千多题,逐条跑不现实,按学科分批、按 batch 推理,每批结果立刻写盘,中途崩了不用从头来。下面是一个最小可用的批处理骨架:
import json, re from tqdm import tqdm def run_eval(model, tokenizer, samples, batch_size=8, cot=False): results, parse_fail = [], 0 for i in tqdm(range(0, len(samples), batch_size)): batch = samples[i:i+batch_size] prompts = [build_prompt(s["q"], s["opts"], DEV_EXAMPLES, cot) for s in batch] inputs = tokenizer(prompts, return_tensors="pt", padding=True).to(model.device) outs = model.generate(**inputs, max_new_tokens=512 if cot else 8, do_sample=False) # 贪心,保证可复现 for s, out in zip(batch, outs): text = tokenizer.decode(out[inputs["input_ids"].shape[1]:], skip_special_tokens=True) m = re.findall(r"答案[::]\s*([ABCD])", text) or re.findall(r"\b([ABCD])\b", text) if not m: parse_fail += 1 pred = None else: pred = m[-1] # 取最后一次出现,防止推理中途提到别的选项 results.append({"subject": s["subject"], "pred": pred, "gold": s["answer"]}) json.dump(results, open("preds.json", "w"), ensure_ascii=False) return results, parse_fail逻辑说明:do_sample=False是硬约束,评测不是创作;取正则匹配的最后一个结果,是因为 CoT 输出里模型常在推理中复述错误选项,取第一个会误判;parse_fail必须单独计数,它和答错是两回事。参数上batch_size按显存调,长 CoT 样本显存吃紧就降到 2~4,别为了快把 OOM 当常态。
3.4 按学科聚合分数
出分不是算一个总平均就完事。正确做法是先按学科算准确率,再按难度层、按四大主题类分别聚合,最后才给总平均。这样你才能看出模型是“理工强人文弱”还是“低难度稳高难度崩”。C-EVAL HARD 子集单独拉出来算一遍,这个数字往往比总分更能说明推理能力的天花板。
| 聚合维度 | 用途 | 注意点 |
|---|---|---|
| 单学科准确率 | 定位具体短板 | 样本少的学科波动大,别过度解读 |
| 难度层(中/高/大/专) | 看知识深度曲线 | 大学层占比大,会主导总分 |
| 四大主题类 | 看能力结构 | STEM 与人文分开报更有信息量 |
| C-EVAL HARD | 看推理上限 | 接近随机(25%)说明推理没起来 |
4. 分数背后的坑:数据污染、解析失败与不可比的横评
4.1 现象:某模型总分异常高,单科却离谱
原因:训练语料里混进了 C-EVAL 的题或高度相似的模拟题,模型是“背过”不是“会做”。解决:先看单科分布是否出现“整体高但个别学科接近满分”的畸形结构;再用同分布的新模拟题做一次对照,分数掉一大截基本就是污染。C-EVAL 本身用模拟卷、地方卷来降低这个风险,但你自己的训练数据得自己查。
4.2 现象:CoT 模式分数反而比 answer-only 低
原因:max_new_tokens给小了,推理写到一半被截断,答案行没生成,全被记成解析失败或错答。解决:把生成长度提到 512 以上,先抽 50 条人工看输出,确认“答案:X”确实出现在末尾,再跑全量。
4.3 现象:两次跑同一模型分数差好几个点
原因:开了采样(temperature>0)、batch 内 padding 影响、或 prompt 里 dev 示例顺序变了。解决:固定贪心解码、固定 dev 示例及其顺序、固定 prompt 模板,把这三样写进配置文件,跑之前 diff 一遍。
4.4 现象:拿自己的分数和公开榜单直接比
原因:few-shot 数量、prompt 措辞、是否 CoT、解析规则不同,分数根本不在一个尺度上。解决:横评只在同一套 prompt、同一解析器、同一数据划分下做;要和公开数字比,先复现它的设置,别拿自己的 5-shot CoT 去对别人的 0-shot answer-only。
提示:解析失败率超过 2% 就先别信分数,回去查生成长度和输出格式,这是最常见的“假低分”来源。
5. 把评测做成习惯:子集切片与可复现的对照实验
跑通一次全量只是起点,真正有用的是把 C-EVAL 当成一套可切片的诊断工具。我的做法是固定一个“评测配置档”:prompt 模板、dev 示例、解码参数、解析正则全部写进一个 YAML,每次模型迭代只换权重路径,其余不动,这样两次分数差多少就是模型差多少,而不是配置差多少。进阶用法上,别只盯总分——按学科拉出前后两次的差值表,找出“退步的学科”,往往比找进步学科更有价值,微调把某个领域调崩是常事。C-EVAL HARD 建议每次必跑,它接近随机线的表现是推理能力没跟上最诚实的信号。还有一个容易被忽略的技巧:val 集用来调 few-shot 数量和 prompt 措辞,test 集只在最终定稿时跑一次,反复在 test 上调 prompt 等于变相过拟合。血泪经验是,早期我图省事直接在 test 上试了七八版 prompt,最后报出去的分数自己都不敢信,换一批同分布题立刻打回原形。从那以后我每次改 prompt 都强制先在 val 上走一遍,test 只在最后跑一次并留档。希望这套拆法能帮到你,少在“分数到底可不可信”上翻车。
本文还有配套的精品资源,点击获取