🤖 第7章 基于最小二乘法的分类
📄 一句话概括

在一份可分数据上扫 720 个方向: 有 196 个方向把 120 个点全分对,占 27.2%,这些方向上 0/1 损失全是零。 hinge 损失在这片平台上不平,它挑出 (0.602, 0.799) 这个方向,最小间隔 0.818。 缩放实验:把系数乘 0.25 到 4,0/1 错误数一直是 0,而平方和在缩放 1.0 处取极小 8.4、 4 倍时涨到 1013。

📝 本节和前面几节的关系

7.1 用的是平方损失,它在优化「预测值接近正负 1」。 但分类真正在意的事情只有一件:符号对不对。 这一节把这件事写成 0/1 损失,然后说明它为什么不能直接优化。 「间隔」是这中间的桥。 它把「分对」这个离散的事,变成一个连续的、可以优化的量。 第 8 章的整个支持向量机,都建立在这一个概念上。

一、0/1 损失:定义简单,没法优化

0/1 损失的定义:分对了记 0,分错了记 1。 它的值只有两个,所以它既不连续也不可导,更谈不上凸。 Demo 把这件事测出来了:在可分数据上,720 个方向里有 196 个能把 120 个点全分对。 这 196 个方向上 0/1 损失全是零,完全没有区别。 所以「最小化 0/1 损失」这个问题的最优解不是一组参数,是一整片区域。 没有唯一解还有个连带问题:没有梯度。 梯度下降需要「往哪个方向改会变小」这个信息,而 0/1 损失在平台内部处处是平的。 你想改进,它告诉你「已经最好了」;你随便走一步,它可能突然变成 1。 更麻烦的是,0/1 损失对参数的依赖是「跳变」的:把某个系数连续调小,直到某个点跨过边界, 损失才会从 0 跳到 1。 这类函数在优化里几乎没有可用的结构。

二、间隔:把离散变连续

间隔的定义是「标签乘输出值」:分对了是正数,分错了是负数,绝对值代表离边界多远。 它把「对错」和「信心」压在一个数里。 有了间隔,0/1 损失可以写成「间隔小于零记 1,否则记 0」。 这还是一条跳变的分界线,但它周围有了结构——间隔本身是连续的。 所以下一步是「找一个连续函数,在间隔为负时大、为正时小」。 这样的函数叫代理损失。 hinge 是最常用的一个:间隔小于 1 时罚「1 减间隔」,大于 1 时不罚。 hinge 的两个性质让它成为好代理:它是凸的,而且它处处是 0/1 损失的上界。 上界这个性质很重要:你把上界压小,真损失一定也跟着小。 这是「最小化代理损失」在理论上站得住的理由。 注意 hinge 在间隔大于 1 之后就完全不管了。 这意味着「分得很对的点」不再影响解——这正是支持向量机稀疏性的来源(8.3)。

三、实测:平台与缩放

缩放倍数 最间隔 0/1 错误数 hinge 和 平方和
0.25 0.076 0 92.1 71.2
0.50 0.152 0 64.2 36.3
1.00 0.303 0 16.7 8.4
2.00 0.607 0 1.0 120.0
4.00 1.214 0 0.0 1013.0
最间隔那一列严格按倍数增长:0.076、0.152、0.303、0.607、1.214。
因为间隔是线性的,系数乘 k,间隔就乘 k。
0/1 错误数那一列五格全是 0,一格没动。
因为符号不随正的缩放改变——这就是「0/1 损失对尺度不变」的实证。
平方和那一列在缩放 1.0 处最小(8.4),往两边都变大。
这不是巧合:缩放 1.0 对应的就是最小二乘解本身,而最小二乘的定义就是让平方和最小。
一个指标对尺度完全不敏感,另一个在某个尺度上取极小——两个目标关心的东西根本不同。
所以「最小二乘分类」和「最小化分类错误」不是同一件事,前者只是后者的近似。

四、hinge 也不完美

