TL;DR
- 场景:同一模型多次训练评估波动大,单次划分不可信,K 值难定
- 结论:用 K 折交叉验证看"平均分 + 方差",优先选高均值且方差小的区间
- 产出:一套可复用的 cross_val_score 流程 + 结合学习曲线的稳定性选参方法


交叉验证
确定了 K 值之后,我们还能够观察到一个重要现象:每次运行模型时,学习曲线都会发生变化,模型的效果时好时坏,呈现出不稳定性。这种波动现象主要是由以下原因造成的:
-
数据集划分的随机性:
- 每次运行时,训练集和测试集的划分方式不同(通常是通过随机采样)
- 举例来说,假设我们有1000条数据,第一次可能随机选取700条作为训练集,剩下的300条作为测试集;第二次运行时,这个划分组合又会发生变化
-
数据分布的影响:
- 不同的训练集会学到略微不同的模式
- 测试集的不同组成会导致评估指标波动
- 例如,如果某次测试集中恰好包含较多边界案例,模型表现就会较差
在实际业务场景中,这种情况反映了几个关键问题:
-
历史数据与新数据的差异:
- 训练数据通常是静态的历史数据
- 测试数据则模拟未来新进入系统的实时数据
- 在电商推荐系统中,用过去3个月的订单数据训练,但需要预测未来一周的购买行为
-
模型评估的核心目标:
- 我们追求的是模型在未知数据上的表现
- 这种能力被称为泛化能力(Generalization Ability)
- 好的泛化能力意味着:
- 对噪声数据保持鲁棒性
- 能处理未见过的数据模式
- 避免过拟合训练数据的特定特征
-
提高泛化能力的方法:
- 使用交叉验证代替单次划分
- 增加数据多样性
- 采用正则化技术
- 例如,在金融风控模型中,会使用5折交叉验证来更可靠地评估模型效果
这种现象提醒我们,在模型开发过程中不能仅依赖单次运行的评估结果,而需要通过多次验证来获得稳定的性能评估,这才是保证模型在实际业务中可靠表现的关键。
泛化能力
我们在进行学习的时候,通常会将一个样本集分化为【训练集】和【测试集】,其中训练集用于模型的学习和训练,而后测试集通常用于评估训练好的模型对于数据的预测性的评估。
- 训练误差代表模型在训练集上的错分样本比率。
- 测试误差代表模型在测试集上的错分样本比率。
训练误差的大小,用来判断给定问题是不是一个容易学习的问题,测试误差反应了模型对未知数据的预测能力,测试误差小的学习方法具有很好的预测能力,如果得到的训练集和测试集没有交集,通常将此预测能力称为泛化能力(generalization ability)。 我们认为,如果模型在一套训练集和数据集上表现优秀,那说明不了问题,只能在众多不同的训练集和测试集上都表现优秀,模型才是一个稳定的模型,模型才是具有真正意义上的泛化能力。 为此,机器学习领域有发挥神作用的技能:【交叉验证】,来帮助我们认识模型。
k折交叉验证
最常用的交叉验证方法是 K 折交叉验证(K-Fold Cross Validation)。这种方法通过将数据集划分为 K 个大小相似的互斥子集,每次使用其中 K-1 个子集作为训练数据,剩下的 1 个子集作为验证数据,重复这个过程 K 次,最终得到 K 个模型评估结果的平均值。
具体步骤如下:
- 将原始数据集随机划分为 K 个等大小的子集(通常 K=5 或 10)
- 依次选取第 i 个子集作为验证集(i=1,2,...,K),其余 K-1 个子集合并作为训练集
- 在训练集上训练模型,在验证集上评估模型性能
- 重复步骤 2-3 直到所有子集都充当过验证集
- 计算 K 次评估结果的平均值作为最终模型性能指标
这种方法相比简单的训练集-测试集划分具有以下优势:
- 充分利用有限的数据资源
- 减少数据划分带来的随机性影响
- 提供更可靠的模型性能评估
- 特别适用于中小规模数据集
实际应用中,K 值的选择需要权衡:
- 较小的 K 值(如 5)计算效率更高
- 较大的 K 值(如 10)评估结果更稳定
- 极端情况 K=N(样本数)时即为留一法交叉验证
通过多次交叉验证求取均值,可以显著降低单次数据划分带来的评估偏差,为模型选择和参数调优提供更可靠的依据。

