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

同一个问题,δ 从 10 降到 0.1:斜率从 4.168 单调滑到 2.185,真值 2.000。 两端就是两个老朋友——δ 很大退化成 ℓ2,δ 很小退化成 ℓ1。 这次扫出来不是 U 形,是一条单调的斜线:越小越好。 但 δ 等于 0.1 时有 28 个点超阈,等于把正常点也降了权,那个「更好」是假象。

📝 本节和前面几节的关系

6.1 的 ℓ1 损失抗离群,但代价是效率只有 64%,而且处处不可导。 这一节要一个两头都占的东西:残差小的地方像 ℓ2,残差大的地方像 ℓ1。 做法很简单:拿一个阈值 δ 把两种损失拼起来。 δ 以内用平方,δ 以外用线性,并且让两段在 δ 处接上、斜率也接上。 这个「接缝要平滑」的要求,正好让它变得可导——于是解唯一、算法稳定。

一、两段拼接,接缝要平滑

Huber 损失的定义:残差绝对值不超过 δ 时罚「二分之一残差平方」, 超过 δ 时罚「δ 乘(残差绝对值减二分之一 δ)」。 写成式子之后可以验证:在 δ 那一点,两段的值相等、导数也相等,都是 δ。 这个「导数连续」的性质,是 Huber 和 ℓ1 最本质的差别。 ℓ1 在零点导数从负 1 跳到正 1;Huber 处处有导数。 所以 Huber 是凸且光滑的,解唯一,IRLS 一定收敛。 δ 的意义可以直白地说:它是「多大的残差还算正常」这条分界线。 小于 δ 的点按平方罚,等于承认它们是噪声;大于 δ 的点按线性罚,等于把它们降级成可疑点。 它比 ℓ1 温和的地方在于:超阈的点权重是 δ 除以残差,不是残差的倒数。 分母里少了那个常数倍,所以权重更大——离群点仍然有一点发言权。 这既是它的弱点(抗不住极端离群),也是它的优点(不会把有用信息一起扔掉)。

二、IRLS:权重只有两种形态

Huber 的 IRLS 权重特别干净:残差在 δ 以内是 1,超过 δ 是 δ 除以残差绝对值。 没有第三种形态,也不需要调步长。 这个权重函数是单调的:残差越大,权重越小,但永远不归零。 图基损失(6.3)会把权重摁到零,那叫「重降」;Huber 只是「降」,不是「重降」。 这个区别决定了 Huber 凸、图基不凸。 实现上,它和 6.1 的代码只差一行:把「1 除以残差」换成「1 或 δ 除以残差」。 但这一行的差别让它的收敛性质完全变了:ℓ1 的权重会趋于无穷,Huber 的权重有下界吗? 也没有,但它衰减得慢——残差 100 时权重还有 δ 除以 100,而 ℓ1 那时权重是 0.01。

三、实测:一条单调的斜线

δ 斜率 参数误差 超阈点数 干净测试 MSE
0.1 2.185 0.194 28 0.0674
0.3 2.229 0.250 10 0.0885
1.0 2.580 0.672 6 0.4215
3.0 3.599 1.898 6 3.1520
10.0 4.168 2.596 0 5.8933
这张表最该注意的不是最低点,是它没有最低点。
δ 越小越好,一路单调——按调参的思路看,这是个「坏结果」。
原因是这份数据的离群点偏了 7 到 9.5,任何小于 7 的 δ 都能把它们降权,δ 越小降得越狠。
而正常点的残差都在 0.2 量级,δ 只要大于 0.5 就几乎不影响它们。
所以中间那一大段 δ 的效果几乎一样,越往小走越接近 ℓ1。
「超阈点数」这一列给出了识别假象的办法。
δ 等于 0.1 时 28 个点超阈——40 个点里有 28 个被降权,那不叫鲁棒,那叫「几乎不信任何点」。
噪声标准差是 0.2,合理的 δ 应该在 0.2 到 0.4 之间。
0.1 已经在噪声水平以下了。

四、δ 到底怎么取

