跳到主要内容

模型校准

模型校准(Probability Calibration)是机器学习中一个容易被忽视但非常重要的话题。许多分类器能够输出预测概率,但这些概率是否真正反映了事件发生的可能性?模型校准确保模型输出的概率具有实际意义,这对于需要基于概率做决策的场景至关重要。

为什么需要模型校准?

概率预测的含义

当分类器输出"预测概率为 0.8"时,理想情况下应该意味着:在所有预测概率为 0.8 的样本中,确实有 80% 属于正类。

然而,许多分类器的概率输出与真实概率存在偏差。例如,一个模型可能总是给出接近 0.5 的概率,或者总是给出极端的概率值(接近 0 或 1)。

校准的重要性

决策依赖概率的场景

  • 医疗诊断:医生需要根据患病概率决定是否进行进一步检查
  • 金融风控:根据违约概率决定贷款审批和利率
  • 营销活动:根据转化概率分配营销预算
  • 异常检测:根据异常概率触发警报

在这些场景中,准确的概率比单纯的类别预测更有价值。

示例

假设模型预测某患者患病的概率是 60%。如果概率校准良好,医生可以据此做出理性的医疗决策。但如果模型实际上总是高估概率,那么"60%"可能实际上只对应 40% 的真实风险,导致过度医疗。

校准曲线

校准曲线(Calibration Curve),也称为可靠性图(Reliability Diagram),是评估模型校准质量的可视化工具。

原理

  1. 将预测概率分成若干区间(bin)
  2. 计算每个区间内的平均预测概率(x 轴)
  3. 计算每个区间内正类的实际比例(y 轴)
  4. 绘制曲线与对角线对比

解读校准曲线

  • 曲线在对角线上:校准良好
  • 曲线在对角线下方:概率预测偏高(过度自信)
  • 曲线在对角线上方:概率预测偏低(不够自信)

绘制校准曲线

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.calibration import CalibrationDisplay
import matplotlib.pyplot as plt

# 生成数据
X, y = make_classification(
n_samples=2000, n_features=20, n_informative=15,
n_redundant=3, random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.5, random_state=42
)

# 定义分类器
classifiers = {
'LogisticRegression': LogisticRegression(C=1.0),
'RandomForest': RandomForestClassifier(n_estimators=100),
'GaussianNB': GaussianNB()
}

# 绘制校准曲线
fig, ax = plt.subplots(figsize=(10, 8))

for name, clf in classifiers.items():
clf.fit(X_train, y_train)
CalibrationDisplay.from_estimator(clf, X_test, y_test, n_bins=10, name=name, ax=ax)

ax.set_title('校准曲线比较')
plt.show()

不同模型的校准特性

逻辑回归:通常校准良好,因为其损失函数直接优化概率输出。

朴素贝叶斯:倾向于将概率推向极端值(接近 0 或 1),因为假设特征条件独立,而这在实际中很少成立。

随机森林:倾向于将概率推向中间值(避免 0 和 1),因为 bagging 的方差导致极端预测困难。

支持向量机:通常呈现 S 形校准曲线,因为最大间隔方法关注边界附近的样本。

CalibratedClassifierCV

sklearn 提供了 CalibratedClassifierCV 类来校准分类器的概率输出。

校准方法

Sigmoid 校准(Platt Scaling)

使用逻辑函数对原始输出进行变换:

P(y=1f)=11+exp(A×f+B)P(y=1|f) = \frac{1}{1 + \exp(A \times f + B)}

其中 ff 是分类器的原始输出(decision_functionpredict_proba),AABB 是通过最大似然估计得到的参数。

  • 适合:样本量较小、校准误差对称的情况
  • 假设:校准曲线可以通过 sigmoid 函数校正

保序回归(Isotonic Regression)

拟合一个非参数的单调递增函数:

mini(yif^i)2s.t.f^if^j when fifj\min \sum_{i} (y_i - \hat{f}_i)^2 \quad \text{s.t.} \quad \hat{f}_i \geq \hat{f}_j \text{ when } f_i \geq f_j

  • 适合:样本量较大(>1000)、校准曲线非 S 形的情况
  • 更灵活,但更容易过拟合

基本用法

from sklearn.calibration import CalibratedClassifierCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split

# 数据
X, y = make_classification(n_samples=1000, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 未校准的随机森林
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)

# 使用 Sigmoid 校准
rf_sigmoid = CalibratedClassifierCV(rf, method='sigmoid', cv=5)
rf_sigmoid.fit(X_train, y_train)

