
简介FastSVDD 是一份面向异常检测与单类分类学习者的 MATLAB 代码包围绕支持向量数据描述SVDD的快速实现展开适合需要复现实验、比较算法效率或开展故障诊断、入侵检测等应用的研究人员与工程师。压缩包共 164 个文件约 1012KB其中 75 个 .m 脚本承载算法主流程与辅助函数49 个 .png 图像用于查看实验结果另有 20 个 .txt、5 个 .mat 数据及 tex 排版文件兼顾代码、数据、图表与文档说明。已有 344 人学习下载。包内可获取主程序、示例数据集、预处理与可视化工具、评估脚本及使用说明便于理解核心对象选择、核函数与参数调整对收敛和性能的影响也可据此改造成自己的异常检测实验。1. SVDD 只在“异常样本几乎拿不到”时才值得上某公司的设备健康监测项目断断续续采了大半年数据标签却只有“正常”一种——故障停机是偶发事件样本量个位数连划分验证集都不够。这种场景下判别式模型会直接翻车没有负类分类边界根本无从谈起硬凑出来的模型在产线上基本等于随机报警器。SVDDSupport Vector Data Description支持向量数据描述走的是另一条路它不学“异常长什么样”而是用一个最小超球把正常样本尽可能紧地包住球外的点天然判为异常对负样本数量没有硬性要求。FastSVDD 就是把这个思路在 MATLAB 里做成了跑得动的实现。原始 SVDD 的对偶问题求解复杂度随样本数平方甚至立方增长核矩阵本身又是 n×n 稠密存储几千条样本就要等上几分钟、吃掉上 GB 内存工程上很难迭代。加速版本的基本盘是把“一次解全局”换成“一轮轮修局部”配合核矩阵分块与复用把内存峰值压到跟工作集同量级。适合谁手上只有单类数据、用 MATLAB 做离线建模或算法验证的从业者尤其是想把 SVDD 从论文公式搬到产线脚本里的人。2. FastSVDD 在 MATLAB 里怎么跑从对偶问题到第一段可复现脚本2.1 对偶问题到底慢在哪加速又省在哪先把公式摆出来不然后面调参全是玄学。SVDD 的目标是在特征空间里找一个球心 a、半径 R让球尽量小、同时允许少量样本落在球外。写成对偶形式变量是每个样本一个 αmax Σ_i α_i K(x_i,x_i) - Σ_i Σ_j α_i α_j K(x_i,x_j) s.t. 0 ≤ α_i ≤ C, Σ_i α_i 1判决函数是查询点到球心的距离平方f(x) K(x,x) - 2 Σ_i α_i K(x_i,x) Σ_i Σ_j α_i α_j K(x_i,x_j)f(x) 大于 R² 就判为球外。慢的地方有三处一是核矩阵 K 是 n×n 稠密矩阵n5000 时光存一份双精度就要 5000×5000×8 字节接近 190 MB迭代过程中还会产生若干同尺寸临时矩阵峰值轻松上 GB二是标准 QP 求解器要对整个 Hessian 做一次分解n 过了三千基本就卡死三是每次调 sigma 都要从头再算一遍核矩阵。FastSVDD 的加速思路是分解求解每轮只挑出违反 KKT 条件的样本构成工作集在工作集上解一个小规模子问题解完更新 α再挑下一轮。核矩阵按需分块计算并缓存不一次性全量展开。结果是内存占用跟着工作集走而不是跟着样本总数走单轮迭代的耗时也被压到可控范围。理解了这一点后面调参和排查才有依据——很多“跑不动”的问题本质是工作集被撑爆了而不是算法本身不行。2.2 拿到包之后先核对三件事MATLAB 工具箱类资源最怕签名不一致代码贴上去直接报“输入参数太多”。动手之前我一般先看这三项核对项为什么先看它具体看什么训练函数签名决定后面所有脚本怎么写参数顺序是 (X, sigma, C) 还是 (X, C, sigma)有没有用 nu 代替 C 的别名返回值里有没有 α 和 R²缺一项就没法自己做判决和迁移只返回标签说明阈值被写死在内部工程上不好接有没有预处理入口归一化参数必须跟模型一起落盘包内若自带归一化要能拿到 mu/sd否则线上必然错位第三条是最容易被忽略的。训练时你对着归一化后的数据调 sigma推理时忘了减均值除标准差模型给出的判决值会整体漂移看起来像“模型失效”其实是数据没对齐。函数名以你手上这版的实际签名为准下面的示例统一按fastsvdd_train(X, sigma, C)和fastsvdd_predict(model, X)来写签名不同就做一层薄封装不要改动内部逻辑。2.3 最小可复现脚本500 条正常样本跑通一次训练先跑通再谈调参。这段脚本只用正常样本训练异常样本只在推理阶段参与符合单类任务的真实用法%% 1) 只用正常样本训练异常一概不进训练集 rng(42); % 固定种子保证每次结果可复现 n 500; d 6; Xn randn(n,d)*0.8 2; % 正常样本均值约 2单维散布 0.8 mu mean(Xn,1); sd std(Xn,0,1); Xn (Xn - mu) ./ sd; % 量纲统一不然后面 sigma 根本调不动 %% 2) 训练超球 sigma 1.2; % RBF 核宽 C 0.1; % 上界约束越小容忍越多样本落在球外 model fastsvdd_train(Xn, sigma, C); %% 3) 构造一批“没见过”的异常样本整体平移 1.5 个标准差 Xa randn(100,d)*0.8 2; Xa (Xa - mu) ./ sd; % 必须复用训练的 mu/sd Xa Xa 1.5; %% 4) 打分dec 是到球心的距离平方越大越异常 [dec_n, out_n] fastsvdd_predict(model, Xn); [dec_a, out_a] fastsvdd_predict(model, Xa); fprintf(支持向量占比 %.1f%%\n, 100*numel(model.svIdx)/n); fprintf(半径平方 R2 %.4f\n, model.R2); fprintf(训练集球外比例 %.2f%%\n, 100*mean(out_n)); fprintf(异常集检出比例 %.2f%%\n, 100*mean(out_a));逻辑上分四步先造一批有聚集结构的正常样本再做标准化把各维拉到同一量级然后训练超球最后用平移后的样本来模拟分布漂移。参数含义值得逐一说清sigma控制 RBF 核的“视野半径”越小越关注局部细节C是每个 α 的上界间接控制允许落在球外的训练样本比例C 越小边界越松dec是判决值它跟 R² 同量纲阈值就是model.R2所以别自己去猜一个 0.5 之类的数字。运行之后我通常先看两个数支持向量占比和训练集球外比例。占比落在 8%15%、球外比例在 5%10%说明这次训练处于一个健康区间占比超过 40% 基本可以判定 sigma 太小或者 C 太小模型把每个样本都当成了边界。2.4 模型结构里必须落盘的字段训练完把model直接save成 mat 文件是省事但迁移到别的语言做推理时会踩坑。下面几个字段是判决函数真正依赖的一个都不能少字段含义落盘注意svX支持向量本身必须存只存行号 svIdx 换个数据集就对不上alpha支持向量对应的 α 值必须存判决加权靠它sigmaRBF 核宽必须存和训练不一致结果全错R2判决半径的平方必须存是唯一的硬阈值constΣΣ α_iα_jK(x_i,x_j)判决里的常数项建议存省掉每次推理重复算 O(n_sv²)mu / sd训练时的归一化参数强烈建议一起存线上对齐全靠它落盘本身一行就够关键是别漏字段% 把模型和归一化参数捆在一个结构体里避免上线时漏传 bundle.model model; bundle.mu mu; bundle.sd sd; bundle.sigma sigma; bundle.C C; bundle.trainDate datestr(now,yyyy-mm-dd HH:MM:SS); % 出问题能对上训练批次 save(svdd_bundle.mat, -struct, bundle);-struct会把结构体字段展开成独立变量存盘读回来直接load就能拿到同名变量。加上训练时间戳是个小习惯线上判决异常时能第一时间确认是不是加载了旧模型。2.5 推理侧把判决压成一次矩阵乘法当查询样本成批进来时逐条循环算核函数会慢得离谱因为 MATLAB 的循环开销远大于向量化。判决函数里的核矩阵可以整体展开——RBF 下 K(x,x)1于是f(x) 1 - 2·K_sv·α const其中 K_sv 是“查询样本 × 支持向量”的核矩阵。用欧氏距离平方展开来算避免任何双重循环function [dec, isOut] svdd_score(model, Xq) % model 需包含 svX / alpha / sigma / R2 / const sq sum(Xq.^2, 2); % 查询样本的模平方n×1 ss sum(model.svX.^2, 2); % 支持向量的模平方1×m D2 sq ss - 2*(Xq * model.svX); % 欧氏距离平方矩阵n×m D2(D2 0) 0; % 浮点误差可能产生极小负数截断掉 K exp(-D2 / (2*model.sigma^2)); % RBF 核 dec 1 - 2*(K * model.alpha) model.const; isOut dec model.R2; end这里有两个参数说明要咬住sigma必须跟训练时完全一致因为核宽直接决定距离被压缩的程度差 10% 判决结果就会整体偏移const是标量训练时算一次存下来即可不要每次推理重算。D2 0的截断看着不起眼但在高维小样本场景里sq ss - 2*x*y的浮点累积误差确实会产出 -1e-16 这样的值进exp之后虽然影响很小但会让同一批数据的两次推理结果出现 1e-12 级差异做 AB 对比时非常烦人。3. 核宽与惩罚项C 和 sigma 到底怎么定3.1 两个旋钮管的是两件不同的事很多人把 sigma 和 C 当成需要一起乱搜的两个超参其实它们的作用域分得很清楚。sigma 决定核空间的“分辨率”sigma 很小RBF 核矩阵接近单位阵每个样本在特征空间里几乎是孤立的点超球为了包住它们必须膨胀支持向量比例直奔 100%训练集误差很好看新数据一来全员报警sigma 很大核矩阵趋近全 1 矩阵所有样本被压成一团球包住了几乎所有东西检出率直接塌到 0。C 决定的是“允许多少训练样本被丢在球外”可以理解为对噪声的容忍度。在对偶约束 Σα_i 1 和 0 ≤ α_i ≤ C 之下C 越小单个样本能分到的权重上限越低为了满足求和为 1 的约束必然有更多样本获得非零 α边界随之松弛。经验做法是直接按“我能容忍多少训练集噪声”反推C ≈ 1 / (n · nu)n 是训练样本数nu 是你预期落在球外的训练样本比例。生产数据里如果有 5% 的采样异常就取 nu0.05。这样比盲搜 C 有效得多因为 C 的量级和样本数强相关——500 条样本时 C0.1 可能刚好5 万条样本时同样的 0.1 会把球撑得完全失去判别力。3.2 用支持向量比例做网格搜索单类任务没有验证标签没法直接看准确率选参。我一般用支持向量占比作为主抓手训练集球外比例作为副抓手两条同时满足才算合格n size(Xn,1); sigmas [0.5 0.8 1.2 1.8 2.5]; % 从窄到宽跨度覆盖一个数量级 nus [0.20 0.10 0.05 0.02]; % 期望的球外比例 Cs 1 ./ (n * nus); % 由 nu 反推 C别盲搜 best struct(svr, inf, sigma, NaN, C, NaN); for s sigmas for c Cs m fastsvdd_train(Xn, s, c); svr numel(m.svIdx) / n; % 支持向量占比 nOut sum(fastsvdd_predict(m, Xn)); % 训练集自身被判球外的条数 % 双闸门SV 占比落在 8%~15%且球外比例不超过设定 nu 的两倍 if svr 0.08 svr 0.15 nOut/n 2*(1/(c*n)) if abs(svr - 0.12) abs(best.svr - 0.12) best struct(svr,svr,sigma,s,C,c); end end end end fprintf(选中 sigma%.2f C%.4f SV占比%.1f%%\n, best.sigma, best.C, 100*best.svr);逻辑上先按 nu 生成候补 C再遍历 sigma每个组合训练一次、记录占比最后在合格组合里挑最接近 12% 的那一组。为什么目标定 12%这是我在若干组产线数据上试出来的经验值占比再低边界过紧、新样本误报明显上升占比再高则说明模型在记忆训练集。你手上的数据分布不同可以在 8%20% 之间调整这个目标带但不要放弃这个指标直接看训练误差——训练误差在单类任务里几乎没有参考价值。3.3 一段能直接抄的留出验证流程网格搜索定完参数还得用留出法确认泛化能力。做法是把正常样本切一部分出来不参与训练再混入人工构造的异常样本同时统计误报率和检出率nRep 5; % 重复 5 次不同切分减小偶然性 fpr zeros(nRep,1); tpr zeros(nRep,1); for r 1:nRep p randperm(n); tr p(1:round(0.8*n)); te p(round(0.8*n)1:end); m fastsvdd_train(Xn(tr,:), best.sigma, best.C); [~, oN] fastsvdd_predict(m, Xn(te,:)); % 留出的正常样本 fpr(r) mean(oN); Xa_r randn(100,d)*0.8 2; Xa_r (Xa_r - mu) ./ sd 1.5; [~, oA] fastsvdd_predict(m, Xa_r); tpr(r) mean(oA); end fprintf(FPR %.3f ± %.3f\n, mean(fpr), std(fpr)); fprintf(TPR %.3f ± %.3f\n, mean(tpr), std(tpr));注意两点留出的正常样本在训练时完全没出现过这样算出的 FPR 才是可信的异常样本用平移构造只是快速验证真实项目里要拿实际故障片段补进去合成异常和真实异常的检出率往往差得很远。这段流程跑完如果 FPR 超过 10% 或者 TPR 低于 0.8回 3.2 重新定参数别急着上线。4. 避坑与排查五类在真实数据上翻车的情况4.1 离线看着好好的上线几乎每条都报警现象训练完在本地跑predict训练集球外比例是 3%一切正常接到实时数据流上一天报了几百条人工复核全是误报。原因归一化参数没跟着模型走。训练时用了(X - mu) ./ sd线上直接把原始量纲的采样值喂进去特征整体偏移判决值当然全部越过 R²。解决把 mu/sd 和模型打进同一个 mat 文件推理入口强制从 bundle 里取禁止在推理侧重新计算均值。这条我在 2.4 的落盘表里已经单列实际项目里还是会有新人图省事在外面重算一遍测试环境数据量小看不出来一上量就暴露。4.2 换一批设备数据重训直接报错或结果发散现象同一套参数A 设备上跑得好好的B 设备的数据一喂进去就提示核矩阵非正定或者dec出现NaN。原因B 设备的某些传感器通道长期恒定输出标准差为 0归一化时0/0产生了NaN另一种情况是两个通道高度线性相关核矩阵数值上接近奇异分解时直接失败。解决训练前加一道体检把标准差小于 1e-8 的列剔除再检查特征间的相关系数超过 0.98 的保留一个。这两步做了之后绝大多数“换数据就崩”的问题会消失。4.3 样本量一涨训练从秒级变成几十分钟现象500 条样本时训练 2 秒涨到 2 万条时跑了 20 分钟还没结束内存占用一直在涨。原因加速实现虽然做了分块但默认工作集大小可能随样本数线性增长2 万条样本的工作集一旦超过某个规模分块带来的收益就被核矩阵计算本身吃掉了。解决限制最大工作集尺寸通常取 200500 之间比较稳同时做训练集精简先用 k-means 或分层抽样把 2 万条压到 5000 条代表点。单类任务对样本冗余的容忍度很高精简后边界形状变化通常小于 2%但训练时间能降一个数量级。4.4 支持向量数接近样本总数R² 大得离谱现象训练完numel(model.svIdx)几乎等于样本数R² 比正常值大三四倍所有样本都在球面上。原因sigma 设置过小或者 C 设得过小导致几乎每个 α 都顶到上界。两种情况都会让球无意义地膨胀。解决回到 3.2 的网格搜索先固定 C 按 nu 反推再扫 sigma。如果扫完所有组合 SV 占比都下不来检查数据是否做过归一化——没归一化时各维尺度差几个数量级RBF 距离被最大那一维完全主导等效于把 sigma 调到了极小。4.5 MATLAB 里评分正常导出到别的语言推理就翻判现象同一批查询样本MATLAB 里判为正常用 Python 或 C 重写推理后边界样本翻判差异出现在小数点后三四位。原因一是核函数形式没对齐MATLAB 侧用的是sigma导出侧按gamma写成了1/sigma^2正确的换算是gamma 1/(2*sigma^2)二是距离计算方式不同逐点算和展开算的浮点误差路径不一样。解决导出时把gamma和const一起写进配置文件并且在导出侧用双精度重算一遍判决值跟 MATLAB 结果逐条比对允许误差设 1e-9。这个比对脚本值得保留每次改推理代码都跑一遍比事后排查省事得多。提示以上五条里4.1 和 4.5 属于工程对齐问题跟算法无关但实际项目里翻车频率最高动手前先把这两条落实。5. 验证与效率对比别只盯一个 AUC5.1 单类任务的评估口径要自己搭纯单类数据没有负类AUROC 这类指标从定义上就没法算。我在实际项目里用两套口径一套是留出正常样本算误报率FPR反映模型对“见过的分布”有多保守另一套是注入式异常算检出率TPR异常来源优先用真实故障片段实在没有才用分布平移合成。两套口径要同时报告只看一个必然出问题。把 sigma 调到极大FPR 会非常漂亮几乎不报警但 TPR 也一起塌掉把 sigma 调到极小TPR 接近 1FPR 也接近 1等于没有模型。真正有意义的指标是“FPR 控制在 5% 以内时的 TPR”这个数才有上线参考价值。% 扫一遍阈值画 FPR-TPR 曲线取 FPR0.05 时的最大 TPR ths linspace(min(dec_n), max(dec_n), 200); bestTPR 0; for t ths f mean(dec_n t); % 正常样本中被判异常的比例 if f 0.05, continue; end bestTPR max(bestTPR, mean(dec_a t)); end fprintf(FPR5%% 时的 TPR %.3f\n, bestTPR);这段代码绕开了模型自带的 R²直接扫判决值阈值。好处是能看出模型输出的排序质量——如果扫遍所有阈值都找不到一个 FPR 低于 5% 的点说明模型的区分能力本身就不够调阈值没用得回去调 sigma。5.2 耗时和内存要分开测效率对比最容易做假的地方是只报一个总耗时。训练时间和推理时间要分开内存峰值要单独记否则看不出加速实现的收益到底在哪。测训练耗时的时候记得先 warmup 一次——MATLAB 的函数首次调用有解析和 JIT 开销直接计时会把冷启动算进去测出来的数字比真实值高出一截。% warmup让函数完成首次解析避免冷启动污染计时 fastsvdd_train(Xn(1:50,:), sigma, C); t zeros(3,1); for k 1:3 tic; m fastsvdd_train(Xn, sigma, C); t(k) toc; end fprintf(训练耗时 中位数 %.3f s\n, median(t)); % 内存峰值用 whos 粗看或用 memory 看可用内存变化 w whos(m); fprintf(模型占用 %.2f MB\n, w.bytes/1024/1024);重复三次取中位数比单次计时稳得多。内存这块whos只能看模型本身的大小看不到求解过程中的临时矩阵要精确测就得在训练前后调memory看可用内存差值或者直接用系统监视工具。工程上我一般只关注“会不会爆内存”给个粗略上界就够了。5.3 一次能说服自己的对比对比实验的设计原则是“只改一个变量”。固定同一份归一化数据、同一个 sigma 和 C只把求解器从标准 QP 换成 FastSVDD看三件事训练耗时、内存峰值、判决结果的一致性。一致性这条最容易被跳过。加速实现如果工作集选择策略有问题会在边界附近产生跟标准解不同的 α 分布虽然判决值差别不大但支持向量集合可能完全不同。验证方式是拿同一批查询样本两套实现各算一遍dec看最大绝对误差。误差在 1e-6 量级以内可以认为等价超过 1e-3 就要怀疑收敛条件设得太松。对比项标准 QP 实现FastSVDD判读标准训练耗时5000 样本分钟级秒级数量级差异才算有效内存峰值随 n² 增长随工作集增长关注是否能在目标机器上跑完判决值一致性基准跟基准比最大绝对误差 1e-6支持向量占比基准跟基准比差异 2 个百分点表格里没有填具体数字是因为不同机器、不同数据分布下差异很大填了反而误导。这张表的价值在于给出判读标准——你跑完自己的数据拿结果去套最后一列比看别人的数字有意义。6. 进阶核矩阵缓存与增量重训的触发条件模型训完之后推理性能的瓶颈通常在核矩阵计算上。如果支持向量集合固定K_sv 的每一列只依赖对应的那个支持向量和查询样本跟批次里其他样本无关所以可以把支持向量预先整理成连续内存块查询时分块乘、结果拼起来% 分批推理避免一次性构造 n×m 大矩阵把内存打满 function dec svdd_score_batched(model, Xq, bs) n size(Xq,1); dec zeros(n,1); for i 1:bs:n j min(ibs-1, n); [d, ~] svdd_score(model, Xq(i:j,:)); % 复用 2.5 的向量化实现 dec(i:j) d; end end批大小bs的经验取法是让bs × numel(svIdx)不超过可用内存的三分之一一般 5122048 之间。这个改动看起来朴素但在支持向量有几百个、查询流持续不断的场景里能把内存峰值压下来一个量级。另一个值得做的是增量重训的判断逻辑。设备运行状态会缓慢漂移模型不可能一劳永逸但每次来新数据就全量重训又太浪费。我一般监控两个信号新样本判决值的中位数相对训练期基准的漂移比例以及新样本中判为球外的比例是否显著超过设定的 nu。base median(dec_train); % 训练期的判决值中位数随模型一起存 drift abs(median(dec_new) - base) / max(abs(base), eps); outR mean(dec_new model.R2); if drift 0.15 || outR 3*nu % 两个条件任一触发就重训训练集用旧的正常样本 新样本 model fastsvdd_train([Xn; Xnew], sigma, C); base median(fastsvdd_predict(model, [Xn; Xnew])); enddrift 0.15这个阈值是根据几组设备数据的漂移速度定的漂得太快说明工况变了漂得太慢说明模型在慢慢失准。3*nu是给球外比例留三倍余量避免正常波动频繁触发重训。两个条件用“或”连接而不是“与”是因为工况突变时往往只有一个信号先动等两个都满足可能已经漏检一批了。从那以后我每次上线前都强制把归一化参数、sigma、const 和训练期判决值中位数打进同一个文件少一个都不发版——4.1 那个坑我踩过一次排查了大半天才发现是均值没对齐代价太大。希望帮到你。本文还有配套的精品资源点击获取