🤖 第6章 鲁棒学习
📄 一句话概括

40 个点、6 个杠杆点在右端被抬高 7 到 9.5。 最小二乘的斜率 4.168,ℓ1 损失的斜率 2.211,真值 2.000。 污染扫描更刺眼:ℓ2 一路从 2.000 爬到 5.044,ℓ1 到 20% 还是 2.000。 但 ℓ1 在 25% 时突然塌到 5.462,它的稳健也不是无上限的。

📝 本节和前面几节的关系

第 5 章罚的是系数:给参数的绝对值加罚,把不重要的压成零。这一章换一个位置罚——罚残差。 两个位置互不干扰,所以可以任意组合,6.4 会把它们加起来。 为什么需要换位置?因为第 3 到 5 章全都建立在「误差平方」这个前提上。 平方对大误差罚得太狠,一个离群点就能决定整条直线。 鲁棒学习就是把「平方」换成一个长得慢一点的函数。

一、换位置:从罚系数到罚残差

最小二乘的目标是残差平方和。ℓ1 损失最小化把它换成残差绝对值之和。 两者只差一个指数,和 4.2 到 5.1 的关系一样。 性质上有个好听的名字:ℓ2 损失的最优解是「条件均值」,ℓ1 损失的最优解是「条件中位数」。 均值会被极端值拉走,中位数不会。 这就是鲁棒的直觉来源——你换的不是算法,是「用什么统计量代表中心」。 一个常被忽略的细节:ℓ1 损失在一维时可以直接排序取中位数。 高维时没有这个捷径,只能用迭代。 Demo 用的是 IRLS:每轮按当前残差算权重,权重是残差绝对值的倒数,然后解一次加权最小二乘。

二、为什么罚得慢就抗得住

把两种损失摆在一起看残差 6 的情形:ℓ2 罚 18.00,ℓ1 罚 6.00。 差三倍。残差 12 呢?ℓ2 罚 72,ℓ1 罚 12——差六倍。 残差越大,两者的差距越悬殊,所以离群点对 ℓ2 的「话语权」是超线性的。 换一个角度:ℓ1 的损失函数斜率恒为 1,所以它对残差大小不敏感,只关心「偏了多少个」。 ℓ2 的斜率等于残差,残差越大越敏感。 一个离群点的 ℓ2 罚值 18 分,能顶九个正常点(各 2 分);ℓ1 罚值 6 分,只顶六个点。 这个「顶几个点」的说法有个正式版本,叫崩溃点。 ℓ2 的崩溃点是 0:一个足够远的点就能把直线拉过去。 ℓ1 的理论崩溃点是 0.5:污染不到一半,它还能找对方向。 但 Demo 里 25% 就崩了,离 0.5 差得远。 原因是那 6 个点是杠杆点——x 全在右端,位置特别坏。 崩溃点是「最坏情况下的下界」,而实际能不能扛住,还取决于坏点长在哪儿。 这一条是教科书常省略的:鲁棒性的理论保证普遍偏乐观。

三、实测:斜率与崩溃

方法 斜率 截距 参数误差 干净测试 MSE
ℓ2 损失 4.168 2.427 2.596 5.8933
ℓ1 损失 2.211 1.056 0.218 0.0746
参数误差从 2.596 掉到 0.218,干净测试误差从 5.89 掉到 0.075。
差了将近八十倍——这不是「略有改善」,是「从废掉到能用」。
注意 ℓ1 也没有完全恢复真值:斜率 2.211,还是偏了 0.211。
原因有两个:一是六个离群点仍然带着一点权重;二是 IRLS 的 epsilon 让它收敛不到精确的 L1 解。
鲁棒不等于无偏,它只是把「被拽歪的程度」压小了一个数量级。
残差绝对值和那一列也值得看:ℓ2 是 73.98,ℓ1 是 53.86。
ℓ1 的解让这个和更小,符合定义;但反过来,ℓ2 的解让平方和更小。
两个解各自最优于自己的准则,所以「哪个更好」必须先定「用哪个准则评价」。

四、代价:不可导与低效率

ℓ1 损失在残差为零处有一个尖角,所以梯度不存在。 IRLS 的权重是 1 除以残差绝对值,残差接近零时会炸。 代码里那个 1e-6 就是防这个的,它同时也决定了收敛精度。 第二个代价是统计效率。 在误差真的是正态分布时,ℓ1 的效率只有 ℓ2 的 64%。 换个说法:要达到同样的精度,ℓ1 需要多一半的数据。 所以「无脑用 ℓ1」是错的。 正确的判断是:先看数据里有没有离群点。 没有,用 ℓ2;有而且不少,用 ℓ1 或 Huber。 Huber 存在的意义就是「两头都要」——下一节。

