☰
基于Python的故障预警系统:TimesNet与PatchTST时序模型实战
2026/10/3 8:52:58 网站建设 项目流程

简介:基于Python的故障预警系统设计源码,定位为面向时序异常检测和智能运维场景的完整工程,适合希望上手深度学习预警模型的研究者或工程师。整套资源共三十四个文件,压缩后约七十七兆,主要包含九个源文件、六个模型权重文件、三个配置文件、两个日志文件及一个交互式笔记文件,分别对应模型实现、训练产出、参数设置、运行记录和可视化分析。该项目已有一百四十一人学习,代码结构清晰,附有PatchTST、TimesNet等模型的训练评估脚本和预训练权重,可直接复现预测效果;同时数据处理、指标计算、模型封装等模块划分合理,便于二次开发或与自研算法对比。对于理解故障预警系统从数据流构建到模型推理的完整链路,具有很高的参考价值。

1. 基于Python的故障预警系统源码:不靠阈值硬扛,靠时序模型提前半步

做过设备监控的都知道,传统告警全靠阈值,温度过了80度才报警,等收到通知,设备已经磨出铁屑了。这套基于Python的故障预警系统设计源码,核心不是堆规则,而是用TimesNet和PatchTST两个时序模型去拟合正常模式,拿预测残差做异常判定。源码包里的结构很工程化:src下挂着数据预处理、模型定义、训练、评估一套完整链路,runs目录里还留着两组实验记录和按epoch保存的最佳权重。适合正在做设备预测性维护、服务器指标监控、工业异常检测的Python工程师,也适合想从传统阈值告警升级到时序深度模型的团队当脚手架。

2. 系统架构与数据链路:从原始采集到滑窗样本

2.1 模块划分与数据流向

这套源码的目录结构非常规整,我第一次打开的时候就注意到,它没有把代码堆成一个巨型脚本,而是按职责拆成了七个模块。src下放着processor.py、preprocessed.py、dataset.py、model.py、timesnet.py、train.py、evaluation.py、metrics.py、utils.py。这个拆法对应了一条完整的数据流:processor处理原始采集数据,preprocessed做清洗和归一化,dataset负责滑窗切分,model和timesnet定义网络结构,train启动训练,evaluation和metrics做离线评估和在线推理。

数据在模块间的流转顺序很关键。原始传感器数据先进processor.py,这一步解决的是“能不能用”的问题:字段筛选、缺失值标记、时间戳排序。然后交给preprocessed.py做统计变换,把不同量纲的指标拉到同一尺度。dataset.py拿到标准化后的数据,按固定窗口切出样本,每个样本是“过去N个时间步的特征”,标签是“未来一个时间步是否异常”。模型只认这个格式的输入。

代码里这块有一个容易忽略的细节,dataset.py里滑窗生成器的步长参数是可控的。步长等于窗口长度时,样本之间没有重叠,适合做快速的粗筛;步长小于窗口长度时,样本重叠度上升,数据量变大,模型能学到更平滑的模式,但训练时间也成倍增加。我一般会先用不重叠窗口跑通流程,确认模型能收敛后再调小步长做精细训练。

2.2 滑窗切分:窗口长度、步长与样本组织

滑窗是时序异常检测里最基础也最影响效果的一步。窗口长度直接决定了模型能“看到”多长的历史,设短了模型抓不住周期,设长了训练样本数量骤减。step是相邻窗口的起点间隔,它控制样本之间的重叠程度。这两者在代码里通常不是硬编码,而是从配置文件读入。下面是这套代码里dataset.py常见的一种实现思路,用numpy把一组多变量时间序列切成监督学习格式。

def sliding_window(data, window_size, step, horizon=1): samples, labels = [], [] for i in range(0, len(data) - window_size - horizon + 1, step): # 取从 i 开始、长度为 window_size 的窗口作为特征 sample = data[i : i + window_size, :] # 窗口结束后的 horizon 步作为预测目标,故障时置 1 label = int(np.any(data[i + window_size : i + window_size + horizon, -1])) samples.append(sample) labels.append(label) return np.array(samples), np.array(labels)

