news 2026/11/5 4:07:55

pyTEP 实战:TEP 故障诊断数据集生成与 PCA 体检

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
pyTEP 实战:TEP 故障诊断数据集生成与 PCA 体检

简介:pyTEP 是面向过程控制、化工仿真与算法验证的开源 Python 接口,用于田纳西伊士曼(Tennessee Eastman)流程的交互式模拟。它将复杂仿真场景的搭建、操作条件调整和仿真数据存储封装为较直观的 API,使用者无需深入了解模拟器底层,即可完成交互式实验,适合控制算法测试、教学演示与流程仿真入门。压缩包共 76 个文件,约 754KB,包含 23 个 .m、16 个 .py、8 个 .pkl、7 个 .mdl、3 个 .mat 以及 rst、md、txt 等文档与配置,覆盖 Python 接口、MATLAB 桥接、模型数据、测试用例和说明材料,docs、tests、simulator、utils 等目录便于按模块检索。已有 546 人学习下载。借助完整源码、示例配置与测试材料,读者可快速理解 pyTEP 的 API 组织与仿真设置方式,复用数据记录流程,并在此基础上扩展自己的 TEP 实验或控制算法验证;文档与脚本中还保留 TEP 二进制重编译、Python 环境准备等说明,对具备 Python 基础的学习者较为友好。

1. pyTEP 是什么:把 Tennessee Eastman 流程包成 Python 对象的那层壳

第一次碰过程监控方向的人,多半会在 Tennessee Eastman 流程上先摔一跤:Fortran 老代码要自己配编译环境,变量表和故障编号对不上,跑出来的曲线看着像噪声又不敢确定是不是自己设错了。pyTEP 就是冲这件事来的,它是一个 Python 侧的开源仿真 API,把 TEP 这套化工基准流程包成可调用对象,复杂的场景搭建被压成几行配置,同时保留了交互式逐步推进的选项,方便你在每一步检查状态、改参数、插故障再继续。它适合三类人:做故障诊断与过程监控算法的、需要在统一基准上横向对比方法的、以及想把 TEP 直接接进 Python 训练流水线的从业者。这套东西的价值不在于算法多新,而在于它把“基准”这两个字落地了,你跑出来的数据和别人论文里的数据具备可比性。

2. 装起来并能跑通:从虚拟环境到第一条 52 列时间序列

这一章只干一件事,把包装进一个干净环境,然后拿到一条能看懂维度、能存成文件的时间序列。别急着上故障、上批量,先确认单次运行是稳的,后面所有工作都建立在这个前提上。

2.1 环境隔离与安装路径的选择

TEP 这类仿真器通常不是纯 Python 写的,原始实现是 Fortran,pyTEP 在它外面套了一层接口。这就决定了安装这件事有两种走法:官方如果发布了对应平台的预编译包,pip install一把过;如果落到源码编译,就需要本机有 C 或 Fortran 的编译工具链。我一般会先建独立虚拟环境,避免和机器上已有的 numpy、scikit-learn 版本打架,尤其是后面要拿生成的数据去训练模型,numpy 的大版本差异会让随机数和精度表现不一致。

# 独立环境,别偷懒装进 base python -m venv .venv-tep source .venv-tep/bin/activate # Windows: .venv-tep\Scripts\activate python -m pip install -U pip setuptools wheel # 第一步:优先吃预编译轮子 pip install pyTEP # 第二步:装不上再强制源码编译,看日志里缺什么 pip install --no-binary :all: pyTEP -v

--no-binary :all:的含义是不接受任何预编译产物,强制从源码构建;-v打开详细日志,编译失败时可以直接看到是缺gfortran、缺meson,还是缺某个头文件。Windows 上这一步最容易翻车,血泪经验是先装好对应版本的构建工具再回头重试,不要在报错里反复试错。装完之后立刻确认模块路径,能import但路径指向了别的目录,说明你装到了另一个环境里。

提示:环境建好后先把pip freeze存一份到requirements-lock.txt,后面数据生成、模型训练都按这份锁定,换机器复现时才不会出现“同样的代码结果差一个量级”的玄学问题。