hinge 是凸上界,但它不是唯一的代理。 交叉熵(10.1)也是,而且是光滑的、可以输出概率。 两者的差别在于「对分得很对的点管不管」:hinge 完全不管,交叉熵永远在推。 这个差别带来一个实际后果:hinge 的解只由少数「靠近边界」的点决定, 所以对数据扰动不敏感(只有支持向量变了才变);交叉熵的解会被所有点轻微影响。 前者稳定、后者信息利用更充分,各有各的用处。 还有一点:这一节我只扫了方向、把系数的长度固定成 1。 严格的最大间隔分类要把长度也当成变量——最小化长度,同时要求所有间隔不小于 1。 在长度固定时,hinge 事实上在最大化「所有点间隔之和」,那是个折中,不是严格的最大最小间隔。

五、动手 Demo:0/1 损失:720 个方向里 196 个零错误,缩放对它完全无效

另造一份分得开的数据:两团各 60 个点,中心在 (负 1.7, 负 1.7) 与 (1.7, 1.7),标准差 0.65, 所以存在能把两团完全分开的直线。 先让系数取单位长度、只扫方向(720 个角度),数出「零错误方向」有多少个, 并找出 hinge 损失最小的那个方向。再固定方向、只改长度,看四个指标怎么变。

th = np.arange(0.0, 2.0 * np.pi, 2.0 * np.pi / 720.0)
for a in th:
    w = np.array([np.cos(a), np.sin(a)])       # 单位长度,只扫方向
    sc = X @ w                                 # 输出(截距为 0)
    err = np.sum(np.where(sc >= 0, 1.0, -1.0) != y)   # 0/1 损失
    hg = np.sum(np.maximum(0.0, 1.0 - y * sc))        # hinge 损失

实跑输出:

--------------------------------------------------------------------
0/1 损失:720 个方向里 196 个零错误,hinge 只挑出其中 1 个
--------------------------------------------------------------------
  数据                     另造的一份可分数据:120 个点、二维,中心 (负 1.7, 负 1.7) 与 (1.7, 1.7)
  扫法                     让系数取单位长度、只扫方向,共 720 个角度(步长约 0.5 度)
  间隔                     标签乘输出值,正数代表分对了,大小代表「离边界多远」
  hinge 损失               间隔小于 1 时罚「1 减间隔」,大于 1 时不罚

  **先说平台:720 个方向里有 196 个把 120 个点全分对,占 27.2%。**
  **这些方向上 0/1 损失全是 0,完全一样。**
  **所以最小化 0/1 损失的最优解不是「一个点」,是「一整片方向加任意长度」——既没有唯一解,也没有梯度。**
  **这就是它不能直接用的原因:不是「效果不好」,是「连怎么优化都说不清」。**

  **hinge 在这片平台上不平:它挑出 (0.602, 0.799) 这个方向,此时最小间隔是 0.818。**
  **hinge 是 0/1 损失的凸上界,而且在「分对了」的区域里它还在继续下降,所以它不会停在平台的边缘,会往「间隔更大」的方向走。**
  **但要说清楚:这里我固定了系数长度是 1,所以 hinge 事实上在最大化「所有点的间隔之和」,不是「最小间隔」。**
  **严格的最大间隔分类,要把系数的长度也当成变量去最小化——最小化长度、同时要求间隔不小于 1,那才是第 8 章的正题。**

  再看缩放:把最小二乘解乘上不同的倍数,四个指标怎么变。

  缩放倍数   最间隔   0/1 错误数   hinge 和   平方和
  -------------------------------------------------
      0.25    0.076            0       92.1     71.2
      0.50    0.152            0       64.2     36.3
      1.00    0.303            0       16.7      8.4
      2.00    0.607            0        1.0    120.0
      4.00    1.214            0        0.0   1013.0

  **0/1 错误数那一列五行全一样,都是 0。**
  **因为 0/1 损失只看符号,乘一个正数不改变任何符号,它对缩放完全不敏感。**
  **平方和那一列在缩放 1.0 那一格最小(8.4),往两边都变大:0.5 倍是 36.3,4 倍是 1013.0。**
  **这正好是「它就是最小二乘解」的定义——缩放 1.0 就是原来的 w,再乘任何数都只会让残差变大。**
  **一个指标(0/1)对缩放完全不敏感,另一个(平方和)在缩放 1.0 处取极小。**
  **两个目标关心的东西根本不一样,这就是最小二乘分类只是近似的原因。**
  要说明:**方向扫描时截距固定为 0**,因为两团点的中心关于原点对称,**最优超平面本来就过原点;如果数据偏了,这个限制就不成立。**

