常见异常检测方法简介

聊到异常检测,很多新手的第一反应就是「设个阈值不就行了?」。嗯,这个想法很直接,但现实往往没那么简单。我在早期做服务器监控时,也这么干过——给 CPU 使用率设了个 90% 的硬阈值。结果呢?大促期间流量翻了三倍,阈值天天被触发,告警刷屏,运维兄弟差点把我拉黑。

为什么会这样?

因为真实的系统行为是动态的、有模式的。单一的阈值根本抓不住那种「明明没超限,但行为就是不对劲」的异常。

下面我带你过一遍工业界常用的几种异常检测方法。每种方法都有自己的脾气,选对了才是好工具。

基于统计的方法

这是最经典的一类。核心假设是:正常数据服从某个已知的概率分布,异常点则是小概率事件。

  • 3-Sigma 原则:假设数据服从正态分布,超出均值 ±3σ 的点标记为异常。
  • Grubbs 检验:单变量场景下,逐个检测离群点。
  • 箱线图(四分位距):不依赖正态分布,用 Q1-1.5×IQR 和 Q3+1.5×IQR 画边界。

这类方法计算快,可解释性强。但局限也很明显——它假设数据是独立同分布的。说白了,如果你的数据是时序的,有趋势有周期,用纯统计方法就容易误报。

注意:统计方法对分布假设很敏感。我曾经在一个风控项目里直接用 3-Sigma 检测日活异常,结果节假日流量下降被大面积标红——正常波动被当成异常,这就要命了。

基于距离的方法

核心思路:异常点离它最近的邻居都比较远。典型的代表是 k-最近邻距离法。

我给一个简单的检测框架:

from sklearn.neighbors import NearestNeighbors
import numpy as np

def knn_anomaly_score(X, k=5):
    nbrs = NearestNeighbors(n_neighbors=k).fit(X)
    distances, _ = nbrs.kneighbors(X)
    # 用第k近的距离作为异常分数
    scores = distances[:, -1]
    return scores

# 设定百分位阈值
scores = knn_anomaly_score(data, k=5)
threshold = np.percentile(scores, 95)
anomalies = np.where(scores > threshold)[0]

距离法比较容易理解,不受分布假设的约束。但它有两个硬伤:第一,计算复杂度高——数据量一大,全量计算 k-近邻基本跑不动;第二,它对「密度不均」的数据集效果很差——假设正常数据本身就有稀疏区域,你按距离一刀切,那些正常的稀疏点也会被误判为异常。

基于密度的方法

为了解决上面那个「稀疏区域 vs 真实异常」的问题,有人提出了局部异常因子(LOF, Local Outlier Factor)。

LOF 不看距离,而是看「局部可达密度」。什么意思呢?它计算一个点跟邻居的紧密程度,再跟邻居们的紧密程度做个对比。通俗点说:你的邻居们如果都住得挺挤,就你一个人孤零零的,那你大概率有问题。

LOF 的优势在于能处理局部异常。我在生产环境里用它检测过一波流量异常——同样是 2000 QPS,放在 A 机房的正常业务上算正常,放在 B 机房的静默业务上就是异常。LOF 能区分出这种「局部上下文」的差异。

LOF 的调参压力比较大——邻居数 k 选不好,结果天差地别。一般建议先跑一下小规模数据试一下。

基于树的方法

「孤立森林」是这类方法的代表。它不试图建模「正常是什么样」,反过来去想——异常点往往更容易被「孤立」出来。

算法的做法很暴力:随机选一个特征,随机选一个分裂值,把数据不断切分。正常点需要很多次分裂才能被隔离,异常点没几下就被单独分出来了。

from sklearn.ensemble import IsolationForest

model = IsolationForest(
    n_estimators=100,
    max_samples='auto',
    contamination=0.05,  # 预期异常比例
    random_state=42
)
model.fit(X)

# outlier 标签:1 正常,-1 异常
labels = model.predict(X)
# 异常分数(越小越异常)
scores = model.decision_function(X)

孤立森林速度快,几乎不需要什么假设约束,高维数据也不怵。但我个人习惯提醒一句:它的随机性很强,建议多跑几次取均值,结果会更稳定。

基于聚类的方法

这种方法也很好理解:先把数据聚成几团,然后看哪些点「不属于任何一团」。常见的做法有 DBSCAN 和 K-Means 的变体。

拿 DBSCAN 举例,它天然可以识别噪声点——每个样本如果落不到任何核心点的邻域半径内,就直接标记为 -1(噪声),这不就是异常嘛。

实践经验:DBSCAN 的两个参数(eps 邻域半径、min_samples 最小样本数)非常影响结果。我的做法是先画 k-距离图来估算 eps,然后用领域知识确定 min_samples。不要凭感觉瞎填。

聚类方法的明显局限:如果异常本身自己也能聚成一团(比如某条链路持续掉包形成的线形轨迹),那它反而会被当成正常的簇。这一点需要警惕。

方法速查对比

我把这几类方法的关键差异列了个表,方便你查阅选型:

方法类别 核心思想 适用场景 典型上限(样本数) 调参复杂程度
统计方法 分布假设 + 低概率 单变量、数据量大且稳定 百万级 低
距离方法 到最近邻居的距离 低维、小数据集 万级 中
密度方法(LOF) 局部密度对比 密度不均匀、中低维 万级 高
孤立森林 随机切分,孤立难度 高维、大样本、混合类型 十万级 低
聚类方法 远离所有簇 簇形状规整、有先验 万级~十万级 中~高

方法选择框架图

我整理了一张选择逻辑图,帮你快速定位用哪一种方法:

异常检测方法选择框架 原始数据 数据量大? 小规模 (<1万) 中规模 (1万-10万) 大规模 (>10万) 距离法 / LOF k-NN、局部异常因子 孤立森林 / 聚类 Isolation Forest、DBSCAN 统计方法 / 孤立森林 3-Sigma、箱线图 需要强解释性? 需要强解释性? 需要强解释性? 统计方法 统计方法 3-Sigma LOF / 距离法 孤立森林 孤立森林 提示:橙色节点偏向速度与自动化,紫色节点偏向可解释性 实际生产中建议先跑小样本横向对比,再决定最终方案

这张图的思路是从数据规模出发做第一层过滤,再从「是否需要解释性」做第二层。我个人习惯先拿小数据集快速跑一轮对比,看看各方法的召回率分布,再定最终方案——别一上来就调大参数,浪费时间。

小结

异常检测没有银弹。每种方法都有自己的脾气,也都有自己的盲区。你想想看——统计方法怕分布假设不满足,距离法怕局部密度不均匀,聚类法怕异常自己抱成团,孤立森林又带随机性。

我的建议是:至少掌握 2-3 种方法,能根据数据特征灵活切换。下一轮我们再聊如何把这些方法封装成可复用的 SDK 模块。