160 个点里只有 23 个乘子不为零,占 14.4%,另外 137 个是零。 三类乘子的间隔范围完全不重叠:等于零的那批间隔 1.1067 到 6.7281, 在中间那 3 个间隔精确等于 1.0000,顶到 C 的那 20 个间隔从负 0.6840 到 0.7698。 只用这 23 个点重解,w 的前四位有效数字完全一致(差 2.6e-05); 在 (9, 9) 加一个点,w 和 b 一位都没变。
8.2 的迭代轨迹里,支持向量个数一路涨到 24 又回落到 23,而样本总数是 160。 这一节解释那 137 个零是怎么来的,以及它们带来了什么。 「稀疏」在这里指的是样本稀疏:大多数样本的乘子是零,它们对解没有任何贡献。 注意和第 5 章区分开——那里的稀疏是指系数为零,是特征层面的事;这里是样本层面的事。
一、乘子的三种状态
KKT 条件把每个样本分成三类,每一类对应乘子的一个区间: 乘子等于 0,说明这个点没有顶到约束,间隔大于 1,它对解没影响; 乘子在 0 和 C 之间,说明约束正好卡住,间隔精确等于 1,它在边界上; 乘子等于 C,说明约束被违反了、而且话语权已经用满,间隔小于 1。 这三条不是三条经验规则,是同一组 KKT 条件在不同情形下的写法。 它们直接给出了一个可检验的预测:把样本按乘子分组,每一组的间隔范围应该落在互不重叠的区间里。 Demo 就是去验这件事。 顺带说清一个容易绕晕的地方:为什么「乘子等于 C」的间隔可以小到负数? 因为软间隔允许分错。分错的点间隔是负的,它当然违反了「不小于 1」, 而这种违反在 C 用满之后就到此为止——再多违反也不再增加惩罚的权重。
二、实测:三段不重叠的间隔
| 乘子状态 | 个数 | 间隔范围 | 含义 |
|---|---|---|---|
| 等于 0 | 137 | 1.1067 到 6.7281 | 约束不紧,不参与 |
| 在 0 与 C 之间 | 3 | 1.0000 到 1.0000 | 正好卡在边界上 |
| 等于 C | 20 | 负 0.6840 到 0.7698 | 违反约束且话语权用满 |
| 第二行那个范围缩成了一个点:1.0000 到 1.0000。 | |||
| 这不是四舍五入的结果,是数学上必须精确等于 1。 | |||
| 这三个点的乘子在盒子里,既没到 0 也没到 C,所以约束是「紧的」,也就是等式成立。 | |||
| 第一行的下限是 1.1067,比 1 大;第三行的上限是 0.7698,比 1 小。 | |||
| 三段之间留了空档,谁也没压到谁——这正是 KKT 条件预测的形态。 | |||
| 要是有个点乘子等于零、间隔却只有 0.9,那就说明求解没收敛。 | |||
| 第三行的下限是负的(负 0.6840),负间隔就是判错的点。 | |||
| 这份数据训练错了 8 个,全在这一行里。 | |||
| 注意「判错」和「是支持向量」在这里是重叠的——分错的点一定是支持向量,反之不成立。 |
三、实测:扔掉 137 个点,解不变
把乘子为零的 137 个点全删掉,只用剩下 23 个重解: 全量解 w 等于 (1.146283, 1.395033),只用 23 个点解出 (1.146288, 1.395059)。 差在十万分之一,就是求解器自己的精度(8.2 里 KKT 残差也是这个量级)。 这个检验比「数一数有多少个零」有力得多。 零多可能只是「这个 C 下恰好如此」,而「删掉之后解不变」说明那些点真的没在说话。 换句话说:SVM 的解不由样本总量决定,由边界附近那一小撮决定。 再换个方向验一次:在 (9, 9) 加一个标成正类的点。 它离两团都很远、分得对、离边界也远,结果乘子是 0.000e+00,w 和 b 的变化是 0.00e+00。 一位都没变,连浮点噪声都没有——因为它压根没进优化。 这两件事合起来说明:SVM 的「信息预算」花在边界上。 样本多但都堆在远处,对 SVM 毫无帮助;样本少但都在边界附近,反而决定一切。 这也是它在文本、图像这类「高维、样本不算多」的问题上特别受欢迎的原因之一。
四、稀疏不总是成立
这里必须说清楚条件的边界:稀疏是这套损失加约束的结构带来的,不是无条件的。 把 C 调大,顶到上界的点会变多,支持向量就变多(8.2 的表里 36 个到 18 个,不是单调的, 但都在十几到三十几的量级)。 数据重叠得越厉害,边界附近的点越密,支持向量也越多。 核化之后更明显。 8.4 的 Demo 里,同心圆数据用高斯核,60 个点里有 15 个是支持向量; 而线性核在那份数据上有 49 个——几乎全都算支持向量,因为线性核根本分不开它。 所以「SVM 稀疏」这句话在实际使用中经常打折扣。 还有一点:乘子恰好为零这件事,在数值上要靠阈值判断。 求解器给出的是 1e-12 还是 1e-08,取决于它怎么剪枝。 所以「支持向量个数」这个数,换一个实现、换一个精度,可能差几个。 别把它当成一个有精确定义的量去做理论推导。
五、动手 Demo:稀疏性:160 个点里只有 23 个乘子不为零,扔掉其余 137 个解不变
和 8.2 同一份数据、同样 C 取 1。 先把样本按乘子状态分成三组, 看每组的间隔范围是不是落在互不重叠的区间里——这直接验证 KKT 条件的三条。 然后只用支持向量重解一遍,比较两次的 w 和 b。最后在 (9, 9) 加一个标成正类的远点,看边界动没动。
a, b, it = svm_fit(X, y, C=1.0)
mg = y * (X @ w + b) # 间隔
z = a <= 1e-9 # 乘子为零
mid = (a > 1e-9) & (a < C - 1e-9) # 卡在边界上
up = a >= C - 1e-9 # 顶到上界
a2, b2, _ = svm_fit(X[a > 1e-6], y[a > 1e-6], C=1.0) # 只用支持向量
print(np.abs((a2 * y[a > 1e-6]) @ X[a > 1e-6] - w).max())
实跑输出:
--------------------------------------------------------------------
稀疏性:160 个点里只有 23 个乘子不为零,占 14.4%
--------------------------------------------------------------------
数据 和 D026 同一份,160 个点、C 取 1,训练错 8 个
间隔 标签乘输出,1 表示正压在边界上,负数表示判错
乘子状态 个数 间隔范围 含义
---------------------------------------------------------------------
乘子等于 0 137 1.1067 ~ 6.7281 间隔不小于 1,不参与
乘子在 (0, C) 3 1.0000 ~ 1.0000 间隔正好等于 1,压在边界上
乘子等于 C 20 -0.6840 ~ 0.7698 间隔小于 1,违反约束并被封顶
**三行的间隔范围是完全不重叠的三段,这不是巧合,是 KKT 条件逐条对应的。**
**乘子为 0:约束不紧,间隔大于 1。乘子在中间:约束正好卡住,间隔精确等于 1(所以范围缩成一个点)。乘子顶到 C:约束被违反,间隔小于 1。**
**第三行的下限是负的(-0.6840),负的间隔就是判错的点。这一份数据训练错 8 个,全在这一行里。**
**按这三条一查,160 个点里 137 个乘子是 0,占了 85.6%——这就是「稀疏」在这里的意思。**
**注意和第 5 章的稀疏不是一回事:那里的稀疏是指系数为 0,这里的稀疏是指样本的乘子为 0。**
真正的检验:只用那 23 个支持向量重解一遍,看解变不变。
全量 160 个点 w = (1.146283, 1.395033),b = -0.279626
只用 23 个点 w = (1.146288, 1.395059),b = -0.279641
最大偏差 w 上 2.63e-05,b 上 1.44e-05
**扔掉了 137 个点,解基本没变:w 的前四位有效数字完全一致,差在十万分之一。**
**偏差 2.63e-05 已经是我这个 SMO 的收敛精度了(D026 里 KKT 残差也是这个量级),不是信息差。**
再加一个离得远远的点((9, 9),标成正类),看边界动没动:
新点 (9, 9),标成正类,离两团都很远
它的乘子 0.000e+00
边界变化 w 上 0.00e+00,b 上 0.00e+00
**乘子是 0,边界一点没动。一个离得很远、又分得对、又不在边界上的点,对 SVM 来说等于不存在。**
**这就是为什么 SVM 在「样本多」的场合比「维度高」的场合更吃紧:它的解只由边界附近那一小撮点决定,剩下的点再多也不加信息。**
要说明:**稀疏不等于「什么数据都稀疏」**。C 调大、或者数据重叠得厉害,支持向量会明显变多(D026 里 C 从 0.1 到 100,支持向量个数一直在涨)。稀疏是这套损失加约束的结构带来的,不是无条件的。
阈值 1e-9 和 1e-9 这两个数是为这份数据挑的,一般写成 1e-8 更稳。
关键是别用 a == 0 直接比——浮点算出来的乘子会有 1e-17 这种尾巴,严格等于零几乎永远不成立,
三组会全落空。
a >= C - 1e-9 同理会漏掉那些「C 减 1e-12」的点。
软间隔下上界的判断永远要用容差,这是数值计算里最常见的坑之一。
只用支持向量重解这一段,注意 y[a > 1e-6] 要用同一个掩码,不能只筛 X。
筛漏了标签,顺序就错位,得到的 w 会荒谬但不会报错。
这种错在调试时最难查,因为每一行都能跑通。
「解不变」这个结论有个前提:重解时 C 要和原来一样。
因为乘子的上界决定了解的形状;C 变了,那 23 个点的乘子可能被截断得不一样,解就变了。
要说明:「只有 23 个支持向量」是这份数据、这个 C 下的结果。
换成高斯核,支持向量会到十几个到几十个不等(8.4 的表),换成线性核去分同心圆,
几乎全是支持向量。
所以别把这个比例当成 SVM 的固定性质。
30 秒实验
- 把 C 设成 0.05,重数一遍三组的个数。乘子等于 C 的那组会变多还是变少?为什么?
- 把 160 个点里乘子为零的那 137 个全部复制一份(变成 297 个点),重解。 w 会变吗?如果变了,说明什么?
- 把支持向量里的每一个点各复制三份,重解。w 会变吗?这时的乘子怎么分配?
常见误区
- 「支持向量就是离边界最近的点」。 只对了方向。 准确定义是「乘子大于零的点」,而这些点里有一批是分错的、离边界很远。 实测里第三组的间隔从负 0.68 到 0.77,跨度很大。
- 「乘子为零的样本删掉对结果没影响,所以可以先删再训」。 顺序反了:你得先训练才知道谁乘子为零。 这个性质的价值不在「省数据」,在于「理解解的结构」——以及在大规模问题里做「工作集」: 每轮只在可能违反约束的子集上优化。
- 「SVM 一定稀疏」。 不一定的。 C 大、数据重叠严重、或者核函数很宽,支持向量都会占到很大比例。 核化之后尤其如此,因为核把每个点都映射到了高维,边界附近变得拥挤。
- 「乘子等于零就是被判对且分得很开的点」。 方向对,但严格说是「间隔不小于 1 且乘子为零」。 软间隔下,如果一个点间隔大于 1 但乘子不是零,那说明求解还没收敛。
自测三问(点开对答案)
1. 为什么「乘子在 0 和 C 之间」的样本,间隔必须精确等于 1? —— **因为乘子在这段区间里,KKT 的互补松弛条件要求对应的约束必须是紧的。** **约束是「间隔不小于 1」,紧了就是「间隔等于 1」。** **这批点就是真正「压在边界上」的点,它们决定了边界的精确位置。** 2. 删掉乘子为零的样本,重新训练,解为什么不变? —— **因为解只由「间隔小于等于 1」的那些点决定,其余的点在优化里从来没被激活过。** **它们对偶目标里的贡献是零(乘子为零),删掉等于删掉零项,最优点不动。** **实测的差别 2.6e-05 完全来自求解精度,不是信息差。** 3. 如果多加一个离边界很远、但标错了类的点,边界会变吗? —— **会变,而且可能变得很厉害——前提是 C 足够大。** **标错的远点间隔是很大的负数,它一定会变成支持向量(乘子顶到 C),把边界往自己这边拽。** **这和「加一个标对的远点」是两件完全不同的事:后者乘子为零,前者话语权用满。** **8.6 的 ramp 损失就是为了对付这种情况。**到这儿为止,边界一直是直线。可现实里的数据经常是「一圈套一圈」,任何直线都分不开。 下一节的办法不是换模型,而是换一个看数据的角度:把点映射到高维,让它在高维里变直, 然后只算内积、不算坐标。
- ☐ (本节完,下一节 8.4 8.4)