交叉验证与网格搜索:从原理到鸢尾花实战
学完 KNN 后,一个自然的问题是:邻居数量应该设为 3、5,还是其他值?某一次测试准确率高,就能说明这个参数好吗?
这篇文章从我学习时遇到的几个疑惑出发,梳理交叉验证与网格搜索的关系,再用鸢尾花数据集实现完整流程。
先记住两个分工:交叉验证负责评估一组参数,网格搜索负责尝试不同参数组合。
一、先分清三个概念
| 数据部分 | 作用 | 是否参与当轮模型训练 |
|---|---|---|
| 当轮训练集 | 拟合模型 | 是 |
| 当轮验证集 | 评价当前参数配置,辅助选参 | 否 |
| 独立测试集 | 选参结束后评估最终模型 | 否 |
本文采用“先留出测试集,再在训练部分内部做交叉验证”的流程:
1 | 全部数据 |
交叉验证中的验证集,是从训练部分里轮流划出来的。它与最后保留的独立测试集不是同一回事。
二、5 折交叉验证,是把 20 条再细分吗?
假设参与交叉验证的训练部分有 100 条数据,做 5 折交叉验证:
- 将这 100 条分为 5 份,每份 20 条。
- 每轮拿其中一份作为验证集。
- 剩余四份共 80 条作为当轮训练集。
- 一共做 5 轮,让每份都当一次验证集。
那 20 条直接作为验证集,不需要在这轮里再次划分训练集和验证集。
| 轮次 | 当轮训练集,80 条 | 当轮验证集,20 条 |
|---|---|---|
| 第 1 轮 | B、C、D、E | A |
| 第 2 轮 | A、C、D、E | B |
| 第 3 轮 | A、B、D、E | C |
| 第 4 轮 | A、B、C、E | D |
| 第 5 轮 | A、B、C、D | E |
这里的 100 条是“用于交叉验证的训练部分”,不包含独立测试集。如果总共只有 100 条数据,并先留出 20 条测试,那么参与交叉验证的就只有 80 条。
数据量不能整除折数时,各折大小通常相差不超过一条。
三、是不是每一轮所有数据都参与?
对,但参与的角色不同。
每轮的 100 条数据中,80 条负责训练,20 条只负责验证。全部参与这一轮,不代表全部参与训练。
五轮结束后,每条数据都参与了:
- 4 次训练;
- 1 次验证。
每一轮都会重新拟合模型,不会拿上一轮已经训练好的模型继续训练。否则,上一轮见过的样本可能成为这一轮的验证数据,验证就不再独立。
四、怎样选出“最好的参数”?
以 KNN 的邻居数量为例,准备尝试 K=1、K=3、K=5。
KNN 的 K 表示邻居数量;5 折表示数据划分份数。这是两个独立的设置。
先固定 K=1,跑完五折并计算平均准确率;再换成 K=3、K=5,分别重复。
下面的分数是说明原理的假设示例:
| 邻居数量 | 第 1 折 | 第 2 折 | 第 3 折 | 第 4 折 | 第 5 折 | 平均准确率 |
|---|---|---|---|---|---|---|
| K=1 | 85% | 90% | 85% | 95% | 85% | 88% |
| K=3 | 95% | 90% | 95% | 90% | 95% | 93% |
| K=5 | 90% | 90% | 95% | 90% | 85% | 90% |
这次比较中,K=3 的平均准确率最高,因此选择 K=3。不同候选参数使用相同的折划分和评分指标,才能公平比较。
所谓“最优”,是在当前候选范围、数据划分和评分指标下表现最好,并不保证是所有可能参数中的绝对最优。
准确率越高越好;原始误差通常越低越好。scikit-learn 的搜索评分统一按越大越好处理,因此有些误差评分使用负数形式,例如 neg_mean_squared_error。
五、某一折准确率特别高,会影响平均值吗?
会,但它只贡献五折平均值的五分之一。
例如 K=3 的五折准确率是:
1 | 100%、80%、80%、80%、80% |
平均值为:
1 | (100 + 80 + 80 + 80 + 80) / 5 = 84% |
如果 K=5 每折都是 90%,平均值就是 90%,仍然更好。
这就是为什么不能说“第 4 折最好,所以选第 4 折”。我们要比较的是不同参数配置的平均分,不是挑一折最高分。
平均分接近时,也可以观察分数的标准差。波动更小,说明模型在这些划分上的表现更稳定;不过,这不是对未来数据表现的保证,五折也不能完全消除偶然性。
六、网格搜索到底在搜索什么?
超参数是训练前设置的配置,例如 KNN 的 n_neighbors。它不会像线性回归的权重那样,在普通拟合过程中直接学出。
网格搜索会枚举给定的候选组合,逐一评估。
1 | param_grid = {'n_neighbors': [i for i in range(1, 11)]} |
这里的列表推导式:
1 | [i for i in range(1, 11)] |
等价于:
1 | numbers = [] |
得到 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],也可以写成 list(range(1, 11))。range 包含起点,不包含终点。
如果再加入另一个超参数:
1 | param_grid = { |
就有 3×2=6 种组合。uniform 表示邻居等权投票,distance 表示距离近的邻居权重更大。
七、原始代码里需要修正的两个地方
1. 不要在交叉验证之前拟合整个训练集的标准化器
原来的写法是:
1 | x_train = transfer.fit_transform(x_train) |
虽然独立测试集没有参与标准化器的拟合,但仍然存在交叉验证内部的数据泄漏。
以鸢尾花为例:先划出 120 条训练数据,再做四折,每轮应该只用当轮的 90 条计算均值和标准差。但上面提前使用了全部 120 条,因此当轮验证集的 30 条也影响了标准化规则。
正确流程是:每折只在当轮训练部分拟合标准化器,然后转换训练部分和验证部分。将 StandardScaler 和 KNN 放进 Pipeline,可以让这个过程随每轮拟合一起执行。官方数据泄漏说明
2. 搜索结束后,不要又手动写死 K=3
原来的最后两行:
1 | estimator2 = KNeighborsClassifier(n_neighbors=3) |
有两个问题:
- 搜索结果不一定是 K=3,写死参数可能丢掉选参结果。
- 这两行只有拟合,没有预测或评分,不能称为“模型评估”。
GridSearchCV 默认 refit=True,选参后会在传入的全部训练数据上重新拟合最佳配置。因此可以直接调用搜索对象的 predict() 和 score(),不必再手动创建模型。GridSearchCV 文档
八、完整代码:Pipeline + GridSearchCV
下面保留 random_state=22 和 K=1~10、四折交叉验证的设置,并加入分层划分,使各类别比例更均衡。因为划分方式和标准化流程调整了,结果不必与原代码一致。
1 | import pandas as pd |
为什么多了 knn__?
因为这次搜索对象是包含两个步骤的流水线,要告诉它调整哪个步骤的参数:
1 | knn__n_neighbors |
如果搜索对象直接是 KNN,就用 n_neighbors;如果是本文这条流水线,就用 knn__n_neighbors。
cv=4 与本文写法的关系
对于本例这种分类任务,GridSearchCV(..., cv=4) 默认使用不打乱顺序的分层四折划分。本文显式使用 StratifiedKFold,便于设置打乱和随机种子。
两种都可以;关键是理解验证数据怎样分配,以及不同候选参数应共享同一组折划分。
九、四个结果属性应该怎么看?
| 属性 | 含义 |
|---|---|
best_score_ |
最佳参数组合的交叉验证平均分 |
best_params_ |
选出的参数组合 |
best_estimator_ |
使用最佳参数重新拟合后的模型;本文中是一整条流水线 |
cv_results_ |
全部候选组合的逐折分数、均值、标准差、排名与耗时等 |
例如,假设 best_params_ 显示:
1 | {'knn__n_neighbors': 5} |
就表示这次搜索选择了 5 个邻居,而不是说“五折中的第 5 折最好”。本文实际上设置的是四折。
特别注意:cv_results_ 中的 split0_test_score、mean_test_score,指的是交叉验证内部留出折的评分,不是最后那 30 条独立测试集上的分数。这里 API 名称里的 test 容易造成混淆。
best_score_ 也不是最终独立测试准确率。它经过了选参筛选,可能偏乐观;最后应另外报告测试集上的结果。
十、这一段代码实际拟合了多少次?
本例有 10 个候选邻居数量,每个做四折交叉验证:
1 | 10 组参数 × 4 折 = 40 次交叉验证拟合 |
选出最佳参数后,refit=True 再用全部 120 条训练数据拟合一次:
1 | 共 41 次流水线拟合 |
前 40 次,每次只用当轮 90 条训练数据拟合标准化器和 KNN;最后一次用全部 120 条训练数据拟合。30 条独立测试数据始终不参与拟合。
best_estimator_ 是最后这次重新拟合得到的结果,不是从 40 个折模型里直接挑出来的某一个。
十一、学完后应当能讲清楚的流程
1 | 先留出独立测试集 |
交叉验证让一组参数经历多次划分的检验,网格搜索让多组参数在相同规则下比较。选好之后,才训练最终模型。
测试集用于最后检验,不应根据测试分数反复调整 K 或随机种子;否则它也会逐渐变成参与选参的验证集。





