3、分层K折交叉验证:处理类别不平衡数据

先问个问题:你手上有个二分类数据集,正样本只占5%。直接拿普通K折去划分,某折里可能一个正样本都没有——那这一折的评估结果直接崩掉。

我以前就踩过这个坑。有个风控项目,坏样本不到3%,我图省事直接用了KFold,结果某一折的recall直接是0。后来查原因才发现那折压根没分到坏样本,模型只能猜全好样本。折腾了大半天,最后改用StratifiedKFold,三分钟解决问题。

说白了,分层K折就是为了让每一折都保持和原始数据集一样的类别比例。它不解决模型内部的类别不平衡,它解决的是验证阶段的分布失真问题。

为什么普通K折不够用?

普通K折 (KFold)分层K折 (StratifiedKFold)
随机打乱后平均切分按类别比例分层抽样后切分
可能某折全是一类样本每折各类比例与原始数据集一致
评估方差大,不稳定评估结果更稳定、可信
对不平衡数据直接崩对不平衡数据友好

你想想看,如果测试集中小类样本极少甚至没有,你算出来的准确率再高,上线后也可能一脸懵。所以分层K折的核心价值,就是让每一折的分布“代表”原始分布。

StratifiedKFold 怎么用?

用法其实和KFold很像,只是多了一个shuffle参数。我习惯把shuffle=True加上,同时设个固定random_state保证可复现。

from sklearn.model_selection import StratifiedKFold
import numpy as np

X = np.random.randn(100, 5)
y = np.array([0]*95 + [1]*5)   # 95%负样本, 5%正样本

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)):
    y_train, y_val = y[train_idx], y[val_idx]
    print(f"Fold {fold+1}: 训练集正样本比例={y_train.mean():.3f}, "
          f"验证集正样本比例={y_val.mean():.3f}")

运行结果大概是这样的:

Fold 1: 训练集正样本比例=0.050, 验证集正样本比例=0.050
Fold 2: 训练集正样本比例=0.050, 验证集正样本比例=0.050
Fold 3: 训练集正样本比例=0.050, 验证集正样本比例=0.050
Fold 4: 训练集正样本比例=0.050, 验证集正样本比例=0.050
Fold 5: 训练集正样本比例=0.050, 验证集正样本比例=0.050

注意看——每折都是5%正样本。换成普通K折,可能某折正样本比例是0%或20%,飘得厉害。

multilabel 和 multiclass 也能用

StratifiedKFold 不光能处理二分类,对多分类和 multilabel 也同样适用。多分类时按每个类别的比例分层;multilabel 时用的是MultilabelStratifiedKFold(来自iterative-stratification库)。

# 多分类示例
from sklearn.model_selection import StratifiedKFold
y_multiclass = np.array([0, 1, 2, 0, 1, 2, 0, 1, 2, 0])

skf = StratifiedKFold(n_splits=3, shuffle=True, random_state=0)
for train_idx, val_idx in skf.split(X, y_multiclass):
    print(np.bincount(y_multiclass[val_idx]))

输出中每个验证集的类别计数比例会尽量与原数据集一致。

什么时候必须用分层?

  • 二分类不平衡(正样本 < 20% 或 > 80%)。
  • 多分类类别数多且分布偏(比如医学病变分级,常见级别占90%以上)。
  • 小样本数据集(几百条以下,普通K折很容易把稀有类别全切到一折里)。
  • 时间序列?不能直接分层,得用TimeSeriesSplit;但其他场景我都建议先考虑分层。

避坑指南:分层K折不是万能的

⚠️ 我曾经在20万样本的数据集上用分层K折,结果碰到一种极端情况——某个类别只有2个样本,分成5折后每折分不到1个。 这种时候分层会报错,因为无法保持比例。解决办法是:要么把稀有类别合并到相近的类里,要么改用 Leave-One-Out 或 RepeatedStratifiedKFold。

另一个坑:分层K折适用于分类问题,回归问题不能用。为什么呢?回归的y是连续值,没法分层。如果非要分层,可以先把y分桶,变成有序分类,再跑分层K折——嗯,这种操作叫“分层回归”,但实际应用中没那么常见。

分层K折 vs 普通K折:一个直观对比

下面这张图能帮你秒懂:

分层K折 vs 普通K折(二分类不平衡数据) 普通 KFold 原始数据:95% 负样本 + 5% 正样本 折1: 100% 负样本(无正样本) 折2: 4% 正样本 折3: 8% 正样本 折4: 0% 正样本 折5: 6% 正样本 × 比例波动大,评估不稳定 StratifiedKFold 原始数据:95% 负样本 + 5% 正样本 折1: 5% 正样本 ✅ 折2: 5% 正样本 ✅ 折3: 5% 正样本 ✅ 折4: 5% 正样本 ✅ 折5: 5% 正样本 ✅ ✓ 每折比例一致,评估稳定 分层操作保证每折类别比例与原始数据相同,尤其对小类别样本重要

总结一下优势

  • 保比例:每一折的类别比例和原始数据集一致,评估结果不偏。
  • 方差低:多次重复实验的评估指标波动小,模型对比更可靠。
  • 对稀有类别友好:不至于因为采样随机让稀有类别在验证集里消失。
  • 代码简单:就一个StratifiedKFold,参数和KFold几乎一样。

小技巧:如果你在GridSearchCV或RandomizedSearchCV中做超参搜索,直接把cv参数设成StratifiedKFold(n_splits=5)就行,比默认的KFold更稳。

我个人习惯是:只要做分类问题的交叉验证,一律用分层K折。除非样本量极大且每类样本都很充足,否则分层带来的稳定性远远超过那一点点额外计算开销。