异常检测:建模正常,标记偏离


文档摘要

异常检测:建模正常,标记偏离 本节摘要:「正常」容易定义,「异常」就是不符合的任何东西。一张信用卡下午 2 点在纽约刷,2:05 在东京刷;工厂传感器读 150 度而正常是 80120;服务器每秒发 5 万请求而日均 200。这些都是异常(Anomaly),找到它们很要紧——欺诈损失数十亿,设备故障损失停机,网络入侵损失数据。挑战在于:你几乎没有异常的标注样本。欺诈只占交易 0.1%,设备故障一年几次。你训不了标准分类器,因为「异常」类几乎没东西可学;而且见过的异常不是你会遇到的全部,明天的欺诈套路跟今天不一样。异常检测翻转问题:不学什么是异常,学什么是正常,任何偏离正常的都可疑。这无需标签、适应新型异常、可扩展到海量数据。

异常检测:建模正常,标记偏离

本节摘要:「正常」容易定义,「异常」就是不符合的任何东西。一张信用卡下午 2 点在纽约刷,2:05 在东京刷;工厂传感器读 150 度而正常是 80~120;服务器每秒发 5 万请求而日均 200。这些都是异常(Anomaly),找到它们很要紧——欺诈损失数十亿,设备故障损失停机,网络入侵损失数据。挑战在于:你几乎没有异常的标注样本。欺诈只占交易 0.1%,设备故障一年几次。你训不了标准分类器,因为「异常」类几乎没东西可学;而且见过的异常不是你会遇到的全部,明天的欺诈套路跟今天不一样。异常检测翻转问题:不学什么是异常,学什么是正常,任何偏离正常的都可疑。这无需标签、适应新型异常、可扩展到海量数据。本节讲透三种异常类型(点、上下文、集合)、有监督 vs 无监督的权衡,从零实现 Z 分数、IQR、孤立森林(Isolation Forest)、局部离群因子(LOF),并讨论生产里的阈值选择与告警疲劳。

学习目标

阅读完本节,你应当能够:

  1. 从零实现 Z 分数、IQR、孤立森林异常检测方法。
  2. 区分点异常、上下文异常、集合异常,为每种选合适的检测方法。
  3. 解释为何异常检测被框定为「建模正常数据」而非「分类异常」。
  4. 对比无监督异常检测与有监督分类,评估新型异常覆盖与精确率之间的权衡。

一、问题与直觉

一张信用卡下午 2 点在纽约刷,2:05 在东京刷。工厂传感器读 150 度而正常范围 80~120。服务器每秒发 5 万请求而日均 200。

这些都是异常。找到它们很要紧。欺诈损失数十亿。设备故障损失停机。网络入侵损失数据。

挑战在于:你几乎没有异常的标注样本。欺诈占交易 0.1%。设备故障一年几次。你训不了标准分类器,因为「异常」类里几乎没东西可学。即使你有些标签,你见过的异常类型不是你会遇到的全部。明天的欺诈套路跟今天不一样。

异常检测翻转问题。不学什么是异常,学什么是正常。任何偏离正常的都可疑。这无需标签、适应新型异常、可扩展到海量数据。

异常类型

不是所有异常都一样:

  • 点异常(Point Anomaly):单个数据点不论上下文都不寻常。读数 500 度。一个通常花 50 美元的账户刷 5 万。
  • 上下文异常(Contextual Anomaly):给定上下文不寻常的数据点。90 度夏天正常、冬天异常。同样数值,不同上下文。
  • 集合异常(Collective Anomaly):作为一组不寻常的数据点序列,即使每个单独点可能正常。5 次登录失败正常,连续 50 次是暴力破解。

多数方法检测点异常。上下文异常需要时间或位置特征。集合异常需要序列感知方法。

无监督框定

标准分类里两类都有标签。异常检测里你通常处于三种情况之一:

  1. 完全无监督:无任何标签。你在所有数据上拟合检测器,希望异常足够稀有不至于污染「正常」模型。
  2. 半监督:你只有一份干净的正常数据。在这份干净集上拟合,给其他一切打分。可能时这是最强的设置。
  3. 弱监督:你有少量标注异常。用它们做评估而非训练。无监督训练,再在标注子集上测精确率/召回率。

