简介:利用长短期记忆网络(LSTM)在C-MAPSS数据集上实现涡扇发动机剩余寿命预测的Pytorch完整代码包,面向故障预测与健康管理(PHM)领域的研究者、工业数据分析人员以及深度学习时序建模爱好者。整个资源包共包含19个文件,压缩后大小约13.62MB,其中以可运行的Python脚本和结果记录文本为主,涵盖数据预处理、模型定义、训练与测试等完整流程,同时提供了不同子数据集的划分文件与预测结果输出。实验环节列出了多轮训练后的score和RMSE误差指标,并与多种混合深度学习模型进行了对比,体现出该方法在收敛速度和接近失效时刻预测精度上的优势。已有1641人学习下载,非常适合需要快速掌握时序预测建模、复现剩余寿命基准实验或参考其数据预处理与划分思路的开发者,无论是毕业设计、课程作业还是竞赛复现,均可从中获得完整的建模范式。 第一次看到C-MAPSS这个数据集的时候,我第一反应是“又一个跑模型刷榜的公开数据集”。但真正上手用LSTM做剩余寿命预测之后,我才意识到,这个任务最锻炼人的地方不在于把模型网络搭得多花哨,而在于能不能把工业时序数据处理好、能不能把“寿命”这个标签设计对。如果你刚入门工业PHM方向,或者已经会PyTorch基础操作、想找一个小而完整的回归任务练手,用C-MAPSS配合LSTM跑一个剩余寿命预测项目,是非常合适的路径。
这个项目要解决的事情很直观:发动机从出厂到退化的过程中,传感器会记录下一系列周期性信号,我们希望根据最近一段时间的监测数据,预测这台发动机还能正常工作多少个周期。用LSTM来做,是因为这类数据天然就是多变量长时间序列,而LSTM对时序依赖的建模能力,刚好匹配这个需求。下面我从数据、预处理、模型、训练、评估到各种坑,完整拆一遍我实践下来的思路。
1. 项目概述与C-MAPSS数据集
1.1 C-MAPSS到底预测的是什么
C-MAPSS是NASA公开发布的仿真数据集,模拟涡扇发动机在不同工况下的退化过程。里面包含四个子集,最常用的是FD001,也是最适合新手起步的一个。FD001只包含一种工况、一种故障模式,训练集有100台发动机的完整生命周期数据,测试集有100台发动机的截断数据,目标就是根据截断之后的监测信号,预测每台发动机还能继续运行多久。
数据文件里每一行是一个监测周期的记录,主要包含三部分信息:发动机编号、当前循环周期、21个传感器读数。发动机编号用于区分不同发动机,循环周期表示当前记录发生在发动机生命周期的第几个cycle。测试集不会给出完整的退化过程,而是在某一时刻截断,预测目标就是从这个截断点到最后故障之间的周期数,也就是剩余使用寿命RUL。
用生活化的例子来理解:这就像你的车仪表盘上显示“还能跑XX公里”。不同的是,C-MAPSS里的“车况”由21个传感器信号共同表征,而且这些信号会随着使用时间逐渐偏离正常状态。我们要做的,就是从这一段传感器数据里识别出退化趋势,并估算剩余里程。
1.2 为什么这个任务首选LSTM
工业时序数据和股票预测那种纯随机序列不一样,传感器读数里通常存在明显的长期退化趋势,也有短期噪声。普通全连接网络对单个时间点做预测,等于把数据打成“散点”来看,完全丢失了时间顺序信息;而LSTM的优势在于它天然以序列为单位处理数据,在每一个时间步都会维护一个记忆单元,并通过输入门、遗忘门和输出门决定哪些信息要保留、哪些信息要更新。
最开始我也想过,要不要直接上Transformer或者时序卷积网络。后来实际对比下来,在C-MAPSS这种中等长度序列任务上,LSTM完全够用,而且训练速度和调参成本低很多。Transformer需要大量数据做预训练,位置编码和注意力的超参数也比LSTM敏感;LSTM用一个小两层的结构就能得到一个合理基线,后续再往上加注意力、加双向结构,也能清晰看到每一处改动带来的收益。
另外,相比普通RNN,LSTM的门控机制能明显缓解长时间依赖中的梯度消失问题。发动机退化过程跨越上百个周期,恰好是LSTM最舒服的序列长度范围。所以作为这个项目的baseline模型,LSTM几乎是不二之选。
2. 数据预处理与序列构建实操
2.1 传感器筛选与归一化
拿到原始数据之后,不能直接把全部21个传感器塞进模型,因为其中有一部分传感器读数在整条生命周期里几乎没有变化。这些恒定传感器对预测毫无贡献,反而会引入无意义的特征,增加模型参数量。常见的做法是筛选掉方差接近0或标准差为0的列,FD001数据集中一般在筛完后会留下14个传感器通道。
归一化也是这个项目不能省的一步。LSTM使用sigmoid和tanh作为激活函数,对输入数值范围非常敏感。常用方案是MinMaxScaler,把每个传感器特征映射到[0,1]区间;如果传感器数据存在明显离群点,也可以改用StandardScaler。这里必须强调一个最容易踩的坑:归一化只能基于训练集训练scaler,然后分别transform训练集和测试集,绝对不能把训练和测试数据合在一起fit。否则测试信息会泄漏到训练过程里,验证指标虚高,最后线上效果完全对不上。
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_data) test_scaled = scaler.transform(test_data)2.2 滑窗构建序列与RUL标签设计
LSTM输入需要的是形如(batch, seq_len, feature_dim)的序列数据,而原始数据是一个发动机的多条连续记录,不能直接把整段序列一次性丢进模型。我需要定义一个滑窗长度,比如sequence_length=30,表示模型每次看30个连续周期的传感器数据。滑窗从每个发动机数据的第0个周期开始,以步长1向后滑动,每滑一次生成一个样本。
窗口最后一个周期对应的RUL值,就是当前样本的标签。对于训练集,RUL就是真实剩余周期数;测试集则要根据已知的截断点计算。这里还有一个数据处理的细节:对于序列长度不足sequence_length的发动机,通常直接丢弃这些不完整窗口,或者用padding补零,我在工程上更倾向于丢弃,因为补零会让模型学到一种“空数据”的假模式。
RUL标签的经典设计里面,还有一个被很多人忽略的点:线性标签还是分段线性标签。直接用真实RUL作为回归目标当然可以,但发动机在生命周期早期退化非常缓慢,此时预测RUL的意义不大,模型反而会把大量注意力放在“如何拟合一个很大且差异不明显的常数”上。所以很多论文会设置一个上限,比如把超过130的RUL全部截断为130,让模型更关注后期的快速退化阶段。这也是我在实验中提升效果最快的改动之一。
2.3 划分数据集与PyTorch Dataset实现
拿到滑窗样本之后,还需要从训练集内部再划分出一个验证集,用于监控模型收敛和做EarlyStopping。这一步特别容易出错:千万不要直接按样本随机划分,因为同一台发动机生成的多个窗口之间存在强相关性,如果同一台发动机的窗口同时出现在训练集和验证集,验证集指标会失真。正确做法是先把发动机编号划分开,让验证集包含一批完全没参与训练的发动机,再在这些发动机的窗口中切出样本。
接下来是用PyTorch把数据包装成Dataset,方便DataLoader调用。我习惯在预处理阶段就把滑窗和标签存成numpy数组,然后定义Dataset类,这样代码清晰,调试也方便。
import torch from torch.utils.data import Dataset class RulDataset(Dataset): def __init__(self, windows, labels): self.windows = windows self.labels = labels def __len__(self): return len(self.windows) def __getitem__(self, idx): x = torch.tensor(self.windows[idx], dtype=torch.float32) y = torch.tensor(self.labels[idx], dtype=torch.float32) return x, y之后只要把滑窗数组和标签数组传入Dataset,配合DataLoader就能拿到训练批次:
from torch.utils.data import DataLoader train_dataset = RulDataset(train_windows, train_labels) train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)3. LSTM模型设计与PyTorch实现
3.1 网络结构与关键参数选择
模型结构并不复杂:LSTM负责从输入序列中提取时序特征,最后只取序列输出中的最后一个时间步,再经过几个全连接层回归出RUL值。为什么只取最后一个时间步?因为当前窗口最后一个周期就是“现在”,我要预测的是从“现在”开始还能用多久,所以只需要最后时刻的特征。
关于num_layers和hidden_size,我常用的是两层LSTM + 每层64个隐藏单元。层数太少拟合能力不够,层数太多在小数据集上容易过拟合,两层是平衡点。hidden_size增加会明显提升参数量,但C-MAPSS训练样本并不是几十万级别的大数据,64或128足够。如果用了多层LSTM,建议在非最后一层加Dropout,PyTorch的nn.LSTM里自带dropout参数,只在num_layers>1时生效。
参数规模可以简单算一下。假设输入特征维度是14,隐藏层大小64,单层LSTM的参数量大约是4 * (hidden_size * (input_size + hidden_size) + hidden_size),拆开看就是三个门的权重加偏置,加上候选细胞状态的参数。第一层约为4 * (64 * 78 + 64) = 20224,第二层输入从14变成64,约为4 * (64 * 128 + 64) = 33024。两层LSTM合计5.3万参数,再加后面全连接层,总参数量不到5.5万,在这类任务里是非常轻量的。
3.2 PyTorch代码实现
我用PyTorch定义模型时,通常会设置batch_first=True,这样输入形状就是(batch, seq_len, input_size),不用每次转换维度。forward里用LSTM拿到所有时间步的输出后,取last = output[:, -1, :],再通过回归头。回归头用两层全连接加ReLU,最后输出一个标量RUL值。
import torch.nn as nn class RULPredictor(nn.Module): def __init__(self, input_size=14, hidden_size=64, num_layers=2, drop_prob=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=drop_prob ) self.regressor = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Dropout(drop_prob), nn.Linear(32, 1) ) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) last = out[:, -1, :] # 取最后一个时间步 return self.regressor(last).squeeze(-1)训练循环中,损失函数用MSELoss,优化器用Adam,学习率初始1e-3。因为RUL回归本质上是最小化预测值与真实值之间的平方误差,MSE是这类连续值预测最直接的选择。每个batch都要记得optimizer.zero_grad(),否则梯度会累加。
import torch.optim as optim model = RULPredictor() criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) for epoch in range(80): model.train() for windows, labels in train_loader: optimizer.zero_grad() outputs = model(windows) loss = criterion(outputs, labels) loss.backward() optimizer.step()3.3 训练配置与正则化
这类小数据集上,训练轮次并不是越多越好。我一般会在训练过程中每个epoch结束后用验证集算一次RMSE,一旦连续多轮验证指标不再下降就提前停止。这比固定训练100个epoch然后盲目保存最后一个模型要可靠得多。
除了EarlyStopping,还要把随机种子固定下来。LSTM在初始化时带随机性,不固定种子的话,同样的代码跑两次结果可能差出好几分。PyTorch里可以设置torch.manual_seed(42),同时如果是GPU训练,还要配合torch.cuda.manual_seed_all(42)。模型内部的全连接层加上Dropout,既能缓解过拟合,也能让预测更平滑。
学习率调度上,我推荐ReduceLROnPlateau。这类任务的loss曲线经常是在下降到某个平台就停住,这时候手动调低学习率很麻烦,交给调度器监控验证loss,连续几次不下降就降低学习率,比我凭经验调省事很多。
4. 评估指标、常见问题与效果优化
4.1 RMSE与NASA评分函数
评估模型不能只看训练loss,C-MAPSS官方提供了两个核心指标:RMSE和NASA评分函数。RMSE就是均方根误差,衡量预测RUL与真实RUL的平均偏离程度,数值越小越好。但RMSE对高估和低估进行了对称惩罚,这并不完全符合工业场景的真实代价。
NASA评分函数则是不对称的:如果预测的RUL比真实值小,也就是提前报警,惩罚系数是13;如果预测的RUL比真实值大,也就是延迟报警,惩罚系数是10。延迟报警意味着你低估了故障风险,设备可能还没到维护点就坏了,这比提前更换零件更严重。所以做这个项目时,除了RMSE,一定要同时看Score,毕竟工程落地时业务方更关心“漏报”而不是“误报”。
Score的公式可以表示成:对每个测试样本计算差值d = predicted_rul - true_rul,如果d小于0,累加exp(-d/13) - 1;如果d大于0,累加exp(d/10) - 1。核心代码我贴一下,方便直接套用。
import numpy as np def compute_score(true_rul, pred_rul): true_rul = np.array(true_rul).reshape(-1) pred_rul = np.array(pred_rul).reshape(-1) diff = pred_rul - true_rul score = 0.0 for d in diff: if d < 0: score += np.exp(-d / 13) - 1 else: score += np.exp(d / 10) - 1 return score在FD001子集上,一个调参合理的简单LSTM,RMSE通常能跑到15左右,再配合数据清洗、分段RUL和合适的滑窗长度,可以压到12甚至更低。所以不要一开始就追求夸张的十以下,先拿到一个稳定基线更重要。
4.2 常见问题排查速查表
我跑这个项目的时候,前前后后踩过不少坑。有几个问题出现频率极高,如果你刚开始跑C-MAPSS,很可能也会遇到。
| 常见现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 验证集指标很好,测试集一塌糊涂 | 训练/验证划分时没有按发动机编号分开 | 确保同一发动机的所有窗口只出现在一个集合里 |
| 预测结果整体偏高或偏低 | 归一化时把训练和测试数据合并fit了 | 严格只用训练集数据fit scaler,再transform测试集 |
| 训练正常,预测阶段报维度错误 | 测试数据没有滑窗,直接以原始序列输入模型 | 测试集也要用同样的窗口长度和特征列构造窗口样本 |
| Loss变成NaN | 学习率过大或输入数据中有NaN值 | 降低学习率,检查预处理后的数组是否存在异常值 |
| 训练速度很慢 | 序列长度过长或LSTM层数过多 | 适当缩短滑窗长度,或者减少hidden_size再观察 |
这里重点说两个容易被忽略的细节。第一个是测试集的滑窗方式:因为我们只关心测试集最后一刻的RUL,所以在推理时,不是把测试发动机每个窗口都预测一遍取平均,而是只取最后一个完整窗口预测一次。第二个是标签的尺度:如果训练时对RUL做了分段截断,测试集评估时通常仍然使用真实RUL,不应该把截断后的标签用在测试评估里,否则计算出来的Score会不符合官方评估逻辑。
4.3 从Baseline到更优:进阶方向
当你的LSTM baseline稳定之后,想要继续提升效果,我建议按顺序尝试几个方向。第一个是双向LSTM,把序列从正反两个方向读一遍,能够同时捕捉退化趋势的“上升阶段”和“末期突变”,在很多C-MAPSS实验里都能带来稳定提升。第二个是给模型加一层时间注意力机制,让网络自己学会关注最后几十个周期里更重要的几个时间点,这比单纯用最后一个时间步输出的信息量更大。
另外,FD002到FD004子集涉及多工况和多故障模式,如果后续想挑战复杂场景,可以把工况设置列也作为输入特征,或者先按工况聚类再分别训练模型。不过对于刚接触这个任务的人来说,先把FD001这套单工况流程跑顺,理解滑窗、标签、归一化、评估这些核心环节,比盲目堆模型结构要重要得多。我自己在这个项目上最大的体会就是:RUL预测的分数,很多时候不是差在模型,而是差在数据处理的细节上。传感器选哪些、窗口开多长、RUL要不要截断,这些都值得多花时间做实验对比。
本文还有配套的精品资源,点击获取