从正常走到异常: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/) 转载或引用必须申明原指尖魔法屋来源及源地址!