
模型融合集成算法这个方向里最容易被忽视但决定成败的一步往往不是模型选得多花哨而是验证集划分得对不对。我自己在做分类项目时几乎默认把StratifiedKFold当作交叉验证的“标配”——它既解决了普通K折在类别不平衡数据上验证失真的问题也是搭建stacking这类模型融合管线时生成OOF预测的核心工具。这篇文章就围绕sklearn里的StratifiedKFold把原理、参数、代码和踩坑经验一次讲透适合正在系统学习交叉验证、准备上模型融合或者调参调到头大的朋友。1. 为什么K折交叉验证是模型评估的地基1.1 从一次划分到K折划分稳的不是一点半点单个训练集/验证集划分最怕什么怕这一刀切下去验证集的分布刚好和训练集不一样。比如数据按时间排序前80%是上个月后20%是这个月如果你只切一次模型在这20%上的表现可能完全不能代表真实水平。K折交叉验证的思路很简单把训练集切成K份每次拿K-1份训练剩下1份验证轮流做K次最后把K次分数综合起来。这样做的核心收益在于每一个样本都有机会出现在验证集里评估结果不再依赖某一次“运气较好或较差”的划分。好比期末考试前做了五套模拟卷每套题都当作一次摸底最后看平均分和波动幅度肯定比只看最顺手那一套的成绩更接近真实水平。在sklearn里普通K折就是KFold。它的循环逻辑很直白先拆索引再训练再打分。但用久了会发现一个问题它只保证把数据切成K份根本不看每一份里的类别比例这在分类任务里会埋雷下面细说。1.2 普通KFold的盲区类别比例失真假设手头有100个样本其中90个是“负类”10个是“正类”也就是典型的不平衡数据。如果直接用KFold切成5折每折20个样本由于切分完全随机且不感知标签完全有可能出现某一折里20个样本全部是负类一个正类都没有。这时会发生什么模型在这一折的验证集上没有机会展示它对正类的预测能力但你却拿这一折的准确率去衡量模型好坏。最常见的结果是准确率虚高——因为模型只要无脑输出“负类”准确率就能冲到90%以上。更麻烦的是在风控、医疗、故障检测这类场景里正类恰恰是你最关心的那部分样本如果验证集里没有正类整个评估就失去意义。这个问题在多分类里更隐蔽。比如10个类别某个小类别总共只有3个样本分到5折里很可能其中四折完全没有这个类别的样本那模型对该类的召回率根本无从验证。所以K折虽然从“单次划分”升级到了“多次划分”但还需要一层保障每一折的类别比例都要和整体数据集保持接近。这就是StratifiedKFold里Stratified分层的意义。1.3 分层StratifiedKFold的核心价值分层采样是把整体数据按类别拆开然后在每个类别内部独立做K折切分最后再把各个类别的折拼接成完整的K折。这样每一折中的负类与正类比例尽量和原始数据集保持一致。在模型评估阶段分层避免了上面说的“某折缺少某个类别”导致分数失真。在模型融合阶段分层的重要性更直接。做stacking或blending时我们需要为训练集的每个样本生成“out-of-fold”预测简称OOF。OOF的本质是让每个样本都经过一次“模型没在训练时见过它”的预测从而得到一组相对诚实的预测结果。如果切分时不分层OOF样本的标签分布就会在某一折里明显失衡元模型stacker基于这种失衡数据学习融合规则时学到的规律在线上往往不成立最终导致融合结果变差。我之前在一个电商流失预测项目里就吃过这样的亏用普通KFold搭stacking线下AUC比单模型高出一截结果上线后表现不升反降。后来排查发现问题根源不在融合算法而是OOF数据的类别比例在每个fold上失衡元模型学到的权重被少数极端折带偏了。换成StratifiedKFold重新生成OOF之后线下线上才基本对得上。这也是为什么我建议分类任务里不要默认KFold先考虑StratifiedKFold。它没有多少额外计算成本只是多一点智能却能让评估和融合站在一个更稳的地基上。2. sklearn中的StratifiedKFold原理、参数与选型2.1 API拆解n_splits、shuffle、random_state一次说清楚sklearn里StratifiedKFold用法非常直接先来看最常用的初始化方式from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_index, val_index in skf.split(X, y): X_train, X_val X[train_index], X[val_index] y_train, y_val y[train_index], y[val_index] # 用X_train, y_train训练模型在X_val, y_val上评估这里有三个参数我把它们掰开揉碎讲。n_splits折数也就是你想把数据分成几份。常用5或10没有绝对标准。数据量大可以少折一点省时间数据量小尤其是少数类样本很少时折数不宜太大。shuffle是否在分层前打乱样本顺序。这个参数很多人会忽略但它很关键。如果不打乱数据按原始行的顺序切分一旦原始数据里存在时间排序、用户ID排序之类的潜在规律某几折就可能集中出现某段时间或某批用户造成评估偏差。random_state随机种子。打开shuffle之后必须固定一个种子比如42、2024否则每次运行得到的切分都不同模型分数忽高忽低实验无法复现。有一点要提醒shuffle和random_state是一对好搭档。只开shuffle不设random_state相当于每次都在摇骰子今天跑出0.85明天跑出0.83你根本分不清是模型不稳定还是切分不稳定。2.2 和KFold、GroupKFold、StratifiedGroupKFold怎么选sklearn的model_selection模块里和StratifiedKFold长得像的表亲不少选错场景会出大问题。我整理了一张对比表直接照着选就行。类适用场景切分依据典型问题KFold回归任务、无类别概念的通用K折纯随机切分分类时类别比例可能失衡StratifiedKFold分类任务二分类/多分类按类别比例分层不适合回归不适合分组数据GroupKFold样本存在组结构同一用户、同一设备按组切分不保证类别比例StratifiedGroupKFold分类组结构同时存在先按组再尽量分层需要sklearn较新版本RepeatedStratifiedKFold小数据、想重复多次取均值分层多次不同随机种子计算量成倍增加选型逻辑其实不复杂。回归问题没有类别标签老老实实用KFold分类问题优先StratifiedKFold但如果样本里有明显的组结构比如同一个用户的多条行为记录同时出现在训练集和验证集里会造成严重的数据泄漏这时必须用GroupKFold族。一个典型例子某算法要做“用户级别”的流失预测一个用户可能有多条记录如果用StratifiedKFold随机切分同一个用户的一部分记录在训练集、另一部分在验证集模型就相当于提前见过这个人的部分行为验证分数会虚高。这时候应该用GroupKFold确保同一个人的所有记录只落在同一折里。如果既要按组切分又希望类别比例平衡那就用StratifiedGroupKFold也就是“分组和分层两手抓”。2.3 不要忽略的细节分层逻辑与极端类别样本分层并不是魔法它有个前提每个类别至少要有n_splits个样本。比如10个类别中的某一个只有3个样本而你想做5折那这个类别注定不可能进入每一折。sklearn在这种情况下不会报错但该类别只会零散出现在某些折里遇到这种极端不平衡要么降低折数要么换个评估策略。另外分层的对象是原始标签y而不是模型预测概率。有的朋友会问回归任务能不能用StratifiedKFold答案是不能直接传连续值但可以先把目标变量分箱比如分成5个区间再用分箱后的类别标签做分层切分。这样做的目的是让每一折里目标变量的分布区间也尽量一致对回归模型的评估稳定性有明显帮助。后面实操部分我会给具体代码。还有一点和shuffle有关的细节打开shuffle后sklearn会先把样本打乱再按类别分组切折。如果不打开shuffle样本原来的顺序会直接影响分组结果尤其当数据按时间排序时前几折和后几折的样本分布可能完全不同。所以只要不是刻意研究时间顺序影响都建议打开shuffle。3. 实操用StratifiedKFold做模型评估与模型融合3.1 一个可以直接套用的交叉验证评估模板如果只想拿StratifiedKFold做模型评估下面的代码就是一个通用模板。我以二分类为例使用某公开分类数据集这里用占位数据表示import numpy as np from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import roc_auc_score, f1_score from sklearn.model_selection import StratifiedKFold X np.random.rand(1000, 10) y np.array([0] * 900 [1] * 100) # 模拟不平衡二分类 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) auc_scores [] f1_scores [] for fold, (train_idx, val_idx) in enumerate(skf.split(X, y), start1): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] model GradientBoostingClassifier(n_estimators100) model.fit(X_train, y_train) y_prob model.predict_proba(X_val)[:, 1] y_pred model.predict(X_val) auc roc_auc_score(y_val, y_prob) f1 f1_score(y_val, y_pred) auc_scores.append(auc) f1_scores.append(f1) print(fFold {fold}: AUC{auc:.4f}, F1{f1:.4f}) print(fMean AUC: {np.mean(auc_scores):.4f} ± {np.std(auc_scores):.4f}) print(fMean F1: {np.mean(f1_scores):.4f} ± {np.std(f1_scores):.4f})这段代码的核心要点是每次循环都创建一个全新的模型只用当前折的训练部分做拟合验证部分只用于评估。不要图省事把模型放在循环外面否则每一折用的都是同一个已经见过前几折训练数据的模型验证结果会失真。实际项目里我还会在循环里同时记录每折的feature importance和训练时间方便看出模型在不同数据子集上的稳定程度。如果某几折的AUC波动特别大通常说明数据本身存在较大的分布差异或者少数类样本太少导致切分不稳定。3.2 在模型融合中生成OOFout-of-fold预测模型融合或者说集成算法并不是只有bagging和boosting。我们经常会手动把多个差异化的模型组合起来比如逻辑回归、随机森林、LightGBM、XGBoost的预测结果做加权平均或作为新特征输入元模型。这套操作里OOF预测是核心中间产物。所谓OOF就是交叉验证过程中每个样本恰好有一次机会出现在验证集里把模型对它的预测收集起来按原始索引放回得到一组长度为“总样本数”的预测结果。每个样本的这个预测都来自一个“没有把它纳入训练”的模型所以OOF比直接拿完整训练集训练后预测更接近真实泛化水平。用StratifiedKFold生成单个模型的OOF代码如下def get_oof(model, X, y, n_splits5, random_state42): skf StratifiedKFold(n_splitsn_splits, shuffleTrue, random_staterandom_state) oof np.zeros(len(y)) for train_idx, val_idx in skf.split(X, y): X_train, X_val X[train_idx], X[val_idx] y_train y[train_idx] model_clone model.__class__(**model.get_params()) # 每次重开新模型 model_clone.fit(X_train, y_train) oof[val_idx] model_clone.predict_proba(X_val)[:, 1] # 二分类取正类概率 return oof这里有几个容易踩坑的细节我提一下。第一必须克隆新模型。sklearn里同一个模型实例fit多次会覆盖上一次的训练状态所以不能拿同一个实例在循环里fit了又fit。代码里我用model.__class__(**model.get_params())方式复制了一个参数相同的空模型保证每折独立训练。第二OOF只收集验证集预测。不要在训练集上也做预测然后混入OOF那样等于把模型“见过”的信息带进元模型会让融合分数虚高线上翻车。第三返回的OOF数组长度等于传入y的长度顺序和原始X、y的行顺序一致。如果后面要拼接新特征直接按行拼接即可不要打乱顺序。3.3 把多个模型的OOF拼起来训练元模型有了每个基模型的OOF模型融合中最关键的一步就完成了用OOF特征去训练一个元模型也就是stacking。下面是一段简化的融合管线from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from lightgbm import LGBMClassifier X np.random.rand(1000, 10) y np.array([0] * 900 [1] * 100) base_models [ LogisticRegression(max_iter1000), RandomForestClassifier(n_estimators200, random_state42), LGBMClassifier(n_estimators200, random_state42), ] # 生成每个基模型的OOF oof_list [] for model in base_models: oof get_oof(model, X, y, n_splits5) oof_list.append(oof) # 把多个OOF按列拼接成特征矩阵 X_oof np.column_stack(oof_list) # 在OOF特征上训练元模型 meta_model LogisticRegression(max_iter1000) meta_model.fit(X_oof, y) # 新数据预测时也需要先让基模型在完整训练集上重新训练 # 然后用基模型的预测概率拼成元特征再喂给meta_model这里元模型用逻辑回归比较常见因为OOF特征通常只有几个到十几个维度逻辑回归稳定而且不容易过拟合。当然也可以换成其他轻量模型但没必要用太复杂的。为什么这套流程天然依赖StratifiedKFold因为OOF特征矩阵的每一行必须和原始样本的标签分布保持一致。如果某折里全是负类OOF那一部分的预测概率分布就会和全局其他折差异巨大逻辑回归在拟合时会对这部分样本产生偏置。分层切分可以保证即使有个别极端样本整体趋势也是和原始分布一致的。多说一句如果做的是软投票或者加权平均融合不训练元模型那OOF的作用就是用来计算每个模型的最优权重。算法很简单在OOF预测值上搜索一组非负权重使得加权组合在某个指标比如AUC上最大化。这一步同样要求OOF是“诚实”的所以生成OOF时用StratifiedKFold依然是正解。3.4 回归任务和分组场景的StratifiedKFold变通虽然StratifiedKFold是为分类设计的但回归任务里也有分层需求。做法是先对y做分箱用分箱后的标签作为分层依据然后再进入StratifiedKFold。下面这个代码片段可以直接用在回归评估上import pandas as pd y_reg np.random.rand(1000) # 回归目标 # 分成5个区间每个区间样本量尽量接近 y_binned pd.qcut(y_reg, q5, labelsFalse) skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, val_idx in skf.split(X, y_binned): # 注意split的y参数传的是分箱标签不是原始连续值 pass分箱的数量不要太夸张5到10个区间就够用了。分箱太多每个箱子的样本数变少分层效果反而退化。分组场景的应对方法也提一下。如果样本属于同一用户的多次操作或者同一设备的多条日志必须用GroupKFold。如果你想在分组的基础上再尽量保持类别比例sklearn在较新版本中提供了StratifiedGroupKFold它的切分约束是“每组样本不能同时出现在训练和验证”在这个前提下尽量满足类别比例。这个类在数据组数和类别数都很多时会比较慢因为每次切分都要搜索可行解属于正常现象。4. 常见问题与排查技巧实录4.1 报错速查看到这些报错先检查什么StratifiedKFold用多了会碰到几个高频报错我把原因和解决办法整理成一张表。报错信息常见原因解决办法Supported target types are: (binary, multiclass)把连续回归目标传给了StratifiedKFold回归任务用KFold或先把y分箱n_splits cannot be greater than the number of members in each class少数类样本数少于折数调低n_splits或检查少数类样本量The least populated class has only N members某个类别样本太少考虑重采样、合并罕见类别或减小折数shuffleTrue requires random_state某些环境和版本会提醒你不设种子不可复现固定random_state其中第一个报错应该是见得最多的。StratifiedKFold的split(X, y)要求y是二分类或多分类标签不能是一堆连续浮点数。看到这个报错时先确认自己是不是拿回归任务硬套了。第二个报错的本质是分层的前提条件不满足每个类别样本数必须大于等于n_splits。假设最少的类别只有5个样本你设n_splits10sklearn就会直接报错。这其实是在保护你因为它宁可停下来也不愿意给你一个分不均匀的结果。4.2 数据泄漏交叉验证中最容易犯的低级错误用StratifiedKFold做评估和融合时有一个很多新手会忽略的泄漏点数据预处理没有放进交叉验证循环内部。举一个最常见的例子。先对整个X做标准化然后再把标准化后的数据交给StratifiedKFold切分。看起来好像没什么区别但实际上标准化使用的均值和方差来自全量数据包括验证集验证集的信息在模型训练之前就被偷看到了。这样得到的分数通常会虚高一点虽然有时只差零点零零几但在比赛或高精度项目里这一点偏差会误导后续所有决策。正确的做法是切分之后只拿训练折去fit标准化器再用同一个标准化器去transform训练折和验证折。我用一个简单代码说明from sklearn.preprocessing import StandardScaler skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, val_idx in skf.split(X, y): scaler StandardScaler() X_train scaler.fit_transform(X[train_idx]) X_val scaler.transform(X[val_idx]) # 继续训练和验证这个原则适用于一切需要拟合数据的预处理比如缺失值填充器的fit、PCA、特征选择中的fit_transform都必须在每个fold里独立完成。简单记一句话在交叉验证循环里凡是基于训练集计算了统计量这个统计量就只能在训练集上合法使用。另一个泄漏点再强调一遍OOF预测只能来自验证折。有的写法是先在整个训练集上训练模型再对全量数据预测然后把预测结果当作OOF特征去训练元模型这是明显的数据泄漏元模型看到的基模型预测里包含了“自己”的信息自然会把线下AUC抬得很高。4.3 两个真实踩坑案例分层与否决定线下线上差距案例一某风控场景的二分类项目负类与正类比例大概为95比5。初版评估用KFold做5折得到AUC 0.87当时觉得模型很不错。后来一位朋友建议换成StratifiedKFold结果AUC降到0.84。这个差距不是模型变了而是因为数据按时间记录排序KFold里某些折正好集中了某个时间段的低风险样本导致分数虚高。换成StratifiedKFold之后每折的类别比例更加接近真实评估结果也更能反映线上表现。案例二模型融合环节某基模型生成的OOF用于stacking第一次用KFold元模型线下AUC比单模型高出0.02看起来融合有效。后来跑稳定性测试时发现只要换一个random_state结果波动幅度很大。排查后发现KFold生成的OOF在每一折上的正样本占比波动明显元模型学到的是“某些折更信任某个模型”这种伪规律。换成StratifiedKFold生成OOF后结果波动明显下降融合效果也稳定下来。这两个案例说明一个道理在分类任务里分层不是一个可选项它应该是一个默认项。用KFold得到的高分需要多问一句这个高分是否只是切分巧合带来的。4.4 独家心得几招提升StratifiedKFold使用体验用StratifiedKFold这么长时间我总结了几个日常经验算是常规文档里不会直接写的东西。第一打开shuffle并固定random_state这是可复现实验的底线。如果你有好几套模型要对比不管用哪种切分都应该保证它们看到完全相同的训练和验证折。不固定种子的话两个模型的差异可能有一半来自切分随机性。第二在跑大量融合实验之前先检查一下每折的类别比例。可以用下面几行代码快速打印出来确认分层是否真的生效for fold, (train_idx, val_idx) in enumerate(skf.split(X, y), start1): val_labels y[val_idx] print(fFold {fold}, val positive ratio: {val_labels.mean():.4f})一个合格的5折分层切分每折验证集的正类占比应该在全局正类占比附近小幅波动而不是忽高忽低。第三类别极端不平衡时不要只依赖单次StratifiedKFold。可以用RepeatedStratifiedKFold重复跑5到10次取平均分和标准差这样能更全面评估模型稳定性。代价是计算时间几倍增长但小数据或者关键决策环节值得这么花。第四在模型融合场景里OOF生成之后我通常还会检查一下每个基模型OOF与真实标签的相关性比如直接画miscalibration图或者算AUC。如果某个模型的OOF AUC远低于它在交叉验证里的平均AUC说明它在某些折上表现极不稳定融合前最好先排查原因而不是直接拿来喂元模型。最后再分享一点个人体会StratifiedKFold在我做过的分类项目里已经从“评估工具”变成了“默认基础设施”。它本身不提升模型上限但它保证了你的每一次评估、每一个融合决策都建立在稳定的切分之上。一个不靠谱的验证集划分会让你调半年参数、融一堆模型结果都是在和随机性较劲。我个人的习惯是所有分类任务的第一版基线实验统一用StratifiedKFold(n_splits5, shuffleTrue, random_state42)先把评估基准线打牢再去谈特征工程和模型融合。至于回归任务则先用分箱分层或者干脆KFold重点观察不同折之间的稳定性。最后送一个小技巧没有绝对最优的折数但为了对比方便同一个项目里所有实验尽量保持同一个n_splits和random_state。你会在无数次的实验对比中体会到一个稳定的验证集划分比任何一个花哨的模型技巧都更能让人安心。