五、动手 Demo:ℓ1 损失:斜率从最小二乘的 4.168 拉回 2.211

40 个点,y 等于 2x 加 1 加噪声 0.2,然后把 x 最大的 6 个点分别抬高 7 到 9.5。 两种损失都用 IRLS 求解:每轮按残差改权重,再解加权最小二乘,跑 300 轮。 另留 400 个干净点,用来判断「估出来的直线对不对」。

def l1_fit(A, y, it=300):
    w = np.linalg.lstsq(A, y, rcond=None)[0]
    for _ in range(it):
        r = y - A @ w
        wt = 1.0 / np.maximum(np.abs(r), 1e-6)   # 权重 = 1 除以残差绝对值
        wt = wt / wt.sum()
        sw = np.sqrt(wt)
        w = np.linalg.lstsq(A * sw[:, None], y * sw, rcond=None)[0]
    return w

实跑输出:

--------------------------------------------------------------------
l1 损失:斜率从最小二乘的 4.168 拉回 2.211(真值 2.000)
--------------------------------------------------------------------
  数据                     40 个点,其中 x 最大的 6 个被抬高 7 到 9.5
  真值                     斜率 2.000,截距 1.000,噪声标准差 0.2
  求解                     两种损失都用 IRLS:每轮按残差改权重,再解加权最小二乘

  方法                   斜率    截距   参数误差   残差绝对值和   干净测试 MSE
  ---------------------------------------------------------------------------
  ℓ2 损失(最小二乘)   4.168   2.427      2.596          73.98         5.8933
  ℓ1 损失               2.211   1.056      0.218          53.86         0.0746

  污染比例扫描(x 最大的 k 个点各抬高 8,真斜率 2.000):

  污染点数   污染比例   ℓ2 斜率   ℓ1 斜率
  --------------------------------------
         0         0%     2.000     2.000
         2         5%     2.797     2.000
         4        10%     3.465     2.000
         6        15%     4.047     2.000
         8        20%     4.565     2.000
        10        25%     5.044     5.462

  **ℓ2 那一列从 2.000 一路爬到 5.044,斜率被抬了一倍多。**
  **ℓ1 那一列前五行全是 2.000,小数点后三位没动过——直到 25% 才塌。**
  **为什么差这么多?因为平方把「差 8」变成 64,绝对值只记 8。**
  一个离群点的 64 分,能顶三十个正常点的 2 分。
  所以最小二乘不是「不在意」离群点,是「太在意」了。
  **ℓ1 的罚是线性的,一个离群点只占 8 分,摊不动整条直线。**

  **但 ℓ1 也不是无敌的:它到 25% 就直接塌了(5.462 对真值 2.000)。**
  **理论上它的崩溃点是 0.5,可这里 0.25 就崩——因为坏点是杠杆点,位置全在右端,破坏力比随机撒的强。**
  **而 ℓ2 的崩溃点是 0:一个点就能带偏,只是带偏多少的问题。**
  要说明:**离群点我特意放在 x 最大的那一端**,因为杠杆点的影响最大。**如果均匀撒在中间,同样的偏移对斜率的破坏会小得多。**

  最后把四种损失的形状摆出来对齐着看(同一个残差,四种罚法):

  残差      ℓ2     ℓ1   Huber   图基
  ---------------------------------
    -6   18.00   6.00    5.50   1.50
    -5   12.50   5.00    4.50   1.50
    -4    8.00   4.00    3.50   1.50
    -3    4.50   3.00    2.50   1.50
    -2    2.00   2.00    1.50   1.24
    -1    0.50   1.00    0.50   0.45
    +0    0.00   0.00    0.00   0.00
    +1    0.50   1.00    0.50   0.45
    +2    2.00   2.00    1.50   1.24
    +3    4.50   3.00    2.50   1.50
    +4    8.00   4.00    3.50   1.50
    +5   12.50   5.00    4.50   1.50
    +6   18.00   6.00    5.50   1.50

  **看残差等于正负 6 的那两行:ℓ2 罚 18.00,ℓ1 罚 6.00,Huber 罚 5.50,图基罚 1.50。**
  **ℓ2 是唯一会「随残差平方增长」的:残差翻倍,罚值四倍。**
  **ℓ1 是唯一「永远线性」的:残差再大,罚值也只跟残差同比例。**
  **Huber 在 1 以内跟 ℓ2 一样,超出后改成线性——小残差保留平方的高效率,大残差改成不敏感。**
  **图基更狠:超过 3 之后罚值完全不涨。这就是「重降」,离群得越远越不被当回事。**

  **这张表其实回答了「鲁棒」到底靠什么:靠罚值的增长速度。**
  **平方增长最快,所以最不鲁棒;线性其次;封顶最鲁棒。**
  **但增长速度越慢,正常点上的统计效率就越低;ℓ1 的效率只有 ℓ2 的 64%,它估出来的系数波动更大。**
  **所以没有免费的鲁棒,只有「你更怕离群点,还是更怕估不准」。**
  要说明:**这张表是纯函数值,没有做任何拟合**,**四列用的是同一个残差,可以直接横着比。**

