2.时序异常检测入门到实战:评估的陷阱:point-adjust 如何把烂模型刷成 SOTA
2026/7/26 7:06:58 网站建设 项目流程

时序异常检测入门到实战(二)· 评估的陷阱: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至少蒙中一个的概率
1040%
5092%
10099.4%
20099.99%

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

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

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

亲手跑一遍,眼见为实

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

importnumpyasnpdefpoint_adjust(pred,label):"""标准 point-adjust:真实异常段内只要命中≥1 个点,整段全部改判为检出。"""pred,label=pred.astype(bool).copy(),label.astype(bool)n,i=len(label),0whilei<n:iflabel[i]:j=iwhilej<nandlabel[j]:# 找出这一整段连续异常 [i, j)j+=1ifpred[i:j].any():# 段内蒙中至少一个pred[i:j]=True# → 整段点亮i=jelse:i+=1returnpreddefprf(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)returnp,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)forbinrange(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,本质上是在"贴合运维直觉"和"防止指标注水"之间调哪一个旋钮?


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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询