之前回顾了HuggingFace数据集创建以及LLM评估工具的使用,算是对LLM评估有了一定的认识。这次主要是创建一个私有数据集,并在通过lm-evaluation-harness套件在这个数据集上尝试对LLM进行评估,算是实战一下数据集创建和评估工具使用。

创建的数据集从直觉上来说应该尽可能新,以防止泄露到LLM训练数据中,另外数据集本身可能需要一定的权威性和认可度。本文中使用2024年管理类联考的逻辑部分试题来作为评估集,手动将该数据集录入为文字版,称为MJEE_2024数据集。

数据集构建

从互联网上获取了2024年管理类联考的真题pdf,之后使用了逻辑推理部分的试题作为评估集。逻辑推理部分的试题全是选择题,便于评估时进行客观计分。

为了保证pdf转换为Markdown格式过程中数据的准确性,手动复制粘贴并调整格式以达到所有题目都满足同一个格式。格式要求如下:

1
2
3
4
5
<试题序号>.<题干内容>
<选项标签(A/B/C/D/E)>. <选项内容>

参考答案: <答案选项>

如果题干内容中涉及到图形等视觉信息,向题干添加#need_image信息标识该题为多模态类型的题目。

之后,编写脚本将其转化为csv文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import re
import csv

# 读取Markdown文件内容
with open('管理类联考 逻辑推理题.md', 'r', encoding='utf-8') as file:
content = file.read()

# 正则表达式匹配题目和答案
pattern = re.compile(r'\n*(\d+)\.(.*?)\n参考答案:(\w+)\n', re.DOTALL)

# 解析题目
matches = pattern.findall(content)

# 将解析结果写入CSV文件
with open('math_questions.csv', 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['ID', 'Question', 'Answer']) # 写入表头
for match in matches:
question_id = match[0]
question_content = match[1].strip()
answer = match[2].strip()
writer.writerow([question_id, question_content, answer])

print("CSV文件已生成:logic_questions.csv")

由此,我们获得了30道逻辑推理的选择题及答案,其中包含1道需要图像理解的推理题。

上传HuggingFace

csv文件本身已经可以上传HF作为独立的数据集使用。但考虑可扩展性,比如之后我们可以把2024年的数学题、条件推理题也按上述方式处理,并作为一个子集加入到MJEE_2024的话,就需要在数据集的README.md中对整个数据集进行合理的组织。

例如,按如下结构组织数据集(使用tree \F命令生成):

1
2
3
4
5
6
7
8
9
10
│  README.md

├─ condition_2024
│ val.csv

├─ logic_2024
│ val.csv

└─ math_2024
val.csv

其中,logic_2024/val.csv就是之前生成的csv文件。

README.md中的引言区指明各个数据集子集与csv文件的对应关系及基本信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
---
language:
- zh
license:
- cc-by-sa-4.0
source_datasets:
- original
task_categories:
- question-answering
task_ids:
- open-domain-qa
- multiple-choice-qa
pretty_name: MJEE
dataset_info:
- config_name: logic_2024
features:
- name: ID
dtype: int64
- name: Question
dtype: string
- name: Answer
dtype: string
- config_name: math_2024
features:
- name: ID
dtype: int64
- name: Question
dtype: string
- name: Answer
dtype: string
- config_name: condition_2024
features:
- name: ID
dtype: int64
- name: Question
dtype: string
- name: Answer
dtype: string
configs:
- config_name: logic_2024
data_files:
- split: validation
path: logic_2024/val.csv
- config_name: math_2024
data_files:
- split: validation
path: math_2024/val.csv
- config_name: condition_2024
data_files:
- split: validation
path: condition_2024/val.csv
---

# Dataset Card for MJEE
...

最后,我们将在这个文件夹上传的HF中,以方便后续调用。

1
huggingface-cli upload xxx/MJEE ./yyy . --repo-type dataset

注册任务

上传完数据集之后,完成了数据集构建和评估任务的解耦,之后更新数据集可以在前两步下手,而后续注册任务和运行评估则可以通过联网获取最新数据集来进行评估结果更新。