带交叉验证的学习曲线
对于带交叉验证的学习曲线,我们需要观察的就不仅仅是最高的准确率了,而是准确率高且方差还相对较小的点,这样的点泛化能力才是最强的,在交叉验证+学习曲线的作用下,我们选出的超参数能够保证更好的泛化能力。
python
from sklearn.model_selection import cross_val_score as CVS
Xtrain,Xtest,Ytrain,Ytest = train_test_split(X,y,test_size=0.2,random_state=420)
clf = KNeighborsClassifier(n_neighbors=8)
#训练集对折6次,一共6个预测率输出
cvresult = CVS(clf,Xtrain,Ytrain,cv=6)
#每次交叉验证运行时估算器得分的数组
cvresult
执行结果如下:
查看均值、方差
python
# 均值:查看模型的平均效果
cvresult.mean()
# 方差:查看模型是否稳定
cvresult.var()
执行结果如下所示:
绘制图片进行查看:
python
score = []
var = []
#设置不同的k值,从1到19都看看
krange=range(1,20)
for i in krange:
clf = KNeighborsClassifier(n_neighbors=i)
cvresult = CVS(clf,Xtrain,Ytrain,cv=5)
# 每次交叉验证返回的得分数组,再求数组均值
score.append(cvresult.mean())
var.append(cvresult.var())
plt.plot(krange,score,color='k')
plt.plot(krange,np.array(score)+np.array(var)*2,c='red',linestyle='--')
plt.plot(krange,np.array(score)-np.array(var)*2,c='red',linestyle='--')
运行结果如下:
输出的图片如下所示: 
是否需要验证集
最标准,最严谨的交叉验证应该有三组数据:训练集、验证集和测试集。当我们获取一组数据之后:
- 先将数据集分成整体的训练集和测试集
- 然后我们把训练集放入交叉验证中
- 从训练集中分割更小的训练集(k-1 份)和验证机(1 份)
- 返回的交叉验证结果其实是验证集上的结果
- 使用验证集寻找最佳参数,确认一个我们认为泛化能力最佳的模型
- 将这个模型使用在测试集上,观察模型的表现
通常来说,我们认为经过验证集找出最终参数后的模型的泛化能力是增强了的,因此模型在未知数据(测试集)上的效果会更好,但尴尬的是,模型经过交叉验证在验证集上的调参之后,在测试集上的结果没有变好的情况时有发生。
原因其实是:
- 我们自己分的训练集和测试集,会影响模型的效果
- 交叉验证后的模型的泛化能力增强了,表现它在未知数据集上方差更小,平均水平更高,但却无法保证它在现在分出来的测试集上预测能力最强
- 如此来说,是否有测试集的存在,其实意义不大
如果我们相信交叉验证的调整结果是增强了模型的泛化能力,那即便测试集上的测试结果并没有变好(甚至变坏),我们也认为模型是成功的。 如果我们不相信交叉验证的调整结果能够增强模型的泛化能力,而一定要依赖测试集来进行判断,我们完全没有进行交叉验证的必要,直接用测试集上的结果用来跑学习曲线就好了。 所以,究竟是否需要验证集,其实是存在争议的,在严谨的情况下,大家还是使用了有验证集的方式。
其他交叉验证
交叉验证的方法不止"K 折"一种,分割训练集和测试集的方法也不止一种,分门别类的交叉验证占据了 sklearn 中非常长的一章。 所有的交叉验证都是在分割训练集和测试集,只不过侧重的方向不同。
- K 折就是按顺序取训练集和测试集
- ShuffleSpilt 就侧重于让测试集分布在数据的全方位之内
- StratifiedKFold 则认为训练数据和测试数据必须在每个标签分类中占有相同的比例
各类交叉验证的原理繁琐,大家在机器学习的道路上一定会逐渐遇到更难的交叉验证,但是万变不离其宗:本质上交叉验证是为了解决训练集和测试集的划分对模型带来的影响,同时检测模型的泛化能力。

