简介:单类支持向量机(One-Class SVM)是一种典型无监督异常检测算法,本资源为基于MATLAB的完整实现源码,面向算法学习者、数据挖掘人员及需要构建数据边界模型的工程实践者。代码针对MATLAB 6.5环境编写,仅需正样本即可完成模型训练,特别适合类别不平衡或缺乏负样本的检测任务。压缩包内共1个.m主程序文件,包体仅2KB,虽精简却完整覆盖数据预处理、SVM参数设置、模型训练、决策分类与结果可视化等核心流程,便于直接阅读和移植。通过该源码可深入理解单类SVM的间隔最大化原理、核函数映射方式以及异常判决逻辑,配合少量调试即可应用于实际数据场景。目前已有110人学习,适合具备一定MATLAB基础、希望快速掌握One-Class SVM代码实现与调参技巧的读者。
1. Main_SVM_One_Class 这类脚本,到底救的是什么场景
凌晨两点,值班手机弹出设备告警:温度曲线比历史正常区间高了 15%。你翻遍监控日志,发现过去一个月根本没出现过故障样本,手里全是"正常数据",异常样本要么攒不够、要么标记成本高到离谱——这时候二分类 SVM 完全无从下手。One-Class SVM 就是为这种"单类样本"场景设计的:它只学正常样本的分布,把偏离这个分布的点判为异常。Main_SVM_One_Class这类以 R 脚本为主体的方案,就是把这条路走通的最小落地单位。用 e1071 或 kernlab 包就能复现,不依赖复杂框架,适合做设备预警、业务风控、质量检测的工程师直接照着改。
2. 单类SVM的原理与选型:它和二分类SVM不是一回事
2.1 二分类SVM的优化目标,离开负类就不成立
标准二分类 SVM 的原始问题长这样:
minimize 1/2 ||w||² + C Σ ξᵢ s.t. yᵢ(w·xᵢ + b) ≥ 1 - ξᵢ
这里 yᵢ 必须是 +1 或 -1,每个样本的约束都依赖"对面那个类别"存在。当你手里只有正常样本时,这个约束没有落点:没有负类样本,超平面往哪边推都没有意义。这也是为什么你没法用普通svm(formula, data, type="C-classification")硬train——分类器会把所有样本都归成一类,而且边界毫无区分度。
硬间隔 SVM 的梯度下降在解原问题时,每一步迭代要用到全部样本的拉格朗日乘子 αᵢ 和类别标签 yᵢ。一旦 yᵢ 只有一种取值,梯度里"推动边界"的那一项就消失了,剩下只是把所有样本往里收。换句话说,二分类 SVM 学的是两个分布之间的分界面,单类 SVM 学的是"这个分布本身长什么样"。Single-class 问题必须换一套目标函数。
2.2 One-Class SVM 的决策函数与"包住"数据的直觉
One-Class SVM(通常也叫 Support Vector Data Description 的变体)的做法是:把坐标原点当成唯一的负类样本,然后寻找一个超平面,让所有正常样本尽量落在超平面一侧,同时最大化超平面到原点的距离。训练完成后,决策函数是:
f(x) = sign( Σ αᵢ K(xᵢ, x) - ρ )
其中 xᵢ 是支持向量,K 是核函数,ρ 是训练时学到的阈值。f(x) 大于 0 判为正常,小于 0 判为异常。这里有一个直觉:RBF 高斯核相当于在特征空间里把每个正常样本都变成一个小山包,模型学习到的边界就是把这些山包连起来的外轮廓。
在 R 的 e1071 包里,训练完成后模型对象里保存了几个关键字段:fit$SV是支持向量,fit$coefs是对应的 αᵢ,fit$rho就是那个 ρ。这几个字段在调参诊断时比 predict 结果有用得多:如果fit$SV的数量接近全部训练样本,说明 gamma 设得过大,边界在"逐点包围";如果支持向量极少,边界又可能过平滑,漏掉局部异常。
2.3 e1071、kernlab、隔离森林:三条路线怎么选
常见做法是先在 e1071 里跑通,因为它内部调的是 LIBSVM,默认高斯核,速度快,接口也最简单。kernlab 的ksvm(type="one-svc")提供更多核函数选项,比如弦距离核、拉普拉斯核,适合特征本身有特殊结构的场景。隔离森林(IsolationForest)是另一条路线,它不依赖核矩阵,高维数据下内存更友好,但结果不如 SVM 直观,也不容易拿到类似决策值的连续分数。
| 方案 | 适合场景 | 主要局限 |
|---|---|---|
| e1071::svm one-class | 数据量几万以内、特征维度几十到几百、需要决策值 | 高斯核矩阵 O(n²),样本太大内存爆炸 |
| kernlab::ksvm one-svc | 需要自定义核、非线性结构明显 | 调参项比 e1071 多,上手慢 |
| 隔离森林 | 十万级以上样本、高维稀疏特征 | 分数解释性弱,难以固定决策边界 |
实际项目里我的选择标准很粗暴:先拿 2000 条正常样本试 e1071,如果决策值分布靠谱,就直接沿用;如果误报率压不下来,再换 kernlab 试不同核;数据量一上到十万,就别跟核矩阵硬扛,直接隔离森林。
3. 用 R 复现 Main_SVM_One_Class 的最小可运行版本
3.1 解包、装包与数据准备
假设你拿到一个名为Main_SVM_One_Class.rar的压缩包,里面是一个标准的 R 脚本工程。Windows 上直接用 WinRAR 或 7-Zip 解压,Linux 服务器上用命令行处理:
# 在 Linux 上解压 rar 包,需要先装 p7zip sudo apt-get install p7zip-full 7z x Main_SVM_One_Class.rar -o./main_svm_project cd ./main_svm_project解压后通常能看到主脚本main_svm_one_class.R和数据集文件。接着安装 R 依赖包:
# 安装 e1071,LiblineaR 在某些脚本方案里也会被用到 install.packages("e1071") install.packages("kernlab")命令说明:7z x里的x表示解压到指定目录,-o指定输出路径。R 端只需要 e1071 就能跑通单类 SVM,kernlab 是备选方案。这里不建议用install.packages("e1071", repos="...")指定任意镜像源,直接用默认 CRAN 即可。
3.2 最小数据集上的训练与预测
不需要真实业务数据,先用模拟数据把流程跑通。下面的代码生成 200 个正常样本,两个特征都服从正态分布,然后训练一个 RBF 核的 One-Class SVM:
library(e1071) set.seed(42) n <- 200 normal_data <- data.frame( feature1 = rnorm(n, mean = 50, sd = 8), feature2 = rnorm(n, mean = 0.8, sd = 0.15) ) # 训练单类SVM fit <- svm(~ feature1 + feature2, data = normal_data, type = "one-classification", kernel = "radial", nu = 0.05, gamma = 0.5) summary(fit)代码逻辑:svm()的第一个参数用公式接口,~ feature1 + feature2表示用这两列做特征;type = "one-classification"是关键,告诉 e1071 走单类模式;nu是异常比例估计;gamma是 RBF 核的带宽参数。set.seed(42)保证模拟数据可复现。
训练完成后做预测,并把决策值取出来:
# 生成一些测试点:大部分正常,少量明显偏离 test_data <- data.frame( feature1 = c(rnorm(50, mean = 50, sd = 8), 70, 30), feature2 = c(rnorm(50, mean = 0.8, sd = 0.15), 2.0, 0.1) ) pred <- predict(fit, test_data) table(pred) # 取决策值:正数表示正常,负数表示异常 dec_vals <- attr(predict(fit, test_data, decision.values = TRUE), "decision.values") head(dec_vals)这里predict()返回的是因子向量,TRUE 表示样本被判定为正常,FALSE 表示异常。加decision.values = TRUE后,predict 结果带一个decision.values属性,是连续的有符号距离,正数越大越"正常",负数越大越"异常"。实际做告警时,不应该只看 TRUE/FALSE,而应该看决策值的分布,后面第 4 章会展开讲。
3.3 从模型对象里读诊断信息
训练完不要急着部署,先看三个东西:
# 支持向量数量 nrow(fit$SV) # 决策阈值 rho fit$rho # 支持向量在原始数据中的索引 fit$indexfit$SV的行数如果占了训练集的 60% 以上,说明 gamma 设太大,边界在逐点包围,泛化能力差。fit$rho是决策阈值,决策值大于-rho判正常还是大于 0 判正常取决于 e1071 内部符号约定,最稳妥的做法是直接用 predict 结果,不要手动套公式。fit$index指示哪些原始样本成了支持向量,可以用它回看训练数据里哪些点位于边界附近,这些点往往是调参时需要重点检查的对象。
4. 单类SVM必调参数:nu、gamma、kernel 的取值边界
4.1 nu:异常比例的估计值,不是正则化强度
nu 是 One-Class SVM 里最容易被误解的参数。它不是 C 那样的正则化强度,而是异常比例的上界。理论上,nu 同时限制了支持向量比例的下界和上界:把 nu 设为 0.05,模型会倾向于让训练集中至多 5% 的点变成异常。注意这是训练集内部的异常比例,不是泛化到生产后的异常比例。
实际调参时,nu 应该反映你对数据质量的判断。如果业务上知道传感器正常运行时大概有 1% 的抖动数据,nu 就从 0.02 试到 0.05。如果数据是清洗过的、公认干净,nu 可以取 0.01 以下,但此时边界会把所有点都包进去,新到的异常可能也包进去了。经验值是:nu 设成预期污染物率的 1 到 2 倍,给边界留一点余量,然后用带标签的验证集去收窄。
4.2 gamma:核带宽决定边界的"脾气"
gamma 是 RBF 核的参数,控制单个样本的影响力半径。gamma 很小的时候,核函数值衰减慢,所有样本都互相"看得见",边界非常平滑,结果是只抓住整体分布,抓不住局部小异常。gamma 很大的时候,每个样本只影响自己周围一小片区域,边界跟着每个点走,训练集里稍微偏一点的正常点都被当成支持向量,模型几乎是在背数据。
用前面模拟数据做实验,gamma 取 0.01 时边界近似一个椭圆;取 0.5 时边界开始出现局部凹陷;取 5 时边界会包住单个样本。生产环境里我不会直接调 gamma,而是先固定 nu,然后按数量级扫:2^-5 到 2^5。gamma 和特征缩放是强耦合的,见第 5 章避坑部分。
4.3 单类SVM的网格搜索:用正常验证集而不是普通交叉验证
One-Class SVM 不能直接套用分类问题的交叉验证,因为训练集里没有类别标签,每一折之间的"分类准确率"没有意义。我在项目里常用的是两段式验证:拿一段连续时间内的正常数据训练,再用另一段正常数据测"通过率"——正常情况下模型应该让 95% 以上的正常验证集样本落在边界内。同时人工注入一些已知异常点,测"捕获率"。
# 简单的网格搜索示意 nu_seq <- c(0.01, 0.03, 0.05, 0.1) gamma_seq <- c(0.01, 0.05, 0.2, 0.5, 1.0) for (nu_val in nu_seq) { for (gamma_val in gamma_seq) { fit <- svm(~ feature1 + feature2, data = train_normal, type = "one-classification", kernel = "radial", nu = nu_val, gamma = gamma_val) pred <- predict(fit, valid_normal) pass_rate <- mean(pred == TRUE) # 再算注入异常后的召回率 if (pass_rate > 0.95) { cat("nu=", nu_val, "gamma=", gamma_val, "pass_rate=", pass_rate, "\n") } } }这里的逻辑是:先保证正常样本不被误杀,再考虑异常捕获。如果 pass_rate 远低于 1 - nu,说明模型在训练集上已经过拟合,验证集上的正常点进不了边界。网格搜索的结果不是选一个准确率最高的点,而是选一个"正常通过率约 0.95 且注入异常召回率尽量高"的点,这就是安全阈值的概念。
4.4 和 lasso、CNN 放在一起看:什么时候单类SVM不是最优解
Lasso 和 SVM 在特征预处理阶段可以协作:Lasso 做特征筛选,把无关维度压成零,再用 One-Class SVM 处理筛选后的特征。高维稀疏数据里,lasso 的 L1 正则优先于 RBF 核,因为核矩阵在稀疏特征上容易过拟合。CNN 这类深度方法在图像和时序信号上更合适,但至少需要上万条正常样本去拟合分布,工程成本高;而单类 SVM 在几百到几千样本时就能给出稳定边界,这是它的核心价值。SVM 的梯度下降和 CNN 的反向传播在优化目标上完全是两套逻辑,前者是凸优化,后者是非凸,所以你没法直接把 CNN 的早停、学习率调度经验套到 SVM 上来。
5. 避坑:One-Class SVM 真实场景里最容易翻车的五个地方
5.1 数据没缩放,gamma 等于白设
现象:训练集 30 维特征,其中一维是温度,数值在 400 到 800 之间,另一维是振动幅度,数值在 0.01 到 0.05 之间。网格搜索每次跑出的最优 gamma 都不一样,换一段数据后结果完全变样。
原因:RBF 核内部算的是欧氏距离的平方。温度维的差异轻松达到几百,振动维的差异才零点几,距离计算完全被温度主导。gamma 调的其实是"多大距离算相似",在未缩放的数据上,这个距离没有意义。
解决:训练前对所有特征做标准化。e1071 的svm()默认scale = TRUE,但当你传入数据框时,它做的是列级别的中心化和标准化,对类别型特征反而有害。我一般手动先 scale:
scaled_train <- as.data.frame(scale(train_normal)) scaled_test <- as.data.frame(scale(test_normal, center = attr(scaled_train, "scaled:center"), scale = attr(scaled_train, "scaled:scale")))注意测试集必须用训练集的均值和标准差,不能用测试集自己的,否则特征分布被重新拉了一遍,边界形状就变了。这是很多第一次部署的人踩的坑。
5.2 预测结果全是正常,异常一个都抓不出来
现象:模型上线跑了一个星期,所有样本都判为正常,连手动注入的明显异常也没报警。查日志发现 decision value 全在 0 以上。
原因:最常见的原因是 nu 设得太小,比如 0.001,边界把所有样本都包了进去。另一个原因是训练数据里本身就包含了少量异常,像传感器偶发漂移、人工录入错误,这些点把边界往外撑,正常数据反而被包得更宽松。
解决:先看fit$SV数量。如果支持向量数接近训练集数量,说明 gamma 过大或 nu 过大;如果 support vector 特别少但 decision value 全部正得离谱,说明 nu 太小。把 nu 调到 0.05 再试,同时用table(predict(fit, train_normal))看训练集上的内分类别比例,应该有接近 nu 比例的点被判为异常才对。
5.3 nu 设得过分乐观,把好样本误杀
现象:业务方说数据全干净,把 nu 设成 0.01,结果生产环境每天误报几百条,值班同事开始骂模型。
原因:nu 不是你想让模型报多少异常就报多少,它是模型对训练集内部异常比例的假设。如果训练集里有 2% 的脏数据,nu 设 0.01,模型为了满足约束,会把这 2% 的脏数据也当作"正常分布的一部分",边界被污染,后续正常样本反而被推出去。这就是为什么"数据干净"这种判断不能靠拍脑袋。
解决:对训练集先做鲁棒的离群点剔除,用中位数加减 3 倍 MAD(绝对中位差)把明显离群点挑出来,再训练。nu 设置成剔除后残余污染物率的 2 倍左右。MAD 方法对少量离群点不敏感,比均值标准差靠谱:
mad_filter <- function(x) { med <- median(x, na.rm = TRUE) mad_val <- mad(x, na.rm = TRUE) x[abs(x - med) > 3 * mad_val] <- NA x }把过滤后的数据拿去训练,能明显降低误杀。
5.4 把 decision value 直接当概率用,阈值乱定
现象:有人拿到 decision value 后,看到 -0.1 就当成异常,看到 +0.1 就当成正常,阈值调来调去,调得毫无根据。
原因:One-Class SVM 的决策值是有符号距离,不是概率。不同批次的模型,决策值分布完全不同,同一个阈值在这个模型上是 95% 分位,换到另一个模型就变成了 80% 分位,绝对值没有可比性。
解决:在验证集上算决策值的经验分布,取 5% 分位数作为告警阈值。比如验证集有 2000 个正常样本,排序后取第 100 个样本的决策值作为阈值。这样模型输出的不是"是/否",而是"这个点比训练集中 95% 的正常点都更偏离"。如果业务上要求 0 到 1 的分数,可以在分位数基础上做等渗回归校准,但校准集必须另留一批数据,不能用训练集。
5.5 样本量上了十万,高斯核跑不动
现象:数据从几万涨到几十万,训练时间从几分钟变成几小时,内存占用直接冲上 40G,最后进程被杀。
原因:RBF 核需要计算两两样本之间的核矩阵,复杂度是 O(n²)。n 到十万时,核矩阵有百亿个元素,e1071 虽然内部用稀疏存储,但支持向量的数量也会随数据量增长,最终内存耗尽。
解决:这时候不要硬扛高斯核。可以先用 Mini-batch K-means 对正常样本做聚类,每类取中心点附近的一部分样本作为代表,把训练集压到一到两万,再用 One-Class SVM 训练边界。或者直接换隔离森林,它不需要核矩阵,能处理百万级样本。工业界常见做法是白天用模型预测,凌晨用当天新增的正常样本滚动更新模型,控制训练集规模在十万以内。
6. 进阶:把单类SVM从实验脚本推向在线检测
在线场景和离线实验最大的区别是:阈值不能是静态的,模型也不能长期不更新。我现在的做法是每个时间窗口结束时,把当天的正常样本追加到一个滑动集合里,窗口长度取 7 天,每天用最新的 7 天数据重训一次模型。重训前必须做两件事:一是对特征做滚动标准化,二是用第 5 章的 MAD 方法过滤掉窗口内的离群样本,防止它们污染新边界。
决策值校准方面,我会在模型每次更新时,同步计算最近 7 天正常样本的决策值分位数,并把告警阈值设在 5% 分位处。这样即使模型边界本身发生了变化,告警阈值也会跟随分布移动,不会出现"模型换了一版,晚上告警全红了"的问题。验证手段上,我习惯留出一份人工复核过的"黄金样本集",大概 200 到 500 条,每次重训后用这批样本跑一遍通过率,发现通过率低于 95% 就立即回溯训练数据的特征分布,而不是直接调参数。
最后说一个我自己吃过亏的习惯:不要相信"训练集绝对干净"这句话,先跑一次summary(fit),看看支持向量里有没有明显不该出现的高杠杆点;每一条生产环境的告警,我都会保存当时的特征快照和决策值,方便两周之后复盘模型边界有没有在悄悄漂移。单类 SVM 真正困难的地方不在训练,而在你有多信任那些"正常样本"——它们一旦不干净,后面所有参数调整都是白费功夫。希望这些踩坑记录能帮你少走几段弯路。
本文还有配套的精品资源,点击获取