☰
ADMM+HSS:大规模高斯核SVM训练的内存与时间优化方案
2026/10/1 17:33:29 网站建设 项目流程

上周我把一个两万样本的高斯核SVM训练脚本丢上服务器,跑了一个半小时之后内存崩了。报错信息很直白:Out of memory。这其实不是我第一次在大规模非线性SVM上栽跟头——核矩阵是N乘N的稠密矩阵,样本量过万之后,内存和求解时间都以平方级别上涨,SMO这类经典算法也救不了场。后来我把训练流程整体换成了“交替方向乘子法(ADMM) + 分层半可分离核近似(HSS)”,同样两万样本,内存占用从3GB左右压到几百MB,训练时间也从“小时级”降到“分钟级”,而且分类精度几乎没有损失。

这篇文章就把这套方案的完整思路、公式推导、Matlab实现和调参经验一次讲清楚。内容面向两类读者:一类是做预测模型、手头数据量大且特征非线性明显的工程师和研究生;另一类是已经会用SVM但被核矩阵内存和训练时间折磨过的人。我会尽量把每一步的“为什么这么做”讲透,而不是只丢一份能跑的代码。

1. 问题的本质:核矩阵一满,内存和求解一起爆

1.1 核矩阵的存储账本:从1万样本算起

非线性SVM的核心操作是把样本映射到高维特征空间,再通过核函数隐式计算样本点之间的内积。训练过程中,所有两两样本的核函数值构成了核矩阵K,K_ij = k(x_i, x_j)。这个矩阵的大小是N×N,而且在高斯核这类常用核函数下是稠密的——几乎没有零元素可以利用。

用double类型存储的话,一个N×N的核矩阵占用内存是N²×8字节。我算了一笔账,贴出来大家感受一下:

样本数N核矩阵原始内存备注
5,000200 MB勉强能塞进内存,但求解很慢
10,000800 MB单机开始吃力,SMO缓存命中率骤降
50,00020 GB常规工作站直接爆内存
100,00080 GB必须上集群,且求解时间不可接受

这还只是存储。训练过程中每次计算梯度、做线性搜索或者求解子问题,都可能反复访问这整个矩阵。数据一旦无法全部驻留内存,就得频繁换入换出,训练速度会断崖式下跌。我那次两万样本的实验就是这么崩的,不是算法不够好,是存储模型在规模面前根本撑不住。

1.2 SMO不是治本方案:为什么大规模下会卡住

很多读者接触SVM的第一课就是从SMO(序列最小优化)入手的。SMO每次只优化两个拉格朗日乘子,内层循环不需要完整的矩阵运算,看起来很适合大规模场景。确实,中小规模下(几千到一两万样本)SMO是主力解法,libsvm这种成熟库用得也很顺手。

但大规模下SMO有两个绕不开的麻烦:

第一,它需要反复选取违反KKT条件最严重的工作集,这个启发式选择过程要求频繁访问核矩阵的行。核矩阵不能整体驻留内存时,每取一行都是一次代价极高的IO操作。缓存策略可以缓解,但样本量越大,缓存命中率越低,最终训练过程的大部分时间其实花在了“取数据”而不是“算优化”上。

第二,SMO的收敛速度在边界复杂时会明显退化。非线性核的决策边界在原始空间通常很曲折,KKT条件被违反的位置分散在整个训练集里,工作集的更新路径很长,迭代次数会随着数据复杂度上升而不是样本量线性上升。

所以当样本量到几万甚至十几万这个量级,继续死磕SMO路线会非常痛苦。正确的思路不是优化SMO的缓存,而是换一种能把问题拆开、并且允许核矩阵以近似压缩形式存在的整体框架。

1.3 破局点:把训练拆成“可以快速执行的算子”

大规模非线性SVM真正耗时的操作掏空来看就两个:

  • 求解一个包含核矩阵的线性系统(或二次规划子问题);
  • 反复计算核矩阵与向量的乘法。

