AI 工程基础体系 · 第 51/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。

异常检测:统计方法、Isolation Forest、One-Class 与阈值治理

异常检测(anomaly detection)是在缺少完整异常标签,或异常样本极少的条件下,识别与正常数据生成机制显著不同的样本、事件或时间片。它并不等同于“分类器预测异常类别”:分类需要学习 正常/异常 的判别边界,而异常检测通常首先学习正常数据的分布、密度、几何结构或重构规律。

在生产系统中,异常检测的输出也不应直接等同于告警。一个完整链路通常是:

flowchart LR
    A[原始事件] --> B[特征与时间窗口]
    B --> C[异常模型]
    C --> D[异常分数]
    D --> E[阈值与策略]
    E --> F[告警/拦截/降级]
    F --> G[人工处置与反馈]
    G --> H[标签、阈值和模型评估]
    H --> C

模型回答“这个样本有多不正常”,阈值治理回答“多不正常才触发什么动作”。二者必须分开,否则模型分数的变化会被误认为业务风险的变化。


一、先明确异常检测问题

设观测样本为 xRdx\in\mathbb{R}^d,正常数据分布为 Pnormal(x)P_{\text{normal}}(x)。理想情况下,异常是满足

Pnormal(x) 很小P_{\text{normal}}(x) \text{ 很小}

的样本。但这个定义有三个现实限制:

  1. 正常分布通常未知;
  2. 特征可能有不同量纲、相关性和时间依赖;
  3. “异常”是业务语义,不一定是统计上的低概率。

例如,一笔金额为 100 万元的交易可能统计上罕见,但对企业客户来说完全正常;一次金额只有 10 元的交易,如果在极短时间内来自大量不同设备,则可能是欺诈。

因此需要区分三种任务。

1. 离群点检测

训练数据本身可能混有异常点,目标是找出其中的离群点。这属于 outlier detection。训练集不是纯净正常数据,模型可能被异常污染。

2. 新颖性检测

训练阶段假设输入数据主要或完全来自正常分布,部署后判断新样本是否偏离该分布。这属于 novelty detection。例如,使用过去 30 天的健康设备指标训练模型,判断今天的新指标是否异常。

3. 监督分类

如果已经有质量较高、覆盖充分且及时的异常标签,应优先考虑二分类、代价敏感分类或排序模型。异常检测仍可作为未知异常发现手段,但不应替代有标签问题上的监督学习。

scikit-learn 中的 IsolationForestLocalOutlierFactorOneClassSVM 等方法都同时涉及离群点和新颖性检测,但训练数据假设与调用方式不同。尤其是 LocalOutlierFactor 的新颖性检测模式需要显式设置 novelty=True,不能把训练阶段的 fit_predict 语义直接套到新数据上。


二、统计方法:从单变量阈值到多变量距离

统计方法的优势是可解释、成本低,适合作为基线、数据质量检查和第一层防线。它们的缺点是通常需要较强的分布假设,或者对维度、相关性、时间结构比较敏感。

2.1 Z-score:均值和标准差下的偏离程度

对单变量 XX,给定均值 μ\mu 和标准差 σ\sigma,标准化分数为

z=xμσz=\frac{x-\mu}{\sigma}

其中:

  • xx 是当前观测;
  • μ\mu 是参考正常样本的均值;
  • σ\sigma 是参考正常样本的标准差;
  • z|z| 越大,表示观测离均值越远。

若假设 XN(μ,σ2)X\sim N(\mu,\sigma^2),则常见规则是:

z>3标记为异常|z|>3 \Rightarrow \text{标记为异常}

这不是数学定律,而是经验阈值。标准正态分布下,双侧区间 [3,3][-3,3] 的概率约为 99.73%,所以在独立、稳定且确实近似正态的情况下,单个观测超过 3 个标准差的概率约为 0.27%。

完整算例

假设某接口在正常时段的响应时间为:

[100,110,90,105,95][100, 110, 90, 105, 95]

均值为:

μ=100+110+90+105+955=100\mu=\frac{100+110+90+105+95}{5}=100

若使用总体标准差:

σ=(0)2+102+(10)2+52+(5)25=507.071\sigma=\sqrt{\frac{(0)^2+10^2+(-10)^2+5^2+(-5)^2}{5}} =\sqrt{50}\approx 7.071

新观测 x=125x=125 的 Z-score 为:

z=1251007.0713.536z=\frac{125-100}{7.071}\approx 3.536

因此在 z>3|z|>3 的规则下会被标记。

但如果新观测为 x=80x=80,则:

z=801007.0712.828z=\frac{80-100}{7.071}\approx -2.828

它虽然低于均值很多,却不会被这个阈值标记。是否需要检测低延迟异常,取决于业务:低延迟可能代表缓存命中,也可能代表请求绕过了关键处理。

Z-score 的反例

考虑数据:

[10,10,10,10,100][10,10,10,10,100]

均值为 28,标准差约为 36。对 100 计算:

z=1002836=2z=\frac{100-28}{36}=2

它可能不会超过 3,但从数据结构看,100 明显是极端值。异常点本身抬高了均值和标准差,使 Z-score 失去敏感性,这叫做污染导致的掩蔽效应

2.2 Robust Z-score:用中位数和 MAD 抵抗污染

更稳健的做法是使用中位数 mm 和中位数绝对偏差(MAD):

MAD=median(xim)\operatorname{MAD}=\operatorname{median}(|x_i-m|)

在近似正态分布下,MAD 与标准差的关系近似为:

σ1.4826MAD\sigma\approx 1.4826\cdot\operatorname{MAD}

于是稳健 Z-score 可写为:

zrobust=xm1.4826MADz_{\text{robust}} =\frac{x-m}{1.4826\cdot\operatorname{MAD}}

仍以:

[10,10,10,10,100][10,10,10,10,100]

为例,中位数 m=10m=10,所有偏差为:

[0,0,0,0,90][0,0,0,0,90]

MAD 为 0。此时公式无法计算,且这不是简单地把除数换成一个很小的数就能正确解决的问题。工程上应:

  • 对近乎常数的特征单独处理;
  • 若业务允许,使用一个明确的测量误差下限;
  • 或直接判断“偏离常数基线超过业务容差”。

这说明稳健统计也有边界:当正常数据没有可估计的离散程度时,异常判断必须依赖业务容差,而不是伪造统计尺度。

2.3 分位数阈值:不要求正态分布

若只关心高值异常,可以用正常参考数据的 qq 分位数作为阈值:

x>T,T=Q0.995(X)x>T,\qquad T=Q_{0.995}(X)

分位数方法不要求数据服从正态分布,适合延迟、金额、流量等右偏变量。但它的含义是“参考样本中约有 0.5% 高于阈值”,并不自动意味着这 0.5% 都是业务异常。

分位数估计还受到样本量影响。若每天只有 100 个样本,估计 99.9% 分位数几乎没有足够数据支撑;阈值应考虑置信区间、跨天聚合或采用极值理论,而不是机械地指定更多小数位。

2.4 多变量 Mahalanobis 距离

单变量检测无法表示变量之间的相关性。设正常数据均值向量为 μ\mu,协方差矩阵为 Σ\Sigma,样本为 xx,Mahalanobis 平方距离为:

D2(x)=(xμ)Σ1(xμ)D^2(x)=(x-\mu)^\top\Sigma^{-1}(x-\mu)

它衡量的是沿着协方差结构标准化后的距离,而不是普通欧氏距离。

完整数值例子

假设二维正常数据的均值和协方差为:

μ=[00],Σ=[10.90.91]\mu= \begin{bmatrix} 0\\0 \end{bmatrix}, \qquad \Sigma= \begin{bmatrix} 1&0.9\\ 0.9&1 \end{bmatrix}

考虑样本:

x=[22]x= \begin{bmatrix} 2\\-2 \end{bmatrix}

协方差矩阵的逆为:

Σ1=110.92[10.90.91][5.2634.7374.7375.263]\Sigma^{-1} =\frac{1}{1-0.9^2} \begin{bmatrix} 1&-0.9\\ -0.9&1 \end{bmatrix} \approx \begin{bmatrix} 5.263&-4.737\\ -4.737&5.263 \end{bmatrix}

因此:

D2=xΣ1x=4(5.263)+4(5.263)+2(2)(2)(4.737)76D^2=x^\top\Sigma^{-1}x =4(5.263)+4(5.263)+2(2)(-2)(-4.737) \approx 76

虽然 xx 到原点的欧氏距离只有 8\sqrt{8},但正常变量高度正相关,正常点通常位于 x1x2x_1\approx x_2 的方向;(2,2)(2,-2) 偏离了这一相关结构,所以 Mahalanobis 距离很大。

反例:协方差矩阵不稳定

当特征数 dd 接近或超过正常样本数 nn 时,样本协方差矩阵可能不可逆;强共线性也会造成数值不稳定。此时直接求逆会失败或放大噪声。可选方法包括:

  • PCA 降维后在低维空间计算;
  • 使用收缩协方差估计;
  • 使用稳健协方差估计;
  • 移除重复或近似线性相关的特征。

