Engee 文档
Notebook

为回归问题创建 seq2seq 模型

在本例中,我们将训练一个深度神经网络来预测燃气涡轮发动机的剩余寿命。

加载和解压数据

CMAPSSData 开放式数据集存储了模拟发动机数据:100 个训练示例和 100 个验证示例。训练数据包含由从启动到故障发生的一系列测量参数组成的时间序列。验证数据在故障发生前截断。

每行包含 26 个变量:

  • 1: 发动机编号
  • 2: 运行时间(以循环为单位)
  • 3-5: 发动机设置
  • 6-26: 传感器测量值 1-21

准备工作环境并加载数据集。

In [ ]:
Pkg.add(["Statistics", "DelimitedFiles", "JLD2", "Flux"])
In [ ]:
using DelimitedFiles
In [ ]:
dataTrain = Float32.( readdlm( "CMAPSSData/train_FD001.txt" ));

准备训练数据

经过上述处理后,我们将得到数组XTrainYTrain ,其中包含由性状(预测因子,predictors)和目标变量(目标,targets)组成的时间序列。

In [ ]:
function dataPreparation( dataTable )
    numObservations = maximum( dataTrain[:,1] )

    predictors = []
    responses = []
    
    for i  1:numObservations
        idx = dataTable[:,1] .== i
        push!( predictors, dataTable[idx, 3:end] )
        timeSteps = dataTable[idx,2]
        push!( responses, reverse(timeSteps) )
    end
    
    return predictors, responses
end
Out[0]:
dataPreparation (generic function with 1 method)
In [ ]:
XTrain, YTrain = dataPreparation( dataTrain );

去除常数符号

常量符号对我们的学习过程没有任何帮助,因此为了加快计算速度,我们将去除常量符号。也就是说,我们将从数据集中删除最小值和最大值相同的列。

In [ ]:
m = [ minimum( table, dims=1 ) for table in XTrain ];
M = [ maximum( table, dims=1 ) for table in XTrain ];
In [ ]:
idxConstant = [ m[i] .== M[i] for i in 1:length(m) ];
constant_features = vec( maximum( vcat(idxConstant...), dims=1 ) );
In [ ]:
XTrain = [ xtrain[:, .!constant_features] for xtrain in XTrain  ];
numFeatures = size( XTrain[1], 2 )
Out[0]:
16

这样,我们就为每个观测值保留了 16 个属性(请记住,所有观测值的长度都不一样,每个发动机在故障前工作的周期数都不一样)。

特征值归一化

另一种通常能加快学习速度的技术是,通过赋予每个序列 0 的数学期望值和 1 的方差来准备特征值。让我们将所有电机的信号收集到一列中。

In [ ]:
using Statistics

mu = vec(mean( vcat([xtrain for xtrain in XTrain]...), dims=1 ));
sig = vec(std( vcat([xtrain for xtrain in XTrain]...), dims=1 ));

XTrain = [ mapslices(row -> (row .- mu) ./ sig, xtrain; dims=2) for xtrain in XTrain ];
In [ ]:
gr()
plot(
    heatmap( XTrain[1], cbar=false, yflip=true, title="Наблюдения (predictors)", ylabel="Время (циклы)" ),
    heatmap( reshape(YTrain[1],:,1), yflip=true, title="Целевая переменная: остаток ресурса (target)" ),
    size=(800,400)
)
plot!( titlefont = font(9) )
Out[0]:

输出变量的约束条件

我们将预测一个单一的数字--故障发生前剩余的工作周期数。对我们来说,更重要的是在生命周期即将结束时预测更准确。

因此,我们将把预测变量限制为 150 个 MTBF 周期。

In [ ]:
thr = 150
YTrain = [ min.(ytrain, 150) for ytrain in YTrain ];

这就是数据集中某个电机的传感器读数。

In [ ]:
obs_id = 77

dl = size( XTrain[obs_id], 1 );
cl = maximum( findall( YTrain[obs_id] .== thr ))

a = plot( XTrain[obs_id], leg=:false, title="Показания датчиков", label=:none )
vline!(a, [dl], lw=3, lc=:red, label=:none )
plot!(a,  [1, cl], [-4,-4], fillrange=[4,4], fillcolor=:springgreen, fillalpha=0.3, linealpha=0.0, label=:none )

b = plot( YTrain[obs_id], title="Наработка на отказ (в циклах)", label="Ресурс" )
vline!(b, [dl], lw=3, lc=:red, label="Отказ" )
plot!(b,  [1, cl], [0,0], fillrange=[thr+20,thr+20], fillcolor=:springgreen, fillalpha=0.3, linealpha=0.0, label="Полный ресурс" )

plot(a, b, layout=(2,1) )
Out[0]:

让我们对数据进行排序

这是一个可选步骤,在某些时候它将帮助我们把数据分成更理想的批次,但首先我们只需要找出样本中最长序列的长度等于多少。

