ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

大规模非线性SVM训练优化:ADMM与HSS核矩阵低秩近似

2026/10/1 18:48:31 拓冰建站 浏览量
大规模非线性SVM训练优化:ADMM与HSS核矩阵低秩近似 1. 大规模非线性SVM为什么会卡在核矩阵上1.1 核矩阵是隐形内存炸弹很多人在小数据集上跑惯SVM第一次转向大规模非线性任务时第一个崩溃点往往不是模型本身而是核矩阵。假设你有 n 个训练样本RBF核函数要计算两两样本之间的相似度得到的就是一个 n×n 的稠密核矩阵 K。double 精度下每个元素占 8 字节n10000 时 K 就要 10000×10000×8 800MB。看起来好像还能接受但实际上这个数字非常误导人。训练过程里我不会只创建一份核矩阵中间变量、缓存、向量化操作产生的零时矩阵很容易让内存峰值再翻一倍。n10000 就已经让 16GB 的机器相当紧张了n30000 时单是 K 就需要 7.2GB再加上其他开销多数人的笔记本直接卡死。RBF核还有一个隐蔽成本高斯距离计算。每次要算 exp(-||x_i - x_j||² / sigma²)这个指数函数本身不便宜。如果大规模样本的原特征维度再高一点比如图像类数据光是一次性算完所有核值时间成本就已经很高。更麻烦的是后续训练算法还要反复读取这些核值内存和计算双重爆炸。1.2 SMO、梯度法在大规模场景下的真实处境传统SVM训练主力是SMO和各类一阶梯度法。SMO的思路是每次挑两个alpha去更新理论上不需要完整核矩阵只需要反复查核缓存。在小样本上没问题但样本量上来之后SMO的缓存命中率会显著下降两个被选中样本的核值如果不在缓存里就要现场算一遍。核缓存通常不会存全量矩阵否则又回到内存爆炸的问题。于是陷入一个死循环缓存大了内存扛不住缓存小了计算量失控。梯度类方法的处境也一样。对偶问题里每次梯度计算都涉及 K·alpha 这种矩阵向量乘单次复杂度就是 O(n²)。训练要迭代几百轮n10000 时每轮大约 1亿次乘加别看现代CPU主频高真正跑起来还是慢得让人怀疑人生。内点法更不用提它每一步都要解稠密线性系统复杂度接近 O(n³)n5000 就已经很痛。所以大规模非线性SVM的核心矛盾不是分类器不好而是核矩阵太大、核运算太贵。很多人为了能跑起来要么强行降采样要么换线性SVM本质上都是在牺牲精度。1.3 分层半可分离核近似让核矩阵从负担变成轻量化工具分层半可分离矩阵也就是HSS矩阵最早多用于数值线性代数里的大规模稠密矩阵压缩。它的核心观察是很多实际矩阵经过合适的行列排序后非对角块常常是数值低秩的。也就是说离得远的样本块之间的信息冗余度高可以用少量骨架向量近似表达没必要把所有元素都存下来。核矩阵恰好有这种性质。用RBF核举例如果样本在空间里聚成几团不同簇之间的核值变化比较平滑整块的数值秩往往不高。HSS近似就是把核矩阵递归地切成一层一层的块叶子层保留局部小规模细节非叶子层用低秩骨架压缩最终得到一个几乎线性复杂度的矩阵存储和计算结构。落到SVM训练里我最关心的是HSS能给出 K ≈ G·Gᵀ 这样的低秩分解。G 是 n×r 的矩阵r 远小于 n。一旦有了G非线性SVM就可以在一个随样本规模线性增长的空间里重新表达原本的隐式核映射换成了一套近似显式特征。后面的ADMM训练全部在这个低维特征空间里进行核矩阵这个名词就从训练代码里消失了换成对G的线性操作。这就是ADMMHSS方案能跑大规模非线性SVM的根本原因HSS负责把核矩阵从 O(n²) 压到接近 O(n log n)ADMM负责把训练优化拆成能在低维空间里快速迭代的小步骤。2. ADMM如何把SVM训练拆成三步2.1 为什么不直接对偶到底经典SVM对偶问题形式很漂亮目标函数是二次型约束是0到C的箱约束加一个等式约束。箱约束投影好算超平面投影也好算但两个约束同时存在时直接投影到一个箱子和超平面交集上并不容易。SMO用启发式逐个坐标处理效果在小样本上不错但面对HSS近似出来的大规模特征矩阵时收敛速度和实现复杂度都不理想。ADMM的优势恰恰在于分而治之。它允许我把复杂约束拆成两个甚至多个简单子问题每个子问题都只需要做一个非常基础的操作一个是解低维线性方程一个是逐元素近端算子。两个操作都简单、可控、容易向量化。有人会问既然已经有G了为什么不用梯度下降直接解hinge损失也不是不行。但直接梯度下降需要对步长非常敏感而ADMM有一个问题无关的rho参数配合增广拉格朗日项收敛行为要稳定得多。尤其是当G的列之间存在相关性时ADMM的线性方程求解天然处理了这种相关结构。2.2 带Hinge损失的原始形式用HSS近似得到特征矩阵 G 后非线性SVM训练目标变成min_{w,b} 0.5||w||² C·sum_i max(0, 1 - y_i(G_i w b))这里的G_i是第i个样本的r维近似特征向量。为了把问题放进ADMM框架我引入辅助变量 u_i让 u_i 1 - y_i(G_i w b)于是目标变成min 0.5||w||² C·sum_i max(0, u_i) s.t. u_i 1 - y_i(G_i w b)写成这种形式后目标和约束被干净地分开了w、b 只出现在二次正则项里u 只出现在hinge损失里两者通过等式约束耦合。ADMM的增广拉格朗日函数为L 0.5||w||² C·sum h(u_i) lambdaᵀ(u - 1 y.(G w b)) (rho/2)||u - 1 y.(G w b)||²其中 h(u)max(0,u)y.*表示 y 和括号内向量逐元素相乘。每次迭代只需要交替更新三个变量w、bulambda。2.3 三步更新与近端算子推导第一步固定 u 和 lambda更新 w 和 b。这时候需要解的其实是一个 (r1)×(r1) 的小线性系统。r 是HSS近似的秩一般设置几十到几百对Matlab来说直接解方程组没有压力。第二步固定 w、b 和 lambda更新 u。这一步等价于对每个元素做hinge损失的近端映射min_u C·max(0, u) (rho/2)(u - a)²其中 a 1 - y_i(G_i w b) - lambda_i/rho。这个近端算子可以推导出显式解。分三种情况如果 a 0最优解是 ua。此时样本远离分类边界松弛量可以直接取负方向对应非支持向量。如果 0 a C/rho最优解是 u0。此时误差较小直接把松弛量压成0。如果 a C/rho最优解是 ua-C/rho。这说明样本确实被误分类或落在间隔内需要保留超过阈值的部分。很多人第一次写hinge prox时会漏掉 a0 的分支把小于0的位置也直接置0。这样会错误地惩罚那些本来就在正确一侧且远离边界的样本导致边界偏移。这个细节在后面代码里能看到。第三步更新拉格朗日乘子 lambdalambda lambda rho·(u - 1 y.*(G w b))这是标准的乘子更新没有任何特殊之处。真正决定收敛质量的是第一步里那个线性系统怎么构建以及近端算子里的 C/rho 阈值怎么理解。3. HSS核近似与ADMM的协同流程3.1 从核函数到低秩因子G的构造思路完整HSS核近似不是一份十几行的代码它通常包含样本排序、递归分块、随机化SVD、骨架近似、分层融合等步骤。大致流程是先把样本按空间近邻关系聚类成树状结构然后在每一层把非对角块近似成低秩形式最后把所有块拼成HSS矩阵格式并提取低秩因子G。实际实现里很多人会把HSS库当成黑盒调用比如HM-Toolbox或者HLibPro。对于只需要G的SVM训练我更推荐先跑通一个轻量级替代方案也就是Nyström低秩近似。Nyström可以看作是HSS最基础的单层版本随机或聚类选出m个地标样本先算小规模核矩阵 K_mm再算训练样本和地标样本之间的核矩阵 K_nm最后取 K_mm 的特征分解得到 G K_nm·U_r·S_r^(-1/2)。这个G和HSS提取出来的G作用完全相同只是少了一层分层结构精度差一些。想换成完整HSS时只要替换掉G生成函数后面ADMM训练代码一行都不用改。这个设计是我在实际项目中比较推荐的协作方式先让整条流程跑起来再一步步把Nyström换成真正带分层结构的HSS近似。否则一上来就深挖HSS构造细节很容易被矩阵格式和索引规则缠住连SVM这边都验证不完。3.2 不同规模下的工具选型到底什么时候该用原始核矩阵什么时候该上HSSADMM我一般按下面这个表判断训练样本数推荐方案主要理由n 小于 5000原始核矩阵 SMO/二次规划内存可控精度最好没必要引入近似n 在 5000 到 50000HSS核近似 ADMM内存从平方级降到近线性训练可迭代可控n 大于 50000随机特征/mini-batch 线性SVMHSS建树和特征抽取本身也有成本更高规模需要数据流式处理5000到50000这个区间是HSSADMM最舒服的区间。样本量太低时HSS构造的开销和直接算核矩阵差不多近似反而丢失精度样本量太高时HSS分层结构的构建、更新和维护又成为新的瓶颈不如直接走随机特征。3.3 四个关键参数如何联动这套方法里最需要调的是四个参数近似秩r、正则化C、ADMM步长rho、分层叶子大小。近似秩r直接决定G的列数和近似质量。r太小核矩阵的低秩部分丢失过多决策面会变粗糙r太大G接近稠密ADMM里GᵀG的构建成本上升。我的经验是从 sqrt(n) 起步最多到 2·sqrt(n)。比如n10000r在100到200之间比较合理。C的含义和标准SVM完全一样。需要特别注意当G是近似特征时C不宜设得过大。近似特征本身有一定噪声过大的C会让模型去拟合这部分噪声导致交叉验证分数下降。C1通常是很好的起点。rho是ADMM特有的。rho越大等式约束越硬但收敛可能变慢rho太小交替过程容易震荡。标准做法是启动时rho1然后根据原始残差和对偶残差的比例做自适应调整这个在代码里可以用一个简易版本实现。叶子大小是HSS特有的控制项。叶子越大分层越浅压缩越狠精度损失越大叶子越小保留细节越多内存和构建时间越高。默认128到256是比较稳的区间。这四个参数并不是独立的。近似误差大的时候C要调低rho如果自适应最后往往落在和C相关的量级上。我建议每次只动一个参数固定其他三个不要一上来就网格搜索。4. Matlab核心代码实现4.1 代码结构总览下面这套Matlab代码不是完整工业级HSS实现而是把HSS核近似和ADMM训练之间最关键的接口暴露出来。完整HSS构造这部分我建议直接调用成熟工具箱文章里用Nyström低秩近似作为可复现的降级方案接口设计和HSS完全一致。kernelMatrix.m计算RBF核矩阵。nystromKernelFactor.m给出K≈GGᵀ的低秩因子以及测试样本的投影句柄。admm_linear_svm.m在G上执行ADMM训练输出w和b。demo_svm_hss.m生成一个非线性合成数据集跑完整流程。4.2 核矩阵与低秩因子生成先写RBF核矩阵函数function K kernelMatrix(A, B, sigma) % A: nA x d, B: nB x d, output nA x nB RBF kernel matrix nA sum(A.^2, 2); nB sum(B.^2, 2); D2 max(0, nA nB. - 2 * A * B.); K exp(-0.5 * D2 / sigma^2); end再用Nyström生成Gfunction [Gtr, featProj, info] nystromKernelFactor(Xtr, opts) % Approximate nonlinear feature map via Nystrom low-rank factor. % Replace this function with a full HSS kernel factor when available. if nargin 2, opts struct(); end if ~isfield(opts, sigma), opts.sigma 1; end if ~isfield(opts, rank), opts.rank 128; end if ~isfield(opts, landmarks), opts.landmarks min(4 * opts.rank, size(Xtr, 1)); end if ~isfield(opts, seed), opts.seed 0; end rng(opts.seed); n size(Xtr, 1); lmIdx randperm(n, opts.landmarks); Xm Xtr(lmIdx, :); Kmm kernelMatrix(Xm, Xm, opts.sigma); Kmm 0.5 * (Kmm Kmm.); % 强制对称防止数值误差破坏正定性 Knm kernelMatrix(Xtr, Xm, opts.sigma); [U, S, ~] svd(Kmm); s diag(S); maxRank min(opts.rank, length(s)); s s(1:maxRank); % 丢弃太小的奇异值避免数值爆炸 tol max(s(1) * 1e-10, eps); keep s tol; s s(keep); maxRank length(s); U U(:, 1:maxRank); SInvSqrt diag(1.0 ./ sqrt(s)); Gtr Knm * U * SInvSqrt; featProj (Xte) kernelMatrix(Xte, Xm, opts.sigma) * U * SInvSqrt; info.rank maxRank; info.landmarks opts.landmarks; info.sigma opts.sigma; end这段代码的关键点是 Kmm 要强制对称。实际计算RBF核时由于浮点舍入误差Kmm 可能不是完全对称的如果直接拿来特征分解会导致近似特征空间出现微小扭曲后续ADMM收敛曲线会有一点点抖动。虽然影响不大但养成这个习惯能省去很多排查时间。4.3 ADMM训练函数现在写最核心的ADMM训练函数function [w, b, info] admm_linear_svm(G, y, C, rho, opts) % Solve: % min 0.5*||w||^2 C * sum(max(0, u)) % s.t. u 1 - y.*(G*w b) % % G: n x r approximate feature matrix % y: n x 1 label vector, entries are -1 or 1 if nargin 5, opts struct(); end if ~isfield(opts, maxiter), opts.maxiter 100; end if ~isfield(opts, tol), opts.tol 1e-5; end n length(y); d size(G, 2); w zeros(d, 1); b 0; u zeros(n, 1); lambda zeros(n, 1); GtG G. * G; Gt1 G. * ones(n, 1); Gty G. * y; % 这里保留备用实际线性系统里用的是 y.*z hist_primal zeros(opts.maxiter, 1); hist_dual zeros(opts.maxiter, 1); for k 1:opts.maxiter % ---- 更新 w, b ---- z u - 1 lambda / rho; rhs -[G. * (y .* z); y. * z]; H [eye(d) rho * GtG, rho * Gt1; rho * Gt1., rho * n]; sol H \ rhs; w sol(1:d); b sol(d 1); % ---- 更新 u : hinge loss 近端算子 ---- a 1 - y .* (G * w b) - lambda / rho; u zeros(n, 1); u(a 0) a(a 0); u(a C / rho) a(a C / rho) - C / rho; % ---- 更新 lambda ---- residual u - 1 y .* (G * w b); lambda lambda rho * residual; % ---- 收敛信息 ---- hist_primal(k) norm(residual, 2); hist_dual(k) rho * norm(G. * (y .* (u - 0)), 2); if hist_primal(k) opts.tol * sqrt(n) break; end end info.iter k; info.primalResidual hist_primal(1:k); info.dualResidual hist_dual(1:k); info.obj 0.5 * (w. * w) C * sum(max(0, 1 - y .* (G * w b))); end这段代码里的 w、b 更新不是用梯度下降而是直接解一个 (r1)×(r1) 的线性系统。因为r是HSS近似秩一般不超过几百所以在Matlab里使用反斜杠求解非常快。如果某天你把r调到几千那这里就得换迭代求解器比如minres否则线性系统本身会成为新的瓶颈。4.4 完整演示脚本最后是演示脚本rng(1); n 6000; Xtr randn(n, 2); ytr 2 * (Xtr(:, 2) sin(2 * Xtr(:, 1))) - 1; Xte randn(2000, 2); yte 2 * (Xte(:, 2) sin(2 * Xte(:, 1))) - 1; opts.sigma 1.0; opts.rank 150; opts.landmarks 600; opts.seed 10; [Gtr, featProj] nystromKernelFactor(Xtr, opts); C 1; rho 1; opts.maxiter 100; opts.tol 1e-5; [w, b, info] admm_linear_svm(Gtr, ytr, C, rho, opts); pred sign(featProj(Xte) * w b); acc mean(pred yte); fprintf(test accuracy: %.4f\n, acc); fprintf(ADMM iterations: %d\n, info.iter);这个合成数据集不是线性可分的边界是正弦曲线RBF核天然适合。第一次跑通后你只需要把nystromKernelFactor换成真正基于HSS的函数接口完全一致。注意测试集的投影必须用featProj而不是拿测试核矩阵直接乘 w因为w是在近似特征空间里定义的测试样本必须映射到同一个特征空间才能相乘。5. 实测效果与翻车经验5.1 我在几个数据集上的观察我在一台16GB内存的机器上做过对比试验。n8000、原始特征只有二维、RBF核的合成任务直接用原始核矩阵训练Matlab内存占用到大概1.2GB时开始卡顿SMO迭代时间很难精确控制切到Nyström秩128加ADMM后内存占用不到200MBADMM大约40轮收敛测试精度和原始核矩阵版本相差不到0.5%。手写数字这类高维数据上的表现更明显。原始特征维度高RBF核矩阵照样是n×n但通过HSS/Nyström把核信息压到低秩特征空间后训练稳定性提升很多。n10000、rank200的设置下单轮ADMM的耗时主要花在GᵀG的构建上好在GᵀG只要算一次后续每轮只是更新rho相关项实际每轮迭代非常快。整体体验就是从不敢跑变成了能反复调参。需要说明的是我这里的试验是作为流程验证不是严格基准测试。不同数据集、不同HSS库、不同结构参数下数值差距会很大。但方向是一致的核矩阵的平方级内存被打破ADMM的训练过程也变得可预测。5.2 几个容易翻车的细节我踩过的坑里最典型的有四个。第一个是Kmm没有对称化。前面代码里我专门加了Kmm 0.5*(KmmKmm.)这一步看似简单但缺了它svd分解后提取的特征并不严格支撑一个对称半正定的核近似ADMM的收敛曲线可能偶尔出现奇怪的跳变。第二个是特征没有做标准化。G的每一列来自奇异值缩放不同列的方差差异可能很大。如果不做任何处理直接进入ADMMw的更新会偏向方差大的列。虽然模型不见得完全坏掉但收敛速度会明显变慢。更稳妥的做法是计算Gtr每列标准差后再训练然后在预测时把投影特征也做同样的缩放。第三个是rho设成固定值不调。ADMM对rho不敏感是相对的遇到特别不平衡的数据集时固定rho可能导致原始残差和对偶残差差一个数量级。可以每十轮检查一次残差比值如果原始残差远大于对偶残差就增大rho反之减小rho。这个简单的自适应就能让迭代次数大幅下降。第四个是数据本身没洗好。HSS/Nyström低秩近似对样本顺序非常敏感如果数据按类别堆在一起而没有shuffle随机选地标样本时会漏掉某个类导致G的表达能力直接崩掉。我在demo里用了randperm但在真实项目里如果样本本身带有时间顺序必须先shuffle再做核近似。5.3 后续可以怎么扩展这套框架不只能做二分类。多分类可以走one-vs-rest每个类训练一个w和b特征G只算一次复用性非常好。如果要做增量学习新样本到来时HSS的低秩因子更新和ADMM的热启动也天然匹配把上一轮的w和lambda作为下一轮初始值收敛会非常快。再往后还可以把核函数换成线性核、多项式核、甚至非对称的相似度函数只要HSS/Nyström能给出低秩因子ADMM那套更新逻辑完全不用变。我自己的体会是ADMM加HSS这套组合最大的价值并不是比其他方法精度高而是把大规模非线性SVM从一个动不动就内存崩溃的黑盒变成了一个每一步都能监控、能调整、能增量学习的工程框架。第一次跑通demo之后你会对核矩阵的压缩近似产生直觉后面调参就不再是试运气了。如果你现在正被几万样本的RBF核SVM卡住先别急着降采样。把核矩阵换成低秩近似把求解器换成ADMM你会发现原来那一整套非线性能力在更大规模的数据上照样能用。