🧮 第2章 有监督学习
📄 一句话概括

能分开两类的线有无数条,支持向量机(SVM)只要间隔最宽的那一条。 它只用边界附近那几个点(支持向量)来决定边界,远处的点删掉也不影响。 实测 120 个点里只有 9 个是支持向量,删掉其余 111 个重训,系数最大只变了 0.00137; 间隔宽度 2.0240。

📝 本节和前面几节的关系

2.3 节的逻辑回归也在找分界线,但它的目标是「让每个点的概率尽量对」。 支持向量机换了个目标:只关心边界那一圈,让缓冲带最宽。这个目标的不同,带来两个后果: 抗噪更好,以及——2.5 节能用核函数把它掰弯。 数学上的完整推导(拉格朗日对偶、KKT 条件)在 40 课 第 8 章, 这里只求「会用、能说清」。

一、先想一个几何问题:哪条线最稳

两类点线性可分时,能切开的线有无穷多条。 逻辑回归会给你其中一条,但你没法定它——换一个随机种子,线就换个角度。 支持向量机先问了一个更基本的问题:这么多条线里,哪一条最不怕新数据? 答案直觉上很清楚:离两边最近的点都尽量远的那一条。因为新数据只可能出现在已有的点附近, 缓冲带越宽,被误伤的概率越小。 这个「缓冲带」有名字,叫间隔。严格定义是:分界线到两边最近点的距离之和。 数学上它等于 2 除以系数向量的长度。所以「最大化间隔」等价于「最小化系数长度」。 这一句是支持向量机的全部秘密: 它在最小化 ||w||。而 2.2 节的正则化也在压 ||w||——两者看着像一回事,实际目标不同: 正则化压 w 是为了防过拟合,SVM 压 w 是为了把间隔撑到最宽。巧合的是它们经常指向同一个解。

二、软间隔:允许几个点违规

上面的推导假设数据完全线性可分。真实数据几乎不可能——总有重叠、总有标错。 硬要求「所有点都在缓冲带外侧」会让问题无解。 教材的处理是引入软间隔:允许点违反间隔,但违规要付代价。 写成损失函数就是合页损失(hinge loss):分对了且离边界足够远,损失为零; 离得太近或分错,损失按距离算。 这个损失和逻辑回归的交叉熵有个本质区别:它是截断的。足够远的点损失恰好是 0, 梯度也是 0——它们根本不参与训练。交叉熵不会截断,再远的点也在拉那条线。 这就是「支持向量」这个名字的来源: 只有落在间隔内(含边界)的点贡献梯度,其余的点梯度为零。 一个样本是不是支持向量, 只看它离边界够不够近。 Demo 里 120 个点只有 9 个是支持向量(−1 类 5 个、+1 类 4 个), 剩下的 111 个对这条线毫无影响。 这是支持向量机和别的算法最不一样的地方。

三、验证:把非支持向量全删掉

「只有支持向量有影响」这句话可以真验证。 做法很直接:训练完一次,找出所有支持向量,把其余样本全部删掉,重新训练,比较系数。 Demo 里样本从 120 个砍到 9 个,系数最大变化 0.00137。 这个数小到可以认为一模一样。 也就是说:另外那 111 个点白给了。 这里有个实现细节值得说:代码里损失按样本求和而不是求平均。 如果求平均,删掉样本会改变分母,最优解就跟着变; 求和则严格不变——数学上「删点不变」只在求和形式下成立。 教材没提这一点,但自己写代码时会踩到。 顺带说:这也解释了为什么 SVM 适合「样本多但只有少数在边界上」的场景。 预测时也只需要算和支持向量的内积,不需要扫全表——比 KNN 快得多。

四、C 是什么:容忍违规的价钱

