OpenResearch 论文级基准对比与消融图绘制指南:从 grouped bars 到 delta 点图
【免费下载链接】OpenResearchTurn your coding agents into research agents项目地址: https://gitcode.com/GitHub_Trending/op/OpenResearch
本文是 OpenResearch 项目中orx-figures技能参考文档(agent-skills/orx-figures/references/comparison.md)的完整实战讲解。它回答论文写作中最常被问到的两个问题:在多个基准上哪个方法赢了?被消融的组件到底有没有用?读完本文,你将掌握分组柱状图(grouped bars)与差分布点图(delta dot-and-whisker)的选型逻辑、配色与排序规范、diff_ci差分置信区间统计,以及两套可直接运行的 matplotlib 模板,并了解 OpenResearch 统一的orx_figstyle样式模块如何在源码层面强制这些规范。
两个相似的问题,两种不同的图
"多个方法在多个基准上的对比"与"消融实验里某个组件的作用"看起来都要画对比图,但它们的可信编码方式完全不同。原文档用一个决策表给出了判断准则:
| 对比的类型 | 应该用 | 原因 |
|---|---|---|
| 多个方法横跨多个基准,分数从零开始测量 | 分组柱状图—— 评测类论文的主图 | 当坐标轴从零开始时,柱子的长度是诚实的信息编码;分组让读者能快速扫描某个方法在所有基准上的表现 |
| 单一指标,差异相对种子噪声很小 | 差分布点图(dot-and-whisker on the difference) | 位置编码数值,坐标轴可以只覆盖感兴趣的区间而不会撒谎 |
这两条规则底下是同一条铁律:柱子的长度是一种承诺——坐标轴必须从零开始。为了让 0.4 分的差距看起来决定性而截断柱状图坐标轴,是 ML 论文里最常见的作弊图。注意这是"反对截断"而非"反对柱子":accuracy、pass@k、win rate、counts 这些指标本来就合法地从零开始,柱子正是它们该用的图形标记。
分组柱状图:让"他们的"和"我们的"一眼可分
色相(Hue)属于方法族,明度(Shade)属于变体
灰色给未调优的基线模型,一种色相给前人工作,另一种色相给我们的方法,族内用浅到深的明度渐变区分变体(由样式模块中的family()函数生成)。这样读者在读到图例的任何一个条目之前,就能先区分出"别人的"和"我们的"。七个任意选择的颜色什么都传达不了。
源码层面,family(name, n, lightest=0.55)在 orx_figstyle.py 中实现:它取 Okabe-Ito 调色板中某个基色,在lightest=0.55(浅)到0.0(深)之间线性插值出n个明度,返回十六进制颜色列表。而MUTED = "#CFCFCF"(orx_figstyle.py)就是文档模板中灰色基线用的填充色。调色板本身(PALETTE)是色盲友好、灰度打印可区分的 Okabe-Ito 八色,明确禁止jet、rainbow、hsv这类会"发明数据中不存在的结构"的配色(见 orx-figures/SKILL.md 的 Non-negotiables 第 7 条)。
方法顺序全图一致,且与图例顺序相同
读者是通过"横向扫描某个方法在所有分组中的柱子位置"来读这张图的。每个分组内的方法顺序必须完全一致,并且与图例顺序一致。如果每个组内部重新排序,这种扫描式阅读就被破坏了。
每根柱子都标上数值
给每根柱子标注其值,让图同时承担结果表格的职责——读者引用你的数字时不必眯着眼去对坐标轴。这是全文中唯一允许字号低于 7pt 刻度字号的地方,最低可到 5pt:因为标签与柱高信息冗余,读不清标签的读者仍能从坐标轴恢复数值。满栏宽下约 5pt;如果柱子密到标签互相碰撞,那就是"删掉一个方法或拆分图片"的信号,而不是继续缩小字号的信号。
基准分组要有意排序
按难度、按规模、或按与结果表格一致的顺序来排基准分组。按字母序排序是浪费机会——它把读者引导到随机位置,而不是你想强调的叙事顺序。
图例的框与位置
房子风格(house style)默认图例无框,这在对坐标轴之外时是对的;当图例浮动在绘图区之上时,必须给它不透明的底框,否则它会被读成图表的一部分。同时把图例放在柱子最短的区域,避免遮挡数据(模板中将frameon=True, framealpha=1.0, edgecolor="black"同时设置,就是这个道理)。
规模上限
分组柱状图在约 6 方法 × 6 基准处触顶。再往上,任何样式技巧都救不了它——请改用表格。
消融实验:直接画差值
当主张是"这个组件重要"时,画variant − baseline(变体减基线),在零处画一条参考线,按效应大小排序。读者一眼看到符号、大小、以及置信区间是否跨零——而这正是全部问题所在。
必须显示"差值"的置信区间(diff_ci)
要显示的是差值的区间。两个各自重叠的 per-variant 区间并不意味着差值无法与零区分——这是一个被广泛重复的错误。如果某个变体的区间跨零,诚实的读法是"在当前种子数下没有可检测的效应"。
OpenResearch 在 orx_figstyle.py 中专门实现了diff_ci(treatment, baseline)(L424-L448):计算均值差treatment.mean() - baseline.mean(),用Welch t 检验(不假定两组方差相等)构造 95% 置信区间。关键细节:
- 用 t 临界值而非正态近似,因为种子数很少,正态近似会低估区间宽度(
_T95表从 df=1 的 12.706 到 df=120 的 1.980,见 L58-L62); - 自由度向下取整——"a smaller df gives a larger t, so the interval errs wide"(区间偏宽而不是夸大少量种子对均值的分辨力,见
_t95的注释 L416-L421); - 若任一侧只有 1 个种子,函数发出警告并返回
(delta, delta, delta)——区间退化为一个点,必须在 caption 中说明"single seed"。
同时,mean_ci 对单种子也会告警 "one seed: the band is a line, not an interval"。
单种子怎么办
每个变体只有一个种子时没有区间。在 caption 里写明 "single seed",并且不要画暗示存在区间的 whisker。
什么时候该用表格而不是图
如果读者需要精确数字,或变体超过约 8 个,或差异落在噪声之内,一张booktabs表格比任何图表都传达得更好。表格格式规范见orx-paper模块(agent-skills/orx-paper/SKILL.md),其中明确\toprule、\midrule需要加载booktabs包,并给出了可直接编译的最小 preamble。
常见陷阱速查
原文档给出了一张陷阱对照表,值得逐条核对:
| 陷阱 | 修正 |
|---|---|
| y 轴截断的柱状图 | 从零开始,或改用 delta 点图 |
| 每个方法一个不同色相 | 基线用灰色,一族一个色相,族内用明度渐变 |
| 方法顺序随分组变化 | 固定顺序,并与图例一致 |
| 误差棒含义不明 | 写清楚:"95% CI over 5 seeds"。SD、SEM 与 CI 相差可达数倍 |
| 旋转 45° 的刻度标签 | 用水平点图加左对齐的名称,或加宽图幅 |
| "我们的"无论分数如何都排最前 | 诚实地排序;在某个子集上输了也是一个结果 |
| 柱标签互相碰撞 | 减少方法数或拆分图片——而不是缩小字号 |
模板一:跨基准分组柱状图
原文档提供了完整可运行的figs/benchmarks.py,它读取figs/benchmarks.csv(列:benchmark,method,score)。核心结构如下:
"""Accuracy across benchmarks. Regenerate: python figs/benchmarks.py""" import csv import math from collections import defaultdict import numpy as np from orx_figstyle import MUTED, WIDE, family, figure, save, use_style DATA = "figs/benchmarks.csv" # Fixed order everywhere, and the family each method belongs to. Grey for the # untuned model, blue for prior work, red for ours. METHODS = [ ("Qwen2.5-Math-7B", "base"), ("SimpleRL-Zero-7B (GRPO)", "prior"), ("OpenReasoner-Zero-7B (PPO)", "prior"), ("Oat-Zero-7B (Dr.GRPO)", "prior"), (r"ES$_\mathrm{CHKPT-1}$", "ours"), (r"ES$_\mathrm{CHKPT-2}$", "ours"), (r"ES$_\mathrm{CHKPT-3}$", "ours"), ] BENCHMARKS = ["AIME 2024", "Minerva Math", "OlympiadBench", "AMC", "MATH500"] def method_colors(): members = defaultdict(list) for name, group in METHODS: members[group].append(name) shades = { "base": [MUTED] * len(members["base"]), "prior": family("blue", len(members["prior"])), "ours": family("red", len(members["ours"])), } return { name: shades[group][members[group].index(name)] for name, group in METHODS }main()部分的关键点:
def main(): use_style() scores = load(DATA) colors = method_colors() fig, ax = figure(width=WIDE, ratio=0.28) x = np.arange(len(BENCHMARKS)) # Near-touching bars inside a group; the gap between groups does the separating. width = 0.9 / len(METHODS) for i, (name, _) in enumerate(METHODS): offset = (i - (len(METHODS) - 1) / 2) * width bars = ax.bar( x + offset, [scores[name][benchmark] for benchmark in BENCHMARKS], width, label=name, color=colors[name], edgecolor="black", linewidth=0.4, ) # Redundant with the bar height, so it may sit at the 5pt floor. ax.bar_label(bars, fmt="%.1f", fontsize=5, padding=1.5) ax.set_xticks(x, BENCHMARKS) ax.set_ylabel("Accuracy (%)") # Derived, never hardcoded: a bar clipped at the axes edge no longer # encodes its value, which is the failure this whole reference is about. highest = max(v for row in scores.values() for v in row.values()) top = max(10, 10 * math.ceil(highest * 1.12 / 10)) ax.set_ylim(0, top) ax.set_yticks(range(0, top + 1, 10)) ax.tick_params(axis="x", length=0) ax.legend( ncol=2, loc="upper left", fontsize=6.5, frameon=True, framealpha=1.0, edgecolor="black", borderpad=0.5, ) save(fig, "figs/benchmarks")模板中几个被注释点名的设计决定值得展开:
width=WIDE(6.75 英寸):WIDE是双栏论文两栏合并宽度(figure*环境),COLUMN=3.25是单栏宽(ICML、CVPR、IEEE 风格),TEXT=5.5是单栏论文的\textwidth(NeurIPS、article),定义见 orx_figstyle.py。按最终印刷尺寸构建,而不是先画大再缩放——LaTeX 中的缩放会连文字一起缩放。use_style():设置全套 house rcParams(sans 字体栈、Type 42 字体嵌入、8pt 正文字号、7pt 刻度字号、y 轴网格、去上右 spine、Okabe-Ito 色彩循环等,见 L92-L146)。family()按方法族生成明度渐变;MUTED是基线灰。- y 轴上界是算出来的(
10 * ceil(highest * 1.12 / 10)),绝不明文硬编码:被坐标轴边缘剪切的柱子不再编码它的值——这正是整篇参考文档要防的失败模式。 save():同时写出 PDF(供\includegraphics)与 SVG(供预览),并在保存后运行审计(详见下文"交付前的审计")。
模板二:消融 delta 点图
原文档同时给出figs/ablation.py,它读取figs/ablation.csv(列:variant,seed,value):
"""Accuracy change vs. the baseline. Regenerate: python figs/ablation.py""" import csv from collections import defaultdict import numpy as np from orx_figstyle import BASELINE, COLUMN, PALETTE, diff_ci, figure, save, use_style DATA = "figs/ablation.csv" REFERENCE = "baseline" HIGHLIGHT = "ours" def load(path): runs = defaultdict(list) with open(path) as handle: for row in csv.DictReader(handle): runs[row["variant"]].append(float(row["value"])) return {variant: np.array(values) for variant, values in runs.items()} def main(): use_style() runs = load(DATA) reference = runs[REFERENCE] n_seeds = min(len(values) for values in runs.values()) effects = [ (variant, *diff_ci(values, reference)) for variant, values in runs.items() if variant != REFERENCE ] effects.sort(key=lambda row: row[1]) # ascending: best ends up on top fig, ax = figure(width=COLUMN, ratio=0.1 + 0.16 * len(effects)) ax.axvline(0, color=BASELINE, linewidth=0.8, zorder=1) for y, (variant, delta, lo, hi) in enumerate(effects): color = PALETTE["blue"] if variant == HIGHLIGHT else "#4D4D4D" ax.plot([lo, hi], [y, y], color=color, linewidth=1.0, solid_capstyle="round", zorder=2) ax.plot([delta], [y], "o", color=color, zorder=3) # Sign included: a delta figure is read for direction first. ax.annotate( f"{delta:+.2f}", xy=(hi, y), xytext=(4, 0), textcoords="offset points", va="center", fontsize=7, color=color, ) ax.set_yticks(range(len(effects)), [variant for variant, *_ in effects]) ax.set_ylim(-0.6, len(effects) - 0.4) ax.set_xlabel(f"Accuracy vs. {REFERENCE} (points)") ax.grid(axis="x") ax.grid(axis="y", visible=False) ax.tick_params(axis="y", length=0) ax.margins(x=0.18) print(f"n={n_seeds} seeds per variant — state this in the caption") save(fig, "figs/ablation")设计要点:
- 排序后最好的在最上:按效应升序排序,
effects.sort(key=lambda row: row[1]),最好的结果出现在图顶——读者第一眼看到的就是最强的主张。 - 零线用
BASELINE灰:基线/机会水平/参考线永远是灰色,颜色只属于被比较的对象(orx_figstyle.py)。 - 差值标注带符号(
f"{delta:+.2f}"):差值图首先是被用来读方向的,正负号必须显式写出。 - y 轴无刻度线、横向网格:水平点图靠 y 轴的变体名称定位,y 网格关闭、只留 x 网格,减少视觉噪声。
- 脚本末尾打印种子数(
print(f"n={n_seeds}...")),提醒你把它写进 caption——这呼应了 SKILL.md 的 Non-negotiable 第 5 条"展示不确定性,或者说清楚没有"。
把图放进论文:caption、多面板与交付前审计
caption 才是标题
论文图中禁止ax.set_title——标题会重复 caption 并偷走垂直空间(orx_figstyle的_audit甚至会检出 "axes title duplicates the caption — delete it" 并报告为问题,见 L211-L212)。多面板图用a、b 面板字母命名部件(panel_labels(),L163-L179),正文用 "Fig. 2b" 引用。
Caption 的写法(来自 orx-figures/SKILL.md 的 "Write the caption with the figure" 一节):以一句加粗的结论短语开头,然后给出读者信任它所需的细节——种子数、带状/柱状的含义、任何平滑与归一化、哪些点被拟合哪些被排除、前沿线是实测还是引导线。当前论文 caption 中位数约 28 词。针对本文主题,消融图 caption 必须写明 "n=5 seeds" 以及区间是of the difference(95% CI on the difference)。
多面板:共享坐标轴
面板共享同一数量时必须sharey=True,共享轴只在显示刻度的那个面板上带一个标签。两个面板对同一指标使用悄悄不同的区间,就是多面板版的截断柱轴——读者在比较并不可比的柱高。figure_grid(nrows, ncols, width=TEXT, sharey=True)(orx_figstyle.py)把整个网格按最终印刷宽度排版。
交付前的save()审计
save()在写文件之后运行_audit()(L190-L252),检查:印刷宽度是否为已知列宽(COLUMN/TEXT/WIDE)、Type 42 字体嵌入、多余的 axes 标题、缺失的轴标签、低于 5pt 下限的文字、文字互相重叠、文字跑出画布。输出figure audit <stem>: clean才是达标线;任何一条问题都是要修的缺陷而不是可忽略的警告。审计只打印不抛异常——"a silent pass is how an unpublishable figure reaches the paper"(一个无声的通过正是不可发表的图到达论文的途径)。
注意save()也写 SVG 预览,且不做bbox_inches="tight":按内容裁剪会改变物理宽度,毁掉"按最终尺寸构建"这一原则(见 L329-L334 的文档字符串与注释)。
运行环境与数据来源
- 运行模板:模板依赖
matplotlib与numpy。独立绘图用uv run --no-project --with matplotlib --with numpy python figs/benchmarks.py;带内联依赖元数据的脚本用uv run --no-project figs/benchmarks.py。引入项目代码的绘图必须在项目环境内运行。 - 数据必须来自真实运行:每个数字都来自一次运行,用
orx logs读取(见 agent-skills/orx-evidence/SKILL.md)。绝不画记忆中的、取整过的或看起来合理的数字,绝不把合成的 demo 数据留在会发布的脚本里。 - 样式模块就地 vendor:
mkdir -p figs && orx skill figures/assets/orx_figstyle.py > figs/orx_figstyle.py,把样式文件放在绘图脚本旁边,保证会话结束后图仍可复现;生成脚本必须与输出图放在同一目标目录。 - 图的落点与引用标签必须匹配:论文图写到工作树
figs/旁,以仓库相对路径引用(figs/benchmarks.pdf,无artifacts/前缀);报告/聊天回答中的图写到 artifacts 目录下的对应主题目录,引用时带artifacts/前缀。标签与落点不匹配是成品图到达用户时变成死链的最常见原因。 - LaTeX 侧:图以
\includegraphics[width=\linewidth]{figs/benchmarks.pdf}引入。按列宽构建并且仍写width=\linewidth:尺寸正确时它缩放为 1.0 不改变任何东西,但万一会议栏宽比你假设的窄它仍能自适应;它救不了构建尺寸错误的图——15 英寸画布丢进 5.5 英寸栏会被缩到 0.35,11pt 刻度标签落到 4pt,这正是真实论文图不可读的最常见原因。
Checklist:交付前逐项自检
- 柱子从零开始,没有任何截断。
- 色相是方法族、明度是变体;基线模型用灰色。
- 方法顺序在所有分组中一致,并与图例顺序匹配。
- 每根柱子都有标签,且标签不碰撞。
- Delta 图:区间是差值的区间,n 与区间含义写进 caption。
- 图例仅在覆盖数据时加框,并避开最高的柱子。
- 超过约 8 个变体,或需要精确数字?改用表格。
【免费下载链接】OpenResearchTurn your coding agents into research agents项目地址: https://gitcode.com/GitHub_Trending/op/OpenResearch
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考