300 个样本切 5 折,五折得分 0.8167/0.7333/0.7500/0.7500/0.7667,平均 0.7633; 单次 80/20 跑 10 次平均 0.7500。 均值差 0.0133,但交叉验证只用 5 次训练就让每个样本都当过一次测试样本。
上一节的 Holdout 有个问题:切一次的结果不稳。这一节讲一个更稳的做法,代价是多训几次。
一、K 折是怎么做的
把数据平均分成 K 份(叫折)。第 1 折当测试集、其余 K-1 折当训练集,训一个模型、得一个分; 然后换第 2 折当测试集,再来一遍。K 次之后,你手上有 K 个分数。 最后取平均。这个平均值就是「模型性能」的估计。 关键性质:每个样本都当过一次测试样本,也只当过一次。 所以这个估计覆盖了全部数据,不像 Holdout 只覆盖了那 20%。 常用 K 是 5 和 10。K 越大估计越稳,但训练次数也越多。 K 等于样本数时叫留一法,理论上最准,实际太贵,基本不用。
二、和 Holdout 比,差在哪
Demo 用 300 个样本:5 折的五个分数是 0.8167、0.7333、0.7500、0.7500、0.7667,平均 0.7633, 标准差 0.0287。 单次 80/20 跑 10 次(每次换种子),平均 0.7500,标准差 0.0387。 两个均值差 0.0133,在 300 个样本的噪声范围内——说明两者都是无偏的,交叉验证没有系统性高估。 两个标准差 0.0287 和 0.0387 不能直接比大小:一个是「不同折之间」的离散, 一个是「不同切分之间」的离散,含义不同。这次前者略小,但这不是定理。 所以交叉验证真正的优势不在方差,在覆盖面和效率:只用 5 次训练, 就把全部 300 个样本都测了一遍。
三、为什么第 1 折特别高
五折里第 1 折是 0.8167,比其余四折高出一截(0.7333 到 0.7667)。 这不是代码错,是数据里刚好有一批「容易分」的样本落在了第 1 折。 300 个样本分成 5 份,每份只有 60 个,单份的噪声就这么大。 这件事本身就是在提醒:单看任何一折都不能下结论,必须看平均。 如果只看第 1 折报 0.82,就高估了 0.06。 更稳的做法是「重复 K 折」:把整个 K 折流程重复若干次,每次重新打乱。 这样折与折之间的随机性也被平均掉了。代价是再乘一个倍数。
四、两个必须注意的前提
一、数据有时间顺序时不能随机切。 比如用 1 到 6 月的销量预测 7 月, 随机切折会让模型用 7 月的数据训练、用 3 月的数据测试——这叫「穿越」。 正确做法是按时间切:前 4 个月训练、后 2 个月测试,或者用时间序列交叉验证。 二、分类任务要用分层 K 折。 如果正类只占 5%,随机切折可能某一折里一个正类都没有, 那一折的分数就没意义了。分层的意思是「每一折里正负比例都和整体一致」。 这两个前提不满足时,交叉验证给出的分数会虚高,而且虚高得很隐蔽——因为流程本身看起来完全正常。 训练师接手一个项目时,这两条是最该先确认的:数据有没有时间戳、标签比例是多少。
五、动手 Demo:5 折交叉验证:平均 0.7633,每个样本都当过测试样本
造 300 个 6 维二分类样本,用固定 300 步的逻辑回归; 先做单次 80/20 切分重复 10 次,再做 5 折交叉验证,比较两者的均值和标准差,并列出每一折的得分。
idx = rg.permutation(len(y))
folds = np.array_split(idx, 5)
for i in range(5):
te = folds[i]
tr = np.concatenate([folds[j] for j in range(5) if j != i])
w, b = fit_lr(X[tr], y[tr])
cv.append(acc(X[te], y[te], w, b))
实跑输出:
--------------------------------------------------------------------
交叉验证:同一个模型,比单次切分稳多少
--------------------------------------------------------------------
数据 300 个样本、6 维、两类中心差 0.6
模型 逻辑回归,固定 300 步
比较 单次 80/20 切分的得分,对 5 折交叉验证的五个得分
折 测试样本数 该折准确率
--------------------------------
第 1 折 60 0.8167
第 2 折 60 0.7333
第 3 折 60 0.7500
第 4 折 60 0.7500
第 5 折 60 0.7667
5 折平均 0.7633
5 折五个分数的标准差 0.0287
单次 80/20 跑 10 次的均值 0.7500
单次 80/20 跑 10 次的标准差 0.0387
两个均值 **0.7633** 和 **0.7500** 差了 0.0133,在 300 个样本的噪声范围内——说明两者都是无偏的。
两个标准差 **0.0287** 和 **0.0387** 不能直接比大小:一个是「不同折之间」的离散,一个是「不同切分之间」的离散,含义不同。这次前者略小,但这只是这一次的结果,不是定理。
5 折真正的好处不在方差,在**覆盖面**:每个样本都恰好当过一次测试样本,所以它估的是「整个数据集上的表现」,不是「某一次随机切分上的表现」。
代价是训练次数。K 折要训 K 次,K=10 就是十倍开销。数据大、模型贵的时候,常用 3 折或 5 折折中。
还有一点要注意:如果数据有时间顺序(比如销量预测),不能随机切折,得按时间切,否则会用「未来」预测「过去」,分数虚高。
五折的分数是 0.8167、0.7333、0.7500、0.7500、0.7667。第 1 折明显偏高,这就是 60 个样本一份的噪声。 平均 0.7633 对单次切分的 0.7500,差 0.0133。 两者接近,说明交叉验证没有系统性偏见——它不会因为「训练集变小」就低估模型。 训练集从 80%(240 个)变成 4 折(240 个),其实一样大。 所以交叉验证的每一折用的训练量和 80/20 差不多,这也是两者均值接近的原因。 如果改成 10 折,每折训练集就是 270 个,均值会略微上升一点点。 K 越大,训练集越接近全量,估计越乐观——这也是 K 不能无限大的一个原因。
30 秒实验
- 把 K 从 5 改成 10,看均值升多少、标准差降多少。
- 把数据按类别排序后再切折(不 shuffle),看分数怎么变。
- 想一想:为什么时间序列数据不能随机切折?
常见误区
- 「交叉验证一定比 Holdout 准」——它更稳,但训练 K 次,成本高得多。
- 「K 越大越好」——K 大了训练集接近全量,估计会偏乐观,而且成本线性增长。
- 「随便切折就行」——时间序列会穿越,不平衡分类会切出空折。
自测三问(点开对答案)
1. K 折交叉验证怎么做? —— 把数据分 K 份,轮流用一份当测试集、其余当训练集,训 K 次取平均。 2. 交叉验证比 Holdout 强在哪? —— 每个样本都当过一次测试样本,覆盖面完整,且只用 K 次训练。 3. 时间序列数据为什么要按时间切? —— 随机切会让模型用未来数据训练、用过去数据测试,造成信息穿越,分数虚高。有了可靠的评估方法,下一步是判断模型到底是过拟合还是欠拟合。那要用两条曲线。
- ☐ (本节完,下一节 9.2.3 验证曲线与学习曲线:了解模型是否过拟合或欠拟合)