c 取 1 到 5,图基损失都把 6 个离群点的权重摁成了精确的 0: 斜率 2.070 到 2.071,参数误差 0.070,几乎就是真值。 代价在另一张表上:在参数平面上数局部极小,ℓ2 和 Huber 各只有 1 个坑,图基有 2 个。 另一个坑在 (4.90, 3.84)——那是「把离群点当正常点」的解。
6.2 的 Huber 给离群点降权,但权重永远大于零,所以极度离谱的点还是能推一把。 这一节把「降权」改成「开除」:超过阈值,权重恰好为零。 换来的是更强的鲁棒,付出的是一个不凸的目标函数。 这是本章里唯一一个「理论上更优、实际上更难用」的方法—— 它的价值在于让你看清「鲁棒」和「可解」之间的取舍到底有多硬。
一、有界损失:罚值封顶
图基损失在 c 以内是一条 S 形曲线,超过 c 之后罚值封顶,恒等于 c 平方除以 6。 注意「封顶」这个动作:残差从 c 涨到 10c,罚值一点不涨。 这就是「有界损失」这个名字的来历——再离谱的点,它能造成的损失也有上限。 ℓ2 无界(平方增长),ℓ1 无界(线性增长),只有图基这种是有界的。 权重是「1 减 (残差除以 c) 的平方」的平方,在 |残差| 等于 c 时恰好为零。 这个「恰好为零」是关键:它让离群点彻底退出,不留一点残余影响。 这个性质有个正式名字叫重降影响函数。 影响函数衡量「单个点能把估计推多远」; 重降的意思是:随着点越来越离谱,它的影响力先增后减,最后归零。 Huber 的影响函数是有界的但不归零;ℓ2 的影响函数无界(越离谱推得越远)。
二、为什么它不凸
权重一旦会归零,目标函数就不再凸。 直观的理由:一旦某个点被开除,它就完全不参与;而「开除谁」取决于当前的解。 于是不同的解会开除不同的点,等于在不同的子集上做优化——这些子问题的解互不相通。 这个现象有个名字叫「多模态」。 Demo 里我把参数平面切成 0.02 的网格,严格比较每个点和它的八个邻居,数出几个坑。 ℓ2 一个,Huber 一个,图基两个。 两个坑分别对应两种解释:一个相信「那 6 个点是坏的」,得出斜率 2.08; 另一个相信「那 6 个点是对的」,得出斜率 4.90。 两个解在各自的逻辑里都成立,数据本身分不出谁对。 这就是非凸的实质:不是算法不够好,是问题本身有两个答案。 所以图基损失必须配一个好初值。 标准做法是先跑 ℓ1 或 Huber(它们凸、有唯一解),再拿那个解当图基的起点。 这叫「用凸方法给非凸方法找起点」,是稳健统计里的常规套路。
三、实测:归零与两个坑
| c | 斜率 | 参数误差 | 权重归零点数 | 干净测试 MSE |
|---|---|---|---|---|
| 1.0 | 2.071 | 0.071 | 6 | 0.0418 |
| 2.0 | 2.071 | 0.071 | 6 | 0.0417 |
| 3.0 | 2.070 | 0.070 | 6 | 0.0417 |
| 5.0 | 2.070 | 0.070 | 6 | 0.0416 |
| 10.0 | 3.382 | 1.637 | 0 | 2.3496 |
| c 从 1 到 5 这四行,结果几乎一模一样:斜率都在 2.070 附近,归零的都是 6 个。 | ||||
| 这说明 c 只要「小于离群偏移、又大于正常残差」,具体取多少不敏感。 | ||||
| 这个稳健性比 Huber 的 δ 还好——Huber 在 δ 等于 0.1 到 10 之间,斜率从 2.185 变到 4.168。 | ||||
| c 等于 10 那一行就失效了:一个点都没归零,斜率 3.382。 | ||||
| 但注意它不是最小二乘的 4.168——因为 c 以内那条 S 形曲线还在往下压,它本来就不是平方。 | ||||
| 这一点容易被误解成「c 大了就退化成 ℓ2」,其实只是「慢慢靠近」。 | ||||
| 参数误差 0.070 这个数比 6.1 的 0.218 和 6.2 的 0.194 都小一个量级。 | ||||
| 因为那 6 个点被彻底排除,剩下的 34 个点上它就是普通最小二乘,估得最准。 |
四、什么时候值得用它
三种情况:离群点极其离谱、你确定它们就是坏数据、而且你能给一个好初值。 三者缺一个,就不该用图基。 反过来说,如果离群点只是「稍微偏一点」,用 Huber 更稳。 因为图基会把「偏得比较多但其实是正常数据」的点也一起开除,那是真的丢信息。 它的归零是硬截断,没有「半信半疑」这个档位。 工程上还有一个隐患:归零是不可逆的。 一旦某个点在某一轮被开除,它后面就再也没有机会回来。 如果初值很差,可能一开始就开除了正确的点,然后越走越偏。 这就是为什么初值必须是 ℓ1 或 Huber 的解。 最后一句实话:本书把它放在 6.3,是当作「损失函数谱系」的一环讲的。 真实项目里,图基损失的使用频率远低于 Huber。 它的主要价值是让你理解「有界损失」和「重降」这两个概念,而不是让你去用它。
五、动手 Demo:图基损失:c 等于 1 时斜率 2.071,参数平面上数出 2 个局部极小
同一份 40 点数据。 c 取 1、2、3、5、10 五个值,用 IRLS 各跑 300 轮,记录斜率、参数误差、权重归零点数。 然后换一件事:在斜率负 0.5 到 5、截距负 2 到 5 的网格上(步长 0.02)把 ℓ2、Huber、 图基三种损失的总和都算出来,严格比较每个网格点和它八个邻居,数出局部极小有几个。
r = y - A @ w
u = r / c
wt = np.where(np.abs(u) < 1.0, (1.0 - u * u) ** 2, 0.0) # 超阈权重恰好 0
sw = np.sqrt(wt)
w = np.linalg.lstsq(A * sw[:, None], y * sw, rcond=None)[0]
实跑输出:
--------------------------------------------------------------------
图基损失:c 等于 1 时斜率达 2.071,参数平面上只数出 2 个局部极小
--------------------------------------------------------------------
数据 同 D018:40 个点、6 个离群、抬高 7 到 9.5
损失 残差小于 c 时是一条 S 形曲线,超过 c 之后罚值封顶不再增长
权重 超过 c 的点权重恰好为 0,不是「很小」而是「完全没有」
c 斜率 截距 参数误差 权重归零点数 干净测试 MSE
------------------------------------------------------------
1.0 2.071 0.995 0.071 6 0.0418
2.0 2.071 0.998 0.071 6 0.0417
3.0 2.070 0.998 0.070 6 0.0417
5.0 2.070 0.999 0.070 6 0.0416
10.0 3.382 1.877 1.637 0 2.3496
**c 等于 1 那一行:斜率 2.071,参数误差 0.071,几乎就是真值。**
**因为 6 个离群点的残差都在 7 以上,远超 c,权重被摁成 0,它们对解毫无影响。**
**c 等于 10 那一行:权重归零 0 个,斜率 3.382,往最小二乘的 4.168 靠过去了。**
**但它不等于最小二乘——权重 (1 减 u 平方) 的平方在 c 以内也在往下压,所以只靠过去一半。**
**c 从 1 到 5 这四行结果几乎一模一样(斜率都在 2.07),归零的都是 6 个。**
**这说明 c 只要「小于离群偏移、又大于正常残差」,具体取多少不敏感。**
**它比 Huber 更狠:Huber 给离群点的权重是 δ 除以残差,永远不为零;图基是直接归零。**
**这个性质叫「重降影响函数」——离群得越厉害,影响反而越小,直到归零。**
代价是它不再凸。我在斜率负 0.5 到 5、截距负 2 到 5 的网格上把三种损失都数了一遍:
损失 局部极小个数 最好的那个(斜率, 截距)
----------------------------------------------------------
ℓ2 1 (4.16, 2.42)
Huber(δ 等于 1) 1 (2.58, 1.34)
图基(c 等于 3) 2 (2.08, 1.00)
**ℓ2 和 Huber 都只数出 1 个坑,所以不管从哪儿出发,滚下去都是同一个地方。**
**图基数出 2 个坑,最好的那个是 (2.08, 1.00),另一个是:**
(4.90, 3.84) 损失 36.70
**那个 (4.90, 3.84) 就是「把离群点当正常点」的那条直线——它也是图基损失的一个局部最优。**
**这就是非凸的代价:没有唯一答案,滚进哪个坑取决于你从哪儿出发。**
**所以图基损失必须配好初值:先跑 ℓ1 或 Huber,再拿它的解当起点。**
要说明:**局部极小是在 0.02 步长的网格上严格比较八个邻居数出来的**,**平地(所有点都被封顶的那个区域)不算极小,所以数量比想象中少。**
np.where 的第二个分支写的是 0.0,不是「很小的数」。
这个 0 是图基和 Huber 的分水岭。
Huber 在同样位置写的是 d / np.abs(r),永远大于零。
(1 - u 平方) 的平方 这个形状值得看一眼:u 等于 0 时权重 1,u 等于 0.5 时权重 0.56,
u 等于 1 时权重 0。
它在中间就开始掉了,所以图基对「稍微偏一点」的点也在降权——这是它丢信息的地方。
wt.sum() < 3 那个保护是必须的。
如果所有点都被开除,权重全零,加权最小二乘就退化成解 0 等于 0,lstsq 会返回垃圾。
所以代码里一旦发现有效点少于三个就停机,保留上一轮的解。
局部极小那一段用的是 sliding_window_view。
它把网格切成一个个 3 乘 3 的窗口,然后把中心那格取出来、八邻居取最小值,比较。
注意必须用「严格小于」:图基损失在大残差区域是一片平地,用「小于等于」会数出几百个假的坑。
我在设计这个 Demo 之前以为会数出几十个坑,实际只有 2 个。
原因是「平地不算坑」这条规则把大量伪极小排除了。
这个结果是诚实的:图基的非凸是「真有两个解」,不是「到处是坑」。
要说明:局部极小是在 0.02 步长的网格上数的,
它数的是「网格上的严格极小」,和连续意义下的极小不一定一一对应,但足以说明「解不唯一」。
30 秒实验
- 把网格步长从 0.02 改到 0.005,重新数局部极小。个数会变吗?变的话说明什么?
- 把 c 改成 1,再数一遍局部极小。c 越小,坑越多还是越少?
- 用最小二乘解当图基的初值(斜率 4.168 那边),和用真值当初值,结果差多少? 这就是「初值敏感」的量化。
常见误区
- 「图基损失比 Huber 好,因为它更鲁棒」。 鲁棒性更强是真的,但「更好」不成立。 它丢信息更狠(把「偏得多但正常」的点也开除),而且非凸、依赖初值。 实践里 Huber 用得更多。
- 「c 越大越接近最小二乘,所以取大一点安全」。 c 大到超过所有残差,所有点都不归零,图基就彻底失效(Demo 里 c 等于 10 那一行)。 但即使失效,它也不等于最小二乘——那条 S 形曲线本身就在压权重。
- 「IRLS 能给非凸问题找到全局最优」。 不能。IRLS 只是「反复解加权最小二乘」,它在一个非凸问题上只能保证收敛到一个局部最优。 Demo 里那两个坑就是证据。
- 「权重归零等于这个点被删掉了」。 近似成立,但不完全:权重取决于当前残差,下一轮解变了,权重可能又变回非零。 只有当解稳定下来、那些点持续超阈时,才真的等于被删。
自测三问(点开对答案)
1. 为什么图基损失的权重会「越离谱越小,最后归零」,而 Huber 只是「变小」? —— **因为图基的损失是有界的:超过 c 之后罚值封顶,所以再怎么偏,损失都一样。** **损失不随残差增长,它对残差的导数就是零——导数为零意味着「这个点对最优解没有推力」。** **Huber 的损失是线性增长,导数恒为 δ,永远有一股推力。** 2. 非凸在这件事上具体坏在哪里? —— **坏在「算法停下来不一定是答案」。** **凸问题里任何局部最优都是全局最优;** **非凸问题里可能停在 (4.90, 3.84) 这个坑,而真答案是 (2.08, 1.00)。** **更麻烦的是你没法从结果本身判断停在了哪个坑——两个解的残差结构都是「自洽」的。** 3. 如果一定要用图基损失,工程上该怎么做? —— **三步:先用 Huber 或 ℓ1 得到一个凸的、唯一的解;再拿它当图基的初值;** **最后用多个初值各跑一遍,如果结果一致才敢用。** **如果不同初值给出不同答案,说明数据本身分不清,这时候该退回 Huber。**第 6 章到此把「损失函数」这条路走完了:ℓ2、ℓ1、Huber、图基。 最后一节把两章的东西接起来——ℓ1 罚系数加 Huber 罚残差。 一个是稀疏,一个是抗噪,两个位置正交,可以一起用。
- ☐ (本节完,下一节 6.4 6.4)