One-Class SVM 异常检测:用高维边界圈出『不正常』的交易日
高斯类方法(马氏距离)默认『正常』只有一个中心,偏离中心越远越异常。但真实市场的正常状态往往是多峰的——平静区制和承压区制是两团各自正常的云。麻烦就藏在两团之间的『山谷』:那些既不像平静、也不像承压的交易日,恰恰是最危险的,而马氏距离会把山谷判成中心(AUC 仅 0.080,方向都反了)。One-Class SVM 用 RBF 核学一条非凸的支撑边界,把两团正常各自圈住、山谷留在外面:合成实验 1000 个正常日(两区制)+ 30 个山谷异常日,OC-SVM AUC 0.961、top-30 命中 33%,而马氏距离几乎为零。纯 numpy 实现带盒约束单纯形投影的对偶求解器,附 ν / γ 双参数敏感性曲线与五类真实陷阱(中阶)。
先说结论:马氏距离假设『正常』只有一个中心,偏离越远越异常;但市场的正常状态经常是多峰的——平静区制和承压区制是两团各自正常的点云,真正危险的日子藏在两团之间的『山谷』里。 山谷距离全局均值很近,马氏距离因此把它判成”最正常”,方向彻底反了。本文合成实验:1000 个正常交易日(分属两个区制)+ 30 个落在山谷里的异常日,马氏距离 AUC 只有 0.080(低于 0.5 意味着比随机还差、把异常排到了最不异常的一端),而 One-Class SVM 用 RBF 核学出一条非凸边界,AUC 0.961、top-30 命中率 33%。

一、问题:正常不止一个样子#
风控里最常见的多维异常检测是马氏距离:估计一个均值 和协方差 ,算每个点的 ,超过某个卡方阈值就报警。它的隐含假设很硬:正常样本服从单个椭圆高斯分布,越靠近中心越正常。
可市场偏偏不是这样。用两个因子(市场因子、波动因子)刻画交易日,你会看到至少两团:
- 平静区制:市场因子低、波动因子低,聚成一团;
- 承压区制:市场因子高、波动因子高,聚成另一团。
这两团各自都完全正常——它们是市场在两种状态下的常态。但如果某天落在了两团之间的山谷:市场因子中性、波动因子也中性,看起来”什么都不极端”,它其实极不正常——因为历史上几乎没有哪天真的停在这个不上不下的位置。这种日子往往对应区制切换的过渡态、流动性的短暂真空,恰恰是最该警惕的。
马氏距离在这里会犯致命错误:山谷离全局均值 最近, 最小,于是被判成”最正常”。在本文实验里,马氏 AUC 掉到 0.080——它不是没抓到异常,是把异常精准地排到了正常度最高的一端。
二、原理:学一条把『正常区域』包起来的边界#
One-Class SVM(Schölkopf 2001)换了个目标:不假设正常长什么形状,直接在特征空间里学一条边界,把绝大多数训练样本圈在里面,落在边界外的就是异常。
关键在于用 RBF 核 把数据映到高维再画边界。RBF 核的效果是”局部相似”——只有彼此靠近的点核值才高。于是学出来的支撑区域可以是非凸的、甚至分成好几块:两团正常各自被圈住,中间的山谷自然被排除在外。这正是它能干掉马氏距离的地方——马氏只会画一个椭圆,OC-SVM 能画出”两个圈 + 中间挖空”。
数学上它解一个对偶问题:
决策函数 ,边界阈值 由落在间隔上的支持向量确定; 判为异常。这里的 是核心旋钮:它同时是异常比例的上界和支持向量比例的下界——你告诉模型”大约有 5% 的训练日可以当异常丢掉”,它就据此定边界的松紧。
三、纯 numpy 实现#
对偶问题是一个带盒约束和单纯形约束的二次规划。工业界用 SMO,这里为了透明用投影梯度:每步沿 走一小步,再把 投影回可行域 。这个投影是本文最容易写错的地方——很多人先 clip 到 再除以和来凑 ,那会破坏盒约束(除完可能又超过 )。正确做法是对偏移量 做二分:找一个 使得 。
import numpy as np
def rbf(A, B, gamma):
a2 = (A ** 2).sum(1)[:, None]
b2 = (B ** 2).sum(1)[None, :]
return np.exp(-gamma * np.maximum(a2 + b2 - 2 * A @ B.T, 0))
def project_capped_simplex(v, C, total=1.0, iters=80):
"""投影到 {0<=a<=C, sum a = total}:对偏移量 tau 二分。"""
lo, hi = v.min() - C, v.max()
for _ in range(iters):
tau = 0.5 * (lo + hi)
if np.clip(v - tau, 0, C).sum() > total:
lo = tau
else:
hi = tau
return np.clip(v - 0.5 * (lo + hi), 0, C)
def fit_ocsvm(K, nu, n, iters=900):
C = 1.0 / (nu * n)
a = project_capped_simplex(np.full(n, C), C) # 可行初始点
lr = 1.0 / (np.linalg.norm(K, 2) + 1e-9) # 步长 ~ 1/Lipschitz
for _ in range(iters):
a = project_capped_simplex(a - lr * (K @ a), C)
f = K @ a
sv = (a > 1e-8) & (a < C - 1e-8) # 间隔上的支持向量
rho = np.median(f[sv]) if sv.sum() > 0 else np.quantile(f, nu)
return a, f, rhopython打分时 score = rho - f,越大越异常。构造数据时有个关键细节:每个区制要用截尾高斯(丢掉马氏距离大于某卡方阈值的远尾样本),否则区制自己的尾巴会比山谷还离群,把 top-k 精度污染掉。
def clipped_gaussian(mean, cov, m, chi2=6.0, rng=None):
mean = np.asarray(mean, float)
s = rng.multivariate_normal(mean, cov, int(m * 1.8))
d = np.einsum("ij,jk,ik->i", s - mean, np.linalg.inv(cov), s - mean)
return s[d < chi2][:m] # 只保留紧致核心,两团各成一个 compact blobpython四、结果:非凸支撑抓住山谷,高斯方法方向全反#
在 1000 个正常日(两区制各 500)+ 30 个山谷异常日上,、:
- OC-SVM:AUC 0.961,top-30 命中率 33%;
- 马氏距离:AUC 0.080,top-30 命中率 0%。

