机器学习项目的生命周期中,模型评估是决定是否上线的重要环节。即使模型在训练集上表现优异,如果评估方法不严谨,上线后可能造成严重偏差。常见的两类问题是:数据划分导致的测试集代表性不足,以及模型输出概率与真实概率不一致。本文聚焦如何通过分层抽样和概率校准解决这两类偏差。

评估偏差的成因:采样与分数失真
评估模型时,通常会将数据集划分为训练集和测试集。最简单的做法是随机划分,但在类别不平衡的场景下,随机划分可能引入显著的抽样误差。设想一个二分类数据集,正样本占比只有5%,随机切分后测试集可能只得到极少正样本,甚至完全没有正样本。这种情况下,准确率可能虚高到95%,但模型对少数类的识别能力完全没被反映出来。评估指标失真会让团队误判模型性能,上线后对少数类场景毫无预警能力。这就是采样偏差的核心问题:测试集无法代表真实数据分布。
另一类偏差来自模型输出的分数。很多分类器如逻辑回归、支持向量机输出的是决策分数或概率估计,但这些分数并不一定与真实事件发生率对齐。例如,一个模型预测某用户违约概率为0.8,但历史数据中同样分数段的用户实际违约率只有0.4,说明模型给出的概率过于自信。这种偏差会影响基于阈值的决策:如果业务人员根据0.5的阈值决定是否拒绝贷款,那么高估概率会导致大量误拒。所以,即使模型排序能力很好(AUC高),概率分数的绝对值偏差也会造成实际应用中的风险。
用分层抽样保障数据代表性
分层抽样的核心思想是:在划分训练集和测试集时,保持目标变量(或关键特征组)的分布比例与原始数据一致。对于分类问题,就是每个类别在训练集和测试集中所占比例相同。这样做能避免随机划分导致的类别缺失或极端不平衡,确保测试集中的少数类样本数量足够支撑可靠的指标计算。Scikit-learn中的train_test_split函数提供了stratify参数,只需传入标签数组即可实现分层划分。对于交叉验证,StratifiedKFold类可以在每一折中都保持类别比例。
下面通过一个简单的代码示例来对比随机划分和分层划分的差异。我们生成一个不平衡数据集,其中正样本占10%。
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, StratifiedKFold
import numpy as np
# 生成不平衡数据:1000个样本,正类占10%
X, y = make_classification(n_samples=1000, n_features=20, weights=[0.9, 0.1], random_state=42)
# 随机划分
X_train_rnd, X_test_rnd, y_train_rnd, y_test_rnd = train_test_split(X, y, test_size=0.2, random_state=42)
print("随机划分测试集类别分布:", np.bincount(y_test_rnd))
# 分层划分
X_train_str, X_test_str, y_train_str, y_test_str = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
print("分层划分测试集类别分布:", np.bincount(y_test_str))
运行上述代码,随机划分的测试集可能仅包含12个正样本(比例6%),而分层划分的测试集包含20个正样本(比例10%)。虽然这个差异看起来不大,但当类别更不平衡(如1%)或数据量更小时,随机划分很容易导致测试集正样本为零。分层抽样确保了每次评估都能覆盖所有类别的样本,让召回率、精确率等指标的计算有实际意义。
需要注意的是,分层抽样并非万能。对于多标签分类问题,可能需要对每个标签分别保持分布,Scikit-learn的iterative_train_test_split可以辅助处理。对于连续目标变量(回归问题),分层通常意味着将目标值离散化后再按区间分层。另外,如果数据存在时间序列特性,单纯按类别分层会破坏时间顺序,此时更适合使用时间序列划分,而不是简单的分层抽样。
概率校准让预测分数映射真实概率
概率校准的目标是调整模型输出的概率分数,使其尽可能接近真实的发生频率。衡量校准好坏常用的指标是Brier分数和可靠性曲线。Brier分数是预测概率与真实标签的均方误差,数值越低越好。可靠性曲线将预测概率分桶,画出每个桶的平均预测概率与实际正样本比例,理想情况下应该接近对角线。很多模型例如朴素贝叶斯倾向于给出极端概率,支持向量机输出到decision_function的分数通过sigmoid转换后可能出现扭曲,这些都需要校准。
常用的校准方法有两类:Platt Scaling和Isotonic Regression。Platt Scaling适用于二分类,它使用逻辑回归将原始分数映射到概率,是一种参数化方法,拟合速度快且不易过拟合。Isotonic Regression是一种非参数方法,学习一个单调递增的分段常数函数,能够拟合更复杂的分数到概率关系,但需要更多数据以避免过拟合。Scikit-learn的CalibratedClassifierCV可以方便地将任意分类器包装为带校准输出的分类器,并支持两种校准方法。
下面的代码展示了如何校准一个随机森林分类器,并比较校准前后的Brier分数。
from sklearn.ensemble import RandomForestClassifier
from sklearn.calibration import CalibratedClassifierCV, calibration_curve
from sklearn.metrics import brier_score_loss
from sklearn.model_selection import train_test_split
# 划分训练集和测试集(建议使用分层划分)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42)
# 原始随机森林
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
prob_uncal = rf.predict_proba(X_test)[:, 1]
print("未校准 Brier分数:", brier_score_loss(y_test, prob_uncal))
# 使用校准包装器,method='sigmoid'对应Platt Scaling
calibrated_rf = CalibratedClassifierCV(rf, method='sigmoid', cv=5)
calibrated_rf.fit(X_train, y_train)
prob_cal = calibrated_rf.predict_proba(X_test)[:, 1]
print("校准后 Brier分数:", brier_score_loss(y_test, prob_cal))
通常校准后的Brier分数会有所下降,说明概率估计更接近真实分布。但要注意,校准不应该在训练数据上直接进行,否则会高估校准效果。CalibratedClassifierCV内部使用交叉验证生成用于校准的预测,避免了数据泄露。另外,校准对排序能力(AUC)几乎没有影响,它改变的是概率的绝对数值。如果业务场景只需要排序,比如推荐系统,校准不是必需;但如果需要概率解释,比如风险评分、医疗诊断,校准就非常重要。
完整流程与工程实践
在实际项目中,分层抽样和概率校准往往需要结合使用,并且要防止数据泄露。一个严谨的评估流程是嵌套交叉验证:外层循环使用StratifiedKFold评估模型的泛化性能,内层循环在训练折上再进行一次交叉验证来训练校准模型。这样得到的评估分数才是无偏的。如果直接在全量训练数据上校准,然后又在同一个测试集上评估,就会把测试集的信息间接泄露给校准过程。
下面的代码展示了如何使用Pipeline将随机森林和校准整合,并在外层使用分层交叉验证进行评估。这里采用cross_val_score,它会自动处理数据划分,并且如果估计器内部使用了CalibratedClassifierCV,内层的校准交叉验证会独立进行。
from sklearn.pipeline import Pipeline
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.calibration import CalibratedClassifierCV
import numpy as np
# 构建Pipeline
model = Pipeline([
('rf', RandomForestClassifier(n_estimators=100, random_state=42)),
('calibrated', CalibratedClassifierCV(method='isotonic', cv=3))
])
# 外层分层交叉验证
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=outer_cv, scoring='neg_brier_score')
print("嵌套交叉验证平均负Brier分数:", np.mean(scores))
需要注意的是,cross_val_score默认使用predict或predict_proba取决于评分函数。neg_brier_score会调用predict_proba,因此整个过程能够正确评估校准后的概率质量。如果只使用accuracy或roc_auc,校准的作用可能体现不出来。
工程实践中还有几点值得关注。第一,校准模型需要定期更新,因为数据分布会随时间漂移,原本校准好的映射关系可能不再适用。第二,对于大规模数据,Isotonic Regression计算量较大,可以优先使用Platt Scaling。第三,多分类问题的校准通常采用one-vs-rest方式,Scikit-learn的CalibratedClassifierCV会自动处理。最后,评估报告中除了报告AUC、F1等指标,也应该加入Brier分数或绘制可靠性曲线,以便全面了解模型是否既排序正确又概率可信。