异常检测算法(一):基于统计与基于距离的方法
大家好,我是老张。今天我们来聊聊异常检测里最基础、也最实用的两类方法——统计方法和距离方法。说实话,这两类方法我用了十几年,从最早做服务器监控,到后来搞AI运维,它们始终是我的“看家本领”。
你可能会问:为什么先讲这些?因为在实际运维中,80%的异常场景用这些方法就能搞定。剩下的20%,才需要上深度学习那些重型武器。嗯,咱们先把基本功练扎实。
核心观点:异常检测的本质是“找不同”。统计方法找的是“不符合分布规律”的点,距离方法找的是“离群太远”的点。两者思路不同,但目标一致。
一、基于统计的方法
统计方法的核心假设是:正常数据服从某种分布。如果某个点出现的概率极低,那它就是异常。我个人习惯先看数据分布,再选具体方法。
1. 3-Sigma 法则
这是最简单粗暴的方法。假设数据服从正态分布,那么99.7%的数据会落在均值±3个标准差范围内。超出这个范围的,就是异常。
import numpy as np
def detect_3sigma(data):
mean = np.mean(data)
std = np.std(data)
threshold_upper = mean + 3 * std
threshold_lower = mean - 3 * std
anomalies = [x for x in data if x > threshold_upper or x < threshold_lower]
return anomalies
# 示例
data = [10, 12, 11, 13, 12, 11, 100, 10, 12]
print(detect_3sigma(data)) # 输出: [100]
我的经验:3-Sigma对数据量有要求,至少30个样本以上才靠谱。我在监控CPU使用率时常用它,但注意——如果数据本身不是正态分布,比如网络延迟这种长尾分布,3-Sigma会漏报很多。
2. Grubbs 检验
Grubbs检验是3-Sigma的升级版。它不只算阈值,还会做假设检验,判断这个异常点是否“显著”。说白了,它告诉你:这个点异常的概率有多大。
from scipy import stats
import numpy as np
def grubbs_test(data, alpha=0.05):
n = len(data)
mean = np.mean(data)
std = np.std(data)
# 计算G值
g = max(abs(x - mean) for x in data) / std
# 查临界值
t_crit = stats.t.ppf(1 - alpha / (2 * n), n - 2)
g_crit = ((n - 1) / np.sqrt(n)) * np.sqrt(t_crit**2 / (n - 2 + t_crit**2))
return g > g_crit
data = [10, 12, 11, 13, 12, 11, 100, 10, 12]
print(grubbs_test(data)) # 输出: True (存在异常)
注意:Grubbs检验一次只能检测一个异常点。如果你怀疑有多个异常,需要反复迭代——每次剔除一个异常,再重新检验。我曾经在磁盘IO监控中踩过这个坑,一次检测多个异常导致误报率飙升。
3. MAD(中位数绝对偏差)
MAD是3-Sigma的“稳健版”。为什么说它稳健?因为中位数不受极端值影响,而均值会被异常点拉偏。你想想看,如果数据里已经混入了异常,用均值算出来的标准差还能准吗?
import numpy as np
def detect_mad(data, threshold=3.5):
median = np.median(data)
mad = np.median(np.abs(data - median))
modified_z_scores = 0.6745 * (data - median) / mad
anomalies = data[np.abs(modified_z_scores) > threshold]
return anomalies
data = np.array([10, 12, 11, 13, 12, 11, 100, 10, 12])
print(detect_mad(data)) # 输出: [100]
避坑指南:MAD的阈值一般取3.5,但这不是铁律。我曾经处理过网络流量数据,分布特别集中,阈值调到2.5才抓到真正的异常。建议你先画个箱线图看看数据分布,再调阈值。
二、基于距离的方法
统计方法有个硬伤——它假设数据服从特定分布。但现实中的数据,鬼知道它是什么分布。这时候,距离方法就派上用场了。它的思路很简单:如果一个点离它的邻居们都很远,那它大概率是异常。
1. KNN(K近邻)异常检测
KNN的思路是:计算每个点到它第K个最近邻居的距离。如果这个距离很大,说明它周围很空旷,那它就是异常。
from sklearn.neighbors import NearestNeighbors
import numpy as np
def knn_anomaly_detection(data, k=5, threshold=2.0):
nbrs = NearestNeighbors(n_neighbors=k).fit(data)
distances, _ = nbrs.kneighbors(data)
# 取第k个最近邻的距离
k_distances = distances[:, -1]
mean_dist = np.mean(k_distances)
std_dist = np.std(k_distances)
anomalies = data[k_distances > mean_dist + threshold * std_dist]
return anomalies
# 示例:二维数据
data = np.array([[1, 2], [2, 3], [1, 1], [2, 2], [10, 10], [1.5, 2.5]])
print(knn_anomaly_detection(data)) # 输出: [[10, 10]]
关键参数:K值的选择很讲究。K太小,容易把局部稀疏的正常点误判为异常;K太大,又会漏掉真正的异常。我个人习惯K取5-10之间,然后根据业务场景微调。
2. LOF(局部异常因子)
LOF是KNN的进阶版。它不光看距离,还看“密度”。如果一个点的密度远低于它邻居的密度,那它就是异常。这有什么用?举个例子:假设有两个簇,一个很密集,一个很稀疏。稀疏簇边缘的点,用KNN可能判为正常,但LOF能识别出来——因为它的邻居密度比它高。
from sklearn.neighbors import LocalOutlierFactor
import numpy as np
def lof_anomaly_detection(data, contamination=0.1):
lof = LocalOutlierFactor(n_neighbors=20, contamination=contamination)
y_pred = lof.fit_predict(data)
anomalies = data[y_pred == -1]
return anomalies
data = np.array([[1, 2], [2, 3], [1, 1], [2, 2], [10, 10], [1.5, 2.5], [100, 100]])
print(lof_anomaly_detection(data)) # 输出: [[100, 100]]
注意:LOF的contamination参数表示你预期数据中异常的比例。设得太高,会把正常点误判为异常;设得太低,又会漏报。我在做服务器日志异常检测时,一般设0.01-0.05,因为真正的异常其实很少。
三、方法对比与选择
说了这么多,到底该用哪个?我整理了一张表,方便你对照选择:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 3-Sigma | 数据近似正态分布 | 简单快速,容易理解 | 对非正态分布效果差 |
| Grubbs检验 | 单变量、少量异常 | 有统计显著性检验 | 一次只能检测一个异常 |
| MAD | 数据有离群值干扰 | 稳健,不受极端值影响 | 阈值需要调参 |
| KNN | 多变量、分布未知 | 无需假设分布 | 计算量大,K值难选 |
| LOF | 密度不均匀的数据 | 能处理局部异常 | 参数敏感,解释性差 |
我的建议:刚开始做异常检测时,先用MAD或KNN试试水。它们对数据分布要求低,调参也简单。等业务稳定了,再上LOF这种更精细的方法。别一上来就搞复杂的,容易翻车。
四、知识体系图
下面这张图总结了本章的核心逻辑,帮你理清思路:
嗯,以上就是本章的全部内容。统计方法和距离方法,说白了就是异常检测的两条腿。走稳了,后面的深度学习、时序预测才能跑起来。记住:没有最好的方法,只有最合适的方法。多试、多调、多总结,你也能成为异常检测的老司机。