简介:一套完整的MATLAB环境下的SVDD(支持向量数据描述)实现资源包,面向需要开展异常检测、单类别分类研究或工程应用的研究者与学习者,解决“仅有正常样本时如何构建分类边界”的核心问题。压缩包共包含6个文件,其中4个.m源文件分别承担数据生成、二次规划求解、模型训练与边界绘图等关键环节,2个.mat数据文件用于快速验证算法效果,整体仅12KB,轻量易用。已有278人学习下载,足见其实用价值。资源内提供可直接运行的训练与预测脚本,基于MATLAB内置优化函数求解最小超球边界,无需额外安装统计与机器学习工具箱;同时配套示例数据集与可视化代码,从样本生成、模型训练到边界输出均有清晰实现。通过研读代码,读者既能掌握SVDD的数学原理与实现细节,也能直接迁移到故障检测、入侵识别等实际场景中,是一份兼顾教学与实战的优质参考。
1. SVDD是什么:在MATLAB复现前先认识的单类边界模型
如果你搜过“svdd matlab”,多半已经下载过某个svdd_matlab.rar,解压出一堆.m脚本和dd_tools文件夹,结果在 MATLAB R2023b 上跑不通。原因往往不是代码写错,而是 SVDD 这个东西本身需要你自己确认“目标类在哪一侧、边界怎么算、核参数怎么给”。SVDD 全称是支持向量数据描述,它解决的是单类分类问题:只给一类样本,学出一个尽量紧的包围区域,落在区域外的新样本判为异常。它和普通二分类 SVM 的思路正好相反——SVM 找分隔面,SVDD 找最小包围超球,适合故障检测、图像异常定位、轴承振动监测这类正常样本易得、故障样本稀缺的场景。本文按“数学推导 → MATLAB 实现 → 参数调优 → 踩坑”的顺序,把一套能跑的 SVDD 代码从头到尾拆给你。
2. SVDD 数学表达与 MATLAB 化:核矩阵和半径计算是核心
2.1 从“最小包围超球”到对偶问题的三步推导
原始问题要求一个中心为a、半径为R的超球,使所有目标样本都落在球内,同时半径尽可能小。为了容忍噪声,引入松弛变量ξ_i和惩罚系数C:
min R^2 + C * sum(ξ_i) s.t. ||φ(x_i) - a||^2 <= R^2 + ξ_i, ξ_i >= 0φ(x_i)是把原始特征映射到高维空间的核变换。写成拉格朗日对偶后,优化变量从a、R变成每个样本的权重α_i,得到与 SVM 非常相似的对偶形式:
max sum(α_i * K(x_i, x_i)) - sum_i sum_j (α_i * α_j * K(x_i, x_j)) s.t. sum(α_i) = 1, 0 <= α_i <= CK(x_i, x_j)是核函数。对偶形式的意义在于:我们不需要显式计算高维映射φ,只需要两两样本的核函数值,这正好对应 MATLAB 里的核矩阵。
决策阶段,新样本z到球心的距离平方为:
D(z) = K(z, z) - 2 * sum_i (α_i * K(z, x_i)) + sum_i sum_j (α_i * α_j * K(x_i, x_j))当D(z) <= R^2,样本在球内,判为目标类;否则判为异常。注意这里的R^2不是直接解出来的,而是取任意一个满足0 < α_s < C的支持向量x_s,把x_s代入上式得到。边界上的支持向量对应的α通常落在开区间(0, C)内,这就是“自由支持向量”。
2.2 RBF 核在 MATLAB 中的约定与“对角线为 1”性质
实际工程里用最多的是高斯径向基核,两种常见写法:
K(x, y) = exp(-||x - y||^2 / (2 * σ^2)) K(x, y) = exp(-γ * ||x - y||^2)两式等价,γ = 1/(2σ^2)。在写 MATLAB 时,我建议统一用γ这个参数,因为pdist2直接返回的是平方欧氏距离,写成exp(-gamma * D)最顺手,少一次除法换算。
| 参数 | 含义 | 常见取值范围 | 对边界的影响 |
|---|---|---|---|
gamma | RBF 核宽度倒数 | 0.001 ~ 10 | 越大边界越复杂,越小越接近球面 |
C | 松弛惩罚上界 | 0.01 ~ 1 | C 越小允许更多样本落在球外 |
K(x_i,x_i) | 核函数对角线 | 恒为 1 | 决定对偶问题中 f 向量的写法 |
RBF 核有个容易被忽略的性质:K(x_i, x_i) = 1对所有样本恒成立。这会让对偶目标函数的第一项sum(α_i * K(x_i, x_i)) = sum(α_i) = 1变成常数,于是在 MATLAB 的quadprog标准型中,线性项可以借这个性质化简。如果你换用多项式核或线性核,对角线不再是 1,后面代码里的f向量就要改成-diag(K),这是新手最容易踩的坑。
3. 不依赖别人代码包:用 quadprog 从零搭一套 SVDD
3.1 生成一组二维测试数据:内圈正常、外圈异常
为了直观验证边界形状,这里用mvnrnd生成一个偏置的高斯分布作为目标类,再用均匀分布在外围撒异常点作为测试。二维数据的优势是能直接把边界画出来看,方便验证数学推导有没有写错。
rng(2024); % 目标类:均值 [1,1],协方差 [0.6 0.3; 0.3 0.8],共 200 个点 mu = [1, 1]; sigma = [0.6, 0.3; 0.3, 0.8]; X_target = mvnrnd(mu, sigma, 200); % 异常点:在 [-3, 5] 范围均匀撒 50 个 X_outlier = -3 + (5 - (-3)) * rand(50, 2); % 把两类合并画出来,目标类蓝色,异常点红色 figure; plot(X_target(:,1), X_target(:,2), 'b.'); hold on; plot(X_outlier(:,1), X_outlier(:,2), 'r.'); axis equal;异常点在这里只用于测试,不参与模型训练。SVDD 的训练输入只有X_target,异常点放在旁边能看到边界是否把它们排除在外。
3.2 写一个 RBF 核函数:用 pdist2 一次算出核矩阵
核矩阵在 MATLAB 中用pdist2配合'squaredeuclidean'参数最简洁。先写一个独立函数方便复用:
function K = rbf_kernel(X, Y, gamma) % X: n1 x d,Y: n2 x d,返回 n1 x n2 核矩阵 D = pdist2(X, Y, 'squaredeuclidean'); K = exp(-gamma * D); endpdist2(X, Y, 'squaredeuclidean')返回的是二维矩阵,第 i 行第 j 列存的是||X(i,:) - Y(j,:)||^2,经过exp(-gamma * D)后得到核矩阵。这里不用自己写双重循环,数据量大时pdist2底层有优化,速度比 for 循环快一个数量级。
3.3 把对偶问题改写成 quadprog 标准型
MATLAB 的quadprog解二次规划的标准型是:
min 0.5 * x' * H * x + f' * x s.t. Aeq * x = beq, lb <= x <= ub把 SVDD 对偶问题代入。我们的目标是最大化sum(α_i * K(x_i,x_i)) - sum_i sum_j α_i α_j K(x_i,x_j),即最小化-sum(α_i*K(x_i,x_i)) + α' * K * α。第一项中K(x_i,x_i)=1,且sum(α_i)=1是常量,但为了代码通用(万一换核),保留成矩阵形式:
H = 2 * K % 对应 0.5 * x' * H * x f = -diag(K) % 对应 f' * x Aeq = ones(1, n) % sum(α_i) = 1 beq = 1 lb = zeros(n, 1) ub = C * ones(n, 1)把 3.1 和 3.2 的函数串起来跑:
n = size(X_target, 1); gamma = 0.5; K = rbf_kernel(X_target, X_target, gamma); H = 2 * K; f = -diag(K); Aeq = ones(1, n); beq = 1; lb = zeros(n, 1); C = 0.1; ub = C * ones(n, 1); opts = optimoptions('quadprog', 'Display', 'off', 'Algorithm', 'interior-point-convex'); alpha = quadprog(H, f, [], [], Aeq, beq, lb, ub, [], opts);quadprog返回的alpha是 n 维列向量,大多数分量趋近于 0,非零分量对应的样本就是支持向量。interior-point-convex算法要求 H 半正定,RBF 核矩阵天然满足这个条件。若看到“Hessian is not symmetric”的警告,用H = (H + H') / 2强制对称即可。
3.4 计算半径和决策值,画边界
半径R2从自由支持向量上取。自由支持向量定义为0 < alpha < C的样本,用find加逻辑索引取出来:
% 取自由支持向量(alpha 严格在 (0, C) 内) idx_sv = find(alpha > 1e-6 & alpha < C - 1e-6); if isempty(idx_sv) error('未找到自由支持向量,请放宽 C 或调整 gamma'); end alpha_sv = alpha(idx_sv); X_sv = X_target(idx_sv, :); K_sv = rbf_kernel(X_sv, X_sv, gamma); % 半径:把某个支持向量代入决策函数 term1 = 1; % K(x_s, x_s) = 1 term2 = -2 * K_sv * alpha_sv; term3 = alpha_sv' * K_sv * alpha_sv; R2 = term1 + term2 + term3;画决策边界时,把(x,y)平面切网格,对每个网格点算D(z),用contour画出D == R2的等高线:
% 生成网格 [xg, yg] = meshgrid(linspace(-3, 5, 200), linspace(-3, 5, 200)); grid_pts = [xg(:), yg(:)]; % 对所有网格点计算决策值 Kg = rbf_kernel(grid_pts, X_sv, gamma); D_grid = 1 - 2 * (Kg * alpha_sv) + term3; D_grid = reshape(D_grid, size(xg)); % 画异常检测边界:D == R2 的等高线 figure; contour(xg, yg, D_grid, [R2, R2], 'k-', 'LineWidth', 1.5); hold on; plot(X_target(:,1), X_target(:,2), 'b.'); plot(X_outlier(:,1), X_outlier(:,2), 'rx'); axis equal;D_grid的表达式里第一项K(z,z)对 RBF 恒等于 1;term3是常数,只算一次。如果边界把大部分蓝色目标点包住、同时把红色异常点排除在外,说明核心逻辑正确。若边界明显外扩把红点也包进去,需要调小C或增大gamma。
4. SVDD 参数标定:gamma 和 C 的网格搜索与交叉验证
4.1 用 5 折交叉验证给 gamma、C 打分
SVDD 没有标签,交叉验证的标签只能来自“目标类 vs 人为模拟的异常”。常见做法是把目标类分成 5 折,轮流拿其中 4 折训练、1 折测试,同时从训练折内随机采样一些点加均匀噪声当作伪异常,计算 F1 分数。这里给出核心逻辑:
function f1 = svdd_cv(X, gamma, C, nfolds) n = size(X, 1); idx = crossvalind('Kfold', n, nfolds); f1_list = zeros(nfolds, 1); for k = 1:nfolds train_idx = (idx ~= k); test_idx = (idx == k); X_train = X(train_idx, :); X_test = X(test_idx, :); % 伪异常:在特征范围内均匀采样,数量与测试集目标类相同 lo = min(X_train) - 1; hi = max(X_train) + 1; X_fake_anom = lo + (hi - lo) .* rand(size(X_test)); % —— 核心:训练 SVDD —— n_tr = size(X_train, 1); Ktr = rbf_kernel(X_train, X_train, gamma); H = 2 * Ktr; fvec = -diag(Ktr); opts = optimoptions('quadprog', 'Display', 'off'); alpha = quadprog(H, fvec, [], [], ones(1, n_tr), 1, ... zeros(n_tr, 1), C*ones(n_tr, 1), [], opts); % 半径和决策 sv_idx = find(alpha > 1e-6 & alpha < C - 1e-6); if isempty(sv_idx), continue; end alpha_sv = alpha(sv_idx); X_sv = X_train(sv_idx, :); R2 = 1 - 2 * (rbf_kernel(X_sv, X_sv, gamma) * alpha_sv) ... + alpha_sv' * rbf_kernel(X_sv, X_sv, gamma) * alpha_sv; % 测试:目标类应判为正常,伪异常应判为异常 Ktest = rbf_kernel(X_test, X_sv, gamma); D_target = 1 - 2 * (Ktest * alpha_sv) + ... alpha_sv' * rbf_kernel(X_sv, X_sv, gamma) * alpha_sv; pred_target = D_target <= R2; Kanom = rbf_kernel(X_fake_anom, X_sv, gamma); D_anom = 1 - 2 * (Kanom * alpha_sv) + ... alpha_sv' * rbf_kernel(X_sv, X_sv, gamma) * alpha_sv; pred_anom = D_anom > R2; % 混淆矩阵算 F1 tp = sum(pred_target); fp = sum(~pred_anom); fn = sum(~pred_target); precision = tp / (tp + fp + eps); recall = tp / (tp + fn + eps); f1_list(k) = 2 * precision * recall / (precision + recall + eps); end f1 = mean(f1_list); end这段代码的代价是每个折都要跑一次quadprog,nfold * grid_size次求解对 200 个样本来说很快,但如果样本量到几千,就要考虑降低网格密度或用下面 4.3 的启发式初筛。
4.2 网格搜索怎么写:给一组可复现的参数组合
gamma的数量级跨度大,网格要按对数均匀取值。C理论上不超过 1,常用[0.01 0.05 0.1 0.2 0.5],但实际中数据集噪声多时,C取 0.1 以下的表现通常更好。
gamma_list = [0.01, 0.05, 0.1, 0.5, 1, 2, 5]; C_list = [0.02, 0.05, 0.1, 0.2, 0.5]; best_f1 = 0; best_param = [0, 0]; results = zeros(length(gamma_list) * length(C_list), 3); row = 1; for gi = 1:length(gamma_list) for ci = 1:length(C_list) f1 = svdd_cv(X_target, gamma_list(gi), C_list(ci), 5); results(row, :) = [gamma_list(gi), C_list(ci), f1]; if f1 > best_f1 best_f1 = f1; best_param = [gamma_list(gi), C_list(ci)]; end row = row + 1; end end fprintf('最优 gamma=%.2f, C=%.2f, F1=%.3f\n', ... best_param(1), best_param(2), best_f1);搜索结果建议配合 3.4 的可视化一起判断:F1 高但边界形状裂成几块,说明gamma太大过拟合;F1 合格但边界明显包进了大片空白区域,说明C太大,超球半径虚胖。
4.3 两个能快速定位参数范围的启发式
网格搜索之前,先算两个数能把搜索范围压缩一个数量级。第一个是训练样本两两距离的中位数med_dist,gamma可以先用1 / (2 * med_dist^2)作为中值点,网格在它左右各扫 5 倍。第二个是支持向量比例经验值:当C = 1/n时,SVDD 接近硬边界,几乎所有样本都变成支持向量;当C = 0.1时,支持向量比例通常会降到 20% 左右,这是一个合理的起点。
% 距离中位数启发式 D_all = pdist2(X_target, X_target, 'squaredeuclidean'); D_all(D_all == 0) = []; % 去掉对角线上的 0 med_dist = sqrt(median(D_all)); gamma_mid = 1 / (2 * med_dist^2); fprintf('gamma 建议从 %.3f 附近开始扫描\n', gamma_mid);这个启发式背后的直觉是:RBF 核的衰减长度应该和数据点之间的典型间距同量级。gamma若比这个值大很多,核矩阵接近单位矩阵,每个点都孤立,边界变成一圈尖刺;若小很多,核矩阵接近全 1 矩阵,退化成线性超球。
5. SVDD 落地中的三个常见错误与验证技巧
5.1 错误一:特征没归一化,高方差维度主导半径
SVDD 的距离是各维度平方和,若一个特征的量纲是另一个的 100 倍,低方差维度对边界的贡献会被完全淹没。常见做法是 z-score 归一化到零均值单位方差,注意归一化参数只能从训练集估计:
% 训练集估计均值和方差 mu_train = mean(X_target, 1); sig_train = std(X_target, 0, 1); X_norm = (X_target - mu_train) ./ sig_train; % 测试/预测时用同一组参数 X_new_norm = (X_new - mu_train) ./ sig_train;归一化之后再跑 4.2 的网格搜索,否则前面调出来的gamma和C换一组数据就不能复用。
5.2 错误二:quadprog 的 H 矩阵写成 K 而不是 2K
二次规划中x' * H * x自带 1/2 系数,四步转换中的H必须是2K。如果误写成H = K,最优解会被缩放为正确值的两倍,约束sum(alpha)=1会直接把所有alpha压到边界上,导致自由支持向量消失。判断方法:跑完看alpha是否大量等于C,如果 90% 以上的 alpha 顶着上界,基本可以断定 H 或 f 的系数写错了。
5.3 错误三:全量核矩阵吃爆内存,忘了分段处理
n 个样本的核矩阵是 n×n 的 double 矩阵,n=1 万时占 800MB,n=5 万时需要 20GB。几个缓解方法按推荐度排序:一是先用 k-means 把目标类聚类成几百个簇中心,用簇中心训练 SVDD,再用簇半径做局部修正;二是改用fitcsvm的 One-Class 变体,它底层用 SMO 而不是全量二次规划,内存随迭代次数增长而不是 n²:
% MATLAB 自带的单类 SVM,适合 n > 5000 的场景 mdl = fitcsvm(X_norm, ones(size(X_norm, 1), 1), ... 'KernelFunction', 'rbf', 'KernelScale', 1/gamma_mid, ... 'Standardize', false, 'OutlierFraction', 0.05); % 预测返回 1 表示在边界内,-1 表示异常 label = predict(mdl, X_new_norm);5.4 验证边界是否靠谱:用一个留出集检查支持向量比例
训练完成后画一张“支持向量比例 vs C”的曲线图能快速验证模型有没有过拟合。C 从 0.01 递增到 0.5,记录每组的支持向量数量和留出集上的异常检出率,支持向量比例应该随 C 增大而上升且曲线平滑。如果曲线出现跳变,说明数据里存在离群簇,需要回到归一化或数据清洗步骤。最后把训练好的alpha、支持向量和R2保存成.mat文件,预测时只加载这些量,不需要重新跑quadprog,这才是 SVDD 工程落地的标准形态。
本文还有配套的精品资源,点击获取