首页 / 资讯中心 / 文章详情

FastSVDD实战:单类异常检测的核列缓存与SMO优化

FastSVDD实战:单类异常检测的核列缓存与SMO优化 ★ FEATURED ARTICLE
简介FastSVDD 面向异常检测与单类分类方向的研究者和工程师围绕支持向量数据描述SVDD的计算效率问题给出了一套 MATLAB 快速实现。它适合需要在中小规模数据上验证核包络思想、对比传统 SVDD 收敛速度或为故障诊断、入侵检测等场景搭建原型的人员参考。压缩包共 164 个文件约 1012KB以 75 个 m 脚本、49 个 png 图、20 个 txt 文本、5 个 mat 数据及若干 tex 排版文件为主分别对应算法代码、结果可视化、数据与实验记录、文档说明等用途。已有 345 人学习说明其在相关课程实验与算法复现中有一定参考价值。包内提供主程序、示例数据集、预处理与评估等辅助函数以及使用说明可帮助读者较快跑通训练、验证和测试流程观察核心对象选择与参数调整对性能的影响并在此基础上做二次开发和实验对照。1. 一条产线上的报警日志FastSVDD 到底解决什么麻烦去年帮一个做设备监测的团队看数据现场装了两万条振动记录全是正常运行工况故障样本一条都没有。想圈出“正常”的边界第一反应就是 SVDD把正常样本包进一个超球球外的判异常。真动手才发现麻烦——对偶问题里躺着一个 n×n 的核矩阵两万乘两万、双精度约 3.2GB机器还没开始算就先爆内存了。FastSVDD 这类快速实现就是冲这个场面来的不显式建全核矩阵用 SMO 每次只拉两列核配合有限的核列缓存把内存压到几百 MBMATLAB 单机就能跑完。适合谁手里只有正样本、需要连续异常分数而不是硬标签、数据量在几千到几万、又不想为此搭 GPU 环境的人。它不是一个通用分类器方向很窄但窄得刚好。2. 技术选型与工程结构一个 MATLAB 单类分类器怎么摆2.1 只有正样本时为什么选 SVDD 而不是二分类二分类的决策边界只对它见过的负样本负责。你把过去半年的故障样本全标出来训练模型学到的也只是“这半年出现过的异常”换个工况、换批物料边界立刻失效。SVDD 反过来只描述正类找一个球心 a 和半径 R让绝大多数正常样本落在球内落在球外的就是异常。它对负样本没有依赖异常样本只在评估阶段出现。和 OCSVM 比两者在 RBF 核下等价选哪个更多是工程口味问题。我倾向 SVDD 的原因有三点。一是输出是到球心的距离平方天然是个连续分数你可以按业务需要调阈值而不是被一个硬标签卡死。二是模型可解释α 落在 (0, C) 区间的样本就是贴在球面上的支持向量α 顶到 C 的是明显越界的样本看一眼就知道边界是被哪些点撑起来的。三是参数量少真正要调的只有核宽 sigma 和 nu 两个网格搜索的代价很低。代价也要说清楚。SVDD 对特征尺度极其敏感标准化没做好半径就会被某一个方差大的维度带跑。另外它是纯单类方法如果手头其实有大量标注好的负样本用二分类或者半监督方法信息利用率更高没必要硬套。2.2 目录结构训练器该拆成几个文件这类包在 MATLAB 里最容易被写成一个三百行的巨型 .m 文件改起来很痛苦。合理的拆法是按职责切一个文件只干一件事FastSVDD/ ├── demo/ │ ├── demo_toy.m % 二维玩具数据最快跑通 │ ├── demo_benchmark.m % 多组数据 AUC 对比 │ └── make_toy_data.m % 造正负样本 ├── core/ │ ├── fastsvdd_train.m % 训练入口返回 model 结构体 │ ├── fastsvdd_predict.m % 预测入口返回分数和标签 │ ├── smo_solver.m % SMO 主循环 │ ├── select_working_set.m % 一阶工作集选择 │ └── make_kernel_cache.m % 按需算核列 有限缓存 ├── utils/ │ ├── standardize_fit.m % 记录 mu/sd │ ├── standardize_apply.m % 用训练集的 mu/sd 变换测试集 │ └── auc_score.m % 不依赖工具箱的 AUC ├── data/ └── README.md文件输入输出改动频率fastsvdd_train.mX, sigma, numodel低smo_solver.m核列函数, C, tolalpha, g低select_working_set.malpha, g, Ci, j, 违反度中换策略时改make_kernel_cache.mX, sigma, 缓存上限函数句柄高换核函数时改fastsvdd_predict.mmodel, Xtestscore, label低把核函数访问封装成函数句柄是个关键决定。solver 只认“给我第 i 列核”这个接口完全不知道底下是 RBF 还是拉普拉斯核换核函数时只动 make_kernel_cache.msolver 一行不改。这种隔离在小项目里容易被忽略等到要试第三种核的时候就知道值了。2.3 一次训练的数据流从 X 到 R²整个链路不长但每一步的产物都要能复现。原始特征矩阵 X 进 standardize_fit拿到标准化后的 Xz 和统计量 mu、sdXz 和 sigma 一起灌进 make_kernel_cache得到一个能按索引取核列的函数smo_solver 在这个函数上迭代产出 alpha 和梯度 g然后筛选支持向量、算球心模长平方 a2 αᵀKα再对自由支持向量逐个算 R² 取中位数最后把 Xsv、alpha、a2、R2、sigma、mu、sd 一起塞进 model 结构体。预测阶段一行训练代码都不碰只用 model 里的东西测试集用 mu/sd 变换和 Xsv 算核得到 d² K(x,x) − 2αᵀk(x) a2和 R2 比大小。这里最容易翻车的地方是测试集另算一遍均值和方差训练和推理不在同一个坐标系里分数全错。所以 mu 和 sd 必须存进 model这是硬性要求不是可选项。提示model 结构体里建议再加一个 trainInfo 字段记录迭代次数、最终 KKT 违反度、支持向量占比。出问题时这三个数比任何日志都好用。3. 核心模块拆解对偶问题、核列缓存与 SMO 双变量更新3.1 对偶形式与代码里的对应关系SVDD 的原始问题是最小化 R² C·Σξᵢ约束 ‖φ(xᵢ) − a‖² ≤ R² ξᵢξᵢ ≥ 0。走拉格朗日对偶之后球心被消掉问题变成关于 α 的二次规划最小化 αᵀKα约束 Σαᵢ 10 ≤ αᵢ ≤ CRBF 核有个很好的性质K(xᵢ, xᵢ) 1所以原始目标里那一项 ΣαᵢK(xᵢ,xᵢ) 恒等于 Σαᵢ 1是个常数可以直接扔掉。这就是为什么代码里只需要最小化 αᵀKα不用管对角项。如果你换成多项式核或者线性核这一项不再是常数得老老实实加回去这也是换核函数时最容易忘的一步。C 和 nu 的关系也在这里确定下来。约束 Σαᵢ 1 加上上界 αᵢ ≤ C要让它可行必须 n·C ≥ 1。所以工程上统一用 nu 参数化C 1/(nu·n)nu ∈ (0, 1]。nu 的含义是支持向量比例的下界nu 越小边界越松支持向量占比越低nu 接近 1 时几乎所有样本都成了支持向量边界贴着数据走过拟合风险陡增。梯度 g Kα 是后面所有更新的核心。gᵢ 就是第 i 个样本在对偶目标上的“当前位置”工作集选择、更新量计算、半径求解全靠它。3.2 核列按需计算与有限缓存不建全矩阵的代价是每算一列就得遍历一遍训练集复杂度 O(n·d)。如果 SMO 每次迭代都重新算n2 万、迭代几千次时间直接失控。必须加缓存。function Kfun make_kernel_cache(X, sigma, maxCache) % 返回一个函数句柄 Kfun(i)取第 i 列 RBF 核 % X: n×d 已标准化训练集sigma: 核宽maxCache: 最多缓存多少列 Xc X; s sigma; cap maxCache; store containers.Map(KeyType, double, ValueType, any); order []; % 记录入缓存顺序做 FIFO 淘汰 Kfun getcol; function k getcol(idx) if isKey(store, idx) k store(idx); % 命中直接返回这是加速的主要来源 return; end % 未命中只算这一列d2 是 n×1 d2 sum((Xc - Xc(idx, :)).^2, 2); k exp(-d2 / (2 * s^2)); store(idx) k; order(end1) idx; if numel(order) cap % 超出上限踢掉最早的一列 remove(store, order(1)); order(1) []; end end end逐段看。d2 sum((Xc - Xc(idx,:)).^2, 2)用的是隐式扩展MATLAB R2016b 之后才支持老版本要写成sum(bsxfun(minus, Xc, Xc(idx,:)).^2, 2)。这一行没有循环交给底层 BLAS 做比自己写 for 快一个量级。缓存用 containers.Map 是为了代码短实际生产中它比预分配数组慢不少。追求性能的话换成两块预分配矩阵一个Kbuf zeros(n, cap)存列一个slot zeros(n,1)记录每个索引落在哪个槽位再加个环形指针做 LRU。cap 的取值有个经验公式cap ≈ min(n, ceil(2e7 / n))保证缓存总元素数不超过两千万双精度下约 160MB一般机器的内存都扛得住。getcol是被闭包捕获的嵌套函数Xc、s、store、order 都在外部作用域调用方只看到Kfun(i)这一个入口。这种写法在 MATLAB 里很自然但要注意别在闭包里改 Xc改了就破坏缓存一致性。3.3 工作集选择与双变量更新的解析解SMO 的核心是每次只动两个 α因为等式约束 Σαᵢ 1 决定了至少得同时改两个分量才能保持可行。选哪两个直接决定收敛速度。function [i, j, viol] select_working_set(alpha, g, C) % 一阶最大违反对i 在 alphaC 里取梯度最小j 在 alpha0 里取梯度最大 up alpha C - 1e-12; % 还能往上加的分量 lo alpha 1e-12; % 还能往下减的分量 gi g; gi(~up) inf; [gmin, i] min(gi); gj g; gj(~lo) -inf; [gmax, j] max(gj); viol gmin - gmax; % 0 表示已经满足 KKT可以停 end参数 alpha 是当前解g 是梯度 KαC 是上界。把不满足条件的索引用 ±inf 屏蔽掉min/max 自然只会挑到合法的候选。viol 是 KKT 违反度它大于等于 −tol 时就可以收工这个量的物理意义是“再动一步最多还能降多少”直接当收敛判据用比看 α 变化量靠谱。选定 i、j 之后沿着方向 eᵢ − eⱼ 走一步αᵢ 加 t、αⱼ 减 t等式约束自动满足。把目标函数展开成关于 t 的二次式求导置零得到解析解t* −(gᵢ − gⱼ) / η其中 η Kᵢᵢ Kⱼⱼ − 2Kᵢⱼ。η 恒大于等于 0核矩阵半正定等于 0 说明两个样本在特征空间里完全重合这时候除零会出 Inf。工程上加个下限保护η 1e-12 时直接置为 1e-12宁可这一步走得慢一点也不能让 alpha 变 NaN。function [alpha, g, iter] smo_solver(Kfun, n, C, tol, maxIter) alpha zeros(n, 1); alpha(1) 1; % 初始可行解满足 sum(alpha)1 g Kfun(1); % 梯度初值 K * e1即第一列 for iter 1:maxIter [i, j, viol] select_working_set(alpha, g, C); if viol -tol, break; end ki Kfun(i); kj Kfun(j); eta ki(i) kj(j) - 2 * ki(j); if eta 1e-12, eta 1e-12; end t -(g(i) - g(j)) / eta; % 无约束最优步长 % 同时满足 0alpha(i)tC 和 0alpha(j)-tC lo max(-alpha(i), alpha(j) - C); hi min(C - alpha(i), alpha(j)); t min(max(t, lo), hi); % 裁剪到可行区间 alpha(i) alpha(i) t; alpha(j) alpha(j) - t; g g t * (ki - kj); % 梯度增量更新O(n) end end参数说明一下。Kfun 是上一节的缓存句柄n 是样本数C 是上界由 1/(nu·n) 算出来tol 是收敛阈值默认给 1e-3 就够想要更精确的解可以给 1e-5但迭代次数会明显上去maxIter 是保险丝防止 eta 保护导致死循环一般设 1e4。梯度那行g g t*(ki - kj)是 SMO 相对批量 QP 的最大优势。两个样本动了梯度只在这两个方向上有增量一次 O(n) 的向量加法就把 g 更新完了。如果每次迭代重算 g Kα复杂度是 O(n²)前面所有的缓存优化都白做了。3.4 半径 R² 与支持向量的判定α 求出来之后球心是 a Σαᵢφ(xᵢ)但代码里从来不需要显式求出 a只要求 a2 ‖a‖² αᵀKα这个标量一个矩阵向量乘就够。样本 x 到球心的距离平方d²(x) K(x,x) − 2ΣαᵢK(xᵢ,x) a2 1 − 2αᵀk(x) a2R² 用自由支持向量算也就是 α 严格落在 (0, C) 里的那些点它们精确贴在球面上。把所有自由 SV 的 d² 取中位数比取平均稳——个别浮点误差大的点不会把半径带偏。sv find(alpha 1e-8); % 所有支持向量 freeSV find(alpha 1e-8 alpha C - 1e-8); % 自由支持向量 if isempty(freeSV), freeSV sv; end % 退化情况兜底 a2 alpha * g; % ||a||^2 R2v zeros(numel(freeSV), 1); for t 1:numel(freeSV) ki Kfun(freeSV(t)); R2v(t) 1 - 2 * (alpha * ki) a2; end R2 median(R2v);1e-8 这个阈值不是玄学是双精度下 alpha 的有效分辨量级。设太小会把数值噪声当成支持向量设太大又会漏掉真实贴边的点。如果 freeSV 为空说明 nu 设得太极端或者 sigma 太小所有 α 都顶在 C 上或掉到 0这时候退化成用全部 SV 估半径同时应该打印个警告——这是参数不合理的信号不是正常的运行状态。预测时分两种情况。硬标签直接拿 d² 和 R2 比要分数就返回 d² 本身值越大越异常。后者在实际系统里更好用因为阈值可以按误报率反推而不是训练完就锁死。4. 跑起来从玩具数据到一次完整的训练与评估4.1 造一份能立刻跑的数据真实数据不方便传仓库里给了个二维生成函数正常样本是一个扁高斯团异常样本是一个环两者在空间上是分开的但不线性可分正好检验核方法的效果。function [X, y] make_toy_data(n, seed) % 生成二维单类数据前 n 个正常1后 15% 为环形异常-1 if nargin 2, seed 42; end rng(seed); Xn randn(n, 2) * [1 0; 0 0.6]; % 正常各向异性高斯 na round(n * 0.15); theta 2 * pi * rand(na, 1); r 4.5 0.3 * randn(na, 1); % 半径带抖动的环 Xa [r .* cos(theta), r .* sin(theta)]; X [Xn; Xa]; y [ones(n, 1); -ones(na, 1)]; % 1 正常-1 异常 endna 是异常样本数取正常样本的 15%是为了让 AUC 有意义又不至于把问题变成简单的距离阈值。环的半径 4.5 大约是三倍标准差开外SVDD 只要核宽合适就能把它整体挡在球外。把数据画出来看一眼是必要步骤尤其第一次跑。scatter(X(y1,1), X(y1,2), 6, filled)加hold on再叠一层异常点能立刻判断任务难度。跳过这一步直接看 AUC很容易被一个虚高的数字骗过去。4.2 sigma 和 nu 怎么定sigma 是 RBF 核的宽度它决定了“多近算相似”。太小每个样本自成一球训练集全判正常新样本全判异常太大球面过度平滑异常点也被包进来。常见的默认做法是中位数启发式算所有样本两两距离的中位数取 sigma 等于它或者它的 0.52 倍扫一遍。function sigma median_heuristic(X, nSub) % 用距离中位数给 sigma 一个量级正确的初值 if nargin 2, nSub min(1000, size(X, 1)); end idx randperm(size(X, 1), nSub); Xs X(idx, :); D pdist(Xs); % 需要统计工具箱 sigma median(D); endnSub 是抽样上限一千个点的两两距离是五十万个数算得很快全量两两距离在 n 上到两万时就是两亿个数纯属浪费。pdist 依赖统计工具箱没装的话用sqrt(sum((Xs - Xs).^2, 2))手工算也行只是内存要留意。nu 的搜索范围一般是 {0.01, 0.05, 0.1, 0.2, 0.5}正常工况下 0.050.1 就够。nu 太大支持向量占比升高模型对训练集里的噪声点过度敏感线上误报会明显变多。两个参数不要一起网格搜索。先用中位数定 sigma 的量级再单独扫 nu最后在最优 nu 附近微调 sigma。二维网格在样本量大的时候训练时间翻十几次收益远不如分开调。4.3 训练、预测、看分数分布[X, y] make_toy_data(600); idxTr find(y 1); % 只用正常样本训练 Xtr X(idxTr, :); model fastsvdd_train(Xtr, 1.2, 0.08); [score, label] fastsvdd_predict(model, X); fprintf(支持向量占比: %.2f%%, 迭代 %d 次\n, ... 100 * numel(model.alpha) / size(Xtr,1), model.nIter); fprintf(正常样本平均分 %.3f, 异常样本平均分 %.3f\n, ... mean(score(y 1)), mean(score(y -1)));训练只用 y1 的行这是单类方法的纪律别图省事把异常样本也喂进去。fastsvdd_train 的第三个参数是 nu不是 C内部会自动换算成 1/(nu·n)。输出的两个均值差得越开越好。正常样本的平均分应该在 R2 附近或略低异常样本明显高出。如果两者差不到 0.1先怀疑 sigma 没调对而不是去调 nu。fastsvdd_predict 内部会调 standardize_apply用 model.mu 和 model.sd 变换测试集保证和训练同一坐标系。这一层封装省了很多事也堵住了最常见的那个坑。4.4 用 AUC 和召回率做验证准确率在单类问题里几乎没有意义因为异常比例是你自己造的。看 AUC 和固定误报率下的召回率更实在。不装统计工具箱也能算 AUC秩和公式就够了function auc auc_score(score, y) % score 越大越异常y-1 为正类异常 pos (y -1); [~, ord] sort(score, ascend); r zeros(numel(score), 1); r(ord) 1:numel(score); np sum(pos); nn numel(score) - np; auc (sum(r(pos)) - np * (np 1) / 2) / (np * nn); end秩和公式避免了逐阈值扫描n 个样本排一次序就出结果。注意方向score 越大越异常所以排序用 ascend异常类取秩和的大头。召回率要配一个业务能接受的误报率来看比如“正常样本误报不超过 3% 时能抓到多少异常”这个指标比 AUC 更贴近上线后的真实表现。仓库里的 demo_benchmark.m 就是按这种方式扫 nu 和 sigma 的输出一张二维表直接看哪个格子最平衡。5. 避坑与常见问题排查五个踩过的坑这一节按我实际遇到的顺序排每条都是先看到现象、再定位原因、最后落到改法。新上手的人至少会撞上其中两个。5.1 内存与缓存相关的两个坑一、样本量一上去就报 Out of memory现象n 从 5000 加到 20000脚本跑到exp(-d2/(2*s^2))附近直接卡死任务管理器内存曲线直线上升最后弹出内存不足。原因虽然训练主流程已经不建全核矩阵了但很多人会在预处理阶段顺手写一句K exp(-pdist2(X,X).^2/(2*sigma^2))存成变量留作后面画图或者调试用。pdist2 返回的正是 n×n 全矩阵20000² 双精度就是 3.2GB加上中间变量翻倍。解决把全矩阵彻底从代码里删掉需要看核矩阵分布就随机抽 500 个样本算 500×500 的子块。另外在 make_kernel_cache 里加一道自检缓存上限按cap min(n, floor(2e7/n))算保证 Kbuf 总元素不超过两千万双精度下约 160MB。我一般还会在训练入口打印一行[cache] n%d, cap%d, mem≈%.1f MB出问题一眼就能看出是不是缓存设大了。二、缓存太小导致训练时间反而变长现象为了省内存把 maxCache 设成 20结果 n8000 的数据训练了四十多分钟比不缓存还慢。原因SMO 的工作集选择是全局扫描相邻两次迭代选中的 i、j 大概率不重复缓存命中率接近 0。每列都要重新算 O(n·d) 的距离加上 containers.Map 的哈希开销比直接算还亏。解决缓存大小要跟 n 匹配粗略原则是“能装下最近几百列”。n 小于 3000 时直接把 cap 设成 n全缓存最省事n 在 3000 到 30000 之间用前面那个 2e7/n 的公式n 超过 30000 时老老实实换二阶工作集选择WSS2用 η 估计每对的下降增益减少迭代次数比省内存更管用。判断方法很简单在 solver 里统计hit/(hitmiss)低于 0.3 就说明缓存白设了。5.2 参数与数值相关的三个坑三、sigma 取小了训练集全对、测试集全错现象训练完打印支持向量占比 100%正常样本的分数全在 R2 以内换一份新数据哪怕只差一点点也被判异常误报率飙到 60% 以上。原因sigma 太小意味着核函数只在极近距离上有响应每个训练样本自成一个孤立的小球球面完全贴着训练点。这等价于把训练集原样背下来泛化能力为零。支持向量占比 100% 是最直观的信号——正常模型的这个数应该在 nu 附近最多两倍于 nu。解决先跑 median_heuristic 看距离中位数的量级sigma 从这个值的 0.5 倍到 2 倍区间扫。扫的时候盯两个数支持向量占比和验证集 AUC。占比突然从 20% 跳到 100% 的那一段就是 sigma 过小的边界取边界右侧第一档。另外标准化必须做不标准化的话各维度的距离量纲不一致中位数启发式本身就会失效。四、迭代不收敛alpha 卡在上下界来回跳现象maxIter 跑到上限还没退出打印出来的 viol 一直在 −1e-2 附近震荡alpha 里出现大量恰好等于 C 或恰好等于 0 的值。原因tol 设得太小比如 1e-8而双精度加上核矩阵的条件数限制梯度的有效分辨精度到不了这个量级viol 在阈值附近永远过不去。另一种情况是 η 逼近 0两个样本在特征空间几乎重合步长算出来巨大被裁剪后变成贴着边界跳。解决tol 默认给 1e-3最大 1e-4再小没有实际收益。η 的保护阈值从 1e-12 提到 1e-10 更稳配合t min(max(t,lo),hi)的双向裁剪保证不会越界。如果还是不收敛检查一下是不是 nu 设得让 n·C 刚好等于 1这种极端情况下只有一个 α 能到 C其余全是 0问题退化换个 nu 就行。五、训练和预测用了两套标准化现象离线评估 AUC 0.95上线后误报率超过一半把测试数据拿回训练脚本里跑又是好的。原因训练里做了 z-score预测脚本里图省事直接拿原始特征算核或者用自己的均值方差重新标准化了一遍。特征空间整个错位算出来的 d² 和 R2 完全不在一个尺度上。解决把 mu 和 sd 存进 model 是硬约束预测函数里只允许调用 standardize_apply(model, Xtest)不允许出现 mean/std 这两个函数。我还会在 fastsvdd_train 末尾加一个自检用训练集自己预测一遍正常样本的误报率应该接近 nu偏离太多说明标准化或者半径计算有问题。这个自检三行代码能挡掉上线后最难排查的一类故障。注意换核函数时d² 公式里的1不能再写死。RBF 核 K(x,x)1 是特例线性核和多项式核的 K(x,x) 随样本变化必须老老实实算 diag(K)。6. 二次开发把训练器塞进自己的流水线改造点集中在三个地方改之前先想清楚要动哪个文件别一上来就改 solver。改造目标改哪个文件需要注意换核函数拉普拉斯/多项式make_kernel_cache.m 预测里的 d² 公式K(x,x) 不再是常数半径和预测都要改在线/增量更新fastsvdd_train.m 增加热启动参数传入上次的 alpha 和 g迭代次数能降一个量级高维降维后训练demo 层加一步 PCA降维的投影矩阵必须和 mu/sd 一起存进 model批量流水线化fastsvdd_train.m 外层包一层多组参数并行时注意缓存句柄不要共享热启动是最划算的一个改造也是我最常用的技巧。设备数据是持续进来的每次全量重训代价太高。做法是把上一次的 alpha 和 g 作为初值传进 smo_solver只跑几十次迭代做微调。实现上就是把 solver 开头那两行初始化改成可选参数function [alpha, g, iter] smo_solver(Kfun, n, C, tol, maxIter, warm) % warm 可选struct(alpha,[], g,[]) if nargin 6 ~isempty(warm.alpha) alpha warm.alpha; g warm.g; % 梯度必须和 alpha 配套不能只传一半 else alpha zeros(n, 1); alpha(1) 1; g Kfun(1); end ... end关键是 alpha 和 g 必须成对传。只传 alpha 让 solver 重算 g等于白热启动只传 g 而 alpha 变了梯度就是错的后面所有更新全歪。这个约束我吃过一次亏在某次重构里把 g 的初始化漏了结果模型安静地输出了一个完全错误的边界AUC 掉到 0.5 才发现。验证改造是否正确的办法不是看代码是看数。每次改完跑三件事玩具数据上的 AUC 有没有掉、支持向量占比是否还在 nu 附近、训练集自预测的误报率是否接近设定值。三个数都对得上才算改对了。从那以后我每次动 solver 或者核函数都强制先跑一遍这三个自检再往下走看着麻烦实际省下的排查时间远多于写自检的时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站