关键洞见:异常检测与分类根本不同。你在建模正常数据的分布,不是两类之间的决策边界。

有监督 vs 无监督:权衡

若你确有标注异常,该用它们训练(有监督分类)还是仅评估(无监督检测)?

有监督(当分类处理):

  • 抓住你以前见过的确切异常类型。
  • 对已知异常类型精确率更高。
  • 完全错过新型异常。
  • 新异常类型出现时需重训。
  • 需要足够异常样本(常常太少)。

无监督(建模正常,标记偏离):

  • 抓住任何偏离正常的,包括新型。
  • 不需要标注异常。
  • 假阳性率更高(不是所有不寻常都坏)。
  • 对分布漂移更鲁棒。

实践中,最好的系统两者结合:无监督检测提供广覆盖,有监督模型处理已知高优先级异常类型,人工审核处理模糊情况。

Z 分数法

最简单的方法。算每特征的均值和标准差。标记任何距均值超过 k 个标准差的点。

z_score = (x - mean) / std 若 |z_score| > threshold 则为异常

默认阈值 3.0(高斯分布下 99.7% 正常数据落在 3 个标准差内)。

优点:简单、快、可解释(「这个值偏离正常 4.5 个标准差」)。

缺点:假设数据正态分布。对训练数据里的离群点敏感(离群点拉偏均值、膨胀标准差,使自己更难被检测)。在多模态分布上失败。

何时好用:单特征监控,数据近似钟形。服务器响应时间、制造公差、有稳定基线的传感器读数。

何时失败:多簇数据(两个办公地不同基线温度)、偏斜数据(交易额 1000 美元稀有但非异常)、训练集含离群点。

IQR 法

比 Z 分数更鲁棒。用四分位距代替均值和标准差。

Q1 = 25 百分位 Q3 = 75 百分位 IQR = Q3 - Q1 下界 = Q1 - factor * IQR 上界 = Q3 + factor * IQR 若 x < 下界 或 x > 上界 则为异常

默认因子 1.5。

优点:对离群点鲁棒(百分位不受极值影响)。适用于偏斜分布。无正态假设。

缺点:仅单变量(每特征独立应用)。抓不到「单看每特征都正常,合起来才异常」的点(一个点可能在每特征上正常,但在联合空间异常)。

实践注记:IQR 的 1.5 因子对应箱线图的须。须外的点是潜在离群点。用 3.0 代替 1.5 让检测器更保守(更少标记、更少假阳性)。合适的因子取决于你对假告警的容忍度。

孤立森林(Isolation Forest)

关键洞见:异常少且不同。在数据的随机划分里,异常更容易被孤立——它们需要更少随机分裂就能从其余中分离开。

工作原理:

  1. 建许多随机树(一个孤立森林)。
  2. 每个节点,随机选特征和该特征最小最大之间的随机分裂值。
  3. 持续分裂直到每个点被孤立(在叶里独占)。
  4. 异常在所有树上的平均路径长度更短。

为何有效: 正常点住在稠密区。需要许多随机分裂才能把它与邻居分开。异常住在稀疏区。一两个随机分裂就够孤立它。

异常分数基于所有树的平均路径长度,用随机二叉搜索树的期望路径长度归一化:

score(x) = 2^(-average_path_length(x) / c(n))

其中 c(n) 是 n 个样本的期望路径长度。分数近 1 意味异常。近 0.5 意味正常。近 0 意味非常正常(深在稠密簇里)。

优点:无分布假设。高维有效。扩展好(样本量次线性,因为每棵树用子样本)。处理混合特征类型。

缺点:在稠密区里的异常挣扎(掩蔽效应)。许多特征无关时随机分裂效果差。

关键超参数:

  • n_estimators:树数。100 通常够。更多树给更稳定分数但更慢。
  • max_samples:每树样本数。原论文默认 256。更小值让单棵树更不准但增加多样性。子采样是孤立森林快的原因——每棵树只看数据的一小部分。
  • contamination:预期异常比例。仅用于设阈值。不影响分数本身。

局部离群因子(LOF)

LOF 比较一个点周围的局部密度与其邻居的密度。一个住在稀疏区、周围是稠密区的点是异常。