2.2 先探接口,再抄骨架

不同版本的 pyTEP 在类名和参数名上可能有出入,直接抄网上的例子经常踩空。稳妥的做法是先让包自己把家底亮出来,看清楚构造入口、运行方法、故障设置函数的真实名字,再往下写业务代码。

import pyTEP # 1) 确认装的是哪个位置 print("模块路径:", pyTEP.__file__) # 2) 列出包级公开接口,类名/函数名一眼看全 public_names = [n for n in dir(pyTEP) if not n.startswith("_")] print("公开接口:", public_names) # 3) 包级 docstring 里通常有最小可运行示例 help(pyTEP)

dir()给出的第一层名字只到包级别,真正的仿真对象往往在下一层,用help(pyTEP.TEP)之类的方式继续下钻。这一步看着笨,但它能把“版本差异”这个最大的不确定因素提前消化掉,比写到一半报AttributeError再回头查要快得多。

2.3 跑通一次基线仿真并落成 DataFrame

拿到接口之后,先跑无故障基线。基线数据的用途很实在:它既是后面所有故障数据的参照系,也是你判断仿真器有没有正常运行的第一道关。TEP 的原始变量是 41 个测量量 XMEAS 加 12 个操纵量 XMV,一共 53 个;公开数据集里普遍把搅拌速度 XMV(12) 剔掉,因为它在绝大多数场景下保持不变,于是剩下的就是常见的 52 列结构。

import numpy as np import pandas as pd import pyTEP tep = pyTEP.TEP() # 构造仿真对象,构造参数按 help() 校准 tep.set_fault(0) # 0 表示无故障基线 tep.set_sampling(0.05) # 采样步长,单位小时:0.05 h = 3 min tep.set_duration(24.0) # 仿真时长,单位小时 raw = tep.run() # 跑一次,返回时间序列 arr = np.asarray(raw, dtype=float) # 兜底转置:不同版本返回的可能是 (步数, 变量) 或 (变量, 步数) if arr.shape[0] == 52 and arr.shape[1] != 52: arr = arr.T cols = [f"xmeas_{i}" for i in range(1, 42)] + [f"xmv_{i}" for i in range(1, 12)] df = pd.DataFrame(arr[:, :52], columns=cols) print(df.shape, df.isna().sum().sum()) df.to_parquet("tep_baseline.parquet", index=False)

set_sampling(0.05)是这个流程的常规节拍,对应 3 分钟一个采样点,24 小时跑下来是 480 步;set_duration的单位跟采样保持一致,都用小时,这个一致性要盯住,混用分钟和小时是最常见的错。代码里的转置兜底不是洁癖,而是真遇到过返回方向不一致的情况,shape检查比肉眼数行数靠谱。最后的isna().sum()是一道廉价的保险,正常运行不会有缺失值,一旦出现非零,说明仿真中途发散了,后面的数据全部作废。

注意:基线的shape一定要打出来看。480 步对应 24 小时、960 步对应 48 小时,如果步数对不上,先查采样步长的单位,别急着怀疑包有问题。

3. 故障怎么注:IDV 1-21 的编号、注入时刻与采样步长

能跑通基线只说明环境没问题,真正决定数据能不能用建模的,是故障怎么注、注在什么时刻、注多久。TEP 的故障集是固定的 21 个编号,编号本身是行业公共约定,你抄别人论文里的结果,前提就是编号含义一致。

3.1 21 个 IDV 的构成与可检测难度

TEP 的故障设计有一套内在逻辑:一部分是阶跃型,变量在某一时刻突然跳变然后稳定在新水平;一部分是随机型,围绕均值做随机波动,统计特征变化但瞬时值不突出;还有一部分是慢漂移和阀门粘滞,变化率极低,靠单点阈值几乎抓不到。这个分类直接决定了你后面用什么检测方法,也决定了哪些故障注定做不出好看的指标。

