分类模型输出的softmax概率经常被当作置信度来使用,但这种做法在神经网络中往往并不可靠。一个在CIFAR-10上达到95%准确率的ResNet,对部分输入会给出0.99的预测概率,但这些高置信度样本的实际正确率可能远低于99%。这种预测概率与真实正确率之间的偏差就是不确定性校准差。

概率校准为什么重要
分类模型最后的softmax输出通常被解释为每个类别的预测概率。例如一张猫的图片,模型输出[0.9, 0.08, 0.02],人们习惯认为模型有90%的把握判断这是猫。但研究表明,现代深度神经网络普遍存在过度自信的问题,softmax输出的概率值往往高于真实正确率。Guo等人在2017年的论文中系统地测量了多种网络结构,发现温度缩放之前,很多模型的ECE非常高。一个预测置信度为0.9的样本集合,实际准确率可能只有0.7甚至更低。
概率校准要求预测置信度和经验准确率保持一致。更严格地说,如果模型对一组样本都给出0.8的置信度,那么这一组样本中应该有大约80%的样本被正确分类。可以用可靠性图来直观检查:把置信度分成若干个区间,横轴是每个区间的平均置信度,纵轴是该区间内的实际准确率。完美校准的模型会落在对角线上。偏离对角线越远,说明校准越差。
除了可视化,常用的数值指标是期望校准误差ECE。ECE将置信度分为M个等宽的桶,对每个桶计算平均置信度与平均准确率之差的绝对值,再按桶内样本数加权平均。ECE越低代表校准越好。另一个指标是负对数似然NLL,它同时衡量模型是否给出正确类别的高概率,因此也能反映校准质量,但NLL相对ECE对这些高置信度错误预测更敏感。
温度缩放的核心原理
温度缩放的做法非常简单:在softmax函数内部对logits除以一个温度参数T。原始softmax计算方式是p_i = exp(z_i) / sum_j exp(z_j),温度缩放后变成p_i = exp(z_i / T) / sum_j exp(z_j / T)。当T大于1时,logits被压缩,概率分布变得更平缓,峰值降低,从而缓解过度自信;当T小于1时,分布更尖锐,模型变得更自信;当T等于1时退化为原始softmax。
一个很重要的性质是温度缩放不会改变模型的预测类别。因为当T是正数时,对logits进行单调变换并取softmax,argmax结果不会变化。这意味着温度缩放只调整概率的大小,不改变分类准确率。它对模型本身没有任何修改,只在推理阶段增加一次除法运算,因此部署成本几乎为零。
温度缩放常和知识蒸馏中的温度参数混淆。知识蒸馏使用高温软化教师模型的输出分布,让软标签包含更多类间信息,从而训练学生模型;而温度缩放是为了校准已经训练好的模型在推理阶段输出的概率,温度参数是在验证集上学习得到的,不会反向传播到模型权重。两者虽然在数学形式上相同,但用途和训练目标完全不同。下面这段代码直接实现了带温度的softmax和ECE计算,方便对比校准前后的差异。
import numpy as np
def softmax_with_temperature(logits, temperature):
"""对logits应用温度缩放后的softmax"""
scaled_logits = logits / temperature
# 减去最大值防止数值溢出
scaled_logits -= np.max(scaled_logits, axis=1, keepdims=True)
exp_logits = np.exp(scaled_logits)
return exp_logits / np.sum(exp_logits, axis=1, keepdims=True)
def compute_ece(confidences, predictions, labels, n_bins=15):
"""计算期望校准误差(ECE)"""
bin_boundaries = np.linspace(0, 1, n_bins + 1)
bin_lowers = bin_boundaries[:-1]
bin_uppers = bin_boundaries[1:]
ece = 0.0
for bin_lower, bin_upper in zip(bin_lowers, bin_uppers):
in_bin = np.where((confidences > bin_lower) & (confidences <= bin_upper))[0]
if len(in_bin) > 0:
avg_confidence = np.mean(confidences[in_bin])
avg_accuracy = np.mean(predictions[in_bin] == labels[in_bin])
ece += np.abs(avg_confidence - avg_accuracy) * len(in_bin)
return ece / len(labels)
在验证集上学习最优温度
温度参数T不能拍脑袋随便设置,需要通过数据学出来。最常用的做法是准备一个独立的校准集(通常就是验证集),固定已经训练好的模型权重,只优化T以最小化负对数似然。这是因为如果直接在训练集上学习T,模型在训练集上的softmax输出往往过于自信,学到的T会偏向大于1,但这不一定能很好泛化到新数据。而验证集独立于训练过程,能反映模型在未参与权重更新的数据上的表现。
实际工程中可以进一步把原始验证集拆分成两部分:一部分用于模型早停和超参数选择,另一部分专门用于温度缩放校准,避免校准过程再次复用同一批数据造成轻微过拟合。对于小数据集,也可以使用交叉验证的方式来估计温度。但需要注意,无论采用哪种切分方式,用来评估最终校准效果和模型准确率的测试集必须完全不参与温度学习。
在PyTorch中,温度参数可以设计成一个可训练的标量,基础模型参数全部冻结。优化器使用LBFGS或简单的SGD都可以,因为只有一个参数,收敛很快。下面的代码把温度参数放在一个包装类中,并定义了在验证集上最小化交叉熵损失的闭包函数。优化完成后打印学习到的温度值,通常会在1.0到2.5之间。
import torch
import torch.nn as nn
import torch.nn.functional as F
class TemperatureCalibratedModel(nn.Module):
def __init__(self, base_model):
super().__init__()
self.base_model = base_model
# 温度参数初始化为1.0,表示不改变原始概率
self.temperature = nn.Parameter(torch.ones(1) * 1.0)
def forward(self, x):
logits = self.base_model(x)
return logits / self.temperature
def calibrate_temperature(model, valid_loader, lr=0.01, max_iter=100):
"""在验证集上优化温度参数,最小化负对数似然"""
# 冻结基础模型参数
for param in model.base_model.parameters():
param.requires_grad = False
optimizer = torch.optim.LBFGS([model.temperature], lr=lr, max_iter=max_iter)
criterion = nn.CrossEntropyLoss()
def eval_loss():
optimizer.zero_grad()
total_loss = 0.0
total_samples = 0
for inputs, labels in valid_loader:
logits = model(inputs)
loss = criterion(logits, labels)
total_loss += loss * inputs.size(0)
total_samples += inputs.size(0)
avg_loss = total_loss / total_samples
avg_loss.backward()
return avg_loss
for step in range(max_iter):
optimizer.step(eval_loss)
print(f"Learned temperature: {model.temperature.item():.3f}")
return model
校准完成后,把同一个验证集或测试集上的置信度重新计算一遍,再调用compute_ece函数,通常可以看到ECE显著下降。例如一个ResNet在CIFAR-100上原始ECE可能超过0.1,温度缩放后可以降到0.02以下。可靠性图上的点也会明显向对角线靠拢。
与其他校准方法对比及实践建议
温度缩放不是唯一的校准方法。Platt缩放把logits输入到一个逻辑回归模型中,用验证集训练两个参数(对于二分类),多分类扩展为矩阵缩放或向量缩放。矩阵缩放为每个类别学习一个权重和一个偏置,参数数量与类别数相关;向量缩放只学习每个类别的缩放系数,参数更少。这些方法比温度缩放更灵活,能对不同类别分别调整,但参数更多,在小数据集上容易过拟合。
保序回归是一种非参数方法,它不对概率分布形状作任何假设,直接拟合一个单调递增的分段常数函数来校准概率。保序回归可以拟合非常复杂的校准曲线,但需要大量校准数据来保证效果,如果校准集很小,反而会引入噪声,导致校准后ECE变差。相比之下,温度缩放只有一个参数,假设所有类别的过度自信程度相同,虽然表达力弱,但极其稳健,适合大多数中小规模数据集的场景。
从工程角度看,建议把温度缩放作为概率校准的首选基线。如果校准集样本充足且模型的不同类别之间校准偏差差异明显,可以尝试向量缩放或矩阵缩放。保序回归在数据量超过数万条时也可以纳入考虑。无论选择哪种方法,都要定期在最新数据上重新校准,因为数据分布偏移会直接改变模型输出分布,旧温度参数可能不再适用。最后要记住,温度缩放只适用于分类模型输出的概率校准,回归任务中的不确定性通常用预测方差或分位数来刻画,不能直接套用。