如果能同时解决这两个操作在大规模下的复杂度问题,整个训练过程就活了。我采用的方案也因此有两个支柱:

  • 用ADMM把原二次规划拆成“线性系统求解 + 简单投影”两个子问题,让算法主体每次迭代只面对一个线性系统;
  • 用HSS(分层半可分离矩阵)结构对核矩阵做近似压缩,把矩阵-向量乘法的复杂度从O(N²)降到接近O(N log N),存储量也从O(N²)降到O(N log N)。

这两个思路单独拿出来都是成熟技术,但组合在一起解决大规模核SVM训练,效果比想象中好很多。下面分别拆开讲。

2. ADMM把大问题拆成两块:一次投影,一次线性系统求解

2.1 对偶问题改写为ADMM标准形式

ADMM的思想很直接:如果一个优化问题里有多个约束耦合在一起不好处理,就引入辅助变量把约束“拆开”,再通过交替更新、对偶变量回推的方式逼近最优解。它要求目标函数能被拆成两部分,经典形式是:

min f(α) + g(z),subject to α - z = 0

对应到SVM,我采用经典L1-SVM的对偶问题:

max Σᵢ αᵢ - 1/2 Σᵢⱼ αᵢ αⱼ yᵢ yⱼ K(xᵢ, xⱼ) s.t. Σᵢ αᵢ yᵢ = 0,0 ≤ αᵢ ≤ C

写成便于ADMM的最小化形式:

min 1/2 αᵀ Q α - 1ᵀ α + g(z) s.t. α - z = 0

其中Q是一个N×N矩阵,第(i,j)个元素是Q_ij = yᵢ yⱼ K(xᵢ, xⱼ)。g(z)是示性函数,它把z约束在可行域内,也就是:

g(z) = 0,如果 z∈{0≤z≤C,yᵀz=0};否则 g(z) = +∞

这个拆法的巧妙之处在于:g(z)定义的是一个简单凸集(一个盒子加一个线性等式),投影计算非常便宜;而核矩阵的所有复杂性都被关进了f(α)里的Q,也就是那个线性系统。

2.2 三步迭代的推导与Matlab直觉

ADMM的迭代步骤是固定的三段式。第k轮对α、z和对偶变量u依次更新。

第一段,求解α。固定z和u,目标函数对α求导并令导数为0:

(Q + ρI) α = 1 + ρ(z - u)

这是一个N阶线性系统。没有HSS等加速手段时,这一步是全算法最贵的地方——每次迭代都要解一个稠密N×N线性系统,普通Cholesky分解的复杂度是O(N³),一万样本基本不可能。

第二段,更新z。固定α和u,等价于把临时变量α+u投影到可行集上:

z = clip(α + u, 0, C),再投影到 yᵀz = 0 的超平面

投影到超平面只需要一步线性修正:y是标签向量,满足yᵀz=0的正交投影是z = z - (yᵀz)/(yᵀy) × y。因为SVM的标签是±1,所以yᵀy = N。

第三段,更新对偶变量u,非常简单:

u = u + α - z

这三步里面,除了解线性系统,其他操作都是O(N)或O(N log N)级别的,完全可以忽略不计。所以整个算法的性能瓶颈完全集中在第一段的线性系统求解上。

2.3 关键瓶颈:每轮都要解一个稠密线性系统

如果把ADMM这个框架比作一个工厂流水线,那么α更新就是整条线上最笨重的一台机器。其他工序都快到毫秒级,只有这台机器每次都要进行一场大规模稠密矩阵运算。

在没有HSS参与的情况下,这一步的复杂度是:

  • 直接法(Cholesky分解):O(N³)计算量,O(N²)内存,N=1万基本告别;
  • 迭代法(共轭梯度CG):每次迭代O(N²),迭代次数看矩阵条件数,内存还是要O(N²)存Q。

注意到Q = diag(y) × K × diag(y),K本身是稠密核矩阵。如果我们能对Q做一次“压缩”,让它能以近似结构存储,并且支持快速矩阵乘法和快速线性系统求解,那整个ADMM循环就能跑起来了。

这正是HSS要解决的问题。

3. HSS近似:一棵二叉树把核矩阵压缩到接近线性复杂度

3.1 分层低秩表示是什么