为了注册任务,创建一个新的文件夹lm_eval/tasks/MJEE,在该文件夹下创建一个用于评估2024年逻辑选择题的logic_2024.yaml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
task: MJEE_logic_2024
# 数据集的路径,如果是上传到HF的即`你的用户名/数据集名称`,如果是在本地的直接输入路径即可。
dataset_path: XXX/MJEE
# 数据集子集的名称和划分
dataset_name: logic_2024
validation_split: validation
output_type: generate_until
# 预处理
process_docs: !function utils.process_docs
# 运行评估的prompt
doc_to_text: "阅读并作答以下逻辑题目,注意最终答案必须新起一行并以`#### 答案: [正确选项]`,如`#### 答案: A`。下面是题目。\n{{Question.strip()}}\n"
doc_to_target: "{{Answer}}"
# 生成答案的相关参数
generation_kwargs:
until:
- "</s>"
- "<|im_end|>"
do_sample: false
temperature: 0.0
max_gen_toks: 16000
# 定义怎么提取答案,这边直接使用正则表达式提取
filter_list:
- name: "score-first"
filter:
- function: "regex"
regex_pattern: "#### 答案: ([ABCDE])"
- function: "take_first"
# 定义评估指标,因为是单项选择题,可以直接用精确匹配来做,对了得分,反之不得分。
metric_list:
- metric: exact_match
aggregation: mean
higher_is_better: true
metadata:
version: 1.0

为了支持对含有图像题目的过滤,我们在同级目录建立utils.py进行预处理支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import datasets
import re
import random

question_pattern = r"(.*?)\n([A-E]\. .*(?:\n[A-E]\. .*)*)"
options_pattern = r"[A-E]\. .*?(?=\n[A-E]\. |\Z)"

def process_docs(dataset: datasets.Dataset) -> datasets.Dataset:
def _process_doc(doc):
# 可以做一些简单的后处理
# 但这里不需要,因此直接返回
return doc

def _filter_doc(doc):
if "#need_image" in doc["Question"]:
# 这边对需要图像的示例置False进行过滤
return False
return True

dataset = dataset.map(_process_doc)
dataset = dataset.filter(_filter_doc)
return dataset


def process_docs_shuffle(dataset: datasets.Dataset) -> datasets.Dataset:
# 在原来的基础上,打乱选项排列以减少记忆题目带来的增益
def _filter_doc(doc):
if "#need_image" in doc["Question"]:
# 这边对需要图像的示例置None进行过滤
return False
return True

def _process_doc(doc):
# 打乱选项
# 先分离问题和选项
match = re.match(question_pattern, doc["Question"], re.DOTALL)
old_question, old_answer = doc["Question"], doc["Answer"]

if match:
# 一般正常选项都可以走这个分支
question_body = match.group(1).strip()
options = match.group(2).strip()

# 将选项拆分为列表
options_list = re.findall(options_pattern, options, re.DOTALL)
# 打乱选项
random.shuffle(options_list)

# 组合为新问题
for i, option in enumerate(options_list, start=1):
question_body += f"\n{chr(64 + i)}. {option[3:]}"
# 处理正确选项的变更
if option[0] == doc["Answer"]:
new_answer = f"{chr(64 + i)}"

doc["Question"] = question_body
doc["Answer"] = new_answer

return doc


dataset = dataset.filter(_filter_doc)
dataset = dataset.map(_process_doc)
return dataset

这里面主要就用到HF的datasets库的filter和map操作。process_docs_shuffle函数是将选项打乱之后的新题目,防止LLM在训练时见过这些题目记住答案从而引入测试误差。可以将打乱后的题目作为一个新的测试项目测试,比如叫logic_2024_shuffle.yaml

1
2
3
include: logic_2024.yaml
task: MJEE_logic_2024_shuffle
process_docs: !function utils.process_docs_shuffle

可以借助include语法复用之前任务的大部分配置,只需要修改任务名和预处理函数即可。