软间隔里有个参数 C,控制「违规罚多重」。C 越大罚得越狠,模型越不愿意让步; C 越小越宽容,宁可让几个点越界也要把间隔撑宽。 Demo 那张表把五档 C 都跑了一遍。 C = 0.001 时间隔宽度 8.4155、 120 个点全部违规——模型几乎放弃了分类,只求间隔最大。C = 1.0 时间隔 1.6145、只剩 4 个支持向量、 1 个点违规。 C 和 2.2 节的 λ 是一对反向旋钮:C 大等于正则弱。 很多教材里干脆把 SVM 写成「合页损失 + L2 正则」,那个正则系数就是 1/(C·n)。 记住这个对应关系,调参时就不会糊涂。 怎么选 C?还是老办法:在验证集上试。经验上从 0.01 到 100 按十倍扫,先看量级再细化。 C 的合适量级和数据尺度有关,所以标准化不只是为了数值稳定,也为了让 C 有个通用起点。

五、动手 Demo:只让边界上那几个点说话

两团点,一类在 (0,0)、一类在 (4,0),各 60 个,标准差 0.6。 这个间距让数据基本线性可分,但边缘仍有几处贴得很近——正好用来演示谁在决定边界。 求解用的是合页损失的次梯度下降,没有用 SMO。原因很实际:SMO 要写几十行对偶更新,容易出错; 而这一节要看的性质(支持向量、间隔、C 的影响)用梯度下降一样能拿到。

import numpy as np

def svm_linear(X, y, C=0.2, lr0=0.05, iters=20000, decay=3000.0):
    w = np.zeros(X.shape[1])
    b = 0.0
    for it in range(iters):
        m = y * (X @ w + b)                  # 间隔:大于 1 就算分对了
        g = np.where(m < 1.0, -y, 0.0)       # 只有违反间隔的点有梯度
        lr = lr0 / (1.0 + it / decay)
        w -= lr * (w + C * (g[:, None] * X).sum(axis=0))
        b -= lr * C * g.sum()
    return w, b

w, b = svm_linear(X, y)
print("w =", np.round(w, 4), " b = %.4f" % b)
print("间隔宽度 %.4f" % (2.0 / np.sqrt(w @ w)))

实跑输出:

--------------------------------------------------------------------
支持向量机:只让边界上那几个点说话
--------------------------------------------------------------------

  两团点,一类在 (0,0)、一类在 (4,0),各 60 个,标准差 0.6。
  两类是线性可分的(一条竖线就能切开),但**能切开的线有无数条**,
  支持向量机要多问一句:哪一条最稳?

  【一】最稳的那条:让两边的空隙一样宽
  学到的分界线是 w = (0.9786, -0.1373)、b = -1.8339。
  w 的方向几乎就是 x 轴正方向——因为两团点是左右排开的,最稳的切法就是一条竖线。
  这条线到两边最近点的距离叫间隔,宽度等于 2 / |w| = **2.0240**。

  【二】谁在决定这条线:支持向量
  类别      样本数    支持向量数    离边界最近的距离
  ----------------------------------------------------
  −1 类     60        5             0.6517
  +1 类     60        4             0.4909
  120 个点里只有 9 个落在间隔边上,**其余的点离得远远的,它们怎么挪都不影响这条线。**
   这就是「支持向量」这个名字的由来——撑住边界的只有这 9 个。

  【三】验证:把非支持向量全删掉,重训一次
  训练集          样本数  w                       b
  --------------------------------------------------------
  全部            120     (0.9786, -0.1373)       -1.8339
  只留边界附近    9       (0.9799, -0.1375)       -1.8318
  样本从 120 个砍到 9 个(约 7%),w 的最大变化只有 0.00137。
  **换句话说:另外那些点删掉也不心疼。**
   这和线性回归完全不同——线性回归里每个点都拉着线,支持向量机只看边界附近的几个。

  【四】那为什么还要 C 这个参数
  C       支持向量数    间隔宽度    违反间隔的点
  ------------------------------------------------
  0.001   120           8.4155      120
  0.010   46            3.4179      41
  0.050   18            2.8121      14
  0.200   9             2.0240      5
  1.000   4             1.6145      1
  C 是「容忍违规的程度」。C 小,模型宁可让几个点越界也要把间隔撑宽;C 大,一个点都不许越界,
  间隔就被挤窄。**它和正则化的 λ 是一对反向旋钮:C 大等于正则弱。**

  一句话总结支持向量机的脾气:
  **它不追求「把所有点都分对」,它追求「分对的同时留出最宽的缓冲带」。**
   这条缓冲带让它比逻辑回归更抗噪——边界外那些离群点根本进不了决策。

