未校准的模型分数乘以金额会变成商业case的算术错误;文章在 1000 条预测上演示诊断和修复方法,并指出 AUC 在校准后不变,但期望值计算和阈值比较必须依赖校准后的概率。
当数字必须得有含义时
对于纯粹的排序问题,Calibration 无关紧要。如果你只是把线索排序然后从列表顶部往下处理,分数可以是真实值的任意单调变换,结果列表完全相同。AUC 在重新校准后完全不会变化。
但一旦分数被用作数字而非顺序,Calibration 就变得至关重要:
期望值。“这个客户有 0.8 的概率续费 £200,所以他们价值 £160”——只有当 0.8 就是 0.8 时才成立。
基于成本的阈值。从成本矩阵推导阈值会产生一个概率,拿未校准的分数与之比较等于在比较两个不同的尺度。
模型组合。对两个方向相反地误校准的模型分数做平均或相乘,得到的产物完全无法解释。
汇报给人类。分析师被告知“72% 可能”时,会把它当作概率来行动,这是他的权利。
Gradient-boosted 模型和支持向量机通常在两端都过度自信,因为损失函数一直在奖励把高置信度行推得更远。朴素贝叶斯在构造上就严重误校准。随机森林在极端情况下通常信心不足,因为对多棵树的平均会把预测拉向中间。在良好指定的特征上训练的逻辑回归往往已经接近校准,这是它能存活下来的一个隐秘原因。
从一个 boosted 分类器取 1,000 个保留预测——模型从未见过的校准集——按预测概率分桶。以下数字是虚构的以便检查算术;形状是过度自信模型产生的那种。
bucket mean pred n positives observed rate pred - obs
0.00-0.10 0.05 300 44 0.147 -0.097
0.10-0.20 0.15 150 39 0.260 -0.110
0.20-0.30 0.25 100 34 0.340 -0.090
0.30-0.40 0.35 80 33 0.413 -0.063
0.40-0.50 0.45 70 33 0.471 -0.021
0.50-0.60 0.55 70 37 0.529 +0.021
0.60-0.70 0.65 60 36 0.600 +0.050
0.70-0.80 0.75 60 40 0.667 +0.083
0.80-0.90 0.85 60 44 0.733 +0.117
0.90-1.00 0.95 50 43 0.860 +0.090
----- -----
1000 383 base rate = 0.383
mean predicted probability = 343/1000 = 0.343
看最后一列。在低端模型信心不足:它说 5% 但交付 15%。在高端它过度自信:它说 95% 但交付 86%。预测从中间被压向两端——太尖锐了——这正是以最小化 log 损失为目标训练但没有 calibration 步骤的模型的特征。
还要注意模型排序非常好。观察率沿表格向下单调递增,所以它的 AUC 没问题,没有任何排序指标会报告问题。Calibration 是一个独立的维度,需要独立的诊断。
ECE 是预测与结果之间差距的加权平均,按每个桶中落入多少预测加权。每个桶一行:
ECE = sum over buckets of (n_b / N) * |pred_b - obs_b|
bucket weight |gap| contribution
0.05 0.300 0.097 0.0291
0.15 0.150 0.110 0.0165
0.25 0.100 0.090 0.0090
0.35 0.080 0.063 0.0050
0.45 0.070 0.021 0.0015
0.55 0.070 0.021 0.0015
0.65 0.060 0.050 0.0030
0.75 0.060 0.083 0.0050
0.85 0.060 0.117 0.0070
0.95 0.050 0.090 0.0045
-------
ECE = 0.0821
8.2 个百分点。在每个概率点值 £2 期望值的组合中,这是每决策 £16 的错误,在分数范围每一端都系统性地朝一个方向偏。
ECE 取决于分桶方式。等宽桶如上易于阅读但把大部分质量放在了第一个桶;等计数桶让每个估计有相同精度但产生不平滑的 x 值。要报告你用了哪种以及多少个桶,因为这个数字不这样就无法比较。用十个桶和 1,000 个点,最小的桶有 50 行,其观察率带着约 0.049 的标准误差——所以它的 0.090 差距是真的但不是精确的 0.090。
Brier 分数——预测与结果之间的均方误差——是另一个通常引用的数字。它是一个恰当的评分规则和良好的整体汇总,但它把 calibration 和 discrimination 混在了一起,所以不是一个好的诊断:一个模型可以通过提升排序能力来改善其 Brier 分数而同时 calibration 变差。用 Brier 比较模型,用 ECE 加可靠性表诊断一个模型。
Platt scaling 对模型的 log-odds 拟合一个一维逻辑回归:两个参数,一个斜率和一个截距。对上表的保留校准集拟合它得到大约 a = 0.60 和 b = 0.06。
z = ln( p / (1 - p) ) raw log-odds
z' = a * z + b a = 0.60, b = 0.06
p' = 1 / (1 + exp(-z')) corrected probability
raw p = 0.95:
z = ln(0.95/0.05) = ln(19) = 2.944
z' = 0.60(2.944) + 0.06 = 1.827
p' = 1/(1 + e^-1.827) = 0.861 observed was 0.860
raw p = 0.05:
z = ln(0.05/0.95) = -2.944
z' = 0.60(-2.944) + 0.06 = -1.707
p' = 1/(1 + e^1.707) = 0.154 observed was 0.147
raw p = 0.25:
z = ln(0.25/0.75) = -1.099
z' = 0.60(-1.099) + 0.06 = -0.599
p' = 1/(1 + e^0.599) = 0.355 observed was 0.340
Recomputing ECE over all ten buckets with the corrected values:
ECE 0.082 -> 0.010
斜率 a 是可解释的参数。小于 1 时它把预测拉向中间,这是对过度自信模型的修正;大于 1 时把它们向外推。0.60 的斜率说明模型的 log-odds 大约被放大了 1.7 倍。
当校准集较小时——不到几千行——Platt scaling 是正确的默认选择,因为两个参数不会过拟合太多。它的局限性在于只能应用一个 sigmoid 形状的修正,所以如果模型的误校准不是这个形状,就会被改善但不能被彻底修正。
Isotonic regression 拟合从原始分数到校准概率的任意非递减阶梯函数。它对形状不做假设,所以可以修正任意误校准——但这种灵活性也让它在小校准集上愉快地拟合噪声,产生平坦区域和一个泛化很差的阶梯函数。
两者都必须在模型没有训练过的数据上拟合。在训练集上做校准是最赤裸的泄漏:模型在训练行上以不传递的方式过度自信,在那里学到的修正到其他所有地方都是错的。
import numpy as np
from sklearn.calibration import calibration_curve
from sklearn.isotonic import IsotonicRegression
from sklearn.linear_model import LogisticRegression
def ece(y_true, p, n_bins=10):
"""Expected calibration error with equal-width bins."""
edges = np.linspace(0.0, 1.0, n_bins + 1)
idx = np.clip(np.digitize(p, edges[1:-1]), 0, n_bins - 1)
total = 0.0
for b in range(n_bins):
m = idx == b
if m.sum() == 0:
continue
total += (m.sum() / len(p)) * abs(p[m].mean() - y_true[m].mean())
return total
# 1. hold out a calibration set the model never trained on
p_cal = model.predict_proba(X_cal)[:, 1]
p_test = model.predict_proba(X_test)[:, 1]
print("raw ECE:", ece(y_test, p_test))
# 2a. Platt: logistic regression on the raw log-odds
eps = 1e-6
z_cal = np.log(np.clip(p_cal, eps, 1 - eps) / (1 - np.clip(p_cal, eps, 1 - eps)))
platt = LogisticRegression().fit(z_cal.reshape(-1, 1), y_cal)
a, b = float(platt.coef_[0][0]), float(platt.intercept_[0])
print(f"slope a = {a:.3f}, intercept b = {b:.3f}")
z_test = np.log(np.clip(p_test, eps, 1 - eps) / (1 - np.clip(p_test, eps, 1 - eps)))
p_platt = platt.predict_proba(z_test.reshape(-1, 1))[:, 1]
print("platt ECE:", ece(y_test, p_platt))
# 2b. isotonic
iso = IsotonicRegression(out_of_bounds="clip").fit(p_cal, y_cal)
p_iso = iso.predict(p_test)
print("isotonic ECE:", ece(y_test, p_iso))
# 3. the reliability table itself
obs, pred = calibration_curve(y_test, p_test, n_bins=10, strategy="uniform")
for pr, ob in zip(pred, obs):
print(f"predicted {pr:.3f} observed {ob:.3f} gap {pr - ob:+.3f}")
Scikit-learn 还提供了 CalibratedClassifierCV,它包装一个分类器并为你处理交叉拟合。它是便捷的选择;检查你安装版本中预拟合估计器的传入方式,因为该参数在版本间有变化,错误传入会静默地重新拟合基础模型。
它不改善排序。这里所有方法都是单调的,所以 AUC 在数值误差范围内不变。如果模型无法分离类别,对它做校准产生的是所有值都接近基准率的诚实概率,这是正确但无用的。
它无法在分布漂移中存活。修正是针对一个人群拟合的。当输入分布移动时,calibration 随之漂移——所以 calibration 属于监控集并需要重新拟合,这通常是最便宜的漂移响应方式,因为它不需要重新训练。
它修正不了错误的基准率。如果训练集被重采样来平衡类别,模型的概率是按重采样的基准率而非真实基准率。在正确采样的保留集上做校准确实能修正这个问题,这也是不要重采样的又一个原因——阈值调优通常胜于重采样。
它不是每个预测的置信度。ECE 是跨桶的汇总。一个模型可能在整体上完全校准但在某个子群体上严重误校准;对你关心的分段单独检查可靠性表,因为汇总数字会掩盖一个系统性地错误的分段。