ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于TCN的航空发动机剩余寿命预测:C-MAPSS数据集Python实现

基于TCN的航空发动机剩余寿命预测:C-MAPSS数据集Python实现 简介基于时间卷积网络的航空发动机剩余寿命预测代码是故障预测与健康管理领域完整可复现的实战案例该网络凭借因果卷积与残差结构擅长捕捉时间序列中的长期依赖广泛适用于各类序列预测任务。代码包面向深度学习初学者、算法工程师及设备运维研究人员围绕发动机退化数据集展开从数据清洗与标准化开始逐步完成网络结构定义、超参数配置、模型训练与测试集验证并配有多维可视化分析便于直观理解训练过程与预测效果压缩包内共包含五个文件分别是三个文本数据文件、一个表格文件和一个脚本文件文本数据提供发动机运行状态与剩余寿命标签表格用于辅助数据查看与整理脚本则完整演示了从数据加载、模型搭建到性能评估的全流程包体大小约5.58MB内容紧凑且易于运行。目前已有五百八十余人学习下载参考价值得到验证复用这一方案读者可以掌握时间卷积网络在回归类时间序列预测中的实施要点并迁移至电力负荷、交通流量等相似场景兼顾理论理解与实际落地。1. 把时间序列预测从 LSTM 换到 TCN航空发动机剩余寿命这个场景为什么值得复现第一次跑航空发动机剩余寿命RUL预测时我默认选了 LSTM结果训练到第 40 个 epoch 损失还在 0.3 上下震荡。后来把网络换成 TCN时间卷积网络同样的数据、同样的窗口长度RMSE 直接降了近 20%训练时间还缩短了一半。这个反差让我意识到序列建模不一定非得用循环网络因果卷积加残差这种结构在长依赖场景下更稳。这份资源就是用 TCN 在 C-MAPSS 数据集上做剩余寿命预测的完整 Python 实现从数据预处理、滑窗切片到模型训练、评估、可视化都有。适合两类人一是刚接触 PHM故障预测与健康管理想找一个能跑通的基线模型的研究生二是已经在用 LSTM 做时间序列但觉得训练慢、调参玄学的工程师想看看卷积路线在时序任务上到底能到什么程度。2. 数据预处理把 C-MAPSS 原始数据变成 TCN 能吃的样本2.1 原始数据长什么样train_FD001 与 test_FD001 的结构C-MAPSS 的 FD001 子集是单工况、单故障模式的场景也是入门 PHM 最常用的数据集。train_FD001.txt 里每一行代表一个时间步的传感器读数列结构是固定的第一列是发动机编号unit id第二列是运行循环数cycle第三列是三个工况设置参数基本不变后面 21 列是传感器测量值。整个训练集有 100 台发动机的完整退化轨迹测试集是另外 100 台发动机的截断数据只给到某个时间点要求预测从该点到发动机失效的剩余循环数。拿到原始 txt 后第一步要做的不是建模而是理解“标签从哪来”。训练集里每台发动机的最后一行 cycle 就是它的寿命终点所以第 i 台发动机在时刻 t 的 RUL 标签等于该台发动机的最大 cycle 减去当前 cycle。测试集没有直接给标签但数据集的官方说明里给了 test_RUL_FD001.txt那是每台测试发动机的真实剩余寿命用来算最终评分。这里有个细节我经常看到有人搞错测试集里每台发动机的行数不是全寿命周期而是从某个中间点开始的截断段。也就是说测试集并不是“从第 1 个 cycle 到失效”的完整轨迹而是只给了一段最新的历史。所以滑窗的时候测试集能产生的样本数远少于训练集而且每台发动机只能截出一小段连续序列。这个特性直接影响训练和测试的数据分布一致性后面讲避坑的时候我会单独展开。# 先用 head 命令看一眼原始文件结构 head -5 train_FD001.txt输出应该是每行 26 列数字空格分隔。看到列数对不上或者有多余逗号说明你下的数据集版本不对先检查这一层再往下走。2.2 标签构造与退化分段为什么不能直接用原始 cycle 当标签一个关键的预处理决策是RUL 标签要不要做截断clipping。在 C-MAPSS 的官方评分函数里预测误差大于真实 RUL 的惩罚远大于小于的情况所以很多论文会把 RUL 标签限制在一个上限范围内比如常见的是截到 125 或 130。原因是早期循环里发动机状态几乎无退化如果标签从几百甚至上千的 cycle 开始学模型会被迫去拟合“健康段”的平坦曲线反而忽略了真正重要的退化段。我一般会先画一台发动机的传感器原始曲线确认哪些传感器存在明显的单调退化趋势再决定保留哪些特征。FD001 的 21 个传感器里像传感器 2、3、4、7、8、11、12、15 等都有退化趋势传感器 1、5、6、10、16、18、19 基本是平线这些对 RUL 预测没有贡献。常见做法是对全部 26 列做标准化然后靠网络自己学但如果你想让模型更聚焦可以手动筛掉不敏感传感器输入维度从 26 降到 14 左右训练速度更快而且精度不降。标签截断和特征筛选这两步都直接影响模型上限。截断值不是拍脑袋定的FD001 场景下 125 是个被反复验证过的合理值。你可以在代码里把它设成参数先跑一版 125再跑一版不截断的对比测试集上的 RMSE 差异。import numpy as np import pandas as pd def load_data(train_path, test_path, max_rul125): # 读取原始数据列名按 C-MAPSS 标准命名 col_names [unit, cycle, op1, op2, op3] \ [fsensor_{i} for i in range(1, 22)] train pd.read_csv(train_path, sep , headerNone, namescol_names) test pd.read_csv(test_path, sep , headerNone, namescol_names) # 去掉列全为 NaN 的尾部空列原始文件行尾有多余空格 train train.dropna(axis1, howall) test test.dropna(axis1, howall) # 计算每台发动机的最大 cycle即寿命终点 max_cycles train.groupby(unit)[cycle].max() # 为训练集构造 RUL 标签 train[rul] train[unit].map(max_cycles) - train[cycle] train[rul] train[rul].clip(uppermax_rul) # 截断到 max_rul return train, test这段代码做了三件事读数据时先按列名对齐防止原始 txt 里行尾空格产生的空列干扰特征矩阵然后用 groupby 求出每台发动机的寿命终点最后用 clip 把 RUL 限制在 0 到 125 之间。clip(uppermax_rul)是最关键的一行它把健康段的平坦标签全部压到 125让模型只关注退化段的斜率变化而不是去拟合一个无意义的几百 cycle 的常数段。2.3 滑动窗口采样窗口长度、步长与样本量控制TCN 和 LSTM 一样输入必须是固定长度的序列。对航空发动机数据来说窗口长度选多少直接决定模型能“看到”多长的历史退化轨迹。窗口太短比如 10 个 cycle模型看不到退化的长期趋势预测值会偏保守窗口太长比如 100 个 cycle虽然上下文更充分但样本数量减少训练时间变长而且早期的健康段会稀释退化信号。我习惯先按窗口长度 30 来做第一版。原因是 FD001 的退化过程多数在最后 50 个 cycle 内加速明显30 的窗口能捕捉到加速段又不至于把样本切得太碎。步长stride这个参数很多人不设默认滑 1 个 cycle导致训练样本量巨大。设成 1 的时候100 台发动机全生命周期数据切出来大概一万多个样本训练一轮 TCN 要几分钟设成 5 的话样本量降到两千左右一轮不到一分钟精度损失在可接受范围内。def create_sequences(data, seq_length30, stride1): 对每台发动机分别滑窗返回 (样本数, 时间步, 特征数) 的数组。 X, y [], [] for unit_id in data[unit].unique(): unit_data data[data[unit] unit_id].sort_values(cycle) feats unit_data.drop(columns[unit, cycle, rul]).values labels unit_data[rul].values for i in range(0, len(unit_data) - seq_length, stride): X.append(feats[i:i seq_length]) y.append(labels[i seq_length - 1]) return np.array(X), np.array(y)这里有几个细节值得注意。第一range(0, len(unit_data) - seq_length, stride)的意思是窗口必须完整覆盖 seq_length 个时间步最后一个不完整的窗口直接丢弃。第二标签取的是窗口末尾那个时间步的 RUL不是窗口内所有 RUL 的平均因为我们要预测的是“当前状态”的剩余寿命不是历史平均。第三代码里对每台发动机独立滑窗避免把不同发动机的轨迹混在一起切否则样本会跨发动机拼接出伪序列模型学到的模式是错的。3. TCN 模型结构因果卷积、膨胀系数与残差块怎么配3.1 因果卷积与膨胀卷积的区别TCN 凭什么能替代 LSTMTCN 的核心是两个概念因果卷积causal convolution和膨胀卷积dilated convolution。因果卷积保证了输出序列第 t 个位置只依赖输入序列前 t 个位置的信息不向后看这和 RNN 的时序逻辑一致。但纯因果卷积的感受野受卷积核大小限制想捕捉长依赖就得堆很多层所以 TCN 引入膨胀卷积在卷积核的每个元素之间插入空洞膨胀系数 d 为 1、2、4、8……这样每一层的感受野按指数增长层数不用很多就能覆盖长序列。我常用一个简单的直觉来对比 TCN 和 LSTMLSTM 是逐时间步传递隐藏状态序列一长梯度路径就很长容易出现梯度消失或者爆炸TCN 是直接对整段序列做卷积梯度可以沿着残差连接的短路路径回传路径短且稳定。这就是为什么 TCN 在同样的训练轮数下损失降得更快、预测曲线更平滑。TCN 结构的超参数里最重要的三个是卷积核大小kernel size、通道数num channels、膨胀系数列表dilations。kernel size 越大每层看到的局部信息越多我一般用 3 或 5通道数决定每层的表达能力64 到 128 之间是安全和高效的区间dilations 控制感受野的深度常见组合是[1, 2, 4, 8, 16]对应 5 层膨胀卷积。3.2 残差块内部的组成权重归一化与 Dropout 的位置TCN 的每个残差块内部通常有两层膨胀卷积每层卷积后面接权重归一化WeightNorm、ReLU 激活和 Dropout。权重归一化比 BatchNorm 更适合小批量序列数据因为 BatchNorm 对 batch 大小的敏感性在序列任务里经常造成训练震荡。残差连接的触发条件是输入输出通道数一致。如果通道数改变了比如从 64 跳到 128就需要用 1x1 卷积对输入做投影保证维度对齐再相加。这个细节在实现的时候最容易翻车如果你看到维度不匹配的报错99% 是这里出了问题。import torch import torch.nn as nn class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, dilation, dropout0.1): super().__init__() self.padding (kernel_size - 1) * dilation self.conv1 nn.utils.weight_norm( nn.Conv1d(in_channels, out_channels, kernel_size, paddingself.padding, dilationdilation)) self.conv2 nn.utils.weight_norm( nn.Conv1d(out_channels, out_channels, kernel_size, paddingself.padding, dilationdilation)) self.relu nn.ReLU() self.dropout nn.Dropout(dropout) self.downsample None if in_channels ! out_channels: self.downsample nn.utils.weight_norm( nn.Conv1d(in_channels, out_channels, 1)) def forward(self, x): residual x out self.relu(self.conv1(x)) out self.dropout(out) out self.relu(self.conv2(out)) out self.dropout(out) if self.downsample is not None: residual self.downsample(residual) return self.relu(out residual)注意这里有个极容易踩的坑padding (kernel_size - 1) * dilation是为了让膨胀卷积在保持序列长度不变的前提下只使用过去的输入不使用未来的输入。因为 PyTorch 的 Conv1d 在 padding 时默认是两侧都补零如果不额外处理卷积核会“偷看”未来时间步因果性就被破坏了。解决办法是让 padding 只加在序列左侧右侧补零。很多 TCN 开源实现会在卷积层用paddingcausal或者在 forward 里手动裁剪右侧多出来的时间步。3.3 全连接输出头从特征序列到单点 RUL 预测TCN 的输出是长度等于输入序列长度的特征序列但 RUL 预测只需要每个样本输出一个标量。常见做法是取最后一层的最后一个时间步的特征接一个线性层映射到 1 维另一种做法是先做全局平均池化或者用 Flatten 接全连接层。我推荐取最后一个时间步因为这样和“当前时刻的剩余寿命”语义完全对齐。如果只用最后一个时间步可能会丢弃序列中较早信息的聚合特征因此有些实现会把所有时间步的均值也拼进去。这个属于可选优化第一版不用加。class TCNRegressor(nn.Module): def __init__(self, input_dim, num_channels, kernel_size3, dilations[1, 2, 4, 8, 16], dropout0.1): super().__init__() layers [] in_ch input_dim for d in dilations: layers.append(ResidualBlock(in_ch, num_channels, kernel_size, d, dropout)) in_ch num_channels self.tcn nn.Sequential(*layers) self.fc nn.Linear(num_channels, 1) def forward(self, x): # x shape: (batch, seq_len, input_dim) x x.permute(0, 2, 1) # Conv1d 需要 (batch, channels, seq_len) out self.tcn(x) last_step out[:, :, -1] # 取最后一个时间步的特征 return self.fc(last_step).squeeze(-1)permute那行是关键PyTorch 的 Conv1d 默认输入格式是(batch, channel, time)而我们在预处理时拿到的是(batch, time, feature)所以必须先转置模型才能跑通。输出头只取out[:, :, -1]即所有通道在最后一个时间步的激活值经过线性层得到标量 RUL。4. 训练闭环损失函数、优化器与评估指标的选择4.1 损失函数MSE 与 RMSE 在 RUL 场景下的取舍训练用的损失和最终评估用的指标最好分开想清楚。训练损失我用 MSE因为它的梯度平滑、收敛稳定适合回归任务。但最终评估时C-MAPSS 官方用的是 RMSE 和 NASA 评分函数RMSE 放大较大误差的影响对预测尖峰更敏感。所以训练过程和测试过程看的是两本账训练看 MSE 有没有下降测试看 RMSE 能不能压到 15 以下FD001 的正常水平是 12 到 17 之间。优化器我建议直接用 Adam学习率 1e-3 起步配合 ReduceLROnPlateau 调度器在验证集损失连续 5 个 epoch 不降时把学习率乘以 0.5。学习率太大容易震荡太小收敛慢你可以先按这个配置跑一版再把学习率改成 5e-4 对比一下损失曲线的斜率差异。4.2 训练循环batch 划分、验证集切分与早停训练集切分的时候要特别注意“按发动机切”不能随机按样本切。同一台发动机的相邻滑窗样本高度相关如果它们一部分进训练集、一部分进验证集验证集的指标会虚高给人造成模型很强的错觉。我一般按 8:2 的比例随机选发动机20 台发动机的样本全部进验证集80 台的进训练集。Batch size 我选 64。这个值太小会让梯度噪声大太大则占内存多而且收敛变慢。TCN 的显存占用比同规模 LSTM 高因为在 forward 过程中保存的中间激活值多如果你用的是 8G 显存的卡batch size 64 配上 128 通道可能需要减半到 32。早停early stopping是必须加的。我设置 patience 为 10 个 epoch也就是验证集损失连续 10 轮没有比历史最优值更低就停止训练并恢复最优权重。这个机制能防止过拟合因为在训练后期训练损失还在降但验证损失已经开始抬头了。def train_model(model, train_loader, val_loader, epochs80, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5) criterion nn.MSELoss() best_val_loss float(inf) patience_counter 0 for epoch in range(epochs): model.train() train_loss 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred model(X_batch) loss criterion(pred, y_batch) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() * X_batch.size(0) # 验证阶段 model.eval() val_loss 0.0 with torch.no_grad(): for X_batch, y_batch in val_loader: pred model(X_batch) loss criterion(pred, y_batch) val_loss loss.item() * X_batch.size(0) scheduler.step(val_loss) # 早停逻辑 if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_tcn.pt) else: patience_counter 1 if patience_counter 10: print(fEarly stop at epoch {epoch}) break model.load_state_dict(torch.load(best_tcn.pt)) return model代码里有一个容易被忽略但非常重要的操作clip_grad_norm_。TCN 的梯度虽然比 LSTM 稳但在训练初期偶尔会出现某个残差块的梯度特别大导致损失突然变成 NaN。梯度裁剪到最大范数 1.0 是一个廉价的保险几乎不影响正常收敛但能挡住那种偶发性的梯度爆炸。4.3 评估结果RMSE 计算与预测曲线怎么解读测试集的预测流程和训练不一样。测试集里每台发动机只给一段截断后的历史数据你可以在这段数据上滑窗生成多个样本然后对同一台发动机的多个预测结果做平均得到最终 RUL 预测值。这个“测试时多窗口平均”的技巧经常能带来 RMSE 的显著下降因为单窗口预测方差较大平均后更加稳定。另一个需要留意的点是测试集样本的 RUL 标签都在截断范围内比如真实 RUL 可能是 90但经过 clip 后变成了 125那么这个样本的标签就被“压缩”了。计算 RMSE 时要用官方给的 test_RUL_FD001.txt 的真实值而不是你预处理后的 clipped 标签。from sklearn.metrics import mean_squared_error def evaluate_rmse(model, test_data, test_rul_true, seq_length30): model.eval() preds [] with torch.no_grad(): for unit_id in test_data[unit].unique(): unit_data test_data[test_data[unit] unit_id].sort_values(cycle) feats unit_data.drop(columns[unit, cycle]).values # 在截断序列上生成所有可能的窗口 windows [] for i in range(0, len(unit_data) - seq_length): windows.append(feats[i:i seq_length]) if len(windows) 0: windows.append(feats) # 若序列过短直接整段预测 windows np.array(windows) windows_t torch.from_numpy(windows).float() unit_preds model(windows_t).numpy() preds.append(unit_preds.mean()) # 多窗口平均 preds np.array(preds) rmse np.sqrt(mean_squared_error(test_rul_true, preds)) return rmse, preds注意窗口循环里range(0, len(unit_data) - seq_length)和训练时保持一致测试序列长度不足一个窗口时做特殊处理直接将整段序列作为唯一输入。这类边界情况在真实数据里很常见不做处理代码就会在某台发动机上报维度错误而且只在测试阶段出现debug 起来很费时间。5. 避坑指南TCN 训练与数据处理的常见问题排查5.1 现象验证集损失下降但测试集 RMSE 很高原因验证集和测试集的数据分布不一致。训练集和验证集都是全生命周期轨迹滑窗出来的覆盖健康段到退化段的全过程而测试集是截断序列大多数样本集中在退化中后期。如果模型过拟合了健康段的平坦模式测试集的退化段预测就会偏。解决按发动机比例划分验证集后还要确认验证集和测试集的 RUL 分布接近。最直接的做法是把训练集里每台发动机的轨迹尾部比如最后 30 个 cycle额外滑窗出一部分样本加入训练模拟测试集的截断特性。这个方法我用了以后RMSE 从 16.8 降到了 14.2。5.2 现象训练到一半 loss 突然变成 NaN原因学习率偏大加上膨胀卷积层数深梯度累积导致数值溢出此外数据标准化时如果只对训练集做统计量测试集输入分布偏移也可能触发异常值。解决先检查标准化是否正确训练集和测试集必须使用同一个 scaler 的均值和方差不能用测试集自己再 fit 一次。然后确认梯度裁剪生效把max_norm从 1.0 改成 0.5 再试。如果 NaN 仍出现检查输入数据里有无 inf 或极大异常值原始 txt 里偶尔会有传感器的尖峰读数需要提前做中位数滤波。5.3 现象TCN 预测结果整体偏高或偏低原因RUL 标签截断值设置不当。截断值过小会把后半段退化过程的标签也压平导致模型对中后期的 RUL 整体低估截断值过大则健康段样本过多模型学到的退化斜率不敏感预测值偏高。解决第一版先按 125 跑通然后画一张预测值和真实值的散点图看偏差方向。如果整体偏高说明健康段样本主导了训练可以考虑把标签上限降到 110 并观察如果整体偏低检查是不是测试集的最后一个 cycle 已经非常接近失效模型输出被 clip 限制在 0 附近。这个调参过程本质是在“预测灵敏度”和“标签平滑度”之间找平衡。5.4 现象CPU 训练极慢一个 epoch 要几分钟原因TCN 在 CPU 上跑卷积操作本身比 LSTM 更吃计算量因为每层膨胀卷积都需要做多次乘加运算。很多人第一次跑代码直接用 CPU等着等着就以为程序卡住了。解决先确认机器有没有可用 GPUtorch.cuda.is_available()返回 True 就把模型和数据都搬到 GPU。如果没有 GPU 且不想换机器有两个折中方案一是把通道数从 128 降到 64训练时间大约能缩短一半精度损失通常在 0.5 以内二是把膨胀卷积层数减少比如dilations[1, 2, 4, 8]去掉最后一层感受野从 63 降到 31适当地牺牲一点长依赖捕捉能力。这个取舍在算力有限时很实用效果和完整模型的差距在 RMSE 上通常不超过 1。6. 进阶技巧多窗口平均、多模型融合与训练好的模型应用第一版单模型的 RMSE 稳定在 15 左右后你会发现继续调参收益越来越小。这时候最值得做的是两件事一是测试时的多窗口平均策略二是多模型融合。我提过测试集每台发动机可以滑出多个窗口你不需要只用最后一个窗口做预测而是把所有窗口的预测结果取平均。窗口越多预测方差越小但窗口数量一旦超过 40边际收益就趋近于零还会拖慢推理速度。我会按窗口数从 1 到 30 画一条 RMSE 曲线通常在窗口数到 15 左右曲线开始变平后面增加的窗口基本只是徒增计算量。多模型融合的思路更直接用不同的随机种子训练 3 个 TCN每个模型 64 通道和 128 通道各一个组合预测时取中位数而不是平均值。中位数比均值更抗极端预测值因为均值会被个别严重偏离的预测带跑偏。我在 FD001 上试过 3 个模型融合RMSE 从 14.8 降到了 13.1提升幅度比单纯调参大得多训练成本只是原来的三倍。还有一个容易被忽略的验证技巧如果你想快速确认整个流程有没有跑通先不追求精度把通道数设成 16、膨胀层设成 3 层、epoch 设成 5全流程跑一遍能出一组预测曲线就说明数据预处理和模型代码都是通的。这个“小模型冒烟测试”的习惯帮我省下大量 debug 时间。从那以后我每次拿到一个新的时间序列数据集都会强制走一遍小模型跑通、再看曲线、再放大规模的流程而不是一上来就用大模型跑几十个 epoch最后一查是数据切错了。希望这些路径和坑位能帮你在 TCN 这条线上走得比我省事一些。本文还有配套的精品资源点击获取
返回列表