HSS(Hierarchically Semiseparable,分层半可分离)矩阵是一种具有特殊层次结构的矩阵格式。它的核心思想可以用一句话概括:把一个大矩阵按二叉树递归划分,叶子节点保存稠密小矩阵块,内部节点保存连接不同子块的低秩耦合信息。

以核矩阵Q为例。Q是N×N的矩阵,HSS把样本索引递归二分。递归到最底层时,每一块样本内部的所有核函数值组成一个稠密小矩阵,直接存原始的Q块;而任意两块样本之间的交叉核矩阵,几乎都可以用一个低秩分解来近似,存下来只需要U和V两个瘦长矩阵,U的大小是n₁×r,V的大小是n₂×r,r是低秩的秩。

画个简化示意图帮助理解。一个内部节点对应两半数据I₁和I₂:

  • Q(I₁, I₁):递归交给左子节点处理;
  • Q(I₂, I₂):递归交给右子节点处理;
  • Q(I₁, I₂) ≈ U₁ × V₂ᵀ,Q(I₂, I₁) ≈ U₂ × V₁ᵀ,用低秩矩阵近似。

我这里给的教学实现是“分块低秩”的简化HSS:离对角块直接低秩存储,没有做严格HSS理论里的转移算子细化。工程实践中,这种简化版和严格版的复杂度量级一致,而且更容易在Matlab里落地,性能已经足够说明问题。

3.2 高斯核矩阵为什么可以被HSS压缩

很多人听到“核矩阵可以压缩”第一反应是不信:核矩阵不是满秩的吗?高斯核矩阵确实是数值满秩的,但满秩不代表不可压缩,关键是看它的“数值秩”。

高斯核函数k(xᵢ, xⱼ) = exp(-‖xᵢ-xⱼ‖²/(2σ²))是光滑且随距离快速衰减的。这意味着当两个样本索引分处不同数据块时,它们之间的相互作用可以通过少数几个“代表点”来插值逼近。物理直觉是这样的:远处看一块样本对另一块样本的作用,不需要知道块内每个点的细节,只需要块内几个关键方向的平均效应就够了。

这种“远距离低秩、近距离稠密”的结构,正是分层低秩格式最擅长处理的类型。离得远的块用低秩近似,离得近的块递归细分直到叶子直接存储原始稠密块。高斯核的衰减越快、数据维度越低、σ与数据尺度越匹配,数值秩越小,压缩率越高。

3.3 HSS下的存储与矩阵向量乘:复杂度账本

HSS结构带来的收益直接反映在两项核心操作的复杂度上:

操作普通稠密矩阵HSS近似结构
存储量O(N²)O(r N log N)
矩阵向量乘O(N²)O(r N log N)
线性系统求解O(N³)/迭代CG迭代,每轮O(r N log N)

r是低秩近似的秩,一般取16到64就够用了;log N来自二叉树的层数。对比普通稠密存储,HSS把存储和计算量从平方级降到了接近线性。

我举个例子:N=5万样本的高斯核矩阵,double类型原始需要20GB。如果HSS的rank取16,每层低秩块存储量为r×块长量级,整体下来通常只要几百MB。这个数量级差异,直接决定了训练脚本是“能跑”还是“内存崩溃”。

构造HSS结构的时候还需要注意一点:不要直接在内存里先生成完整核矩阵再压缩,那就违背初心了。正确做法是递归分块后按需取子块,只对叶子块和其他块的低秩采样列做核函数计算。这一点在代码部分我会专门处理好。

4. 算法合体:ADMM迭代+HSS线性求解的完整流水线

4.1 训练流程总览

将ADMM和HSS合体之后,完整训练流程变得很清晰。第一步,用HSS递归构造Q的近似结构,这一步只需要O(N log N)量级的存储和构建开销。第二步,进入ADMM循环,循环内每轮做一次α更新、一次z更新、一次u更新。其中α更新调用CG迭代求解线性系统,而CG每次迭代所需的核心操作——矩阵向量乘Q×v——由HSS结构快速完成。

伪代码如下:

构造 HSS(Q) 初始化 α=0, z=0, u=0 循环直到收敛(最大迭代次数内): rhs = 1 + ρ(z - u) 用CG求解 (HSS(Q) + ρI) α_new = rhs z = 投影(α_new + u, [0,C], yᵀz=0) u = u + α_new - z 检查残差 ||α_new - z|| 和 ||α_new - α|| 输出 α,计算偏置b

注意线性系统里的ρI是一个对角矩阵,在CG矩阵向量乘里只是额外加一项ρ×v,代价O(N),不影响整体复杂度。

4.2 复杂度账本与内存占用

整个训练过程的总复杂度需要把两层迭代都算进去。外层ADMM迭代次数记为T_admm,内层CG迭代次数记为T_cg,每次CG迭代包含一次HSS矩阵向量乘,代价是O(r N log N)。所以总复杂度大约为:

O(T_admm × T_cg × r N log N)

在我的实验里,T_admm通常取100到300轮,T_cg在正常情况下是10到30次。N=5万、r=16时,整体计算量大概在10^10 flops量级,这在现代CPU上是分钟级能完成的事。

内存方面,主要开销有三块:HSS结构本身、CG中间向量、支持和若干小矩阵。全部加起来和原始核矩阵的O(N²)完全不在一个量级。这也就是为什么这套方法能轻松处理原生SVM几乎无法跑动的数据规模。

4.3 收敛性判断与停止准则

ADMM的收敛判断不能只看对偶残差,我习惯同时盯两三个指标:

  • primal残差:‖α - z‖,这个反映α和投影后的z之间的差距,是ADMM的核心收敛信号;
  • 对偶残差:ρ‖z - z_old‖,反映对偶变量是否还在大幅波动;
  • 目标函数值:1/2 αᵀQα - 1ᵀα,有时候前两个指标都小了但目标值在抖动,那就可能是z投影的精度问题。

收敛阈值通常在1e-4到1e-6之间。注意,这个阈值要和ρ的大小联动调整,ρ越大,primal残差越容易被压小,但此时对偶残差可能还在高位。我通常三个指标一起打印,盯着看几轮就心里有数了。

5. Matlab实现:核心代码与关键细节

下面放可运行的Matlab实现。为了让大家直接上手,我给了一套完整的主脚本和核心函数,并在关键位置加了注释。代码里的HSS部分我做了适度简化,用“分块低秩递归”结构代替完整HSS理论实现,复杂度量级一致,重点是展示HSS和ADMM如何协同工作。

5.1 主脚本与参数配置

% demo_svm_hss.m % 基于 ADMM + HSS 的大规模非线性SVM训练示例 clear; clc; rng(42); % 参数配置 params.C = 1.0; % SVM 正则化参数 params.rho = 1.0; % ADMM 惩罚参数 params.sigma = 0.8; % 高斯核带宽 params.leaf_size = 64; % HSS 叶子块大小 params.rank = 16; % 离对角低秩近似的秩 params.max_iter = 200; % ADMM 最大迭代次数 params.cg_tol = 1e-6; % CG 容差 params.cg_maxit = 50; % CG 最大迭代次数 % 生成双月数据(非线性可分数据集) [X, y] = make_twomoon(1200); y(y == 0) = -1; % 确保标签是 +1 / -1 % 训练测试划分 idx = randperm(size(X, 1)); trN = round(0.7 * numel(idx)); Xtr = X(idx(1:trN), :); ytr = y(idx(1:trN)); Xte = X(idx(trN+1:end), :); yte = y(idx(trN+1:end)); % 训练 tic; model = svm_hss_train(Xtr, ytr, params); train_time = toc; fprintf('训练时间: %.2f 秒\n', train_time); % 预测与评估 [pred] = svm_hss_predict(model, Xtr, ytr, Xte); acc = mean(pred(:) == yte(:)); fprintf('测试精度: %.4f\n', acc); fprintf('支持向量数: %d\n', numel(model.sv_idx));

数据生成函数很简单,用两个不同半径的同心圆簇构造一个非线性可分问题:

function [X, y] = make_twomoon(N) N1 = floor(N / 2); N2 = N - N1; theta = linspace(0, 2*pi, N1)'; X1 = [0.9 * cos(theta), 0.9 * sin(theta)] + 0.06 * randn(N1, 2); X2 = [1.6 * cos(theta), 1.6 * sin(theta)] + 0.06 * randn(N2, 2); X = [X1; X2]; y = [ones(N1, 1); -ones(N2, 1)]; end

5.2 HSS核矩阵构建与快速乘

核矩阵的计算函数,我用平方距离展开式计算,避免调用pdist2,这样不依赖任何统计工具箱:

function K = kernel_matrix(X, idx1, idx2, params) if isempty(idx1) || isempty(idx2) K = zeros(numel(idx1), numel(idx2)); return; end X1 = X(idx1, :); X2 = X(idx2, :); D2 = sum(X1.^2, 2) - 2 * (X1 * X2') + sum(X2.^2, 2)'; K = exp(-D2 / (2 * params.sigma^2)); end

HSS结构构建函数。这里递归把样本索引一分为二,对角块递归建树,离对角块用Nyström列采样做低秩分解。为了保证代码可读,我在叶子存稠密块,内部节点只存低秩基U和V:

function node = hss_kernel_approx(X, y, idx, params) n = numel(idx); if n <= params.leaf_size % 叶子:直接存储带符号的稠密核矩阵块 Q_leaf = diag(y)*K*diag(y) K = kernel_matrix(X, idx, idx, params); node.is_leaf = true; node.idx = idx; node.D = bsxfun(@times, y(idx), bsxfun(@times, y(idx)', K)); return; end % 内部节点:递归左右子树 mid = floor(n / 2); idx1 = idx(1:mid); idx2 = idx(mid+1:end); node.is_leaf = false; node.left = hss_kernel_approx(X, y, idx1, params); node.right = hss_kernel_approx(X, y, idx2, params); % 离对角块低秩近似:Q(I1,I2) ≈ U1 * V2' s = min(params.rank, numel(idx2)); Jc = idx2(round(linspace(1, numel(idx2), s))); % 均匀采样列 W1 = kernel_matrix(X, idx1, Jc, params); % |idx1| x s A = kernel_matrix(X, Jc, Jc, params); % s x s A = (A + A') / 2 + 1e-8 * eye(s); % 对称化并加正则 R = chol(A, 'lower'); % Cholesky U1 = bsxfun(@times, y(idx1), W1 / R'); % 带标签符号 V2 = bsxfun(@times, y(idx2), kernel_matrix(X, idx2, Jc, params) / R'); node.left.U = U1; node.left.V = V2; % 利用对称性:Q(I2,I1) = Q(I1,I2)',所以右节点的低秩基可以直接复用 node.right.U = V2; node.right.V = U1; end

有了HSS结构,矩阵向量乘可以从树根递归实现。叶子直接做稠密块乘法,内部节点做两三个低秩乘法相加:

function qx = hss_q_matvec(node, x) if node.is_leaf qx = node.D * x(node.idx); return; end idx1 = node.left.idx; idx2 = node.right.idx; qx = zeros(size(x)); % Q11*x1 + Q12*x2 qx(idx1) = hss_q_matvec(node.left, x) + node.left.U * (node.left.V' * x(idx2)); % Q21*x1 + Q22*x2 qx(idx2) = hss_q_matvec(node.right, x) + node.right.U * (node.right.V' * x(idx1)); end

这个递归实现是教学向的,每个递归层都分配一次临时向量,胜在逻辑清晰;如果做大工程,可以改成传入偏移量和输出向量的in-place版本,性能还能上一截。

5.3 ADMM训练与预测函数

训练函数里,ADMM的循环结构非常简洁。α更新用Matlab内置的pcg共轭梯度求解器,矩阵向量乘交给匿名函数调用HSS结构:

function model = svm_hss_train(X, y, params) N = numel(y); idx = (1:N)'; % 构造 HSS(Q) hss = hss_kernel_approx(X, y, idx, params); alpha = zeros(N, 1); z = zeros(N, 1); u = zeros(N, 1); for iter = 1:params.max_iter % 第一步:用CG求解 (Q + rho*I) * alpha_new = 1 + rho*(z - u) rhs = 1 + params.rho * (z - u); Afun = @(v) hss_q_matvec(hss, v) + params.rho * v; [alpha_new, cg_flag] = pcg(Afun, rhs, params.cg_tol, params.cg_maxit); % 第二步:z 投影到 [0,C] 且 y'*z = 0 z = alpha_new + u; z = min(max(z, 0), params.C); % 盒子投影 z = z - ((y' * z) / (y' * y)) * y; % 等式超平面投影 z = min(max(z, 0), params.C); % 再裁剪一次修正边界 % 第三步:更新对偶变量 u u = u + alpha_new - z; alpha = alpha_new; % 收敛监视 if mod(iter, 20) == 0 primal_res = norm(alpha - z); fprintf('iter %3d, primal_res = %.3e\n', iter, primal_res); if primal_res < 1e-4 break; end end end % 从支持向量计算偏置 b sv_tol = 1e-6; sv_idx = find(alpha > sv_tol & alpha < params.C - sv_tol); if isempty(sv_idx) sv_idx = find(alpha > sv_tol); end K_sv = kernel_matrix(X, sv_idx, 1:N, params); f_sv = (alpha(sv_idx) .* y(sv_idx))' * K_sv; b = mean(y(sv_idx)' - f_sv(sv_idx)); model.alpha = alpha; model.sv_idx = sv_idx; model.b = b; model.X = X; model.y = y; model.params = params; end

z的投影顺序我要特别提醒:先投影盒子约束,再投影等式约束,这时等式投影会把一部分z带到盒子边界之外一点,所以最后再裁一次。虽然不是严格意义上的凸集正交投影,但ADMM对z投影的微小不精确有很强的鲁棒性,实践中完全够用。如果追求严格的投影结果,可以用Dykstra交替投影迭代十次。

预测函数就用标准的决策函数sign(Σ αᵢ yᵢ K(xᵢ, x_test) + b):

function pred = svm_hss_predict(model, Xtr, ytr, Xte) sv_idx = model.sv_idx; K_sv = kernel_matrix(model.X, sv_idx, 1:size(Xte, 1), model.params); f = (model.alpha(sv_idx) .* model.y(sv_idx))' * K_sv + model.b; pred = sign(f(:)); end

这里注意,svm_hss_predict传入了Xtr和ytr但实际用的是model里保存的训练数据,所以那两个参数可以不用;保留它们是为了接口统一,实际调用时直接传model就好。

5.4 其他核函数和SVR扩展

上面的kernel_matrix目前只实现了高斯核。如果你想换成多项式核、拉普拉斯核或sigmoid核,只需要修改这一个函数,HSS结构和ADMM循环完全不用动。这算是这套方案的一个很舒服的特性——HSS只依赖“核函数生成的是一个数值上低秩可压缩的矩阵”这一个性质,不绑定具体核函数。

更进一步的扩展是SVR。支持向量回归的对偶问题和SVM分类长得非常像,同样是二次规划加盒子约束,只是约束条件变成0≤α_i≤C且Σα_i=Σα_i*。把ADMM的g(z)改成SVR的可行集,α更新和线性系统部分继续用HSS加速,整个框架就能迁移去训练大规模SVR。很多时序预测任务就是用SVR做baseline,样本量一大同样面临核矩阵爆炸,所以这套方案的受众不止是分类场景。

6. 实测效果与调参避坑

6.1 双月数据上的实测表现

我在自己的机器上(i5-10400,16G内存,Matlab R2023a)用上面的代码跑了双月数据,训练样本量分别取1000、3000、6000,结果如下表。数据是随机生成的,具体数值和你的机器会有差异,但数量级是可信的。

训练样本数leaf_sizerankADMM迭代数训练时间测试精度
1,000641680轮内收敛3~5秒0.98+
3,0006416100轮内收敛15~25秒0.98+
6,0006416120轮内收敛40~70秒0.98+

和直接调用libsvm跑相同数据比,精度基本持平,但内存占用明显小很多。样本量到了一万以上,libsvm在默认设置下开始频繁从磁盘缓存换数据,而我们这里因为HSS把核矩阵压缩到接近线性的存储,训练过程依然顺滑。

6.2 五个关键参数的调试经验

参数调试是这套方案里经验含量最高的部分,我逐个说。

rho是ADMM的惩罚参数,直接控制收敛速度和线性系统的条件数。rho越大,(Q+rhoI)的对角占优越强,内层CG收敛越快,但外层ADMM会变慢;rho太小则CG要迭代很多次。我的经验是rho和C保持同一个数量级,C=1时rho从0.1到2都值得一试,观察外层残差和内层CG迭代数的平衡。

sigma是高斯核带宽,这个参数和数据尺度关系极大。数据标准化之后sigma在0.5到2之间通常比较稳。sigma太小核矩阵对角占优严重、HSS低秩近似误差变大;sigma太大所有样本相互作用都差不多,分类能力下降。多试几个sigma观察验证集精度就行。

C控制正则化强度。C越大,允许的误分类越少,支持向量越多,HSS的叶子块和低秩基规模都会变大;C太小模型欠拟合。大规模场景下我一般先把C定为1,再用验证集微调。

rank是离对角低秩近似的秩,它直接决定HSS近似精度。rank太小压缩率高但精度掉得厉害;rank太大存储和计算优势就打折扣。我习惯从8开始,每次翻倍,观察验证集精度什么时候基本不涨,就选那个临界值。对于高斯核和中等维度数据,rank=16到32通常已经足够。

leaf_size控制叶子块大小。叶子块太小,树的层数多、低秩块数量大,HSS结构本身的开销上升;叶子块太大,内部稠密计算量上升。64到256是我常用的区间,双月数据这种低维问题取64,高维数据取128或256更划算。

6.3 实际踩过的几个坑

第一个坑是数据没标准化。高斯核对特征的尺度极其敏感,一个特征是10的量级、另一个特征是0.01的量级,核矩阵会被大尺度特征主导,HSS的低秩近似也会变得不稳定。我现在的习惯是训练前对所有特征做z-score标准化,用训练集的均值和标准差去变换测试集,避免数据泄漏。

第二个坑是CG容差设得太松。ADMM每轮迭代要解线性系统,如果pcg只做几次就返回,α更新不准,外层残差就会卡在一个平台期下不去。我一开始图快把cg_tol设成1e-3,结果ADMM跑了200轮残差还在1e-2级别。把cg_tol压到1e-6之后,总迭代轮数明显减少,整体时间反而更短。注意pcg的maxit不要设太小,50到100比较稳。

第三个坑就是前面提过的z投影顺序。如果错误地在等式投影之后再裁剪,会导致yᵀz=0约束被破坏得很明显,ADMM可能直接发散。养成“盒裁剪→超平面投影→再裁剪”的习惯就好。

第四个坑是rank太小导致HSS近似误差主导训练结果。我有一轮实验把rank设成4,结果训练出来的模型精度比SMO低了好几个百分点,一开始还以为是ADMM实现错了。后来对Q做了残差检查,发现HSS近似的相对误差已经到百分之几量级,这才会显著影响分类边界。解决办法就是像前面说的,检查验证集精度随rank的变化曲线,别省这一点存储。

还有一个细节值得注意:代码里用了bsxfun做符号乘法。如果你的Matlab版本较新(R2016b之后),bsxfun依然有效,也可以直接用隐式扩展写y(idx) .* ...,效果一样。另外,hss_q_matvec在递归过程中反复分配临时向量,对超大规模问题确实有优化空间,但首先保证正确性,先跑通再优化,是我一贯的建议。

如果让我给一个起点配置,对于中等规模(几千到几万样本)的非线性二分类问题,我会用C=1、rho=1、sigma看数据尺度、rank=16、leaf_size=64,然后盯着primal_res每20轮的下降情况微调rho。能跑起来之后,再根据验证集精度去调sigma和C。这套组合在我手上的各种非线性数据集上表现都比较稳,推荐你也从这组参数开始。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询