工作原理:

  1. 对每点,找它的 k 近邻。
  2. 算局部可达密度(邻域多稠密)。
  3. 把每点密度与其邻居密度比。
  4. 若一点密度比邻居低很多,它是离群点。

LOF 分数:

  • LOF 接近 1.0 意味与邻居密度相似(正常)。
  • LOF 大于 1.0 意味密度比邻居低(可能异常)。
  • LOF 远大于 1.0(如 2.0+)意味密度显著低(很可能异常)。

「局部」是关键。考虑一个数据集含两簇:1000 点的稠密簇和 50 点的稀疏簇。稀疏簇边缘的点并非全局不寻常——它有 50 个邻居。但若它的直接邻居比它稠密,它在局部就不寻常。LOF 抓住了全局方法错过的这种细微之处。

优点:检测局部异常(在邻域里不寻常的点,即使全局不寻常)。处理不同密度的簇。

缺点:大数据集慢(朴素实现 O(n^2))。对 k 的选择敏感。很高维表现差(维度灾难影响距离计算)。

对比

方法 假设 速度 处理高维 检测局部异常
Z 分数 正态分布 极快 是(每特征)
IQR 无(每特征) 极快 是(每特征)
孤立森林 部分
LOF 距离有意义

评估挑战

评估异常检测器比评估分类器难:

  • 极端类别不平衡:异常占 0.1%,全部预测「正常」拿 99.9% 准确率。准确率无用。
  • AUROC 有误导性:重不平衡下,AUROC 看起来好,即使模型在实用阈值下错过多数异常。
  • 更好指标:Precision@k(被标记的前 k 项里多少是真异常)、AUPRC(精确率-召回率曲线下面积)、固定假阳性率下的召回率。

异常检测流水线

实践中,异常检测遵循这个流程:

  1. 采集基线数据。理想情况下,一段你知道没有(或极少)异常的时期。
  2. 特征工程。原始特征加派生特征(滚动统计、时间特征、比率)。
  3. 训练检测器。在基线数据上拟合。模型学「正常」长什么样。
  4. 给新数据打分。每个新观测拿到一个异常分数。
  5. 阈值选择。选分数截断。这是业务决策:阈值高意味更少假告警但更多漏报。
  6. 告警并调查。被标记的点进人工审核或自动响应。
  7. 反馈采集。记录被标记项是真异常还是假告警。用这些数据评估检测器并随时间调阈值。

流水线永不「完工」。数据分布漂移,新型异常涌现,阈值需调整。把异常检测当活系统,而非一次性模型。

二、从零实现

code/anomaly_detection.py 从零实现 Z 分数、IQR、孤立森林。

Z 分数检测器

def zscore_detect(X, threshold=3.0): mean = X.mean(axis=0) std = X.std(axis=0) std[std == 0] = 1.0 z = np.abs((X - mean) / std) return z.max(axis=1) > threshold

简单且向量化。任一特征超阈值就标记。

IQR 检测器

def iqr_detect(X, factor=1.5): q1 = np.percentile(X, 25, axis=0) q3 = np.percentile(X, 75, axis=0) iqr = q3 - q1 iqr[iqr == 0] = 1.0 lower = q1 - factor * iqr upper = q3 + factor * iqr outside = (X < lower) | (X > upper) return outside.any(axis=1)

从零实现孤立森林

从零实现构建随机划分特征空间的孤立树:

class IsolationTree: def __init__(self, max_depth): self.max_depth = max_depth def fit(self, X, depth=0): n, p = X.shape if depth >= self.max_depth or n <= 1: self.is_leaf = True self.size = n return self self.is_leaf = False self.feature = np.random.randint(p) x_min = X[:, self.feature].min() x_max = X[:, self.feature].max() if x_min == x_max: self.is_leaf = True self.size = n return self self.threshold = np.random.uniform(x_min, x_max) left_mask = X[:, self.feature] < self.threshold self.left = IsolationTree(self.max_depth).fit(X[left_mask], depth + 1) self.right = IsolationTree(self.max_depth).fit(X[~left_mask], depth + 1) return self

孤立一个点的路径长度决定其异常分数。路径越短越异常。

