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

60 个样本、80 维、8 个离群点。 最小二乘的参数误差 3.670, 只加 ℓ1 罚是 1.803,只换 Huber 损失还是 3.670,两个一起用降到 0.493。 「只换 Huber」那一行和最小二乘一模一样,这是本节最值钱的一个负面结果: 80 维对 60 个样本,它能把数据全穿过去,残差全为零,Huber 的权重就全是 1。

📝 本节和前面几节的关系

第 5 章罚系数,第 6 章罚残差。这两件事在公式里是两个不同的加项,所以可以同时用。 这一节的价值不在于「又发明一个方法」,而在于让你看清「两个位置正交」这件事。 看懂之后,整个书里的方法都可以按「罚哪儿」和「怎么罚」两个维度排成一张表。

一、两个位置,四种组合

目标函数有三项:残差项、系数罚项、残差罚项。 严格说第三项是「换掉残差项的形状」,不是加一项。 所以组合是二乘二:残差用平方还是 Huber,系数加不加 ℓ1。

方案 残差项 系数项 擅长什么
最小二乘 平方 无 干净数据下的最高效率
Lasso 平方 ℓ1 稀疏、选变量
Huber Huber 无 抗离群
这一节 Huber ℓ1 既稀疏又抗离群
这张表的四行,正好是本课第 3 到 6 章的四个主角。
所以第 6 章不是「又一个新方法」,是「把第 3 章那个假设换掉」。
岭回归放在哪?它是第五行:残差平方、系数加 ℓ2。
把 ℓ2 和 ℓ1 都加上,就是 5.5 的弹性网。
于是六个方法排成了一张三列的表:残差怎么罚、系数罚不罚、罚哪个范数。

二、那个一模一样的行,为什么

Demo 里「Huber,无罚项」那一行,非零 80 个、参数误差 3.670,和最小二乘一模一样。 换成鲁棒损失,居然一点用都没有。 原因是维度比样本多:80 个参数、60 个方程,最小二乘有足够的自由度把每个点都穿过去。 残差全是零,Huber 的权重要么是 1(残差小于 δ), 要么是 δ 除以残差(残差大于 δ)——残差为零时全是 1。 权重全是 1,IRLS 第一轮之后就不动了,等于普通最小二乘。 这个结果的教训很硬:鲁棒损失的前提是「残差能反映出谁不正常」。 而一个过参数化的模型会把残差消掉,于是鲁棒机制被自己的自由度架空了。 先有约束、后有鲁棒——顺序不能反。 这也解释了为什么「深度学习加鲁棒损失」经常没用:网络参数远多于样本, 训练集上残差本来就接近零。 要让鲁棒损失起作用,得先有正则化,或者在有噪声的批上训练。

三、实测:四行对照

方案 非零个数 参数误差 干净测试 MSE
最小二乘 80 3.670 13.8813
ℓ1 罚项(平方损失) 34 1.803 3.3567
Huber,无罚项 80 3.670 13.8813
ℓ1 罚项加 Huber 9 0.493 0.2617
最后一行:非零 9 个(真值 5 个),参数误差 0.493,测试误差 0.26。
比单用任何一个都好一个量级。
注意它选出的 9 个里有 4 个是噪声。
这很正常:λ 是在「干净数据上正好选出 5 个」这个标准下定的,加了离群点之后最优 λ 会变。
参数误差 0.493 而不是 0,也说明那 4 个噪声项带了一点偏差进来。
「ℓ1 罚项,平方损失」那一行非零 34 个,而不是 5 个左右。
因为离群点把每个系数的 rho 都撑大了,软阈值的门槛 λ 相对就变小了,于是关不掉。
这也是一句实话:脏数据会让稀疏失效。

四、怎么实现:两层循环

外层管损失,内层管罚项。 外层算残差、算 Huber 权重;内层用这些权重解一个加权 lasso。 重复十几轮,两者一起稳定下来。 加权 lasso 的坐标下降和 5.1 的几乎一样,只改两处:内积乘权重,分母也乘权重。 因为加权最小二乘的一维子问题,分母就是加权后的列平方和。 代码里就是 (wt * A[:, j]) @ r 和 (A * A * wt[:, None]).sum(0) 这两行。 这两层循环的收敛性没有严格保证——外层是非线性加权,内层是凸的。 实践中十几轮就够,因为权重的变化很快趋于稳定。 如果要求严格,可以监控「连续两轮权重的最大变化」,小于阈值就停。 代价是计算量:内层每轮要扫 d 个坐标,外层要跑十几轮。 Demo 里 60 个样本、80 维、内外各 200 乘 12,跑完大约几秒。 维度再高一个量级,就得换成更聪明的算法(比如把两边合成一个增广的 lasso)。

