概述

概率校准是机器学习中的核心问题:一个良好校准的分类器应该这样表述——当它预测某个样本属于某类别的概率为 时,这个预测正确的频率应该约为

例如,如果我们用同一个校准良好的模型处理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 Scaling2线性简单、可解释表达能力有限
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 loss

2. 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, lam

4. 蒙特卡洛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对分箱数敏感报告多种分箱数
过度校准在小数据集上过拟合使用正则化
忽略基率变化分布偏移影响校准定期重新校准

参考


相关阅读


Footnotes

  1. Guo, C., et al. (2017). “On Calibration of Modern Neural Networks”. ICML 2017.

  2. Nixon, J., et al. (2019). “Measuring Calibration in Deep Learning”. CVPRW 2019.