多变量时间序列异常检测这块我前前后后折腾了不少方案。最早用的也是先预测、再算残差的老路子后来复现了 GDN 和 KDA 这两个基线说实话越用越觉得别扭。直到我自己折腾出一个叫 CyFA 的思路直接扔掉 Delta Rule反而在几个数据集上同时超过了它们。这篇就把来龙去脉、原理细节和复现经验一次性说清楚。Delta Rule 不是指神经网络权重更新里的那个 delta 规则而是指异常检测里最常见的打分逻辑先预测下一步应该是什么再用真实值和预测值的差delta来判断异常。GDN 这类图偏差网络本质上就是套着图结构做预测然后拿残差说事。而 CyFA 从设计上就把这一步砍掉了——我们不预测未来我们只建模正常状态长什么样然后看样本能不能在循环映射中保持自身特征一致。1. 为什么想扔掉 Delta RuleGDN 与 KDA 让我头疼的地方1.1 先理清三个名字先说清楚这三个缩写到底指什么。GDN 全称是 Graph Deviation Network核心做法是先把传感器或特征之间的依赖关系建模成图再用图神经网络提取空间依赖配合时序卷积捕捉变化趋势最后预测某个窗口内的未来值异常分数来自预测值和实际观测的偏差。KDA 在我这里指的是 Kernel Density Attention一种基于核密度估计的注意力基线。它不直接预测数值而是用核密度函数估计正常样本在特征空间中的分布密度密度低的点被判为异常。这套思路逻辑上没问题但实际跑起来对核宽、特征维度、采样规模都很敏感。CyFA 是我自己起的缩写全称是 Cycle Feature Alignment中文可以叫循环特征对齐。它的核心不是估计残差也不是估计密度而是训练一组可逆的映射让正常样本在编码-重构-再编码的循环过程中保持特征几乎不变异常样本则会在循环中明显走样。这个循环一致性差异就成了异常分数。1.2 预测残差类方法的通病GDN 用预测残差这类方法有一个绕不开的痛点它把异常检测问题转化成了预测问题而预测模型的性能上限直接限制检测效果。工业传感器数据里很多异常恰恰发生在系统状态发生非线性突变的时候这时候模型还没来得及学到新模式预测误差变大确实能暴露异常。但在另外一些情况下异常只体现为变量之间的相关性被破坏单个变量的数值看起来依然平滑预测残差可能很小漏报就来了。另一个问题是残差分布不稳定。正常工况下的预测误差也不是平稳的早晚高峰、设备老化、工况切换都会让残差方差漂移。这意味着阈值得跟着动态调不然误报忽高忽低。GDN 里常见的做法是用偏差标准化来缓解但这是个打补丁的思路治标不治本。我实际复现过 GDN 之后最大的感受是它的效果很大程度取决于能不能把图结构学对。不同传感器之间互相影响有些是强耦合有些是虚假相关。一旦图学习模块把不相关的节点连上线预测残差里混入噪声异常分数就开始说谎。1.3 KDA 的另一类问题KDA 这类密度估计方法看上去避开了预测-差分的路线但它又带来新的麻烦。第一个麻烦是维度灾难传感器维度一高核密度估计需要的样本量呈指数增长实际训练数据根本不够。第二个麻烦是它对正常样本分布的形状有隐性假设高斯核假设局部是球形分布但真实工业数据的正常流形往往是弯曲的、多模态的密度值在这种流形上会误导判断。我试过把 KDA 接到自编码器的隐空间上用效果比直接在原始空间好一些但隐空间本身是用重构损失训出来的重构损失会把容易重建和正常粗暴地画等号。有些异常样本在数值上偏离正常范围但依然能被自编码器重建得七七八八这时候 KDA 就失灵了。这些尝试让我确定了一件事我想要一种既不依赖预测残差、又不依赖局部密度假设的检测方法。它应该直接学习正常样本的流形结构并且用结构保持程度来打分而不是用某个误差或密度阈值。2. CyFA 的核心思路用循环一致性替代残差判断2.1 什么叫循环特征对齐循环一致性这个概念如果你接触过无监督图像转换肯定不陌生。它的核心想法是假设有两个域A 域和 B 域学习一个把 A 转成 B 的映射再学习一个把 B 转回 A 的映射。把一张 A 域图片先转成 B再转回 A如果映射足够好得到的图片应该和原图几乎一样。这个过程叫循环重建。CyFA 把同样的逻辑搬到了时间序列异常检测上我们把正常样本当成 A 域把隐空间表示当成 B 域。训练一个编码器把样本映射到隐空间再训练一个解码器从隐空间重建回原始空间然后再过一遍编码器。正常样本因为落在学习过的流形上循环前后在隐空间里的位置基本不动。异常样本脱离了正常流形编码器把它强行拉回已知区域解码器再重建出来就和原样本对不上了这个对不上的程度就是异常分数。这就是为什么不需要 Delta Rule我们从来不计算真实值减预测值我们只看样本经过一次循环后自己和自己能对齐多少。对齐良好代表结构正常对齐崩溃代表结构异常检测信号来自数据自身的循环一致性而不是来自对未来值的预测。2.2 网络结构长什么样CyFA 的整体结构不复杂三个模块特征编码器、特征解码器、以及一个时序模式编码器。第一个模块负责把长度为 T、通道数为 C 的输入窗口压缩成隐向量第二个模块负责把隐向量还原成原始窗口第三个模块负责在隐空间里做时间步上的模式提取用来计算循环一致性损失。具体来说输入窗口先经过编码器得到隐向量 z然后解码器把 z 重建成窗口 x再把 x 重新喂给编码器得到 z。循环一致性损失就定义在 z 和 z 的距离上同时加上重建损失比较 x 和 x。从实现上看编码器可以用一维卷积加门控循环单元解码器用反卷积加上采样。隐向量维度我一般设成输入通道数的四分之一到二分之一太小会丢失细节太大会让异常样本也能轻松通过。这里没有固定答案得靠实验调。2.3 训练目标与正常性度量训练 CyFA 时我只用正常样本目标函数由三部分组成。第一项是重构损失比较原始窗口和解码器重建窗口之间的逐点均方误差这保证解码器能有效还原正常数据。第二项是循环一致性损失比较 z 和 z 的余弦距离或均方误差这保证编码器对经过一次完整循环的样本保持特征稳定。第三项是隐空间正则项约束 z 的分布不要太分散让正常样本在隐空间里聚成紧凑的流形。训练结束后评分阶段不再需要任何监督信息。对任意一个待测窗口我把它送进编码器得到 z解码器重建再编码得到 z异常分数定义为 z 和 z 的欧氏距离加上原始窗口和重建窗口 x 与 x 的差异加权合并。这个分数越高说明样本越难维持循环一致性也就越可能异常。这里有个细节值得强调循环一致性损失和重构损失是互补的。重构损失只盯着像素级还原循环损失盯着隐空间稳定性。只优化重构损失模型可能变成什么都能重建的老好人只优化循环损失模型可能学会偷懒把隐向量全部压成同一个点。两个一起优化才逼着网络去学习真实的结构约束。3. 不需要 Delta Rule 的检测分数是怎么算出来的3.1 从特征循环差异到异常分数具体评分时我先定义一个窗口 X长度为 48 个时间点步长可以按场景调整。X 经过编码器 E 得到隐向量 z然后解码器 G 把 z 重建成 X_hat。接着把 X_hat 再输入 E得到 z_hat。异常分数 S 由两部分构成S α * ||z - z_hat||₂ β * ||X - X_hat||₂前面是隐空间循环差异后面是重建差异。α 和 β 我一般取 0.6 和 0.4但这个比例需要根据数据维度调整。如果原始数据噪声比较大重建差异会偏大那就把 α 调高一点让隐空间稳定性占据主导。计算完得分后用一个滑动窗口对 S 做平滑减少单点尖峰造成的误报。然后设定阈值。阈值的选择我放在后面讲。这个评分方式没有用任何预测未来值的操作所以它天然不受预测模型误差漂移的影响。正常运行状态下z 和 z_hat 很接近S 维持在一个低水平异常出现时x 偏离正常流形E 强行拉回已知区域G 再重建就会变形z_hat 跟着偏移S 快速上升。3.2 为什么能超过先预测再差分预测残差类方法本质上是在回答一个问题未来应该是什么回答错了就报警。但回答错不等价于异常模型可能因为没见过的正常工况也回答错。CyFA 回答的问题变成这个东西是否保留了正常样本的结构规则一个样本就算数值上从未出现过只要它遵循正常传感器之间的相关性模式依然能在循环后保持对齐不会报警。这在多变量相关性异常上特别明显。我之前构造过一个模拟场景某个传感器数值完全在正常范围内但它和相邻传感器的同步关系突然被打破。预测类方法看单变量残差几乎发现不了而 CyFA 在循环对齐时因为输入窗口里各通道间的联合结构已经异常隐向量无法同时还原所有通道z 和 z_hat 出现明显偏差异常被判出来了。另一个原因是自回归预测的误差具有累积性。预测步长越长误差越大这会让检测器对突变的敏感性下降。CyFA 是单步循环不存在误差累积问题一个窗口直接编码、重建、再编码整个过程是定长的没有时间递推所以响应速度也更快单窗口推理时间大约只有自回归预测的一半。3.3 阈值选定和实时性考量阈值我一般分两步定。第一步在验证集正常样本上计算 S 的均值和标准差把初始阈值设成均值加三倍标准差。第二步再看验证集里标注异常样本的 S 分布如果发现异常样本最低分在阈值以下就往下调直到保证召回率在 95% 以上然后再用另一段验证数据看误报尽量把阈值往上收。实时性方面CyFA 的推理只有一次前向传播前向过程不涉及递归解可以用批处理同时打分多个窗口。单块消费级显卡上窗口长度 48、通道数 32、隐维度 16 的情况下我测过大约 0.8 毫秒一个窗口完全扛得住秒级采样的在线监控。边缘设备上把编码器换成轻量卷积结构也能跑到 10 毫秒以内。要注意的是滑动窗口的重叠不要设得太大重叠率高了以后一个异常事件会被重复打分很多次输出会产生拖尾阈值和报警逻辑都得相应处理。4. 实验对比CyFA vs GDN vs KDA4.1 数据与设置对比实验我用了三个数据集分别模拟了三种典型场景。第一个是某工业过程数据包含 28 个传感器采集时长约一周里面包含设备退化、外部干扰和传感器卡死三类异常。第二个是某服务器监控数据有 15 个指标涵盖 CPU、内存、IO、网络流量异常主要是突刺和渐进式劣化。第三个是某能源设备数据9 个通道异常模式比较隐蔽属于相关性破坏型。所有数据先按时间顺序切分前 60% 作为训练集只含正常数据后 40% 作为测试集测试集中前 10% 作为阈值验证段。窗口长度统一设为 48步长 8所有方法都用相同的标准化方式逐通道零均值单位方差。GDN 的学习率是 0.001隐藏层维度 64KDA 核宽用中值启发式CyFA 隐藏层维度 32隐向量维度 16循环损失权重 0.5重建损失权重 1.0。评估指标用了 F1 分数和 F1 最大值后者是遍历所有阈值取最优的 F1反映方法本身的分辨能力不反映阈值工程水平。标注异常按事件粒度评估连续命中同一事件只算一次。4.2 结果表格下面给出平均结果。方法数据集一 F1数据集一 F1-max数据集二 F1数据集二 F1-max数据集三 F1数据集三 F1-maxGDN0.8210.8660.7840.8130.7010.744KDA0.7650.8090.7230.7610.6880.715CyFA0.8720.9010.8310.8590.7830.817第三个数据集上差距最明显CyFA 比 GDN 的 F1 高出 8 个多百分点。这正对应了前面说的场景当异常主要是相关性破坏而非单点突变时预测残差失效循环对齐反而敏感。第一个数据集上GDN 的表现其实不差因为三种异常都伴随明显的数值跳变预测残差能抓住。CyFA 仍然略胜主要靠更低的误报率。KDA 整体垫底尤其在第三个数据集上密度估计对弯曲流形处理得很吃力。我检查过它的检测结果很多假阳性都出现在正常工况切换的过渡带附近密度估计在这些位置很不稳定。4.3 消融实验循环损失到底起了多大作用为了确认循环一致性不是摆设我做了三组消融。第一组去掉循环一致性损失只保留重构损失相当于一个普通自编码器。第二组去掉重构损失只保留循环一致性损失。第三组是完整 CyFA。去掉循环一致性后F1 在两个数据集上分别跌了 9% 和 11%而且在相关性破坏异常上几乎失效。这说明靠重构误差打天下的自编码器确实容易陷入重建得好不一定正常的陷阱。去掉重构损失后F1 也跌了 5% 左右表现为正常样本的评分波动变大误报增多。两个损失一起用模型才既能学习流形的几何结构又能保证隐空间映射的稳定性。我还试过把循环次数从 1 次加到 3 次也就是让样本在编码器和解码器之间循环三个来回再计算损失。结果 F1 提升不到 0.5%推理耗时却变成了三倍。所以最终版本就采用单循环简单高效。这个实验也说明循环一致性的价值在第一轮循环中就已经充分体现多循环带来的边际收益太低。5. 复现 CyFA 时我踩过的坑5.1 循环损失权重太大导致模式坍塌我最开始调参的时候把循环一致性损失的权重设成了和重构损失一样结果训练到一半隐空间里的所有 z 全部挤到一个很小的区域。正常样本还好一旦有异常样本进来编码器也毫不犹豫地把它们映射到同一个区域解码器再重建出来循环差异非常小异常检测直接失效。后来我才意识到循环一致性损失太大会让编码器找到一种答案完全不变的捷径。隐空间分布坍缩成单点后任何输入都能完美循环对齐模型退化成一个常数映射。解决办法是把循环损失权重降到重构损失的一半左右同时加上隐空间正则项强制 z 的分布靠近标准正态。这样既保留了循环约束又不会让隐空间失去表达能力。这个坑我强调一下如果你复现时发现异常分数几乎不区分正常和异常先怀疑隐空间是不是坍缩了。把 encoding 出来的 z 投影到二维可视化一下如果正常样本全部堆在一起基本就是权重失衡。5.2 数据标准化方式对结果影响极大我之前在第一个数据集上用的标准化方式是全量数据包含测试集计算均值方差。这样看起来正常实际是信息泄露测试集的分母比训练集更大方差被稀释导致阈值在训练段表现很好上线后误报成倍增加。正确的做法是只用训练集正常样本计算均值方差测试集全部用这个统计量来变换。还有很多细节要留意工业传感器里偶尔会有恒值通道标准化之后这些通道全是零编码器很容易忽略它们但这恰好是卡死异常最明显的信号。我在 CyFA 里额外保留了一个原始输入分支把标准化后的特征和原始特征拼接在一起作为输入恒值通道就能继续发挥作用。如果环境里的数据经常出现这种问题你也可以考虑用鲁棒标准化替换 Z-score对离群点不那么敏感。另外时间序列窗口拼接时不要打乱顺序CyFA 虽然不预测未来但它学习的是连续时间窗口里的模式把时间顺序打乱会抹掉序列依赖训练出来的模型对动态异常完全不敏感。我一开始吃过亏后来严格按时间顺序分批采样效果才恢复正常。5.3 对 KDA 基线公平性的一点提醒如果你也想拿 CyFA 和 KDA 对比有件事必须说清楚KDA 的核宽选择极度影响结果我试过用默认的 Scott 规则和用中值启发式F1 能差 10 个点以上。我在上面的实验里选择了对 KDA 更友好的中值启发式但即便如此它仍然低于 CyFA。这里有个很微妙的公平性问题很多人在对比时给基线方法用的是默认参数给自己的方法调了一大轮这不算超过只能算别人没认真调参。我在对比之前先花时间把 GDN 和 KDA 都调到各自的最优状态然后再把 CyFA 放上去。你不一定知道基线的最优状态是什么那就多做几次参数扫描宁可花两天调基线也不要让实验结果被人一句话推翻。还要注意评估口径。事件粒度评估和点粒度评估差异很大有的方法在点粒度上有优势但在事件粒度上会漏报连续异常的后半段。我在文中用的都是事件粒度如果你换成点粒度F1 会比表中数据低一些但 CyFA 相对 GDN、KDA 的排序没有变化。最后再分享一个我平时快速验证小技巧先别急着上复杂数据找一个只有几十个正常样本、两类异常模式的小模拟集跑通 CyFA 的完整流程。如果循环一致性在这个小集子上都不起作用别怀疑数据量先去检查编码器是不是太复杂、损失是不是没收敛。模型简化到只有两层卷积加一个线性映射都能在小集子上刷出一个可用的检测结果再换大数据集才有意义。