IsolationForest 类包装多棵树:

class IsolationForest: def __init__(self, n_estimators=100, max_samples=256, seed=42): self.n_estimators = n_estimators self.max_samples = max_samples def fit(self, X): sample_size = min(self.max_samples, X.shape[0]) max_depth = int(np.ceil(np.log2(sample_size))) for _ in range(self.n_estimators): idx = rng.choice(X.shape[0], size=sample_size, replace=False) tree = IsolationTree(max_depth=max_depth) tree.fit(X[idx]) self.trees.append(tree) def anomaly_score(self, X): avg_path = 各树上平均路径长度 scores = 2.0 ** (-avg_path / c(max_samples)) return scores

归一化因子 c(n) 是含 n 个元素的二叉搜索树里一次失败搜索的期望路径长度。它等于 2 * H(n-1) - 2*(n-1)/n,其中 H 是调和数。这个归一化保证分数跨不同大小数据集可比。

演示场景

代码生成多个测试场景:

  1. 单簇含离群点:2D 高斯簇,中心远处注入异常。所有方法都该有效。
  2. 多模态数据:三个不同大小密度的簇。簇之间的点异常。Z 分数挣扎,因为每特征范围宽。
  3. 高维数据:50 特征,但异常只在其中 5 个上不同。测试方法能否在特征子集里找异常。

每个演示用精确率、召回率、F1、Precision@k 对比所有方法。

三、框架对比

用 sklearn(用库实现,非从零):

from sklearn.ensemble import IsolationForest from sklearn.neighbors import LocalOutlierFactor iso = IsolationForest(n_estimators=100, contamination=0.05, random_state=42) iso.fit(X_train) predictions = iso.predict(X_test) lof = LocalOutlierFactor(n_neighbors=20, contamination=0.05, novelty=True) lof.fit(X_train) predictions = lof.predict(X_test)

注意 contamination 设预期异常比例。设对很重要——太低漏异常,太高造假告警。

code/anomaly_detection.py 在同一数据上对比从零实现与 sklearn。

sklearn 的 contamination 参数

sklearn 的 contamination 决定把连续异常分数转二元预测的阈值。它不改底层分数。

iso_5 = IsolationForest(contamination=0.05) iso_10 = IsolationForest(contamination=0.10)

两者产生相同异常分数。但 iso_5 标记前 5%、iso_10 标记前 10%。若你不知真实异常率(通常不知),设 contamination 为 "auto",直接用原始分数。基于假阳性与假阴性的成本权衡自设阈值。

单类 SVM(One-Class SVM)

另一个值得了解的无监督异常检测器。单类 SVM 在高维特征空间(用核技巧)里给正常数据拟合一个边界。

from sklearn.svm import OneClassSVM oc_svm = OneClassSVM(kernel="rbf", gamma="auto", nu=0.05) oc_svm.fit(X_train) predictions = oc_svm.predict(X_test)

nu 参数近似异常比例。单类 SVM 在中小数据集上好用,但不能扩展到很大数据(核矩阵二次增长)。

自编码器方法(预览)

自编码器是学着压缩并重建数据的神经网络。在正常数据上训练。测试时,异常有高重建误差,因为网络只学了重建正常模式。

这在第 4 章(深度学习)讲,但原理相同:建模正常,标记偏离。

集成异常检测

就像集成方法改进分类(第 11 节),组合多个异常检测器改进检测。最简方法:

  1. 跑多个检测器(Z 分数、IQR、孤立森林、LOF)。
  2. 把每个检测器的分数归一化到 [0, 1]。
  3. 平均归一化分数。
  4. 标记平均分数超阈值的点。

这降假阳性,因为不同方法有不同失败模式。被四种方法都标记的点几乎肯定异常。只被一种标记的可能是那种方法的怪癖。

更复杂的集成按估计的可靠性加权每个检测器(在有已知异常的验证集上测,若有)。