这样就把MJEE_logic_2024MJEE_logic_2024_shuffle任务注册到lm-evaluation-harness中了。

运行评估

运行评估很简单,以使用deepseek-chat的api进行评估为例,只需运行:

1
2
3
4
5
6
7
lm_eval --model openai-chat-completions \
--model_args model=deepseek-chat,base_url=https://api.deepseek.com/v1/chat/completions,max_retries=3,num_concurrent=1,eos_string="</s>" \
--tasks MJEE_logic_2024 \
--apply_chat_template \
--log_samples \
--output_path /root/lm_eval_output \
--wandb_args project=MJEE_2024,name=deepseek-chat \

如果是想测试一下运行得对不对,可以使用--limit 5这样的参数限制运行的样本数。

评估对象

搭好框架之后,可以选几个LLM评估在这个数据集上的效果了。因为有些LLM的API需要绑定银行卡才能调用,选择了下面几个比较调用门槛较低、比较方便的LLM做评估。

基础模型:

  • DeepSeek v3:2024年12月份发布的MoE大模型,总参数量671B,激活参数量37B。
  • GLM-4-Plus:智谱的旗舰大模型,参数量未知。
  • GLM-4-Air:智谱的性价比大模型,参数量未知。
  • GLM-4-AirX:据说是极速推理,没有看到和Air有什么明显区别,参数量未知。
  • GLM-4-Air-0111:智谱新升级的Air模型,在多个维度上比旧版Air更好。(截至我写这篇博客时,似乎Air-0111已经合入Air模型了,但我测试时这俩是分开的,因此在这也分开避免混淆吧)
  • GLM-4-FlashX:GLM的Flash版本似乎是开源的9B模型,FlashX是是GLM-4-Flash模型的增强版本,可能是进行了更好的后训练,参数量可能也是9B。
  • Doubao-1.5-pro-32k:字节的主力大模型,参数量未知。

推理模型:

  • DeepSeek R1:基于v3基座,通过强化学习诱使模型输出长思维链从而增强了推理能力。
  • GLM-Zero-Preview:智谱训练的推理模型。

有些LLM没开源,因此选择从API价格来入手,以便我们比较不同价格的模型能力差距有多明显:

模型名称 输入价格(元/1M tokens) 输出价格(元/1M tokens)
DeepSeek v3 2 8
GLM-4-Plus 50 50
GLM-4-Air 1 1
GLM-4-AirX 10 10
GLM-4-Air-0111 0.5 0.5
GLM-4-FlashX 0.1 0.1
Doubao-1.5-pro-32k 0.8 2
DeepSeek R1 4 16
GLM-Zero-Preview 10 10

需要注意的是,我测试时DeepSeek的API尚处于优惠期,而且调用也很快。但是现在价格已经涨回去了,临时优惠的价格并不能很好地反映其真实推理成本,因此这边还是用正常价格。

评估结果

终于要开始评估了,直接配置好API的端点,然后设置:

1
export OPENAI_API_KEY = xxx(对应的API KEY)

之后按运行评估章节的命令启动评估即可。

首先所有模型都是一次测试的结果,测试结果无法排除偶然因素。而且实际测试的题目只有29道逻辑选择题而已,一方面覆盖面不够广,不一定能反映模型真实能力;另一方面选择题也有运气好蒙对的可能。总之,这个评估结果就是对上面这个评估任务的一次实操,攒经验的,并不是真正严谨地测评这些模型的能力。

逻辑题评估结果

测评结果如下所示。使用了绿色柱状图标识推理模型,基础模型则使用蓝色柱状图。

MJEE_logic_2024上的评估结果
    

基础模型组里,我觉得智谱、DeepSeek和豆包的旗舰模型表现都是可比的。豆包在这个任务上更如鱼得水一些,Doubao-1.5-pro-32k以79.31分和实惠的API价格拿下基础模型组的第一。GLM-4-Plus稍好于DeepSeek v3,但要算经济账的话,我个人会更倾向于v3一些。智谱的高性价比模型GLM-4-Air-0111以很小的差距追逐DeepSeek v3,考虑到前者的成本只有后者的25%以下,这个结果还是不错的。