In [ ]:
# Упорядочить данные по размеру
sequenceLength = [ size(xtrain,1) for xtrain in XTrain ];
idx = sortperm( sequenceLength, rev=true );
XTrain = XTrain[idx];
YTrain = YTrain[idx];
In [ ]:
bar( sort(sequenceLength), xlabel="Последовательность", ylabel="Длительность", title="Отсортированные данные", xflip=true)
Out[0]:

神经网络架构

我们将训练两个递归神经网络并比较结果:

RNN 神经网络个基本的递归层,然后是几个全连接层) LSTM 神经网络一个稍复杂的 LSTM 层代替基本的递归层,其他层相同*)。

Flux 库更新到最新版本之前,以下代码会产生无害的错误信息Error during loading of extension DiffEqBaseZygoteExt... ,不会影响操作。只需右击它并选择Удалить выбранное 即可。

In [ ]:
Pkg.add( "Flux" ); # Установка библиотеки на случай ее отсутствия
In [ ]:
using Flux

numResponses = size( YTrain[1], 2 )
numHiddenUnits = 100
numHiddenUnits2 = 40

rnn = Chain( RNN(numFeatures => numHiddenUnits), 
             Dense(numHiddenUnits => numHiddenUnits2),
             Dropout( 0.5 ),
             Dense(numHiddenUnits2 => numResponses) ) |> f32;

lstm = Chain(
    LSTM(numFeatures => numHiddenUnits),
    Dense( numHiddenUnits => 50 ),
    Dropout( 0.5 ),
    Dense( 50 => numResponses )
) |> f32;

让我们记住这两个神经网络在训练前的状态,以便稍后与训练后的网络进行比较。

In [ ]:
rnn_empty = deepcopy( rnn );
lstm_empty = deepcopy( lstm );

损失函数(RMSE)的计算公式如下:

In [ ]:
function loss(x, y)
    s = 0
    for (xi,yi) in zip(x,y)
        Flux.reset!( model );
        xi_pred = [ model(xi[i,:])[end] for i in 1:size(xi,1) ];
        s = s + (mse(xi_pred, yi)) .* 2
    end
    return sqrt( s )
end
Out[0]:
loss (generic function with 1 method)

它返回样本中每个序列的所有预测误差之和。

训练周期

首先,我们来训练 RNN 神经网络。

In [ ]:
epochs = 100;

该网络训练到第 100 个历元的时间为 5-7 分钟。

In [ ]:
model = rnn

opt = Flux.Optimiser(ClipValue(1), Adam(0.01))
θ = Flux.params( model )

for epoch  1:epochs
    print( epoch, " ")
     = gradient(θ) do
        loss( XTrain, YTrain )
    end
    Flux.update!(opt, θ, )
end

# Сохраним полученную нейросеть в файл
using JLD2
if !isfile( "rnn.jld2" ) jldsave("rnn.jld2"; rnn); end

作为比较,让我们来训练一个 LSTM 神经网络(注意,训练到第 100 个历元的时间可能会超过 15 分钟)。我们还要补充一点,隐藏层中神经元的数量当然会影响训练时间,但影响程度不如epoch的数量。

In [ ]:
model = lstm

opt = Flux.Optimiser(ClipValue(1), Adam(0.01))
θ = Flux.params( model )

for epoch  1:epochs
    print( epoch, " ")
     = gradient(θ) do
        loss( XTrain, YTrain )
    end
    Flux.update!(opt, θ, )
end

# Сохраняем нейросеть
if !isfile( "lstm.jld2" ) jldsave("lstm.jld2"; lstm); end

LSTM 神经网络比 RNN 可以 "学习 "更长期的依赖关系。在传统的递归神经网络中,近期测量结果对结果的影响总是高于早期测量结果对结果的影响。也就是说,RNN 往往会"忘记遥远的过去 "

但是,由于每个神经元中的方程数量较多,LSTM 神经网络的学习速度比 RNN 慢 2-3 倍。

检查结果

神经网络非常擅长预测训练数据:

In [ ]:
test_id = 6

xi = XTrain[test_id]
yi = YTrain[test_id]

# Прогнозы до обучения
y_pred_rnn_empty = [ rnn_empty(xi[i,:])[end] for i in 1:size(xi,1) ];
y_pred_lstm_empty = [ lstm_empty(xi[i,:])[end] for i in 1:size(xi,1) ];
# Прогнозы после обучения
y_pred_rnn = [ rnn(xi[i,:])[end] for i in 1:size(xi,1) ];
y_pred_lstm = [ lstm(xi[i,:])[end] for i in 1:size(xi,1) ];

plot(
    plot( [y_pred_rnn_empty y_pred_lstm_empty], label=["rnn" "lstm"], title="Прогнозы до обучения", lw=2 ),
    plot( [y_pred_rnn y_pred_lstm yi], label=["rnn" "lstm" "истина"], title="Прогнозы обученной нейросети", lw=2 ),
    size=(800,300), titlefont=font(11)
)
Out[0]:

正如我们所见,在训练之前,神经网络的预测结果总是接近于零。训练后,它学会了将 16 维向量与剩余资源的标量预测联系起来。我们可以认为,模型考虑到了过程的前史,因为尽管信号的行为相当混乱,但预测结果总是呈下降趋势。

让我们研究一下训练样本的预测结果。

In [ ]:
function get_plot(id)
    
    xi = XTrain[id]
    yi = YTrain[id]
    
    Flux.reset!( rnn )  # Сбросим состояние нейросети, чтобы накопленная информация
    Flux.reset!( lstm ) # от прошлых прогонов не повлияла на будущие прогнозы
    
    y_pred_rnn = [ rnn(xi[i,:])[end] for i in 1:size(xi,1) ];
    y_pred_lstm = [ lstm(xi[i,:])[end] for i in 1:size(xi,1) ];
    
    return plot( [y_pred_rnn y_pred_lstm yi], legend=false, lw=2, title="Прогнозы для выборки #$id" )
end

plot( get_plot.(1:16)..., titlefont=font(7), size=(800,600) )
Out[0]:
Out[0]:

可以看出,在 LSTM 测试数据上,神经网络已经学会了很好地检测资源的起始点,并跟踪动态直至结束。但在 RNN 神经网络的训练结果中,学习特征的非线性表现得更为明显(函数中间的驼峰)。

让我们准备测试数据:

In [ ]:
dataTest = Float32.( readdlm( "CMAPSSData/test_FD001.txt" ));
XTest, YTest = dataPreparation( dataTest );
XTest = [ xtest[:, .!constant_features] for xtest in XTest  ];
mu = vec(mean( vcat([xtest for xtest in XTest]...), dims=1 ));
sig = vec(std( vcat([xtest for xtest in XTest]...), dims=1 ));
XTest = [ mapslices(row -> (row .- mu) ./ sig, xtest; dims=2) for xtest in XTest ];

# Дополнение: мы знаем остаточный ресурс двигателей в тестовой выборке
YTest = Float32.( readdlm( "CMAPSSData/RUL_FD001.txt" ));
YTest = [ collect((size(XTest[i],1)+(YTest[i])-1):-1:YTest[i]) for i in 1:length(XTest) ];
YTest = [ min.(YTest[i], 150) for i in 1:size(YTest,1) ];

让我们看看模型如何预测总样本(测试样本)中一系列测量值的 MTBF。

In [ ]:
function get_plot( id )
    
    xi = XTest[id]
    yi = YTest[id]
    
    Flux.reset!( rnn )  # Сбросим состояние нейросети, чтобы накопленная информация
    Flux.reset!( lstm ) # от прошлых прогонов не повлияла на будущие прогнозы
    
    y_pred_rnn = [ rnn(xi[i,:])[end] for i in 1:size(xi,1) ];
    y_pred_lstm = [ lstm(xi[i,:])[end] for i in 1:size(xi,1) ];
    
    return plot( [y_pred_rnn y_pred_lstm yi], legend=false, lw=2, title="Прогнозы для выборки #$id", c=[4 6 3] )
end

plot( get_plot.(1:16)..., titlefont=font(7), size=(800,600), legendfont=font(8) )
Out[0]:
Out[0]:

神经网络在测试数据上的表现稍差,但这一信息可以让我们选择进一步完善架构和训练过程的方向。正如我们所预期的那样,当我们切换到测试数据时,两种神经网络的预测质量都会下降(LSTM 下降 2.1 倍,RNN 下降 2.7 倍)。

最后,让我们研究一下所研究的生命周期各阶段结束时的资源预测误差直方图:

In [ ]:
dataset_final_rul = [ ytest[end] for ytest in YTest ]
rnn_final_prediction = [ [(Flux.reset!( rnn ); rnn(xi[i,:])[end]) for i in 1:size(xi,1)][end] for xi in XTest]
lstm_final_prediction = [ [(Flux.reset!( lstm ); lstm(xi[i,:])[end]) for i in 1:size(xi,1)][end] for xi in XTest]

plot(
    histogram( dataset_final_rul .- rnn_final_prediction, leg=:none, title="Ошибка прогноза RNN" ),
    histogram( dataset_final_rul .- lstm_final_prediction, leg=:none, title="Ошибка прогноза LSTM" ),
    size=(900,300), titlefont=font(9)
)
Out[0]:
Out[0]:

可以说,LSTM 网络的平均误差比 RNN 更接近于 0。

我们将神经网络保存到文件中,以备进一步使用。

要将它们加载到内存中,首先需要加载库Flux

结论

我们已经训练了一个预测 MTBF 的神经网络,现在可以构建一个间接测量传感器,在 Engee 模型环境中进行实践,并为嵌入式平台生成代码和进行实际测试。

还要注意的是,为了加速学习过程,我们采用了一个非常小的神经网络,并对其进行了相当少次数的训练。在两个处理器(不含 GPU)*的 4 个线程中,训练两个模型各耗时约 10 分钟。