简介:FastSVDD 是支持向量数据描述(SVDD)算法的一种高效 MATLAB 实现,面向从事异常检测、单类分类与故障诊断等方向的研究人员和工程实践者,帮助其在保持模型性能的前提下降低计算复杂度、提升训练与推理效率。资源包共 164 个文件,约 1012KB,以 75 个 .m 脚本为主体,涵盖算法主程序、数据加载与预处理、神经网络对比实验等模块;另有 49 张 png 结果图、20 个 txt 说明与数据文件、5 个 mat 数据文件,以及 tex、sty 等论文排版源文件,便于复现实验与撰写报告。已有 345 人学习下载。读者可据此获得可直接运行的 SVDD 优化实现、示例数据集与评估脚本,理解核心对象选取、核函数与参数调整等关键策略,并借助可视化结果快速验证模型在异常检测场景中的表现。
1. 从一次异常检测翻车说起:FastSVDD 到底解决什么问题
去年帮一个做工业质检的朋友调模型,产线上每天几万张图,正常样本堆成山,缺陷样本凑不出两百张。这种极端不平衡场景,One-Class 分类是天然选择,SVDD(支持向量数据描述)理论上很合适——用一个超球把正常样本包住,球外即异常。可真跑起来就傻眼了:标准 SVDD 要解一个带约束的二次规划,样本量一上去,训练时间直接按平方甚至立方涨,等它收敛,产线都换型了。后来翻到 FastSVDD 这个 MATLAB 实现,才算把这条路走通。它干的事很明确:把 SVDD 那个昂贵的优化过程做加速,让中小规模、高维特征的异常检测任务能在可接受时间内跑完。适合谁?手里有 MATLAB 环境、要做单类异常检测、又不想自己啃 SMO 推导的开发者——尤其是做设备故障诊断、图像缺陷筛查、日志异常识别这类“正常样本多、异常样本少”的活。关键词 MATLAB 在这里不是凑数,整个项目就是围绕 MATLAB 的矩阵运算和优化工具箱展开的,后面拆代码你会看到大量向量化写法。
2. 项目定位与技术选型:为什么是 MATLAB 而不是 Python
2.1 它到底是不是“又一个 SVM 封装”
先把这个误解掐掉。FastSVDD 不是调fitcsvm换个核函数了事,它实现的是 SVDD 本身:寻找球心 $a$ 和半径 $R$,使超球尽可能小地包住绝大多数训练样本,同时允许少量样本落在球外(用松弛变量控制)。标准形式的对偶问题是一个二次规划:
$$\min_{\alpha} \sum_i \alpha_i K(x_i, x_i) - \sum_{i,j} \alpha_i \alpha_j K(x_i, x_j)$$
约束是 $0 \le \alpha_i \le C$ 且 $\sum_i \alpha_i = 1$。这个 QP 的规模随样本数增长,稠密核矩阵一存,内存先炸。FastSVDD 的“Fast”就落在两处:一是对核矩阵计算做向量化,避免双重循环;二是对优化迭代做工作集裁剪,只对可能成为支持向量的样本反复求解,把每轮 QP 的规模压下来。这是典型的 SMO 类分解思想,只是用 MATLAB 写出来,读起来比 C++ 版本友好得多。
2.2 技术栈与依赖边界
项目本身是纯 MATLAB 脚本/函数集合,没有额外的 MEX 编译依赖,这点对新手很友好——拷过去就能跑。核心依赖是 MATLAB 自带的矩阵运算和quadprog(Optimization Toolbox)。如果你的 MATLAB 没装优化工具箱,那要么补装,要么把内部小 QP 换成自己写的解析解(两变量子问题其实有闭式解,后面 3.3 会讲)。常见做法是:小规模验证直接用quadprog,上产线前把子问题求解器替换掉,去掉工具箱依赖。
| 组成 | 作用 | 是否必需 |
|---|---|---|
| 核矩阵计算函数 | 向量化生成 Gram 矩阵 | 必需 |
| 工作集选择逻辑 | 挑出违反 KKT 条件的样本 | 必需 |
| 子 QP 求解 | 每轮解小规模二次规划 | 必需(可替换) |
| 参数搜索脚本 | 调sigma和C | 建议保留 |
| 可视化脚本 | 画决策边界/球外样本 | 可选 |
2.3 和标准 SVM 的差别,别混用
很多人拿fitcsvm的 one-class 模式来对标,这里有个坑:fitcsvm的 OneClass 是把原点当负类,学的是一个过原点的超平面,而 SVDD 学的是超球,两者在对偶形式上有联系但几何解释不同。FastSVDD 输出的是球心和半径,判定规则是“样本到球心距离是否大于 R”,这个语义在做故障阈值解释时更直观——你能直接说“这个样本离正常工况中心偏了多远”。选型时如果你的业务需要可解释的距离度量,SVDD 比 one-class SVM 更合适。
3. 核心模块拆解:从核矩阵到决策函数的完整链路
3.1 核矩阵的向量化计算
先看最容易被写慢的地方。朴素写法是双重for循环算 $K(x_i,x_j)=\exp(-\gamma|x_i-x_j|^2)$,样本上千就卡。FastSVDD 用的是 Gram 矩阵展开:
function K = rbf_kernel(X, Y, sigma) % X: n x d, Y: m x d, 返回 n x m 的 RBF 核矩阵 % sigma 为核宽,注意这里用的是 sigma 而非 gamma if nargin < 2 || isempty(Y) Y = X; end % 利用 ||x-y||^2 = ||x||^2 + ||y||^2 - 2x'y 展开,避免循环 xx = sum(X.^2, 2); % n x 1 yy = sum(Y.^2, 2)'; % 1 x m D2 = xx + yy - 2 * (X * Y');% n x m 的平方距离矩阵 D2(D2 < 0) = 0; % 数值误差可能导致微小负值,截断 K = exp(-D2 / (2 * sigma^2)); end逻辑说明:xx + yy - 2*X*Y'这一步把 $n \times m$ 次距离计算压成一次矩阵乘法,MATLAB 底层走 BLAS,速度差一个量级。参数上sigma是核宽,越大决策边界越平滑、越容易欠拟合;越小越容易把正常样本也判成异常。D2(D2<0)=0这行别省,浮点误差会让平方距离出现 -1e-12 这种值,exp之后虽然影响小,但在迭代里累积会干扰 KKT 判断。我一般会在调试时打印min(D2(:)),确认截断没吃掉真实信息。
3.2 工作集选择:谁该进这一轮优化
分解法的核心是每轮只挑一部分 $\alpha_i$ 更新。FastSVDD 的选择依据是 KKT 违反程度。对 SVDD 对偶问题,样本的梯度分量大致是:
$$g_i = K(x_i,x_i) - 2\sum_j \alpha_j K(x_i,x_j)$$
按 $\alpha_i$ 所处区间(0、边界 C、中间)判断它是否满足 KKT。违反最狠的样本优先拉进工作集。代码骨架大致是:
function [idx, viol] = select_working_set(alpha, K, C) % alpha: n x 1 当前解 % K: n x n 核矩阵 % C: 上界 n = size(K, 1); g = diag(K) - 2 * (K * alpha); % 梯度近似 viol = zeros(n, 1); for i = 1:n if alpha(i) <= 1e-8 viol(i) = max(0, -g(i)); % 下界处应 g>=0 elseif alpha(i) >= C - 1e-8 viol(i) = max(0, g(i)); % 上界处应 g<=0 else viol(i) = abs(g(i)); % 自由变量应 g=0 end end [~, order] = sort(viol, 'descend'); idx = order(1:min(10, n)); % 取前若干个 end逻辑说明:diag(K)取核矩阵对角元,对应 $K(x_i,x_i)$,RBF 核下恒为 1,但保留通用写法方便换核。viol的计算按 $\alpha$ 的三段区间分别处理,这是 KKT 条件的直接翻译。参数min(10,n)是工作集大小,太小收敛慢、太大每轮 QP 又变贵,实践中 5~20 之间调。注意这里用1e-8做边界容差,如果你的数据尺度大,容差要相应放大,否则会在边界附近反复震荡——这个坑我在 5.2 会细说。
3.3 子问题求解与quadprog的替换
每轮拿到工作集后,固定其余 $\alpha$,只对工作集内的变量解一个小 QP。项目默认调quadprog:
function alpha_ws = solve_subqp(K_ws, alpha_fixed, C) % K_ws: 工作集内样本的核矩阵块 % alpha_fixed: 非工作集样本的 alpha(视为常数) m = size(K_ws, 1); % 目标函数二次项 H 和一次项 f,对应 SVDD 对偶 H = 2 * K_ws; f = diag(K_ws) - 2 * K_ws * alpha_fixed; Aeq = ones(1, m); beq = 1 - sum(alpha_fixed); lb = zeros(m, 1); ub = C * ones(m, 1); options = optimoptions('quadprog', 'Display', 'off'); alpha_ws = quadprog(H, f, [], [], Aeq, beq, lb, ub, [], options); end逻辑说明:H = 2*K_ws来自对偶目标里的二次项系数,f里-2*K_ws*alpha_fixed是把非工作集样本的贡献挪到一次项。等式约束sum(alpha)=1在子问题里变成beq = 1 - sum(alpha_fixed),这个细节容易写错——很多人忘了减掉固定部分,导致每轮总和不守恒,最后半径算出来是错的。Display off关掉迭代输出,批量跑参数搜索时不然刷屏。如果要去掉工具箱依赖,两变量 SMO 子问题有闭式解:固定一个变量,另一个由等式约束直接推出,再裁剪到 $[0,C]$,十几行就能写完,这是上产线前的常规改造。
3.4 半径计算与决策函数
$\alpha$ 收敛后,支持向量是那些 $0<\alpha_i<C$ 的样本,半径取它们到球心距离的均值(或中位数,抗噪更好):
function [R, a] = compute_radius(alpha, X, K, sigma, C) % 支持向量筛选 sv = find(alpha > 1e-6 & alpha < C - 1e-6); if isempty(sv) sv = find(alpha > 1e-6); % 退化情况兜底 end % 球心表示为支持向量的核加权和 a_coef = alpha(sv); K_sv = rbf_kernel(X, X(sv, :), sigma); dist2 = 1 - 2 * (K_sv * a_coef) + a_coef' * K_sv(sv, :) * a_coef; R = sqrt(median(dist2(sv))); % 用中位数更稳 end逻辑说明:RBF 核下样本到球心的平方距离可写成 $1 - 2\sum_j \alpha_j K(x,x_j) + \sum_{i,j}\alpha_i\alpha_j K(x_i,x_j)$,代码里dist2就是这一项的向量化。用median而不是mean是我自己的习惯——支持向量里混进个别边界样本时,均值会被拉偏,中位数更抗造。判定新样本时,算它到球心距离,大于R即异常。这里sigma必须和训练时一致,传错就是玄学翻车现场。
4. 跑起来:环境准备、参数搜索与结果验证
4.1 最小可运行流程
拿到代码后别急着上自己的数据,先用项目自带的示例跑通。典型流程是:加载数据 → 标准化 → 划分训练/测试 → 设sigma和C→ 训练 → 算异常分数 → 评估。标准化这步别跳,RBF 核对特征尺度敏感,某个特征量纲大十倍,距离就被它主导了。我一般用 z-score:
% 最小示例:假设 X_train 为正常样本,X_test 含异常 X_train = zscore(X_train); mu = mean(X_train); sg = std(X_train); X_test = (X_test - mu) ./ sg; % 用训练集统计量,别用测试集自己的 sigma = 1.0; % 先给个经验值,后面网格搜 C = 0.1; % 异常比例上界的倒数关系,先粗调 model = fastsvdd_train(X_train, sigma, C); score = fastsvdd_score(model, X_test); pred = score > model.R;逻辑说明:zscore那行只对训练集做,测试集必须用训练集的mu和sg变换,这是数据泄漏的高发点——直接用zscore(X_test)会让评估结果虚高。C的物理含义是允许落在球外的样本比例上界,正常样本里如果有 5% 的噪声,C别设得比这还小,否则模型硬要把噪声也包进去,球被撑大,异常全漏。
4.2 参数搜索怎么搜才不白跑
sigma和C两个参数,网格搜是常规操作,但别盲目铺大网格。我的经验是先固定C(比如取正常样本噪声比例的倒数附近),在sigma上做对数扫描,看验证集上的 AUC 或 F1 曲线,找到拐点再细调C。项目里的搜索脚本大致长这样:
sigma_list = 2.^(-3:0.5:3); % 0.125 到 8 C_list = [0.01 0.05 0.1 0.2 0.5]; best = struct('f1', -inf); for s = sigma_list for c = C_list m = fastsvdd_train(X_train, s, c); sc = fastsvdd_score(m, X_val); f1 = evaluate_f1(sc > m.R, y_val); if f1 > best.f1 best = struct('f1', f1, 'sigma', s, 'C', c); end end end逻辑说明:2.^(-3:0.5:3)是对数步长,比线性网格更合理,因为核宽的影响是指数级的。验证集要单独留,别拿测试集调参,不然最后报的指标没法信。evaluate_f1是自写的评估函数,注意异常检测里正负样本极不平衡,F1 比准确率靠谱得多。搜完记得把最优参数和对应的混淆矩阵一起存下来,后面复现时省事。
4.3 结果怎么验证才算数
训练完看三个东西:支持向量占比、球外正常样本比例、异常样本的分数分布。支持向量占比太高(比如超过 40%),说明sigma太小或C太大,模型过拟合到噪声;球外正常样本比例应该接近你设定的噪声容忍度;异常样本的分数分布要和正常样本明显分开,如果重叠严重,先回去查标准化和sigma。我一般会画个分数直方图,正常和异常两组叠在一起看,分不开就是参数或特征的问题,别急着换模型。
5. 避坑与排查:五个真实踩过的坑
5.1 现象:训练不收敛,alpha在边界反复跳
原因:KKT 容差1e-8对当前数据尺度过严,浮点误差让边界样本的梯度判断来回翻转。解决:把容差按数据规模放大到1e-6甚至1e-5,并在工作集选择后加一个“连续两轮选同一批样本则强制换一批”的兜底逻辑。这个坑我在高维稀疏特征上踩过,容差调大后迭代次数直接降了一半。
5.2 现象:半径算出来是 NaN
原因:支持向量筛选条件alpha > 1e-6 & alpha < C-1e-6一个都没选中,sv为空,后面median对空数组操作返回 NaN。解决:加退化兜底(3.4 代码里那行if isempty(sv)),同时检查C是不是设得太小导致所有alpha都顶到上界。根因往往是C小于1/n,等式约束sum(alpha)=1根本没法满足。
5.3 现象:测试集指标很好,上线后误报爆炸
原因:标准化用了测试集自身的均值和方差,评估时分布被“对齐”了,线上真实数据没这个待遇。解决:训练阶段把mu、sg和模型一起存下来,推理时严格复用。这是血泪经验,第一次上线前没查,被产线反馈打脸。
5.4 现象:换一批数据后quadprog报“问题不可行”
原因:子问题里beq = 1 - sum(alpha_fixed)可能为负或超过m*C,等式约束和边界约束冲突。解决:在调quadprog前检查beq范围,越界时把工作集扩大或调整C。常见于工作集太小、固定部分已经把预算吃光的情况。
5.5 现象:同样参数两次运行结果不一致
原因:工作集选择里sort对相同viol值的排序不稳定,加上浮点误差,导致迭代路径不同。解决:排序时加第二关键字(比如样本索引)保证确定性,调试阶段固定随机种子。做对比实验时这个不解决,结论全是噪声。
6. 二次开发:把 FastSVDD 改成能上产线的样子
改造方向我按优先级排:第一,去掉quadprog依赖,用两变量 SMO 闭式解替换 3.3 的子问题求解,这样部署环境不用装优化工具箱,也方便后续转 C 或嵌入式。第二,把训练和推理拆成两个独立函数,模型存成结构体(含alpha、支持向量、R、sigma、标准化参数),推理端只依赖这个结构体,不碰训练代码。第三,加增量更新——产线数据是流式的,全量重训不现实,可以固定旧支持向量、只对新样本做一轮工作集优化,近似更新球心。
验证改造是否等价,我的做法是:同一份数据、同一组参数,改造前后对比支持向量集合的重合率和半径差值,重合率 95% 以上、半径差在 1% 内就算通过。下面是个对比脚本的骨架:
m1 = fastsvdd_train(X, sigma, C); % 原版 m2 = fastsvdd_train_smo(X, sigma, C); % 改造版 sv1 = find(m1.alpha > 1e-6); sv2 = find(m2.alpha > 1e-6); overlap = numel(intersect(sv1, sv2)) / numel(union(sv1, sv2)); fprintf('SV 重合率 %.3f, 半径差 %.4f\n', overlap, abs(m1.R - m2.R));逻辑说明:intersect/union算 Jaccard 重合率,比单纯比数量靠谱。半径差用绝对值,因为改造版可能略大或略小。如果重合率低,先查子问题求解的容差是否和原版一致,再查工作集大小是否差太多——这两个是最常见的偏差来源。
还有个技巧:调参阶段把每次实验的sigma、C、支持向量数、半径、验证集 F1 写进一个 CSV,跑几十组之后回头看,能快速定位参数敏感区。我现在的习惯是,任何一次参数改动前先跑一遍基线并存档,改完必须和基线对比,不然改着改着就不知道哪步引入的退化。从那以后我每次动核心迭代逻辑,都强制走一遍“基线存档 → 改动 → 重合率对比”的流程,省了太多回头找 bug 的时间。希望帮到你。
本文还有配套的精品资源,点击获取