教科书的标准答案是 1.345 倍残差的标准差。 这个系数不是凑的:在残差服从正态分布时,它让 Huber 估计的效率达到 95%。 也就是说,几乎不损失效率,却换来了抗离群的能力。 问题是「残差的标准差」本身会被离群点污染。 所以实践里先用一个稳健的尺度估计,最常用的是 MAD:残差绝对值的中位数乘 1.4826。 1.4826 这个常数是为了让 MAD 在正态分布下等于标准差。 这条链条值得记:先用中位数(而不是均值)估中心,再用 MAD(而不是标准差)估散布, 最后用 1.345 倍 MAD 定 δ。 三步都用稳健统计量,整个流程才不会被离群点带偏。 如果用普通标准差去定 δ,离群点会先把 δ 撑大,然后 δ 就失效了。 还有一个实践细节:δ 要按 y 的量纲来定,所以建模前最好把 y 标准化。 否则同样一份数据,换个单位(元换成万元),δ 就要跟着改。

五、动手 Demo:Huber 损失:δ 从 0.1 到 10,斜率从 2.185 滑到 4.168

和 6.1 同一份数据:40 个点、6 个杠杆点在右端被抬高 7 到 9.5。 对 δ 取 0.1、0.3、1.0、3.0、10.0 五个值,用 IRLS 各跑 300 轮,记录斜率、截距、参数误差、超阈点数、 以及干净测试集上的误差。

def huber_fit(A, y, d, it=300):
    w = np.linalg.lstsq(A, y, rcond=None)[0]
    for _ in range(it):
        r = y - A @ w
        wt = np.where(np.abs(r) <= d, 1.0, d / np.abs(r))   # 超阈就降权
        sw = np.sqrt(wt)
        w = np.linalg.lstsq(A * sw[:, None], y * sw, rcond=None)[0]
    return w

实跑输出:

--------------------------------------------------------------------
Huber 损失:δ 从 0.1 到 10,斜率从 2.185(近 l1)滑到 4.168(近 l2)
--------------------------------------------------------------------
  数据                     同 D018:40 个点、6 个离群、抬高 7 到 9.5
  损失                     残差小于 δ 时罚平方,大于 δ 时罚线性
  参照                     ℓ2 斜率 4.168,ℓ1 斜率 2.211,真值 2.000
  求解                     IRLS:权重在 1 和 δ 除以残差之间切换,损失本身是凸的

     δ    斜率    截距   参数误差   超阈点数   干净测试 MSE
  --------------------------------------------------------
   0.1   2.185   1.059      0.194         28         0.0674
   0.3   2.229   1.100      0.250         10         0.0885
   1.0   2.580   1.340      0.672          6         0.4215
   3.0   3.599   2.023      1.898          6         3.1520
  10.0   4.168   2.427      2.596          0         5.8933

  **看斜率那一列:δ 等于 0.1 时 2.185,δ 等于 10 时 4.168。**
  两端就是两个老朋友:δ 很小,Huber 退化成 ℓ1;
  δ 很大,退化成 ℓ2。
  **「超阈点数」这一列是 δ 的直接含义:残差超过 δ 的点被当成可疑点,权重从 1 降到 δ 除以残差。**
  **中间那几行是它自己的样子,但这次不是 U 形,是一条单调的斜线——越小越好。**
  **这个结果是这份数据的特性,不是 Huber 的规律:离群点偏了 7 到 9.5,任何小于 7 的 δ 都能把它们降权,δ 越小降得越狠。**
  **但看超阈点数就露馅了:δ 等于 0.1 时有 28 个点超阈,等于把正常点也当成了可疑点。**
  **噪声标准差是 0.2,合理的 δ 该在 0.2 到 0.4 之间;0.1 已经在噪声以下,那一行「更好」是假象。**

  **Huber 最值钱的地方是「凸」:导数是连续的,所以解唯一、IRLS 一定收敛。**
  **ℓ1 在零点有个尖角,Tukey 在大残差处会掉头,只有 Huber 两头都平滑。**
  **代价是它抗不住「特别离谱」的点:残差 9.5 的点权重是 δ 除以 9.5,不是零,还是有影响。**
  **所以它是「温和的鲁棒」,不是「彻底的鲁棒」。**
  要说明:**δ 是我在干净测试集上比出来的**,真实项目里没有这条信息。**常用做法是按噪声尺度取:δ 大约等于 1.345 倍残差标准差,这是正态下效率 95% 的经典值。**

