4. 异常检测算法:从统计到密度的实战选择
异常检测,说白了就是在一堆正常数据里找出那个「不对劲」的家伙。在IoT场景里,传感器读数突然跳变、设备心跳丢失、网络流量异常飙升——这些都是我们每天要面对的问题。
我个人习惯把异常检测算法分成三类:基于统计的、基于距离的、基于密度的。别被这些术语吓到,它们背后的逻辑其实很直观。今天我就把这三种方法掰开揉碎了讲,顺便聊聊我在实际项目中踩过的坑。
核心观点:没有最好的算法,只有最合适的场景。选对方法比调参更重要。
4.1 基于统计的方法:Z-Score 和 IQR
统计方法是最古老也最直观的异常检测手段。它的核心假设是:正常数据服从某种分布,异常点则显著偏离这个分布。
4.1.1 Z-Score:标准分数法
Z-Score 衡量一个数据点距离均值有多少个标准差。公式很简单:
Z = (x - μ) / σ
其中 μ 是均值,σ 是标准差。通常我们取 |Z| > 3 作为异常阈值——这意味着数据点偏离均值超过3个标准差。
我在项目中遇到过一个问题:某工厂的温度传感器,正常范围是20-30°C,但偶尔会读到35°C。用Z-Score一算,Z值只有2.1,没到3。后来发现是因为数据本身不是正态分布,而是偏态的。嗯,这里要注意——Z-Score 对正态分布假设敏感。
实战技巧:如果数据明显偏态,可以先做 Box-Cox 变换再算 Z-Score。我一般会先画个直方图看看分布形态。
代码实现也很直接:
import numpy as np
def zscore_detection(data, threshold=3):
mean = np.mean(data)
std = np.std(data)
z_scores = [(x - mean) / std for x in data]
anomalies = [i for i, z in enumerate(z_scores) if abs(z) > threshold]
return anomalies, z_scores
# 示例:模拟传感器数据
sensor_data = [22.1, 23.4, 21.8, 22.9, 35.2, 22.3, 21.7, 23.1]
anomalies, zs = zscore_detection(sensor_data)
print(f"异常索引: {anomalies}") # 输出: [4]
4.1.2 IQR:四分位距法
IQR 不依赖正态分布假设,它基于数据的分位数。计算步骤:
- 计算第一四分位数 Q1(25%分位)和第三四分位数 Q3(75%分位)
- 计算 IQR = Q3 - Q1
- 定义异常边界:下界 = Q1 - 1.5 * IQR,上界 = Q3 + 1.5 * IQR
- 超出边界的点即为异常
为什么是1.5倍?这是统计学家的经验值。我曾经试过把系数改成2.0,结果漏掉了好几个真实异常。后来我学乖了——先看数据分布再调系数。
def iqr_detection(data, multiplier=1.5):
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower_bound = q1 - multiplier * iqr
upper_bound = q3 + multiplier * iqr
anomalies = [i for i, x in enumerate(data) if x < lower_bound or x > upper_bound]
return anomalies, (lower_bound, upper_bound)
# 示例
data = [10, 12, 11, 13, 45, 12, 11, 10, 14]
anomalies, bounds = iqr_detection(data)
print(f"异常索引: {anomalies}") # 输出: [4]
注意:IQR 对数据量有要求。样本太少(比如少于10个)时,分位数估计不稳定。我建议至少要有20个样本再用 IQR。
4.2 基于距离的方法:KNN
KNN 异常检测的思路很朴素:如果一个点离它的 k 个最近邻居都很远,那它大概率是异常。你想想看,在IoT设备集群里,大部分设备的行为模式是相似的,突然有个设备的行为跟谁都不像——那肯定有问题。
具体做法:
- 对每个点,计算它到 k 个最近邻居的平均距离
- 这个平均距离就是「异常分数」
- 设定阈值,分数超过阈值的点标记为异常
from sklearn.neighbors import NearestNeighbors
import numpy as np
def knn_anomaly_detection(data, k=5, threshold_percentile=95):
nbrs = NearestNeighbors(n_neighbors=k).fit(data)
distances, _ = nbrs.kneighbors(data)
# 取第k近的距离(排除自身)
scores = distances[:, -1]
threshold = np.percentile(scores, threshold_percentile)
anomalies = [i for i, s in enumerate(scores) if s > threshold]
return anomalies, scores
# 示例:二维数据
X = np.array([[1, 2], [2, 3], [1, 1], [10, 10], [2, 2], [1.5, 2.5]])
anomalies, scores = knn_anomaly_detection(X, k=3)
print(f"异常索引: {anomalies}") # 输出: [3]
KNN 的优点是简单直观,但有个致命问题——计算复杂度高。数据量一大,O(n²) 的复杂度会让你等到怀疑人生。我在一个百万级设备的数据集上跑过一次,等了半小时还没出结果。后来改用 KD-Tree 加速,才把时间压到几分钟。
优化建议:数据量大时,用 sklearn 的 NearestNeighbors 并设置 algorithm='kd_tree' 或 'ball_tree'。k 值一般取 5-20,太小容易受噪声影响,太大则可能平滑掉局部异常。
4.3 基于密度的方法:LOF
LOF(Local Outlier Factor)是我个人最喜欢的算法之一。它不像 KNN 那样只看全局距离,而是考虑局部密度。说白了,就是看一个点周围的「拥挤程度」跟邻居比是否异常。
LOF 的核心思想:
- 计算每个点的局部密度(基于 k 近邻的距离)
- 比较该点的密度与其邻居的平均密度
- 如果该点密度远低于邻居,LOF 值 > 1,标记为异常
LOF 值接近 1 表示正常,远大于 1 表示异常。一般取 LOF > 1.5 或 2 作为阈值。
from sklearn.neighbors import LocalOutlierFactor
def lof_detection(data, contamination=0.1):
lof = LocalOutlierFactor(n_neighbors=20, contamination=contamination)
y_pred = lof.fit_predict(data)
# -1 表示异常,1 表示正常
anomalies = [i for i, pred in enumerate(y_pred) if pred == -1]
scores = -lof.negative_outlier_factor_ # LOF 分数
return anomalies, scores
# 示例
X = np.array([[1, 2], [2, 3], [1, 1], [10, 10], [2, 2], [1.5, 2.5], [9, 11]])
anomalies, scores = lof_detection(X, contamination=0.2)
print(f"异常索引: {anomalies}") # 输出: [3, 6]
LOF 的优势在于能发现局部异常。举个例子:一个温度传感器在高温区(比如 80-90°C)出现一个 85°C 的读数,在全局看可能正常,但在局部看它周围的点都是 82-83°C,那 85°C 就是异常。KNN 可能漏掉这种情况,但 LOF 不会。
避坑指南:我曾经在一个数据分布极度不均匀的数据集上直接用 LOF,结果把正常的高密度区域边缘点全判成了异常。后来发现是 n_neighbors 设得太小了。建议 n_neighbors 至少取 10,且不要超过样本数的 10%。
4.4 三种方法对比
说了这么多,到底该用哪个?我整理了一张对比表,方便你快速决策:
| 方法 | 核心假设 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Z-Score | 数据服从正态分布 | 计算极快,解释性强 | 对分布敏感,受异常值影响大 | 单变量、数据量大、分布已知 |
| IQR | 数据有分位数结构 | 鲁棒性强,不受极端值影响 | 对样本量有要求 | 单变量、分布未知、有离群点 |
| KNN | 异常点远离邻居 | 直观,支持多变量 | 计算复杂度高,k 值敏感 | 中小规模、多变量、全局异常 |
| LOF | 异常点局部密度低 | 能发现局部异常,鲁棒性好 | 参数敏感,计算量较大 | 密度不均匀、局部异常场景 |
4.5 实战选择策略
在实际项目中,我通常按这个流程来选算法:
- 先看数据维度:单变量?用 Z-Score 或 IQR。多变量?考虑 KNN 或 LOF。
- 再看数据分布:正态分布?Z-Score 首选。偏态或未知?IQR 更安全。
- 评估计算资源:数据量大(>10万条)?优先统计方法。数据量小?KNN 和 LOF 都可以。
- 考虑异常类型:全局异常用 KNN,局部异常用 LOF。
我记得有一次做智能电表的异常检测,数据有 50 万条、20 个维度。一开始用 LOF,跑了 10 分钟没出结果。后来改用 Z-Score 对每个维度单独检测,再取并集,30 秒搞定,效果还过得去。所以说,别一味追求复杂算法,简单方法往往更实用。
我的建议:先用统计方法快速过滤掉明显异常,再用 KNN 或 LOF 做精细检测。这种「两阶段法」在工业界很常见,效率和效果都能兼顾。