五、动手 Demo:ℓ1 加 Huber:最小二乘误差 3.670,两个一起用降到 0.493

60 个样本、80 维,真值只有 5 个非零,另把 8 个点抬高 5 到 6。 λ 固定为 8(在干净数据上这个值正好选出 5 个),δ 固定为 0.8。 跑四种方案:最小二乘、只加 ℓ1 罚、只换 Huber 损失、两个一起用。 干净测试集是 300 个点,y 不含噪声也不含离群点。

def wlasso(A, y, lam, wt, it=200):
    col = (A * A * wt[:, None]).sum(0) + 1e-12      # 分母带权重
    w = np.zeros(A.shape[1])
    r = y - A @ w
    for _ in range(it):
        for j in range(A.shape[1]):
            r += A[:, j] * w[j]
            rho = float((wt * A[:, j]) @ r)         # 内积也带权重
            w[j] = np.sign(rho) * max(abs(rho) - lam, 0.0) / col[j]
            r -= A[:, j] * w[j]
    return w

w = wlasso(X, y, lam, np.ones(n))                   # 先当所有权重都是 1
for _ in range(12):                                 # 外层改权重,内层解 lasso
    w = wlasso(X, y, lam, huber_w(X, y, w), it=200)

实跑输出:

--------------------------------------------------------------------
l1 加 Huber:最小二乘参数误差 3.67,两个一起用降到 0.49
--------------------------------------------------------------------
  数据                     60 个样本、80 维,真值 5 个非零,另有 8 个点被抬高 5 到 6
  干净测试                 300 个点,y 不含噪声也不含离群点
  参数误差                 估计系数与真系数之间的欧氏距离

  方案                       非零个数   参数误差   干净测试 MSE
  ------------------------------------------------------------
  最小二乘                         80      3.670        13.8813
  l1 罚项(平方损失)              34      1.803         3.3567
  l2 损失(Huber,无罚项)         80      3.670        13.8813
  l1 罚项加 Huber 损失              9      0.493         0.2617

  **四行正好是四种组合:不加、只加罚、只换损失、两个都加。**
  **第一行非零 80 个全是开的、参数误差 3.670:80 维对 60 个样本,它能把数据全部穿过去。**
  **第二行非零 34 个,稀疏有了一部分,参数误差还有 1.803。**
  原因是 8 个离群点在平方损失下每个要付差不多 18 分,整条解都被拽歪了。
  **第三行最值得看:非零 80 个、参数误差 3.670,和第一行一模一样。**
  **换成 Huber 损失,居然一点用都没有。**
  **因为维度比样本多,最小二乘能把 60 个点全部穿过去,残差全是零。**
  **残差是零,Huber 的权重就全是 1,于是它就退化成最小二乘——鲁棒的前提是「残差能看出谁不正常」,而它把残差全消掉了。**
  **这是个很硬的教训:鲁棒损失救不了过参数化的模型,因为模型有的是自由度去迁就坏点。**
  **第四行非零 9 个、参数误差 0.493——先靠 ℓ1 把自由度掐掉,残差才有意义,Huber 才使得上劲。**

  为什么能拆开加?因为罚项和损失是两个独立的位置:
  **罚项罚的是系数,损失罚的是残差。**
  所以理论上可以任意组合:ℓ1 罚加 ℓ2 损失、ℓ2 罚加 ℓ1 损失、ℓ1 罚加 Huber 损失。
  **教材把它们分成第五章和第六章,就是这个原因:它们正交。**
  要说明:**λ 和 δ 都是我在这份数据上调过的**,真实项目要用交叉验证。**λ 取 8 是因为干净数据在这个值下正好选出 5 个。**

