🤖 第8章 支持向量机分类
📄 一句话概括

48 个可分点,SMO 解出 w = (1.0135, 0.6622), ||w|| 等于 1.210651,几何间隔 2 除以 ||w|| 等于 1.652004,最小间隔正好是 1.000000。 扫满 720 个方向、每个方向穷举截距,最好的间隔是 0.825999; SMO 给出的 1 除以 ||w|| 是 0.826002,只差 3.1e-06。 把解整体乘 0.5 到 4,判错数一直是 0, 可 2 除以 ||w|| 从 3.304 变到 0.413——间隔不能直接当目标,得先用约束把尺度钉死。

📝 本节和前面几节的关系

7.2 结尾留了个尾巴:0/1 损失有一整片并列最优,没法优化; hinge 在这片平台上不平,但要严格地「最大化最小间隔」,还得让系数的长度也参与优化。 这一节就是把它做成一个正经的优化问题。 核心的一句话:把「最大化间隔」翻译成「最小化 ||w||」。 这个翻译成立的前提,是约束把尺度钉死了。 没这一条,同一个分类器能报出任意大的间隔。

一、间隔:一个数装两件事

对一个样本,间隔的定义是「标签乘输出」:y 乘 (w 点乘 x 加 b)。 符号管对错——正数分对、负数分错;大小管信心——绝对值越大离边界越远。 但要分清两种间隔。 函数间隔就是 y 乘输出本身,它会被缩放影响; 几何间隔是函数间隔除以 ||w||,它才是真正的欧氏距离。 Demo 里最小函数间隔是 1.000000、最小几何间隔是 0.826002, 两者差的就是那个 ||w|| 等于 1.210651。 几何间隔的意义是「这条边界离最近的点有多远」。 两条平行边界之间的距离是几何间隔的两倍,所以 Demo 里那条带的宽度是 1.652。 名字叫「支持向量机」,说的是「决定这条带宽度的只有边界上那几个点」,8.3 会验证。

二、为什么要把间隔顶到 1

先把「最大化间隔」写成一个问题:让所有点的几何间隔都不小于某个正数,再把那个正数往大推。 可这里有个陷阱:把 w 和 b 同时乘 2,几何间隔一点没变,函数间隔翻倍。 所以「间隔」这个量在不同的缩放下不可比较。 解决的办法是固定尺度:规定「最小函数间隔不小于 1」。 这一条不损失任何东西——任何一组 (w, b) 除以自己最小函数间隔的绝对值, 都能让最小函数间隔变成 1。 它也真的把尺度钉死了:满足这条的 (w, b) 是唯一的,没有倍数自由度。 钉住之后,「最大化间隔」就变成了「最小化 ||w||」。 因为间隔等于 1 除以 ||w||,让间隔大就是让 ||w|| 小。 再写成 ||w|| 平方除以 2,是为了求导方便——平方和除以 2 之后导数正好是 w, 而且和 ||w|| 在同一点取最小,问题完全等价。 于是得到标准形式:在约束「每个点的函数间隔不小于 1」下,最小化 ||w|| 平方的一半。 这是一个凸二次规划:目标是二次的、约束是线性的,所以有唯一全局最优。 这一点和第 7 章完全不一样——那里有一整片并列最优,这里只有一个点。

三、实测:缩放与暴力核对

先把「缩放不影响分类、却影响间隔读数」这件事测出来。 把 Demo 的解整体乘 0.5、1、2、4,看四个指标:

| 缩放 | ||w|| | 最小间隔 | 2 除以 ||w|| | 判错数 | |---:|---:|---:|---:|---:| | 0.5 | 0.6053 | 0.5000 | 3.3040 | 0 | | 1.0 | 1.2107 | 1.0000 | 1.6520 | 0 | | 2.0 | 2.4213 | 2.0000 | 0.8260 | 0 | | 4.0 | 4.8426 | 4.0000 | 0.4130 | 0 | 判错数那列四行全是 0,因为乘正数不改变任何符号,边界位置一点没动。 可「2 除以 ||w||」从 3.304 一路掉到 0.413,八倍差距。 同一个分类器,换个缩放就能报出「间隔 3.3」或者「间隔 0.4」, 所以间隔本身没法当目标函数——必须先固定尺度。 再看「最大间隔」这句话对不对。 办法很笨但很硬:把方向扫满 720 个角度,每个角度都穷举截距、算出这个方向能撑开的最大间隔, 看最好的那个是多少。 扫出来是 0.825999(角度 33.0 度),SMO 的 1 除以 ||w|| 是 0.826002。 差 3.1e-06,相对差万分之零点四;方向夹角 0.157 度。 这个偏差不是算法误差,是 720 格扫描的离散化代价:一格 0.5 度, 最好的那格不一定正好压在最优方向上。 要是扫得更密,两者会继续靠近。所以「最大间隔」是可以被暴力验证的结论,不是「书上那么说」。

