第一篇博文,想聊聊最近关注度很高的 Diagram-MMU 这个科学图解多模态评测基准。前阵子在给团队选型视觉语言模型(VLM)时,我们最大的困扰不是模型效果不够好,而是“好”这个结论到底怎么量化。常规 benchmark 刷分和真实业务里的科学图解理解场景差距很大,直到 Diagram-MMU 出现,才感觉有了一个更对口的测试集。接下来我会把它的设计思路、评测原理,以及如何在自己的环境里跑通完整评测流程,做一个完整的拆解。
1. Diagram-MMU 是什么:一个专注科学图解的视觉问答基准
1.1 为什么需要一个新的多模态评测基准
过去两年,多模态大模型(VLM)的发展速度非常快,从早期的图像描述、OCR 识别,到现在的复杂图表推理、跨学科知识问答,模型的视觉理解能力一直在升级。但评测体系却长期存在两个问题。
第一个问题是“测不准”。很多公开榜单上的任务和真实科研、教育场景脱节。比如模型能识别一张自然照片里的猫,能念出身份证上的文字,但当你给它一张电子电路图,问它“这个反馈网络属于什么类型”时,它往往答非所问。自然图像理解和科学图解理解是两种完全不同的能力,后者需要同时具备视觉结构解析、学科知识调用和逻辑推理能力。
第二个问题是“测不全”。传统的 VQA(视觉问答)数据集大多是单学科、单题型,涉及科学图解的部分也比较零散,缺少一个覆盖多学科、多难度、多图解类型的系统性评测集。这导致开发者在模型选型时,很难回答一个基本问题:“这个模型在科学图表理解上,到底处于什么水平?”
Diagram-MMU 正是为了解决这个问题而提出的。它是一个面向科学图解的多模态评测基准,核心目标是考察视觉语言模型“理解和推理科学图解”的能力。
1.2 Diagram-MMU 的核心设计思路
从公开资料来看,Diagram-MMU 在设计上有几个很鲜明的特点。
第一个特点是题目来源真实。测试题不是人工凭空编造的,而是来源于教科书、讲义、学术论文中的真实科学图解。这样做的好处是题目本身具备严谨性,模型没法靠“背诵题库”来刷分。
第二个特点是分学科、分难度。题目覆盖了物理、化学、数学、生物、计算机科学等多个学科,难度上划分了高中、大学、研究生(博士)等不同等级。这种分层设计非常实用:高中题目考的是基础图解阅读能力,大学题目考的是专业知识调用,博士题目则侧重前沿论文图表的理解。
第三个特点是图解类型多样。科学图解不只是“图表”两个字能概括的,它包含流程图、结构示意图、架构图、映射关系图、统计图等。不同类型的图解对模型的挑战并不一样。流程图考察的是时序逻辑理解,架构图考察的是层次关系提取,统计图考察的是数值与趋势判断。
这三个特点合在一起,让 Diagram-MMU 成为一个既考察“看没看清”又考察“懂没懂”的评测基准。
1.3 与 MMMU、ScienceQA 等基准的定位差异
很多读者之前接触过 MMMU(Massive Multi-discipline Multimodal Understanding)和 ScienceQA,这里有必要做个区分。
MMMU 是一个覆盖面很广的多模态学科理解基准,包含了艺术、商科、科学等多个领域,题目以大学级别为主。它的优势是“广”,但科学图解只是其中一部分,并不会针对图解类型做专门细化。
ScienceQA 则是一个科学问答数据集,题目以自然科学为主,包含图文混合的问答形式。它更偏向知识问答,图解本身的结构和推理深度相对有限。
Diagram-MMU 的定位介于两者之间,但它更聚焦“图解理解”这个能力维度。如果说 MMMU 是“百科知识考试”,ScienceQA 是“科学课随堂测验”,那么 Diagram-MMU 更像是一场“看图解题的专项能力测试”。
| 维度 | Diagram-MMU | MMMU | ScienceQA |
|---|---|---|---|
| 核心考察点 | 科学图解理解与推理 | 多学科综合理解 | 科学知识问答 |
| 学科覆盖 | 物理、化学、数学、生物、计算机等 | 艺术、商科、科学等 30 个学科 | 自然科学为主 |
| 难度分层 | 高中 / 大学 / 研究生 | 大学级别为主 | K-12 科学课程 |
| 图解类型 | 流程图、架构图、示意图等 | 自然图像、图表混用 | 图文混排为主 |
| 适用场景 | 科研教育类 VLM 选型评测 | 通用 VLM 能力对比 | 基础教育科学模型评测 |
了解这层差异后,你在做技术选型时就能更有针对性:如果你的业务场景是教育题库、科研论文分析、工程图纸理解,那么 Diagram-MMU 的参考价值会比通用 benchmark 更高。
2. 评测原理与指标拆解
2.1 任务定义:多选形式的视觉问答
Diagram-MMU 的任务形式是多项选择视觉问答。给定一张科学图解和一个问题,模型需要从若干个候选项中选择正确答案。这种任务形式有几个好处。
第一,客观性更强。比起开放性问答,多选题的评分标准非常明确,答案非对即错,减少了人工评分的主观偏差。第二,可比性更强。不同模型在同一组题目上的准确率可以直接对比,便于排行榜排序。第三,更容易定位错误类型。通过统计模型在不同学科、不同难度、不同图解类型上的准确率,可以反推模型的能力短板。
从任务定义可以看出,Diagram-MMU 本质上是一个多选题形式的视觉问答评测,模型需要先理解图像内容,再调用学科知识完成推理。
2.2 常用评估指标:准确率与部分正确率
Diagram-MMU 使用的指标主要是 Accuracy(准确率)和 PPR(Partial Point Rate,部分正确率),不过具体以官方最新说明为准。
准确率的计算很简单,就是模型预测正确的题数占总题数的比例。所有题目等权计算,公式如下:
accuracy = correct_count / total_countPPR 则是针对部分题目设计的细粒度评估。有些题目存在多个子问,或者答案分多个要点,如果模型只答对了一部分,可以按比例给分。这类指标在多选题、复合题型中很有价值,能反映模型“部分掌握”的情况。
从工程角度看,评测脚本里最重要的是把“模型输出”和“标准答案”做规范化对齐。很多模型会输出多余的解释文字,直接进行字符串比较会浪费大量正确结果,所以通常需要先抽取答案选项字母,再做比对。
2.3 按学科、难度、图解类型拆分评估
单一总分只是一个粗略的参考,真正有价值的是细粒度拆分。我建议在自己的评测脚本里,把结果按照三个维度切分。
按学科拆分,可以知道模型在物理电路图、化学分子结构、生物流程图上的表现差异。按难度拆分,可以观察模型是从高中阶段就开始掉点,还是到了研究生级别才出现明显下滑。按图解类型拆分,则能看出模型是流程图理解差,还是架构图理解差。
这三种拆分组合起来,可以形成一张“模型能力热力图”,对后续优化方向的指导意义远大于一个孤零零的总分。
3. 环境准备与数据准备
3.1 运行环境建议
本节进入实操环节。先说明一点:Diagram-MMU 的官方发布信息和训练/评测代码会持续更新,以下示例以常见的多模态评测流程为基础,重点演示评测思路,实际运行时请以官方仓库为准。
推荐环境如下:
- 操作系统:Linux(Ubuntu 20.04 及以上)或 macOS
- Python 版本:3.10 及以上
- 主要依赖:
transformers、torch、openai、Pillow、pandas、openpyxl - 推理框架:可以使用 Hugging Face Transformers,也可以通过 OpenAI 兼容 API 调用远端模型
建议先创建独立的虚拟环境,避免依赖冲突:
conda create -n diagram_mmu python=3.10 -y conda activate diagram_mmu pip install transformers torch openai pillow pandas openpyxl3.2 数据集获取与目录结构
以官方发布为准,通常数据集会包含图片文件和标注文件。标注文件常见为 JSON 或 JSONL 格式,每条记录包含题目 ID、学科、难度、图解类型、问题文本、选项列表、标准答案和对应图片路径。
建议把数据整理成如下目录结构:
diagram_mmu_eval/ ├── data/ │ ├── images/ │ │ ├── 00001.png │ │ ├── 00002.png │ │ └── ... │ └── annotations/ │ ├── dev.jsonl │ ├── test.jsonl │ └── ... ├── scripts/ │ ├── run_eval.py │ ├── score.py │ └── analysis.py └── results/ └── ...下面是一个典型的 JSONL 标注示例(字段名为示例,实际以官方格式为准):
{ "id": "diagram_mmu_00001", "subject": "physics", "difficulty": "undergraduate", "diagram_type": "circuit_diagram", "question": "该电路中的反馈类型是什么?", "options": ["A. 电压串联负反馈", "B. 电压并联负反馈", "C. 电流串联负反馈", "D. 电流并联负反馈"], "answer": "A", "image": "images/00001.png" }拿到数据后,先做一步数据完整性检查,确认图片文件都能正常打开,再进入评测流程。
4. 动手实战:跑通一次 Diagram-MMU 评测
4.1 加载评测数据
先把 JSONL 文件加载成 Python 对象列表。这里需要注意编码问题和缺失字段容错。
# 文件路径:scripts/load_data.py import json def load_jsonl(file_path): data = [] with open(file_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue try: record = json.loads(line) data.append(record) except json.JSONDecodeError as e: print(f"解析失败: {line},错误: {e}") return data if __name__ == "__main__": records = load_jsonl("data/annotations/test.jsonl") print(f"共加载 {len(records)} 条评测样本") print(records[0]["question"])这段代码做了基础容错:解析失败的行会打印出来但不会中断整个流程。实际评测时,我建议先单独跑通这条脚本,确认数据没有问题,再继续后续步骤。
4.2 编写模型推理脚本
模型推理是整个评测流程的核心。这里给出两种调用方式:一种是调用 OpenAI 兼容接口,适合远端模型;另一种是基于 Hugging Face Transformers,适合本地模型。
方式一:OpenAI 兼容 API
假设后端模型支持gpt-4o或qwen-vl这类多模态接口,图片通常以 base64 编码后随请求发送。
# 文件路径:scripts/vlm_api_infer.py import base64 import os from openai import OpenAI client = OpenAI( base_url=os.getenv("VLM_BASE_URL", "http://localhost:8000/v1"), api_key=os.getenv("VLM_API_KEY", "EMPTY"), ) def image_to_base64(image_path: str) -> str: with open(image_path, "rb") as f: return base64.b64encode(f.read()).decode("utf-8") def run_vlm(question, options, image_path, model="gpt-4o"): prompt = ( "请根据图片内容回答下面的科学图解选择题。\n" f"问题:{question}\n" f"选项:{' '.join(options)}\n" "请直接输出一个选项字母(如 A、B、C、D),不要输出额外解释。" ) img_b64 = image_to_base64(image_path) response = client.chat.completions.create( model=model, messages=[ { "role": "user", "content": [ {"type": "text", "text": prompt}, { "type": "image_url", "image_url": { "url": f"data:image/png;base64,{img_b64}" }, }, ], } ], temperature=0, ) return response.choices[0].message.content.strip()这里使用temperature=0是为了最大程度保证输出的确定性,减少随机性对评测结果的影响。如果你使用本地 vLLM 服务,也可以通过 OpenAI 兼容协议直接对接。
方式二:Hugging Face Transformers 本地推理
本地模型的调用方式略有差异,不同模型的对话模板和图像处理方法不同。下面是一个最小示例思路:
# 文件路径:scripts/vlm_hf_infer.py from transformers import AutoProcessor, AutoModelForCausalLM from PIL import Image model_id = "your-vlm-model-id" processor = AutoProcessor.from_pretrained(model_id) model = AutoModelForCausalLM.from_pretrained( model_id, device_map="auto", torch_dtype="auto", ) def run_vlm_hf(question, options, image_path): image = Image.open(image_path).convert("RGB") prompt = ( "请根据图片内容回答下面的科学图解选择题。\n" f"问题:{question}\n" f"选项:{' '.join(options)}\n" "请直接输出一个选项字母(如 A、B、C、D)。" ) inputs = processor( text=prompt, images=image, return_tensors="pt", ).to(model.device) output = model.generate( **inputs, max_new_tokens=32, do_sample=False, ) generated = processor.decode(output[0], skip_special_tokens=True) return generated[len(prompt):].strip()不同的模型处理器在参数细节上可能有差异,需要按实际模型调整。这里演示的是通用流程。
4.3 批量评测与结果缓存
评测数据集通常有上千条样本,为了防止中途断网或进程异常导致前功尽弃,强烈建议引入结果缓存机制。每推理完一条,就立即写入结果文件。
# 文件路径:scripts/run_eval.py import json import os from load_data import load_jsonl from vlm_api_infer import run_vlm def load_done_ids(result_path): done_ids = set() if os.path.exists(result_path): with open(result_path, "r", encoding="utf-8") as f: for line in f: record = json.loads(line) done_ids.add(record["id"]) return done_ids def main(): data_path = "data/annotations/test.jsonl" result_path = "results/predictions.jsonl" model_name = "gpt-4o" records = load_jsonl(data_path) done_ids = load_done_ids(result_path) os.makedirs("results", exist_ok=True) with open(result_path, "a", encoding="utf-8") as out_f: for idx, record in enumerate(records): if record["id"] in done_ids: continue image_path = os.path.join("data", record["image"]) prediction = run_vlm( record["question"], record["options"], image_path, model=model_name, ) out_f.write(json.dumps({ "id": record["id"], "subject": record["subject"], "difficulty": record["difficulty"], "diagram_type": record["diagram_type"], "ground_truth": record["answer"], "prediction": prediction, }, ensure_ascii=False) + "\n") out_f.flush() if (idx + 1) % 50 == 0: print(f"已完成 {idx + 1} / {len(records)} 条") if __name__ == "__main__": main()这种“边推理边写文件”的方式,可以在进程中断后从断点继续跑,不需要重新推理已完成的部分。
4.4 结果评分与规范化匹配
模型输出可能包含各种格式,比如“答案是 A”“我认为是 B”“选 C”等。因此评分前必须做输出规范化。
# 文件路径:scripts/score.py import re import json def normalize_answer(text): """从模型输出中抽取选项字母。""" if not text: return "" text = text.strip() # 直接命中的情况:输出为 A、A.、A) 等 m = re.match(r"^[A-D]", text, flags=re.IGNORECASE) if m: return m.group(0).upper() # 从“答案是 A”这类句子中抽取 m = re.search(r"答案(?:是|为)?[::]?\s*[((]?([A-D])[))]?", text) if m: return m.group(1).upper() # 从“选 C”这类句子中抽取 m = re.search(r"选\s*([A-D])", text) if m: return m.group(1).upper() return "" def calc_accuracy(results): correct = 0 total = len(results) for item in results: pred = normalize_answer(item["prediction"]) gt = item["ground_truth"].strip().upper() if pred == gt: correct += 1 return correct / total if total else 0 if __name__ == "__main__": results = [] with open("results/predictions.jsonl", "r", encoding="utf-8") as f: for line in f: if line.strip(): results.append(json.loads(line)) acc = calc_accuracy(results) print(f"样本总数: {len(results)}") print(f"整体准确率: {acc:.4f}")normalize_answer函数里做了三层匹配:先处理最简单的情况,再处理中英文混合句子,最后兜底提取“选 X”的表达。这种做法可以显著减少因输出格式导致的误判。
4.5 细粒度分析:按学科和难度拆分
整体准确率只是第一步,下面给出按学科和难度拆分的分析脚本。
# 文件路径:scripts/analysis.py import json from collections import defaultdict from score import calc_accuracy, normalize_answer def analyze(results): metrics = defaultdict(lambda: {"correct": 0, "total": 0}) for item in results: pred = normalize_answer(item["prediction"]) gt = item["ground_truth"].strip().upper() key = item["subject"] metrics[key]["total"] += 1 if pred == gt: metrics[key]["correct"] += 1 for key, value in metrics.items(): acc = value["correct"] / value["total"] if value["total"] else 0 print(f"{key}: {value['correct']}/{value['total']} = {acc:.4f}") if __name__ == "__main__": results = [] with open("results/predictions.jsonl", "r", encoding="utf-8") as f: for line in f: if line.strip(): results.append(json.loads(line)) print("=== 分学科准确率 ===") analyze(results)类似的逻辑可以扩展到难度、图解类型维度。把这些维度组合起来,就能找出模型具体的薄弱环节。
5. 常见问题与排查思路
在跑 Diagram-MMU 评测时,我踩过一些坑,这里整理成排查清单,供大家参考。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 图片加载失败 | 图片路径拼错或文件损坏 | 在推理前用PIL统一验证图片是否可打开 |
| 模型输出为空 | 远端服务超时或本地显存不足 | 调大超时时间、降低并发、检查显存占用 |
| 准确率异常低 | 输出规范化不完整,模型回答被误判 | 检查normalize_answer是否能覆盖常见输出格式 |
| 部分学科样本为 0 | 数据集字段名与脚本不匹配 | 先打印一条样本,核对字段映射 |
| 同一模型多次评测结果不一致 | temperature未设为 0 或后端有随机采样 | 统一设置采样参数,关闭随机性 |
| 评测中断后重复推理 | 没有结果缓存或断点续跑逻辑 | 采用逐条写入 JSONL 的缓存机制 |
| 内存占用过高 | 批量加载过多图片 | 改为单条流式处理,不要一次性把所有图片读入内存 |
这里特别强调图片加载问题。前期我遇到过图片路径包含相对路径前缀的情况,直接用os.path.join("data", record["image"])处理时,假如record["image"]本身已经含有images/前缀,就会重复拼接。建议先打印一条完整路径确认,再大规模运行。
另外一个容易忽视的问题是大规模评测的成本。如果调用远端 API,上千张图片的推理费用和对端并发限制都需提前评估。建议先随机抽 100 条做小规模验证,确认整体流程没问题后,再跑全量数据。
6. 提升模型在 Diagram-MMU 上表现的最佳实践
6.1 数据层面:构建高质量微调集
如果目标是提升自己模型在 Diagram-MMU 上的表现,首要手段是构建高质量的科学图解指令微调集。建议从教科书、公开课程、论文配图中收集图文问答对,覆盖不同类型的图解,并注意与评测集隔离,避免数据泄露。
在扩充数据时,可以采用“题目改写”策略:保留原图,对问题做同义改写,或者对选项顺序做扰动。这样可以增加数据多样性,降低模型对固定选项顺序的依赖。同时,要保证答案的准确性,任何标注错误都会直接污染模型能力。
6.2 模型层面:选择合适的底座与输入分辨率
多模态模型的图像编码器分辨率直接影响科学图解的识别效果。很多模型默认会把图片缩放到固定尺寸,而科学图解中常常有细小的文字、符号和图例,缩放后信息丢失严重。训练或推理时,如果模型支持高分辨率输入,建议优先开启。
从模型选型角度看,科学图解理解强的模型通常具备两个特征:一是视觉编码器参数量大、分辨率高,二是语言模型部分有较强的逻辑推理能力。实际选型时,可以先用 Diagram-MMU 的上百条子集快速筛选,再做全量评测。
6.3 提示词层面:让模型先看结构再作答
推理时的提示词设计对结果有明显影响。对于科学图解类题目,可以引导模型分步骤思考:先描述图中的关键结构和元素,再结合问题做推理,最后给出答案。
请按以下步骤回答科学图解选择题: 1. 简要描述图片中的核心结构或流程。 2. 找出与问题直接相关的关键信息。 3. 结合学科知识分析每个选项。 4. 最后单独一行输出你的答案字母。需要提醒的是,这种“思维链”式提示会增加输出长度和推理耗时。如果评测目的是对比模型能力,建议对不同模型使用统一提示词,保证公平性;如果目的是挖掘模型极限性能,则可以为每个模型单独优化提示词。
6.4 评测工程化:建立可复现的评测流水线
评测工作不只是跑一条脚本,建议搭建可复现的评测流水线。具体包括:
- 固定模型版本和推理参数,记录环境依赖清单。
- 使用结果缓存和断点续跑机制,支持大规模评测。
- 每次评测后生成细粒度分析报告,沉淀到版本管理中。
- 对模型输出做抽样人工复核,防止自动化评分掩盖语义问题。
这样做的好处是,当模型版本更新时,可以快速重跑同一套评测,观察能力变化。
7. 写在最后的几点实践建议
评测多模态模型的科学图解能力,不能只盯着一张“排行榜单”看。通过 Diagram-MMU 这类细粒度 benchmark,可以透视模型在不同学科、不同难度、不同图解类型上的真实表现,这才是评测的核心价值。
如果接下来你想上手实践,我建议顺序是先下载一个开源 VLM 跑通推理脚本,再用 100 条样本验证评分逻辑,最后再全量评测。这套流程跑通之后,你会发现后续换模型、调参数都变得非常顺畅。如果你在跑评测时遇到其他奇怪的问题,也欢迎在评论区交流,我们一起把这份评测经验补完整。