代码逻辑不复杂:外层循环按step步长在时间轴上滑动,每次截取window_size行作为样本特征,窗口结束后的horizon个时间步里只要有一个异常点,这个样本的标签就为1。最后一列放的是故障标签列,这是运维数据的常见约定,把标签和特征放在同一个二维数组里,切窗时一起切片,避免特征和标签错位。

参数调的时候有三条经验:一是window_size要覆盖至少两个完整周期,比如设备有24小时周期性,窗口就不能小于48小时;二是step设为window_size的一半时效果和样本量比较均衡,也就是50%重叠;三是horizon是预警提前量,horizon越大,模型对未来的预测误差越大,误报也会增多。这套源码里runs目录下有配置好的参数组合,直接用默认值能跑通,但换自己的数据时这三个值一定要重新标定。

2.3 归一化与缺失值处理:先切窗再fit,否则信息泄漏

归一化是这套代码里最常见的翻车点。很多人在切窗之前就对全量数据做了标准化,也就是用整个数据集的均值和方差去减,之后再切窗训练。这个顺序在离线实验里看不出问题,指标还很好看,但一部署到线上就露馅——线上拿不到未来数据,均值方差只能从训练集估计,数据分布一变,模型的预测残差整体偏移,误报率直线上升。

正确做法是先切窗,再用训练窗口的统计量做拟合,校验窗口只做变换不参与统计量计算。下面这段代码展示的是这套源码里preprocessed.py的常见处理逻辑:

from sklearn.preprocessing import StandardScaler # 训练集切窗前先拆分,防止未来信息进入统计量 train_raw = raw_data[:int(len(raw_data) * 0.7)] val_raw = raw_data[int(len(raw_data) * 0.7):] scaler = StandardScaler() # 只在训练集上 fit,验证集和测试集只 transform train_norm = scaler.fit_transform(train_raw) val_norm = scaler.transform(val_raw) # 后续再对 normalize 后的数据做滑窗切分 X_train, y_train = sliding_window(train_norm, window_size=48, step=24) X_val, y_val = sliding_window(val_norm, window_size=48, step=24)

这段代码的要点在fit和transform的分离。scaler.fit_transform在训练集上计算均值和标准差,val和线上推理阶段直接用这套统计量,不再更新。如果数据分布随时间漂移,比如设备老化导致温度整体上升,固定统计量会让模型误判为异常,这时候的解法是定期用最近一段正常数据重拟合scaler,我在第六章会讲动态阈值的处理思路。

对于缺失值,这套代码里的做法是前向填充,也就是用上一个有效值补缺口。工业采集数据经常出现某几秒断连,前向填充是保守做法,不会引入未来信息。如果缺失太严重,比如连续一小时没有数据,我建议直接丢弃该区间,而不是硬填,否则会造出一段人为的“平直”数据,干扰模型的周期识别。

3. 核心模型选型:TimesNet与PatchTST的互补逻辑

3.1 TimesNet:把一维时序折叠成二维,捕捉多周期特征

TimesNet在2023年提出,核心思路是“把一维时间序列展开成二维张量,再交给CNN处理”。为什么这么做?因为现实中的设备数据往往同时包含多个周期:电机的振动信号有高频分量,温度变化有小时级周期,负载规律有日级周期。这些周期混在一起,直接在原始一维序列上建模,模型很难区分哪些波动是周期内的正常变化,哪些是真正的异常前兆。TimesNet的做法是先用FFT找出序列里能量最高的几个频率,按频率把一维序列重塑成二维矩阵,再用2D卷积提取特征。这样做的好处是:不同周期被显式分开处理,模型能同时捕捉“这个点偏离了分钟级趋势”和“这个点偏离了日级规律”两类异常。

源码里timesnet.py实现了这个结构,其中频率选择这一段是核心,常见实现如下:

import torch.fft as fft def top_k_frequencies(x, k=3): # x: [batch, seq_len, channels] freq = fft.rfft(x, dim=1, norm="ortho") amplitude = freq.abs().mean(dim=(0, 2)) # 按序列维度平均幅度 topk_idx = amplitude.topk(k).indices return topk_idx # 选中幅度最大的 k 个频点

代码的思路是先把时域信号变换到频域,计算每个频率分量的平均振幅,然后选出振幅最大的k个频点。振幅大意味着这个频率成分在数据里占主导,是“重要周期”。这个k一般取3到5,太大会把噪声也当成周期,太小又抓不全多周期特征。TimesNet把这几个主导频率对应的周期长度作为reshape的依据,把序列折叠成多个二维平面,再对这些平面做二维卷积。每个平面代表一个周期视图,同一时刻的点在不同周期视图里被重复分析,最终按自适应权重融合。

这套源码里TimesNet和PatchTST两个模型都实现了,训练脚本允许你通过配置文件指定用哪一个。我在实际测试中,TimesNet对周期性强的工业数据更友好,训练收敛快,显存占用也小,适合作为第一版基线模型。

3.2 PatchTST:补丁化输入,长序列上的Transformer轻量化

PatchTST相比TimesNet走的是另一条路线。它把时间序列切成一段段的小patch,每个patch作为Transformer输入的一个token。这个方法解决了两个问题:一是序列太长时Transformer的自注意力计算量是平方级增长的,切成patch后序列长度骤降,计算量可控;二是把局部时间段作为一个整体输入,模型能捕捉短时间内的局部模式,而不是只见单个时间点。PatchTST在做故障预警时有一个明显优势:它对输入窗口长度不敏感,可以吃很长的历史序列,比如几百个时间步,这跟TimesNet对周期长度的依赖形成互补。

patch_len和stride是PatchTST最核心的两个参数。patch_len是每个patch的长度,stride是相邻两个patch的起始点间隔。源码的config.json里通常会这样配置:

{ "patch_len": 16, "stride": 8, "n_heads": 4, "d_model": 128, "enc_layers": 3 }

patch_len取16,stride取8,意味着一个长度为512的输入序列会被切成约63个patch,每个patch内部16个点整体参与计算。patch_len设大了,patch内部被压缩成一个向量,局部细节丢失,误报率上升;设小了,token数量变多,训练变慢且容易过拟合。stride小于patch_len提供了重叠信息,重叠部分越多,模型对边界的鲁棒性越好,但代价是计算量增大。我在用这套代码时,用patch_len=16、stride=8作为起步值,如果验证集效果不佳,先调stride而不是patch_len,因为patch_len对性能跳变的影响更剧烈。

PatchTST沿用了先预训练再微调的范式。runs目录下patchtst_best_map_epoch_0.pt到epoch_4.pt这些权重文件说明它做了多轮实验、每轮结束都保存了验证集上指标最优的权重。后面第4章我会讲这些权重文件应该怎么选、怎么加载,这是新手最容易忽略的一环。

3.3 config.json 参数映射与双实验目录设计

runs目录里有两个独立的实验文件夹,一个叫timesnet_20250122_152320,另一个叫patchtst_20250122_214748,各自下面都带一份config.json。这种“一次实验一套配置一个输出目录”的组织方式值得借鉴。它保证了实验的完全可复现——三个月后再看,模型结构是什么、数据怎么切、学习率多少,全都固化在那一份JSON里,不需要翻聊天记录回忆参数。

我对照config.json和训练产出整理了一张参数对照表,方便你在自定义数据集时做映射:

配置项 | 作用范围 | 典型值 | 调整方向 字段 | 含义 | 示例取值 | 调参建议 window_size | 输入窗口长度 | 48 | 至少覆盖2个周期 patch_len | patch长度 | 16 | 先固定,最后调 stride | patch滑动步长 | 8 | 优先调这个 lr | 初始学习率 | 1e-4 | 不收敛时调小 batch_size | 批大小 | 64 | 显存不足时减半 early_patience | 早停容忍轮数 | 10 | 波动大时增大 save_best | 按验证指标存权重 | true | 固定开启

