🤖 第4章 带有约束条件的最小二乘法
📄 一句话概括

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 秒实验

  1. 把折数从 5 改成 10,看 CV 曲线会不会变平滑。同时注意计算时间翻了多少倍。
  2. 把折的划分从固定步长改成随机抽,再跑一遍。CV 的最低点会变到哪一次?这说明什么?
  3. 对每个次数重复 20 次随机 5 折,把 CV 的标准差算出来。 然后用一倍标准差规则重新选模型,结果和只跑一次差多少?

常见误区

  1. 「用测试集选参数,反正最后报的也是测试误差」。 这是最普遍也最严重的一个错。 你在测试集上挑过一次,它就参与了决策,报出来的数一定偏乐观。 正确做法是:训练集内部交叉验证选参数,测试集只在最后看一眼。
  2. 「CV 误差最低的那个就是最好的」。 要看方差。 如果相邻几档的差距小于 CV 自身的标准差,那它们统计上分不出高下,该选简单的。
  3. 「交叉验证可以随便切」。 时间序列必须按时间切(用过去预测未来), 分组数据必须按组切(同一用户不能同时出现在训练和验证里)。 随机切会让模型偷看到未来或者同一组的信息,误差估得极度乐观。
  4. 「先标准化再交叉验证」。 顺序反了就是泄漏。 标准化要用训练折的均值方差,再套到验证折上。 在外面统一做完再切,验证折的分布信息已经漏进去了。
自测三问(点开对答案) 1. 为什么训练误差随复杂度单调下降? —— **因为复杂度增加等于搜索空间变大,原来的最优解还在里面,所以最优值只会更小。** **这是「最小化」这个动作的必然结果,和模型好不好没有任何关系。** 2. 5 折交叉验证和留一法,各自适合什么场景? —— **5 折适合数据量中等、要控制计算量的场景;留一法适合样本极少(几十个)的场景。** **留一法几乎无偏,但方差可能很大,而且训练次数等于样本数。** **实践中还有个折中:重复多次 5 折,比单次 10 折更稳。** 3. 如果 CV 曲线在最优附近很平,你会怎么选? —— **选最优附近最简单的那个。** **理由是 CV 本身有噪声,平坦区里的差异多半是噪声,而简单模型更不容易在真实数据上翻车。** **这就是一倍标准差规则,它把「不确定性」也纳入了决策。**

第 4 章讲完了两种约束和一套选择流程。下一章换一种约束:ℓ1。 它和 ℓ2 只差一个指数,但结果完全不同——它会把不重要的系数压成精确的零。 这就是稀疏学习,也是特征选择的数学基础。

相关笔记