
简介面向时间序列异常检测研究者与算法开发人员这份可运行源码包整理了 SMD、SMAP/MSL、SWaT、WADI 等常用多变量基准数据集并提供标准化 Python 预处理代码解决不同来源数据时间格式不统一、异常标签格式混乱等常见问题可直接用于算法效果对比和模型验证。包内共 10 个文件以 csv 数据文件为主配套 py 处理脚本、txt 说明和 html 预览页面便于对照理解字段含义并直接运行调试压缩包整体约 30MB整体轻量、便于快速下载。目前已有 502 人浏览学习。按目录可快速定位 MSL、SMAP、SMD、WADI、SWaT 的处理入口脚本覆盖时间戳统一、标签提取与数据标准化等关键步骤能直接支撑异常检测算法训练、基准测试和复现对比显著减少数据清洗与预处理耗时既适合科研实验与论文复现也可用于课程设计和工程入门。 做多变量时间序列异常检测差不多一年算下来最耗时间的不是调模型而是跟数据集较劲。开源模型一个接一个但每次想复现论文光是搞明白数据从哪下、什么格式、怎么切成训练集和测试集就能耗掉一两天。这篇博文把我实际整理过的主流多变量时间序列异常检测数据集、踩过的坑以及一套能直接跑通的最小源码都端出来给正准备入坑这个方向的兄弟做个参考。无论你是刚接触异常检测的新手还是被各种数据集格式折腾到崩溃的老手这篇内容应该都能帮你省下不少时间。1. 多变量异常检测为什么数据比模型更值得较真1.1 多变量比单变量难在哪里单变量时间序列异常检测看的是“这条曲线自己有没有突变”比如CPU使用率从20%跳到95%一眼就能看出来。但多变量时序数据的每一个采样点都是一个向量比如传感器A、B、C同时读数单看任何一个指标可能完全正常三个指标的组合却违背了系统运行规律这种情况单变量方法根本发现不了。所以多变量异常检测的核心在于捕获通道之间的相关性。SWaT数据集里的水位和流量服务器监控里的请求量和延迟它们之间都有复杂的联动关系。模型需要学习的是这种“正常联动模式”一旦模式被打破即使每个指标都在合理范围内也应该判定为异常。这也是为什么多变量场景下不能简单套用单变量的阈值方法必须用能建模相关性的模型比如自编码器、图神经网络、Transformer这类结构。1.2 数据集决定了你的实验可信度异常检测领域有一个尴尬的现实模型在某个数据集上效果好不代表在另一个数据集上效果也好。原因在于各数据集的异常类型、异常比例、噪声水平差异巨大。有的数据集异常是突然发生的点异常有的是缓慢爬坡的趋势异常有的则是周期性被打断的上下文异常。如果你的模型只在某一个数据集上验证结论的说服力相当有限。公开数据集的作用就是把训练集、测试集、异常标签都固定下来所有论文在这些统一基准上对比结果才具备可复现性和可比性。我在实际整理中体会到数据集的选择直接决定了论文实验的“观感”用SWaT这种工业场景数据集读者会认为你的方法能在关键基础设施上应用用SMD/PSM这种服务器指标数据集读者会认可你的方法对互联网业务有价值。反过来数据集预处理做得不规范比如归一化时把测试集统计量带进训练过程那么实验做出来的数字再漂亮也是空中楼阁。2. 主流多变量异常检测数据集逐个拆解2.1 SWaT和WADI工业控制系统的标准考题SWaTSecure Water Treatment是新加坡iTrust实验室搭建的六阶段城市水处理测试平台采集的数据包含51个传感器和执行器指标。正常数据约7天攻击数据约4天共包含36次攻击异常类型包括传感器篡改、执行器卡死、通信延迟等。这是工业异常检测论文里出场率最高的数据集之一几乎所有做CPS信息物理系统异常检测的工作都会拿它做验证。WADI是同一实验室推出的水分配系统数据集可以理解为SWaT的姊妹版。它包含14天正常数据和2天攻击数据攻击次数15次。相比SWaTWADI的正常数据更长攻击比例更低检测难度更高适合作为严格一点的评估基准。获取方式上SWaT和WADI都需要在iTrust官网填写申请表格说明用途和单位审核通过后才会收到数据邮件。这个流程通常要等几天不少同学卡在这一步建议尽早申请。另外一个常见坑是SWaT数据文件里时间戳格式五花八门不同版本的标签粒度不同有的只标出攻击段起始和结束位置有的已经精确到每个时间点。用之前务必确认清楚标签形式否则评估时会出现标签错位。2.2 SMAP和MSL来自太空探索的真实异常SMAPSoil Moisture Active Passive土壤湿度主被动探测卫星和MSLMars Science Laboratory火星科学实验室是NASA公开的两个真实航天器遥测数据集。它们虽然也属于多变量时间序列但结构和SWaT完全不同每个数据集被拆分成多个独立通道每个通道是一个长度不等的单序列各自带有异常标签。SMAP的维度在几十个通道量级MSL类似每个通道代表一种遥测指标比如电压、温度、功率等。航天器数据的异常往往对应真实设备故障所以异常模式非常“有据可查”不是人为构造的。这两个数据集以.npy格式存储通常还需要额外的length文件来标记每个通道的序列长度第一次接触可能会被这个结构搞晕。使用SMAP/MSL时有一个很值得注意的点不同通道的异常分布极不均衡某些通道甚至完全不含异常。如果直接把所有通道拼接成一个大批次训练模型容易偏向数据量大的正常通道。我的做法是先按通道分别标准化再统一切窗口确保每个通道在训练中的贡献相对均衡。2.3 SMD和PSM互联网公司产线上的真实战报SMDServer Machine Dataset来自一家大型互联网公司的28台服务器每台服务器采集38个指标包括CPU、内存、网络流量、磁盘IO等。整个数据集按时间划分前约3/4为训练数据后约1/4为测试数据异常比例非常低贴近真实运维场景。PSMPooled Server Metrics由eBay开源采集自eBay的多个应用服务器节点共26个指标。PSM的数据量比SMD小但时间跨度长包含了明显的周期性和趋势变化异常既有点异常也有上下文异常。因为来源是真实生产环境所以节假日流量突变、发布上线引发的指标波动都会被标记为异常这让模型必须具备较强的时间上下文建模能力。这两个数据集在GitHub上可以找到格式大多是CSV读取难度低。需要注意SMD的文件划分是按机器编号组织的要把每台机器的数据分别处理PSM则直接给了train.csv和test.csv。相比SWaT的申请流程这两个数据集用起来省心很多适合作为快速验证想法的第一站。2.4 各数据集横向对比速查数据集所属领域指标维度训练/测试规模异常类型获取难度论文出现频率SWaT工业水处理51维约7天正常 / 4天攻击点异常上下文异常需申请极高WADI城市水分配约123维14天正常 / 2天攻击点异常上下文异常需申请高SMAP卫星遥测多通道各通道不定点异常为主官网下载高MSL火星车遥测多通道各通道不定点异常上下文异常官网下载高SMD服务器监控38维/台约21天训练 / 7天测试点异常趋势异常GitHub下载极高PSM服务器监控26维约26天训练 / 约10天测试点异常上下文异常GitHub下载中高除了表格里的数据集NABNumenta Anomaly Benchmark也值得提一句。它是一个综合性的时序基准包含58条真实场景序列覆盖IT指标、交通流量、社交文本等单变量和多变量都有并且官方提供了统一的评分脚本。如果你想做纯算法横向对比NAB的评估体系比自定义脚本更有说服力。3. 拿到数据之后预处理管线才是能不能跑通的分水岭3.1 文件格式解析和标签对齐我刚开始接触这些数据集时最大的障碍不是模型而是数据格式五花八门。SWaT是CSVSMAP/MSL是.npySMD按机器目录拆分NAB又是JSON格式。每换一个数据集读取代码就要重写一遍。标签处理是重灾区。SWaT的标签文件通常是一列0/1但有的版本把“正常数据”和“攻击数据”放在不同的CSV里需要按时间戳拼接SMAP/MSL的标签伴随.npy提供但某些通道的标签包含缺失值SMD的标签粒度是每5分钟一个点需要确认和特征数据的采样频率是否一致。我的经验是先单独写一个小脚本把每个数据集的原始文件读取后保存成统一的“长表”格式包含时间戳、所有特征列、标签列后续所有实验都从这个标准格式出发避免重复踩坑。3.2 滑动窗口的窗口大小和步长怎么定多变量异常检测的输入几乎都是滑动窗口因为单个时间点看不出上下文。窗口大小没有标准答案但有一条经验法则先看数据的周期长度。如果数据有明显的24小时周期窗口至少要覆盖一个完整周期比如96个点每15分钟采样或1440个点每分钟采样如果数据没有明显周期先试100到200的窗口效果不好再加大到512。步长stride决定窗口重叠程度。步长为1时窗口数量最多模型见到的样本最丰富但训练时间暴增步长为窗口的1/4到1/2时可以在样本量和训练速度之间取得平衡。我做实验的习惯是先用stride10快速验证模型是否能收敛确定方向后再用stride1做正式训练。这里还要注意窗口标签的对齐通常采用“窗口末点对齐”规则把窗口最后一个时间点的标签当作该窗口的标签这样预测结果和原始时间点可以直接对应。3.3 归一化只在训练集上fit千万别偷看测试集数据归一化是看似简单却最容易出致命错误的地方。很多新手图省事把训练集和测试集拼在一起直接用sklearn的StandardScaler做transform这在时序异常检测里是大忌本质上是把测试集的均值方差泄漏到了训练过程中模型无形中“偷看”了未来信息测试指标自然虚高。正确做法是先对训练集调用fit_transform再对测试集单独调用transform。代码就是一行的事但背后的意义很重测试集必须模拟“模型没见过的新数据”任何用到测试集统计量的操作都会让实验失真。另外多变量数据的特征尺度差异往往很大比如温度可能是20到30流量可能是0.5到2.5不归一化会导致模型完全被大尺度特征主导基本学不到什么有效表示。4. 一套你能直接跑起来的最小源码4.1 为什么先选AutoEncoder作为baseline异常检测里最直观的思路就是自编码器先用正常数据训练一个编码-解码结构让模型学会压缩和重建正常模式然后在测试阶段计算重构误差。正常样本重构误差小异常样本因为不符合已学到的正常模式重构误差会明显偏大这就是判断依据。选AutoEncoder作为baseline有两个原因一是结构简单、训练稳定十分钟就能跑通二是它提供了一个清晰的对比锚点。你后续换LSTM-AE、Transformer、GNN等复杂模型时如果效果连AutoEncoder都打不过那说明问题大概率不在模型上而在数据处理或训练设置上。先跑通一个朴素基线再一步步加复杂度这是最稳的工程路线。4.2 源码从DataLoader到训练评估下面这份代码基于PyTorch实现假设你已经把某个多变量数据集整理成了三份CSVtrain.csv训练特征、test.csv测试特征、test_label.csv测试标签0/1列。代码做了标准化、滑动窗口、构建Dataset、训练AutoEncoder、计算重构误差和评估指标这几件事复制保存后调整文件路径就能跑。import numpy as np import pandas as pd import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset from sklearn.preprocessing import StandardScaler from sklearn.metrics import f1_score, precision_score, recall_score # ---------- 1. 读取与标准化 ---------- train_df pd.read_csv(train.csv, index_col0) test_df pd.read_csv(test.csv, index_col0) labels pd.read_csv(test_label.csv, index_col0).values.ravel() scaler StandardScaler() X_train scaler.fit_transform(train_df.values) # 只在训练集上fit X_test scaler.transform(test_df.values) # 测试集只用transform # ---------- 2. 滑动窗口切分 ---------- def make_windows(x, window100, stride10): windows [] for i in range(0, len(x) - window 1, stride): windows.append(x[i:i window]) return np.stack(windows) # 形状 [窗口数, window, 特征维度] X_train_w make_windows(X_train, window100, stride10) X_test_w make_windows(X_test, window100, stride10) label_w labels[99::10] # 窗口末点对齐100-199步长10 class WindowDataset(Dataset): def __init__(self, x): self.x torch.tensor(x, dtypetorch.float32) def __len__(self): return len(self.x) def __getitem__(self, idx): return self.x[idx] train_loader DataLoader(WindowDataset(X_train_w), batch_size256, shuffleTrue) test_loader DataLoader(WindowDataset(X_test_w), batch_size256, shuffleFalse) # ---------- 3. 简单的AutoEncoder模型 ---------- class AutoEncoder(nn.Module): def __init__(self, n_features, hidden32, latent8): super().__init__() self.encoder nn.Sequential( nn.Linear(n_features, hidden), nn.ReLU(), nn.Linear(hidden, latent) ) self.decoder nn.Sequential( nn.Linear(latent, hidden), nn.ReLU(), nn.Linear(hidden, n_features) ) def forward(self, x): return self.decoder(self.encoder(x)) device torch.device(cuda if torch.cuda.is_available() else cpu) model AutoEncoder(X_train_w.shape[-1]).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() # ---------- 4. 训练 ---------- model.train() for epoch in range(30): total_loss 0.0 for xb in train_loader: xb xb.to(device) loss criterion(model(xb), xb) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() print(fepoch {epoch1}, loss {total_loss / len(train_loader):.4f}) # ---------- 5. 测试与评估 ---------- model.eval() scores [] with torch.no_grad(): for xb in test_loader: xb xb.to(device) recon model(xb) mse torch.mean((recon - xb) ** 2, dim1) scores.extend(mse.cpu().numpy()) scores np.array(scores) threshold np.percentile(scores, 95) # 更严谨的做法是在验证集上选阈值 pred (scores threshold).astype(int) print(F1:, f1_score(label_w, pred)) print(Precision:, precision_score(label_w, pred)) print(Recall:, recall_score(label_w, pred))这里用MSE作为重构损失窗口内每个时间点、每个特征的重构误差取平均得到一个窗口级异常分数。如果你要做逐点point-wise预测可以把窗口内所有位置的重构误差按原始时间位置对齐再取每个时间点的均值或最大值。4.3 阈值选择和评估指标的坑代码里我直接用测试集分数的95%分位数作阈值这在快速验证时没问题但严谨来说属于“阈值泄漏”。如果测试集的异常比例远高于正常比例这种阈值选择方式会严重影响结果。更规范的做法是把训练集尾部切出一段作为验证集在验证集上根据F1或F1-beta选择最优阈值再对测试集做预测。学术论文里如果用了这种方式必须清楚说明阈值是在哪个集合上定的。评估指标方面最常用的是F1、Precision、Recall和AUC。多变量异常检测还有一个特殊的“point adjustment”策略如果算法预测出的连续异常段内存在至少一个真实异常点就把整个预测段视为正确预测。这个策略能把很多算法的F1从0.3拉到0.8视觉效果非常惊艳但它本质上是在奖励“命中片段”而非“精确定位”。这两年审稿人对point adjustment越来越敏感如果你在论文里用了一定要明文写出来否则容易被质疑结果真实性。5. 常见问题与排错实战5.1 数据集文件层面的坑SWaT/WADI申请邮件发过去等了一周没回复。这种情况很常见iTrust实验室的审核部门通常按批次处理不要在短时间内反复发邮件催。等待期间可以先用SMD或PSM跑通代码流程。SMAP的.npy文件里存在NaN值。航天遥测数据出现缺失值很常见处理方式有两种一是直接丢弃包含NaN的整条序列二是用前后有效值做插值。我的经验是如果某个通道NaN占比超过5%直接丢掉更好插值会引入虚假信息。SMD下载解压后发现文件编码不统一。有的文件是UTF-8有的文件带BOM头用pandas读取时会出现第一列列名多出“\ufeff”的情况。读取时指定encodingutf-8-sig可以规避大部分问题。5.2 训练环节的经典报错训练时loss变成NaN大概率是输入数据没有归一化或者学习率设置过大。先把学习率降到1e-4试一波再把输入数据用StandardScaler归一化基本能解决。显存不足模型明明不大但GPU爆了。问题出在滑动窗口切分上窗口数量巨大如果全部一次性加载到显存会非常吃内存。解决办法是使用DataLoader的batch机制让模型每次只处理一个batch的数据而不是一次性把全量测试集喂进去。loss下降很慢或者几乎不降。检查是不是输入特征中存在全零列比如某些传感器在采集期间一直是0。全零特征会让模型学到“这个维度不用重建”的捷径严重干扰训练。把这类列删掉或者单独过滤后重新训练。5.3 指标虚高的三个自查清单第一检查归一化是否存在数据穿越。标准做法是训练集fit、测试集transform一旦发现你写代码时不小心把concat之后的整体数据做了fit_transform立刻改正并重新训练。第二检查阈值是否选在测试集上。如果阈值是在测试集上根据标签调出来的那相当于用答案反推动作指标自然好看。严谨做法是用训练集尾部切出的验证集选阈值或者完全用无监督的固定百分位数。第三检查评估时是否无意中用了未来信息。最典型的错误是先用整个时间序列做平滑或填充再切训练集和测试集导致测试集的值被训练集阶段的信息影响。正确的预处理顺序应该是先切分再做平滑、填充、归一化等操作。写在最后的个人经验我现在的固定流程是拿到一个新数据集先跑通这份AutoEncoder代码把数据的读取、标准化、窗口切分、标签对齐、阈值选择和评估指标全部理顺然后再去换复杂模型。这样做的收获是能快速判断一个模型的真实增益到底来自算法本身还是来自某些容易被忽视的数据处理细节。做异常检测这一年多我最大的体会是这个领域的模型迭代很快但扎实的数据工程能力永远是稀缺品。后面如果你想继续深入可以把这里的AutoEncoder直接替换成LSTM-AutoEncoder或Transformer编码器只需要改模型类的forward部分数据管线完全不用动。先跑通再优化这比一开始就追求花哨模型靠谱得多。本文还有配套的精品资源点击获取