分层K折交叉验证:保持类别分布
前面讲了K折交叉验证怎么划分数据。但有个问题——如果你的数据分类很不均衡,比如正样本只有10%,那随机切分可能会让某一折里完全没有正样本。模型在这种折上训练,效果可想而知。
我记得第一次接手一个医疗诊断项目时,真实患者只占整个数据集的5%。我用普通K折跑完,发现验证集的AUC忽高忽低,折腾了两天才意识到,是数据切分把少量正样本都分到同一折去了。后来换成分层K折,稳定性一下子就上来了。
分层是什么?
说白了,就是保证每折中各类别的比例,和原始数据集几乎一致。比如原始数据中有80%的A类和20%的B类,那么每一折里也尽量是80%的A和20%的B。
你想想看,这样做至少有两个好处:
- 每折的验证结果更稳定,不会因为某一折缺少某类样本导致评估指标剧烈波动
- 模型在每折上都能见到各类别,训练更充分,尤其是对小众类别
和普通K折的对比
我用一个表格帮你理清差异:
| 维度 | 普通K折 | 分层K折 |
|---|---|---|
| 划分依据 | 随机打乱后按顺序切分 | 按类别比例分层采样 |
| 类别分布 | 各折可能偏差较大 | 各折尽量保持一致 |
| 适用场景 | 类别均衡,数据量很大 | 类别不均衡,小样本场景 |
| 偏差 | 可能高估或低估模型能力 | 更接近真实分布 |
| Python实现 | KFold |
StratifiedKFold |
嗯,这里要注意:分层K折并不是万能药。 如果你的类别本身就极少(比如只有两三个样本),分层也无法拯救,而且可能会报错——因为没法保证每折都有样本。
代码实操
我习惯用scikit-learn的StratifiedKFold。直接看代码:
from sklearn.model_selection import StratifiedKFold
import numpy as np
# 模拟一个不均衡的二分类
X = np.random.rand(100, 5)
y = np.array([0]*80 + [1]*20) # 80%负类,20%正类
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)):
train_labels = y[train_idx]
val_labels = y[val_idx]
print(f"Fold {fold+1}: 训练集正类比例={train_labels.mean():.3f}, 验证集正类比例={val_labels.mean():.3f}")
输出大概是这样:
Fold 1: 训练集正类比例=0.200, 验证集正类比例=0.200
Fold 2: 训练集正类比例=0.200, 验证集正类比例=0.200
Fold 3: 训练集正类比例=0.200, 验证集正类比例=0.200
Fold 4: 训练集正类比例=0.200, 验证集正类比例=0.200
Fold 5: 训练集正类比例=0.200, 验证集正类比例=0.200
你看,每折正类比例都精确保持一致。换成普通KFold,很可能某一折正类比例掉到0.1以下,或者飙到0.3以上。
关键点:StratifiedKFold的split()方法必须传入y(标签),因为它要靠标签来做分层。如果不给y,它会退化成普通K折。
什么时候必须用分层?
我总结了几个典型场景:
- 分类任务中,类别占比严重失衡(比如欺诈检测、罕见病诊断)
- 样本总量不大(千级或万级),随机波动会明显影响评估
- 你打算用交叉验证来调超参数,希望超参选择更稳定
反过来,如果数据非常均衡(各类别各占一半左右),而且样本量很大,普通K折的效果其实相差不大。但我个人习惯,只要做分类,默认就用StratifiedKFold——多写一个参数的事,能省很多麻烦。
小技巧:如果你用的是cross_val_score或GridSearchCV,直接设置cv=StratifiedKFold(5)就行了。底层会自动调用分层划分。
多标签与回归场景
这里有个容易踩的坑。分层K折只适用于单标签分类。如果是多标签或回归问题,标准的分层定义就不适用了。不过scikit-learn提供了StratifiedGroupKFold和针对回归的StratifiedKFold变体吗?
- 多标签:可以考虑
MultilabelStratifiedKFold(第三方库iterative-stratification) - 回归:可以对目标变量分箱(比如分成10个分位数区间),再按分箱后的类别做分层
我曾经在一个回归预测房价的项目里,直接把房价分成10个等频的桶,然后对桶标签做分层K折。虽然没有原始分层那么精确,但比无脑随机切割好太多。
注意:不要把分层K折用于时间序列数据!时间序列天然有顺序依赖,分层打乱会破坏时序结构。这种情况请用前向链式验证(TimeSeriesSplit)。
知识结构图
下面这张图帮你一目了然地理解分层K折在交叉验证家族里的位置,以及它的核心逻辑:
总结
分层K折是我做分类任务时的默认选项。它不复杂,就是加个StratifiedKFold的事,但带来的收益却很实在——评估指标更可靠,超参数搜索更一致,尤其是面对不均衡数据时,能避免很多“假阳性”的调优结果。
如果你之前一直用普通K折,我建议你今天就换成分层版本试试。你会发现验证曲线平滑了许多,心里也踏实多了。