Engee 文档
Notebook

为回归任务创建seq2seq模型

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

下载和解包数据

CMAPSSData开放数据集存储模拟发动机运行数据。:100个培训示例和100个验证示例。 训练数据包含由从启动到失败记录的测量参数序列组成的时间序列。 验证数据在失败事件发生之前终止。

每行包含26个变量。:

*1:发动机编号
*2:操作时间(以周期为单位)
*3-5:引擎设置
*6-26:传感器测量1-21

让我们准备工作环境并上传数据集。

In [ ]:
Pkg.add(["Statistics", "DelimitedFiles", "JLD2", "Flux"])
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`
In [ ]:
using DelimitedFiles
In [ ]:
dataTrain = Float32.( readdlm( "CMAPSSData/train_FD001.txt" ));

准备训练数据

作为这种处理的结果,我们将得到数组 XTrainYTrain,包含由特征(预测变量,预测变量)和目标变量(目标,目标)组成的时间序列。

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个操作周期的资源。

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" ); # Установка библиотеки на случай ее отсутствия
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`
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);

学习周期

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

In [ ]:
epochs = 100;

这个网络在第100个纪元之前的训练时间将是5-7分钟。

In [ ]:
using JLD2
if !isfile( "rnn.jld2" )
    model = rnn
    data = zip(XTrain, YTrain)
    opt_state = Flux.setup( OptimiserChain(ClipGrad(1), Adam(1e-2)), model )
    loss( , y ) = sqrt(Flux.mse( , y ))

    for epoch  1:epochs
        print( epoch, " ")
        Flux.train!( model, data, opt_state) do m, x, y
            loss(m(x')', y) # Функция потерь - ошибка на каждом элементе датасета
        end
    end

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

为了比较,我们将训练一个LSTM神经网络(小心,直到第100个epoch的训练时间可能超过15分钟)。 我们还要补充一点,隐藏层中的神经元数量当然会影响学习时间,但不会像epoch的数量那么多。

In [ ]:
if !isfile( "lstm.jld2" )
    model = lstm
    data = zip(XTrain, YTrain)
    opt_state = Flux.setup( OptimiserChain(ClipGrad(1), Adam(1e-2)), model )
    loss( , y ) = sqrt(Flux.mse( , y ))

    for epoch  1:epochs
        print( epoch, " ")
        Flux.train!( model, data, opt_state) do m, x, y
            loss(m(x')', y) # Функция потерь - ошибка на каждом элементе датасета
        end
    end

    jldsave("lstm.jld2"; lstm); # Сохраняем нейросеть
end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 

据信,LSTM神经网络可以比Rnn"内化"更长期的依赖关系。

在常规的循环神经网络中,最近测量对结果的影响总是高于早期测量的影响。

也就是说,Rnn倾向于*"忘记遥远的过去"*。 但是,另一方面,由于每个神经元中的方程数量较多,LSTM神经网络的学习速度比RNN慢2-3倍。

检查结果

神经网络在预测训练数据方面相当出色:

In [ ]:
using JLD2
@load "rnn.jld2" rnn
@load "lstm.jld2" lstm

test_id = 6

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

# Прогнозы до обучения
y_pred_rnn_empty = rnn_empty(xi');
y_pred_lstm_empty = lstm_empty(xi');

# # Прогнозы после обучения
y_pred_rnn = rnn(xi')
y_pred_lstm = lstm(xi')

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');
    y_pred_lstm = lstm(xi');
    
    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]:

可以看到,基于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) ];

让我们看看模型如何从总样本(在测试样本上)预测一系列测量的失败时间。

In [ ]:
function get_plot( id )
    
    xi = XTest[id]
    yi = YTest[id]
    
    Flux.reset!( rnn )  # Сбросим состояние нейросети, чтобы накопленная информация
    Flux.reset!( lstm ) # от прошлых прогонов не повлияла на будущие прогнозы
    
    y_pred_rnn = rnn(xi')
    y_pred_lstm = lstm(xi');
    
    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]:

神经网络在测试数据上的表现略差,但正是这些信息使我们能够为最终确定架构和学习过程选择进一步的方向。 正如预期的那样,当切换到测试数据时,两个神经网络的预测质量都会下降(LSTM的2.1倍和RNN的2.7倍)。

最后,让我们检查每个研究的生命周期段结束时的资源预测误差直方图。:

In [ ]:
dataset_final_rul = [ ytest[end] for ytest in YTest ]
rnn_final_prediction = [(Flux.reset!( rnn ); rnn(xi')[end]) for xi in XTest]
lstm_final_prediction = [(Flux.reset!( lstm ); lstm(xi')[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]:

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

我们已经将神经网络保存到文件中以供将来参考。

要将它们加载到内存中,您需要先下载库 Flux.

结论

我们已经训练了神经网络来预测故障时间,现在我们可以组装一个间接测量传感器,以便在Engee模型环境中进行测量,并为嵌入式平台生成代码并在实际条件下进行测试。

我们还注意到,出于教育目的,为了加快学习过程,我们实施了一个非常小的神经网络,并对其进行了相当少量的周期训练。 两种模型的训练在两个处理器(不含GPU)上的4个线程中花费了大约10分钟