交叉验证的折数不可太大,因为折数越大抽出来的数据集越小,训练数据所带来的信息量就会越小,模型会越来越不稳定。
避免折数太大
如果你发现不使用交叉验证的时候模型表现很好,一使用校验验证模型的效果就骤降
- 一定要查看你的标签是否有顺利
- 然后就是查看你的数据量是否太小,折数是否太高
如果将上面例题的代码中将 cv 将 5 改成 100,代码如下所示:
python
score = []
var = []
#设置不同的k值,从1到19都看看
krange=range(1,20)
for i in krange:
clf = KNeighborsClassifier(n_neighbors=i)
cvresult = CVS(clf,Xtrain,Ytrain,cv=100)
# 每次交叉验证返回的得分数组,再求数组均值
score.append(cvresult.mean())
var.append(cvresult.var())
plt.plot(krange,score,color='k')
plt.plot(krange,np.array(score)+np.array(var)*2,c='red',linestyle='--')
plt.plot(krange,np.array(score)-np.array(var)*2,c='red',linestyle='--')
生成图片如下图所示: 
折数过大:
- 运算效率变慢
- 预测率方差变大,难以保证在新的数据集达到预期预测率
错误速查
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
| 每次跑分差异很大,曲线忽高忽低 | 单次随机划分导致评估方差大;数据量小/类别不均衡更明显 | 固定 random_state 对比;打印每次划分的类别占比 | 用 K 折交叉验证输出 mean/var;分类任务优先 StratifiedKFold/StratifiedShuffleSplit;必要时扩大数据量 |
| 交叉验证后在测试集反而更差 | 在验证折上"过度调参";测试集划分本身偏 | 对比不同随机种子的测试集结果分布 | 严格三段式:train +(CV 调参)+ test;或用 nested CV;减少超参搜索空间 |
| cv 设置很大(如 100)后分数大幅下降且波动增大 | 每折训练集过小,模型估计不稳;运算量暴涨 | 观察每折样本数:n_samples / cv;看 var 是否飙升 | 控制 cv(常用 5 或 10);小数据用重复 K 折(RepeatedKFold)而不是极大 cv |
| 报错:The least populated class in y has only 1 member | 分层交叉验证下,某些类别样本太少,无法分折 | 查看 y 各类别计数 | 合并稀有类/补样本/降低 cv;或改用不分层但需承担分布漂移风险 |
| CV 分数异常高,线上效果很差 | 数据泄漏:在 CV 之前做了全量标准化/特征选择/编码 | 检查预处理是否在 fit 前用到全量数据 | 用 Pipeline 把预处理放进 CV(StandardScaler/Encoder/SelectKBest 等都在折内 fit) |
| 同一份代码在不同机器结果不一致 | 并行/随机性/浮点差异;未固定种子 | 检查 random_state、n_jobs、numpy 随机种子 | 固定 random_state;必要时 n_jobs=1 复现;记录依赖版本与硬件信息 |
| 指标看起来"稳定"但业务不达标 | 选错 scoring(accuracy 不适合不均衡);只看均值不看业务约束 | 对比多指标:precision/recall/F1/AUC;看混淆矩阵 | 明确业务目标与阈值;设置 scoring;分类不均衡用 macro/weighted 指标并配合分层 CV |
其他系列
🚀 AI篇持续更新中(长期更新)
AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究 ,持续打造实用AI工具指南! AI研究-132 Java 生态前沿 2025:Spring、Quarkus、GraalVM、CRaC 与云原生落地
💻 Java篇持续更新中(长期更新)
Java-207 RabbitMQ Direct 交换器路由:RoutingKey 精确匹配、队列多绑定与日志分流实战 MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务已完结,Dubbo已完结,MySQL已完结,MongoDB已完结,Neo4j已完结,FastDFS 已完结,OSS已完结,GuavaCache已完结,EVCache已完结,RabbitMQ正在更新... 深入浅出助你打牢基础!
📊 大数据板块已完成多项干货更新(300篇):
包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈! 大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解