Main Content

딥러닝을 사용한 시계열 전망

이 예제에서는 장단기 기억(LSTM) 신경망을 사용하여 시계열 데이터를 전망하는 방법을 보여줍니다.

LSTM 신경망은 루프를 사용하여 시간 스텝을 순회하고 순환 신경망(RNN) 상태를 업데이트하여 입력 데이터를 처리하는 RNN입니다. RNN 상태에는 모든 이전 시간 스텝에서 기억한 정보가 포함됩니다. LSTM 신경망을 사용하면 이전 시간 스텝을 입력값으로 사용해서 시계열 또는 시퀀스의 후속 값을 전망할 수 있습니다. LSTM 신경망에 시계열 전망을 훈련시키려면 시퀀스 출력이 있는 회귀 LSTM 신경망을 훈련시키며, 이때 응답 변수(목표값)는 시간 스텝 하나만큼 값이 이동된 훈련 시퀀스입니다. 즉, LSTM 신경망은 입력 시퀀스의 시간 스텝마다 다음 시간 스텝의 값을 예측하도록 학습합니다.

전망 방법에는 개루프 전망과 폐루프 전망의 두 가지 방법이 있습니다.

  • 개루프 전망은 입력 데이터만 사용하여 시퀀스에서 다음 시간 스텝을 예측합니다. 후속 시간 스텝을 예측할 때는 데이터 소스에서 실제 값을 수집하여 입력값으로 사용합니다. 예를 들어, 시퀀스의 1부터 t-1까지의 시간 스텝에서 수집한 데이터를 사용하여 시간 스텝 t에서의 값을 예측한다고 해보겠습니다. 시간 스텝 t+1에서 예측을 수행하려면 시간 스텝 t에서의 실제 값이 기록될 때까지 기다렸다가 이 값을 입력값으로 사용하여 다음 예측을 수행합니다. RNN에 실제 값을 전달하여 다음 예측을 수행할 수 있다면 개루프 전망을 사용하십시오.

  • 폐루프 전망은 이전 예측을 입력값으로 사용하여 시퀀스의 후속 시간 스텝을 예측합니다. 이 경우 모델은 예측을 수행하는 데 실제 값이 필요하지 않습니다. 예를 들어, 시퀀스의 1부터 t-1까지의 시간 스텝에서 수집된 데이터만 사용해서 t부터 t+k까지의 시간 스텝의 값을 예측한다고 해보겠습니다. 시간 스텝 i에 대해 예측을 수행하려면 시간 스텝 i-1에서 예측된 값을 입력값으로 사용합니다. 여러 개의 후속 시간 스텝을 전망해야 하거나 다음 예측을 수행하기 전에 RNN에 전달할 수 있는 실제 값이 없다면 폐루프 전망을 사용합니다.

다음 그림은 폐루프 예측을 사용하여 값을 전망한 시퀀스 예를 보여줍니다.

closedloop.png

이 예제는 파형 데이터 세트를 사용합니다. 이 데이터 세트는 3개 채널로 이루어진 다양한 길이로 생성된 2000개의 합성 파형 데이터를 포함합니다. 이 예제에서는 폐루프 전망과 개루프 전망을 모두 사용하여 이전 시간 스텝에서 주어진 값으로 파형의 미래 값을 전망하는 LSTM 신경망을 훈련시킵니다.

데이터 불러오기

WaveformData.mat에서 예제 데이터를 불러옵니다. 이 데이터는 시퀀스로 구성된 numObservations×1 셀형 배열이며, 여기서 numObservations는 시퀀스 개수입니다. 각 시퀀스는 numChannels×-numTimeSteps 숫자형 배열이며, 여기서 numChannels는 시퀀스의 채널 개수이고 numTimeSteps는 시퀀스의 시간 스텝 개수입니다.

load WaveformData

처음 몇 개 시퀀스의 크기를 확인합니다.

data(1:5)
ans=5×1 cell array
    {3×103 double}
    {3×136 double}
    {3×140 double}
    {3×124 double}
    {3×127 double}