第一行「先当所有权重都是 1」很重要:它是普通 lasso 的解,是整条迭代的起点。 从它出发,Huber 权重才有意义——用最小二乘解出发也行,但会被离群点带偏。 (wt * A[:, j]) @ r 这一步开销是 O(n),而 A * A * wt[:, None] 是 O(nd) 但只算一次。 所以每轮扫 d 个坐标的总开销是 O(nd),和 5.1 一样。 加权没有让它变贵,这是坐标下降的一个好处。 内层次数 200 是偏少的。 加权 lasso 的收敛速度取决于列之间的相关性,200 轮在独立特征上够了,相关特征上可能不够。 判断标准是「再跑 200 轮,解还变不变」。 外层次数 12 也是经验值。 它的作用是让权重稳定:前几轮权重变化大,后面就几乎不动了。 如果数据里离群点和正常点纠缠不清,权重会来回跳,这时候加轮数没用,得换方法。 最后一行那个「9 个非零」值得记住:它比真值多 4 个。 稀疏方法从来没有「恰好选对」的保证,只有「选得少且大致对」的倾向。 真要精确选变量,得看 λ 路径、或者用稳定性选择。 要说明:λ 和 δ 都是我在这份数据上调过的,真实项目要用交叉验证。 λ 取 8 的依据是「干净数据上正好选出 5 个」, 这个依据本身也需要干净数据——而真实项目里你没有干净数据,所以只能靠交叉验证。

30 秒实验

  1. 把 8 个离群点的偏移从 5 到 6 改成 20,再跑四种方案。「ℓ1 罚项加 Huber」还能压住吗?
  2. 把 δ 从 0.8 改成 0.2 和 5.0,看第四行的非零个数和参数误差怎么变。 δ 影响的是「谁被降权」,为什么会影响稀疏性?
  3. 把样本数从 60 加到 200(维度仍 80),再跑。 「Huber 无罚项」那一行还会等于最小二乘吗?为什么?

常见误区

  1. 「先把两个方法拼起来,效果一定更好」。 不一定。两个方法都要调超参,拼起来是两个超参的二维网格,调参成本翻倍。 而且如果数据里没有离群点,Huber 那一半是纯损失(效率、实现复杂度)。
  2. 「鲁棒损失能救过参数化的模型」。 Demo 里第三行就是反例:80 维对 60 个样本,Huber 和最小二乘的结果一模一样。 模型把残差消掉了,鲁棒机制就没有输入。 先约束、再鲁棒,顺序不能反。
  3. 「两层循环一定能收敛到全局最优」。 外层是非凸的(权重依赖于解),所以只有「收敛到一个稳定点」的保证,没有全局最优的保证。 实践中它表现很好,但别把它当成凸优化问题来对待。
  4. 「加权 lasso 就是把权重乘到 X 和 y 上,然后调普通 lasso」。 数学上等价——把权重开方乘上去,加权最小二乘就变成普通最小二乘。 但代码里更常见的写法是把权重直接放进内积和分母,因为这样不用改数据、省一次内存拷贝。
自测三问(点开对答案) 1. 为什么「罚系数」和「罚残差」可以同时做,而不会互相抵消? —— **因为它们在目标函数里是两个独立的加项,作用于不同的变量。** **罚系数只看 w 的大小,罚残差只看 y 减 Aw 的大小。** **两个力方向不同:一个把 w 往零拉,一个改「残差怎么算账」的方式。** **所以它们不冲突,只是让问题更难解。** 2. 为什么「先约束、后鲁棒」这个顺序不能反? —— **因为鲁棒机制靠残差判断谁不正常。** **模型自由度太多时,它会把所有残差都消到零,鲁棒机制拿不到判断依据。** **先加约束(ℓ1、ℓ2 或者降维)把自由度压下来,残差才有信息量。** 3. 这一节的方法和 5.5 的弹性网,是同一类东西吗? —— **不是。弹性网是「两个罚项都加在系数上」,解决的是相关特征和不稳定的问题。** **这一节是「一个罚项在系数、一个改动在残差」,解决的是稀疏加抗离群。** **它们可以三个一起用:ℓ1 加 ℓ2 加 Huber——三个位置,各管一件事。**

第 6 章结束,第二部分「有监督回归」也走完了: 最小二乘、约束、稀疏、鲁棒,四章讲的其实是同一件事的四个侧面。下一章换问题类型—— 从「预测一个数」变成「判断属于哪一类」。 而起点仍然是那个熟悉的动作:最小二乘。

相关笔记