概述
概率校准是机器学习中的核心问题:一个良好校准的分类器应该这样表述——当它预测某个样本属于某类别的概率为 时,这个预测正确的频率应该约为 。
例如,如果我们用同一个校准良好的模型处理100个预测概率为0.7的样本,我们期望大约70个预测是正确的。
本系统介绍概率校准的理论基础、评估指标、经典方法和深度学习中的校准技术。12
校准的定义
形式化定义
完美校准(Calibration):
其中 是预测概率, 是真实标签。
条件校准与边际校准
| 类型 | 定义 | 适用场景 |
|---|---|---|
| 边际校准 | 聚合预测 | |
| 条件校准 | 单个样本 |
深度学习通常关注边际校准。
可靠性图(Reliability Diagram)
import numpy as np
import matplotlib.pyplot as plt
def reliability_diagram(y_true, y_prob, n_bins=10):
"""
绘制可靠性图
横轴:预测置信度分箱
纵轴:真实准确率
完美校准:45度直线
"""
bins = np.linspace(0, 1, n_bins + 1)
bin_indices = np.digitize(y_prob, bins) - 1
confidences = []
accuracies = []
counts = []
for i in range(n_bins):
mask = bin_indices == i
if mask.sum() > 0:
confidences.append((bins[i] + bins[i+1]) / 2)
accuracies.append(y_true[mask].mean())
counts.append(mask.sum())
# 绘制
fig, ax = plt.subplots(figsize=(8, 6))
# 对角线(完美校准)
ax.plot([0, 1], [0, 1], 'k--', label='Perfect calibration')
# 可靠性曲线
ax.bar(confidences, accuracies, width=0.1, alpha=0.6, label='Model')
# 理想点
ax.scatter(confidences, accuracies, c='red', s=100, zorder=5)
ax.set_xlabel('Confidence')
ax.set_ylabel('Accuracy')
ax.set_title('Reliability Diagram')
ax.legend()
ax.grid(True, alpha=0.3)
return fig校准评估指标
期望校准误差(ECE)
定义:预测置信度与实际准确率之间加权绝对差:
其中:
- :第 个置信度分箱
- :分箱内准确率
- :分箱内平均置信度
def expected_calibration_error(y_true, y_prob, n_bins=10):
"""
计算ECE
"""
bins = np.linspace(0, 1, n_bins + 1)
bin_indices = np.digitize(y_prob, bins) - 1
ece = 0.0
for i in range(n_bins):
mask = bin_indices == i
if mask.sum() > 0:
bin_conf = y_prob[mask].mean()
bin_acc = y_true[mask].mean()
ece += mask.sum() * abs(bin_conf - bin_acc)
return ece / len(y_true)ECE的变体
| 变体 | 定义 | 特点 |
|---|---|---|
| 确定性ECE | 等间距分箱 | 简单但受分箱数影响 |
| 自适应ECE | 样本驱动分箱 | 更稳定 |
| MCE(最大校准误差) | $\max_m | \text{acc}(B_m) - \text{conf}(B_m) |
| 校准风险(CR) | 加权MSE | 凸目标,更易优化 |
负对数概率校准(NLL)
优点:概率敏感、理论充分
缺点:过度惩罚过度自信的错误
Brier分数
分解:
Norris指数
专门用于多分类问题:
深度学习中的校准问题
Modern CNN的不校准
发现(Guo et al., ICML 2017):现代神经网络(特别是使用softmax输出的网络)往往严重不校准。
原因分析:
| 因素 | 影响 |
|---|---|
| 模型容量 | 更大的模型更容易过度自信 |
| 权重衰减 | 降低权重衰减通常改善校准 |
| 批量归一化 | 可能损害校准 |
| 温度参数 | softmax温度影响校准 |
过拟合与校准的关系
观察:验证集上的过拟合往往伴随着校准恶化。
def analyze_calibration_during_training(model, train_loader, val_loader):
"""
分析训练过程中的校准变化
"""
train_nll = []
train_ece = []
val_nll = []
val_ece = []
for epoch in range(max_epochs):
# 训练...
# 评估训练集校准
train_preds = predict(model, train_loader)
train_nll.append(negative_log_likelihood(train_preds))
train_ece.append(expected_calibration_error(train_preds))
# 评估验证集校准
val_preds = predict(model, val_loader)
val_nll.append(negative_log_likelihood(val_preds))
val_ece.append(expected_calibration_error(val_preds))
return {
'train_nll': train_nll,
'train_ece': train_ece,
'val_nll': val_nll,
'val_ece': val_ece
}经典校准方法
1. 温度调节(Temperature Scaling)
最简单的后处理校准方法。只调节softmax的温度参数 :
class TemperatureScaling:
"""
温度调节校准
最简单有效的校准方法
"""
def __init__(self):
self.temperature = 1.0
def fit(self, logits, y_true):
"""
通过最小化NLL找到最优温度
"""
def nll_loss(T):
scaled_logits = logits / T
softmax = np.exp(scaled_logits) / np.exp(scaled_logits).sum(axis=1, keepdims=True)
return -np.mean(np.log(softmax[np.arange(len(y_true)), y_true]))
# 优化
from scipy.optimize import minimize_scalar
result = minimize_scalar(nll_loss, bounds=(0.1, 10.0), method='bounded')
self.temperature = result.x
return self
def calibrate(self, logits):
"""应用校准"""
scaled_logits = logits / self.temperature
return np.exp(scaled_logits) / np.exp(scaled_logits).sum(axis=1, keepdims=True)2. Platt Scaling
将logits通过逻辑回归映射到校准概率:
其中 是sigmoid函数, 是学习参数。
class PlattScaling:
"""
Platt Scaling
使用逻辑回归拟合校准函数
"""
def __init__(self):
self.a = 1.0
self.b = 0.0
def fit(self, logits, y_true):
"""
拟合Platt参数
"""
from sklearn.linear_model import LogisticRegression
lr = LogisticRegression(C=1e10) # 高正则化避免过拟合
lr.fit(logits, y_true)
self.a = lr.coef_[0][0]
self.b = lr.intercept_[0]
return self
def calibrate(self, logits):
"""应用Platt校准"""
z = self.a * logits + self.b
return 1 / (1 + np.exp(-z))3. Isotonic Regression
非参数化的校准方法,保持单调性约束:
class IsotonicCalibration:
"""
Isotonic Regression校准
非参数化、保序
"""
def __init__(self):
self.fitter = None
def fit(self, probs, y_true):
"""
拟合保序回归
"""
from sklearn.isotonic import IsotonicRegression
self.fitter = IsotonicRegression(y_min=0, y_max=1, out_of_bounds='clip')
self.fitter.fit(probs, y_true)
return self
def calibrate(self, probs):
"""应用校准"""
return self.fitter.predict(probs)方法对比
| 方法 | 参数量 | 表达能力 | 优点 | 缺点 |
|---|---|---|---|---|
| 温度调节 | 1 | 线性 | 简单、不易过拟合 | 只调温度 |
| Platt Scaling | 2 | 线性 | 简单、可解释 | 表达能力有限 |
| Isotonic | 可变 | 分段常数 | 非参数、灵活 | 可能过拟合 |
| Beta校准 | 2 | 非线性 | 参数少、灵活 | 需要优化 |
深度学习中的校准技术
1. 标签平滑(Label Smoothing)
将硬标签 替换为软标签 :
class LabelSmoothingCrossEntropy(nn.Module):
"""
标签平滑的交叉熵损失
"""
def __init__(self, smoothing=0.1):
super().__init__()
self.smoothing = smoothing
def forward(self, pred, target):
n_classes = pred.size(-1)
# 软标签
smooth_target = torch.zeros_like(pred)
smooth_target.fill_(self.smoothing / n_classes)
smooth_target.scatter_(1, target.unsqueeze(1), 1 - self.smoothing + self.smoothing / n_classes)
# 交叉熵
log_probs = F.log_softmax(pred, dim=-1)
loss = -(smooth_target * log_probs).sum(dim=-1).mean()
return loss2. Focal Loss
通过调制因子降低易分类样本的权重:
class FocalLoss(nn.Module):
"""
Focal Loss用于类别不平衡和校准
"""
def __init__(self, alpha=1, gamma=2):
super().__init__()
self.alpha = alpha
self.gamma = gamma
def forward(self, pred, target):
ce_loss = F.cross_entropy(pred, target, reduction='none')
p_t = torch.exp(-ce_loss)
focal_loss = self.alpha * (1 - p_t) ** self.gamma * ce_loss
return focal_loss.mean()3. Mixup增强
通过对样本进行线性插值,隐式地改善校准:
def mixup_data(x, y, alpha=1.0):
"""
Mixup数据增强
"""
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1
batch_size = x.size(0)
index = torch.randperm(batch_size).to(x.device)
mixed_x = lam * x + (1 - lam) * x[index]
y_a, y_b = y, y[index]
return mixed_x, y_a, y_b, lam4. 蒙特卡洛Dropout
多次前向传播估计不确定性,从而改善校准:
class MCDropoutCalibration:
"""
MC Dropout校准
"""
def __init__(self, model, num_samples=30):
self.model = model
self.num_samples = num_samples
def predict_with_uncertainty(self, x):
"""多次采样预测"""
self.model.train() # 保持dropout
probs_list = []
for _ in range(self.num_samples):
with torch.no_grad():
logits = self.model(x)
probs = F.softmax(logits, dim=-1)
probs_list.append(probs)
self.model.eval()
probs_stack = torch.stack(probs_list)
mean_probs = probs_stack.mean(dim=0)
# Monte Carlo估计不确定性
epistemic_unc = probs_stack.var(dim=0).sum(dim=-1)
return mean_probs, epistemic_unc多分类校准
矩阵缩放
将logits通过一个可学习的变换矩阵 映射:
class MatrixScaling(nn.Module):
"""
矩阵缩放校准
"""
def __init__(self, n_classes):
super().__init__()
self.W = nn.Parameter(torch.eye(n_classes))
self.b = nn.Parameter(torch.zeros(n_classes))
def forward(self, logits):
return F.linear(logits, self.W, self.b)Dirichlet校准
将分类器视为预测Dirichlet参数:
其中 是特征提取器, 是Dirichlet分布参数。
LLM校准
Token级校准
class TokenCalibrator:
"""
LLM Token级校准器
"""
def __init__(self, model):
self.model = model
self.temperature = 1.0
def calibrate_temperature(self, calibration_data):
"""
使用校准数据调节温度
"""
total_nll = 0
total_count = 0
for prompt, completion in calibration_data:
logits = self.model.get_token_logits(prompt, completion)
for i, token in enumerate(completion):
token_logit = logits[i]
token_id = self.model.tokenizer.encode(token)[0]
# 计算NLL
total_nll -= token_logit[token_id].item()
total_count += 1
# 估计最优温度
avg_nll = total_nll / total_count
self.temperature = np.sqrt(avg_nll) # 简化的温度估计
return self.temperature答案级校准
def calibrate_answer_probability(prompt, answers, model):
"""
估计答案的正确概率(而非token概率)
"""
consistency_scores = []
for _ in range(10):
# 生成多次采样
response = model.generate(prompt, temperature=0.7)
consistency_scores.append(response in answers)
# 一致性作为置信度
return sum(consistency_scores) / len(consistency_scores)实践指南
何时需要校准
✅ 需要校准的场景:
- 概率用于决策(如置信度阈值)
- 与其他模型/系统集成
- 不确定性量化需求
- 风险评估应用
❌ 可能不需要的场景:
- 只关心top-1预测
- 后续有额外的优化步骤
- 评估指标本身不依赖概率
校准流程
校准最佳实践
├── 1. 分离数据
│ ├── 训练集:训练模型
│ ├── 校准集:拟合校准参数
│ └── 测试集:评估校准效果
├── 2. 选择校准方法
│ ├── 简单应用 → 温度调节
│ ├── 多分类 → Isotonic Regression
│ └── 深度网络 → Beta校准
├── 3. 评估
│ ├── ECE < 0.01: 优秀
│ ├── ECE < 0.05: 良好
│ └── ECE > 0.10: 需要改进
└── 4. 验证
└── 在独立测试集上验证校准效果
常见陷阱
| 陷阱 | 描述 | 解决方案 |
|---|---|---|
| 数据泄露 | 校准集与测试集重叠 | 严格数据分离 |
| 分箱数选择 | ECE对分箱数敏感 | 报告多种分箱数 |
| 过度校准 | 在小数据集上过拟合 | 使用正则化 |
| 忽略基率变化 | 分布偏移影响校准 | 定期重新校准 |
参考
相关阅读
- LLM不确定性量化 — 大模型时代的不确定性
- 贝叶斯估计理论基础 — 概率论基础
- Laplace近似 — 贝叶斯后验近似
- MC Dropout — 实用的贝叶斯近似方法