交叉验证与网格搜索:让模型选择更可靠
训练机器学习模型时,我们经常会遇到两个问题:
- 这个模型到底是不是真的好?
- 参数应该怎么选,比如 KNN 的 $K$、逻辑回归的正则化强度、决策树的最大深度?
如果只把数据随便分成一次训练集和测试集,然后看一次准确率,很容易被偶然性误导。交叉验证(Cross Validation)就是为了解决“评估不稳定”的问题;网格搜索(Grid Search)则是为了解决“超参数怎么选”的问题。
1. 为什么不能只划分一次数据
假设我们有一份数据集,要判断一个模型效果如何。最常见的做法是:
训练集:用来训练模型
测试集:用来评估模型
这听起来很合理,但有一个问题:一次划分可能有运气成分。
如果测试集刚好比较简单,模型分数会偏高;如果测试集刚好比较困难,模型分数会偏低。也就是说,一次测试结果可能不能代表模型的真实水平。
可以把它想象成考试:
- 只考一张卷子,可能刚好考到你熟悉的题。
- 多考几张不同卷子,平均成绩才更接近真实水平。
交叉验证的思想就是:让模型多考几次,再看平均表现。
2. 什么是 K 折交叉验证
K 折交叉验证(K-Fold Cross Validation)会把数据分成 $K$ 份,每次拿其中一份做验证集,其余 $K-1$ 份做训练集。
假设 $K=5$(上方图中就是5折交叉验证),流程如下:
| 轮次 | 训练集 | 验证集 |
|---|---|---|
| 第 1 轮 | 第 2、3、4、5 份 | 第 1 份 |
| 第 2 轮 | 第 1、3、4、5 份 | 第 2 份 |
| 第 3 轮 | 第 1、2、4、5 份 | 第 3 份 |
| 第 4 轮 | 第 1、2、3、5 份 | 第 4 份 |
| 第 5 轮 | 第 1、2、3、4 份 | 第 5 份 |
最后会得到 5 个验证分数:
$$
s_1, s_2, s_3, s_4, s_5
$$
通常取平均值作为模型表现:
$$
\bar{s}=\frac{1}{K}\sum_{i=1}^{K}s_i
$$
也可以看标准差:
$$
\sigma=\sqrt{\frac{1}{K}\sum_{i=1}^{K}(s_i-\bar{s})^2}
$$
平均值反映模型总体表现,标准差反映模型表现是否稳定(标准差越小越稳定)。
3. 交叉验证解决了什么问题
交叉验证的好处主要有三个。
3.1 评估更稳定
一次训练/验证划分可能有偶然性,而 K 折交叉验证会让每一部分数据都轮流做一次验证集。这样得到的平均分更可信。
3.2 数据利用更充分
每一轮中,大部分数据都参与训练;从整体看,每个样本也都有机会参与验证。对于数据量不大的场景,这一点很重要。
3.3 方便比较模型
如果要比较 KNN、逻辑回归、决策树等模型,使用同样的交叉验证方式,可以让比较更公平。
4. 什么是超参数
在机器学习中,参数和超参数不是一回事。
参数 是模型通过训练学出来的,例如线性回归中的 $w$ 和 $b$。
超参数 是训练前人为设定的,例如:
- KNN 中的 $K$
- 决策树中的
max_depth - 逻辑回归中的正则化强度
C - 随机森林中的树数量
n_estimators - SVM 中的核函数和惩罚系数
超参数不会由模型自动学出来,需要我们提前指定。
问题是:怎么知道哪个超参数最好?
这就需要网格搜索。
5. 什么是网格搜索
网格搜索(Grid Search)就是把可能的超参数组合列成一张“网格”,然后一个个试。
例如用 KNN 做分类时,我们想尝试:
n_neighbors: 3, 5, 7, 9
weights: uniform, distance
这些参数组合起来就是:
$$
4 \times 2 = 8
$$
也就是说,一共有 12 种组合要测试。
网格搜索会对每一种组合都做交叉验证,然后选择平均分最高的组合。
6. 网格搜索和交叉验证如何配合
网格搜索通常不会只用一次验证集判断哪个参数好,而是会把交叉验证嵌进去。
可以理解为:
对每一组超参数:
做 K 折交叉验证
得到 K 个分数
计算平均分
选择平均分最高的超参数组合
假设我们有 12 组参数,每组做 5 折交叉验证,那么总共需要训练:
$$
12 \times 5 = 60
$$
次模型。
这也是为什么网格搜索有时会比较慢:它不是训练一次模型,而是训练很多次模型。
7. 一个直观例子:给 KNN 选择 K 值
假设我们用 KNN 做分类,只想调一个参数:
K = 1, 3, 5, 7, 9
我们对每个 $K$ 做 5 折交叉验证,得到结果:
| K 值 | 5 折平均准确率 |
|---|---|
| 1 | 0.86 |
| 3 | 0.90 |
| 5 | 0.93 |
| 7 | 0.92 |
| 9 | 0.89 |
此时我们会选择:
$$
K=5
$$
因为它的交叉验证平均分最高。
这比“随便选一个 K”可靠得多。
8. Python 实战:cross_val_score
下面先用 cross_val_score 看模型在 5 折交叉验证下的表现(cross_val_score主要用于模型评估,9中使用GridSearchCV用于寻优)。
from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
# 加载鸢尾花数据集,这是一个经典的多分类数据集
iris = load_iris()
X = iris.data
y = iris.target
# 创建 KNN 模型,这里先人为指定 n_neighbors=5
model = KNeighborsClassifier(n_neighbors=5)
# 对模型做 5 折交叉验证
# scoring="accuracy" 表示使用准确率作为评价指标
scores = cross_val_score(
model,
X,
y,
cv=5,
scoring="accuracy",
)
# 每一折都会得到一个分数
print(scores)
# 交叉验证平均分可以看作模型整体表现
print("平均准确率:", scores.mean())
# 标准差越小,说明不同划分下模型表现越稳定
print("标准差:", scores.std())
输出可能类似:
[0.9667 1.0000 0.9333 0.9667 1.0000]
平均准确率: 0.9733
标准差: 0.0249
这表示模型不是只在某一次划分上表现好,而是在多次验证中整体表现比较稳定。
9. Python 实战:GridSearchCV
下面用 GridSearchCV 搜索 KNN 的超参数。
from sklearn.datasets import load_iris
from sklearn.model_selection import GridSearchCV
from sklearn.neighbors import KNeighborsClassifier
# 加载数据
iris = load_iris()
X = iris.data
y = iris.target
# 先创建一个基础模型,具体超参数交给 GridSearchCV 来搜索
model = KNeighborsClassifier()
# 定义要尝试的超参数范围
# GridSearchCV 会把这些参数组合全部遍历一遍
param_grid = {
"n_neighbors": [1, 3, 5, 7, 9],
"weights": ["uniform", "distance"],
"metric": ["euclidean", "manhattan"],
}
# cv=5 表示每一组超参数都做 5 折交叉验证
# scoring="accuracy" 表示用准确率比较不同参数组合
grid_search = GridSearchCV(
estimator=model,
param_grid=param_grid,
cv=5,
scoring="accuracy",
)
# 开始搜索最佳超参数
grid_search.fit(X, y)
# best_params_ 保存交叉验证平均分最高的参数组合
print("最佳参数:", grid_search.best_params_)
# best_score_ 保存最佳参数组合对应的交叉验证平均分
print("最佳交叉验证分数:", grid_search.best_score_)
# best_estimator_ 是已经用最佳参数重新训练好的模型
print("最佳模型:", grid_search.best_estimator_)
这里 GridSearchCV 做了三件事:
- 遍历
param_grid中的所有参数组合。 - 对每个组合做 5 折交叉验证。
- 找出平均准确率最高的组合。
训练完成后,可以直接拿最佳模型做预测:
best_model = grid_search.best_estimator_
pred = best_model.predict(X[:5])
print(pred)
10. 混淆矩阵:别只看准确率
交叉验证和网格搜索可以帮助我们选出平均表现更好的模型,但如果只看准确率,有时仍然不够。
比如一个疾病检测模型,准确率是 $95%$,听起来很高。但如果它把很多真正患病的人预测成健康,那这个模型依然很危险。
因此,在分类任务中,我们常常会进一步查看混淆矩阵(Confusion Matrix)。
混淆矩阵不是一个新的模型,而是一张“预测结果对照表”,用来观察模型具体把哪些类别预测对了,哪些类别预测错了。
以二分类为例,混淆矩阵中有四个核心概念:
| 名称 | 含义 |
|---|---|
| TP | True Positive,真实为正类,预测也为正类 |
| TN | True Negative,真实为负类,预测也为负类 |
| FP | False Positive,真实为负类,但预测成正类 |
| FN | False Negative,真实为正类,但预测成负类 |
如果用疾病检测来理解:
- TP:病人被正确检测为有病。
- TN:健康人被正确检测为健康。
- FP:健康人被误判为有病。
- FN:病人被误判为健康。
不同业务对错误类型的容忍度不同。比如疾病筛查中,FN 往往比 FP 更严重,因为漏诊可能带来更高风险。
11. 从混淆矩阵得到评价指标
有了 TP、TN、FP、FN,就可以计算多个常见指标。
准确率(Accuracy):
$$
Accuracy=\frac{TP+TN}{TP+TN+FP+FN}
$$
它表示所有样本中预测正确的比例。
精确率(Precision):
$$
Precision=\frac{TP}{TP+FP}
$$
它回答的问题是:模型预测为正类的样本中,有多少真的为正类?
召回率(Recall):
$$
Recall=\frac{TP}{TP+FN}
$$
它回答的问题是:真实正类样本中,有多少被模型找出来了?
F1 分数:
$$
F1=\frac{2 \times Precision \times Recall}{Precision+Recall}
$$
它是精确率和召回率的调和平均,适合在二者都重要时使用。
简单理解:
准确率:整体对不对
精确率:预测为正类时靠不靠谱
召回率:真正的正类有没有找全
F1:精确率和召回率的综合平衡
12. Python 实战:混淆矩阵与分类报告
下面在 GridSearchCV 找到最佳模型后,继续查看它在测试集上的混淆矩阵和分类报告。
from sklearn.datasets import load_iris
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.neighbors import KNeighborsClassifier
# 加载鸢尾花数据集
iris = load_iris()
X = iris.data
y = iris.target
# 先划分最终测试集
# stratify=y 可以尽量保持训练集和测试集中的类别比例一致
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
random_state=42,
stratify=y,
)
# 创建基础 KNN 模型
model = KNeighborsClassifier()
# 定义要搜索的超参数网格
param_grid = {
"n_neighbors": [1, 3, 5, 7, 9],
"weights": ["uniform", "distance"],
"metric": ["euclidean", "manhattan"],
}
# 只在训练集上做网格搜索和交叉验证
# 这样可以避免提前看到测试集,降低数据泄漏风险
grid_search = GridSearchCV(
estimator=model,
param_grid=param_grid,
cv=5,
scoring="accuracy",
)
# 在训练集上寻找最佳参数
grid_search.fit(X_train, y_train)
# 取出最佳模型
best_model = grid_search.best_estimator_
# 用最佳模型预测最终测试集
y_pred = best_model.predict(X_test)
# 输出混淆矩阵
# 行表示真实类别,列表示预测类别
print("混淆矩阵:")
print(confusion_matrix(y_test, y_pred))
# 输出分类报告
# precision 是精确率,recall 是召回率,f1-score 是 F1 分数
print("分类报告:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))
这段代码的重点是:网格搜索只在训练集上进行,最终测试集只在最后评估一次。
如果在调参过程中反复查看测试集结果,模型选择过程就会间接“记住”测试集,最后的测试分数会偏乐观。
13. 训练集、验证集、测试集的关系
一个容易混淆的问题是:用了交叉验证,还需不需要测试集?
答案是:通常仍然需要。
比较稳妥的流程是:
原始数据
|
| 先划分
v
训练验证部分 + 最终测试集
|
| 在训练验证部分做交叉验证和网格搜索
v
选出最佳超参数
|
| 用最佳超参数重新训练模型
v
在最终测试集上评估一次
原因是:网格搜索已经反复看过验证集,如果最后仍然用验证分数当最终成绩,可能会偏乐观。最终测试集应该只在最后使用一次,模拟模型面对全新数据的表现。
14. 常见注意点
14.1 分类任务建议使用 StratifiedKFold
分类任务中,如果类别比例不均衡,普通 KFold 可能让某些折里的类别比例失衡。
这时可以使用分层交叉验证:
from sklearn.model_selection import StratifiedKFold
cv = StratifiedKFold(
n_splits=5,
shuffle=True,
random_state=42,
)
它会尽量让每一折中的类别比例接近原始数据。
14.2 数据预处理要放进 Pipeline
如果要做标准化、缺失值填补、特征选择等操作,不建议先对全量数据处理,再做交叉验证。
错误做法:
先对全部数据标准化
再做交叉验证
这样会发生数据泄漏,因为验证集的信息提前参与了标准化。
更推荐用 Pipeline:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
pipe = Pipeline([
("scaler", StandardScaler()),
("model", LogisticRegression(max_iter=1000)),
])
然后对整个 Pipeline 做交叉验证或网格搜索。
14.3 网格不要设得太大
网格搜索会尝试所有参数组合。如果每个参数都设置很多候选值,训练次数会迅速膨胀。
例如:
5 个 K 值 × 4 种距离 × 3 种权重 × 5 折 = 300 次训练
如果模型本身很慢,这会非常耗时。
可以先用粗网格找到大概范围,再用细网格进一步搜索。
15. Grid Search 和 Random Search
网格搜索会穷举所有组合,适合参数数量较少的场景。
如果参数很多,可以考虑随机搜索(Randomized Search)。它不会尝试所有组合,而是随机抽取一部分组合来测试。
对比可以简单理解为:
| 方法 | 思路 | 适用场景 |
|---|---|---|
| Grid Search | 所有组合都试一遍 | 参数少,训练不太慢 |
| Random Search | 随机试一部分组合 | 参数多,训练成本高 |
16. 总结
交叉验证和网格搜索经常一起出现,但它们解决的是两个不同问题。
交叉验证解决的是:
模型评估是否稳定可靠?
网格搜索解决的是:
超参数应该怎么选?
二者结合起来就是:
用交叉验证评估每一组超参数,再选择平均表现最好的那一组。
在实际机器学习流程中,可以记住一个简单顺序:
划分最终测试集
-> 在训练集上做交叉验证
-> 用网格搜索选择超参数
-> 用最佳参数重新训练
-> 在最终测试集上评估
这样得到的模型选择过程,比只看一次训练/测试划分要可靠得多。
