2.时序异常检测入门到实战:评估的陷阱:point-adjust 如何把烂模型刷成 SOTA

时序异常检测入门到实战(二)· 评估的陷阱:point-adjust 如何把烂模型刷成 SOTA

本文是「码海寻道」《时序异常检测入门到实战》系列的第 2 篇。上一篇结尾埋了个雷:有个叫 point-adjust 的流行评估方式,能把一个近乎随机 的模型 F1 刷到 0.9+,一度让整个领域的排行榜集体注水。这一篇就来引爆它。为什么先讲评估、再讲模型?因为评估搞错了,后面比什么都是白比------你会误以为一个花哨模型很强,其实它只是沾了指标漏洞的光。这是本系列的"随机切分翻车"时刻,请务必读完。

本篇几乎全是原理和推演,代码只有一段------但那一段能亲手把陷阱演示给你看,强烈建议跑一跑。


一个荒诞的事实:随机数生成器击败了 SOTA

先把结论摆在这儿,你可能不信:

2022 年,有研究者拿一个纯随机的异常打分器 (连数据都不看,直接掷骰子给每个点打个随机分),套上当时论文里通用的 point-adjust 评估,在多个公开 benchmark 上的 F1,超过了那几年一众精心设计的深度模型

这不是模型太菜,是尺子坏了。用一把会自己往高处走的尺子量身高,谁量都是姚明。今天我们就把这把尺子拆开,看看它坏在哪。


二、先把正确的尺子立起来

在拆穿错的之前,先复习上一篇立的规矩------异常检测该用的三个指标(准确率已经被我们扫地出门了):

  • 查准率 Precision :报警的里头,真是异常的比例。管的是"别乱叫"。
  • 查全率 Recall :真实异常里,被抓到的比例。管的是"别漏报"。
  • F1:两者的调和平均,一个数平衡"别乱叫"和"别漏报"。

这三个指标最朴素的算法,是**逐点(point-wise)**比对:把预测的异常点集合和真实的异常点集合摆一起,一个点一个点数------数对了几个(TP)、错报了几个(FP)、漏了几个(FN)。干净、诚实、没有歧义。

逐点 F1 本身没有任何问题。 出问题的,是有人嫌它"太严",动手改了它------改出来的那个变种,就是 point-adjust。


三、point-adjust 是怎么来的:一个善意的出发点

先说句公道话:point-adjust 的动机并不坏,甚至挺合理。

设想真实的运维场景:一次故障往往不是孤零零一个点,而是持续一段时间 (比如某服务卡了 10 分钟,这 10 分钟里的几百个采样点都被标为异常)。现在你的模型在这段故障里,只在第 3 分钟报了一次警,其余时刻没吭声。

按逐点算,这段有几百个异常点,你只命中 1 个,查全率低得惨不忍睹。可站在运维角度想------这不公平啊 :模型第 3 分钟就把警报拉响了,运维已经冲过去处理整个事件了,你何必揪着"后面几百个点你没继续报"不放?只要在一段故障里报了一次,就该算这段故障被发现了。

这个想法催生了 point-adjust:

point-adjust(点调整)规则 :对每一个真实的异常段,只要模型在段内命中了哪怕一个点 ,就把这整段的所有点,全部改判为"正确检出"。

听起来很体贴,对不对?麻烦就出在这个"哪怕一个点"上。


四、陷阱引爆:为什么它会把烂模型捧上天

我们把 point-adjust 的规则翻译成一句大白话:

在一段异常里蒙对一个点,整段几百个点就全白送给你当"命中"。

问题来了------在一长段里蒙中至少一个点,有多容易?

假设某个异常段有 L 个点 ,你的模型(哪怕纯随机)每个点有 p 的概率 会报警。那么"整段一个都没蒙中"的概率是 (1−p)^L,于是"至少蒙中一个"的概率是:

P(点亮整段) = 1 − (1−p)^L

代几个数进去感受一下,p 只取一个很小的 5%:

异常段长度 L 至少蒙中一个的概率
10 40%
50 92%
100 99.4%
200 99.99%

