交叉验证

本节定位
只用一次 train/test 分割来评估模型,结果可能受随机划分的影响很大。交叉验证让每个数据都有机会被用作训练和测试,给出更稳定、可靠的评估结果。
学习目标
- 理解留出法的局限性
- 掌握 K 折交叉验证
- 掌握分层 K 折交叉验证
- 了解留一法和时间序列交叉验证
- 会用
cross_val_score和cross_validate
先说一个很重要的学习预期
这一节最容易让新人误会的地方,是把交叉验证理解成:
- “多跑几次平均一下”
但更值得第一遍先学会的其实是:
交叉验证是在更稳地估计模型的泛化能力。
也就是说,这节的重点不是先记住多少 split 类名,而是先知道:
- 为什么一次划分不够
- 为什么不同任务要配不同切法
- 为什么评估设计本身也是建模的一部分
先建立一张地图
交叉验证这节最适合新人的理解顺序不是“记不同 split 类名”,而是先看清它到底在解决什么问题:
这节真正想解决的是:
- 为什么一次随机划分不够可信
- 为什么评估方法本身也要匹配任务类型
一、留出法的问题
1.1 一次划分够吗?
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
import numpy as np
iris = load_iris()
X, y = iris.data, iris.target
# 不同 random_state 导致不同结果
scores = []
for seed in range(50):
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=seed)
model = DecisionTreeClassifier(max_depth=3, random_state=42)
model.fit(X_train, y_train)
scores.append(model.score(X_test, y_test))
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 4))
plt.bar(range(50), scores, color='steelblue', alpha=0.7)
plt.axhline(y=np.mean(scores), color='red', linestyle='--', label=f'平均: {np.mean(scores):.3f}')
plt.xlabel('随机种子')
plt.ylabel('准确率')
plt.title(f'50 次不同划分的准确率(标准差: {np.std(scores):.3f})')
plt.legend()
plt.grid(axis='y', alpha=0.3)
plt.show()
print(f"最低: {min(scores):.3f}, 最高: {max(scores):.3f}, 差距: {max(scores)-min(scores):.3f}")
问题
一次划分的结果不稳定——不同的随机种子可能差异很大。我们需要更可靠的评估方式。
1.2 一个更适合新人的判断标准
如果你现在还在想:
- “我这次随机划分分数不错,应该就可以了吧?”
那这节要帮你建立的就是:
- 一次分数不重要,稳定分数才重要
1.3 一个更适合新人的类比
你可以先把交叉验证想成:
- 不要只考一次就定水平
- 而是换几套题、多考几轮,再看平均发挥
这样你得到的就不是:
- “这次刚好考得不错”
而是:
- “整体水平大概就在这里”
二、K 折交叉验证
2.1 原理
把数据分成 K 份,每次用 1 份做测试、其余 K-1 份做训练。重复 K 次,取平均。
2.2 sklearn 实现
from sklearn.model_selection import cross_val_score, KFold
from sklearn.tree import DecisionTreeClassifier
model = DecisionTreeClassifier(max_depth=3, random_state=42)
# 最简单的用法
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"5 折交叉验证:")
print(f" 每折分数: {scores}")
print(f" 平均: {scores.mean():.4f} ± {scores.std():.4f}")
2.3 手动控制 KFold
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=42)
# 可视化每折的划分
fig, axes = plt.subplots(5, 1, figsize=(12, 6), sharex=True)
for fold, (train_idx, test_idx) in enumerate(kf.split(X)):
ax = axes[fold]
ax.scatter(train_idx, [0]*len(train_idx), c='steelblue', s=3, label='训练')
ax.scatter(test_idx, [0]*len(test_idx), c='red', s=10, label='测试')
ax.set_ylabel(f'折 {fold+1}')
ax.set_yticks([])
if fold == 0:
ax.legend(loc='upper right', ncol=2)
axes[-1].set_xlabel('样本索引')
plt.suptitle('5 折交叉验证的数据划分', fontsize=13)
plt.tight_layout()
plt.show()
2.4 K 值怎么选?
| K 值 | 优点 | 缺点 |
|---|---|---|
| K=3 | 速度快 | 方差大,不够稳定 |
| K=5 | 常用默认值 | 平衡了速度和稳定性 |
| K=10 | 更稳定 | 速度稍慢 |
| K=n(留一法) | 最稳定 | 非常慢 |
2.5 第一次做项目时怎么选最稳?
一个够稳的顺序通常是:
- 入门项目:先用
cv=5 - 想更稳一点:再看
cv=10 - 样本很少:再考虑 LOO
所以很多时候不是值越大越好,而是:
- 先用一个够稳、计算也能接受的值