AI 工程地基 27|最宽的一条街,最少的几颗钉子

有一份 119 个点的数据,两类,中间留着一条空带。课程自带的程序训练完之后宣布:这 119 个点里有 16 个支持向量,也就是拽住分界线的关键点。

目录
  1. 最宽的一条街,最少的几颗钉子
  2. 先把街修起来
  3. C 的旋钮,拧的不是它说的那个东西
  4. 钉子站在街里,不只站在街沿上
  5. 印出来的街宽,也不是那条街
  6. 好的部分:把高维藏在点积里
  7. 打平的对手,不同的行李

最宽的一条街,最少的几颗钉子

有一份 119 个点的数据,两类,中间留着一条空带。课程自带的程序训练完之后宣布:这 119 个点里有 16 个支持向量,也就是拽住分界线的关键点。

我把同一份数据交给 scikit-learn,把容忍度拧到最严,逼它给出教科书定义下那条最宽的街。它说:2 个。

119 个点,真正决定这条线的,只有 2 颗钉子。这是支持向量机最漂亮的地方,也是这门课自己没走到的地方。

先把街修起来

两堆点,中间画一条线。能用的线有无穷多条,选哪条?

想象这条线是一条街的中心线,街的两沿各压着一类点。街修得越宽,新来的点越不容易掉进错误的领地。训练数据能告诉你的就这么多,未来的点会在训练点附近晃,街宽就是提前留出的容错空间。支持向量机的答案:选街最宽的那条线。

怎么把「最宽」写成数学?一条直线写成 w·x + b = 0,一个点到这条线的距离是 |w·x + b| 除以 w 的长度。要比较街宽,先把两侧的街沿各自钉在最近的点上,也就是要求每个点满足 y·(w·x + b) ≥ 1。在这个约束下,街宽恰好等于 2 除以 w 的长度。于是问题变成:把 w 的长度压到最小,同时所有点都留在街外。

这是个凸二次规划。凸的好处第 18 课讲过:只有一个全局最优解,没有局部坑,从哪儿出发都通向同一个答案。骑在街沿上的那些点,y·(w·x + b) 恰好等于 1 的点,叫支持向量。它们是仅有的钉子。删掉任何一个站在街外的点,线纹丝不动;碰一颗钉子,线就跟着走。

课本给了 50 行纯 Python:梯度下降直接压铰链损失——这种损失的规矩是,点站得离街沿足够远就一分不罚,一旦越线,罚金随深度线性上涨。不需要二次规划求解器。先替它说句公道话,这 50 行是对的。同样的数据、同样的参数,手写版和工业库逐档对齐:C 从 0.001 扫到 100,手写版量出的街宽一档档是 6.41、2.99、1.74、1.45、1.39,scikit-learn 给 6.44、2.99、1.69、1.45、1.40,一路贴住。一个下午的手写能到这个程度,地基是实的。

麻烦出在它给自己讲的故事上。

C 的旋钮,拧的不是它说的那个东西

真实数据很少能一刀切干净。总有几个点站错边,或者缩在街里。于是有了软边距:给每个点发一张越界券,越多少记多少,再乘上一个价格 C。课本的表格说:C 大,罚得狠,街窄,错分少,会过拟合;C 小,管得松,街宽,错分多,会欠拟合。

这句话在它自己的演示数据上,两头都反着。

演示程序造了 300 个带噪点,扫了六档 C,表格印出来:C=0.001 时街宽 6.28,训练准确率 0.9042,是六档里最高的;测试准确率 0.8667,是六档里最低的。C=100 时街宽 1.39,训练 0.8875,测试 0.9000,测试反而是六档里最高的。表格底下紧跟着两行总结:小 C 分错更多、泛化更好;大 C 分错更少、有过拟合风险。总结句和正上方的表格说的是反的,而打印它们的是同一个程序、同一份数据。

怕是单次运气,我换了十个随机种子重跑。方向稳定:小 C 的训练准确率每次都更高,平均 0.9008 对 0.8879;测试每次都更低,0.8683 对 0.9000。再交给精确求解器复核:C=0.001 分错 24 个点,C=100 分错 27 个。「大 C 分错更少」在这份数据上不成立。

那 C 到底在优化什么?把每个点的越界深度加起来,这正是罚单上算的那笔数:C=0.001 时是 135.4,C=0.01 时 79.6,C=0.1 时 63.2,C=100 时 61.2。一路单调下降,一步不回头。与此同时,分错数纹丝不动地停在 24 到 27 之间。

C 这个旋钮拧的不是「分错几个」,是「越界多深」。在干净可分的数据上这两件事同涨同落:街一收窄,站不下的点被推过线,就成了分错。在带噪数据上它们分家了。把三个点干脆推过线,比让六个点深深陷在街里,罚单更便宜。课本的表格讲的是前一个世界的规律,演示的数据生成在后一个世界。

钉子站在街里,不只站在街沿上

更深的一处,出在钉子的定义上。