np.where 那一行就是 Huber 的全部内容。 对比 6.1 的 1.0 / np.abs(r):这里把「δ 以内」的权重钉死成 1。 这一钉很关键——它保证了正常点不打折,效率才保得住。 注意这里没有 1e-6 那种下限。 因为 δ 大于零时,np.abs(r) 只在超阈分支里出现,而超阈意味着它已经大于 δ。 分母永远不会接近零,所以不需要防。 这是 Huber 比 ℓ1 好实现的一个具体好处。 初值取最小二乘,这是个坏初值——它已经被离群点带歪了(斜率 4.168)。 Huber 依然能从它收敛到 2.185,因为目标是凸的,只有一个坑。 这一点和 6.3 的图基损失形成对照:那个对初值敏感得多。 δ 等于 3.0 和 10.0 那两行,超阈点数都是 6 和 0,但斜率差很多。 δ 等于 3.0 时超阈的正好是那 6 个离群点(残差 7 到 9.5 都大于 3),权重降到 3 除以 8 左右; δ 等于 10 时一个都不超阈,等于纯最小二乘。 所以「降权」和「不降权」之间没有中间态,δ 一旦越过 9.5 就彻底失效。 要说明:δ 是我在干净测试集上比出来的,真实项目里没有这条信息。 正确做法是按 1.345 倍 MAD 取,或者直接把这个 δ 也放进交叉验证里当超参。

30 秒实验

  1. 把 δ 固定在 0.3,逐步增加污染点数(2、4、6、8、10),看斜率怎么变。它比 ℓ1 崩得早还是晚?
  2. 用 MAD 自动算 δ(残差绝对值中位数乘 1.4826 再乘 1.345),看算出来是多少。 和表里最好的那一格差多少?
  3. 把 6 个离群点的偏移改成 40(远超 δ),再跑一遍。Huber 还能拉回来吗?为什么?

常见误区

  1. 「Huber 就是 ℓ1 的平滑版」。 方向对,但漏了要点:Huber 在 δ 以内是平方,所以它保留了 ℓ2 的效率。 ℓ1 是处处线性,效率只有 64%;Huber 在正常点上和 ℓ2 完全一样。
  2. 「δ 越小越鲁棒,所以取个很小的值」。 δ 小到噪声水平以下,正常点也会被降权,等于把 ℓ2 的效率优势全扔了。 Demo 里 δ 等于 0.1 时 28 个点超阈,那已经不是鲁棒了。
  3. 「Huber 能完全排除离群点」。 不能,权重是 δ 除以残差,永远大于零。 偏移 40 的点权重还有 δ 除以 40,虽然很小但确实还在。 要彻底排除必须用重降损失,代价是非凸。
  4. 「δ 用普通标准差乘 1.345 就行」。 普通标准差本身会被离群点撑大,δ 跟着变大,然后失效。 必须用稳健尺度,比如 MAD。
自测三问(点开对答案) 1. Huber 的两段为什么必须在 δ 处「导数也相等」? —— **因为只有导数连续,整个损失函数才可导,才能保证是光滑凸函数、解唯一。** **如果只让两段的值相等而斜率不等,δ 处会出现一个折点,那就退化成 ℓ1 那类不可导问题了。** **线性段写成「δ 乘(残差绝对值减二分之一 δ)」而不是别的形式,** **正是为了让斜率在那里等于 δ。** 2. 为什么说 Huber 是「用偏差换效率」的典型? —— **它在正常点上和 ℓ2 一样(效率高),在离群点上和 ℓ1 一样(影响有上限)。** **代价是它对「稍微大一点」的正常残差也做了降权,引入了少量偏差。** **δ 就是这个交换的旋钮:δ 大,偏差小、鲁棒弱;δ 小,偏差大、鲁棒强。** 3. 如果残差分布是重尾的,但不是离群点,Huber 还合适吗? —— **合适,而且往往比 ℓ2 更好。** **重尾意味着「大残差是常态」,不是「坏点」。** **ℓ2 会把这些常态的大残差当成大事来罚,导致模型被少数几个尾部点主导;** **Huber 会温和地处理它们。** **这时 δ 要按尾部的尺度取,不能按核心的尺度取。**

Huber 给超阈的点降权,但权重永远大于零。下一节再狠一点:超过阈值直接归零,一点话语权都不给。 这就是图基损失——它换来更强的鲁棒,也换来一个不凸的目标函数。

相关笔记