채널 개수를 확인합니다. LSTM 신경망을 훈련시키려면 각 시퀀스의 채널 개수가 같아야 합니다.

numChannels = size(data{1},1)
numChannels = 3

처음 몇 개의 시퀀스를 플롯으로 시각화합니다.

figure
tiledlayout(2,2)
for i = 1:4
    nexttile
    stackedplot(data{i}')

    xlabel("Time Step")
end

데이터를 훈련 세트와 테스트 세트로 분할합니다. 관측값의 90%는 훈련용으로 사용하고 나머지는 테스트용으로 사용합니다.

numObservations = numel(data);
idxTrain = 1:floor(0.9*numObservations);
idxTest = floor(0.9*numObservations)+1:numObservations;
dataTrain = data(idxTrain);
dataTest = data(idxTest);

훈련을 위해 데이터 준비하기

시퀀스의 미래 시간 스텝 값을 전망하려면 목표값을 시간 스텝 하나만큼 값이 이동된 훈련 시퀀스로 지정하십시오. 즉, LSTM 신경망은 입력 시퀀스의 시간 스텝마다 다음 시간 스텝의 값을 예측하도록 학습합니다. 예측 변수는 최종 시간 스텝이 없는 훈련 시퀀스입니다.

for n = 1:numel(dataTrain)
    X = dataTrain{n};
    XTrain{n} = X(:,1:end-1);
    TTrain{n} = X(:,2:end);
end

더 적합한 피팅을 위해, 그리고 훈련의 발산을 방지하기 위해 평균 0, 분산 1이 되도록 예측 변수와 목표값을 정규화합니다. 예측을 수행할 때는 훈련 데이터와 동일한 통계량을 사용하여 테스트 데이터도 정규화해야 합니다. 모든 시퀀스에 대해 평균과 표준편차를 쉽게 계산하려면 시간 차원으로 시퀀스를 결합하십시오.

muX = mean(cat(2,XTrain{:}),2);
sigmaX = std(cat(2,XTrain{:}),0,2);

muT = mean(cat(2,TTrain{:}),2);
sigmaT = std(cat(2,TTrain{:}),0,2);

for n = 1:numel(XTrain)
    XTrain{n} = (XTrain{n} - muX) ./ sigmaX;
    TTrain{n} = (TTrain{n} - muT) ./ sigmaT;
end

LSTM 신경망 아키텍처 정의하기

LSTM 회귀 신경망을 만듭니다.

  • 입력 데이터의 채널 개수와 일치하는 입력 크기를 가진 시퀀스 입력 계층을 사용합니다.

  • 128개의 은닉 유닛을 가진 LSTM 계층을 사용합니다. 은닉 유닛의 개수는 계층에서 학습한 정보의 양을 결정합니다. 더 많은 은닉 유닛을 사용하면 더 정확한 결과를 얻을 수 있지만 훈련 데이터에 과적합을 초래할 가능성이 더 커집니다.

  • 입력 데이터와 같은 채널 개수를 가진 시퀀스를 출력하려면 입력 데이터의 채널 개수와 일치하는 출력 크기를 가진 완전 연결 계층을 포함하십시오.

  • 마지막으로 회귀 계층을 포함합니다.

layers = [
    sequenceInputLayer(numChannels)
    lstmLayer(128)
    fullyConnectedLayer(numChannels)
    regressionLayer];

훈련 옵션 지정하기

훈련 옵션을 지정합니다.

  • Adam 최적화를 사용하여 훈련시킵니다.

  • 훈련을 Epoch 200회 수행합니다. 크기가 큰 데이터 세트의 경우에는 양호한 피팅을 위해 이렇게 많은 Epoch 횟수만큼 훈련시키지 않아도 될 수 있습니다.

  • 각 미니 배치에서 시퀀스들이 같은 길이를 갖도록 왼쪽을 채웁니다. 왼쪽을 채우면 RNN이 시퀀스의 끝에서 채우기 값을 예측하는 것을 방지합니다.

  • 매 Epoch마다 데이터를 섞습니다.

  • 훈련 진행 상황을 플롯으로 표시합니다.

  • 상세 출력값을 비활성화합니다.

options = trainingOptions("adam", ...
    MaxEpochs=200, ...
    SequencePaddingDirection="left", ...
    Shuffle="every-epoch", ...
    Plots="training-progress", ...
    Verbose=0);

순환 신경망 훈련시키기

trainNetwork 함수를 사용하여 지정된 훈련 옵션으로 LSTM 신경망을 훈련시킵니다.

net = trainNetwork(XTrain,TTrain,layers,options);

순환 신경망 테스트하기

훈련 데이터와 동일한 단계를 사용하여 예측을 위한 테스트 데이터를 준비합니다.

훈련 데이터에서 계산된 통계량을 사용하여 테스트 데이터를 정규화합니다. 목표값을 시간 스텝 하나만큼 값이 이동된 테스트 시퀀스로 지정하고 예측 변수를 최종 시간 스텝이 없는 테스트 시퀀스로 지정합니다.

for n = 1:size(dataTest,1)
    X = dataTest{n};
    XTest{n} = (X(:,1:end-1) - muX) ./ sigmaX;
    TTest{n} = (X(:,2:end) - muT) ./ sigmaT;
end

테스트 데이터를 사용하여 예측을 수행합니다. 훈련에 사용된 것과 동일하게 채우기 옵션을 지정합니다.

YTest = predict(net,XTest,SequencePaddingDirection="left");

정확도를 평가하려면 테스트 시퀀스마다 예측값과 목표값 사이의 평균 제곱 오차의 제곱근(RMSE)을 계산합니다.

for i = 1:size(YTest,1)
    rmse(i) = sqrt(mean((YTest{i} - TTest{i}).^2,"all"));
end

오차를 히스토그램으로 시각화합니다. 값이 낮을수록 높은 정확도를 나타냅니다.

figure
histogram(rmse)
xlabel("RMSE")
ylabel("Frequency")

모든 테스트 관측값에 대해 평균 RMSE를 계산합니다.

mean(rmse)
ans = single
    0.5080

미래의 시간 스텝 전망하기

입력 시계열 또는 시퀀스에 대해 미래의 여러 시간 스텝의 값을 전망하려면 predictAndUpdateState 함수를 사용하여 한 번에 하나의 시간 스텝을 예측한 다음 각 예측에 대해 RNN 상태를 업데이트하십시오. 각 예측에서 직전의 예측을 함수의 입력값으로 사용합니다.

테스트 시퀀스 하나를 플롯으로 시각화합니다.

idx = 2;
X = XTest{idx};
T = TTest{idx};

figure
stackedplot(X',DisplayLabels="Channel " + (1:numChannels))
xlabel("Time Step")
title("Test Observation " + idx)

개루프 전망

개루프 전망은 입력 데이터만 사용하여 시퀀스에서 다음 시간 스텝을 예측합니다. 후속 시간 스텝을 예측할 때는 데이터 소스에서 실제 값을 수집하여 입력값으로 사용합니다. 예를 들어, 시퀀스의 1부터 t-1까지의 시간 스텝에서 수집한 데이터를 사용하여 시간 스텝 t에서의 값을 예측한다고 해보겠습니다. 시간 스텝 t+1에서 예측을 수행하려면 시간 스텝 t에서의 실제 값이 기록될 때까지 기다렸다가 이 값을 입력값으로 사용하여 다음 예측을 수행합니다. RNN에 실제 값을 전달하여 다음 예측을 수행할 수 있다면 개루프 전망을 사용하십시오.

resetState 함수로 먼저 상태를 재설정하여 RNN 상태를 초기화한 다음 입력 데이터의 처음 몇 개의 시간 스텝을 사용하여 초기 예측을 수행합니다. 입력 데이터의 처음 75개의 시간 스텝을 사용하여 RNN 상태를 업데이트합니다.

net = resetState(net);
offset = 75;
[net,~] = predictAndUpdateState(net,X(:,1:offset));

예측을 더 전망하려면 시간 스텝을 루프를 사용해 순회하고 predictAndUpdateState 함수를 사용하여 RNN 상태를 업데이트합니다. 루프를 사용해 입력 데이터의 시간 스텝을 순회하여 테스트 관측값의 나머지 시간 스텝에 대해 값을 전망하고 이 값들을 RNN의 입력값으로 사용합니다. 첫 번째 예측은 시간 스텝 offset + 1에 대응하는 값입니다.

numTimeSteps = size(X,2);
numPredictionTimeSteps = numTimeSteps - offset;
Y = zeros(numChannels,numPredictionTimeSteps);

for t = 1:numPredictionTimeSteps
    Xt = X(:,offset+t);
    [net,Y(:,t)] = predictAndUpdateState(net,Xt);
end

예측값과 목표값을 비교합니다.

figure
t = tiledlayout(numChannels,1);
title(t,"Open Loop Forecasting")

for i = 1:numChannels
    nexttile
    plot(T(i,:))
    hold on
    plot(offset:numTimeSteps,[T(i,offset) Y(i,:)],'--')
    ylabel("Channel " + i)
end

xlabel("Time Step")
nexttile(1)
legend(["Input" "Forecasted"])

폐루프 전망

폐루프 전망은 이전 예측을 입력값으로 사용하여 시퀀스의 후속 시간 스텝을 예측합니다. 이 경우 모델은 예측을 수행하는 데 실제 값이 필요하지 않습니다. 예를 들어, 시퀀스의 1부터 t-1까지의 시간 스텝에서 수집된 데이터만 사용해서 t부터 t+k까지의 시간 스텝의 값을 예측한다고 해보겠습니다. 시간 스텝 i에 대해 예측을 수행하려면 시간 스텝 i-1에서 예측된 값을 입력값으로 사용합니다. 여러 개의 후속 시간 스텝을 전망해야 하거나 다음 예측을 수행하기 전에 RNN에 전달할 수 있는 실제 값이 없다면 폐루프 전망을 사용합니다.

resetState 함수로 먼저 상태를 재설정하여 RNN 상태를 초기화한 다음 입력 데이터의 처음 몇 개의 시간 스텝을 사용하여 초기 예측 Z를 수행합니다. 입력 데이터의 모든 시간 스텝을 사용하여 RNN 상태를 업데이트합니다.

net = resetState(net);
offset = size(X,2);
[net,Z] = predictAndUpdateState(net,X);

예측을 더 전망하려면 시간 스텝을 루프를 사용해 순회하고 predictAndUpdateState 함수를 사용하여 RNN 상태를 업데이트합니다. 이전에 예측된 값을 RNN으로 반복적으로 전달하여 다음 200개의 시간 스텝을 전망합니다. RNN이 더 예측을 수행하기 위해 입력 데이터가 필요하지 않으므로 전망할 시간 스텝의 개수를 원하는 대로 지정할 수 있습니다.

numPredictionTimeSteps = 200;
Xt = Z(:,end);
Y = zeros(numChannels,numPredictionTimeSteps);

for t = 1:numPredictionTimeSteps
    [net,Y(:,t)] = predictAndUpdateState(net,Xt);
    Xt = Y(:,t);
end

전망한 값을 플롯으로 시각화합니다.

numTimeSteps = offset + numPredictionTimeSteps;

figure
t = tiledlayout(numChannels,1);
title(t,"Closed Loop Forecasting")

for i = 1:numChannels
    nexttile
    plot(T(i,1:offset))
    hold on
    plot(offset:numTimeSteps,[T(i,offset) Y(i,:)],'--')
    ylabel("Channel " + i)
end

xlabel("Time Step")
nexttile(1)
legend(["Input" "Forecasted"])

폐루프 전망은 임의의 시간 스텝 개수를 전망할 수 있지만 전망이 진행되는 동안 RNN이 실제 값을 얻을 수 없기 때문에 개루프 전망보다 정확도가 낮을 수 있습니다.

참고 항목

| | |

관련 항목