看懂了吗?只要异常段足够长,哪怕你完全靠掷骰子,也几乎必然能蒙中其中一个点 ------然后 point-adjust 大手一挥,把整段几百个点全判给你算命中。查全率于是趋近 100%,几乎是白送的。

而查准率呢?也垮不到哪去。因为你只需要报警很少一部分点(比如 5%),落在正常区的那些误报本就不多;而落在异常段里的哪怕一个点,都会"回本"式地点亮一整段真异常。一来二去,查准也被垫高了。查全接近满分、查准也不差,F1 自然虚高到 0.9+

一句话总结这个陷阱:point-adjust 把"检出一个点"的难度,偷偷换成了"整段全对"的奖励。异常段越长、异常占比越高,这个杠杆撬得越狠------最后连随机数都能骑着它登顶。

亲手跑一遍,眼见为实

空口无凭。下面这段完整代码,构造一批较长的异常段 ,然后让一个纯随机打分器(完全不看数据)分别在"原始逐点"和"point-adjust"两把尺子下打分。复制即跑:

python 复制代码
import numpy as np

def point_adjust(pred, label):
    """标准 point-adjust:真实异常段内只要命中≥1 个点,整段全部改判为检出。"""
    pred, label = pred.astype(bool).copy(), label.astype(bool)
    n, i = len(label), 0
    while i < n:
        if label[i]:
            j = i
            while j < n and label[j]:      # 找出这一整段连续异常 [i, j)
                j += 1
            if pred[i:j].any():            # 段内蒙中至少一个
                pred[i:j] = True           # → 整段点亮
            i = j
        else:
            i += 1
    return pred

def prf(pred, label):
    """逐点查准/查全/F1。"""
    pred, label = pred.astype(bool), label.astype(bool)
    tp = (pred & label).sum(); fp = (pred & ~label).sum(); fn = (~pred & label).sum()
    p = tp / (tp + fp + 1e-9); r = tp / (tp + fn + 1e-9)
    return p, r, 2 * p * r / (p + r + 1e-9)

rng = np.random.default_rng(0)
n = 10000
# 真实标签:10 段、每段长 200 的异常(占比 20%),互不重叠
label = np.zeros(n, dtype=bool)
for b in range(10):
    start = b * 1000 + rng.integers(0, 800)
    label[start:start + 200] = True

# 一个纯随机打分器:完全不看数据,掷骰子打分,报警分数最高的 5%
score = rng.random(n)
pred = score >= np.quantile(score, 0.95)

print("原始逐点   P/R/F1 = %.2f / %.2f / %.2f" % prf(pred, label))
print("point-adjust P/R/F1 = %.2f / %.2f / %.2f" % prf(point_adjust(pred, label), label))

你会看到类似这样的结果:

复制代码
原始逐点   P/R/F1 = 0.20 / 0.05 / 0.08
point-adjust P/R/F1 = 0.83 / 1.00 / 0.91

同一个随机模型,同一批数据,只是换了把尺子 :F1 从惨不忍睹的 0.08 跳到堪比论文 SOTA 的 0.91 ,查全率直接拉满到 1.00。而这个模型,连数据长什么样都没看过。把上面的段长 200 调大到 400,虚高会更夸张;这就是整个领域被误导了好几年的真相。


五、那正确的评估该怎么做

拆穿了陷阱,得给你可落地的替代方案。记住下面这套,能让你在读论文、做实验时不再被漂亮数字忽悠。

铁律:永远让"随机 / 朴素基准"先过一遍你的指标

这是本系列最想让你养成的评估习惯,和预测系列"任何模型先打败 Naive"是同一条筋:

拿一个纯随机打分器、或上一篇那个移动窗口 3σ,用你即将报告的那套指标算一遍。如果它们也能拿高分,那说明你的指标坏了,而不是你的模型好了。

就像上面的实验------一旦发现随机数在 point-adjust 下能到 0.91,你立刻就该警觉:这个 0.91 不值钱。这一步几乎零成本,却能帮你避开绝大多数自欺欺人的结论。