# 使用保序回归校准
rf_isotonic = CalibratedClassifierCV(rf, method='isotonic', cv=5)
rf_isotonic.fit(X_train, y_train)

# 比较概率输出
print("未校准的概率范围:", rf.predict_proba(X_test)[:, 1].min(), "-", rf.predict_proba(X_test)[:, 1].max())
print("Sigmoid校准的概率范围:", rf_sigmoid.predict_proba(X_test)[:, 1].min(), "-", rf_sigmoid.predict_proba(X_test)[:, 1].max())
print("保序回归校准的概率范围:", rf_isotonic.predict_proba(X_test)[:, 1].min(), "-", rf_isotonic.predict_proba(X_test)[:, 1].max())

集成模式 vs 单一模式

集成模式(ensemble=True,默认)

# 集成模式:每个交叉验证折训练一个校准器
calibrated = CalibratedClassifierCV(
RandomForestClassifier(),
method='sigmoid',
cv=5,
ensemble=True # 默认
)
calibrated.fit(X_train, y_train)

# 预测时取所有校准器的平均
y_proba = calibrated.predict_proba(X_test)

优点:集成效果,略高精度 缺点:训练更慢,模型更大

单一模式(ensemble=False)

# 单一模式:使用所有数据训练一个校准器
calibrated = CalibratedClassifierCV(
RandomForestClassifier(),
method='sigmoid',
cv=5,
ensemble=False
)
calibrated.fit(X_train, y_train)

优点:训练更快,模型更小 缺点:可能精度略低

校准已训练的模型

如果模型已经训练好,需要使用独立的数据进行校准。

sklearn 1.6+ 版本:可以使用 FrozenEstimator 包装已训练的模型:

from sklearn.calibration import CalibratedClassifierCV
from sklearn.frozen import FrozenEstimator # sklearn 1.6+

# 假设有一个已训练的模型
trained_model = RandomForestClassifier(n_estimators=100)
trained_model.fit(X_train, y_train)

# 使用独立数据校准(不再重新训练模型)
calibrated = CalibratedClassifierCV(
FrozenEstimator(trained_model),
method='sigmoid'
)
calibrated.fit(X_calib, y_calib) # 只学习校准参数

sklearn 1.6 之前版本:使用 cv='prefit' 参数:

from sklearn.calibration import CalibratedClassifierCV

# 假设有一个已训练的模型
trained_model = RandomForestClassifier(n_estimators=100)
trained_model.fit(X_train, y_train)

# 使用 cv='prefit' 表示模型已训练
calibrated = CalibratedClassifierCV(
trained_model,
method='sigmoid',
cv='prefit' # 表示模型已经拟合
)
calibrated.fit(X_calib, y_calib) # 只学习校准参数
注意

cv='prefit' 方式在 sklearn 1.6 中已弃用,建议升级后使用 FrozenEstimator

重要:用于校准的数据必须与训练数据不重叠,否则会导致过拟合。

多分类问题的校准

对于多分类问题,sklearn 采用一对一(One-vs-Rest)方式进行校准:

from sklearn.datasets import load_iris
from sklearn.calibration import CalibratedClassifierCV
from sklearn.svm import LinearSVC

# 多分类数据
iris = load_iris()
X, y = iris.data, iris.target

# 线性 SVM(不直接支持 predict_proba)
svm = LinearSVC(random_state=42)

# 校准后可以输出概率
calibrated_svm = CalibratedClassifierCV(svm, method='sigmoid', cv=3)
calibrated_svm.fit(X, y)

# 现在可以输出概率
y_proba = calibrated_svm.predict_proba(X[:5])
print("类别概率:")
print(y_proba)

Temperature Scaling

对于多分类问题,sklearn 还支持温度缩放(Temperature Scaling):

P(y=kz)=exp(zk/T)jexp(zj/T)P(y=k|z) = \frac{\exp(z_k/T)}{\sum_j \exp(z_j/T)}

其中 zz 是 logits(decision_function 输出或 predict_proba 的对数),TT 是温度参数。

# Temperature Scaling(需要 sklearn 1.6+)
from sklearn.calibration import CalibratedClassifierCV

calibrated = CalibratedClassifierCV(
LogisticRegression(),
method='temperature',
cv=5
)
calibrated.fit(X_train, y_train)

Temperature Scaling 的优势

  • 只有一个参数 TT,不易过拟合
  • 天然支持多分类
  • 不改变预测结果(只改变概率分布)