四、边界只由两个点决定

Demo 的支持向量只有 2 个,下标 11 和 34,乘子都是 0.733。 48 个点里,44 个乘子是 0。 也就是说:这条边界的位置和斜率,是由两个点定下来的,另外 46 个点投了弃权票。 这不是这份数据运气好,是约束的必然结果。 最小间隔等于 1,意味着至少有一个点正好卡在约束上; 在二维里,两个向量方向不平行就能确定一条直线,所以两个点就够了。 维度越高需要的点越多,但「只需要一小撮」这件事不变。 这件事有个反直觉的推论:删掉一个离边界很远的点,边界纹丝不动; 而把边界上那个点挪一毫米,整条边界都跟着转。 所以 SVM 对「边界附近的标注质量」极其敏感,对远处的样本则完全不敏感。 这个性质在 8.3 会定量测出来,在 8.6 会变成「异常点也能被无视」的依据。

五、动手 Demo:间隔最大化:720 个方向里最好的间隔 0.825999,SMO 给出 0.826002

48 个点、二维,两团中心在 (负 1.3, 负 1.0) 与 (1.3, 1.0),标准差 0.42,所以线性可分。 用 SMO 在 C 等于 1000(几乎等于硬间隔)下求解,看最小函数间隔是不是正好等于 1、几何间隔是多少、 支持向量有几个。 然后把方向扫满 720 个角度、每个角度穷举截距,求出「这个方向能撑开的最大间隔」, 看全局最好的是不是就是 SMO 给的那一个。最后把解整体缩放,看判错数和间隔读数怎么变。

a, b, it = svm_fit(X, y, C=1e3)            # SMO,见 8.2
w = (a * y) @ X
print(np.linalg.norm(w), (y * (X @ w + b)).min())

def best_margin(u):                        # 固定方向 u,穷举截距
    t = y * (X @ u)                        # 已经乘过标签
    cand = ((t[y < 0][None, :] - t[y > 0][:, None]) / 2.0).ravel()
    return max(np.min(t + y * bb) for bb in cand)

th = np.arange(0.0, 2.0 * np.pi, 2.0 * np.pi / 720.0)
mv = [best_margin(np.array([np.cos(t), np.sin(t)])) for t in th]

实跑输出:

--------------------------------------------------------------------
间隔最大化:720 个方向里最好的间隔 0.825999,SMO 给出 0.826002
--------------------------------------------------------------------
  数据                     48 个点、二维,两团中心 (负 1.3, 负 1.0) 与 (1.3, 1.0),标准差 0.42
  求解                     SMO,C 取 1000(几乎等于硬间隔),迭代 13 轮
  约束                     要求每个点的间隔 y 乘输出 不小于 1——这条把尺度钉死了

  边界                     w = (1.0135, 0.6622),b = -0.2752
  长度                     ||w|| = 1.210651
  几何间隔                 2 / ||w|| = 1.652004
  最小间隔                 min(y 乘输出) = 1.000000(正好等于 1)
  支持向量                 2 个,乘子 a = 0.733、0.733
  它们的下标               11、34

  **先看「最小间隔」那一行:1.000000,正好顶在约束上。**
  **这说明最优解就在可行域的边界上——不是碰巧,是必然:间隔要是还有富余,就说明尺度没定满,还能再撑大。**

  暴力核对:把方向扫满 720 个角度,每个角度都穷举截距、求这个方向能撑开的最大间隔。

  扫描最好                 0.825999,方向 (+0.8387, +0.5446),角度 33.0 度
  SMO 给出                 1 / ||w|| = 0.826002
  两者之差                 3.117e-06,相对 0.0004%
  方向夹角                 0.157 度(差在 720 格扫描的粒度之内)

  **扫描值比 SMO 值略小 3.117e-06,这个差就是离散化的代价:0.5 度一格,最好的那格不一定正好压在 w 上。**
  **换成连续优化,理论上两者严格相等。所以「最大间隔」这句话是可以被暴力验证的,不是书上说了算。**

  为什么非要把间隔顶到 1?把解整体缩放一下就看出来了:

  缩放    ||w||   最小间隔   2/||w||   判错数
  ------------------------------------------
   0.5   0.6053     0.5000    3.3040        0
   1.0   1.2107     1.0000    1.6520        0
   2.0   2.4213     2.0000    0.8260        0
   4.0   4.8426     4.0000    0.4130        0

  **判错数那一列四行全是 0,因为乘一个正数不改变任何符号,边界的位置一点没动。**
  **可 ||w|| 和「2/||w||」跟着缩放一起变——同一个分类器,量出来的间隔却可以是任意大。**
  **所以间隔不能直接当目标去最大化,得先固定尺度。约束「最小间隔不小于 1」就是干这个的:它把 (w, b) 钉在唯一一个倍数上,于是「最大化间隔」才等价于「最小化 ||w||」。**
  **这也是 SVM 和最小二乘最根本的区别:最小二乘在拟合数值,SVM 在拿尺度当旋钮,只留边界形状这一个自由度。**

