学支持向量机的时候,很多人卡在“拉格朗日对偶法”这一关。我知道那种感觉——公式推导看得懂,但总觉得缺一个“为什么”:为什么要转成对偶问题?KKT条件到底是来干嘛的?为什么推导完突然就蹦出一个核函数?说实话,我当时第一次看教材,从头到尾抄了一遍推导,抄完了还是懵。直到后来自己动手从零实现了一遍SVM,那些公式才真正连成一条线。
这篇内容,我就按一条我自己“终于懂了”的路径来写:从SVM怎么变成一个带约束的优化问题,到拉格朗日对偶法怎么给这个约束问题松绑,再到KKT条件怎么把两个问题焊接起来,最后落到手写一个简化版SMO算法,在二维数据上真正看到支持向量长什么样。如果你是刚学SVM的入门者,这篇能帮你把整条推导链打通;如果你已经会用工具包,这篇可以帮你把底层的数学直觉补齐。
1. SVM的优化问题:从“找一条线”到“解一个凸二次规划”
1.1 最大间隔与函数间隔、几何间隔的关系
SVM做的事情,一句话说就是:在两类样本之间找一条分割线(高维里是超平面),让这条线离两边最近的点都尽量远。这个“离最近的点尽量远”的距离,就是间隔。间隔越大,分类器对新样本的泛化能力通常越好,这个说法不是经验玄学,而是在结构风险最小化框架下有理论支撑的结论。
这里有两个容易混的概念:函数间隔和几何间隔。
- 函数间隔:对于样本((x_i,y_i)),函数间隔定义为(\hat{\gamma}_i = y_i(w^T x_i + b))。注意这里(y_i)取(+1)或(-1),所以只要分类正确且离边界越远,值就越大。
- 几何间隔:是函数间隔除以(|w|),即(\gamma_i = \frac{y_i(w^T x_i + b)}{|w|}),它的几何含义是样本点到超平面的真实距离。
为什么要用几何间隔来做优化目标?因为函数间隔有个致命问题:你把(w)和(b)同时放大2倍,超平面本身没变,但函数间隔变成了原来的2倍。这是个虚的膨胀。几何间隔因为除以(|w|),把这个尺度伸缩抵消掉了,只反映真实的几何距离。
所以我们的目标是:最大化几何间隔(\frac{1}{|w|} \min_i y_i(w^T x_i + b))。这一步的关键在于,最优化问题对尺度不敏感,所以我们可以做一个非常聪明的归一化:直接规定距离超平面最近的那些样本满足(y_i(w^T x_i + b) = 1)。为什么可以这么规定?因为(w)和(b)同时缩放多少倍,超平面都不变,我们完全可以选定一个尺度,让最近样本的函数间隔恰好为1。
做完这个归一化,最大化最小几何间隔(\frac{1}{|w|}),就等价于最小化(\frac{1}{2}|w|^2)。
于是SVM的原始问题变成一个标准的带约束优化问题:
[ \min_{w,b} \frac{1}{2}|w|^2 ]
[ \text{s.t.} \quad y_i(w^T x_i + b) \ge 1, \quad i = 1, ..., n ]
这个形式非常重要。它说明SVM不是凭空冒出来的,它的本质是一个带有不等式约束的凸二次规划。
1.2 为什么这个优化问题是“凸”的,凸有什么好处
目标函数(\frac{1}{2}|w|^2)是关于(w)的二次函数,而且Hessian矩阵是正定的(就是单位矩阵的倍数),所以它是严格凸函数。约束(y_i(w^T x_i + b) \ge 1)是线性不等式,线性不等式定义的可行域是凸集。
凸优化最大的好处就是:局部最优解就是全局最优解,而且对偶间隙为零(后面会讲到这意味着什么)。在SVM之前你如果学过感知机,你会发现感知机只要求找一个能把数据分开的超平面,找到哪个算哪个,解不唯一。而SVM因为有了“最大间隔”这个目标,解是唯一的、稳定的,这正是由凸性保证的。
我当时学到这里有个小心得:机器学习里凡是能写成凸优化的问题,基本都是“有标准答案”的问题,求解靠谱、理论完备。研究出SVM的人高明之处,就是先把这个几何直观问题精准地描述成凸二次规划,剩下的事情就可以交给最优化理论这个成熟工具箱了。
1.3 为什么要转到对偶问题而非直接求原始问题
到这里你可能会问:这个原始问题直接用现成的二次规划求解器不就行了吗?行,小数据量时确实可以。但转到对偶问题有它不可替代的价值:
- 对偶问题的变量个数等于样本数(n),而不等于特征维度(d)。当特征维度很高的时候,原始问题在高维空间里求优化不方便,对偶形式反而更舒服。
- 对偶问题暴露了“样本内积”这个结构,核函数由此自然登场,这是SVM最有杀伤力的武器。
- 对偶形式里,大部分(\alpha_i)会优化为0,只有支持向量对应的(\alpha_i)非零。这个稀疏性既是计算上的优势,也是SVM解释性的来源。
所以接下来的主题,就是这个看似抽象的数学工具——拉格朗日对偶法。
2. 带约束极值:拉格朗日乘子法是拉格朗日对偶法的地基
2.1 先说最简单的:等式约束下的拉格朗日乘子法
再深入对偶之前,先退一步看一个老朋友:在等式约束下求极值。假设你要在曲线(g(x,y)=0)上找(f(x,y))的最小值。教科书里的方法是构造:
[ L(x,y,\lambda) = f(x,y) + \lambda g(x,y) ]
然后对(x)、(y)、(\lambda)分别求偏导等于0,联立求解。
怎么理解这件事的几何直觉?在极值点,目标函数的等高线和约束曲线必然相切,也就是说目标函数的梯度(\nabla f)和约束函数的梯度(\nabla g)方向平行。(\lambda)就是那个把它们联系起来的比例系数。对(\lambda)求偏导等于0,等价于把约束(g(x,y)=0)给“请”了回来。
这个视角很重要。拉格朗日乘子法不是某个天才凭空发明的公式,它的本质就一句话:在约束曲面上,梯度方向被分解,极值处目标梯度与约束梯度平行。
2.2 不等式约束下的拉格朗日函数:一个容易坑人的符号约定
SVM的约束是不等式(y_i(w^T x_i + b) - 1 \ge 0),不是等式。这种约束怎么处理?思路是给每个不等式配一个拉格朗日乘子(\alpha_i \ge 0),把不等式“吸收”进目标函数。
这里有一个决定后面所有推导方向的约定问题。我的写法采用国内教材里最常见的一种:
[ L(w,b,\alpha) = \frac{1}{2}|w|^2 - \sum_{i=1}^n \alpha_i \left( y_i(w^T x_i + b) - 1 \right) ]
其中要求(\alpha_i \ge 0)。
这个减号怎么来的?可以这样想:在可行域内,(y_i(w^T x_i + b) - 1 \ge 0)恒成立。如果某个(\alpha_i)是负数,那么(-\alpha_i(\cdots))这一项反而可能把目标函数往下拉,导致优化走偏。所以约束(\alpha_i \ge 0)是我们构造拉格朗日函数时主动加的,目的是让对偶问题给出一个有意义的、不越过可行域的优化。
如果你去看其他资料,会发现有人写成加法形式:
[ L(w,b,\alpha) = \frac{1}{2}|w|^2 + \sum_{i=1}^n \alpha_i \left( 1 - y_i(w^T x_i + b) \right) ]
这两种写法本质一样,只是(\alpha_i)的取值范围和后面推导的“对齐方向”不同。我自己学的时候在符号上栽过跟头:看A教材推导到一半,去查B教材,发现求偏导的符号对不上,整个推导就乱套了。所以我的建议是从头到尾选定一种约定跟到底,不要混着看。
2.3 min-max与max-min:原始问题和对偶问题的关系
有了拉格朗日函数,下一步的操作是整个对偶法的核心结构。
考虑这样一个量:(\max_{\alpha \ge 0} L(w,b,\alpha))。对固定的(w,b),如果某个样本违反了约束(即(y_i(w^T x_i + b) < 1)),那么当(\alpha_i \to \infty)时,(-\alpha_i(\cdots))会趋向(+\infty),整个最大值就爆炸了。反过来,如果所有样本都满足约束,那么最大时应该让所有(\alpha_i)都取0。所以:
[ \max_{\alpha \ge 0} L(w,b,\alpha) = \begin{cases} \frac{1}{2}|w|^2, & \text{若所有约束都满足} \ +\infty, & \text{若有任一约束被违反} \end{cases} ]
接下来再对(w,b)求最小,就得到了原始问题的等价形式:
[ \min_{w,b} \max_{\alpha \ge 0} L(w,b,\alpha) ]
这个形式表面上没什么用,但它揭示了一个结构:我们先是“拉格朗日化”把约束放进目标,再通过(\max)把违反约束的情况用无穷大惩罚掉。
现在把min和max交换一下顺序,得到对偶问题:
[ \max_{\alpha \ge 0} \min_{w,b} L(w,b,\alpha) ]
这个交换不是白换的。内层(\min_{w,b} L)没有约束,可以直接求偏导解出闭式解,代入后得到一个只关于(\alpha)的函数,这就是对偶函数。为什么要费这个劲?因为对偶问题往往更好解,而且能带出支持向量和核函数。
这里必须理清一个关键概念:原始问题和对偶问题不是总相等的。我们解对偶问题,本质上是解一个“原始问题的下界”。对偶问题的最优值一定小于等于原始问题的最优值,这叫弱对偶。两者相等的情况,叫强对偶。SVM恰好属于强对偶成立的场景,这也是为什么我们敢放心去解对偶问题。
3. 强对偶与KKT条件:原始问题和对偶问题何时握手
3.1 弱对偶恒成立,强对偶需要Slater条件
为什么说对偶问题的最优值(d^)永远不超过原始问题的最优值(p^)?因为对任意(w,b)和(\alpha \ge 0),有:
[ \min_{w,b} L(w,b,\alpha) \le L(w,b,\alpha) ]
对任意的(\alpha)取最大后,依然满足:
[ \max_{\alpha \ge 0} \min_{w,b} L(w,b,\alpha) \le \min_{w,b} \max_{\alpha \ge 0} L(w,b,\alpha) ]
这是一个恒成立的不等式关系,不需要任何额外条件。真正的问题是:什么时候等号成立?对凸优化问题,有一个很方便的充分条件叫Slater条件:如果存在一个严格可行的点,也就是说存在某个((w,b))让所有不等式约束都严格成立(不是贴着边界),那么强对偶成立。
3.2 SVM为什么天然满足强对偶条件
回到SVM的约束:(y_i(w^T x_i + b) \ge 1)。只要数据是严格线性可分的(不存在恰好落在分界线上或重叠的点),你总能稍微放大一下间隔,找到一个让所有约束都严格大于1的超平面。这个“严格大于”就满足了Slater条件,因此SVM的原始问题和对偶问题有相同的解。
这就是为什么SVM的教科书推导都在解对偶问题,却没有花大篇幅解释“为什么可以这样交换顺序”——因为对满足Slater条件的凸问题,这个交换是合法的。理解了这一点,你再回看那些跳过强对偶条件直接交换min-max的推导,就知道作者默认了什么前提。
其实完整地说,即使数据不是严格线性可分的,加了松弛变量后,只要处理得当,强对偶依然能成立。严谨性在真正的推导中当然重要,但初学阶段你首先要建立“SVM能安全转对偶”的直觉。
3.3 KKT四个条件逐一拆解
当强对偶成立时,原始问题的最优解和对偶问题的最优解必须满足KKT条件。KKT条件本质上是“极值点梯度为零+约束可行性+互补松弛”的组合。对SVM来说,具体是四条:
- 梯度为零:(\frac{\partial L}{\partial w} = 0),(\frac{\partial L}{\partial b} = 0)。这是无约束最优化的必要条件,拉格朗日函数里的极值点必须满足。
- 原始可行性:(y_i(w^T x_i + b) - 1 \ge 0)。这是SVM本身的约束,不能丢。
- 对偶可行性:(\alpha_i \ge 0)。这是我们构造对偶问题时要求(\alpha_i)非负带来的条件。
- 互补松弛:(\alpha_i (y_i(w^T x_i + b) - 1) = 0)。这是最意味深长的一条。
互补松弛说的是:如果一个样本对应的(\alpha_i > 0),那么它必须刚好在约束边界上,也就是(y_i(w^T x_i + b) = 1)。反过来,如果一个样本离边界有距离((y_i(w^T x_i + b) > 1)),那它的(\alpha_i = 0)。
想想这是什么意思:只有那些距离超平面最近、恰好挂在线上的样本,才会在优化里留下非零的(\alpha_i),其他样本的(\alpha_i)全部归零。这些“恰好在线上的点”就是支持向量。所以KKT条件不只是一堆公式,它是SVM“稀疏性”的理论来源——到最后模型只需要支持向量就能完全描述。
我第一次真正被这个公式打动,就是在自己写代码时打印出每个(\alpha_i),发现除了少数支持向量对应的(\alpha)非零,其他全是数值上接近于0的小数。那个瞬间,书上的互补松弛公式才在我脑子里立起来。
4. 对偶形式推导:w是怎么“消失”的,核函数入口是怎么出现的
4.1 对w和b求偏导等于0,代入消元
强对偶成立,我们就可以放心求对偶问题。先把拉格朗日函数对(w)和(b)求偏导:
[ \frac{\partial L}{\partial w} = w - \sum_{i=1}^n \alpha_i y_i x_i = 0 \quad \Rightarrow \quad w = \sum_{i=1}^n \alpha_i y_i x_i ]
[ \frac{\partial L}{\partial b} = -\sum_{i=1}^n \alpha_i y_i = 0 \quad \Rightarrow \quad \sum_{i=1}^n \alpha_i y_i = 0 ]
第一个式子揭示了最优(w)的形态:它是所有样本的加权线性组合,权重是(\alpha_i y_i)。这本身就是一个极简的观点——分隔超平面的法向量完全由训练数据张成。
第二个式子则是一个藏在约束里的等式:所有(\alpha_i y_i)加起来必须为0。这个约束在对偶问题中会一直跟着,后面SMO算法每次更新都绕不开它。
把这两个式子代回拉格朗日函数,就能消掉(w)和(b)。我在推导时习惯一步步展开:
先看(\frac{1}{2}|w|^2)这一项,代入(w = \sum \alpha_i y_i x_i)后变成:
[ \frac{1}{2}\sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j (x_i^T x_j) ]
再看(\sum_i \alpha_i y_i (w^T x_i + b) - \sum_i \alpha_i)这一整块,其中(\sum_i \alpha_i y_i w^T x_i = \sum_i \sum_j \alpha_i \alpha_j y_i y_j (x_i^T x_j)),而(\sum_i \alpha_i y_i b = b \sum_i \alpha_i y_i = 0)。
所有的二次项合并后,对偶目标函数变成:
[ \max_{\alpha} \sum_{i=1}^n \alpha_i - \frac{1}{2}\sum_{i=1}^n \sum_{j=1}^n \alpha_i \alpha_j y_i y_j (x_i^T x_j) ]
约束就两条:
[ \alpha_i \ge 0, \quad \sum_{i=1}^n \alpha_i y_i = 0 ]
整个过程里,(w)和(b)这两个原始变量完全消失了,取而代之的是每个样本一个的(\alpha_i)。这就是拉格朗日对偶法在SVM里最直观的功劳。
4.2 对偶问题的最终形态与KKT联立解释
把这个对偶问题用矩阵形式写出来,会更清楚一点。令(Q_{ij} = y_i y_j x_i^T x_j),那么对偶问题变成:
[ \max_{\alpha} \quad \mathbf{1}^T \alpha - \frac{1}{2} \alpha^T Q \alpha ]
[ \text{s.t.} \quad \alpha \ge 0, \quad y^T \alpha = 0 ]
QF矩阵(Q)是半正定的(因为可以写成(y * X)的外积形式),所以这也是一个凸二次规划,不过是关于(\alpha)的。用SMO算法可以高效求解。
求解完(\alpha)之后,原始问题的参数通过下面两个式子恢复:
[ w = \sum_{i} \alpha_i y_i x_i ]
[ b = y_j - w^T x_j \quad \text{(对某个} \alpha_j > 0 \text{的支持向量)} ]
结合KKT的互补松弛条件,这里的一切都圆上了:(\alpha_i > 0)的样本就是支持向量,它们挂在边界上,给出(w)的表达式,同时提供计算(b)所需的信息;(\alpha_i = 0)的样本对模型完全没贡献。
4.3 内积一出现,核函数的入口就打开了
上面的推导出现了一个极其关键的形态:目标函数里所有数据都以内积(x_i^T x_j)的形式出现,决策函数也是。这意味着什么?意味着我们根本不关心单个样本的长相,只关心样本两两之间的内积。
如果原始特征空间里样本不是线性可分的,我们可以先把样本映射到一个更高维的空间(\phi(x)),然后在高维空间里做线性SVM。这个操作放到对偶问题里,唯一的改动就是把所有内积(x_i^T x_j)替换成高维内积(\phi(x_i)^T \phi(x_j))。
核函数(K(x_i, x_j))正是用来规避“显式写高维映射”的:只要这个函数能等价于某个高维空间中的内积,我们就不需要真的去算(\phi(x)),计算量还停留在低维。这就是著名的核技巧。对偶形式里内积作为唯一的数据交互形式,是核技巧能成立的前提。如果你直接解原始问题,特征映射写都写不出来,更谈不上什么核技巧。
所以核函数的出现不是SVM的发明,而是对偶形式“自我暴露”出来的红利。学到这一层,你才算真正明白为什么教材要坚持走拉格朗日对偶这条路。
5. 训练完的模型参数怎么恢复:从α到w和b
5.1 w是支持向量的加权组合
对偶问题解出来后,模型参数(w)很简单:
[ w = \sum_{i=1}^n \alpha_i y_i x_i ]
因为大多数(\alpha_i = 0),本质求和只剩支持向量。所以(w)是支持向量的加权线性组合,权重由(\alpha_i y_i)给出。这个式子我在实现的时候直接用了numpy的一行代码:
w = np.sum(alpha * y * X, axis=0)其中alpha、y、X都是numpy数组。注意这里必须用alpha乘y逐元素相乘,得到一个与样本同维的权重向量列表,再按样本维度求和。这个操作在数值上很稳定,但要注意alpha里那些理论上应该是0的点,实际会有极小的非零值(比如(10^{-12})),求和时不会造成明显误差,但这给了我们一个判断支持向量的标准:设一个小阈值,比如alpha > 1e-5的样本才算有效支持向量。
5.2 b的求法:一个容易出错的细节
(b)的求法没有(w)那么直观,很多初学者在这里踩坑。根据KKT互补松弛条件,对于任意(\alpha_i > 0)的支持向量,有:
[ y_i(w^T x_i + b) = 1 ]
两边乘(y_i)(注意(y_i^2 = 1)),得到:
[ b = y_i - w^T x_i ]
理论上,用任意一个支持向量代入都能得到相同的(b)。但数值计算中有浮点误差,实践中更稳的做法是取所有支持向量计算的平均值:
support_vectors = np.where(alpha > 1e-5)[0] b = np.mean(y[support_vectors] - X[support_vectors].dot(w))我当时自己实现的时候,第一版只用了一个支持向量求(b),结果每次运行结果在小数点后第三位有波动。改成所有支持向量平均之后,稳定性立刻好了。这里我想强调一个容易被忽略的点:取平均本身不是一个数学要求,而是一个数值稳定性措施——KKT条件在数学上保证了每个支持向量给出的(b)相同,但浮点运算下平均值更可靠。
5.3 软间隔扩展:对偶形式只多了一个C上界
现实数据几乎都不是严格线性可分的,允许部分样本犯错是常态。硬间隔SVM对噪声极其敏感,一个离群点就能把整个决策边界拉偏。解决办法是引入松弛变量(\xi_i),允许第(i)个样本突破边界一段距离,同时在目标函数里加惩罚项。原始问题变成:
[ \min_{w,b,\xi} \frac{1}{2}|w|^2 + C \sum_{i=1}^n \xi_i ]
[ \text{s.t.} \quad y_i(w^T x_i + b) \ge 1 - \xi_i, \quad \xi_i \ge 0 ]
(C)是惩罚系数,越大代表越不能容忍违反约束。
神奇的事情来了:重新走一遍拉格朗日对偶的推导,最终的对偶问题几乎不变,唯一的变化是约束从(\alpha_i \ge 0)变成了:
[ 0 \le \alpha_i \le C ]
也就是说,(C)给拉格朗日乘子加了一个上限。这个变化在SMO算法里就是一行裁剪逻辑,但它带来的影响是本质的:支持向量的权重不再像硬间隔那样无上限,异常点不会获得过大的影响力,模型的鲁棒性因此大大提升。
我第一次实现的时候,先用硬间隔跑了一个带噪声的数据集,决策边界被一个孤立点拽得面目全非。然后把代码改成软间隔,加了个裁剪操作,边界立刻恢复了直觉上合理的位置,那一刻我才真正理解(C)这个参数为什么是SVM最重要的旋钮之一。
6. 手写一个简化版SMO:从零实现线性与RBF支持向量机
6.1 SMO的核心直觉:为什么一次必须改两个α
对偶问题有等式约束(\sum_i \alpha_i y_i = 0)。如果每次只改一个(\alpha_i),这个约束很容易被打破。比如你改了(\alpha_1),剩下所有(\alpha)的和就必须反向变化才能维持等式成立,但你又不能一次动“剩下所有”——这就很麻烦。
SMO的思路是:每次固定其他所有(\alpha),只优化两个(\alpha_i)和(\alpha_j)。因为只有两个变量时,等式约束变成一个线性关系,老(\alpha_i)和新(\alpha_j)之间存在线性替换关系,可以把问题化成一元二次优化,直接有闭式解。这就是SMO“每次选一对”的根本原因。
所以SMO表面上的技巧是“坐标下降”,里子其实是等式约束逼出来的“成对更新”。理解了这个动机,你再看Platt那篇论文里的各种启发式选点规则,才不觉得那是一个黑盒。
6.2 α更新的核心式子和裁剪逻辑
对于一对(\alpha_i, \alpha_j),更新公式可以写得很紧凑。定义误差:
[ E_i = u_i - y_i ]
其中(u_i = \sum_j \alpha_j y_j K(x_j, x_i) + b)是当前模型对样本(i)的预测值。再定义学习步长的分母:
[ \eta = K(x_i, x_i) + K(x_j, x_j) - 2K(x_i, x_j) ]
则(\alpha_j)未经裁剪的更新值为:
[ \alpha_j^{new, unc} = \alpha_j^{old} + \frac{y_j (E_i - E_j)}{\eta} ]
然后根据(y_i)和(y_j)是否相同,以及(\alpha_i + \alpha_j = s)这个线性关系,把(\alpha_j)裁剪到([L, H])区间内。简单起见,我实现的版本里直接统一用(C)作为上下界:
[ \alpha_j^{new} = \min(\max(\alpha_j^{new, unc}, 0), C) ]
最后用等式约束反推(\alpha_i):
[ \alpha_i^{new} = \alpha_i^{old} + y_i y_j (\alpha_j^{old} - \alpha_j^{new}) ]
这一步是SMO里最容易写错的地方,我当时调试了半天才发现是自己忘了乘(y_i y_j)。等式的推导细节是:由(\sum \alpha_k y_k = 0),改动前后(\alpha_i^{old} y_i + \alpha_j^{old} y_j = \alpha_i^{new} y_i + \alpha_j^{new} y_j),解出来就是这个式子。
更新完(\alpha)后,还要更新(b)。标准的做法是根据更新后的(\alpha_i)和(\alpha_j),分别计算两个候选(b_1, b_2),如果(\alpha_i)在((0, C))内则取(b_1),如果(\alpha_j)在((0, C))内则取(b_2),否则取两者平均。这个逻辑同样源于KKT条件:只有(0 < \alpha < C)的点才严格在边界上,才有资格决定(b)。
6.3 核心代码:核函数、误差缓存、b更新
我写的这个简化版SMO不追求工程效率,主要为了把对偶形式跑通。核函数我实现了线性和RBF两种,代码结构上把核矩阵预计算好,这样SMO迭代时只查表,不用反复算内积,代码更清晰。
import numpy as np def linear_kernel(X1, X2): return X1.dot(X2.T) def rbf_kernel(X1, X2, gamma=0.5): sq1 = np.sum(X1**2, axis=1).reshape(-1, 1) sq2 = np.sum(X2**2, axis=1).reshape(1, -1) dist2 = sq1 - 2 * X1.dot(X2.T) + sq2 return np.exp(-gamma * dist2) def simple_smo(X, y, C=1.0, kernel='linear', max_iter=100, tol=1e-3): n = X.shape[0] K = linear_kernel(X, X) if kernel == 'linear' else rbf_kernel(X, X) alpha = np.zeros(n) b = 0.0 for it in range(max_iter): changed = 0 for i in range(n): Ei = (alpha * y).dot(K[i]) + b - y[i] if (y[i] * Ei < -tol and alpha[i] < C) or (y[i] * Ei > tol and alpha[i] > 0): # 选一个j,这里简化处理:随机挑 j = np.random.choice([k for k in range(n) if k != i]) Ej = (alpha * y).dot(K[j]) + b - y[j] alpha_i_old, alpha_j_old = alpha[i], alpha[j] eta = K[i, i] + K[j, j] - 2 * K[i, j] if eta <= 0: continue alpha[j] += y[j] * (Ei - Ej) / eta # 裁剪 alpha[j] = min(max(alpha[j], 0), C) alpha[i] += y[i] * y[j] * (alpha_j_old - alpha[j]) # 更新b b1 = b - Ei - y[i] * (alpha[i] - alpha_i_old) * K[i, i] - y[j] * (alpha[j] - alpha_j_old) * K[i, j] b2 = b - Ej - y[i] * (alpha[i] - alpha_i_old) * K[i, j] - y[j] * (alpha[j] - alpha_j_old) * K[j, j] b = (b1 + b2) / 2 changed += 1 if changed == 0: break return alpha, b这段代码我建议初学者一行行对照公式看。有两个地方和完整SMO差别很大:一是j的选取完全随机,没有走启发式;二是没有维护误差缓存E的全局副本,每次现算。这两点都会让收敛变慢,但逻辑简单、不容易写错,适合用来理解原理。等跑通这个版本,再看Platt原文里的选点策略,就是锦上添花的事了。
6.4 二维实验效果观察支持向量
我用这个简化版SMO跑了一个二维模拟数据实验:程序生成两类高斯分布的点,每个类50个样本,先跑线性核,再跑RBF核,然后把决策边界和支持向量画出来。
关键的观察点有两个。第一,线性核时,决策边界就是一条直线,落在边界线上的点恰好是那些(\alpha)大于阈值的支持向量,数量远少于全部训练样本。第二,换成RBF核之后,决策边界变成一条弯曲的曲线,原本线性不可分的数据被完整分开,而且边界附近的支持向量依然清晰可见。
从(\alpha)的数值分布看,大量样本的(\alpha)都是0,只有一小撮非零。这个实验让我彻底明白了一件事:SVM的模型容量不是由样本总量决定的,而是由支持向量决定的。这也是它在小样本、高维场景下特别能打的原因。
7. 学习这条路的几个抗坑心得
7.1 符号约定混乱是我见过最多的问题
拉格朗日函数到底写成加法还是减法,不同资料、不同课程习惯不同。这本身不是错误,但它会连锁影响你对偶推导中的每个正负号。我的建议是:选定一种约定,把从拉格朗日函数到最终对偶形式的推导从头到尾完整写一遍,中途不换教材。等你把一套推顺了,再看其他写法就只是符号层面的转换而已。
7.2 数值上与理论上的α,千万不要拿微小值硬套
理论上(\alpha_i)应该精确等于0的样本,在数值计算中得到的往往是(10^{-12})这种极小值。如果你用alpha > 0去筛支持向量,会把几乎所有样本都筛进来,决策边界就乱了。实践里设一个阈值,比如1e-5,小于阈值的直接按0处理。同样的道理也适用于判断KKT条件是否满足,不要用严格等于,要用容忍度。
7.3 别急着上现成求解器,先手推一遍再动手实现
现在用现成的向量机工具包,一行代码就能完成训练,核函数随便换,参数自动调。但我仍然强烈建议你至少手写一次SMO,哪怕是最简化的版本。因为你在推导中踩过的每一个正负号错误、每一个裁剪边界问题,都会让你对SVM的认知深一层。我自己的经验是:手写完一个简化SMO之后,再回头看书里的拉格朗日对偶推导,公式之间突然就有了手感——它们不是一堆需要背的符号,而是一条有来龙去脉的推理链。
最后再分享一个小技巧。如果你在学对偶问题时感到抽象,可以像我一样把整个推导拆成“原始问题→拉格朗日函数→对偶函数→KKT条件→SMO算法→参数恢复”六步,每步用代码验证一个中间结果。数学公式的意义,往往不是看出来的,是跑出来的。等你亲眼看自己的程序正确分出了两类点,那些公式才算真正长在了你的脑子里。