编号类型作用对象可检测难度
IDV(1)阶跃A/C 进料比,B 恒定易
IDV(2)阶跃B 进料比,A/C 恒定易
IDV(3)阶跃D 进料温度中
IDV(4)阶跃反应器冷却水入口温度易
IDV(5)阶跃冷凝器冷却水入口温度易
IDV(6)阶跃A 进料损失易
IDV(7)阶跃C 进料损失,伴随压力恢复中
IDV(8)随机A、B、C 进料组成中
IDV(9)随机D 进料温度难
IDV(10)随机C 进料温度难
IDV(11)随机反应器冷却水入口温度中
IDV(12)随机冷凝器冷却水入口温度中
IDV(13)慢漂移反应动力学难
IDV(14)粘滞反应器冷却水阀难
IDV(15)粘滞冷凝器冷却水阀难
IDV(16) 至 IDV(20)未知未公开的内部扰动难
IDV(21)固定位置阀门卡在稳态位置难

这张表里 IDV(9)、IDV(15)、IDV(21) 是公认的硬骨头,文献里检出率长期偏低。如果你的模型在这几个编号上表现极差,先别怀疑代码,去对照一下公开基准,确认是不是方法本身的天花板。反过来,如果 IDV(4)、IDV(11) 这种也检不出来,那基本可以确定是数据管道出了问题。

3.2 注入时刻与训练/测试集的常见约定

故障不是从第 0 步就开始注的,前面必须留一段正常工况做基线,否则模型没有参照。业界比较通行的做法是三段式:训练集每个故障跑 25 小时、共 500 个采样点,故障在运行 1 小时后注入;测试集跑 48 小时、共 960 个采样点,故障在运行 8 小时后注入。这个安排的用意是训练集让模型尽早见到故障形态,测试集则留出更长的正常段,用来统计误报率和检测延迟。

# 训练集:25 h,故障在第 1 h 注入 tep = pyTEP.TEP() tep.set_fault(4) # 反应器冷却水入口温度阶跃 tep.set_sampling(0.05) # 3 min tep.set_duration(25.0) # 500 步 tep.set_fault_time(1.0) # 故障注入时刻,单位小时 train = np.asarray(tep.run(), dtype=float) # 测试集:48 h,故障在第 8 h 注入 tep.set_duration(48.0) # 960 步 tep.set_fault_time(8.0) test = np.asarray(tep.run(), dtype=float)

set_fault_time这类函数名按你装到的版本对齐,语义是“从第几个小时开始注入”。这里最容易犯的错是训练集和测试集用了同一个注入时刻,这样两边的故障段长度占比不同,模型学到的边界和你评估的边界不是一回事,指标会虚高。我一般会在生成脚本里把(size, fault_time)显式写成配置表,避免手滑。

提示:注入时刻一定要写进数据集的元信息里。半年后回头看一个 parquet 文件,只有 52 列数字,没有故障编号和注入时刻,那这份数据就只能扔了。

3.3 控制模式的取舍:开环、基础控制与闭环

TEP 内置了多种控制配置,粗略可以分成开环、基础控制和更复杂的闭环控制几档。开环模式下操纵变量完全由外部输入决定,仿真更容易发散,但它对控制算法的激励最充分;基础控制模式把若干回路闭合,过程更稳定,也更接近真实装置的运行状态,是绝大多数监控类研究用的配置。选哪一档,取决于你要验证的是控制律还是监控算法,这两件事对数据的要求正好相反。

我的习惯是先把基础控制跑通,确认所有操纵量都在合理区间里波动,再考虑要不要切到开环。切换控制模式之后,同一组故障编号的响应形态会变,之前调好的检测阈值全部要重来。这也是为什么数据集必须带上控制配置的说明,否则别人拿你的数据复现不出结论。

4. 批量造数据集:训练/测试划分、随机种子与多进程落盘

单次仿真只能验证流程,真正喂给模型的是几十上百次运行堆出来的数据集。这一章解决三个问题:怎么划分才不泄漏、怎么跑得快、怎么存才不后悔。

4.1 按仿真轮次划分,而不是按行切

