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