为什么固定长度?因为 0/1 损失对长度完全不敏感,长度是多余的维度。 固定成 1 之后,扫的 720 个方向就是一个完整的「参数空间切片」, 每一点都对应一个实质不同的分类器。 np.where(sc >= 0, 1.0, -1.0) != y 得到的是一个布尔数组,np.sum 会把它当 0 和 1 加起来。 这个写法比 np.mean 更适合数个数,但两者只差一个除法。 hinge 的写法 np.maximum(0.0, 1.0 - y * sc) 值得盯一秒:y * sc 就是间隔。 间隔大于 1 时括号里是负的,取 max 之后归零; 小于 1 时罚「1 减间隔」,刚好在间隔为 1 处接上零点。 196 个零错误方向这个数,说明了「平台」有多大:27.2% 的方向都能做到零错误。 这不是数据太简单,恰恰是可分数据的常态——能分开的直线本来就有一整片。 hinge 挑出的方向 (0.602, 0.799) 不是 (0.707, 0.707),这一点值得注意: 它最大化的是「所有点的间隔之和」,而不是「最小的那个间隔」。 这两者不一样,前者会被点多的那一侧拉偏。真正的最大间隔要靠长度也参与优化。 要说明:扫描时截距固定为 0,因为两团点中心关于原点对称、最优超平面本来就过原点。 如果数据整体偏移,这个限制就不成立,得把截距也扫进去。

30 秒实验

  1. 把标准差从 0.65 加到 1.3,让两团重叠。零错误方向还剩几个?如果有,说明什么?
  2. 把 hinge 的阈值从 1 改成 0.5 和 2,重新找最优方向。选出来的方向会变吗?
  3. 把数据整体平移 (2, 1),再扫一遍。截距固定为 0 会造成什么后果?与刚才差多少?

常见误区

  1. 「0/1 损失就是分类错误率,所以它就是我们要优化的目标」。 它是我们要的最终指标,但不是能优化的目标。 不可导、不连续、有大量并列最优——优化算法在它上面完全失灵。
  2. 「hinge 是最好的代理损失」。 它是最常用的之一,但交叉熵也很常用。 hinge 不给概率,交叉熵给;hinge 只由支持向量决定,交叉熵用上所有点。 选哪个取决于你要不要概率输出。
  3. 「间隔越大,模型一定越好」。 间隔大只说明「训练集上分得开」,不说明泛化好。 如果数据本身有重叠,强行追求大间隔会让模型对噪声过度敏感。 间隔是手段,测试误差才是目的。
  4. 「0/1 损失对缩放不变,所以模型也不会被缩放影响」。 分类结果确实不变,但正则化、优化的数值行为都会变。 ℓ2 正则化会直接惩罚系数的长度,它和缩放是纠缠在一起的——所以实践中必须先固定尺度。
自测三问(点开对答案) 1. 为什么 0/1 损失会有一大片并列最优,而不是唯一解? —— **因为把点分开的直线本来就有一整片(只要数据可分)。** **这整片区域里,任何一条的 0/1 损失都是零,所以它们并列最优。** **「并列最优」在凸优化里并不可怕,可怕的是这片区域是个开集、没有内部结构,** **于是算法无从下手。** 2. 间隔为什么能同时表达「对错」和「信心」? —— **因为它是标签乘输出值:符号就是「对错」,大小就是「离边界多远」。** **一个数装了两件事,这是它比 0/1 损失好用的核心原因。** **而且它对参数是线性的,求导方便。** 3. 为什么说 hinge 是 0/1 损失的「上界」这件事很重要? —— **因为上界给了优化的合法性:你把上界压小,真损失不可能跑得比它更大。** **没有这个性质,最小化代理损失就只是「碰运气」——你可能把代理压得很小,** **而真正的错误率却很大。**

两类的道理讲完了,可现实里类别往往不止两个。 下一节把标签做成 one-hot、解一个多输出的回归、再取最大的那一类。 这个做法能用,但会带进一种新的偏置——三种类别的话语权会不一样。

相关笔记