这套源码的双实验设计还有一个好处:可以公平对比模型在相同数据下的表现差异。TimesNet训练日志里能看到loss曲线,PatchTST的实验里能看到best_map权重。对比时要注意保证window_size、数据切分方式完全一致,只替换模型结构,否则对比结果没有说服力。runs目录下两组实验的启动时间显示它们来自同一个训练流程,只是模型和配置不同,这是这套源码设计得比较完整的地方。

4. 训练与评估:从train.py到指标解读

4.1 训练入口与实验记录

train.py是整套系统的启动入口,它负责读取config.json、加载数据、实例化模型、启动训练循环、在验证集上评估、按条件保存权重。这套代码在训练管理上做得比较规范,每次运行都自动创建一个带时间戳的目录,训练过程中把配置和日志一并存入,形成完整的实验档案。这种习惯在工程实践里很值得学——我见过太多人训练完模型,隔一个月就忘了当时用的什么学习率,找参数比重新训练还痛苦。

训练主循环的核心逻辑是这个模式:

# 每个 epoch 结束时在验证集上计算评估指标 model.eval() with torch.no_grad(): val_pred = [] val_true = [] for batch in val_loader: pred, target = model(batch) val_pred.append(pred) val_true.append(target) # 计算当前验证集上的 MAP cur_score = evaluation.calculate_map(val_pred, val_true) if cur_score > best_score: best_score = cur_score torch.save(model.state_dict(), save_path) logger.info(f"Epoch {epoch}: best MAP = {cur_score:.4f}, model saved.")

这段代码展示了save_best机制的关键:每个epoch结束后在验证集上计算一次指标,如果指标优于历史最优,就把当前权重覆盖保存到磁盘。这就是为什么runs目录里有patchtst_best_map_epoch_0.pt到epoch_4.pt多个文件——它们分别对应不同epoch结束时达到的验证集最佳状态。注意保存的是model.state_dict()而不是整个model对象,前者只包含权重参数,文件体积小、加载灵活,不依赖模型类的定义路径。

日志系统的价值在这个环节最能体现。train.log里每行是一个时间戳加一条训练信息,包含当前epoch、训练loss、验证指标、学习率、已用时间。当训练异常中断时,这些日志是排查的第一手材料。如果你看到loss在某个epoch后不再下降,去查日志里对应时间段的学习率调度变化,多半能找到原因。这套源码的日志记录了一个完整的训练周期,从20250122_152320这个时间戳能看到训练大概跑了多久。

4.2 评估指标:MAE、MSE之外,故障预警更要看召回

evaluation.py和metrics.py负责评估。对故障预警系统来说,评估指标的选择直接决定模型往哪个方向优化。如果只看MSE或MAE,模型只要把正常波动预测得足够平滑,误差就能很低,但故障前的那点异常突变作为离群值,反而对MSE的贡献巨大,会让训练过程过分关注这些少数点,导致正常点拟合变差。

metrics.py里计算指标的方式上,这张表可以帮你理解不同指标的适用场景:

指标 | 计算公式 | 适用场景 | 注意点 字段 | 含义 | 建议用法 | 坑 MAE | 预测误差绝对值均值 | 跟踪训练收敛 | 对异常点不敏感 MSE | 预测误差平方均值 | 判断整体偏差 | 异常点会主导数值 Precision | 预测为故障中真实故障占比 | 告警准确率 | 低误报场景优先 Recall | 真实故障中被预测出的占比 | 漏报敏感场景 | 设备安全优先 F1 | 二者的调和平均 | 权衡场景 | 两个指标差太多时参考 MAP | 按排序加权的平均精度 | 模型选优 | 跨阈值比较友好

这列指标在类不平衡场景下的表现差别很大。故障数据在设备监控里是典型的小概率事件,可能一万个样本里只有三十个异常点。这种分布下,模型只要全部预测正常,准确率就有99.7%——纸面数据极其好看,实际上一个故障都没抓住。所以评估时不能只看accuracy,要盯recall和F1。这套代码里保存权重用的best_map指标,MAP对正负样本不平衡相对不敏感,比accuracy更适合做模型选优的基准。