学到的 w 是 (0.9786, −0.1373),间隔 2.0240。w 的方向几乎就是 x 轴正方向——两团点是左右排开的, 最稳的切法就是一条竖线,模型自己找到了这个「显然」的答案。 支持向量只有 9 个:−1 类 5 个、+1 类 4 个。这 9 个点的间隔值都在 1 附近(0.49 到 1.05), 其余 111 个都在 1.4 以上。间隔外那些点,删了对模型没有任何影响。 第三段是硬验证:删到只剩 9 个点重新训练,w 只变了 0.00137。这个差异来自优化器的收敛精度, 不是模型真的变了。如果是线性回归,删掉 92% 的点,系数早就飞到天上去了。 C 那张表要看「间隔宽度」和「违反间隔的点」两列的联动。C 从 0.001 加到 1.0, 间隔从 8.4155 缩到 1.6145,违规点数从 120 降到 1。 这就是「宽容」和「严格」的取舍。 C 极小时所有点都算违规, 模型干脆学成一个几乎不分类的东西——这也是一种失败模式,只是很少人提。 最后一句话把 SVM 和逻辑回归区分开:逻辑回归追求「每个点的概率都对」, SVM 追求「边界外留出最宽的缓冲带」。前者在乎全部,后者只在乎边缘。

30 秒实验

  1. 把 C 加到 10,间隔会缩到多少?支持向量剩几个?准确率会变吗?
  2. 在两团之间加 5 个「故意标反」的点,SVM 和逻辑回归哪个受的影响更大?为什么?
  3. 如果把数据整体乘 10(尺度变大),C = 0.2 还是合适的吗?想想 C 的量级和什么有关。

常见误区

  1. 以为支持向量机「一定比逻辑回归准」。线性可分且噪声小的时候,两者常常打平; SVM 的优势在边界清晰、离群点多的场景。没有免费的午餐,也没有通吃的算法。
  2. 不做标准化就用默认的 C。C 的合适量级取决于特征尺度,尺度大十倍, 等效的 C 就小一百倍(因为间隔按 ||w|| 算)。换数据不换 C,等于换了个惩罚强度还不知道。
  3. 以为「支持向量越多模型越好」。恰恰相反:支持向量多说明边界附近挤满了点,数据可能重叠严重。 支持向量少而准确率高,才是干净的数据。
自测三问(点开对答案) 1. 为什么最大化间隔等价于最小化系数向量的长度? —— 因为分界线到最近点的距离是 1/||w||,两边加起来间隔就是 2/||w||。 让间隔最大,就是让 ||w|| 最小。 2. 删掉 92% 的样本重训,系数几乎不变,这说明什么? —— 说明那些点不是支持向量,它们离边界足够远,合页损失为零、梯度为零,从来没有参与过训练。 SVM 的解只由边界附近那几个点决定。 3. C 变大,间隔会变宽还是变窄?和正则化的 λ 是什么关系? —— C 变大间隔变窄(更不容忍违规)。C 相当于正则强度的倒数:C 大等于 λ 小,也就是正则弱。

支持向量机再强,也只能画一条直线。同心圆那种数据它一样没辙。 2.5 节的核方法给了一个办法:先把数据抬到高维,再切。

相关笔记