(y * (X @ w + b)).min() 这一句是整个 Demo 的支点。 它算的是最小函数间隔,硬间隔下它必须正好等于 1。 如果算出来是 0.98 或者 1.02,说明求解没收敛,或者是软间隔下 C 不够大。 这是一个非常好用的自检:不信任求解器的时候,先查这个数。 best_margin 里那行穷举截距不是随便写的。 min 对 b 是凹的分段线性函数,它的最大点一定落在某个折点上; 折点就是「两个点的值相等」的位置,也就是 (t 负 减 t 正) 除以 2。 所以只查这些点就够,不用网格扫 b。 注意 best_margin 里的 t 已经乘过标签了,所以后面加 y * bb 才是对的。 漏掉那个 y,截距的方向就反了,正类和负类会互相顶。 这类错不报错、只是结果偏小,很容易被当成「离散化误差」放过去。 支持向量只有 2 个这个结果,一开始我也怀疑算错了。 核对之后是对的:二维、可分、硬间隔,最优解就是「最近的两个点各卡一边」,两点定一条直线, 再多的点都插不进来。 换成三维,最少要 3 个点;d 维最少要 d 个。这是几何,不是巧合。 要说明:这份数据是按高斯造的、两团收得很紧(标准差 0.42),所以必然线性可分、间隔清晰。 真实数据几乎不会这样,8.2 就会换成有重叠的那一份,届时最小间隔会小于 1。

30 秒实验

  1. 把标准差从 0.42 加到 0.75,让两团轻微重叠,再把 C 设成 1000 求解。 最小函数间隔还能到 1 吗?如果到不了,说明什么?
  2. 把 720 个方向加密到 7200 个,重跑扫描。 扫描值和 SMO 值的差距会缩小多少?缩小到零需要多少格?
  3. 在 (6, 5) 加一个标成正类的点,重解。 w 和 b 会变吗?它的乘子是多少?如果换成在 (0.2, 0.2) 加一个标成正类的点呢?

常见误区

  1. 「间隔越大,模型越好」。 间隔大只保证训练集上分得开,不保证测试集。 如果数据本身有重叠,硬把间隔撑大等于让模型对噪声过度敏感。 8.2 里把 C 从 0.1 调到 100,间隔窄了 2.6 倍,准确率却在 95.0% 到 95.6% 之间没动。
  2. 「最大化间隔就是最小化 ||w||,两者是同一个东西」。 只在「最小函数间隔等于 1」这个约束下才是。 没有那个约束,||w|| 可以被任意缩放,最小化它没有下界(会趋近于 0)。 约束不是配角,它是让问题有解的前提。
  3. 「支持向量就是离边界最近的那些点」。 方向上对,但精确说法是「乘子大于零的点」。 软间隔下,有些点被分错了、离边界很远,乘子照样大于零,它也是支持向量。 8.3 的实测里就有一批间隔是负的支持向量。
  4. 「几何间隔和函数间隔是两回事,别混」。 它们只差一个 ||w||,而且换算是固定的。 真正要小心的是「哪个在优化里被约束成 1」——那是函数间隔。 读别人的公式时先确认他约束的是哪一个,否则会差一个 ||w|| 的因子。
自测三问(点开对答案) 1. 为什么「最大化间隔」必须先固定尺度,不能直接当目标函数? —— **因为间隔对 (w, b) 的缩放不敏感,而 ||w|| 敏感。** **不固定尺度的话,同一个分类器能报出任意大的间隔读数,目标函数值没有意义。** **约束「最小函数间隔不小于 1」把所有等价的缩放里挑出一个代表,** **于是间隔和 ||w|| 建立了一一对应,最大化间隔才等价于最小化 ||w||。** 2. 二维可分数据上,为什么支持向量最少只要 2 个? —— **因为边界是一条直线,两点就能定一条直线。** **最优解要让最近的点正好卡在两条平行边界上,只要每边有一个点卡住就够了。** **d 维空间里超平面由 d 个点确定,所以最少要 d 个支持向量;这是维度决定的,和数据量无关。** 3. 几何间隔和函数间隔差在哪里,为什么优化时用的是函数间隔? —— **几何间隔是真正的欧氏距离,函数间隔是它乘上 ||w||。** **优化时约束函数间隔,是因为它关于 (w, b) 是线性的,约束就成了线性约束,** **问题变成二次规划。** **如果直接约束几何间隔,||w|| 会跑到分母上,约束变成非线性的,问题就难解了。**

标准形式是个带线性约束的二次规划,直接解很麻烦。 下一节走对偶这条路:给每个样本配一个乘子,把约束吸收进目标,问题变成「在盒子里调乘子」。 然后还得有个能真跑起来的算法——只看两个乘子的那个办法,叫 SMO。

相关笔记