分层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折交叉验证知识结构 交叉验证方法 普通K折 分层K折 保持每折类别比例一致 适用于不均衡分类任务 StratifiedKFold 随机切分,比例可能偏移 不适用于不均衡数据 ⚠ 分层K折不适用于: 时间序列 | 多标签(默认) | 回归(需分箱处理)

总结

分层K折是我做分类任务时的默认选项。它不复杂,就是加个StratifiedKFold的事,但带来的收益却很实在——评估指标更可靠,超参数搜索更一致,尤其是面对不均衡数据时,能避免很多“假阳性”的调优结果。

如果你之前一直用普通K折,我建议你今天就换成分层版本试试。你会发现验证曲线平滑了许多,心里也踏实多了。