如果使用 χd2\chi^2_d 分布给 D2D^2 设阈值,还额外假设正常数据近似多元正态。这个分布假设不成立时,阈值应使用正常验证集分位数校准。


三、Isolation Forest:通过隔离难度定义异常

Isolation Forest(孤立森林)不直接估计概率密度,而是观察一个样本需要多少次随机切分才能被单独隔离。直觉是:稀疏区域中的样本更容易被切出来,因此路径更短;密集区域中的样本需要更多切分,路径更长。

3.1 单棵隔离树如何工作

对一个包含多个样本的节点,算法重复:

  1. 随机选择一个特征;
  2. 在该特征的最小值和最大值之间随机选择切分点;
  3. 将数据分成左右子节点;
  4. 对目标样本所在的子节点继续切分;
  5. 直到样本被单独隔离,或达到限制条件。

设样本在一棵树中的路径长度为 h(x)h(x),对多棵树取平均:

E[h(x)]=1ti=1thi(x)E[h(x)]=\frac{1}{t}\sum_{i=1}^{t}h_i(x)

其中 tt 是树的数量。平均路径越短,越可能异常。

当节点因为达到树高限制或只剩多个样本而提前停止时,需要对未完全隔离的节点进行校正。常见实现使用:

c(n)=2H(n1)2(n1)nc(n)=2H(n-1)-\frac{2(n-1)}{n}

其中 nn 是该节点中的样本数,H(k)H(k) 是调和数:

H(k)=i=1k1iH(k)=\sum_{i=1}^{k}\frac{1}{i}

c(n)c(n) 表示在随机二叉搜索树中,包含 nn 个样本时的平均路径长度。它防止“因为树提前停止”而把不同节点错误地视为相同异常程度。

经典 Isolation Forest 的归一化形式可写成:

s(x)=2E[h(x)]c(ψ)s(x)=2^{-\frac{E[h(x)]}{c(\psi)}}

其中 ψ\psi 是每棵树抽取的子样本量。因为 E[h(x)]E[h(x)] 越短,s(x)s(x) 越接近 1,表示越容易被隔离。

需要注意:不同库可能对分数做单调变换或改变符号。使用分数时必须以具体 API 文档为准,不能把论文中的 s(x)s(x)、库中的 score_samplesdecision_function 混为一谈。

3.2 一个小型隔离过程

考虑一维正常样本:

[10,11,12,13,14,15][10,11,12,13,14,15]

待判断样本为 x=100x=100

第一次随机切分点若为 50,则:

  • 左节点:[10,11,12,13,14,15][10,11,12,13,14,15]
  • 右节点:[100][100]

目标样本一次切分就被隔离,路径很短。

若待判断样本为 x=13x=13,随机切分点可能把数据分成:

[10,11,12][13,14,15][10,11,12]\quad|\quad[13,14,15]

之后还需要继续切分,才能把 13 单独分离。大量随机树平均后,13 的路径明显长于 100。

3.3 Isolation Forest 的优势和反例

它适合:

  • 特征维度中等或较高;
  • 不希望先拟合复杂概率分布;
  • 异常是少数且与正常数据存在空间分离;
  • 需要较低的推理成本。

但它不是“任何少数样本都能发现”的算法。

反例一:异常形成大而密集的群体

正常数据有 10,000 个,异常数据有 5,000 个,异常群体自身很密集且占据一个完整区域。异常点未必比正常边缘点更容易被隔离,Isolation Forest 可能把它们视为普通簇。

反例二:异常只存在于特征组合关系中

如果异常是“两个特征之间的精确关系被破坏”,但每个单独特征的边际范围都正常,随机轴向切分可能需要很多树和较深路径才能识别。此时显式构造比值、残差、差分或使用能表达关系的模型更有效。

反例三:时间结构被忽略

把一整天不同时间段的数据混在一起训练,可能将夜间低流量视为异常;实际问题是季节性和时间上下文,而不是孤立点。应先按时间窗口构造基线,或把周期特征、滚动统计量纳入输入。

3.4 scikit-learn 中的分数语义

下面示例使用 Isolation Forest 对二维合成数据建模:

import numpy as np
from sklearn.ensemble import IsolationForest
from sklearn.model_selection import train_test_split
from sklearn.metrics import precision_recall_curve, average_precision_score

rng = np.random.default_rng(7)

# 训练数据:主要是正常样本
normal = rng.normal(loc=0.0, scale=1.0, size=(2000, 2))
X_train, X_cal = train_test_split(normal, test_size=0.3, random_state=7)

