从正常走到异常:AI异常检测笔记
很多人一上来就讲AI异常检测笔记的全景图;我更想先把这次卡住的点说清楚。
为什么写这篇
最近在做一个日志监控系统的告警优化,之前用的方案是简单阈值:CPU 超过 80% 就报警、响应时间超过 3 秒就报警。结果每天被无效告警轰炸,真正出事的时候反而被淹没了。
问题不在于阈值设错了,而在于**“正常"和"异常"的判断太粗糙**。有些服务平时 CPU 就是 60%,偶尔冲到 85% 也正常;有些服务平时 10%,突然到 40% 就该看看了。后者才是真正的异常。
这篇就是记录我用异常检测算法重构告警规则的过程,从踩坑到大概能用,以及中间踩过的那些明显的坑。
需求其实很简单
一开始需求就两句话:
- 告警要准,真正有问题才响
- 不要漏掉之前没见过的问题
看着简单,落地的时候就发现了一堆问题:
- 没有标注数据:历史日志里没有"这是异常"的标注,只有一堆已经处理过的工单,但工单里只有结论没证据
- 异常种类太多:有时候是流量突增,有时候是某个接口变慢,有时候是数据库连接数泄露,没法用统一模式覆盖
- 要实时:等半小时后发现异常已经晚了
- 要低误报:运维团队已经被之前的告警疲劳折磨得对任何告警都不敏感了
最后定了几个边界条件:
- 只用无监督学习,因为没标注数据
- 能接受部分误报,但要把误报率压到可接受范围
- 告警要附带简单解释,不能只是"检测到异常”
- 先做单指标异常检测,多指标交叉验证以后再说
算法选择过程
一开始想得很简单:异常检测不就是找个模型拟合正常分布,然后看哪些点离得远吗?查了一圈后发现这领域算法多得让人眼花缭乱。
考试期的混乱
大概花了一周时间把常见算法过了一遍:
- 统计学方法:Z-score、IQR、Grubbs’ test。计算简单、速度快,但只适用于单峰分布。我们的指标很多都是长尾分布,一用就炸。
- 孤立森林:对高维数据效果好,但解释性差,告警出来后很难解释"为什么这个点异常"。
- 自编码器:能学到数据模式,但训练不稳定,而且需要调参,对于快速迭代不太友好。
- One-Class SVM:对小样本场景好,但对高维数据计算复杂,而且核函数选择很玄学。
- LOF (局部异常因子):考虑了局部密度,解释性好,但计算复杂度高,实时性不够。
- LSTM Autoencoder:能处理时序特征,但训练资源消耗大,而且对于突发的点异常敏感度不够。
最后选了个折中方案:对单指标先用统计学方法做快速判断,再用孤立森林做二次验证。解释性够用,计算成本可控,也能覆盖大部分场景。
# 单指标异常检测示例
import numpy as np
from sklearn.ensemble import IsolationForest
def detect_anomaly(values, contamination=0.05):
"""检测指标中的异常点
Args:
values: 数值型序列
contamination: 预期异常比例,调参的关键参数
Returns:
异常点索引列表
"""
# 先用 IQR 过滤明显异常
q1 = np.percentile(values, 25)
q3 = np.percentile(values, 75)
iqr = q3 - q1
lower = q1 - 1.5 * iqr
upper = q3 + 1.5 * iqr
# 简单过滤
candidates = [i for i, val in enumerate(values) if val < lower or val > upper]
# 用孤立森林做二次验证
model = IsolationForest(contamination=contamination, random_state=42)
preds = model.fit_predict(values.reshape(-1, 1))
outliers = []
for i in candidates:
val = values[i]
if val < lower or val > upper:
if preds[i] == -1:
outliers.append(i)
return outliers
踩坑记录
坑一:contamination 参数的玄学调参
孤立森林的 contamination 参数控制预期异常比例,一开始设成 0.05,结果生产环境上线第一天就报警了 50 多次,实际只有 2 次是真实问题。
后来发现这个问题:
- 不同指标的正常区间差异很大:CPU 使用率的波动本来就比内存大,用同样的异常比例会漏掉内存的真正异常
- 时间尺度没考虑:按秒级监控时,正常业务高峰期间的波动会被误判为异常
最后改成动态调整 contamination:先统计过去 24 小时的波动范围,再根据指标的变异系数调整异常比例。稳定性指标用更低的异常比例,波动大的指标放宽一些。
def dynamic_contamination(values, base=0.05):
"""根据数据波动动态调整异常比例
Args:
values: 数值型序列
base: 基础异常比例
Returns:
调整后的异常比例
"""
cv = np.std(values) / np.mean(values) # 变异系数
if cv > 0.5:
# 波动大,放宽异常判断
return min(base * 2, 0.2)
elif cv < 0.2:
# 波动小,收紧异常判断
return max(base * 0.5, 0.01)
else:
return base
坑二:时序特征的丢失
一开始把监控数据当成独立样本处理,完全没考虑时间序列特征。结果发现:
- 周期性被当成异常:每天的业务高峰在模型眼里都是异常
- 趋势变化没捕捉到:内存缓慢增长这种渐进式异常,被模型当成了正常波动
后来加了一个预处理步骤:用 STL (Seasonal-Trend decomposition using LOESS) 分解出趋势和季节性分量,只对残差部分做异常检测。
from statsmodels.tsa.seasonal import STL
def remove_seasonality(values, period=24):
"""移除季节性成分
Args:
values: 时序数据
period: 季节性周期,例如 24 小时
Returns:
去除季节性后的残差
"""
stl = STL(values, period=period)
result = stl.fit()
return result.resid
坑三:单指标误报太多
单指标检测的问题在于:一个指标异常不一定说明有问题,可能是正常业务变化。比如流量增加时,CPU 和延迟都会上升,但这是正常的。
后面加了一个多指标交叉验证层:只有当多个相关指标同时出现异常时才触发告警。
def cross_validate_anomalies(metric_anomalies, min_metrics=2):
"""多指标交叉验证
Args:
metric_anomalies: dict {指标名: 异常时间点列表}
min_metrics: 最少需要多少个指标同时异常
Returns:
高置信度异常时间点
"""
all_points = set()
for points in metric_anomalies.values():
all_points.update(points)
confirmed_anomalies = []
for point in all_points:
count = sum(1 for points in metric_anomalies.values() if point in points)
if count >= min_metrics:
confirmed_anomalies.append(point)
return confirmed_anomalies
实现效果
折腾了一个月后,系统终于勉强能用了。跟之前的固定阈值方案对比:
| 指标 | 固定阈值方案 | 异常检测方案 |
|---|---|---|
| 每日告警数 | 120+ | 15-20 |
| 误报率 | 85% | 30% |
| 漏报率 | 10% | 5% |
| 平均响应时间 | 15 分钟 | 8 分钟 |
当然这数字只是内部测的,实际效果还得看运行一段时间再说。不过至少运维团队反馈:现在终于敢把告警通知打开微信了,之前都直接屏蔽了。
仍然没解决的问题
写了这么多坑,但还有几个问题暂时没想清楚:
- 异常归因难:能检测出异常,但很难说清楚"因为什么导致的"。现在只能把异常的相关指标都列出来,让人工判断。
- 冷启动问题:新上线服务没有历史数据,没法做训练。现在还是回退到固定阈值,等运行一周后再切到异常检测。
- 概念漂移:业务变化、系统升级后,正常模式会变化,模型需要重新训练。目前还没有自动化处理机制。
- 告警疲劳转移到算法调参:虽然误报少了,但现在运维团队开始抱怨参数调得太敏感或太宽松,本质上只是把问题转移了。
结语
异常检测不是银弹,它只是把"判断什么是正常"这个问题交给模型处理,但"什么算异常"仍然要人来定义。
这套方案对我们这种小团队算是够用了:不需要标注数据、计算成本可控、误报率在可接受范围。但如果你们团队有专职运维和标注资源,可能半监督或监督学习的效果会更好。
最后想说一句:模型能做的只是放大信号,但最终解释信号、做决策的还是人。别指望把所有判断都丢给模型,那只会带来新的问题。
附:完整代码和配置文件已经整理到 GitHub 仓库,有类似需求的可以直接拿去改。不过参数调优部分还得根据你们自己的数据特点来,别直接抄。
版权声明: 本文首发于 指尖魔法屋-从正常走到异常:AI异常检测笔记(https://blog.thinkmoon.cn/post/323-ai-anomaly-detection-normal-identify/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。