数据泄漏在这类时序仿真数据上极其隐蔽。常见的错误做法是把每个故障的 960 步打乱之后按 8:2 切分,train 和 test 里都混着同一次仿真的相邻片段,模型等于在做插值,指标能到 99%,换一批数据立刻塌。正确做法是按仿真轮次划分:一次run()产出的整条序列,要么全进训练集,要么全进测试集,绝不拆开。

import numpy as np rng = np.random.default_rng(20240501) def split_runs(runs, test_ratio=0.3): """runs: list[np.ndarray],每个元素是一次完整仿真的结果""" idx = rng.permutation(len(runs)) n_test = max(1, int(len(runs) * test_ratio)) test_idx = set(idx[:n_test].tolist()) train = [runs[i] for i in range(len(runs)) if i not in test_idx] test = [runs[i] for i in range(len(runs)) if i in test_idx] return train, test

default_rng(seed)里的种子要固定并且记录下来,随机型故障(IDV 8 到 12、16 到 20)本身带随机扰动,两次运行结果天然不同,种子不固定的话,你连自己都复现不了自己的数据集。test_ratio我通常按轮次取 0.3,样本量太少时按固定条数切更稳,别让int()截断把测试集吃成 0。

4.2 多进程生成与断点续跑

21 个故障、每个故障重复若干次,串行跑起来耗时相当可观。多进程能压时间,但必须在进程内独立构造仿真对象,不能共享同一个实例,否则状态互相污染,跑出来的曲线会很诡异。

from concurrent.futures import ProcessPoolExecutor from pathlib import Path import numpy as np def run_one(job): """job = (fault_id, repeat_id, seed)""" fault_id, repeat_id, seed = job import pyTEP tep = pyTEP.TEP() tep.set_fault(fault_id) tep.set_sampling(0.05) tep.set_duration(48.0) tep.set_fault_time(8.0) tep.set_seed(seed) # 随机型故障必须固定种子 arr = np.asarray(tep.run(), dtype=float) out = Path("data") / f"idv{fault_id:02d}_r{repeat_id:02d}.npy" out.parent.mkdir(parents=True, exist_ok=True) if out.exists(): # 断点续跑:已存在就跳过 return str(out) np.save(out, arr) return str(out) if __name__ == "__main__": # 多进程必须守住入口 jobs = [(f, r, 1000 + r) for f in range(0, 22) for r in range(10)] with ProcessPoolExecutor(max_workers=6) as ex: for p in ex.map(run_one, jobs): print("done:", p)

三个要点。set_seed的真实接口名按版本核对,作用是固定随机型故障的扰动序列。if out.exists(): return是断点续跑的关键,跑长时间任务时机器崩一次不至于全部重来。max_workers别开满,每个进程都吃一份仿真器内存,开 6 到 8 个通常就够,再往上收益递减还可能因为内存压力被杀进程。

注意:if __name__ == "__main__":在这一段里不是可选项。Windows 上的 spawn 启动方式会因为缺这个守卫导致进程无限递归创建,机器直接卡死。

4.3 存储格式的取舍

文件格式的选择在数据量小的时候无所谓,到几十 GB 就有明显差别。我的取舍标准写在下面这张表里,按你的下游用法选,不要一刀切。

格式读写速度是否保留列名适合场景主要缺点
CSV慢是交付给别人、需要肉眼检查体积大、浮点精度有损
Parquet快是建模流水线、长期存档需要 pyarrow 依赖
npz/npy最快否纯数值训练、追求加载速度列信息要单独存一份

我自己的习惯是双份:.npy供训练脚本快速加载,同时导出一份meta.csv记录文件名、故障编号、注入时刻、种子、控制模式。这份元信息文件看着不起眼,等你要复现某次实验结论的时候,它就是唯一的后悔药。

5. 避坑与排查:五类让仿真结果不可信的真实故障

数据和代码都能跑,不代表结果可用。下面这五条是我在实际拆这类仿真器时反复遇到的,每条都按现象、原因、解决三段写,遇到对应的症状直接对号入座。

5.1 仿真层的三类异常

