如何阅读一篇机器学习论文
翻开论文,第一页可能就出现十几个缩写。往后翻,公式越来越长,表格里还挤满了你没有听过的模型。读了半小时,似乎每句话都认识,合起来却说不清作者究竟做了什么。
这很常见。论文通常写给已经熟悉问题的同行:背景讲得快,很多实现细节藏在附录里,作者还希望用有限篇幅突出自己的贡献。我们不必照着它的排版顺序,一页一页硬啃。
这一章一起读一篇具体论文。我们会带着一个问题进去,沿着公式、图表和代码来回寻找答案,再把其中一个说法变成能运行的计算。读到最后,希望你能合上论文,用自己的话讲清楚:它想解决什么,做了什么,哪些证据支持它,还有什么没有弄明白。
1. 先选一个真的想知道答案的问题
假设你训练了一个图片分类器。它说:“这张图是猫,我有 90% 的把握。”
我们以前常问的是“猫猜对了吗”。现在换个问题:把所有声称有九成把握的预测放在一起,真的大约有九成正确吗?
如果一百次里只有六十次正确,模型的语气就比表现更自信。即使它仍是候选分类器中准确率最高的那个,也不意味着它报出的概率值得照单全收。这就是置信度校准关心的问题。
本章精读的是 Chuan Guo、Geoff Pleiss、Yu Sun 和 Kilian Q. Weinberger 在 ICML 2017 发表的 《On Calibration of Modern Neural Networks》。阅读对象固定为 PMLR 会议版,正文与补充材料分开提供。它研究分类模型的置信度,并比较几种训练后的校准方法,其中一种叫温度缩放。
这篇论文适合接在前面的章节之后:分类、softmax、交叉熵和验证集都已经见过。陌生的部分集中在“怎样判断概率报得准不准”,不需要同时补上好几门新课。
打开论文时,顺手留下标题、作者、年份、版本和原文地址。只记一个模型简称,过几个月很容易找错材料;预印本、会议版和后续修改版的图表编号也可能不同。这里使用会议版正文和单独的补充材料,后文的编号都按这两个文件。
2. 阅读顺序,可以跟着问题走
第一次阅读,可以先看标题、摘要、引言末尾和结论,写下一句朴素的问题:
分类器的概率是否过于自信?能否在保留原有分类器的情况下,让这些概率更符合实际正确率?
这一步先不要求理解每个公式。摘要是作者对工作的概括,还不是你已经核实的结论。遇到“有效”“优于”“简单”时,给自己留个小问号:在哪些数据上有效,和谁比较,简单到什么程度?
接着跳到主要图表。看横纵轴、表头、数据集名称和图注,找出作者用什么东西回答这个问题。在本例里,结果表报的是校准误差;如果把它当成分类错误率,后面整段理解都会跑偏。
然后回到方法,沿着一条结果追踪:模型原来输出什么,新增方法改变了哪里,参数从什么数据中得到,最后怎样评分。等这条路径连起来,再补与它有关的推导、相关工作和附录。
这不是固定的“三遍阅读法”。碰到自己很熟悉的论文,也许可以直接看实验;遇到全新领域,可能要先读一篇入门材料。顺序的作用是减少无目的地翻页,不是给阅读增加一套打卡流程。
可以随手写三个短栏目:
| 已经弄清楚 | 还不明白 | 接下来去哪里找 |
|---|---|---|
| 方法作用于分类分数之后 | 为什么不改变类别? | 温度缩放公式 |
| 表中主要报告 ECE | 它怎样把很多样本合成一个数? | 正文第 2 节 |
| 温度需要拟合 | 是否使用了测试标签? | 方法、实验设置与代码 |
“还不明白”最好写成能回答的问题。写“数学太难”很难推进;写“这里为什么要对类别求和,而不是对样本求和”,就已经找到了下一步。
3. 把问题、方法和结论分开摆好
读完大意后,可以试着讲出下面这条关系:
原来的分类器报出置信度 → 我们怀疑它与实际正确率不匹配 → 增加一个可拟合的温度 → 在保留数据上检查概率与正确率。
其中有几件事需要分开。
研究问题是想弄清楚什么,例如准确率与置信度是否相配。方法是做了什么,例如把所有类别分数除以一个正数。实验结果是某个设置下实际观察到什么。解释或假设则是在说为什么可能如此。
还有一种容易混淆的“假设”:它可能是作者准备检验的猜想,也可能是方法成立所依赖的条件。比如“一个温度是否足以改善校准”可以做实验;“校准数据能代表后续使用情形”则是把结果用于新样本时需要考虑的条件。后者不能靠在校准集上得到漂亮分数就自动满足。
本章会用两句话练习这种区分:
- 共同的正温度不会改变哪个类别分数最大。
- 在校准集上拟合温度以后,任意新数据上的校准误差都会下降。
第一句可以从运算性质推出;第二句强得多,需要面对数据变化和有限样本,而且我们稍后就能构造反例。两句话都出现“温度”,证据要求却完全不同。
4. 读公式时,先问每个数从哪里来
论文中的公式不必一眼看懂。先把符号换回计算过程,往往就顺了。
设一个样本有 个候选类别。分类器最后输出的原始分数为:
它还不是概率,各项可以为负,也不必加起来等于 1。原来的 softmax 概率为:
温度缩放在 softmax 前增加一步:
本例把原始概率写成 ,缩放后的概率写成 。这是为了方便对照;读论文时要以作者对符号的定义为准,不必要求每篇文章都用同一套字母。
| 符号 | 在一次预测里是什么 | 形状或范围 |
|---|---|---|
| 分类器输出的原始分数 | 长度为 的向量 | |
| 所有类别共用的温度 | 一个正数 | |
| 缩放后的类别概率 | 长度为 ,和为 1 | |
| 概率最高的类别编号 | 个编号之一 | |
| 这个最高类别的概率 | 到 之间 |
正文的温度公式还在概率外面取了一个最大值,得到 。整条概率向量、最高概率、最高概率对应的类别,是三个不同对象。 这是值得在纸上圈出来的地方。
把三个数放进去试试
令原始分数为 ,分别使用三个温度:
| 温度 | 类别 0 | 类别 1 | 类别 2 |
|---|---|---|---|
| 0.5 | 0.867 | 0.117 | 0.016 |
| 1.0 | 0.665 | 0.245 | 0.090 |
| 2.0 | 0.506 | 0.307 | 0.186 |
图 1:数值由本章代码计算,保留三位小数,所以显示值的和可能稍有舍入误差。它们不是论文的实验数据。
时没有改变; 时差距变小,分布更平缓; 时差距放大,分布更尖。把温度调大并不自动等于“更正确”:原来若已经不够自信,继续调大反而可能更糟。
第一句判断现在可以直接检查。对于任意两个类别,如果 ,由于 :
除以相同正数、取指数、除以相同正分母,都不改变大小顺序。因此,最大类别保持不变。原来并列最大时,并列关系也保留;实现还需要使用一致的并列处理规则。
这支持的是通常的 top-1 分类结果不变。如果业务规则是“最高概率超过 0.9 才自动处理”,温度改变概率后,自动处理的数量仍可能变化。读到“不改变预测”时,要先弄清论文所说的预测是哪一种决定。
5. 从公式画出一条数据流
公式明白以后,可以自己画张小图。即使原文已经有图,重新画一遍也能暴露理解中的空缺。
图 2:按本章的理解自绘温度缩放过程。橙色部分是新增的可拟合量;它不重新训练前面的分类器。
这里的关键不在于方框画得漂亮,而在于箭头有没有解释清楚。
模型已经训练好。我们让它给一份带真实标签的校准集打分,然后只调整 。为避免与模型训练时的验证用途混淆,本章把用于拟合这个温度的数据明确叫校准集。
假设有 条校准样本,真实类别是 ,缩放后分配给真实类别的概率是 。拟合目标使用平均负对数似然:
这就是以前见过的平均交叉熵。这里的可训练对象只有温度,分类器的参数保持不变。对固定样本数,使用损失总和或平均值会得到相同的最优温度,但报告数值时仍要说明采用哪个口径。
原文在验证集上最小化 NLL 来选择温度。注意:拟合目标是 NLL,不是结果表中的 ECE。 一个方法可以按某个目标训练,再用多个指标评价;不要看到表头就反过来猜它的训练损失。
换到新的测试样本时,分类器和温度都应已经固定。若看完测试标签再调整温度,得到的就不再是未参与拟合的数据上的评价。至于校准集大小、重复使用验证集和随机划分会带来什么影响,后面两章会继续做实验。
6. 先读表头,再看最小的数字
论文表 1 的主要指标是 ECE,即期望校准误差的一种分箱估计。先按预测的最高概率把样本分组,比如把 的预测放进同一组。
对于第 组,记:
- 为样本数;
- 为这一组实际正确的比例;
- 为这一组最高预测概率的平均值。
然后计算:
空组不贡献这一项。每组先算“说得多自信”与“实际有多准”的差,再按样本数加权。十条预测都报 0.9,实际八条正确,那么这一组的差就是:
ECE 较小不等于分类错误较少,也不等于每个样本或每类人群都可靠。分组会汇总不同样本,正负偏差可能在组内抵消;换分箱数,数字也可能变。阅读时需要连同分箱约定一起记下来。
下面只摘取原文表 1 的三行和两个方法列,用来练习阅读;不是完整方法排名。原表使用 15 个等宽区间,数值按百分数显示:
| 数据与模型 | 未校准 ECE | 温度缩放 ECE |
|---|---|---|
| CIFAR-10,ResNet 110 | 4.60% | 0.83% |
| CIFAR-100,ResNet 110 | 16.53% | 1.26% |
| Reuters,DAN 3 | 0.85% | 0.91% |
这些数字来自正文表 1,PDF 第 6 页,由原论文作者报告;本章没有运行这些网络。
第一眼可以看见两行下降、一行略升。接着还有几件值得追问的事。
CIFAR-100 那一行下降了 个百分点,不是准确率提高了 15.27 个百分点。Reuters 的变化则提醒我们,不能把“很多设置有效”改写成“每个数据集必然改善”。
再回到完整表格:CIFAR-10 的 ResNet 110 那一行,温度缩放也不是所有列中最小的数。只抽取两列,可以说明相对未校准模型的变化,不能据此宣布它赢过所有校准方法。
最后看不见的部分:这张表没有给每个结果附上重复运行的波动范围。仅凭表格,我们无法判断一处很小的差距是否会随着划分或种子改变。它提供了实测点,不自动提供误差条、显著性或跨任务保证。
图又能补充什么
可靠性图把分组置信度与分组正确率放在一起,能看出偏差主要出现在哪个置信区间。读图时先确认两条轴、分组规则和柱子的含义,再看离对角线多远。
还要问每组有多少样本:一个只有两条记录的组与一个有两千条记录的组,画出的柱子可能一样宽。没有样本计数,就很难判断某一处偏离是否稳定。原文第 2 节也提醒,可靠性图本身并不显示各组的样本占比。画自己的图时,可以额外标出计数。
7. 附录与代码,是回来查细节的地方
主文没有回答的问题,不一定真的缺失。先顺着文中的引用去找补充材料。
例如,我们刚证明正温度不会改变最大类别,那么实验里分类错误率怎样?补充表 S2,PDF 第 3 页单独报告了分类错误率。CIFAR-100 的 ResNet 110 在未校准和温度缩放下同为 27.83%。它和主表中 ECE 的变化并不矛盾:类别没变,概率变了。
补充材料还把校准指标的讨论、温度缩放的推导和更多可靠性图放在不同小节。第一遍阅读不必把每个推导全部重做,但应知道:自己需要的论据在哪一节,它依赖哪些条件。如果暂时跳过证明,可以记“理解了方法的计算形式,尚未核对这一推导”,比含糊地写“已读懂”更有用。
代码可以帮忙回答另一些问题。例如,“公式中的分数”究竟是 softmax 前还是之后的数?模型在收集校准分数时是否处于评估模式?校准指标的边界点算进哪一格?
作者提供了演示仓库。本章核对的代码提交为 ce1154ec7d5235eee046f28875c3dd8421f11d45;其中 temperature_scaling.py使用 logits 输入、NLL 拟合温度,并另外计算 ECE。仓库已经标明停止维护,所以它适合用来理解作者的实现选择,不能假设旧环境命令在今天直接可用。
代码与论文也可能存在实现层面的差别。正文要求 ,作者这份演示把 直接作为可优化参数;读代码时仍要检查优化过程是否满足这个条件。本章的小程序明确只搜索正温度区间。我们没有复制原仓库的训练流程,也不会因为写出相同的一行公式,就称自己已经复现整篇论文。
如果使用搜索工具或语言模型辅助阅读,可以让它帮忙解释一个符号、列出待核查的问题。表格数字、实验条件和作者究竟说了什么,仍要回到原文。一个流畅的解释也可能混用了不同版本,或替作者补上并不存在的实验。
8. 让一句说法接受一次小检查
现在动手检验前面那两句话。程序只用 Python 标准库,不下载模型。
uv run --python 3.12 python src/examples/calibration_reading.py
前面三个类别的概率可以直接计算。接下来构造十条二分类记录,每条的原始分数都为 。由于 ,模型始终给类别 0 分配 的概率。
在校准组中,八条真实类别是 0,两条是 1。我们只用这十条记录选择温度。代码在事先给定的 范围内,按平均 NLL 搜索;在对数温度轴上不断缩小区间,保证搜索过程中温度始终为正。
这个小例子还能手算检查。若十条记录都使用同一个类别 0 概率 ,损失为:
在 内求导并令它为零:
当概率趋近 0 或 1 时,这个例子的损失都会趋向无穷;区间内只有这一个导数为零的点,因此它就是最低点。而 ,所以:
程序得到同一个温度。校准组上的概率由 0.9 变为约 0.8,准确率仍是 0.8,NLL 与 ECE 都下降。
接着单独构造一个反例组:仍是相同的原始分数,但这十条记录里有九条真实类别为 0。它不参与选择温度。此时原来的 0.9 正好符合这组的正确比例;套用刚才选出的温度,概率却被降到了约 0.8。
完整输出是:
T=0.5:三类概率=[0.867, 0.117, 0.016];最高分类别=0
T=1.0:三类概率=[0.665, 0.245, 0.090];最高分类别=0
T=2.0:三类概率=[0.506, 0.307, 0.186];最高分类别=0
十条手工校准样本:拟合 T=1.584963
校准组:准确率 0.800 → 0.800;NLL 0.5448 → 0.5004;ECE 0.100 → 0.000
另构造的反例组:准确率 0.900 → 0.900;NLL 0.3251 → 0.3618;ECE 0.000 → 0.100
这是手工分数的公式实验,没有训练分类器,也没有复现论文的基准成绩。
图 3:虚线温度只用八条正确、两条错误的校准组确定。另一组曲线用于展示反例,没有参与选择温度。
两组数据是专门构造出来的,不是从真实使用场景独立采集的测试集。因此,它们不能估计温度缩放在实际应用中改善的概率。它们能完成两件更小但清楚的工作:检查公式和解析解是否一致;推翻“对任意新数据必然改善”的过强说法。
这也没有推翻原论文报告的实测结果。一个反例否定的是全称保证,不会让别人已经做过的实验凭空消失。读论文时,既要防止把结果吹得太大,也要避免用不相干的失败否定整个方法。
展开完整计算代码
"""第 44 章:把温度缩放公式变成可检查的计算;数据全为手工构造。"""
from argparse import ArgumentParser
from hashlib import sha256
import json
from math import ceil, exp, isfinite, log, sqrt
from pathlib import Path
import platform
def scaled_logits(logits, temperature):
if not isfinite(temperature) or temperature <= 0:
raise ValueError("温度必须为有限正数")
if len(logits) < 2 or not all(isfinite(z) for z in logits):
raise ValueError("至少需要两个有限类别分数")
# 先减最大值,避免所有分数同时很大时的指数溢出。
maximum = max(logits)
return [(z - maximum) / temperature for z in logits]
def probabilities(logits, temperature=1.0):
values = [exp(z) for z in scaled_logits(logits, temperature)]
total = sum(values)
return [v / total for v in values]
def nll(logits, labels, temperature=1.0):
if not logits or len(logits) != len(labels):
raise ValueError("分数和标签必须非空、长度相同")
losses = []
for row, label in zip(logits, labels):
if not isinstance(label, int) or not 0 <= label < len(row):
raise ValueError("标签必须是有效类别编号")
z = scaled_logits(row, temperature)
losses.append(log(sum(exp(v) for v in z)) - z[label])
return sum(losses) / len(losses)
def metrics(logits, labels, temperature=1.0, bins=15):
loss = nll(logits, labels, temperature)
if not isinstance(bins, int) or bins < 1:
raise ValueError("分箱数必须为正整数")
rows = []
for row, label in zip(logits, labels):
p = probabilities(row, temperature)
prediction = max(range(len(row)), key=row.__getitem__)
confidence = p[prediction]
# 第 m 格为 (m/M, (m+1)/M],m 从 0 开始。
bucket = min(bins - 1, max(0, ceil(confidence * bins) - 1))
rows.append({"prediction": prediction, "label": label,
"confidence": confidence, "correct": prediction == label,
"bucket": bucket})
grouped = []
for bucket in range(bins):
selected = [r for r in rows if r["bucket"] == bucket]
if selected:
grouped.append({"bin": bucket, "count": len(selected),
"confidence": sum(r["confidence"] for r in selected) / len(selected),
"accuracy": sum(r["correct"] for r in selected) / len(selected)})
return {"nll": loss, "accuracy": sum(r["correct"] for r in rows) / len(rows),
"confidence": sum(r["confidence"] for r in rows) / len(rows),
"ece": sum(r["count"] * abs(r["accuracy"] - r["confidence"])
for r in grouped) / len(rows),
"bins": grouped, "rows": rows}
def fit_temperature(logits, labels, bounds=(0.05, 20.0)):
"""在预先给定的正温度区间,用黄金分割搜索平均 NLL 的最低点。"""
lower, upper = bounds
if not (isfinite(lower) and isfinite(upper) and 0 < lower < upper):
raise ValueError("温度边界必须为递增的有限正数")
left, right = log(lower), log(upper)
ratio = (sqrt(5) - 1) / 2
a = right - ratio * (right - left)
b = left + ratio * (right - left)
fa, fb = nll(logits, labels, exp(a)), nll(logits, labels, exp(b))
for _ in range(80):
if fa < fb:
right, b, fb = b, a, fa
a = right - ratio * (right - left)
fa = nll(logits, labels, exp(a))
else:
left, a, fa = a, b, fb
b = left + ratio * (right - left)
fb = nll(logits, labels, exp(b))
candidates = [lower, exp((left + right) / 2), upper]
return min(candidates, key=lambda t: nll(logits, labels, t))
def run():
logits = [[log(9), 0.0] for _ in range(10)]
calibration_labels = [0] * 8 + [1] * 2
counter_labels = [0] * 9 + [1]
temperature = fit_temperature(logits, calibration_labels)
analytic = log(9) / log(4)
assert abs(temperature - analytic) < 1e-6
temperatures = [0.5 + i * 0.025 for i in range(141)]
return {
"source_sha256": sha256(Path(__file__).read_bytes()).hexdigest(),
"python": platform.python_version(),
"data_kind": "hand_constructed_logits_and_labels",
"logits": logits,
"calibration_ids": [f"cal-{i}" for i in range(10)],
"counterexample_ids": [f"counter-{i}" for i in range(10)],
"calibration_labels": calibration_labels, "counterexample_labels": counter_labels,
"temperature": temperature, "analytic_temperature": analytic,
"bounds": [0.05, 20.0], "bins": 15,
"calibration": {"before": metrics(logits, calibration_labels),
"after": metrics(logits, calibration_labels, temperature)},
"counterexample": {"before": metrics(logits, counter_labels),
"after": metrics(logits, counter_labels, temperature)},
"three_class": [{"temperature": t, "probabilities": probabilities([2, 1, 0], t)}
for t in [0.5, 1.0, 2.0]],
"curve": [{"temperature": t,
"calibration_nll": nll(logits, calibration_labels, t),
"counterexample_nll": nll(logits, counter_labels, t)}
for t in temperatures],
}
def show(report):
for row in report["three_class"]:
formatted = ", ".join(f"{p:.3f}" for p in row["probabilities"])
print(f"T={row['temperature']:.1f}:三类概率=[{formatted}];最高分类别=0")
print(f"十条手工校准样本:拟合 T={report['temperature']:.6f}")
for key, name in [("calibration", "校准组"), ("counterexample", "另构造的反例组")]:
before, after = report[key]["before"], report[key]["after"]
print(f"{name}:准确率 {before['accuracy']:.3f} → {after['accuracy']:.3f};"
f"NLL {before['nll']:.4f} → {after['nll']:.4f};"
f"ECE {before['ece']:.3f} → {after['ece']:.3f}")
print("这是手工分数的公式实验,没有训练分类器,也没有复现论文的基准成绩。")
if __name__ == "__main__":
parser = ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path)
args = parser.parse_args()
result = run()
show(result)
if args.out:
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
也可下载脚本,或查看包含全部分数、标签、温度和曲线的记录。程序每次默认重新计算;加 --out runs/calibration-reading.json 可以保存自己的结果。
9. 合上论文,写一份能接着做事的笔记
笔记不用把论文缩写一遍。可以先试着写这样一段:
这篇工作关心分类模型的置信度与实际正确率是否相符。我重点读了温度缩放:冻结分类器,在保留数据上按 NLL 拟合一个共同正温度。正温度不改变类别排序,这一点可以直接推导。原文表 1 提供了多组 ECE 对照,也存在未改善的设置;补充表 S2 报告分类错误率。我的小程序目前只验证了公式和一个反例,尚未重新训练原文模型。
然后再留一个具体问题:
换成自己训练的小分类器,使用独立校准集拟合温度后,未参与拟合的数据上的 NLL 与 ECE 怎样变化?校准样本很少时,结果会不会不稳定?
这一问就能引出下一章的实验。它比“以后研究一下校准”更容易执行:对象、改变的部分、评价位置和指标都有了。
读到什么程度算暂时完成,也取决于用途。如果只是判断这篇是否值得细读,弄清问题与主要证据就够了;如果准备复现,数据划分、优化设置和代码版本还得继续追;如果准备把方法用在自己的系统里,还要检查它是否覆盖自己的输入分布和决策方式。
10. 带着几个问题再读一次
- 一篇论文报告 ECE 从 10% 降到 2%。能否说分类准确率提高了 8 个百分点?还需要查哪个指标?
- 为什么把所有类别分数除以同一个正温度,不改变 top-1 类别?如果分别除以不同的正数,还一定成立吗?
- 温度缩放在本例中按什么目标拟合?为什么“报告 ECE”不等于“直接最小化 ECE”?
- 有两组预测:第一组 4 条,平均置信度 0.6,其中 3 条正确;第二组 6 条,平均置信度 0.9,其中 3 条正确。按这两组计算的加权校准误差是多少?
- 为什么本章反例组上的 ECE 变差,不足以否定论文表 1 中的改善结果?
- 在可靠性图里,一个分组的柱子离对角线很远。除了差距大小,还应查什么?
- 若只看见一张表中的最优数字,没有随机种子、重复次数或误差范围,哪些结论应该暂缓?
- 给自己选的一篇论文写三句话:一个可回答的问题、一条对应证据、一个还没解决的疑问。为证据标上图表或章节编号。
展开参考答案与阅读提示
- 不能。ECE 衡量分组置信度与正确比例的偏差,分类错误率或准确率要另外看。温度缩放可以改变前者而保持后者不变。
- 共同的正数缩放保持分数大小关系,softmax 也保持大小关系。各类使用不同正数则可能改序,例如 分别除以 后变成 。
- 按校准集的平均 NLL 拟合。拟合目标与评价指标承担不同工作;表格展示什么指标,不能代替阅读优化目标。
- 为 。不能直接把两个差平均,因为两组样本数不同。
- 反例用的是另构造的数据,只否定“所有新数据都会改善”的保证。原文的结论应放回它的模型、数据、划分和指标条件下判断。
- 分组样本数、置信区间边界、柱子究竟表示正确率还是数量,以及是否只展示了部分区间。少量样本形成的柱子可能很不稳定。
- 暂缓声称微小差距稳定存在、具有统计显著性或能推广到别的设置。可以先如实说“作者在所报告设置中观察到这个数值”。
- 例如:“共同正温度是否改变分类结果?”对应温度公式与补充表 S2;疑问可以是“加入概率阈值拒答以后,自动处理的覆盖率怎样变化?”问题要足够具体,方便下一次阅读或实验。
下一章会把阅读笔记里的问题接到可复现的实验上。到那时,读懂一条公式只是起点:我们还要让数据、版本、配置和结果彼此对得上。