评估校准质量

Brier Score

Brier Score 是评估概率预测质量的常用指标:

Brier Score=1ni=1n(piyi)2\text{Brier Score} = \frac{1}{n} \sum_{i=1}^{n} (p_i - y_i)^2

其中 pip_i 是预测概率,yiy_i 是真实标签(0 或 1)。

from sklearn.metrics import brier_score_loss

# 未校准的 Brier Score
y_proba_uncalibrated = rf.predict_proba(X_test)[:, 1]
brier_uncalibrated = brier_score_loss(y_test, y_proba_uncalibrated)

# 校准后的 Brier Score
y_proba_calibrated = rf_sigmoid.predict_proba(X_test)[:, 1]
brier_calibrated = brier_score_loss(y_test, y_proba_calibrated)

print(f"未校准 Brier Score: {brier_uncalibrated:.4f}")
print(f"校准后 Brier Score: {brier_calibrated:.4f}")
print(f"改善: {(brier_uncalibrated - brier_calibrated) / brier_uncalibrated:.2%}")

Brier Score 解读

  • 范围:[0, 1]
  • 越小越好
  • 0 表示完美预测
  • 比"盲猜"(预测基准率)更好才有意义

对数损失(Log Loss)

Log Loss=1ni=1n[yilog(pi)+(1yi)log(1pi)]\text{Log Loss} = -\frac{1}{n} \sum_{i=1}^{n} [y_i \log(p_i) + (1-y_i) \log(1-p_i)]

from sklearn.metrics import log_loss

# 计算对数损失
log_loss_uncalibrated = log_loss(y_test, y_proba_uncalibrated)
log_loss_calibrated = log_loss(y_test, y_proba_calibrated)

print(f"未校准 Log Loss: {log_loss_uncalibrated:.4f}")
print(f"校准后 Log Loss: {log_loss_calibrated:.4f}")

注意:校准不影响 AUC

通常,校准不会影响排序指标如 AUC,因为校准是单调变换(sigmoid)或近似单调变换(保序回归)。但如果需要严格保持排序和 AUC 分数,建议使用 sigmoid 校准。

完整示例:比较校准效果

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import LinearSVC
from sklearn.calibration import CalibratedClassifierCV, CalibrationDisplay
from sklearn.metrics import brier_score_loss, log_loss

# 1. 生成数据
X, y = make_classification(
n_samples=3000, n_features=20, n_informative=15,
n_redundant=3, random_state=42
)
X_train, X_calib, y_train, y_calib = train_test_split(X, y, test_size=0.5, random_state=42)

# 2. 定义模型
models = {
'Logistic Regression': LogisticRegression(C=1.0),
'Random Forest': RandomForestClassifier(n_estimators=100),
'Gaussian NB': GaussianNB(),
'Linear SVM': LinearSVC(C=1.0)
}

# 3. 训练和校准
results = {}

for name, model in models.items():
# 训练
model.fit(X_train, y_train)

# 校准(使用独立数据)
calibrated = CalibratedClassifierCV(
model, method='isotonic', cv='prefit'
)
calibrated.fit(X_calib, y_calib)

# 预测概率
if hasattr(model, 'predict_proba'):
y_proba_uncal = model.predict_proba(X_calib)[:, 1]
else:
# 对于 SVM,使用 decision_function
y_proba_uncal = (model.decision_function(X_calib) + 1) / 2 # 归一化到 [0, 1]

y_proba_cal = calibrated.predict_proba(X_calib)[:, 1]

# 计算指标
results[name] = {
'uncalibrated': {
'brier': brier_score_loss(y_calib, y_proba_uncal),
'log_loss': log_loss(y_calib, y_proba_uncal)
},
'calibrated': {
'brier': brier_score_loss(y_calib, y_proba_cal),
'log_loss': log_loss(y_calib, y_proba_cal)
},
'model_uncal': model,
'model_cal': calibrated,
'y_proba_uncal': y_proba_uncal,
'y_proba_cal': y_proba_cal
}

# 4. 可视化结果
fig, axes = plt.subplots(2, 2, figsize=(14, 12))

# 校准曲线对比
ax = axes[0, 0]
for name, data in results.items():
CalibrationDisplay.from_predictions(
y_calib, data['y_proba_uncal'],
n_bins=10, name=f"{name} (未校准)", ax=ax
)
ax.set_title('校准曲线 - 未校准模型')
ax.get_legend().remove()