现象:跑几十步之后数值突然爆炸,变量量级到 1e8 甚至出现 NaN。原因多半是控制回路没闭合。开环模式下某些积分型环节会持续累积,尤其在没有基础控制兜底的时候,反应器温度和压力会互相推着走高。解决方式是先确认控制配置项是否真的生效——不是设了参数就等于开了控制,有些版本还需要额外一次初始化调用。判断方法很简单:把操纵量的时间序列画出来,正常闭环下它们会在中位值附近小幅波动,开环发散时是一条单调上扬的直线。

现象:同样的设置跑两次,结果差异大到模型学不了。这是随机种子的问题。IDV 8 到 12 和 IDV 16 到 20 这类随机型故障自带扰动项,包内如果没有用固定种子初始化,每次运行的扰动序列都不一样。解决办法是在构造仿真对象之后、运行之前显式设置种子,并且把种子写进文件名和元信息。别指望在 numpy 层面设全局种子就能覆盖,仿真器内部的随机数发生器往往是独立的一套。

现象:换一台机器之后,同一份脚本出来的数据量级差一个数量级。原因通常在两处:编译精度或 numpy 版本。源码编译时单精度和双精度配置不同,浮点累积误差在几千步之后会被放大。解决方式是锁定环境,把pip freeze的结果作为依赖清单,并在数据生成脚本开头把 numpy 版本、仿真器版本、机器架构一起写进日志。这不是强迫症,是复现的底线。

5.2 数据与标签层的两个陷阱

现象:模型在自建测试集上准确率 99%,换成公开基准立刻掉到 60%。原因几乎可以确定是数据泄漏,训练集和测试集共享了同一次仿真的片段,或者两边用了不同的注入时刻导致故障段占比不一致。解决办法是严格按仿真轮次切分,并且把训练集和测试集的(时长, 注入时刻)写成同一张配置表,谁改了都要在元信息里留痕。另外,公开基准通常剔除了 XMV(12),你自己生成的数据如果保留了 53 列,喂给同一个模型会因为维度不匹配直接报错,或者更糟——静默错位。

现象:IDV(9)、IDV(15)、IDV(21) 怎么调都检不出来,怀疑数据有问题。这三个编号在文献里的检出率本来就不高,属于方法层面的天花板,不是数据 bug。验证方法很直接:拿 IDV(4) 或 IDV(11) 当对照组,如果对照组检出率正常、只有这几个编号差,那就是难度问题;如果对照组也差,才回去查数据。评估指标上,别只看准确率,故障检测这个场景里误报率和平均检测延迟更能反映真实水平,一个从不报警的模型准确率也很高。

提示:把每个故障编号的检出率和检测延迟做成一张表存档。三个月后你要判断新方法是真好还是数据换了,这张表是唯一可靠的对照。

6. 数据体检:用 PCA 的 T² 与 SPE 在建模前先验一遍

在把数据丢进任何复杂模型之前,我固定会跑一遍 PCA 体检。理由很朴素:如果连线性子空间方法都分不出来的故障,多半是数据本身有问题;如果 PCA 能分出大部分,说明数据管道是通的,后面上深度模型才有意义。这套体检用的是两个经典统计量:T² 衡量样本在主元子空间里的偏离程度,SPE 衡量残差子空间的能量,两者互补,一个抓幅值异常,一个抓相关性结构破坏。

做法是先只用正常工况数据拟合,算出一组基线阈值,然后拿故障数据去比较检出率。阈值取正常数据上的 99 分位数,这个分位数不是拍脑袋,而是对应 1% 的误报率,方便和文献里的数字直接对齐。

import numpy as np from sklearn.decomposition import PCA # X_normal: 仅正常工况,形状 (n_samples, 52) X = np.asarray(X_normal, dtype=float) mu, sd = X.mean(0), X.std(0) + 1e-8 # 防止常数列除零 Z = (X - mu) / sd # 逐列标准化,量纲差异必须先抹平 pca = PCA(n_components=0.90).fit(Z) # 保留 90% 方差,主元数由数据决定 print("主元个数:", pca.n_components_) def t2_spe(X_raw, pca, mu, sd): Z = (np.asarray(X_raw, dtype=float) - mu) / sd score = pca.transform(Z) t2 = ((score ** 2) / pca.explained_variance_).sum(1) rec = pca.inverse_transform(score) spe = ((Z - rec) ** 2).sum(1) return t2, spe t2_base, spe_base = t2_spe(X_normal, pca, mu, sd) t2_lim = np.percentile(t2_base, 99) # 1% 误报率对应的阈值 spe_lim = np.percentile(spe_base, 99) t2_f, spe_f = t2_spe(X_fault, pca, mu, sd) # X_fault: 待检的故障数据 detect = (t2_f > t2_lim) | (spe_f > spe_lim) print(f"检出率: {detect.mean():.3f}")