课本说,支持向量是「恰好骑在街沿上的点」。这个定义属于硬边距的世界。软边距一开,格局就变了。每个点背后藏着一个数 α,可以理解为这个点拽住线的力道:α 是零,这个点就完全撒手。数学上可以严格证明,α 大于零当且仅当这个点的 y·(w·x + b) ≤ 1。骑在街沿上的、被允许缩进街里的、干脆站到对面去的,全都拿着非零的力道,全都在拽这条线。钉子不止站在街沿上,还有一批站在街里面。

课程的数法只数前者:在 y·(w·x + b) = 1 附近开一条小带子,落进带子的算支持向量。C=0.001 那行,它数出 64 个。我换成 y ≤ 1 从头数:240 个训练点里 229 个。那条 6.28 宽的街把 95% 的训练点都装了进去。scikit-learn 在同一档位给出的支持向量数:230。两套独立实现都说真实答案在 229 附近。课程印的 64,不到真实数的三分之一,站在街里的那批点被整个漏掉了。

这不是数错一个数的小事。支持向量的个数之所以重要,是因为它给泛化误差提供了上界:留一法的错误率,不会超过支持向量在数据中的占比。数漏成这样,这个上界就无从谈起。

还有个细节值得玩味。课本的动手环节分两步:第 2 步用梯度下降训出模型,第 4 步找支持向量,代码里写的容差是 0.001。这两步拼起来跑一遍,返回一个空列表,一颗钉子都没有。程序没崩,是几何对不上:软边距训出来的解里,最近的点站在 y·f(x) = 0.69 的位置,六个点缩在街里,其余全在街外,但没有任何一个恰好等于 1。「骑在街沿上」是硬边距解才有的景象——在那个解里,最近的点被精确地钉在 1 上。仓库里的完整代码悄悄把容差放宽到 0.1、0.3、0.5,才数得出东西;课文里那个 0.001 的版本,跑在课文自己第 2 步训出来的模型上,一颗钉子都找不到。

印出来的街宽,也不是那条街

顺着看下去,程序末尾印的「margin width」同样值得多看一眼。它印的是 2 除以 w 的长度。这个数等于街宽的前提,还是「最近点恰好骑在街沿上」。

第一组演示实测:印出来 0.906。这个解实际留出的空街有多宽?正类最近的点离中线 0.313,负类最近的点离中线 0.429,加起来 0.742,比印出来的窄了 18%。而这份数据如果按硬边距来训,能拿到的最优街是 0.778,钉子恰好 3 颗。三个数,0.906、0.742、0.778,量的是三件不同的事,程序印的是最乐观的那个。第二组数据更直白:按它印出的权重算,名义街宽 1.57,硬边距真解 1.29;它自己的表格里就印着一个 y·f(x) = 0.656 的点,作为「这条街其实没那么宽」的活证据,只是程序没往那儿看。

好的部分:把高维藏在点积里

这门课的好部分,也值得讲透。

SVM 有一个著名的改写:把优化问题换成等价的对偶形式,换完之后,整个问题只通过点积看到数据,x_i·x_j,别无他物。于是有人想到,把点积换成一个任意的二元函数 K,等于偷偷换掉了背后的几何。多项式核 (x·z + c)^d 把数据隐式送进一个维度按 d 次方暴涨的空间,但计算本身还是原来的开销;RBF 核干脆对应一个无穷维的空间。你永远不用真的去那个空间,需要的只是那个空间里的内积值,而核函数一步把它算完。

演示数据是一内一外两个同心圆。线性 SVM 的测试准确率 0.55,和抛硬币差不多。手工加三个特征——x₁的平方、x₂的平方、两者的乘积,圆的方程本来就是它们的线性组合——再用同一个线性 SVM:0.90。核技巧做的事,就是把这种「替你造特征」的活儿,藏在点积里自动化掉。

打平的对手,不同的行李

最后是和逻辑回归的对比。同一个数据,两者测试准确率完全打平:0.9250 对 0.9250。差别在行李上:按正确的数法,SVM 的解由 22 个点撑着,逻辑回归的行李里装着全部 160 个。顺带修正课本一句话:它说 SVM 预测省内存是因为「只存支持向量」——这话对带核的 SVM 成立;线性 SVM 的预测只需要 w 和 b 两个数,连那 22 个点都不用带,比它说的还省。

课文的练习 2 让你把 C 从 0.001 扫到 1000,看街从宽变窄。值得多做一步:每个 C 下,数一数街里站着多少个点,用 y ≤ 1 这个数法,别用带子。你会看到 C=0.001 时 240 个点里 229 个缩在街里,C=100 时只剩 62 个。一条街从吞掉整片数据,收窄到只裹住四分之一的点。C 的全部故事,就在 229 和 62 这两个数字之间。

暂无表态

想参与讨论或点赞?登录后使用完整功能

讨论回复(0)

暂无回复,登录后可参与讨论
合作

智谱 GLM-5 已上线

在智谱开放平台 BigModel.cn 打造 AI 应用。新一代旗舰模型 GLM-5 在推理、代码、智能体综合能力达到开源模型 SOTA。

领取 2000万 Tokens