# 评估数据:正常样本 + 人工构造异常
X_test_normal = rng.normal(0.0, 1.0, size=(1000, 2))
X_test_anomaly = rng.uniform(6.0, 8.0, size=(30, 2))
X_test = np.vstack([X_test_normal, X_test_anomaly])
y_test = np.r_[np.zeros(len(X_test_normal)), np.ones(len(X_test_anomaly))]

model = IsolationForest(
    n_estimators=300,
    max_samples="auto",
    contamination="auto",
    random_state=7,
    n_jobs=-1,
)
model.fit(X_train)

# 在 scikit-learn 中:
# decision_function 越小越异常;负值通常表示低于模型内部 offset
decision = model.decision_function(X_test)
score = -decision  # 为了让“越大越异常”更直观

ap = average_precision_score(y_test, score)
print(f"average precision = {ap:.3f}")

# 不依赖模型内部 contamination 阈值,
# 在独立校准集上取异常分数的高分位数
cal_score = -model.decision_function(X_cal)
threshold = np.quantile(cal_score, 0.99)

pred = (score >= threshold).astype(int)
print("threshold =", threshold)
print("detected anomalies =", pred.sum())

这个示例有几个关键点:

  • X_train 用于拟合结构;
  • X_cal 用于确定阈值,避免用同一批训练数据同时拟合和评估;
  • X_test 只用于评估;
  • average_precision 衡量排序质量,不依赖某一个阈值;
  • 阈值 0.99 表示校准集最高 1% 被当作候选异常,不代表真实异常率必然是 1%。

在 scikit-learn 的常见实现中,decision_function 越低越异常,predict 返回正常 1、异常 -1contamination 会影响内部 offset_ 和由 predict 使用的分界;contamination="auto" 的具体分界语义不应被理解成“自动知道真实异常率”。如果业务有标签或明确告警预算,通常应在独立校准集上治理阈值,而不是把 contamination 当作业务规则的替代品。


四、One-Class:只学习“正常类”的边界

“One-Class”不是单一算法,而是一类只用正常样本学习支持区域或边界的方法。最常见的是 One-Class SVM,也包括支持向量数据描述、密度估计、自编码器等。

4.1 One-Class SVM 的优化目标

给定主要由正常样本组成的训练集 x1,,xnx_1,\ldots,x_n,One-Class SVM 希望在特征空间中找到一个区域,使大多数正常样本落在区域内,同时避免区域无限扩大。

其经典形式之一为:

minw,ρ,ξ12w2+1νni=1nξiρ\min_{w,\rho,\xi} \frac{1}{2}\|w\|^2 +\frac{1}{\nu n}\sum_{i=1}^{n}\xi_i-\rho

约束为:

wϕ(xi)ρξi,ξi0w^\top\phi(x_i)\ge \rho-\xi_i,\qquad \xi_i\ge 0

其中:

  • ϕ(x)\phi(x) 是把输入映射到特征空间的函数;
  • ww 定义边界方向;
  • ρ\rho 定义边界位置;
  • ξi\xi_i 是允许样本违反边界的松弛量;
  • ν(0,1]\nu\in(0,1] 控制边界松弛程度。

对核函数 K(xi,xj)=ϕ(xi),ϕ(xj)K(x_i,x_j)=\langle\phi(x_i),\phi(x_j)\rangle,决策函数通常形如:

f(x)=i=1nαiK(xi,x)ρf(x)=\sum_{i=1}^{n}\alpha_i K(x_i,x)-\rho

f(x)0f(x)\ge 0 时判为正常,当 f(x)<0f(x)<0 时判为异常。

在 scikit-learn 中,OneClassSVM.predict 对正常样本返回 1,异常返回 -1decision_function 越低越异常。

4.2 参数 ν\nuγ\gamma 的含义

在 RBF 核中:

K(x,z)=exp(γxz2)K(x,z)=\exp(-\gamma\|x-z\|^2)

  • γ\gamma 越大,单个样本的影响范围越小,边界更曲折,容易过拟合;
  • γ\gamma 越小,影响范围越大,边界更平滑,可能漏掉局部结构;
  • ν\nu 是训练目标中的控制参数,常被理解为对训练误报比例和支持向量比例的上界/下界关系,但它不是部署数据真实异常率的可靠估计。

由于 RBF 核依赖距离,未缩放特征会造成严重问题。若一个特征取值范围为 [0, 1],另一个特征为 [0, 1_000_000],后者会几乎完全支配 xz2\|x-z\|^2

