多模态异常检测实战:Transformer骨架与九份时间序列数据集解析 简介面向深度学习和异常检测研究者与开发者一份107.6MB的zip压缩包聚焦基于Transformer的多模态异常检测。资源共314个文件主类型包括164个npy数据文件、116个txt说明或标签、12个csv数据集、11个md教程文档、4个py源码脚本以及少量xlsx、json和pub文件。csv与npy提供多种合成及真实场景的异常检测数据涵盖机器温度、CPU利用率、纽约出租车流量、EC2请求延迟等常见用例py脚本实现Transformer自定义模块、数据加载与训练评估流程md与txt则给出项目介绍、运行步骤和数据集说明。对需要同时处理视觉、时序等多源数据并识别异常的读者来说这是一份可运行、可复现、可学习的完整实践包结合已有256人浏览/学习关注度适合用作课程设计、课题入门或算法对比的参考材料。整体目录包含src、README等模块能帮助读者快速理清代码、数据与说明文件的关系。1. 多模态异常检测的第一份动手项目九份数据集和一个 Transformer 骨架Transformer 和异常检测放到一起时大多数人第一反应是去研究 self-attention 公式但真正把这份资源跑通之后你会发现报错最多、结果最玄学的地方几乎全在数据预处理上。这套资源打包了九份 CSV 时间序列合成数据加真实监控日志、汇总标注文件和一个可扩展的 Transformer 训练骨架适合刚把 attention 背完、想尽快看到“重构误差在异常点上变尖”的新手也适合手上只有分类模型、想快速评估 Transformer 在时间序列异常检测上是否有增量价值的一线算法工程师。它解决的不是“把异常点标出来”这个表面问题而是如何在多个信号源并行输入时用自注意力找到真正偏离正常轨道的那一段。我的建议是下载后别急着改模型结构先按 README 把数据读进来把标签口径对齐后面所有调参才不是玄学。2. 数据集不是拿来就用的九份 CSV 的文件清单、时间字段与标签口径2.1 文件清单先过一遍这份资源里最容易被低估的是数据集。很多人下载后直接打开 synthetic_data_with_anomaly-s-1.csv 开始画图但真正值得先看的是 labeled_anomalies.csv它是全项目的“真值来源”。文件内容异常信号特点synthetic_data_with_anomaly-s-1.csv合成时间序列适合先跑通流程异常点人为注入形状干净labels.csv上述合成数据的逐点标签0/1 序列用于初版验证machine_temperature_system_failure.csv机器内部温度故障前温度持续爬升cpu_utilization_asg_misconfiguration.csv自动伸缩组配置错误引发的 CPU 行为突发性抬升平台型异常ec2_request_latency_system_failure.csv云主机请求延迟延迟骤升伴有周期性波动ambient_temperature_system_failure.csv环境温度慢漂移叠加尖峰nyc_taxi.csv纽约出租车客流量典型节假日/事件型异常rogue_agent_key_updown.csv安全凭证的按键按下序列短时高频凭证盗用特征rogue_agent_key_hold.csv安全凭证的按键持续序列与 updown 互补labeled_anomalies.csv各信号源异常事件窗口汇总窗口级真值非逐点这些文件并非互不相关temperature 系列可以看成一个数据源内部的多模态交叉rogue_agent 两件套本质是同一事件的两路观测。这一点会在后面多信号源并接时用到。真实监控日志的共性特征是正常段占比极高异常窗口往往只占整段数据的 3% 以下所以“无监督加重构误差”的思路天然适配这类问题。2.2 标签口径窗口级和逐点级要分开对待我在这个项目里反复强调“标签口径”因为它直接决定评估函数的写法。synthetic 数据配的 labels.csv 是逐点 0/1适合快速看 loss而 labeled_anomalies.csv 给的是事件发生的时间窗口也就是从某个时间戳开始异常出现持续一段区间。两种口径混用时最容易出问题有人直接把逐点 label 拿来训练结果真实数据的 label 是窗口级batch 拆完才发现对齐不上。我一般会把 labels.csv 当成“能画逐点 ROC 的玩具”把 labeled_anomalies.csv 当成“上线前必须过的事件级真值”。两份数据在代码里是两套评估逻辑不混用。训练阶段只用合成数据跑通验证阶段再切到真实监控数据这是这套资源最稳妥的使用顺序。2.3 读数的第一步时间戳先转 UTC 再排序这几份 CSV 的时间列几乎都是 ISO 格式字符串第一步不是检查空值而是先把字符串转成 datetime 并按时间排序否则后面滑动窗口会整体串位。import pandas as pd def load_ts(path: str, time_col: str timestamp, value_col: str value): df pd.read_csv(path) df[time_col] pd.to_datetime(df[time_col], utcTrue) df df.sort_values(time_col).reset_index(dropTrue) s df[value_col].astype(float) t df[time_col] return t, s t, s load_ts(data/machine_temperature_system_failure.csv) print(s.describe())这段代码做的事很简单但有两个参数容易踩坑utcTrue会把带时区偏移的字符串统一成 UTC避免不同时区的时间混合后波形错位sort_values保证后续窗口的相邻点在时间上真的相邻。value_col默认值是 value如果你导出的数据列名是 metric_value传参换掉即可。读进来以后先画一张全量曲线图确认异常窗口在视觉上“看得见”。如果一段数据画出来全是锯齿、完全看不出台阶或尖峰先去核对时间字段是否有重复时间戳或乱序而不是急着改模型。这一步不产生模型收益但能省掉后面几次无意义的返工。另外建议把每份 CSV 的采样间隔打印出来因为后面选 seq_len 时你必须知道一个窗口实际覆盖了多长时间跨度。3. 把时间序列喂给 Transformer滑窗、标准化与多信号源编码3.1 为什么是 Transformer以及“多模态”到底体现在哪里如果只是拿单条温度曲线做异常检测时序卷积或 GRU 已经够用Transformer 的增量不大。但这套资源里有两类场景会让 TCN/GRU 吃亏第一ec2_request_latency 这类信号异常往往在几分钟内缓慢展开模型需要记住十几分钟前的背景水平第二rogue_agent 两路信号属于同一事件的不同观测单看一路全是毛刺两路放在一起才能看出“按键节奏异常”。这种跨信号源的相关性正是自注意力擅长捕捉的。多模态在这里不等于图像加文本而是多路异构信号在同一时间轴上联合建模这是 Transformer 做多模态异常检测最常见的落地形态。所以我才建议你把 temperature 系列、rogue_agent 系列分别并接起来试而不是一股脑塞进一个大模型。Transformer 模型详解里反复讲 Q/K/V 对长距离依赖的捕捉能力它在时间序列上兑现的条件是输入序列的每个时间步代表一个稳定的时间语义。如果时间步是 5 分钟间隔模型学到的是 5 分钟粒度的上下文如果时间步忽长忽短位置编码就会失真。3.2 滑动窗口切分seq_len 和 stride 怎么定异常检测的常见做法是滑动窗口重构。把连续信号切成固定长度窗口模型学习用窗口内其他点预测被遮蔽的点正常点重构误差小异常点重构误差明显变大。import numpy as np from torch.utils.data import Dataset class WindowDataset(Dataset): def __init__(self, X: np.ndarray, seq_len: int 128, stride: int 16): # X: (N, n_sources)n_sources 是信号源数量 self.X X self.seq_len seq_len self.stride stride def __len__(self): return max(0, (len(self.X) - self.seq_len) // self.stride) def __getitem__(self, idx): start idx * self.stride end start self.seq_len return self.X[start:end].astype(np.float32) # (seq_len, n_sources)seq_len128 表示每个样本看 128 个时间步stride16 表示窗口每隔 16 步滑动一次。相邻两个样本有 112 步重叠训练样本量约等于原始序列长度的 1/16不会因为窗口重叠导致数据集膨胀到内存放不下。seq_len 的选择要和异常窗口宽度匹配如果某个异常持续了 30 个时间步seq_len 至少要能装下 60 步以上的上下文否则异常发生时模型看到的前后文不够重构误差反而被平均掉。这里要特别注意数据 shape 是 (seq_len, n_sources)不是 (n_sources, seq_len)。PyTorch 的 TransformerEncoderLayer 在 batch_firstTrue 时接受 (B, T, S) 形状T 是时间步。把时间维放第一维度模型内部每个时间步的 token 才能完整看到所有信号源在这一时刻的取值这正是“多模态融合”的最小单位。3.3 标准化必须按训练段拟合不许用全量标准化是看着简单、实际最容易把异常“洗掉”的步骤。若直接对整条序列做 StandardScaler异常点也参与计算均值和方差结果就是异常段被压扁重构误差变小模型把异常学成了正常。from sklearn.preprocessing import StandardScaler scaler StandardScaler() scaler.fit(X_train) # X_train 只包含训练阶段子序列 X_train_std scaler.transform(X_train) X_val_std scaler.transform(X_val) X_test_std scaler.transform(X_test)这里的顺序是硬约束先划分训练、验证、测试段再做标准化。如果在切分之前 fit scaler等于把未来的统计量泄漏给训练集后面验证指标全部虚高。多信号源并接时我习惯每个信号源单独 fit 一个 scaler而不是所有列一起 fit。温度和 CPU 利用率量纲不同放在一个 scaler 里数值大的维度会压制注意力权重。拼特征时还要留意时间对齐每个源必须使用同一个时间轴不能各自重采样。这套资源里的 CSV 都是等间隔采样直接 concat 即可如果某个源缺了中间一段先用前后值线性插值再参与并接。提示切分、标准化、窗口化这三步的顺序是硬约束任何一步提前都会造成信息泄漏排查时按这个顺序倒查。3.4 要不要把时间特征一起并接进去如果你直接跑 nyc_taxi很快会发现问题模型在每天固定时段都报异常因为出租车客流本身有强周期性而模型看不到“现在是几点、星期几”这个信息。常见做法是把小时、星期编码成向量拼在数值序列后面n_sources 从 1 变成 4 左右。Transformer 对这种额外输入很宽容每个 token 会自己权衡哪些维度重要。但我建议第一版不加时间特征先跑一个纯数值基线确认模型能 detect 出明显异常后再加时间特征对比。这样做的好处是你能清楚看到精度提升到底来自特征还是来自模型结构而不是一上来就把两件事混在一起调。4. 模型复现与训练循环自注意力编码器、掩码与参数怎么调4.1 一个最小的多信号源 Transformer 编码器src 目录里是模型定义、训练与评估脚本。拿到资源后先按 data_utils、model、train、eval 四个职责过一遍代码再看下面的最小复现。下面是支撑整个流程的模型结构也是这份资源源码里模型部分的典型整理方式。import torch import torch.nn as nn class MultimodalAnomalyEncoder(nn.Module): def __init__(self, n_sources: int, d_model: int 128, nhead: int 8, num_layers: int 4, dropout: float 0.1, max_len: int 500): super().__init__() # 每个时间步上的线性投射把 n_sources 维原始值映射到 d_model 维 self.proj nn.Linear(n_sources, d_model) # 可学习位置编码上限设 500超过就调大或换正弦编码 self.pos nn.Parameter(torch.randn(1, max_len, d_model) * 0.02) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward4 * d_model, dropoutdropout, batch_firstTrue, activationgelu ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.head nn.Linear(d_model, n_sources) def forward(self, x, key_padding_maskNone): # x: (B, T, S)B 是 batchT 是时间步S 是信号源数 h self.proj(x) self.pos[:, :x.size(1), :] h self.encoder(h, src_key_padding_maskkey_padding_mask) return self.head(h) # (B, T, S)与输入同形这段代码有两个关键设计。第一proj等价于给每路信号做一个独立 embedding让模型在进入注意力层之前先把各信号源的量纲差异消化掉pos是位置编码如果你的 seq_len 超过 500就把max_len调大否则切片越界。第二head把编码器输出映射回原始维度loss 可以在原始数值空间计算便于逐个信号源看重构误差。这个输出 shape 必须和输入完全一致后面训练循环里的 mask 才能直接对齐。key_padding_mask在等长窗口场景下可以先传 None。只有当不同窗口长度不一致时才需要给补齐位置打掩码这套资源里的数据生成流程保证了等长不需要过度设计。4.2 掩码重构训练循环模型结构定了训练的关键是“怎么制造监督信号”。这里走掩码重构路线输入序列里随机遮住一部分数值点让 Transformer 用上下文重建被遮住的点。正常点上下文充足重建误差小异常点偏离上下文重建误差大这个误差在推理阶段直接当异常分数用。import torch.nn.functional as F def train_one_epoch(model, loader, optimizer, device, mask_ratio0.15): model.train() total 0.0 count 0 for xb in loader: # xb: (B, T, S) xb xb.to(device) mask torch.rand_like(xb) mask_ratio # 元素级 15% 掩码 masked_xb xb.clone() masked_xb[mask] 0.0 pred model(masked_xb) # (B, T, S) loss F.mse_loss(pred[mask], xb[mask]) optimizer.zero_grad() loss.backward() optimizer.step() total loss.item() * mask.sum().item() count mask.sum().item() return total / max(1, count)这里的 mask 是空间点级别遮盖也就是遮某一时刻的某一路信号不是遮掉整个时间步。这个细节值得展开如果遮掉整个时间步模型很容易学会用相邻时间步直接插值正常点上的重构误差也会偏高按点遮才能逼模型利用跨信号源相关性来补全。mask_ratio0.15 是经验值可以小调到 0.1观察 loss 收敛情况但不要低于 0.05否则模型没有被逼着学上下文。推理阶段不 mask把整段测试序列按滑动窗口往前传输出和真实值逐点算 MSE得到每个时间步的异常分数。训练时 mask 的置零值我用 0.0如果你的数据没有标准化到 0 附近置零会被模型当成一个真实取值所以第 3 章反复强调标准化要先完成。4.3 参数表和推荐调参顺序参数建议值说明seq_len128必须大于异常窗口宽度的两倍d_model128信号源少于 4 路时 64 也够nhead8d_model 必须能被 nhead 整除num_layers4数据量小时先用 2 层防过拟合mask_ratio0.15低于 0.05 效果断崖dropout0.1数据少时提到 0.2batch_size64显存小就 32不要截断窗口lr1e-3AdamW 配 cosine 衰减调参顺序我固定成先固定 seq_len 和 d_model调 num_layers 看 val loss 是否持续下降再调 mask_ratio观察异常分数是否出现尖峰最后才动 lr。不建议一上来就搜 nhead 和 dropout这两个参数影响相对小却最耗时间。学习率这块用 AdamW 加 cosine 衰减给前 5% 的轮数做 warmupTransformer 训练初期 loss 不稳的现象会缓解很多。跑完一轮训练后把异常分数曲线叠到原始信号上看一眼。这份资源的好处是你能直接和 labeled_anomalies.csv 里的事件窗口对照曲线应该在窗口附近出现局部最高点如果最高点出现在窗口之前很远处优先怀疑第 3 章的标准化和窗口切分而不是模型结构。5. 跑通之前先看这里异常检测最常见的五个翻车点与排查5.1 验证集混进未来窗口指标虚高现象训练 loss 掉得很漂亮验证 loss 也很低但把模型挪到测试段后异常检测效果全无异常分数和随机抖动差不多。原因切分数据集时直接对所有窗口随机 shuffle验证集里混进了训练时刻之后的时间窗口又因为窗口之间互相重叠训练窗口和验证窗口可能只差一个 stride信息泄漏。模型等于在背答案验证指标自然好看。解决按时间顺序切块。前 70% 时间段的窗口给训练后 30% 给验证且切块边界留一个至少 seq_len 长度的 buffer杜绝验证集第一个窗口引用到训练集末尾的数据。我每次跑实验都会把这种切分逻辑打印出来确认训练结束时间小于验证开始时间才继续。5.2 标准化泄漏异常被“洗掉”现象某几路信号的异常分数几乎全部低于 1.0可视化时看不到尖峰只有把阈值调得很低才能勉强标出窗口。原因数据预处理时先对整份文件调用了 StandardScaler再切窗口。异常值参与了均值和方差计算异常段被压缩到正常范围内模型按“正常”把它们重构了。解决scaler 只 fit 训练段这是最低要求。更稳的做法是滑动窗口标准化对每个窗口内部做 z-score但这样会改变窗口间的数值分布和全局 scaler 两种策略只能选一种不要混用。我一般用全局训练段 scaler 加原始值重构因为异常分数保持真实量纲阈值语义更清晰。5.3 逐点 loss 把周期性当成异常现象在 nyc_taxi 上模型每天固定时段都输出高异常分数真实标注的节假日窗口反而淹没在周期性尖峰里精确率极低。原因逐点重构误差在周期数据的正常波峰处本来就偏高。模型学到的是“序列的大致形状”而不是“这个时间点该有的值”。没有把时间特征交给模型它只能把周期峰值当成偏离。解决给输入并接星期、小时等时间特征这是治本评估端对异常分数做平滑后加滑动中位数去趋势再按事件窗口找局部极大值这是治标。我通常两件事一起做这一组合能把节假日型异常明显浮出来。5.4 标签错位评估结果全乱现象事件级命中率算出来是 0%但把异常分数曲线叠到原始图上肉眼观察又“对得上”。原因labeled_anomalies.csv 里的时间戳是窗口起始时间把它当成逐点标签直接和分数序列逐位对齐等于把窗口起点当成唯一异常点逐位比对当然全部错开。解决写一个事件窗口解析函数把标注转成 [start, end] 区间评估时只要模型在窗口内触发过就算命中。另外确认 CSV 加载时时间戳有没有被解析成字符串我踩过字符串按字典序排序导致窗口错位的坑数据量一大肉眼根本看不出来。提示窗口级真值不要在评估函数里展开成逐点数组那会产生大量假阴性让命中率看起来永远不及格。5.5 复现不出昨天的 loss以为模型崩了现象同一份代码昨天跑出 val loss 0.42今天却变成 0.51调了半天参数也没回到昨天的水平。原因没有固定 seed。很多人只设了 torch.manual_seed漏了 Python random 和 numpy 的 seedGPU 上 cuDNN 的非确定性算法也会引入波动。多信号源并接时DataLoader 的 worker 线程数还会改变数据到达顺序。解决固定三处 seed同时设置 torch.backends.cudnn.deterministicTrue。DataLoader 设 worker0 或固定生成顺序。还有一个习惯值得养成记录每次实验的 scaler 参数和切分边界“昨天”和“今天”很多时候只是切分起点差了一个样本。排查这类问题时我习惯把样本序号、时间戳、异常分数 top-k 打成一个表逐行看高分数点是不是集中在少数窗口。如果高分数均匀散布在整个时间轴优先怀疑阈值选择而不是模型结构。6. 再用 NAB 的事件窗口给结果打分别只盯着重构 loss6.1 从逐点 loss 到事件级命中一份可以直接抄的评估函数很多人跑完这个项目会盯着重构 loss 曲线说“模型收敛了”但异常检测的交付标准从来不是 loss 多低而是事件命中率。这里用 labeled_anomalies.csv 的思路写一个事件级评估函数给定异常分数序列和异常窗口列表判断模型是否在窗口开始后的 delay 个点内触发。def event_hit_rate(scores: np.ndarray, windows: list, delay: int 20, q: float 0.99): # windows: [(start_idx, end_idx), ...] thr np.quantile(scores, q) anomalies np.where(scores thr)[0] hits 0 for ws, we in windows: hit any((ws a we delay) for a in anomalies) hits int(hit) return hits / len(windows)这个函数的核心是 delay20 的“宽限期”异常窗口开始后 20 个时间步内触发都算命中。提前触发多是被当成误报延后触发要打折扣只有在窗口附近触发才算有效这个思路借鉴了 NAB 的评估思想。q0.99 是初版阈值后续可以换成动态阈值比如分数序列的 p99 加一个绝对偏移。真正让这套流程可交付的是把分数曲线按窗口压缩成“事件分数”对每个标注窗口取窗口内分数最大值再和窗口外的分数分布比较。如果窗口内最大值稳定高于全序列 p99说明模型学到了该信号源的正常模式异常分数可信如果窗口内外分不开说明 seq_len 和 mask_ratio 选得不对回去调数据而不是继续堆模型层数。我自己第一次跑这批数据时只看 loss 曲线就存了模型直到用事件窗口评测才发现预测误差在异常窗口上只比正常高一点点。从那以后每次跑异常检测实验我都会强制走一遍事件级评估把 delay 阈值和 p99 一起打印到日志里。这套资源值得下载它把数据和标注口径都备齐了省去你自己造数据的时间但下载之后更值得花两小时把标签口径和切分逻辑对齐希望帮到你。本文还有配套的精品资源点击获取