跳转至

13.4   交叉验证

小率把活动推荐数据随机分成训练集和验证集。第一次切分,F1 是 0.86;换一个随机种子,变成 0.78;再换一次,又到 0.82。模型没变,分数却像天气一样飘。

一次切分太依赖运气。交叉验证(Cross Validation) 要做的,就是让不同样本轮流当验证集,多测几次,再看平均表现和波动。

图 13.4.1 交叉验证

如果刚好抽到一批简单验证集,分数不就虚高了吗?
所以让每一份数据都轮流接受检验,把单次切分的运气摊薄。

13.4.1   K 折让每份数据都轮到一次

K 折交叉验证(K-Fold Cross Validation) 把数据分成 \(K\) 份。每次用 \(K-1\) 份训练,剩下 1 份验证,重复 \(K\) 次。

如果第 \(k\) 折的验证分数是 \(s_k\),平均分是:

\[ \bar s=\frac{1}{K}\sum_{k=1}^{K}s_k \]

分数波动也要报告:

\[ sd(s)=\sqrt{\frac{1}{K-1}\sum_{k=1}^{K}(s_k-\bar s)^2} \]

平均分告诉你模型大概多好,标准差告诉你这个估计有多稳。

0.84±0.01 和 0.84±0.10,听起来完全不是一回事。
对。平均值相同,不代表可靠程度相同。

13.4.2   数据不能乱切

不同数据有不同切法。切错了,验证分数会看起来很好,却不能代表真实上线表现。

场景 推荐方式 为什么
类别比例不平衡 StratifiedKFold 每折保持类别比例接近
同一同学多条记录 GroupKFold 避免同一个人同时进训练和验证
按时间预测报名 TimeSeriesSplit 只能用过去预测未来
样本很少 Leave-One-Out 尽量利用数据,但计算慢

数据泄漏比模型差更可怕

如果同一个同学的多条记录被拆到训练集和验证集,模型可能只是认出了这个人,而不是学会了活动推荐规律。验证分数会虚高。


13.4.3   调参也会偷看答案

交叉验证常用来选择超参数,比如正则强度、树深度、邻居数。要注意:验证集可以用来选模型,但最后报告性能最好再用独立测试集,或使用 嵌套交叉验证(Nested Cross Validation)

外层负责评估,内层负责调参:

外层第 1 折:训练候选模型(内层调参) -> 外层验证
外层第 2 折:训练候选模型(内层调参) -> 外层验证
...
如果我在同一批验证集上反复试参数,最后挑最高分,是不是也在背验证集?
是的。验证集被用太多次,也会变成训练过程的一部分。

13.4.4   用 sklearn 跑一次交叉验证

from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.linear_model import LogisticRegression

X, y = make_classification(
    n_samples=600,
    n_features=8,
    weights=[0.75, 0.25],
    random_state=2026,
)

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=2026)
model = LogisticRegression(max_iter=1000)
scores = cross_val_score(model, X, y, cv=cv, scoring="f1")

print("每折 F1:", scores.round(3).tolist())
print(f"平均 F1 = {scores.mean():.3f}, 标准差 = {scores.std(ddof=1):.3f}")

报告时怎么写

“5 折分层交叉验证 F1 = 0.81 ± 0.04” 比 “F1 = 0.81” 更完整,因为它同时交代了切分方式、平均表现和不稳定程度。

小率的笔记本

交叉验证让样本轮流当验证集,用多次评估降低单次切分的运气影响。切分方式必须尊重数据结构:类别要分层,同一对象要分组,时间数据要按过去到未来。