4.3 可运行示例:标准化和新颖性检测

import numpy as np
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import OneClassSVM

rng = np.random.default_rng(3)

# 训练数据应尽量是正常样本
X_normal = rng.normal(size=(1000, 3))

detector = make_pipeline(
    StandardScaler(),
    OneClassSVM(
        kernel="rbf",
        gamma="scale",
        nu=0.02,
    ),
)
detector.fit(X_normal)

X_new = np.array([
    [0.1, -0.2, 0.3],  # 接近正常区域
    [6.0, 6.0, 6.0],   # 明显偏离
])

labels = detector.predict(X_new)
scores = detector.decision_function(X_new)

print("labels:", labels)
print("decision scores:", scores)

预期现象是第一行更可能返回 1 且分数较高,第二行更可能返回 -1 且分数较低。但不能把具体数值硬编码为固定预期:核参数、随机数据和 scikit-learn 版本都会影响边界。

4.4 One-Class SVM 的边界

One-Class SVM 对以下情况比较敏感:

  • 训练数据包含大量异常;
  • 高维稀疏数据中距离退化;
  • 特征未缩放;
  • 数据分布持续漂移;
  • 样本量很大,核方法的训练和存储成本过高。

它适合学习复杂的非线性正常边界,但“边界复杂”不等于“边界正确”。高 γ\gamma 可能把训练样本附近包成许多小岛,部署时遇到轻微噪声就大量误报。必须用时间切分的验证集检查边界,而不能只看训练集上的 predict 结果。

4.5 与 Isolation Forest 的根本区别

两者都可以只使用正常样本,但机制不同:

  • Isolation Forest 依赖随机切分后的隔离路径;
  • One-Class SVM 通过核空间中的边界优化定义支持区域;
  • Isolation Forest 通常对尺度不如 RBF 核敏感,但特征范围和异常结构仍会影响随机切分;
  • One-Class SVM 明确依赖距离和核参数,标准化几乎是必要的;
  • One-Class SVM 在大样本上的核计算成本可能明显高于树模型。

没有一种方法天然拥有“异常真值”。选择依据应是正常数据形状、样本规模、延迟预算、可解释性和反馈周期。


五、深度学习与生成式 AI 中的异常分数

当输入是图像、长序列、文本或高维嵌入时,手工统计量可能不足。深度模型常见的异常分数有三类。

5.1 重构误差

自编码器学习:

xz=fθ(x)x^=gϕ(z)x\rightarrow z=f_\theta(x)\rightarrow \hat{x}=g_\phi(z)

异常分数可定义为:

s(x)=xx^22s(x)=\|x-\hat{x}\|_2^2

对多变量时间序列,还可以使用按特征加权的误差:

s(x)=j=1dwj(xjx^j)2s(x)=\sum_{j=1}^{d}w_j(x_j-\hat{x}_j)^2

其中 wjw_j 应根据单位、业务损失或正常波动尺度设定。

反例是“强大的自编码器把异常也重构得很好”。如果训练数据包含异常,或者模型容量足够大,重构误差可能无法区分异常。重构模型必须配合纯净训练集、掩码预测、预测误差或其他约束。

5.2 预测误差

对时间序列,使用过去窗口 xtk:t1x_{t-k:t-1} 预测下一时刻:

x^t=fθ(xtk:t1)\hat{x}_t=f_\theta(x_{t-k:t-1})

异常分数为:

st=(xt,x^t)s_t=\ell(x_t,\hat{x}_t)

预测误差能表达季节性和时间上下文。例如周一上午的流量不能用周日凌晨的固定均值判断。代价是需要正确处理缺失值、延迟、窗口边界和概念漂移。

5.3 生成式 AI 和嵌入空间

对文本、图像或多模态对象,常先得到嵌入 zz,再在嵌入空间进行:

  • k 近邻距离;
  • 类中心或 Mahalanobis 距离;
  • Isolation Forest;
  • 密度估计;
  • 聚类外点检测。

这检测的是“语义或表示空间中的新颖性”,不是安全意义上的事实错误。例如,一段看似罕见的提示词可能只是新主题,不一定是恶意输入;一段生成文本与历史语料距离很近,也不代表它事实正确。

生成式 AI 生产系统还需要检测:

  • 输入提示注入或越权意图;
  • 输出中的敏感信息;
  • 工具调用参数是否超出权限;
  • 生成内容的格式和事实校验失败;
  • token、延迟和费用异常。

