模型校准
模型校准(Probability Calibration)是机器学习中一个容易被忽视但非常重要的话题。许多分类器能够输出预测概率,但这些概率是否真正反映了事件发生的可能性?模型校准确保模型输出的概率具有实际意义,这对于需要基于概率做决策的场景至关重要。
为什么需要模型校准?
概率预测的含义
当分类器输出"预测概率为 0.8"时,理想情况下应该意味着:在所有预测概率为 0.8 的样本中,确实有 80% 属于正类。
然而,许多分类器的概率输出与真实概率存在偏差。例如,一个模型可能总是给出接近 0.5 的概率,或者总是给出极端的概率值(接近 0 或 1)。
校准的重要性
决策依赖概率的场景:
- 医疗诊断:医生需要根据患病概率决定是否进行进一步检查
- 金融风控:根据违约概率决定贷款审批和利率
- 营销活动:根据转化概率分配营销预算
- 异常检测:根据异常概率触发警报
在这些场景中,准确的概率比单纯的类别预测更有价值。
示例:
假设模型预测某患者患病的概率是 60%。如果概率校准良好,医生可以据此做出理性的医疗决策。但如果模型实际上总是高估概率,那么"60%"可能实际上只对应 40% 的真实风险,导致过度医疗。
校准曲线
校准曲线(Calibration Curve),也称为可靠性图(Reliability Diagram),是评估模型校准质量的可视化工具。
原理
- 将预测概率分成若干区间(bin)
- 计算每个区间内的平均预测概率(x 轴)
- 计算每个区间内正类的实际比例(y 轴)
- 绘制曲线与对角线对比
解读校准曲线:
- 曲线在对角线上:校准良好
- 曲线在对角线下方:概率预测偏高(过度自信)
- 曲线在对角线上方:概率预测偏低(不够自信)
绘制校准曲线
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):
使用逻辑函数对原始输出进行变换:
其中 是分类器的原始输出(decision_function 或 predict_proba), 和 是通过最大似然估计得到的参数。
- 适合:样本量较小、校准误差对称的情况
- 假设:校准曲线可以通过 sigmoid 函数校正
保序回归(Isotonic Regression):
拟合一个非参数的单调递增函数:
- 适合:样本量较大(>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):
其中 是 logits(decision_function 输出或 predict_proba 的对数), 是温度参数。
# 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 的优势:
- 只有一个参数 ,不易过拟合
- 天然支持多分类
- 不改变预测结果(只改变概率分布)
评估校准质量
Brier Score
Brier Score 是评估概率预测质量的常用指标:
其中 是预测概率, 是真实标签(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)
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}%")
何时需要校准
需要校准的场景
- 概率用于决策:成本效益分析需要准确概率
- 模型输出不可靠:如 SVM、随机森林
- 不平衡数据:概率可能被扭曲
- 集成模型:多个模型融合需要可靠概率
不需要校准的场景
- 只关心类别预测:准确率是主要指标
- 模型已校准良好:如逻辑回归
- 数据量太小:校准可能过拟合
实践建议
# 推荐的校准流程
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}")
小结
模型校准确保概率输出具有实际意义:
- 校准曲线:可视化评估模型校准质量
- Sigmoid 校准:适合小数据集,假设 S 形校正
- 保序回归:更灵活,适合大数据集
- Temperature Scaling:多分类问题的好选择
- 评估指标:Brier Score 和 Log Loss 衡量概率质量
- 数据划分:训练、校准、测试需要独立数据
对于依赖概率做决策的应用,模型校准是不可忽视的步骤。即使分类准确率很高,如果概率不可靠,也可能导致错误的决策。