ax = axes[0, 1]
for name, data in results.items():
CalibrationDisplay.from_predictions(
y_calib, data['y_proba_cal'],
n_bins=10, name=name, ax=ax
)
ax.set_title('校准曲线 - 校准后模型')

# Brier Score 对比
ax = axes[1, 0]
names = list(results.keys())
brier_uncal = [results[n]['uncalibrated']['brier'] for n in names]
brier_cal = [results[n]['calibrated']['brier'] for n in names]

x = np.arange(len(names))
width = 0.35
ax.bar(x - width/2, brier_uncal, width, label='未校准')
ax.bar(x + width/2, brier_cal, width, label='校准后')
ax.set_ylabel('Brier Score')
ax.set_title('Brier Score 对比')
ax.set_xticks(x)
ax.set_xticklabels(names, rotation=45, ha='right')
ax.legend()
ax.set_ylim(0, max(brier_uncal + brier_cal) * 1.2)

# Log Loss 对比
ax = axes[1, 1]
logloss_uncal = [results[n]['uncalibrated']['log_loss'] for n in names]
logloss_cal = [results[n]['calibrated']['log_loss'] for n in names]

ax.bar(x - width/2, logloss_uncal, width, label='未校准')
ax.bar(x + width/2, logloss_cal, width, label='校准后')
ax.set_ylabel('Log Loss')
ax.set_title('Log Loss 对比')
ax.set_xticks(x)
ax.set_xticklabels(names, rotation=45, ha='right')
ax.legend()
ax.set_ylim(0, max(logloss_uncal + logloss_cal) * 1.2)

plt.tight_layout()
plt.show()

# 5. 打印结果
print("校准效果总结:")
print("-" * 60)
print(f"{'模型':<20} {'Brier改善':<15} {'Log Loss改善':<15}")
print("-" * 60)
for name, data in results.items():
brier_improvement = (data['uncalibrated']['brier'] - data['calibrated']['brier']) / data['uncalibrated']['brier'] * 100
logloss_improvement = (data['uncalibrated']['log_loss'] - data['calibrated']['log_loss']) / data['uncalibrated']['log_loss'] * 100
print(f"{name:<20} {brier_improvement:>10.1f}% {logloss_improvement:>14.1f}%")

何时需要校准

需要校准的场景

  1. 概率用于决策:成本效益分析需要准确概率
  2. 模型输出不可靠:如 SVM、随机森林
  3. 不平衡数据:概率可能被扭曲
  4. 集成模型:多个模型融合需要可靠概率

不需要校准的场景

  1. 只关心类别预测:准确率是主要指标
  2. 模型已校准良好:如逻辑回归
  3. 数据量太小:校准可能过拟合

实践建议

# 推荐的校准流程
def train_with_calibration(model, X_train, y_train, X_calib, y_calib, method='auto'):
"""
训练模型并进行概率校准

Parameters:
-----------
method : str
'auto' - 自动选择(>1000样本用isotonic,否则sigmoid)
'sigmoid' - Sigmoid校准
'isotonic' - 保序回归
"""
# 训练模型
model.fit(X_train, y_train)

# 决定校准方法
if method == 'auto':
method = 'isotonic' if len(y_calib) > 1000 else 'sigmoid'

# 校准
calibrated = CalibratedClassifierCV(model, method=method, cv='prefit')
calibrated.fit(X_calib, y_calib)

return calibrated

# 使用示例
X_train_full, X_test, y_train_full, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
X_train, X_calib, y_train, y_calib = train_test_split(X_train_full, y_train_full, test_size=0.25, random_state=42)

model = RandomForestClassifier(n_estimators=100, random_state=42)
calibrated_model = train_with_calibration(model, X_train, y_train, X_calib, y_calib)

# 评估
y_proba = calibrated_model.predict_proba(X_test)[:, 1]
print(f"Brier Score: {brier_score_loss(y_test, y_proba):.4f}")

小结

模型校准确保概率输出具有实际意义:

  1. 校准曲线:可视化评估模型校准质量
  2. Sigmoid 校准:适合小数据集,假设 S 形校正
  3. 保序回归:更灵活,适合大数据集
  4. Temperature Scaling:多分类问题的好选择
  5. 评估指标:Brier Score 和 Log Loss 衡量概率质量
  6. 数据划分:训练、校准、测试需要独立数据

对于依赖概率做决策的应用,模型校准是不可忽视的步骤。即使分类准确率很高,如果概率不可靠,也可能导致错误的决策。