用对指标:三个更诚实的选择

  1. 逐点 PR-AUC(首选做主指标) 。前面所有麻烦都出在"先卡一个阈值、再算 F1 "------阈值一动,数字就变,容易被人挑最漂亮的那个报告。PR-AUC(查准-查全曲线下面积)不依赖任何单一阈值,它衡量的是模型"给异常打高分、给正常打低分"的整体排序能力。在极不平衡场景下,它比 ROC-AUC 更能反映真实水平(ROC-AUC 会被海量正常样本稀释得虚高)。

  2. 事件级 / 范围级指标(贴合业务,又不失真) 。如果你确实认同"一段故障报一次就够"的业务诉求,别用会崩坏的 point-adjust,改用更严谨的范围级指标 (如 range-based precision/recall、affiliation 指标):它们同时考量"是否检出、重叠多少、报得及不及时",既奖励"报一次算发现",又不会像 PA 那样把整段无条件白送。

  3. PA%K:给 point-adjust 打上补丁 。实在要用点调整,也别用"蒙中一个就点亮整段"的原始版。改用 PA%K------要求段内被命中的点**达到 K%(比如 20%)**才允许点亮整段。门槛一立,随机数就没法再靠蒙一个点薅羊毛了。

一个务实的报告姿势

别只报一个数。一篇诚实的异常检测评估,通常会同时 给出:阈值无关的 PR-AUC (看整体能力)+ 最优阈值下的逐点 F1 (看落地效果)+ 一条随机/朴素基准(当照妖镜)。三个一起看,漂亮数字才骗不了你。


小结

  • 领域曾集体踩坑:一个纯随机打分器point-adjust 下 F1 能到 0.9+,击败真模型------是尺子坏了,不是模型强
  • point-adjust 的规则是"段内蒙中一个点,整段全算检出 ",出发点(一段故障报一次就够)合理,但那个"一个点"是致命漏洞。
  • 数学根源:段长 L、报警率 p,蒙中整段的概率是 1−(1−p)^L段一长就趋近 1,查全率几乎白送,F1 虚高。
  • 正确姿势:永远拿随机/朴素基准过一遍指标 当照妖镜;主指标用阈值无关的 PR-AUC ;要事件级就用范围级指标PA%K,别用原始 point-adjust。

评估这把尺子校准好了,从下一篇起我们才能放心地上模型、真刀真枪地比。第 3 篇,正式请出全系列的及格线选手------移动窗口 3σ、箱线图、EWMA 这些统计基线,看这些"老古董"到底能打到什么程度,也给后面所有复杂模型立一个必须跨过的标杆。

思考题:PA%K 里的 K 该怎么定?把 K 定成 0% 会退化成什么(提示:就是原始 point-adjust)?把 K 定成 100% 又会苛刻到什么地步?这个 K,本质上是在"贴合运维直觉"和"防止指标注水"之间调哪一个旋钮?


校准尺子,比堆砌模型更重要。下一篇我们让统计基线登场,用几行不需要训练的老代码,定下整个系列的及格线。

相关推荐
第七页独白1 小时前
AI 赋能 APQP 项目管控!全星研发项目管理系统为制造企业创造多重核心价值
人工智能
心疼你的一切1 小时前
Build-Your-Own-X 实战指南:从复刻经典到掌握核心原理
人工智能·ai·aigc
鲜于言悠9051 小时前
Anthropic深夜王炸:Claude Opus 5上线,自带代码自检程序员福音
人工智能
雪隐1 小时前
AI股票小助手10-我用代码管住自己的手:一个普通人的市场观察笔记
前端·人工智能·后端
方方洛1 小时前
AI 教程系列-TUI 应用开发教程02-终端基础
人工智能
Alter12301 小时前
从堆算力到算存网协同,华为给出了Agent时代的新解法
人工智能·华为
道影子2 小时前
《黄帝内经》021章|津凝精晶 沉降为患
人工智能·深度学习·神经网络·算法·机器学习
ajassi20002 小时前
AI语音智能体架构解析(四)AI 语音终端(执行器官)
人工智能·ai·架构·ai编程
超自然祈祷2 小时前
对抗\仿真\推演的共同基础
人工智能