evaluation.py还承担了另一个任务:回放验证。常见做法是把一段历史数据切成长窗口,模拟在线推理时逐窗口输入模型的场景,把每个窗口的预测残差和真实标签对照。有些模型指标好看,一放到回放里就漏报严重,原因是离线评估时每个窗口独立处理,而在线推理是流式的,模型对连续异常会逐渐适应。这套代码把评估和训练分开,evaluation.py不依赖训练时的数据封装,可以独立对任何时间段的历史数据做回放,这一步对部署前的模型体检很重要。

5. 故障预警系统避坑指南:五个翻车点,全是血泪经验

5.1 归一化泄漏:离线指标好看,线上误报爆炸

现象:训练时验证集F1能到0.9,部署到线上后连续三天狂报警,把值班同事烦到直接关了告警。

原因:代码在切窗之前对全量数据做了StandardScaler的fit_transform,验证集和测试集的统计量混进了scaler。离线评估时模型见过“未来”的均值,当然表现好;线上推理时拿到的是实时数据流,分布一变,残差全部偏移。

解决:严格遵循先切窗、后fit的顺序,scaler只在训练窗口上fit,验证和推理阶段只transform。这也是第2.3节那段代码的意义所在。从那以后我每接到一个时序项目,第一件事就是检查预处理脚本里fit和transform是否分离,这个习惯帮我避掉了至少三次误报事故。

5.2 时间戳不连续:窗口长度对,覆盖的真实时长不对

现象:模型的预警位置在时序图上看着有点偏,明明故障发生在下午3点,报警却在3点半才触发。

原因:设备数据采集频率不稳定,网络抖动、采集进程重启都会造成时间戳间隔不固定。dataset.py如果按行数切窗口,每个窗口虽然都是80行,但80行覆盖的真实时间跨度可能是10分钟到3个小时不等。模型学到的“周期”不是真实时间意义上的周期,而是“行数周期”。

解决:切窗前先检查时间戳间隔的分布,如果间隔波动超过20%,先按固定频率重采样。我一般用pandas的resample做等间隔化,间隔取采样频率的众数。重采样后缺失的槽位用前向填充,再进滑窗。记录真实时间戳而不是行号,出问题排查时能省一半时间。

5.3 PatchTST的patch_len和stride不是随便设的

现象:验证集上表现尚可,但观察误差曲线发现,模型对缓慢漂移的异常信号完全无感。故障是逐渐恶化的,前期变化幅度很小,模型把变化都“吸收”成了正常波动。

原因:patch_len设得过大,比如设成32,一个patch覆盖了太长时间段,缓慢漂移在patch内部被平均掉了,变成“没发生什么变化”。patch本质上是一个信息压缩单元,压缩粒度太粗,细微变化就被平滑掉了。

解决:patch_len对应的是你希望模型关注的“最短异常持续时长”。如果想捕捉持续5个采样点的尖峰,patch_len就设在5到8之间;如果想捕捉缓慢漂移,适当增大patch_len但配合causal padding。同时stride设成patch_len的一半,保留重叠信息。源码默认的patch_len=16、stride=8是个中庸值,换数据时必须重新调。

5.4 类别不平衡:不要被99%的accuracy骗了

现象:训练日志显示accuracy一路上升,最后停在99.2%,但实际故障一个都没抓到,漏报率100%。

原因:故障样本占比过低,模型只要输出“正常”就能把loss压得很低,没有任何动力去学异常模式。这是长尾分布在故障预警场景里的典型表现。

解决:一是评估指标换成recall和F1,不认accuracy;二是数据组织上做正负样本比例控制,让每个batch里故障样本占比不低于10%,比如对少数类做重复采样;三是loss层面给正样本加权,让模型对漏报付出更大的代价。这套代码里evaluation.py已经算了F1和recall,训练日志里记得把这两个指标打印出来,别只看loss。