左图是马氏距离的分数:颜色最深(最异常)的是两团的外缘,而红圈标出的真实异常(山谷)反而是最浅的一片——它坐在两个簇心连线的正中央,离全局均值最近。右图是 OC-SVM 的分数:两团被各自的高相似度区域”照亮”成低分(正常),中间山谷因为跟任何一团都不够近,核相似度都低,分数被顶起来。这就是非凸支撑的价值——它允许”正常”由多块不连通的区域组成。
五、两个参数的脾气:ν 定松紧,γ 定形状#
控制边界松紧,直接映射成精确率/召回率的取舍:

从 0.02 抬到 0.2,召回率从 43% 一路爬到 100%(边界越松,圈进来的”外部”越多,异常越容易全被框住),但精确率始终在 14%~23% 徘徊——因为山谷异常和两团的边缘在分数上有重叠,放宽阈值抓全异常的同时也必然误报边缘正常日。这里没有免费午餐: 是你对”能容忍多少误报”的直接声明。
控制 RBF 核的宽度,也就是边界的形状精细度:

太小(0.1)时核太宽,所有点彼此都”相似”,边界糊成一个大椭圆,退化得跟马氏距离一样——AUC 直接掉到 0.0(连两团都分不开,山谷又被当中心)。 在 0.252.0 是甜区,AUC 稳在 0.920.95。 太大(8.0)时核太窄,每个训练点只跟自己相似,模型把每个正常点都圈成一座孤岛——严重过拟合,任何新点(哪怕正常)都落在所有孤岛之外被判异常,AUC 掉到 0.665。这是核方法的通病: 必须交叉验证,不能拍脑袋。
六、六类真实陷阱#
陷阱一:把 OC-SVM 分数当概率。 是到边界的带符号距离,不是”异常概率”。分数 0.3 不代表 30% 概率异常,也没有跨数据集可比的绝对阈值——同一个 在不同市场、不同特征缩放下切出的边界完全不同。
陷阱二:无监督就不用验证集。 OC-SVM 是无监督训练(不看标签),但 、 的选择本质是有监督调参。本文 从 0.1 到 8.0,AUC 从 0.0 到 0.95 再到 0.665——不用带标签的验证集去挑参数,你几乎必然选错。
陷阱三:训练集必须干净。 和孤立森林一样,OC-SVM 假设训练集里”绝大多数是正常”,靠 容忍一小撮污染。但如果污染比例远超 ,边界会被异常点自己撑大,把异常也圈进”正常”里。上线前要估计训练窗口的真实污染率。
陷阱四:特征缩放没做就直接喂。 RBF 核算的是欧氏距离,量纲大的特征会主导。成交量(单位百万手)和收益率(单位百分点)不标准化就进核,等于只用了成交量这一维。本文所有特征都做了 z-score,这不是可选项。
陷阱五:能检测不等于能预测。 抓出”今天异常”是事后描述,不是”明天会跌”的预测。异常日可能是崩盘当天,也可能是崩盘后的恐慌反弹——方向未知。把异常分数直接当择时信号做多/做空,是把描述性工具误用成预测性工具。
陷阱六:维度一高就失效。 RBF 核在高维会遭遇距离集中——所有点两两距离趋于相等,核值失去区分度。本文只用了 2 维演示,几十个因子直接上 OC-SVM 前,通常要先降维(PCA/流形学习)或做特征筛选,否则”高维边界”会退化成噪声。
小结#
One-Class SVM 的价值,不在于它比高斯方法”更强”,而在于它不假设正常是单峰的。当你的正常状态天然分成几团(多区制、多风格、多市场状态),马氏距离这类单中心方法会在”团与团之间”系统性失明,而 OC-SVM 的非凸核支撑恰好能把这些真空地带识别出来。代价是两个必须交叉验证的参数、对特征缩放的敏感、以及高维下的退化——它是一把需要仔细校准的手术刀,不是开箱即用的报警器。