3、基于统计的异常检测:均值-方差阈值法、移动平均与指数平滑、Z-Score与IQR离群点检测、CUSUM累积和算法实战
各位好,欢迎来到第三讲。今天我们要聊的,是流量分析里最基础也最实用的一类方法——基于统计的异常检测。
说白了,就是利用数学上的统计量,来判断当前流量是不是「不对劲」。我刚开始做安全运维那会儿,还没有什么花里胡哨的AI模型,全靠这些统计方法撑场面。你别看它们简单,用好了,效果一点不比深度学习差。
3.1 均值-方差阈值法:最朴素的直觉
这个方法,我估计你一听就懂。它的核心思想就一句话:正常流量应该集中在均值附近,离均值太远的,就是异常。
具体怎么做?我们先计算历史流量的均值 μ 和标准差 σ。然后设定一个阈值 k,通常是 2 或 3。如果当前流量值 x 满足:
|x - μ| > k * σ
那就判定为异常。
嗯,这里要注意:k 值怎么选?我个人的习惯是,对延迟敏感的业务用 k=2,对误报容忍度低的用 k=3。为什么?因为正态分布下,k=3 覆盖了 99.7% 的正常数据,误报率很低。
3.2 移动平均与指数平滑:让阈值「动」起来
均值-方差法有个硬伤:它假设流量是平稳的。但现实中的流量,往往有趋势和季节性。这时候,移动平均就派上用场了。
简单移动平均(SMA):取最近 N 个点的平均值作为基线。N 越大,曲线越平滑,但对突变的响应越慢。
指数加权移动平均(EWMA):给近期的数据更高的权重。公式是:
s_t = α * x_t + (1 - α) * s_{t-1}
其中 α 是平滑因子,一般在 0.1 到 0.3 之间。α 越大,对近期变化越敏感。
我建议你试试 EWMA。为什么?因为它计算简单,只需要记住上一个平滑值,内存占用几乎为零。在百万级 QPS 的场景下,这个优势非常明显。
3.3 Z-Score 与 IQR:两种离群点检测思路
这两种方法,都是用来找「离群点」的。但它们的思路完全不同。
Z-Score:基于均值和标准差。公式是:
z = (x - μ) / σ
z 的绝对值越大,说明 x 越异常。通常 |z| > 3 就认为是异常。
但 Z-Score 有个前提:数据要近似正态分布。如果数据本身偏态严重,Z-Score 就不太靠谱了。
IQR(四分位距法):不依赖正态分布假设。它用中位数和四分位数来定义正常范围:
IQR = Q3 - Q1
下限 = Q1 - 1.5 * IQR
上限 = Q3 + 1.5 * IQR
超出这个范围的就是异常。
我个人更偏爱 IQR。为什么?因为它对极端值不敏感。你想想看,如果数据里已经混入了几个巨大的异常值,均值和标准差都会被「带偏」,Z-Score 就失效了。但 IQR 用的是中位数和四分位数,那几个异常值基本不影响它。
3.4 CUSUM 累积和算法:捕捉微小偏移
前面几种方法,都是检测「大幅突变」的。但有些攻击,比如慢速扫描、低频数据窃取,流量变化非常微小,单点看根本看不出来。
这时候,CUSUM(累积和)算法就登场了。
CUSUM 的核心思想是:把微小的偏移累积起来,直到累积量超过阈值。
它的计算过程是这样的:
S_0 = 0
S_t = max(0, S_{t-1} + (x_t - μ - k))
其中 μ 是目标均值,k 是允许的偏移量(通常设为 0.5σ),h 是决策阈值(通常设为 4σ 或 5σ)。当 S_t > h 时,报警。
你可能会问:为什么要用 max(0, ...)?因为 CUSUM 只关心正向偏移。如果你想同时检测正向和负向偏移,需要两个方向的 CUSUM。
3.5 四种方法对比与选型建议
说了这么多,到底该用哪个?我整理了一个表格,方便你对照:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 均值-方差阈值法 | 平稳流量,快速检测大幅突变 | 简单直观,计算快 | 对趋势和周期性敏感,容易误报 |
| 移动平均/指数平滑 | 有趋势或季节性的流量 | 能自适应变化,内存占用低 | 对突变响应有延迟 |
| Z-Score | 近似正态分布的数据 | 标准化后易于比较 | 对极端值敏感,不适用于偏态分布 |
| IQR | 非正态分布、可能被污染的数据 | 鲁棒性强,不受极端值影响 | 对样本量有要求,小样本效果差 |
| CUSUM | 检测微小但持续的偏移 | 灵敏度高,能捕捉累积效应 | 参数调优较复杂,需要先验知识 |
我的建议是:不要只用一种方法。在实际项目中,我通常会把 IQR 和 CUSUM 结合起来用。IQR 负责抓大幅异常,CUSUM 负责抓微小偏移。两者互补,效果很好。
3.6 实战代码示例:用 Python 实现 CUSUM
光说不练假把式。我给你一段我常用的 CUSUM 实现代码,你可以直接拿去用:
import numpy as np
def cusum_detector(data, target_mean, k=0.5, h=4.0):
"""
CUSUM 累积和异常检测
:param data: 输入流量序列 (list or np.array)
:param target_mean: 目标均值(正常流量均值)
:param k: 允许的偏移量,通常设为 0.5 * std
:param h: 决策阈值,通常设为 4 * std 或 5 * std
:return: 报警点索引列表
"""
s = 0
alarms = []
for i, x in enumerate(data):
s = max(0, s + (x - target_mean - k))
if s > h:
alarms.append(i)
s = 0 # 报警后重置
return alarms
# 示例用法
np.random.seed(42)
normal = np.random.normal(100, 10, 200) # 正常流量,均值100,标准差10
attack = np.random.normal(108, 10, 50) # 攻击流量,均值偏移8
data = np.concatenate([normal, attack, normal])
alarms = cusum_detector(data, target_mean=100, k=5, h=40)
print(f"报警点索引: {alarms}")
这段代码里,我设了 k=5(0.5σ),h=40(4σ)。你跑一下就会发现,攻击流量开始后大约 10-15 个点,CUSUM 就开始报警了。这就是累积效应的威力。
好了,这一讲的内容就到这里。统计方法虽然「古老」,但它们是流量分析的基石。你把这些方法吃透了,后面学机器学习模型会轻松很多。下一讲,我们会进入时间序列分解的世界,看看如何把流量拆解成趋势、季节性和残差三部分。到时候见。