5.5 权重文件加载错:best_map_epoch_4不一定比epoch_0好

现象:加载patchtst_best_map_epoch_4.pt部署,效果反而比epoch_0差。

原因:best_map保存的是验证集指标最优时的权重,但验证集和线上数据分布存在差异,验证集最优不代表线上最优。另外,epoch越靠后,模型对训练集的拟合越充分,泛化能力可能不升反降。

解决:把runs里所有epoch的权重文件逐个在回放数据集上测试,选择综合表现最好的,而不是机械地选epoch数最大的。我通常的做法是固定一个留出的验证时间段,把五个best权重都跑一遍,对比F1趋势,选拐点前的那个权重。这个拐点就是“开始过拟合”的位置,这比在训练日志里猜要靠谱得多。

注意:runs目录下的config.json记录了每个实验的完整参数,加载权重时务必确认代码里的模型结构与config一致,否则state_dict会直接报key不匹配。

6. 进阶:把预警阈值从固定值改成动态自适应

静态阈值是这套系统里最后一个需要动手改造的地方。前面选择TimesNet或PatchTST,我们得到的是每个时间步的预测残差——这个残差就是“模型觉得哪里不对劲”的程度。默认做法是残差超过某个固定倍数标准差就报警,但问题在于工况会漂移。设备磨合期、季节温差、负载特性变化,都会让正常状态下的残差分布缓慢移动。固定阈值在三个月后检查,要么误报多到没人看,要么真实故障的信号已经被磨平了阈值变成摆设。

动态阈值的思路很简单:用模型在最近一段正常数据上的残差分布,滚动估计正常的波动范围。假设残差服从近似正态分布,正常范围就落在均值加减k倍标准差的区间内。k取2到3之间,越小越敏感,越大越保守。这里的要点是“滚动”——每处理完一个窗口,就会用新窗口的数据更新残差的均值和标准差,让阈值跟着工况走。

class AdaptiveThreshold: def __init__(self, warmup_size=1000, alpha=0.3, k=3.0): self.buffer = [] self.warmup_size = warmup_size self.alpha = alpha self.k = k self.mean = 0.0 self.std = 1.0 def update(self, residual): # 先积累足够多的正常残差,再开始估计统计量 if len(self.buffer) < self.warmup_size: self.buffer.append(residual) if len(self.buffer) == self.warmup_size: self.mean = np.mean(self.buffer) self.std = np.std(self.buffer) return False # 用滑动平均的方式更新均值与标准差,避免突然跳变 self.mean = (1 - self.alpha) * self.mean + self.alpha * residual self.std = (1 - self.alpha) * self.std + self.alpha * abs(residual - self.mean) # 残差超过阈值时判定为异常 return abs(residual - self.mean) > self.k * self.std

这段代码有两个关键设计。一是用了增量更新而不是把整个历史都存下来重新计算,alpha控制对最新数据的响应速度,设0.3意味着大约3到5个窗口后阈值就能适应当前工况,设太大容易把故障信号也吸收进“正常范围”。二是warmup阶段先用固定数量的正常残差初始化统计量,避免冷启动时阈值乱跳。部署时用历史正常数据先跑一遍warmup,再进入在线推理。验证动态阈值有没有生效,方法是拿一段包含故障的历史数据做回放,对比固定阈值和动态阈值两种模式下的报警点位置,动态阈值应该能更早捕捉到故障前兆,同时在故障结束后的恢复期不误报。

这套源码我拆下来最大的收获,是它把“故障预警”从一句口号落实成了一个可复现的工程项目:数据预处理、模型对比、实验追踪、权重管理、在线推理,每一环都有对应的代码和产物。评估模型的时候,这套源码让我意识到时序模型不是调完loss就万事大吉,真正的价值是它在异常发生前那几个时间步的表现。从那以后我每次做故障预警,都强制走一遍这个流程:先切窗再归一化,然后固定一组实验配置跑两个模型做对比,最后把阈值改成自适应再看回放效果。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询