n_components=0.90让主元个数由方差贡献自动决定,比手工指定更不容易过拟合;pca.explained_variance_必须和中心化后的得分配合使用,少了这一步 T² 的量级会差很多。mu和sd一定要从正常训练集算出来再套到故障集上,如果在故障集上重新标准化,等于把异常信息洗掉了。

统计量监控对象典型阈值常见误判原因
T²主元子空间幅值偏离正常集 99 分位主元数取太多,把噪声当信号
SPE残差子空间能量正常集 99 分位常数列未做除零保护,值被放大

两个指标都要看。有些阶跃型故障(比如 IDV(4))在 SPE 上几乎不反应,因为变量的相关性结构没被破坏,只是整体偏移,全靠 T² 抓;反过来慢漂移类故障在 T² 上迟钝,SPE 先动。谁先动、动多少,这两条曲线本身就是判断数据质量的诊断信息。

最后说个习惯。早些年我图快,生成完数据直接进模型,结果调了两周超参才发现是某次仿真的控制配置没生效,那一批数据全是发散的,模型学到的全是噪声——白干。从那以后我每换一个环境、每换一次随机种子,都会强制先跑一遍这套体检脚本,先看 T² 和 SPE 在正常数据上的误报率是不是接近 1%,再看几个容易检测的故障编号能不能被分开,确认这两关过了才动模型。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 4:04:04

操作系统课程设计实战:进程调度、页面置换与文件系统排错

简介:面向计算机科学专业学生的东北大学操作系统课程设计资料,围绕进程管理、内存管理、文件系统与设备管理等核心主题,提供可运行、可对照的课程实践样例。压缩包共24个文件,约2.11MB,以21个C源文件为主体&#xff0c…

作者头像 李华
网站建设 2026/11/5 4:03:38

指针仪表检测:XML标注解析、格式转换与YOLO训练配置

简介:面向目标检测任务的指针仪表图像数据集,适合机器学习研究者与开发者训练和评估仪表识别、定位及读数提取模型。压缩包共785个文件,以783张jpg实拍图像为主体,另含2个json标注/配置文件,整体约950.3MB,…

作者头像 李华
网站建设 2026/11/5 4:03:23

DeepSeek提示词生产力:分阶段生成与人工校验

简介:清华大学新媒体研究中心发布的《普通人如何抓住DeepSeek红利》PDF,面向职场人、学生及AI应用初学者,围绕DeepSeek的功能边界、提示词驱动与提问鉴别能力展开,帮助读者在工作写作、学习提效、生活减负和社交沟通中形成可操作的…

作者头像 李华
网站建设 2026/11/5 4:02:33

维普AI降重实测:2026年11月维普AIGC检测96.46%降到0%,免费降AI 1000字

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 4:01:28

链路聚合的负载分担算法:哈希因子怎么选

一、链路聚合负载分担概述 链路聚合(Link Aggregation)是一种网络技术,通过将多个物理链路组合成一个逻辑链路,提高带宽和可靠性。负载分担(Load Balancing)是链路聚合中的一个重要功能,通过哈希…

作者头像 李华
网站建设 2026/11/5 4:01:12

会自我轮转的 Agent:从“命中率 99.9% 还是很贵“到实测省 76%

会自我轮转的 Agent:从"命中率 99.9% 还是很贵"到实测省 76%我在 DeepSeek Harness(DSH)上做了一个上下文预算插件,用 1181 步真实数据校准了成本模型,并把长会话的每步成本压到 1/3.3。 这篇文章把四组实验…

作者头像 李华