ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

拆解SMO算法:从凸二次规划到双变量解析更新的全程推导

拆解SMO算法:从凸二次规划到双变量解析更新的全程推导 提起 SMO很多人的第一印象是“SVM 库内部那个优化器”——调包调久了接口后面发生了什么很少有人细看。但序列最小最优化算法SMO值得每一个做机器学习的人认真拆一遍它把一个大到近乎无解的二次规划问题拆成无数个只包含两个变量的子问题而且每个子问题都能用解析公式直接算出不需要任何数值优化库。这个设计在今天看来依然非常聪明理解它之后你会对 SVM 的训练过程、支持向量的含义、甚至调参时那些“说不清但确实有效”的经验有完全不一样的感觉。这篇文章不准备从零堆数学公式而是顺着 SMO 的求解思路走一遍先从 SVM 训练为什么难解讲起再拆 SMO 的核心机制、两个乘子的解析求解、启发式选择策略、收敛判定细节最后给一份可以照着改的 Python 简化实现以及我在实际调试中踩过的坑。适合想搞懂 SMO 内部逻辑的学生、面试被问到 SMO 细节的求职者以及想手动实现一遍而不是只调库的工程师。1. 为什么SVM训练必须拆成小问题一个被核矩阵卡死的优化困境1.1 从分类超平面到凸二次规划SVM 的分类思路是找一个间隔最大的超平面。线性可分情况下这个超平面要满足所有点都被正确分开同时离它最近的点要尽可能远。数学上这个问题等价于最小化 $\frac{1}{2}||w||^2$约束是 $y_i(w^T x_i b) \ge 1$。但现实数据几乎都不是线性可分的于是引入松弛变量 $\xi_i$ 和惩罚系数 $C$把目标改成$$ \min_{w,b,\xi} \frac{1}{2}||w||^2 C\sum_{i1}^{N}\xi_i $$加上约束 $y_i(w^T x_i b) \ge 1 - \xi_i$、$\xi_i \ge 0$ 之后用拉格朗日乘子法转成对偶问题就会得到下面这个形式$$ \max_{\alpha} W(\alpha) \sum_{i1}^{N}\alpha_i - \frac{1}{2}\sum_{i1}^{N}\sum_{j1}^{N}\alpha_i \alpha_j y_i y_j K(x_i, x_j) $$约束条件有三个$\alpha_i \ge 0$、$\alpha_i \le C$、$\sum_{i1}^{N}\alpha_i y_i 0$。这里的 $K(x_i, x_j)$ 是核函数也就是把原始特征映射到高维空间后的内积。SMO 就是在解这个对偶问题——准确说是在解这个带约束的凸二次规划QP问题。1.2 标准QP方法的内存噩梦如果从来没亲手解过 SVM 的对偶问题你可能会说这很简单啊找个现成的 QP 求解器丢进去不就好了理论上确实如此但一算复杂度就发现问题了。对偶目标函数中有一个 $\alpha_i \alpha_j K(x_i, x_j)$ 的求和项这意味着我们需要一个 $N \times N$ 的核矩阵 $K$。当训练样本有 1000 个时核矩阵是 1000×1000存下来大约 8MB当样本数到 1 万就是 800MB到 10 万80GB直接内存爆炸。即便你把矩阵稀疏化处理或分块迭代标准的数值优化方法每一次迭代都要做矩阵运算代价依然高得离谱。这就是 SVM 在早期很难大规模落地的原因。我记得以前在某高校实验室跑一个模拟项目数据量只有小几万直接用内点法求解对偶问题内存就吃到了好几个 G而且收敛非常慢。后来换了基于分解思想的优化器训练时间从几十分钟直接降到了几分钟这就是 SMO 这类算法存在的意义。1.3 SMO的做法把大问题切成“一口能吃下去”的小块SMO 的想法很直接既然一次性解 $N$ 个 $\alpha$ 代价太大那就一次只解其中的一小部分。它每次选择一个工作集只对这些 $\alpha$ 做优化固定其他所有 $\alpha$然后在子问题上求出解析解。关键点在于SMO 选的工作集大小是 2。为什么不能是 1因为约束 $\sum_i \alpha_i y_i 0$ 把整个 $\alpha$ 向量绑在了一个超平面上如果只动一个 $\alpha_i$约束瞬间就破坏了必须同时动另一个 $\alpha_j$ 来“补偿”它。所以 2 是保持等式约束可行的最小单位。每优化两个 $\alpha$SMO 只需要计算对应的核函数值不需要存储整个核矩阵。那些核函数值可以实时算也可以缓存一部分。空间复杂度从 $O(N^2)$ 降到了 $O(N)$这是 SMO 能处理大规模数据的最根本原因。2. 为什么每次只动两个拉格朗日乘子约束下的最小自由度2.1 单变量更新为何走不通很多第一次接触坐标下降法的人会想那我挨个 $\alpha_i$ 更新不就行了每次固定其他变量只优化一个这就是经典的坐标下降法。但 SVM 对偶问题里的等式约束 $\sum_i \alpha_i y_i 0$ 是一道硬墙。假设你固定除 $\alpha_1$ 外的所有 $\alpha$那么 $\alpha_1$ 的值就已经被唯一确定没有任何可调整空间。同理如果你固定 $\alpha_1, \alpha_3, \alpha_4, \dots$只动 $\alpha_2$$\alpha_2$ 同样被锁死。所以单变量子问题在数学上不可行必须同时更新两个变量才能有真正的自由度。这也是为什么 SMO 叫“序列最小优化”——每次优化的子问题规模是所有满足约束的最小子问题也就是两个变量。2.2 两个变量的组合方式等式约束下的一条线段当只看两个变量 $\alpha_1$ 和 $\alpha_2$ 的时候等式约束变成$$ y_1 \alpha_1 y_2 \alpha_2 y_1 \alpha_1^{old} y_2 \alpha_2^{old} \text{常数} $$这条直线在 $(\alpha_1, \alpha_2)$ 平面上是一条斜线再加上 $0 \le \alpha_i \le C$ 的盒子约束可行域就是这条直线穿过盒子后剩下的线段。换句话说无论我们怎么调整$\alpha_1$ 和 $\alpha_2$ 都必须落在这条线段上。一旦确定 $\alpha_2$ 的新值$\alpha_1$ 的新值就被这条直线唯一确定这就是 SMO 解析更新公式的几何基础。2.3 SMO与坐标下降法的关系严格来说SMO 可以看作坐标下降法在带等式约束条件下的变体它每次在一个二维坐标平面上做精确的一维搜索但因为约束的存在坐标轴要沿着约束方向“旋转”一下。我当初学的时候有个直观的感受坐标下降法就像你在一个有围栏的山坡上每次都只沿着东西或南北方向走一小步而 SMO 则像是沿着围栏的方向走每次保证不越界而且每步都能直接走到这个方向的局部最低点。所以 SMO 的收敛速度在多数情况下是比普通坐标下降快的。2.4 子问题的几何直觉再往深一层如果 $y_1 y_2$那么等式约束变成 $\alpha_1 \alpha_2 \text{常数}$可行域是一条斜率为 -1 的线段如果 $y_1 \ne y_2$等式约束变成 $\alpha_1 - \alpha_2 \text{常数}$可行域是一条斜率为 1 的线段。上下界 $L$ 和 $H$ 就是这条线段两端被盒子边界截断的位置。理解这个几何关系对后面推导 $L$、$H$ 的公式非常有用。很多教程直接甩两个 case 的公式让人背背完就忘理解了截断原理任何时候都能自己推出来。3. 两个乘子的解析求解从二次函数求导到上下界裁剪3.1 拆出与α1、α2相关的目标函数现在进入 SMO 最核心的数学部分。我们把对偶目标函数 $W(\alpha)$ 中所有含 $\alpha_1$、$\alpha_2$ 的项单独拆出来其余项因为在本次迭代中不变可以直接消去。展开后目标函数是$$ W \alpha_1 \alpha_2 - \frac{1}{2} \alpha_1^2 K_{11} - \frac{1}{2} \alpha_2^2 K_{22} - \alpha_1 \alpha_2 y_1 y_2 K_{12} $$ $$y_1 \alpha_1 \sum_{i\ne 1}\alpha_i y_i K_{i1} - y_2 \alpha_2 \sum_{i\ne 2}\alpha_i y_i K_{i2} \text{常数项} $$其中 $K_{ij} K(x_i, x_j)$常数项包含所有与 $\alpha_1$、$\alpha_2$ 无关的部分。由于 $\alpha_1$ 和 $\alpha_2$ 由等式约束绑定我们只需优化 $\alpha_2$ 一个变量$\alpha_1$ 是它的函数。3.2 用等式约束消掉α1令 $s y_1 y_2$等式约束可以写成$$ \alpha_1 s \alpha_2 \alpha_1^{old} s \alpha_2^{old} \gamma $$于是$$ \alpha_1 \gamma - s \alpha_2 $$把 $\alpha_1$ 代入 $W$目标函数就变成只关于 $\alpha_2$ 的一元二次函数。到这里问题已经转化成了高中生的求抛物线极值问题只是别忘了后面还有 $[0, C]$ 的边界约束。3.3 解析更新公式的推导对一元二次函数求导并令导数为 0可以得到未裁剪的 $\alpha_2$ 新值公式$$ \alpha_2^{new, unc} \alpha_2^{old} \frac{y_2(E_1 - E_2)}{\eta} $$其中 $E_i u_i - y_i$ 是第 $i$ 个样本的预测误差$u_i \sum_j \alpha_j y_j K(x_j, x_i) b$ 是当前模型对第 $i$ 个样本的预测输出$\eta K_{11} K_{22} - 2K_{12}$。很多资料里符号跟我这里不太一样原因在于 $E_i$ 的定义究竟是“预测减真值”还是“真值减预测”。我建议统一用“预测减真值”来记忆推导过程会更顺。如果你看到别处的公式符号相反不要慌检查一下它的 $E$ 定义。$\eta$ 的物理含义是两个样本在核函数映射后的距离平方的一半严谨地说$\eta ||\phi(x_1) - \phi(x_2)||^2$它衡量了两个样本在特征空间中的距离。$\eta$ 越小说明两个样本在特征空间里越接近更新的步长就越大$\eta$ 接近 0 时解析解的分母趋近 0这种情况需要单独处理。3.4 可行域裁剪L和H的计算未裁剪的解不一定落在可行域内必须把它裁剪到线段 $[L, H]$ 上。$L$ 和 $H$ 的计算逻辑如下当 $y_1 y_2$ 时$$ L \max(0, \alpha_1^{old} \alpha_2^{old} - C), \quad H \min(C, \alpha_1^{old} \alpha_2^{old}) $$当 $y_1 \ne y_2$ 时$$ L \max(0, \alpha_2^{old} - \alpha_1^{old}), \quad H \min(C, C \alpha_2^{old} - \alpha_1^{old}) $$裁剪得到$$ \alpha_2^{new} \begin{cases} H, \alpha_2^{new, unc} H \ \alpha_2^{new, unc}, L \le \alpha_2^{new, unc} \le H \ L, \alpha_2^{new, unc} L \end{cases} $$然后由 $\alpha_1 \gamma - s \alpha_2$ 反解出$$ \alpha_1^{new} \alpha_1^{old} s(\alpha_2^{old} - \alpha_2^{new}) $$3.5 更新b和误差的时机$\alpha$ 更新完之后$b$ 也必须同步更新否则模型输出 $u_i$ 仍然是旧的后续迭代误差全都会错。$b$ 的更新依赖当前 $\alpha$ 的变化量和新旧核值$$ b_1 b - E_1 - y_1(\alpha_1^{new} - \alpha_1^{old})K_{11} - y_2(\alpha_2^{new} - \alpha_2^{old})K_{12} $$$$ b_2 b - E_2 - y_1(\alpha_1^{new} - \alpha_1^{old})K_{12} - y_2(\alpha_2^{new} - \alpha_2^{old})K_{22} $$选择规则是若 $0 \alpha_1^{new} C$取 $b b_1$否则若 $0 \alpha_2^{new} C$取 $b b_2$若两者都在边界上取 $b (b_1 b_2) / 2$。这个选择规则并不是拍脑袋定的。当某个 $\alpha$ 严格落在 $(0, C)$ 内部意味着该样本恰好落在间隔边界上它对应的 KKT 条件要求 $y_i u_i 1$因此 $b$ 有唯一确定值如果两个 $\alpha$ 都在边界上说明当前迭代没有提供足够的约束信息$b$ 可能落在一个区间内这时取平均是最稳妥的近似。4. 启发式选择策略决定SMO收敛速度的分水岭4.1 盲目选择第一个乘子的代价理论上只要每轮都找一个违反 KKT 条件的乘子对并做一次解析更新SMO 就一定会收敛。但如果每次随机选收敛速度会慢到让人崩溃。原因在于随机选择的两点可能已经在最优值附近更新幅度极小甚至在边界上连动都不能动白白浪费大量计算。实际工程中选的好不好对迭代次数的影响是数量级的差异——同一个数据集上好的选择策略可能 200 次迭代收敛随机选择可能 5000 次都不一定停。4.2 外循环优先寻找违反KKT条件的样本SMO 的外循环负责选第一个乘子 $\alpha_i$。它的策略是优先遍历那些违反 KKT 条件最严重的样本。具体做法通常分两遍走第一遍遍历所有非边界样本也就是 $0 \alpha_i C$ 的那部分。为什么优先它们因为这些样本恰好落在间隔边界上它们对模型的影响最直接也最容易被 KKT 条件判定为“不合格”。第二遍才遍历全部样本。如果第一遍把所有非边界样本都检查完没有发现需要更新的乘子再对全部样本做一次扫描看有没有漏网之鱼。这种“先边界后全局”的策略可以理解为熟悉的地方先仔细查查不到问题再去整个空间扫一遍。很多实现还会记录一轮迭代中是否有 $\alpha$ 发生变化如果有下一轮继续优先扫描非边界样本直到连续多轮都没有变化才切换到全局扫描。这个细节对收敛速度影响非常大。4.3 内循环基于最大步长原则选择配对乘子外循环选好 $\alpha_i$ 之后内循环负责选第二个乘子 $\alpha_j$。SMO 的目标是找一个和 $\alpha_i$ 配对后能产生最大“步长”的 $\alpha_j$。步长可以近似用 $|E_i - E_j|$ 来度量。直觉上如果两个乘子对应的误差相差很大说明它们对模型当前位置的“不满意程度”差异很大配对更新往往能带来更大的修正。反之如果两个误差几乎一样更新可能非常平缓。实现时需要维护一个误差缓存数组 $E$每次更新后同步刷新涉及到的误差这样内循环可以在 $O(N)$ 时间内扫一遍找到 $|E_i - E_j|$ 最大的 $j$。如果最大步长候选的更新幅度太小SMO 会退而求其次从所有非边界样本中随机挑一个实在找不到能有效更新的配对就结束当前外层循环。4.4 误差缓存和边界样本的偏好误差缓存是 SMO 实现中一个容易被忽略但极其重要的工程细节。因为内循环要用到 $E_i$ 和 $E_j$ 的差如果每次现算 $u_i$复杂度是多了一层 $N$ 次核函数求和内循环整体代价会变成 $O(N^2)$ 量级得不偿失。所以成熟实现一般维护一个 $E$ 数组只有当某个样本的 $\alpha$ 被更新过才重新计算它的误差。上文提到的“先边界后全局”策略在误差缓存层面也有意义非边界样本的 $\alpha$ 频繁变动缓存更新有实际价值边界样本的 $\alpha$ 基本是 0 或 $C$误差不太会变可以少花心思。4.5 迭代之间的收敛加速技巧我见过一个很实用的加速技巧在外循环扫描时记录当前所有非边界样本中“刚刚发生更新”的样本集合下一轮优先处理它们。因为在 SMO 迭代的中后期大部分样本已经满足 KKT 条件反复全量扫描会浪费大量算力只有那些刚被调整过的样本周围才最可能还有不满足条件的邻居。另一个技巧是给内循环设置“最大尝试次数”。如果连续多次挑选出的 $j$ 都无法让 $\alpha_2$ 变化超过阈值就让内循环跳出换一个外层变量。这个上限可以避免死循环也可以理解为一个控制计算预算的开关。5. 容差、C值、收敛判定SMO调参中容易被忽略的细节5.1 KKT条件的数值松弛tol的本质严格来说SMO 的收敛条件是所有样本都满足 KKT 条件。但浮点数计算有误差加上 SVM 的 KKT 条件本身是“硬性”的等式不等式直接零容忍判定会永远无法收敛。所以实现中都会引入一个容差参数 $\text{tol}$。对于一个样本 $i$KKT 条件的数值化判定是$$ y_i u_i 1 - \text{tol} \quad \text{且} \quad \alpha_i C \Rightarrow \text{违反} $$$$ y_i u_i 1 \text{tol} \quad \text{且} \quad \alpha_i 0 \Rightarrow \text{违反} $$$\text{tol}$ 的大小直接影响训练何时停止。太大会导致模型还没解好就停预测性能下降太小会让训练时间暴涨因为最后几个不符合条件的样本往往非常顽固。我在实际项目中常用的值是 0.001有时数据噪声比较大时会放宽到 0.01收敛速度会明显改善分类性能几乎不掉。5.2 C值对α边界分布的影响$C$ 是软间隔中控制“容错程度”的惩罚系数。从 SMO 视角看$C$ 直接决定了 $\alpha$ 的取值边界$\alpha_i$ 被限制在 $[0, C]$ 内。$C$ 越大模型越倾向于让训练样本都满足间隔条件$\alpha$ 更容易顶到上界 $C$出现更多的边界样本$C$ 越小模型越宽容$\alpha$ 更容易落在内部或者保持为 0。这也解释了为什么调参时 $C$ 对模型复杂度影响这么大——它实际上在调节“有多少样本成为支持向量”以及“这些支持向量的影响力上限”。5.3 b的选择策略与多解问题第 3 节提到了 $b$ 的更新规则但它背后还有一个更隐蔽的问题当对偶问题存在多个最优解时$b$ 的取值不是唯一的。具体来说如果所有 $\alpha$ 都等于 $C$ 或都等于 0没有任何一个 $\alpha$ 落在 $(0, C)$ 区间内那么支撑间隔的“自由支持向量”就不存在$b$ 可以在一个范围内浮动。SMO 里用 $(b_1 b_2)/2$ 作为一种折中但严格的数学做法还需要从全部支持向量对应的 KKT 条件反解 $b$取所有解的中间值。在实践里如果发现训练后的模型在验证集上表现不稳定尤其是界附近的样本都差不多时可以先检查一下是不是 $b$ 的选择不够合理。5.4 停止条件的判定策略SMO 的标准停止条件是连续若干轮外循环都没有发生任何 $\alpha$ 更新也就是没有发现违反 KKT 条件的样本就终止迭代。不同实现中也会用“总迭代次数超过上限”作为保险丝。我个人的经验是不要把停止条件完全托付给“全部满足 KKT”。数据规模很大时最后 0.1% 的样本可能反复出现“轻微违反—更新—又轻微违反”的振荡如果坚持零违反会白白多跑很多轮。一个实用的做法是记录上一轮更新的 $\alpha$ 数量和单次更新的最大变化量当最大变化量低于某个阈值比如 $10^{-5}$时就提前停止效果几乎不受影响。5.5 超参数组合在真实数据上的表现下面这张表是我在几个不同类型的模拟数据集上跑简化版 SMO 的粗略感受供参考场景Ctol迭代轮数收敛卡点线性可分数据10.001少无明显卡点高噪声数据0.10.01中噪声样本反复违反 KKT高维稀疏数据100.001中边界样本数量大扫描耗时长近似线性相关数据1000.0001多$\eta$ 接近 0更新幅度小而频繁这些数字不是绝对标准但能说明一个问题SMO 的收敛行为和数据本身的几何结构强相关调整超参数时必须结合数据特点来做而不是机械套固定值。6. 一个简化版SMO的实现从伪代码到Python核心逻辑6.1 数据结构与核矩阵计算的取舍写简化版 SMO 时第一个要决定的是要不要缓存核矩阵。全缓存空间大但每次迭代访问快完全不缓存每次现算内循环扫描 $j$ 的时候复杂度会飙到 $O(N^2)$。折中方案是只缓存当前外层变量和其他样本的核函数值内循环扫描时反复用这一列这样时间可控内存占用也只有 $O(N)$。我下面的示例为了可读性每次直接用核函数现算数据量大时你需要自己加缓存原理是相通的。6.2 主循环的逻辑主循环就是一个“外循环 内循环”的嵌套结构外循环遍历样本挑选违反 KKT 条件的 $\alpha_i$。内循环选择配对 $\alpha_j$计算解析更新。如果发生更新同步更新 $b$ 和误差缓存。连续多轮无更新则停止。下面是核心逻辑的 Python 风格示意重点看结构不要直接拿来跑大规模数据import numpy as np def smo_simple(X, y, C, tol, max_passes, kernel): m X.shape[0] alpha np.zeros(m) b 0.0 passes 0 # 简化版每轮都全量重算误差不额外缓存 E np.array([predict(X, y, alpha, b, kernel, X[i]) - y[i] for i in range(m)]) # 实际建议用增量更新误差缓存 def g(i): return np.sum(alpha * y * kernel(X, X[i])) b # 需要矩阵化加速 while passes max_passes: num_changed 0 for i in range(m): Ei E[i] # 检查第 i 个样本是否违反 KKT if (y[i] * Ei -tol and alpha[i] C) or (y[i] * Ei tol and alpha[i] 0): # 内循环选 |Ei - Ej| 最大的 j diff np.abs(E - Ei) diff[i] -1 j int(np.argmax(diff)) Ej E[j] alpha_i_old, alpha_j_old alpha[i], alpha[j] # 计算 L 和 H if y[i] y[j]: L max(0, alpha[i] alpha[j] - C) H min(C, alpha[i] alpha[j]) else: L max(0, alpha[j] - alpha[i]) H min(C, C alpha[j] - alpha[i]) if L H: continue eta kernel(X[i], X[i]) kernel(X[j], X[j]) - 2 * kernel(X[i], X[j]) if eta 0: continue alpha_j_new alpha[j] y[j] * (Ei - Ej) / eta alpha_j_new min(H, max(L, alpha_j_new)) if abs(alpha_j_new - alpha[j]) 1e-5: continue alpha[i] y[i] * y[j] * (alpha[j] - alpha_j_new) alpha[j] alpha_j_new # 更新 b b1 b - Ei - y[i] * (alpha[i] - alpha_i_old) * kernel(X[i], X[i]) \ - y[j] * (alpha[j] - alpha_j_old) * kernel(X[i], X[j]) b2 b - Ej - y[i] * (alpha[i] - alpha_i_old) * kernel(X[i], X[j]) \ - y[j] * (alpha[j] - alpha_j_old) * kernel(X[j], X[j]) if 0 alpha[i] C: b b1 elif 0 alpha[j] C: b b2 else: b (b1 b2) / 2 # 全量重算误差简化版才这么干 for k in range(m): E[k] g(k) - y[k] num_changed 1 if num_changed 0: passes 1 else: passes 0 return alpha, b这个简化版忽略了核矩阵缓存、内循环最大尝试次数、非边界样本优先扫描等加速手段但逻辑是完整可跑的。6.3 解析解、裁剪与b更新代码里 $\alpha_j^{new}$ 的更新公式、$L$ 和 $H$ 的裁剪、$b_1$、$b_2$ 的更新和前面推导的公式完全对应。写代码时最容易出错的地方是更新 $\alpha_i$ 的公式里符号搞反。这里有一个验证技巧更新完之后检查 $\sum \alpha_i y_i$ 是否保持不变。我在调试时曾经加了一行断言立刻抓到了符号错误。如果你自己写建议保留类似的检查直到代码稳定。6.4 玩具数据上的跑通与验证在二维平面生成两类线性可分的数据用高斯核跑一遍简化版 SMO可以直观看到支持向量浮现出来。收敛后$\alpha$ 中大部分是 0只有少量样本的 $\alpha$ 不为 0这些正好是离分类边界最近的点。用这个结果画分类边界和间隔线你会发现这类可视化往往能帮助理解 SVM 模型的实际行为。很多初学者只看到分类边界忽略间隔线上的支持向量真正画出散点图后才能直观明白“为什么少数样本就决定了整个模型”。7. 我在实际训练SMO时踩过的坑与调试心得7.1 误差缓存不同步导致收敛慢得离谱我第一次按教程写 SMO 时最让人困惑的现象是目标函数明明在下降但收敛轮数比预期多了几十倍。排查了很久才发现问题出在误差缓存没有即时更新。当时我在更新 $\alpha_i$ 和 $\alpha_j$ 后只重算了这两个样本的误差其他样本的误差还留在旧值。SMO 的内循环用 $|E_i - E_j|$ 选配对样本如果其他样本的误差停留在旧状态选择就会被误导老是选中一些已经不需要更新的样本。这类问题比较隐蔽因为代码不会报错只是性能异常。教训是涉及误差缓存时宁可多算一点也要保证一致性。7.2 eta很小的时候如何处理高斯核下如果两个样本完全一样或者高度相似$\eta K_{11} K_{22} - 2K_{12}$ 会趋近于 0此时 $\alpha_2^{new}$ 的更新公式分母接近 0一除就得到极大的值裁剪之后可能变成边界值但这不代表真实最优。我见过直接用 $\eta 0$ 跳过更新的实现这其实不太够。更稳的做法是当 $\eta$ 小于某个阈值比如 $10^{-12}$时直接比较目标函数在 $L$ 和 $H$ 两个端点处的值选一个目标值更大的方向走而不是粗暴跳过。这样既不崩溃也不会丢掉真正有效的更新机会。7.3 停止条件设置过严导致过拟合有一段时间我在一个比较小的模拟数据集上调 SMO把 tol 设成了 $10^{-5}$结果模型在训练集上表现很好验证集上反而变差。原因很简单SMO 在解对偶问题tol 越小解越逼近数学意义上的最优但在数据本身有噪声的情况下这个“最优”反而过度拟合了训练数据。后来我调整了思路把 tol 看作一个正则化尺度的一部分而不是单纯的控制收敛精度。项目里处理噪声较多的数据时用较大的 tol 配合合适的 $C$模型泛化性能通常更稳。7.4 核函数数值范围导致的梯度消失问题这个坑跟 SMO 本身无关但会让一切调试无从下手。如果使用多项式核或者 sigmoid 核特征数值范围差异很大时核矩阵的元素可能膨胀到 $10^{20}$ 或者缩小到 $10^{-20}$随之 $\eta$ 和误差更新都发生数值溢出。处理方式是先对特征做标准化再选核函数。我习惯在进入 SMO 之前先做一次 min-max 标准化核函数里再根据数据规模调一下参数。很多人在这上面花过大量时间结果最后发现是数据预处理的问题。7.5 工程实现上的其他实用建议最后整理一些偏工程向的建议虽然不复杂但每个都能帮你省不少时间核函数计算写成矩阵形式至少在选内循环配对样本时用向量化不然纯 Python 循环会慢到怀疑人生。内循环选择 $j$ 时把当前 $i$ 的误差排除在外否则可能出现自己配自己的情况。记录每次迭代中有多少个 $\alpha$ 发生了更新连续几轮更新数量递减是收敛的正常信号如果某轮突然暴涨常见原因是误差缓存不一致。早停并不是越晚越好。大规模数据上不必等到严格 KKT 全满足用“单次最大变化量低于阈值”来停药训练时间和模型质量都很均衡。我自己的使用习惯是先在一个小的子集上把 SMO 跑通再上全量数据避免把调试时间浪费在等待上。SMO 虽然看起来只是 SVM 的内部组件但它把“大问题拆小”这个思想体现得淋漓尽致很多人看完它之后再去看其他分解类优化方法都会有一种触类旁通的感觉。如果你正准备深入学习支持向量机或者优化算法亲手实现一遍 SMO 绝对是最值回票价的一件事。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表