权重是残差的倒数,这就是 ℓ1 的全部机制。 残差大 → 权重小 → 那个点说话不算数。 ℓ2 的权重是 1(所有点一样),所以它没法降谁的权。 wt = wt / wt.sum() 这一行不能省。 不归一化的话,权重整体放大会让加权最小二乘的解不变,但 sqrt 之后数值范围可能溢出或下溢。 归一化之后权重落在 0 到 1 之间,数值上最安全。 为什么用 sqrt 再乘? 因为我们是在解加权最小二乘:最小化权重乘残差平方之和。 把权重开方乘到 A 和 y 上,普通 lstsq 就等于在解加权问题。 这一步是 IRLS 的标准技巧,别绕过它去手写正规方程。 1e-6 那个下限有两个作用:防止除零,也决定了收敛的精度。 它意味着「残差比 1e-6 还小的点,权重封顶在 1e6」。 把下限调小会收敛更准,但迭代更容易抖。 Demo 后半还有一张四种损失的形状对照表,那是把 ℓ2、ℓ1、Huber、 图基四种罚法在同一个残差上算出来的值。它把这一章的四个节串在一条线上:罚值涨得越慢,越抗离群。 要说明:离群点是我特意放在 x 最大那一端的。 如果均匀撒在中间, 同样大小的偏移对斜率的破坏会小得多——杠杆点的破坏力和它的 x 偏离程度成正比。

30 秒实验

  1. 把 6 个离群点的偏移从 7 到 9.5 改成 2 到 3,再跑一遍。ℓ1 还赢吗?差多少?
  2. 把离群点从 x 最大的一端改到 x 最小的一端,重新跑污染扫描。ℓ1 会提前崩还是延后崩?
  3. 把噪声标准差从 0.2 加到 1.0,再跑一遍。ℓ1 的效率损失会不会让它在干净数据上反而不如 ℓ2?

常见误区

  1. 「ℓ1 损失就是 Lasso」。 不是。Lasso 罚的是系数(第 5 章),ℓ1 损失罚的是残差(这一节)。 前者的效果是稀疏,后者的效果是抗离群。 两者可以同时用,那是 6.4。
  2. 「ℓ1 损失能把离群点完全排除」。 不能。它的权重是残差倒数,永远大于零,只是很小。 要真正排除,得用图基损失(6.3),那个权重会恰好等于零。
  3. 「ℓ1 损失一定比 ℓ2 好」。 没有离群点时它更差——统计效率只有 64%。 它的优势只在「数据脏」这个前提下成立,前提不成立时它是纯粹的损失。
  4. 「理论崩溃点 0.5,所以污染 40% 也没事」。 理论下界是针对「随机位置的坏点」的。 Demo 里坏点全是杠杆点,25% 就崩了。 实际能扛多少,必须自己造污染试,不能照抄理论值。
自测三问(点开对答案) 1. 为什么 ℓ1 损失的最优解是「条件中位数」而不是「条件均值」? —— **因为绝对值函数对每个残差只记大小,不记平方。** **把一批数分成两半,移动分界点会让一半的残差增大、一半减小,绝对值之和在一半处最小,** **那就是中位数。** **平方和则是在均值处最小。** 2. IRLS 里权重取残差的倒数,这个权重在物理上意味着什么? —— **意味着「这个点当前的残差有多可信」。** **残差大说明它和大多数点不合群,那就降低它的话语权。** **注意这是一个迭代过程:权重取决于残差,残差又取决于解,所以需要反复几轮才稳定。** 3. 如果数据里一个离群点都没有,用 ℓ1 损失会有什么后果? —— **估计会变差,因为效率只有 ℓ2 的 64%。** **具体表现是系数波动更大——同样的数据重复采样,ℓ1 估出来的系数散布更宽。** **而且 ℓ1 的不可导会让数值求解更麻烦,属于「花了代价什么也没换到」。**

ℓ1 损失的问题是「对大残差还是不放过,只是罚得少」。 下一节换一个折中:小残差照旧罚平方,大残差才改成线性。 这就是 Huber 损失——它有 ℓ2 的效率,也有接近 ℓ1 的稳健。

相关笔记