GLM-4-FlashXGLM-4-AirX这样的模型延迟是比较低的,在评测的时候能感觉出来是轻快的模型。但这里主要评估的是正确率,因此小模型可能会比较吃亏。

推理模型中,DeepSeek R1以绝对的优势拿下第一,并且在逻辑选择题中展现了对基础模型绝对的统治力。29题里只错1题,可以认为和前面的基础模型组的模型有显著差异。反观GLM-Zero-Preview则不那么乐观,似乎它更多地对数学题进行了优化,对这种纯逻辑题有点吃力,性能相对来说比较羸弱。不过是preview版本也还行了,毕竟是去年12月份的模型,时间比较紧张,可能重点去优化了AIME、MATH500这样的benchmark。

打乱选项后的逻辑题评估结果

其实评完还是有些疑惑的,因为这些模型里面不乏新模型,很可能它们已经在这个资料上训练过,记住答案了。因此,为了减少记忆带来的结果虚高,也在打乱选项(即MJEE_logic_2024_shuffle任务)的设置下重新评测了基础模型。图表中用蓝色条表示打乱前的评估分数,绿色条表示打乱选项后的评估分数。

MJEE_logic_2024_shuffle上基础模型的评估结果
    

可以发现,除了小模型GLM-4-FlashX浮动2题之外,大部分基础模型的浮动范围都在1道题以内。至少在打乱选项设置下,可以说这些基础模型应该是展现了它们的推理能力而不是记忆力。

尽管为了严谨应该也对推理模型进行一次MJEE_logic_2024_shuffle任务的测试,但看了DeepSeek R1的论文,它就是用结果监督的进行强化学习的。如果训练的是选择题那也很容易Reward Hacking,DeepSeek应该有意避免了在强化学习时使用选择题,因此这里不严谨地不进行测试。

细粒度分析

除了得到这么一个正确率指标,我们还能进行更细粒度地分析吗?

LLM答题具体情况如何?

把目标聚焦在DeepSeek v3GLM-4-PlusDoubao-1.5-pro-32kDeepSeek R1上,也许会想知道它们具体答对了哪些题目,从答对和答错题目的分布上分析出一点东西。

在看过具体数据后,发现除了答对和答错之外,还有一个特殊情况,就是正则表达式匹配失败了,不知道模型选了什么。因此,将结果标签分为-1(答案解析失败)、0(错误)和1(正确)三类。将9个LLM在29道题上的答题情况以热力图的形式进行展现:

细粒度的热力图
    

观察这个热力图,我们可以发现几个非常有趣的结论:

  1. 尽管DeepSeek R1一骑绝尘,29题中只错一题,可是错的那个33题实在是有些匪夷所思。33题除了同为推理模型的GLM-Zero-Preview答案解析错误之外,就连最小的FlashX模型都答对了。推理模型在简单问题上犯错,这个现象在openai的o3模型上也有发现,比如在ARC Prize的这篇博客中:

In particular, we are very curious about the ~9% set of Public Eval tasks o3 was unable to solve, even with lots of compute, yet are straightforward for humans.

特别是,我们对约 9% 的公共评估任务感到非常好奇,这些任务 o3 即使投入大量计算资源也无法解决,然而对人类来说却很简单。

目前的推理模型在这种逻辑题上占很大便宜,但它们依旧有可能在人类觉得简单的题目上犯错误,甚至是基础模型觉得简单的题目也有概率会出错。说明目前的推理模型还是有进步空间。

  1. GLM-Zero-Preview居然有8道题解析失败,等于8道题答题卡不涂就交卷了。总共才29道题,也难怪分数不高的。这只能怪模型本身,因为按格式输出答案本身也是指令遵循能力的一部分。这边主要关注一下它为什么解析失败。

查找到了它解析失败的题目,发现它把答案格式化成这样了:

1
2
3
**答案: A**
或者
**最终答案:**\n\n\\[\\boxed{D}\\]

看到这个\boxed{X},感觉可能是用来强化学习的数据太单调了,大部分可能是数学题。在微调时模型错误地学习了答案要放到boxed里面,也算是过拟合了吧。毕竟是preview版本,可能目前就针对数学题强化学习过,指令遵循能力差可能是训练数据多样性的问题。像R1那样也针对非数学领域的进行微调和强化学习也许就能重新找回指令遵循能力。

抛开指令遵循能力,人工评估GLM-Zero-Preview的结果,发现答案解析错误的8题里面居然对了5题。那么其实最后纯算推理的话它的得分是\((5+17)/29=75.86\%\),也算挽尊了一手。

  1. 还有一个有趣的发现是怎么指令遵循得好好的GLM-4-Air升级到0111版本之后,反倒指令遵循出问题然后出现3个无法解析的答案呢?

我也不知道具体原因,但结合GLM-Zero-Preview的表现和DeepSeek v3的技术报告,我猜想GLM-4-Air-0111版本可能也和v3增强推理能力的方式类似,从GLM-Zero-Preview拒绝采样了一批数据用于微调0111版本,以增强0111模型的数学能力。但可能受限于模型大小和数据配比,让推理模型指令不遵循的问题”遗传“到GLM-4-Air-0111版本了。

如果抛开指令遵循能力,手动验证无法解析答案的题目之后,GLM-4-Air-0111在3题中做对2题,得分是\((19+2)/29=72.41\%\),还是挺可惜的。

  1. 豆包模型在这个任务上很厉害,而且也比较实惠。正好v3的api也比较繁忙,也许可以尝试使用豆包1.5pro去替代一下。

题目难度如何?

9个LLM,相当于9个学生在答题了。kimi 1.5论文里介绍了一种基于SFT模型的提示难度判断方法:

We adopt a model-based approach that leverages the model’s own capacity to adaptively assess the difficulty of each prompt. Specifically, for every prompt, an SFT model generates answers ten times using a relatively high sampling temperature. The pass rate is then calculated and used as a proxy for the prompt’s difficulty—the lower the pass rate, the higher the difficulty. This approach allows difficulty evaluation to be aligned with the model’s intrinsic capabilities, making it highly effective for RL training. By leveraging this method, we can prefilter most trivial cases and easily explore different sampling strategies during RL training.

我们采用一种基于模型的方法,利用模型自身的能力来适应性地评估每个提示的难度。具体来说,对于每个提示,一个 SFT 模型使用相对较高的采样温度生成答案十次。然后计算通过率,并将其作为提示难度的代理指标——通过率越低,难度越高。这种方法使难度评估与模型的内在能力保持一致,对于强化学习训练非常有效。通过利用这种方法,我们可以预先过滤掉大多数简单的案例,并在强化学习训练期间轻松探索不同的采样策略。

我们这里不是要训练什么模型,只是单纯地判断一下题目的难度。类似地,我们也计算9个LLM对这些题目的通过率,将这个通过率作为题目难度的代理指标。

题目难度热力图
    

如图所示,通过率越靠近0的红色越深,也代表题目的难度越大。反之,越靠近1的绿色越深,代表题目难度越小。可以发现54题非常困难,只有DeepSeek R1一个人做对。也有一些通过率为100%的简单题。整体来说前面的题目比较简单,后面的题目中开始混入难题了。

需要注意的是,因为这里面有很多GLM家族的模型,因此通过率被这些智谱的模型主导了。如果你真的需要通过基于模型的方法对训练数据进行打标,那么使用k1.5的工作流程是更科学的。

总结

本文从2024年管理类联考的真题中抽取了30道逻辑推理的选择题,并通过lm-evaluation-harness对选定的几个大模型进行了评估。不仅通过正确率获取了宏观结果,还通过热力图展示了各种LLM的细粒度答题情况,通过通过率展示了题目的难度情况。算是一次对LLM评估流程全面的实践。