40 个点、多项式 1 到 12 次:训练误差从 0.4894 一路降到 0.0310,十二行没有一个反弹。 所以它不能用来选模型。 5 折交叉验证是 U 形,最低在 7 次(0.0568);留出集最优在 6 次(0.0744)。 差一次不算错,那是「选择」本身的噪声。 7、8、9 次的 CV 是 0.0568、0.0597、0.0660,差得很小——所以该选最优附近最简单的那个。
4.1 和 4.2 给了两个旋钮:子空间维数 k、罚项强度 λ。第 2 章还给过第三个:多项式次数。 这一节回答「旋钮拧到哪一格」。 方法本身不复杂,难点在于纪律:一旦你用最终测试集去挑参数,那个测试集报出来的数就不可信了。
一、为什么训练误差不能用
3.2 已经给过结论:加参数,训练误差必降。Demo 里从 1 次到 12 次, 训练误差从 0.4894 降到 0.0310,中间没有一次反弹。 一条只降不升的曲线,没有最低点。 你要选「最好的那个」,就得有最低点;没有最低点,选择这件事无从谈起。 这不是「训练误差不可靠」,而是「它在数学上就不可能告诉你答案」。 它的下降恰恰是自由度的增加,而自由度增加不等于模型变好。 所以必须换一个「会升」的指标。 会升的指标只有一个来源:在模型没见过的数据上算误差。
二、留出集:最直接,但浪费
最朴素的做法是切一块数据出来,不参与训练,专门用来选。 Demo 里那 300 个点就是干这个的。 它的毛病是浪费。 数据本来就不多,再切掉 20% 到 30%,训练就少了一大块; 而且切出来的那块只用来选一次,利用率极低。 还有一个更隐蔽的毛病:切分本身有随机性。 换一次切分,最优的那个复杂度可能就变了。 数据越少,这个抖动越大。 所以留出集适合「数据够多、只选一次」的场景。 数据少的时候,得想办法把每一块都用上——这就是交叉验证。
三、交叉验证:把每块都当一次验证集
做法:把训练数据分成 k 折,轮流让其中一折当验证集、其余 k 减 1 折当训练集,跑 k 次, 取平均误差。 每一块都当过一次验证集,所以没有数据被浪费。 k 等于 n 时叫留一法:每次只留一个点做验证。 它的好处是无随机性(划分唯一)、几乎无偏(训练集只比全量少一个点); 坏处是要训练 n 次,而且方差可能很大。 k 等于 5 或 10 是实践中的默认。 5 折的计算量是 5 次训练,偏差略大但方差小;10 折更接近留一法,计算量翻倍。 经验上 5 折和 10 折的结论很少差出实质区别。 有一个坑必须说:折的划分要随机,但如果数据本身有结构(时间序列、分组),就不能随机切。 Demo 里 x 是排好序的,我用的是「每 5 个取 1 个」的固定步长,这样每折都覆盖整个区间。 随机抽会让某一折全落在端点,估出来的误差偏乐观或偏悲观。
四、实测:三条曲线
| 多项式次数 | 训练 MSE | 5 折 CV | 留出集 MSE |
|---|---|---|---|
| 1 | 0.4894 | 0.5013 | 0.5713 |
| 3 | 0.0857 | 0.0926 | 0.1374 |
| 5 | 0.0467 | 0.0646 | 0.0750 |
| 7 | 0.0351 | 0.0568 | 0.0977 |
| 10 | 0.0339 | 0.1229 | 0.1515 |
| 12 | 0.0310 | 0.2236 | 0.1108 |
| 训练一路降、CV 先降后升、留出集也先降后升。 | |||
| 三条曲线的形状完全不同,这就是「用什么指标决定选什么模型」的实证。 | |||
| CV 最低在 7 次(0.0568),留出集最优在 6 次(0.0744),差一次。 | |||
| 这个差不说明 CV 错了。 | |||
| CV 估计的是「在没见过的数据上的期望误差」,留出集只是 300 个点的一次抽样。 | |||
| 要警惕的是 CV 在 10 次之后剧烈跳动:0.1229、0.3719、0.2236。 | |||
| 这种跳动说明「CV 估计本身方差很大」——40 个点分 5 折,每折只有 8 个点。 | |||
| 这时候别信那个最低点,它可能只是噪声。 | |||
| 7、8、9 三档的 CV 是 0.0568、0.0597、0.0660,差 0.009。 | |||
| 而 5 折 CV 的标准差大概就在这个量级。 | |||
| 所以「选 7 次」和「选 5 次」在统计上分不出高下——这时候该选简单的。 |
五、纪律:三条不能破的规矩
第一,最终测试集只能用一次。 你用它挑过参数,它就不再是「没见过的数据」,报出来的数会系统性偏乐观。 如果要反复看,就必须再切一层。 第二,任何预处理都只能在训练折上拟合。 标准化、缺失值填补、特征选择,全部要放进交叉验证的循环里。 在外面先做完再切折,等于把验证折的信息漏进了训练——这叫数据泄漏,是新手最常见的错。 第三,报告要带方差,不只报均值。 「CV 误差 0.057」这个说法不完整,要带上「5 折的标准差 0.008」。 没有方差的对比,无法判断差异是不是噪声。 还有一条经验规则:一倍标准差规则。 在「最优值加一个标准差」的范围里,选最简单的那个模型。 它背后的逻辑是:CV 曲线本身有噪声,纠结最后那 0.003 是在拟合噪声。 这条规则在实践中非常有用,尤其是候选模型复杂度差别很大时。
六、动手 Demo:模型选择:训练误差一路降到 0.0310,5 折交叉验证最低在 7 次
40 个训练点,y 等于 sin(1.5x) 加噪声 0.25,另留 300 个点做最终验收。 多项式次数从 1 到 12,对每一个次数同时算三个误差:训练集上的、5 折交叉验证的、300 点留出集上的。 折的划分用「每 5 个取 1 个」的固定步长,因为 x 已经排序。
idx = np.arange(n)
folds = [idx[i::5] for i in range(5)] # 固定步长划分,每折覆盖整个区间
for d in range(1, 13):
A = phi(x, d)
w = fit(A, y)
tr = np.mean((A @ w - y) ** 2)
errs = []
for f in folds:
m = np.ones(n, dtype=bool)
m[f] = False # 这一折当验证集
wf = fit(phi(x[m], d), y[m])
errs.append(np.mean((phi(x[f], d) @ wf - y[f]) ** 2))
cv = np.mean(errs)
实跑输出:
--------------------------------------------------------------------
模型选择:训练误差一路降到 0.0310,5 折交叉验证最低在 7 次
--------------------------------------------------------------------
数据 40 个训练点,另留 300 个点做最终验收
候选复杂度 多项式 1 次到 12 次,参数 2 个到 13 个
交叉验证 5 折,每折轮流做验证集
两个答案 交叉验证选 7 次(0.0568),留出集最优是 6 次(0.0744)
多项式次数 训练 MSE 5 折 CV 留出集 MSE
-------------------------------------------
1 0.4894 0.5013 0.5713
2 0.4587 0.5314 0.5844
3 0.0857 0.0926 0.1374
4 0.0854 0.0997 0.1379
5 0.0467 0.0646 0.0750
6 0.0467 0.0698 0.0744
7 0.0351 0.0568 0.0977
8 0.0348 0.0597 0.1051
9 0.0347 0.0660 0.0973
10 0.0339 0.1229 0.1515
11 0.0336 0.3719 0.2232
12 0.0310 0.2236 0.1108
**训练那一列从 0.4894 一路降到 0.0310,十二行没有一个反弹。**
**这就是第 3.2 节那条性质的实测:加参数,训练误差必降。**
**所以它不能用来选模型——一条只降不升的曲线,没有最低点可选。**
**CV 那一列不是单调的:最低在 7 次(0.0568),到 11 次已经涨到 0.3719。**
**它回升的原因是:多出来的参数开始拟合噪声,而这些噪声在验证折里不存在。**
**留出集最优在 6 次(0.0744),和 CV 选的 7 次差 1 次。**
**差不代表 CV 错了。**
**CV 选的是「在没见过的数据上期望最好」,留出集只是 300 个点的一次抽样。**
**两者的差,就是「选择」这件事本身的噪声——换一批数据,答案也会动。**
**还有个细节:6 次、7 次、8 次的 CV 是 0.0698、0.0568、0.0597,差得很小。**
**所以「选最优」不如「选最优附近最简单的那个」——这叫一倍标准差规则。**
**它背后的逻辑是:CV 曲线本身有噪声,纠结最后那 0.003 是在拟合噪声。**
要说明:数据是我按正弦加噪声**造的**;**5 折的划分用固定步长(每 5 个取 1 个),不是随机抽。**
**因为 x 已经排序,固定步长能让每折覆盖整个区间;随机抽会让某折全落在端点。**
fit 里那个 1e-8 的岭项不能省。12 次多项式在 40 个点上,某些折的基矩阵会接近奇异,
不给一点稳定项,solve 会给出天文数字的系数,CV 误差直接炸到几百。
注意「三个误差」用的是同一批数据、同一套基,唯一的区别是「在哪些点上算」。
所以三条曲线的差异,完全来自「数据有没有参与训练」这一件事。
训练那一条是单调的,这一点在本 Demo 里非常干净:从 0.4894 到 0.0310。
如果你在自己的项目里看到训练误差有反弹,多半是优化没收敛,不是性质不成立。
CV 从 10 次开始剧烈跳动,这是本节最该记住的「负面结果」。
它告诉你:交叉验证不是万能的,样本太少时它自己也不稳。
遇到这种情况,正确做法是增加折数(留一法)、重复多次随机划分,或者干脆退回去选简单模型。
要说明:数据是我按正弦加噪声造的,5 折的划分用固定步长、不是随机抽。
这是有意的:x 排过序,随机抽会让某折全落在端点,估出来的 CV 会失真。
30 秒实验
- 把折数从 5 改成 10,看 CV 曲线会不会变平滑。同时注意计算时间翻了多少倍。
- 把折的划分从固定步长改成随机抽,再跑一遍。CV 的最低点会变到哪一次?这说明什么?
- 对每个次数重复 20 次随机 5 折,把 CV 的标准差算出来。 然后用一倍标准差规则重新选模型,结果和只跑一次差多少?
常见误区
- 「用测试集选参数,反正最后报的也是测试误差」。 这是最普遍也最严重的一个错。 你在测试集上挑过一次,它就参与了决策,报出来的数一定偏乐观。 正确做法是:训练集内部交叉验证选参数,测试集只在最后看一眼。
- 「CV 误差最低的那个就是最好的」。 要看方差。 如果相邻几档的差距小于 CV 自身的标准差,那它们统计上分不出高下,该选简单的。
- 「交叉验证可以随便切」。 时间序列必须按时间切(用过去预测未来), 分组数据必须按组切(同一用户不能同时出现在训练和验证里)。 随机切会让模型偷看到未来或者同一组的信息,误差估得极度乐观。
- 「先标准化再交叉验证」。 顺序反了就是泄漏。 标准化要用训练折的均值方差,再套到验证折上。 在外面统一做完再切,验证折的分布信息已经漏进去了。
自测三问(点开对答案)
1. 为什么训练误差随复杂度单调下降? —— **因为复杂度增加等于搜索空间变大,原来的最优解还在里面,所以最优值只会更小。** **这是「最小化」这个动作的必然结果,和模型好不好没有任何关系。** 2. 5 折交叉验证和留一法,各自适合什么场景? —— **5 折适合数据量中等、要控制计算量的场景;留一法适合样本极少(几十个)的场景。** **留一法几乎无偏,但方差可能很大,而且训练次数等于样本数。** **实践中还有个折中:重复多次 5 折,比单次 10 折更稳。** 3. 如果 CV 曲线在最优附近很平,你会怎么选? —— **选最优附近最简单的那个。** **理由是 CV 本身有噪声,平坦区里的差异多半是噪声,而简单模型更不容易在真实数据上翻车。** **这就是一倍标准差规则,它把「不确定性」也纳入了决策。**第 4 章讲完了两种约束和一套选择流程。下一章换一种约束:ℓ1。 它和 ℓ2 只差一个指数,但结果完全不同——它会把不重要的系数压成精确的零。 这就是稀疏学习,也是特征选择的数学基础。
- ☐ (本章完,下一节 5.1 5.1)