生产考量

  1. 阈值漂移。数据分布漂移时,固定阈值过时。监控异常分数分布并周期性调整。
  2. 告警疲劳。假告警太多,操作员就不再关注。从高阈值开始(更少更可靠的告警),信任建立后再降。
  3. 集成方法。生产中组合多个检测器。只在多种方法一致认为是异常时才标记。这显著降假阳性。
  4. 特征工程。原始特征很少够。加滚动统计、比率、距上次事件时间、领域特征。好的特征集比检测器选择更重要。
  5. 反馈回路。操作员调查被标记项并确认或驳回时,把反馈喂回系统。随时间积累标注数据以评估和改进检测器。
维度 从零实现 sklearn
检测器 Z、IQR、孤立森林 加 LOF、单类 SVM、Elliptic Envelope
路径长度归一化 自己写 c(n) 内置
适用 理解原理 生产

选阈值

异常分数是连续的。你需要阈值做二元决策。这是业务决策,不是技术决策。

考虑两个场景:

  • 欺诈检测:漏欺诈代价高(退单、客户信任)。假告警耗分析师 5 分钟调查。阈值设低以抓更多欺诈,接受更多假告警。
  • 设备维护:假告警意味一次不必要的停工,损失 5 万美元。漏故障意味 50 万美元维修。设阈值平衡这些成本。

两种情况下,最优阈值取决于假阳性与假阴性的成本比。在不同阈值画精确率和召回率,叠加成本函数,选最低成本点。

四、可复用产物

本节产出:

  • outputs/skill-anomaly-detector.md——一个选对检测器的决策技能。
  • code/anomaly_detection.py——从零的 Z 分数、IQR、孤立森林,配 sklearn 对比。

五、练习

  1. 阈值调优:Z 分数检测器用阈值 1.0 到 5.0 步长 0.5 跑。画每阈值的精确率和召回率。你数据的甜点在哪?

  2. 多变量异常:造 2D 数据,每特征单独看正常,但组合异常(如远离主簇对角线的点)。展示每特征 Z 分数错过这些,但孤立森林抓住。

  3. 从零实现 LOF:用 k 近邻实现局部离群因子。在同一数据上对比 sklearn 的 LocalOutlierFactor。用 k=10 和 k=50——k 的选择如何影响结果?

  4. 流式异常检测:修改 Z 分数检测器为流式:随新点到达更新运行均值和方差(Welford 在线算法)。在同一数据上对比批量 Z 分数。

  5. 真实评估:取一个含已知异常的数据集(如 Kaggle 信用卡欺诈)。用 precision@100、precision@500、AUPRC 评估所有四种方法。哪个最好?为什么?

本节要点回顾

  1. 翻转问题:建模正常,标记偏离:无需标签、适应新型异常、可扩展海量数据,这是无监督异常检测的核心框定。
  2. 三种异常类型:点(单个值不论上下文都不寻常)、上下文(给定上下文不寻常,如一月 90 度)、集合(作为一组不寻常的序列,如 50 次连续登录失败)。
  3. 三种数据情况:完全无监督(无标签,希望异常够稀)、半监督(只有干净正常数据,最强)、弱监督(少量标注异常,仅用于评估)。
  4. 有监督 vs 无监督权衡:有监督抓已知类型精确高但漏新型,无监督覆盖广但假阳性高;生产最佳系统两者结合。
  5. Z 分数简单但脆:假设正态、对训练离群点敏感、多模态失败;适合单特征、近似钟形的监控。
  6. IQR 更鲁棒:用四分位距代替均值标准差,抗离群点、无正态假设;但仅单变量。
  7. 孤立森林:异常少且不同,更易孤立:随机划分下异常路径更短,分数 2^(-平均路径/c(n)),无分布假设、高维有效、子采样使其快。
  8. LOF 抓局部异常:比较点与其邻居的局部密度,密度比邻居低很多则异常;能抓全局方法错过的不同密度簇里的离群点,但 O(n^2) 慢。
  9. 准确率无用、用 Precision@k/AUPRC:0.1% 异常下全预测正常拿 99.9% 准确率;AUROC 重不平衡下也有误导性。
  10. 阈值是业务决策:基于假阳性与假阴性成本比;从高阈值开始避免告警疲劳,生产中用多检测器集成降假阳性,持续监控分数漂移并重调。

下一节,我们讲处理不平衡数据——为何 99% 准确率可能是个谎言,以及 SMOTE、类权重、阈值调优如何让模型关注稀有但重要的类。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U