这些问题往往需要规则、分类器、权限策略和统计检测组合完成。异常模型不能替代授权校验:一个“统计上正常”的请求仍可能没有权限。


六、阈值不是模型参数,而是治理对象

模型通常输出连续分数 s(x)s(x)。阈值 τ\tau 把分数转成动作:

a(x)={异常,s(x)τ正常,s(x)<τa(x)= \begin{cases} \text{异常}, & s(x)\ge \tau\\ \text{正常}, & s(x)<\tau \end{cases}

但生产系统往往不只有一个动作。例如:

s<τobserve:仅记录τobserves<τalert:采样或观察τalerts<τblock:告警、人工审核sτblock:阻断或降级\begin{aligned} s<\tau_{\text{observe}} &: \text{仅记录}\\ \tau_{\text{observe}}\le s<\tau_{\text{alert}} &: \text{采样或观察}\\ \tau_{\text{alert}}\le s<\tau_{\text{block}} &: \text{告警、人工审核}\\ s\ge\tau_{\text{block}} &: \text{阻断或降级} \end{aligned}

阈值选择本质上是代价优化。设误报成本为 CFPC_{\text{FP}},漏报成本为 CFNC_{\text{FN}},则期望代价可写成:

R(τ)=CFPP(y^=1,y=0)+CFNP(y^=0,y=1)R(\tau)=C_{\text{FP}}P(\hat y=1,y=0) +C_{\text{FN}}P(\hat y=0,y=1)

如果还有人工审核容量 BB,则需要增加约束:

E[审核量(τ)]BE[\text{审核量}(\tau)]\le B

因此,阈值不能只由“模型分数超过多少”决定,还取决于告警接收能力、业务损失、处置时间和动作可逆性。

6.1 无标签时的阈值

在没有可靠异常标签时,常见方案是对正常校准集取高分位数:

τ=Q1α(s(Xnormal))\tau=Q_{1-\alpha}(s(X_{\text{normal}}))

这里 α\alpha 是允许的候选异常比例。它只能控制参考正常分布下的触发比例,不能保证真实误报率,因为:

  • 校准集可能被污染;
  • 部署分布可能漂移;
  • 样本之间可能相关;
  • 多次监控会放大偶发事件。

例如每分钟检查一次,单次误报概率即使只有 0.1%,一天也有 1440 次机会;“单次看起来很低”的误报率可能仍造成频繁告警。

6.2 有标签时的阈值

有标签时,应在时间上晚于训练集的验证集上计算多个阈值的:

  • precision、recall、F1;
  • 误报数、漏报数;
  • 每小时告警量;
  • 每个业务事件的检测率;
  • 平均提前量;
  • 高风险样本的召回率。

异常通常极不平衡,ROC-AUC 可能看起来很好,但在真实低基准率下 precision 很差。PR-AUC、固定告警预算下的召回率和事件级指标更有意义。

点级评估与事件级评估也要分开。一次持续 30 分钟的故障可能产生 30 个异常点,但业务只需要一次告警;如果只计算点级 precision,会错误地惩罚合理的持续告警策略。

6.3 动态阈值和漂移

季节性系统应使用上下文条件阈值:

τt=Q0.995(s小时,星期,业务状态)\tau_t=Q_{0.995}(s\mid \text{小时},\text{星期},\text{业务状态})

但动态阈值不能无条件跟随当前数据,否则故障期间的高分会进入基线,造成阈值污染。常见保护措施是:

  • 只用已确认正常的数据更新基线;
  • 发生大规模告警时冻结阈值;
  • 对基线更新设置最大步长;
  • 保留固定历史窗口;
  • 记录每次阈值变更的原因和审批人。

七、阈值治理的状态、并发和故障路径

阈值配置应被视为带版本的策略对象,而不是散落在代码中的常量。一个最小配置可包含:

{
  "detector": "payment_iforest_v3",
  "feature_schema_version": "payment_features_12",
  "score_direction": "higher_is_more_anomalous",
  "thresholds": {
    "observe": 0.42,
    "alert": 0.68,
    "block": 0.91
  },
  "effective_from": "2025-01-01T00:00:00Z",
  "expires_at": "2025-02-01T00:00:00Z",
  "owner": "risk-platform",
  "reason": "calibrated_on_rolling_30d",
  "approved_by": "risk-oncall"
}

必须明确分数方向,否则一个模型“越小越异常”、另一个模型“越大越异常”会导致策略反转。

一次线上判定的关键路径可以表示为:

sequenceDiagram
    participant E as 事件流
    participant F as 特征服务
    participant M as 模型服务
    participant P as 策略存储
    participant A as 动作执行器
    participant L as 日志与反馈

    E->>F: 写入事件/读取窗口
    F->>M: 发送特征和schema版本
    M->>P: 读取当前生效阈值版本
    P-->>M: 返回分数方向与阈值
    M->>M: 计算异常分数
    M->>A: 返回分数、等级、模型版本
    A->>L: 记录判定与动作
    L-->>P: 审批后的阈值更新

这里有几个容易被忽略的故障点:

  1. 模型版本和阈值版本不匹配
    新模型的分数尺度改变后继续使用旧阈值,会造成告警爆发或全部沉默。模型发布必须绑定兼容的阈值版本。

  2. 特征 schema 变化
    训练时的列顺序、缺失值处理、缩放器和线上不一致,会产生看似正常但实际错误的分数。应校验 schema 哈希、特征缺失率和数值范围。

  3. 阈值存储不可用
    阈值读取失败时不能随意使用 0 或上一次未确认的配置。高风险拦截场景需要明确 fail-open 或 fail-closed;告警场景则可能选择降级为仅记录。

  4. 并发更新丢失
    两个审批人同时修改阈值时,应使用版本号或条件写入,例如“只有当前版本仍为 17 时才能更新为 18”,避免后写入覆盖先写入。

  5. 重复告警
    流处理重试可能重复消费同一事件。告警动作需要幂等键,例如 entity_id + anomaly_window + policy_version,并通过去重窗口避免重复通知。

  6. 时间乱序和延迟数据
    按事件时间计算窗口时,必须规定允许迟到时间;过迟数据是补算、忽略还是生成修正事件,不能由实现细节决定。


八、滞回、连续点和告警去抖

单点阈值容易抖动。设告警阈值为 τon\tau_{\text{on}},恢复阈值为较低的 τoff\tau_{\text{off}}

τoff<τon\tau_{\text{off}}<\tau_{\text{on}}

状态转移为:

  • NORMALstτons_t\ge\tau_{\text{on}}:进入 ALERT
  • ALERT 且连续 kk 个点满足 st<τoffs_t<\tau_{\text{off}}:恢复为 NORMAL
  • 其他情况保持原状态。
class HysteresisAlert:
    def __init__(self, on_threshold, off_threshold, recover_points=3):
        if off_threshold >= on_threshold:
            raise ValueError("off_threshold must be lower than on_threshold")
        if recover_points < 1:
            raise ValueError("recover_points must be positive")

        self.on_threshold = on_threshold
        self.off_threshold = off_threshold
        self.recover_points = recover_points
        self.state = "NORMAL"
        self.below_count = 0

    def update(self, score):
        if self.state == "NORMAL":
            self.below_count = 0
            if score >= self.on_threshold:
                self.state = "ALERT"
        else:
            if score < self.off_threshold:
                self.below_count += 1
                if self.below_count >= self.recover_points:
                    self.state = "NORMAL"
                    self.below_count = 0
            else:
                self.below_count = 0

        return self.state

如果分数序列为:

[0.2,0.8,0.7,0.65,0.3,0.4,0.2][0.2,0.8,0.7,0.65,0.3,0.4,0.2]

且开启阈值为 0.7、恢复阈值为 0.4、恢复需要连续 3 个点,则第二个点进入告警;第五个点虽然低于恢复阈值,但还未满足 3 个连续点;第七个点时才恢复。这个状态机比每个点独立比较阈值更符合“故障事件”的语义。

但滞回会增加恢复延迟,不适合必须即时解除的安全控制;阻断类动作还应有人工解锁、最大持续时间和紧急旁路机制。


九、评测:排序、分类和事件必须分开

异常检测模型至少产生三个层次的评测问题。

9.1 分数排序质量

不固定阈值时,使用 ROC-AUC 或 PR-AUC 评估分数排序。异常比例很低时,PR-AUC 更能反映高分区域的精确率。

但 AUC 不能告诉你“每天会产生多少告警”,也不能保证在某个具体阈值上可用。

9.2 固定阈值后的分类质量

对阈值 τ\tau 计算:

precision=TPTP+FP\text{precision}=\frac{TP}{TP+FP}

recall=TPTP+FN\text{recall}=\frac{TP}{TP+FN}

生产中还应报告:

  • 每个实体每天的告警数;
  • 告警合并后的事件数;
  • 每个事件的检测率;
  • 严重级别分层召回率;
  • 误报调查耗时;
  • 模型推理和特征计算成本。

9.3 时间切分和泄漏

随机切分会把同一故障、同一用户或相邻时间窗口同时放进训练和验证,导致异常检测结果虚高。更可靠的切分方式包括:

  • 按时间先后切分;
  • 按设备、账户或租户分组切分;
  • 对连续故障按事件整体切分;
  • 使用滚动窗口评估漂移。

如果阈值在测试集上选择,再用同一测试集报告指标,也会产生评估泄漏。应区分训练集、校准集、验证集和最终测试集;阈值治理本身也需要版本化。


十、常见误解与诊断方法

误解一:异常率等于 contamination

contamination 通常是模型内部阈值或训练假设的一部分,不是经过验证的真实异常率。若系统每天真实异常率变化,固定 contamination 不会自动适应业务风险。

诊断方法:分别记录原始分数分布、内部阈值、最终业务阈值和人工确认标签,比较它们随时间的变化。

误解二:分数可以跨模型、跨版本直接比较

Isolation Forest、One-Class SVM、自编码器的分数尺度和方向不同。同一模型重新训练后,树结构、样本窗口或归一化方式变化,也可能改变分数分布。

诊断方法:每个模型版本报告正常校准集的中位数、P95、P99、P99.9 和阈值,不直接比较绝对分数;需要统一展示时使用分位数或校准后的风险等级。

误解三:异常一定是少数

有些数据切换到错误状态后,异常样本可能迅速成为多数。例如服务配置错误导致所有实例同时产生相同异常。此时基于“稀有性”的算法可能失效,应使用已知状态规则、变更关联、监督标签或时间序列基线。

误解四:标准化可以解决所有问题

标准化只能调整尺度,不能解决:

  • 标签错误;
  • 时间泄漏;
  • 分布漂移;
  • 异常群体变大;
  • 训练数据被污染;
  • 特征语义发生变化。

误解五:告警越敏感越好

阈值过低会产生告警疲劳,使真正异常被忽略;阈值过高会漏掉早期信号。阈值应与动作风险匹配:可逆的观察动作可以更敏感,不可逆的扣款、封禁和阻断动作需要更高置信度、多信号确认或人工审核。


十一、生产落地的最小闭环

一个可验证的异常检测系统至少应保留以下信息:

event_id
entity_id
event_time
feature_schema_version
model_version
threshold_policy_version
raw_score
score_direction
decision_level
action
input_quality_flags
label_status

raw_score 不能只保留二值结果,否则之后无法重放阈值、分析漂移或比较新策略。输入质量也应进入记录:缺失值填充、截断、异常范围修正可能本身就是异常信号。

模型上线前应完成以下因果检查:

  1. 用历史正常数据拟合特征变换和模型;
  2. 用独立校准集确定分数方向和阈值;
  3. 用时间上更晚的验证数据检查告警量和事件级效果;
  4. 对特征缺失、服务超时、模型不可用制定降级动作;
  5. 以灰度或影子模式观察真实分数分布;
  6. 阈值、模型、特征和动作策略绑定版本;
  7. 收集人工确认结果,区分误报、漏报、未知;
  8. 定期检查分布漂移、告警预算和处置容量。

成本也应纳入同一系统:特征计算、嵌入生成、模型推理、存储原始分数、人工审核和误报造成的业务中断都属于异常检测成本。生成式 AI 场景尤其要关注嵌入和大模型调用费用;可以先用规则、轻量统计模型或缓存结果过滤明显正常请求,再对高风险候选调用昂贵模型,但必须验证级联过滤不会系统性漏掉某类异常。


十二、如何选择方法

可以按问题结构进行初步选择:

  • 单变量、分布稳定、需要可解释:Z-score、稳健 Z-score、分位数;
  • 多变量且相关性重要:Mahalanobis 距离、稳健协方差、降维后距离;
  • 正常样本较多、异常稀少、希望较低训练成本:Isolation Forest;
  • 正常数据边界复杂、样本规模可控:One-Class SVM;
  • 图像、长序列或高维表示:自编码器、预测模型或嵌入空间检测;
  • 已有充分异常标签:监督分类或排序模型,并保留无监督检测发现未知异常;
  • 安全和权限场景:异常分数只能辅助风险分层,不能替代显式授权和策略校验。

最终目标不是让某个模型在离线数据上得到最高分,而是让“分数—阈值—动作—反馈”形成可解释、可回放、可控制风险的闭环。统计方法提供基线,Isolation Forest 提供基于隔离难度的非参数检测,One-Class 方法学习正常边界,而阈值治理决定这些信号在真实系统中何时变成观察、告警、审核或阻断。


系列导航与关联阅读

官方资料

本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。