基于卷积神经网络的航空发动机剩余寿命预测Matlab实现
本文利用NASA提供的涡扇发动机退化数据集,进行数据预处理,构建训练样本和测试样本,然后搭建卷积神经网络(Convolutional Neural Network,CNN),学习训练数据,最后利用测试数据,分析神经网络模型预测剩余使用寿命(Remaining useful life,RUL)的效果。
1.卷积神经网络
卷积神经网络(Convolutional Neural Networks,CNNs)是一种深度学习模型,它在图像处理、语音识别、自然语言处理等多个领域都有着广泛的应用。
CNNs的设计灵感来源于人类视觉系统的工作原理,它特别适合处理具有网格状拓扑结构的数据,比如图像,著名的卷积神经网络模型有AlexNet、InceptionNet、VGGNet和ResNet等。
值得注意是,上述的这些卷积神经网络是对图片进行卷积操作,它会利用当前位置的前后四周的数据。而在本文案例中,在进行卷积操作时,处理的对象是时间序列,如果还按照上述的卷积方式,会将未来数据带入计算,这是矛盾的,因此需要改变卷积网络的参数。
该数据集为C-MAPSS模拟数据,该数据是模拟大型商用涡扇发动机的数据, 发动机简图如上图所示。该数据的代码采用了MATLAB及其Simulink模块。
2.方法具体流程
2.1 数据预处理
读取航空发动机的退化数据集,并开展数据预处理操作,整理训练样本和测试样本。
clc
close all
clear all
%数据变量的维度的含义
% data文件夹现在包含包含26列数字的文本文件,用空格分隔。
% 每一行都是在单个操作周期中获取的数据,每列表示不同的变量:
% 列 1 — 发动机单元编号
% 列 2 — 时间步
% 列 3–5 — 运行设置
% 列 6–26 — 传感器1–21
% 数据介绍页面(数据集A) https://mp.weixin.qq.com/s/RPToBmMvt5EJth42eXL2JA
%% 数据预处理
dataFolder = "D:\我的文档\硕士\信息\副业\公众号\公众号" + ...
"\20240518基于卷积神经网络的航空发动机剩余寿命预测方法\" + ...
"20240518基于卷积神经网络的航空发动机剩余寿命预测方法\data";
filenameTrainPredictors = fullfile(dataFolder,"train_FD001.txt");
rawTrain = localLoadData(filenameTrainPredictors); %训练样本 Table类型数据
head(rawTrain.X{1},8) %展示部分数据的输入
rawTrain.Y{1}(1:8) %展示部分数据的输出(RUL标签)
stackedplot(rawTrain.X{1},[3,5,6,7,8,15,16,24],XVariable='timeStamp') %展示部分数据在单个样本周期的趋势
prog = prognosability(rawTrain.X,"timeStamp");% 判断单个样本全寿命周期中不随时间周期变换的特征,即无用的输入
idxToRemove = prog.Variables==0 | isnan(prog.Variables); %无用的输入的索引
featToRetain = prog.Properties.VariableNames(~idxToRemove);
for i = 1:height(rawTrain)
rawTrain.X{i} = rawTrain.X{i}{:,featToRetain}; %有用的输入
end
[~,Xmu,Xsigma] = zscore(vertcat(rawTrain.X{:})); %归一化输入,准备进行Z标准化,
preTrain = table();
for i = 1:numel(rawTrain.X) %Z标准化
preTrain.X{i} = (rawTrain.X{i} - Xmu) ./ Xsigma;
end
rulThreshold = 150; %设置最大的RUL标签为150,过大的RUL没有指导意义,合适的RUL也有利于网络训练的收敛
for i = 1:numel(rawTrain.Y) %输出标签预处理
preTrain.Y{i} = min(rawTrain.Y{i},rulThreshold);
end
for i = 1:size(preTrain,1)
preTrain.X{i} = preTrain.X{i}'; %这是第一个维度为输入数据的特征维度
preTrain.Y{i} = preTrain.Y{i}'; %输出也需要
sequence = preTrain.X{i};
sequenceLengths(i) = size(sequence,2);
end
[sequenceLengths,idx] = sort(sequenceLengths,'descend'); %按照样本的时间长短进行排序,对网络训练有用
% 在后续trainingOptions中,每次epoch都不打乱样本的顺序(Shuffle='never'),这是排序是因为每次训练都是一个batch,
% 由于长度不同往往需要一个统一的长度,才能一同送入网络中,当根据样本长度差异较大,需要补0操作,占据存储空间且不利于网络的学习真正有用的特征
% 当排序后,需要补充的0少,所以相对于不排序是具有优势的
XTrain = preTrain.X(idx);
YTrain = preTrain.Y(idx);
这展示了部分数据的输入和输出,上图为部分数据的部分输入。从图中能发现,一些变量在全寿命周期没有变换,因此剔除与不随时间变换的无用特征。因为随着时间的增长RUL是单调递减的,这些特征不发生变化,那么它们对预测RUL是没有意义的,所以需要剔除掉。
除此之外,进行了Z标准化,RUL的标签进行了规整,限制输出的RUL标签最大为150,这有利于网络的收敛,可根据具体实际进行调制,过大的RUL对工程指导意义不大(个人观点)。
对数据进行了排序,这样对网络的收敛是有利的。不进行排序,同时训练参数的shuffle改为every-epoch,会发现网络很难收敛。最终的训练集输入和输出分别保存在变量XTrain和YTrain。
2.2 搭建网络模型
搭建卷积神经网络模型,并利用训练数据训练神经网络模型。
%% 构建神经网络
numFeatures = size(XTrain{1},1); %输入特征的个数,
numResponses = 1;
%神经网络结构
layers = [
sequenceInputLayer(numFeatures)
convolution1dLayer(5,32,Padding="causal") %这里采用了因果填充的方式,确保模型的输出不会违反时间上的因果关系,参考时间卷积神经网络中的因果卷积
batchNormalizationLayer()
reluLayer()
convolution1dLayer(7,64,Padding="causal")
batchNormalizationLayer
reluLayer()
convolution1dLayer(11,128,Padding="causal")
batchNormalizationLayer
reluLayer()
convolution1dLayer(13,256,Padding="causal")
batchNormalizationLayer
reluLayer()
convolution1dLayer(15,512,Padding="causal")
batchNormalizationLayer
reluLayer()
fullyConnectedLayer(100)
reluLayer()
dropoutLayer(0.5)
fullyConnectedLayer(numResponses)
regressionLayer()];
maxEpochs = 40; %最大Epoch数
miniBatchSize = 16; %最小batchsize
options = trainingOptions('adam',...
LearnRateSchedule='piecewise',...
MaxEpochs=maxEpochs,...
MiniBatchSize=miniBatchSize,...
InitialLearnRate=0.01,...
GradientThreshold=1,...
Shuffle='never',...
Plots='training-progress',...
Verbose=0);
net = trainNetwork(XTrain,YTrain,layers,options); %训练神经网络模型
%网络结构展示
figure;
lgraph = layerGraph(net.Layers);
plot(lgraph)
卷积神经网络的训练过程:
从上图结果看,RSME有待进一步提高。多次运行代码,网络收敛的曲线较为接近,无明显差异。修改学习率、Epoch、batchsize大小,结果无明显变化,可能需要进一步进行网络结构的优化,增加新的输入层,提出更好的网络模型。
2.3 剩余寿命预测
采用已训练的神经网络模型,预测测试样本的剩余使用寿命,并采用RMSE评估网络的预测效果。
值得注意是,这个代码里面有两个函数:localLoadData(加载数据用的,同时制作RUL标签)和localLambdaPlot(画图函数)。
%% 测试模型
filenameTestPredictors = fullfile(dataFolder,'test_FD001.txt');
filenameTestResponses = fullfile(dataFolder,'RUL_FD001.txt');
dataTest = localLoadData(filenameTestPredictors,filenameTestResponses);
%测试数据进行Z标准化
for i = 1:numel(dataTest.X)
dataTest.X{i} = dataTest.X{i}{:,featToRetain};
dataTest.X{i} = (dataTest.X{i} - Xmu) ./ Xsigma;
dataTest.Y{i} = min(dataTest.Y{i},rulThreshold);
end
predictions = table(Size=[height(dataTest) 2],VariableTypes=["cell","cell"],VariableNames=["Y","YPred"]);
%预测
for i=1:height(dataTest)
unit = dataTest.X{i}';
predictions.Y{i} = dataTest.Y{i}';
predictions.YPred{i} = predict(net,unit,MiniBatchSize=1);
end
%计算RMSE 评估网络
for i = 1:size(predictions,1)
predictions.RMSE(i) = sqrt(mean((predictions.Y{i} - predictions.YPred{i}).^2));
end
%RMSE直方图
figure;
histogram(predictions.RMSE,NumBins=10);
title("RMSE ( Mean: " + round(mean(predictions.RMSE),2) + " , StDev: " + round(std(predictions.RMSE),2) + " )");
ylabel('Frequency');
xlabel('RMSE');
%% 展示预测曲线
figure;
localLambdaPlot(predictions,"random");
figure;
localLambdaPlot(predictions,"best");
figure;
localLambdaPlot(predictions,"worst");
figure;
localLambdaPlot(predictions,"average");
%% 加载数据的函数
function data = localLoadData(filenamePredictors,varargin)
if isempty(varargin)
filenameResponses = [];
else
filenameResponses = varargin{:};
end
rawData = readtable(filenamePredictors);
% 输入数据的名称
VarNames = {...
'id', 'timeStamp', 'op_setting_1', 'op_setting_2', 'op_setting_3', ...
'sensor_1', 'sensor_2', 'sensor_3', 'sensor_4', 'sensor_5', ...
'sensor_6', 'sensor_7', 'sensor_8', 'sensor_9', 'sensor_10', ...
'sensor_11', 'sensor_12', 'sensor_13', 'sensor_14', 'sensor_15', ...
'sensor_16', 'sensor_17', 'sensor_18', 'sensor_19', 'sensor_20', ...
'sensor_21'};
rawData.Properties.VariableNames = VarNames;
if ~isempty(filenameResponses)
RULTest = readmatrix(filenameResponses);
end
IDs = rawData{:,1};
nID = unique(IDs);
numObservations = numel(nID);
% 初始化
data = table(Size=[numObservations 2],...
VariableTypes={'cell','cell'},...
VariableNames={'X','Y'});
for i=1:numObservations
idx = IDs == nID(i);
data.X{i} = rawData(idx,:);
if isempty(filenameResponses)
% 计算RUL
data.Y{i} = flipud(rawData.timeStamp(idx))-1;
else
sequenceLength = sum(idx);
endRUL = RULTest(i);
data.Y{i} = [endRUL+sequenceLength-1:-1:endRUL]';
end
end
end
%%
function localLambdaPlot(predictions,lambdaCase)
if isnumeric(lambdaCase)
idx = lambdaCase;
else
switch lambdaCase %展示结果
case {"Random","random","r"}
idx = randperm(height(predictions),1);
case {"Best","best","b"}
idx = find(predictions.RMSE == min(predictions.RMSE));
case {"Worst","worst","w"}
idx = find(predictions.RMSE == max(predictions.RMSE));
case {"Average","average","a"}
err = abs(predictions.RMSE-mean(predictions.RMSE));
idx = find(err==min(err),1);
end
end
y = predictions.Y{idx};
yPred = predictions.YPred{idx};
x = 0:numel(y)-1;
plot(x,y,x,yPred)
legend("True RUL","Predicted RUL")
xlabel("Time stamp (Test data sequence)")
ylabel("RUL (Cycles)")
title("RUL for Test engine #"+idx+ " ("+lambdaCase+" case)")
end
上图为测试样本的RMSE偏差分布的直方图,从图中能发现测试样本的RMSE偏差集中在10-15之间,有待进一步优化和提高。
下图为部分样本的RUL预测结果,分别对应了随机选择样本的RUL预测结果、RMSE偏差最小样本的RUL预测结果、RMSE偏差最大样本的预测结果和RMSE偏差适中样本的预测结果。
随机选择样本的RUL预测结果
RMSE偏差最小样本的RUL预测结果
RMSE偏差最大样本的预测结果
RMSE偏差适中样本的预测结果
从上图中能发现,虽然第二个样本的RMSE偏差最小,但预测的RUL较真实的RUL偏差较大。相反,第四个图片展示的结果,随着RUL的减小,预测的RUL不断逼近、跟踪真实的RUL,这表明了CNN网络在预测RUL方面也有一定的可行性。