[시계열] 케라스에서 Luong 어텐션을 활용한 seq2seq2 LSTM 모델 만들기 (번역)

2020. 12. 16. 14:01노트/Python : 프로그래밍

원문 

Building Seq2Seq LSTM with Luong Attention in Keras for Time Series Forecasting | by Huangwei Wieniawska | Level Up Coding (gitconnected.com)

 

Building Seq2Seq LSTM with Luong Attention in Keras for Time Series Forecasting

Do you want to try some other methods to solve your forecasting problem rather than traditional regression? There are many neural network…

levelup.gitconnected.com

 

시계열 예측을 위해 Luong 어텐션을 활용한 seq2seq2 LSTM 모델 만들기

 

 

전통적인 회귀 방법 보다 다른 방법을 사용해 예측 문제를 해결하고 싶나요? 여기에 특히 NLP 분야에 빈번히 적용되는 신많은 신경망 구조들이 시계열 데이터에도 역시 적용될 수 있습니다. 이번 글에서는, 케라스에서 단순한 Seq2Seq LSTM 모델과 Loung 어텐션을 활용한 Seq2Seq LSTM 이라는 두가지 Seq2Seq2 모델을 만들고 예측 정확도를 비교해볼 것입니다.

 

import random
import numpy as np
import matplotlib.pyplot as plt

import pickle as pkl
import keras
from keras.models import Sequential, Model, load_model
from keras.layers import LSTM, Dense, RepeatVector, TimeDistributed, Input, BatchNormalization, multiply, concatenate, Flatten, Activation, dot  
from keras.optimizers import Adam
from keras.utils import plot_model
from keras.callbacks import EarlyStopping
import pydot as pyd
from keras.utils.vis_utils import plot_model, model_to_dot

keras.utils.vis_utils.pydot = pyd

 

데이터 생성하기

첫번째로, 몇개의 시계열 데이터를 만들어봅시다. 

 

n_ = 1000
t = np.linspace(0, 50*np.pi,n_) # 임의 시간 추출 (1차원 배열 만들기)

# pattern + trend + noise 
x1 = sum([20*np.sin(i*t+np.pi) for i in range(5)]) + 0.01*(t**2) + np.random.normal(0,6,n_)
x2 = sum([15*np.sin(2*i*t+np.pi) for i in range(5)]) + 0.5*t + np.random.normal(0,6,n_)

plt.figure(figsize=(15,4))
plt.plot(range(len(x1)), x1 , label='x1')
plt.plot(range(len(x2)), x2 , label='x2')
plt.legend(loc='upper center', bbox_to_anchor=(0.5,-0.15), fancybox=True, shadow=False, ncol=2)
plt.show()

우리는 sin wave, trend, random noise를 결합하여 두 시퀀스, $x1$ $x2$를 만들었다.  다음은 $x1$  $x2$를 전처리 할 것이다. 

 

전처리 

1. 80% 트레인 셋과 20% 테스트 셋으로 시퀀스를 나누기 

train_ratio = 0.8
train_len = int(train_ratio * t.shape[0])
print(train_len)

>>> 800

시퀀스 길이가 n_1000 이기 때문에, 첫번째 800개의 데이터 점들이 트레이닝 데이터로 사용될 것이고, 나머지는 테스트 데이터로 사용될 것이다. 

 

 

2. 트랜드제거 (Detrending)

시계열 데이터를 반드시 트렌드 제거해야되는 것은 아니다. 하지만 안정적인(Stationary) 시계열 데이터가 

모델을 학습하기 더욱 쉽게 만들 것이다. 시차 1만큼의 시퀀스의 차이를 미분하는 것과 같은 시계열의 트렌드를 제거하는 많은 방법이 있다. 여기에서는, 단순하게, 트렌드의 차수가 알려져 있다고 가정하고, 분리된 트렌드 선을 $x1$  $x2$ 에 단순히 적합 시킬 것이다, 그리고 나서 기존의 시퀀스에 상응하는 트렌드 요소를 추출하겠다. 

 

우리는 쉽게 트렌드를 제거했다가 다시 트렌드를 되돌리기 위해서 , 시퀀스의 각 위치에 index를 만들 것이다.  

 

x_index = np.array(range(len(t)))

여기에서는 이 단순한 업무를 완수하기 위해 np.polyfit을 사용할 것이다. 오직 첫번째 800개의 데이터 지점들만 트렌드 선에 적합하기위해 사용된다는 사실을 기억하자. 이는 데이터 부족을 피하고싶기 때문이다. 

# np.polyfit : 주어진 데이터에 대해 최소 제곱을 갖는 다항식 피팅
x1_trend_param = np.polyfit(x_index[:train_len], x1[:train_len],2)
x2_trend_param = np.polyfit(x_index[:train_len], x2[:train_len],1)
print(x1_trend_param)
print(x2_trend_param)

>> [ 2.50838362e-04  3.26363556e-03 -2.09032729e+00]
   [ 0.08045451 -0.82161352]

 

우리가 얻은 위의 값들에 근거해서, $x1$ $x2$ 에 트렌드 선을 다를 수 있게 되었습니다. 

 

x1_trend = (x_index**2)*x1_trend_param[0] + x_index*x1_trend_param[1] + x1_trend_param[2]
x2_trend = x_index*x2_trend_param[0] + x2_trend_param[1]

 

 $x1$ $x2$ 와 함께 트렌드 선을 그려보고 잘 나오는지 확인해봅시다. 

 

plt.figure(figsize=(15,4))
plt.plot(range(len(x1)), x1 , label='x1')
plt.plot(range(len(x1_trend)), x1_trend , linestyle="--" , label='x1_trend')
plt.plot(range(len(x2)), x2 , label='x2')
plt.plot(range(len(x2_trend)), x2_trend , linestyle="--", label='x2_trend')
plt.legend(loc='upper center', bbox_to_anchor=(0.5,-0.15), fancybox=True, shadow=False, ncol=2)
plt.show()

 

위 결과는 잘 나온 것으로 보입니다, 이제 트렌드 요소를 제거해봅시다. 

 

x1_detrend = x1 - x1_trend
x2_detrend = x2 - x2_trend 
plt.figure(figsize=(15,4))
plt.plot(range(len(x1_detrend)), x1_detrend, label="x1_detrend")
plt.plot(range(len(x2_detrend)), x2_detrend, label="x2_detrend")
plt.legend(loc='upper center',bbox_to_anchor=(0.5,-0.15), fancybox=True,shadow=False, ncol=2)
plt.show()

 

트렌드를 제거하고 나니, x1과 x2는 안정적(stationary)으로 되었습니다.

 

3. 시퀀스 결합 

다음 몇가지 스텝에서 쉬운 전처리를 위해, 시퀀스와 그들의 관련 정보를 하나의 배열(array)로 결합할 수 있다. 

 

x_lbl = np.column_stack([x1_detrend,x2_detrend,x_index, [1]*train_len+ [0]*(len(x_index)-train_len)])
print(x_lbl.shape)
print(x_lbl)

결합된 배열 (array)에서 우리는 x_lbl를 만들 수 있다:

  • 첫번째 열은 트렌드가 제거된 $x1$ 이다. 
  • 두번째 열은 트렌드가 제거된 $x2$ 이다.
  • 세번째 열은 index 이다 
  • 네번째 열은 label 이다. ( 1은 트레이닝 셋이고 0은 테스트셋이다.)

 

4. 정규화 (Normalize)

정규화는 모델이 매우 작은 feature들을 무시하면서 큰 feature들을 선호하는 것을 피하도록 도와준다. 여기서 트레이닝 셋에 상응하는 최대값을 나눠서 $x1$$x2$를 정규화할 수있다. 

 

x_train_max = x_lbl[x_lbl[:,3]==1, :2].max(axis=0)
x_train_max = x_train_max.tolist() + [1]*2 # only normalize for the first 2 columns 
print(x_train_max)

위 코드가 오직 열1 (detrended $x1$)과 열2 (detrended $x2$)의 최댓값만 계산한다는 것을 주목하자. 열 3 (index) 과 열 4 (label)의 분모는 1로 설정한다.  이는 우리가 열 3과 열 4를 신경망 네트워크에 입력하지 않기에 그들을 정규화할 필요가 없기 때문이다. 

 

x_normalize = np.divide(x_lbl, x_train_max)
print(x_normalize)

plt.figure(figsize=(15,4))
plt.plot(range(train_len), x_normalize[:train_len,0], label="x1_train_normalized")
plt.plot(range(train_len), x_normalize[:train_len,1], label="x2_train_normalized")
plt.plot(range(train_len, len(x_normalize)), x_normalize[train_len:,0], label="x1_test_normalized")
plt.plot(range(train_len, len(x_normalize)), x_normalize[train_len:,1], label="x2_test_normalized")
plt.legend(loc="upper center", bbox_to_anchor=(0.5,-0.15), fancybox=True, shadow=False, ncol=2)
plt.show()

정상화 한 이후, 모든 값들은 -1과 1 사이 범위 내로 다소 크거나 작았다.

 

5. 절단 (Truncate)

그 다음은, 우리는 input window ( length = 200 타임 스텝) output window ( length = 20 타임 스텝)을 슬라이딩 시키면서 시퀀스를 더 작은 조각으로 자를 것이다.  그리고 이 샘플을 3차원 넘파이 배열로 둘 것이다. 

def truncate(x, feature_cols = range(3), target_cols = range(3), 
             label_col = 3, train_len = 100, test_len = 20): 
    in_, out_, lbl = [], [], []
    for i in range(len(x) - train_len - test_len +1):
        in_.append(x[i:(i+train_len), feature_cols].tolist())
        out_.append(x[(i+train_len):(i+train_len+test_len),target_cols].tolist())
        lbl.append(x[i+train_len, label_col])
    return np.array(in_), np.array(out_), np.array(lbl)

X_in , X_out , lbl = truncate(x_normalize, feature_cols=range(3), target_cols = range(3),
                              label_col=3, train_len =200, test_len =20)
                              
print(X_in.shape, X_out.shape, lbl.shape)
>>> (781, 200, 3) (781, 20, 3) (781,)

 

truncate 함수는 3가지 배열을 만들어냅니다 : 

  • 신경망에 대한 입력값 X_in : 781개 샘플을 포함하고 있고, 각 샘플의 길이는 200 타임 스텝이며, 각 샘플은 3개지 feature를 가지고 있다 : detrended and normalized x1 , detrended and normalised x2 , original assigned data position index . 오직 첫번째 2가지 feature들만이 트레이닝에 사용될 것이다. 
  • 신경망에 대한 타겟 값 X_out : 781개 샘플을 포함하고 있고, 각 샘플의 길이는 20 타임 스텝이며, 각 샘플은 X_in과 동일한 3가지 feature를 가지고 있다. 오직 첫번째 2가지 feature들 만이 타겟에 사용될 것이며, 세번째 feature는 예측값의 트렌드를 회복시킬 때 사용될 것이다. 
  • 라벨 lbl : 트레이닝 셋에는 1이고, 테스트 셋에는 0이다. 
X_input_train = X_in[np.where(lbl==1)]
X_output_train = X_out[np.where(lbl==1)]
X_input_test = X_in[np.where(lbl==0)]
X_output_test = X_out[np.where(lbl==0)]
print(X_input_train.shape, X_output_train.shape)
print(X_input_test.shape, X_output_test.shape)
>>> (600, 200, 3) (600, 20, 3)
    (181, 200, 3) (181, 20, 3)

이제 신경망에 입력될 데이터가 준비됐다! 

 

모델2 : 단순 Seq2Seq LSTM 모델 

위 그림은 Seq2Seq LSTM 모델의 한 레이어를 펼친것을 나타냅니다. 

  • The encoder LSTM cell : 각 타임 스텝의 값으로 encoder LSTM cell에 이전의 cell state c 와 hidden state h 를 함께 넣고, 마지막 cell state c 와 hidden state h 가 생성될 때까지 과정을 반복합니다. 
  • The decoder LSTM cell : encoder로 부터 마지막 cell state c 와 hidden state h 를 decoder LSTM cell의 초기값으로 사용합니다. 마지막 인코더의 hidden state는 또한 20번 복사되며, 각 복사마다 이전의 cell state c 와 hidden state h 를 함께 decoder LSTM cell에 넣습니다. decoder는 모든 20번의 타임 스텝에 대한 hidden state를 출력하고, hidden state는 최종 결과를 출력 하기위해 dense layer에 연결됩니다. 

hidden layers의 수를 설정합시다 : 

n_hidden = 100 

입력 층 (The input layer)

input_train = Input(shape=(X_input_train.shape[1], X_input_train.shape[2]-1))
output_train = Input(shape=(X_output_train.shape[1], X_output_train.shape[2]-1))
print(input_train)
print(output_train)

LSTM 인코더 (The encoder LSTM)

우리는 return_sequencesreturn_state 라는 2가지 중요한 파라미터에 집중할 필요가 있습니다. 이들은 LSTM이 리턴하는 것을 결정하기 때문입니다. 

  • return_sequences = False, return_state = False : 마지막 hidden state: state_h 로 리턴합니다. 
  • return_sequences = True, return_state = False : 쌓여진 hidden states (num_timesteps * num_cells)를 리턴합니다 : 하나의 hidden state는 각 입력 타임 스텝 마다 출력합니다. 
  • return_sequences = False, return_state = FTrue : 3개지 배열(arrays)를 리턴합니다 : state_h, state_h, state_c 
  • return_sequences = True, return_state = True : 3개지 배열(arrays)를 리턴합니다 : stacked state_h, last state_h, last state_c 

심플 Seq2Seq 모델에서는 우리는 last state_h 와 last state_c 만 필요합니다. 

encoder_last_h1, encoder_last_h2, encoder_last_c = LSTM(n_hidden,
                                                        activation='relu', 
                                                        dropout=0.2, 
                                                        recurrent_dropout=0.2,
                                                       return_sequences=False,
                                                       return_state=True)(input_train)
print(encoder_last_h1)
print(encoder_last_h2)
print(encoder_last_c)

encder의 RELU 활성화 함수에 의해 유발되는 경사 폭팔 (gradient explosion)을 피하기를 원하기 때문에 Batch normalisation 이 더해집니다. 

encder_last_h1 = BatchNormalization(momentum=0.6)(encoder_last_h1)
encder_last_c = BatchNormalization(momentum=0.6)(encoder_last_c)

다음으로, encoder의 마지막 hidden state의 20개 복사본을 만들고, decoder의 입력값으로 사용합니다.  encoder의 last cell state와 last hidden state도 또한 decoder의 초기 상태 (initial state)로 사용됩니다. 

 

decoder = RepeatVector(output_train.shape[1])(encoder_last_h1)
decoder = LSTM(n_hidden,
               activation='relu', 
               dropout=0.2, 
               recurrent_dropout=0.2,
               return_sequences=True,
               return_state=False)(decoder, initial_state=[encoder_last_h1, encoder_last_c])
print(decoder)

out= TimeDistributed(Dense(output_train.shape[2]))(decoder)
print(out)

그리고 나서 모든 것을 모델에 넣고, comile 합니다. 여기에서는 우리는 단순히 loss 함수로 MSE를 사용하고, 평가 척도(evaluation metric) 로는 MAE를 사용하겠습니다. Adam optimiser에서 clipnorm = 1 설정했다는 것에 주목하세요. 역전파 중에 경사 폭팔을 피하기 위해 기울기를 정규화하는 것입니다. 

model = Model(inputs = input_train, outputs = out)
opt = Adam(lr = 0.01, clipnorm = 1)
model.compile(loss='mean_squared_error', optimizer = opt, metrics = ['mae'])
model.summary()

오리는 또한 모델을 그려볼 수 있습니다 : 

# !pip install pydot
# !pip install graphviz
plot_model(model, to_file='model_plot.png', show_shapes=True, show_layer_names=True)

다음 단계는 학습하는 것입니다 : 

epc = 100
es = EarlyStopping(monitor='val_loss', mode='min', patience = 50)
history = model.fit(X_input_train[:,:,:2], X_output_train[:,:,:2], validation_split=0.2,
                    epochs=epc, verbose =1 , callbacks=[es],
                    batch_size=100)
train_mae = history.history['mae']
valid_mae = history.history['val_mae']
model.save('model_forecasting_seq2seq.h5')

plt.plot(train_mae, label='train mae')
plt.plot(valid_mae, label='validation mae')
plt.ylabel('mae')
plt.xlabel('epoch')
plt.title('train vs. validation accuracy (mae)')
plt.legend(loc = 'upper center', bbox_to_anchor=(0.5,-0.15), fancybox=True, shadow=False, ncol=2)
plt.show()

예측 (Prediction) 

실제 값 뿐만 아니라 모델 예측값은 정규화되지 않습니다: 

train_pred_detrend = model.predict(X_input_train[:,:,:2])*x_train_max[:2]
test_pred_detrend = model.predict(X_input_test[:,:,:2])*x_train_max[:2]
print(train_pred_detrend.shape, test_pred_detrend.shape)

train_true_detrend = X_output_train[:,:,:2]*x_train_max[:2]
test_true_detrend = X_output_test[:,:,:2]*x_train_max[:2]
print(train_true_detrend.shape, test_true_detrend.shape)

그리고 나서, 정규화 되지 않은 출력 값을 그들에 상응하는 index와 결합합니다. 그래야 트렌드를 회복할 수 있기 때문입니다. 

train_pred_detrend = np.concatenate([train_pred_detrend, 
                                     np.expand_dims(X_output_train[:,:,2], axis=2)], axis=2)
test_pred_detrend = np.concatenate([test_pred_detrend, 
                                     np.expand_dims(X_output_test[:,:,2], axis=2)], axis=2)
print(train_pred_detrend.shape, test_pred_detrend.shape)

train_true_detrend = np.concatenate([train_true_detrend, 
                                     np.expand_dims(X_output_train[:,:,2], axis=2)], axis=2)
test_true_detrend = np.concatenate([test_true_detrend, 
                                     np.expand_dims(X_output_test[:,:,2], axis=2)], axis=2)
print(train_true_detrend.shape, test_true_detrend.shape)

다음으로는 , 복원된 트렌드를 가진 모든 출력 값을 dictionary에 넣습니다 

data_final.

data_final = dict()
for dt, lb in zip([train_pred_detrend, train_true_detrend, test_pred_detrend, test_true_detrend],
                 ['train_pred','train_true','test_pred','test_true']):
    dt_x1 = dt[:,:,0] + (dt[:,:,2]**2)*x1_trend_param[0] + dt[:,:,2]*x1_trend_param[1] + x1_trend_param[2]
    dt_x2 = dt[:,:,1] + dt[:,:,2]*x2_trend_param[0] + x2_trend_param[1]
    data_final[lb] = np.concatenate([
        np.expand_dims(dt_x1, axis=2), np.expand_dims(dt_x2, axis=2)
    ], axis=2)
    print(lb+": {}".format(data_final[lb].shape))

예측 값 분포가 합리적인지 빠르게 체크해봅시다 : 

 

for lb in ['train', 'test']:
    plt.figure(figsize=(15,4))
    plt.hist(data_final[lb+'_pred'].flatten(), bins=100, color ='orange', alpha=0.5, label = lb+' pred')
    plt.hist(data_final[lb+'_true'].flatten(), bins=100, color ='green', alpha=0.5, label = lb+' true')
    plt.legend()
    plt.title('value distribution: '+lb)
    plt.show()

예측값의 데이터 분포와 진짜 값이 거의 겹쳐지는 것을 보아 괜찮게 한 것 같습니다. 

 

또한 명백한 패턴이 있는지 확인하기 위해서 , 모든 표본의 MAE를 시간 순서대로 그려볼 수 있습니다. 이상적인 상황은 선이 랜덤할 때이며, 그렇지 않으면 모형이 충분히 훈련되지 않았음을 나타낼 수 있습니다. 

for lb in ['train','test']:
    MAE_overall = abs(data_final[lb+ '_pred'] - data_final[lb+ '_true']).mean()
    MAE_ =  abs(data_final[lb+ '_pred'] - data_final[lb+ '_true']).mean(axis=(1,2))
    plt.figure(figsize=(15,3))
    plt.plot(MAE_)
    plt.title("MAE "+lb+": overall MAE = "+str(MAE_overall))
    plt.show()

위 그림을 보아, 트레이닝 테스트 MAE 둘다 여전히 명백한 주기적인 패턴이 있다고 말할 수 있습니다. 더 많은 에폭을 학습하면 더 나은 결과를 가져올 수 있을 것입니다. 

 

다음은 랜덤 샘플을 확인하고 예측선과 진짜 선이 일치하는지 확인해볼 것입니다. 

 

또한 각 타임 스텝에 대해 n번째 예측값을 확인해볼 수 있습니다. 

ith_timestep = random.choice(range(data_final[lb+'_pred'].shape[1]))
plt.figure(figsize=(15,5))
train_start_t = 0 
test_start_t = data_final['train_pred'].shape[0]
for lb, tm, clrs in zip(['train','test'], [train_start_t, test_start_t],[['green','red'],['blue','orange']]):
    for i, x_lbl in zip([0,1], ['x1','x2']):
        plt.plot(range(tm, tm+data_final[lb+'_pred'].shape[0]),
                data_final[lb+'_pred'][:, ith_timestep, i],
                linestyle="--", linewidth = 1, color = clrs[0], label='pred '+x_lbl)
        plt.plot(range(tm, tm+data_final[lb+'_true'].shape[0]),
                data_final[lb+'_true'][:, ith_timestep, i],
                linestyle="--", linewidth = 1, color = clrs[1], label='true '+x_lbl)
plt.title('{}th time step in all samples'.format(ith_timestep))
plt.legend(loc='upper center', bbox_to_anchor=(0.5,-0.15), fancybox=True, shadow=False, ncol=8)
plt.show()

테스트 셋에 대해 예측값을 좀 더 가까이서 봅시다 

lb = 'test'
plt.figure(figsize=(15,5))
for i, x_lbl, clr in zip([0,1], ['x1', 'x2'],['green','blue']):
    plt.plot(data_final[lb+'_pred'][:, ith_timestep, i], linestyle='--',
            color=clr, label='pred '+x_lbl)
    plt.plot(data_final[lb+'_true'][:, ith_timestep, i], linestyle='-',
            color=clr, label='true '+x_lbl)
    
plt.title('({}): {}th time step in all samples'.format(lb, ith_timestep))
plt.legend(loc = 'upper center', bbox_to_anchor=(0.5,-0.15), fancybox=True, shadow=False, ncol=2)
plt.show()

 

모델2 : Luong Attention을 활용한 Seq2Seq LSTM 모델 

https://blog.floydhub.com/attention-mechanism/

 

단순한 Seq2Seq 모델의 한계 중 하나는 : 오직 인코더 RNN의 마지막 상태만 decoder RNN의 입력으로 사용된다는 것입니다. 만약 시퀀스가 매우 길다면, 인코더는 초기의 타임스텝에 대해 약한 기억을 가지게 됩니다. 어텐션 메커니즘은 이 문제를 해결할 수 있습니다. 한 어텐션 층이 인코더로부터의 은닉층 아웃풋에 적절한 가중치를 할당할 것이며, 아웃풋 시퀀스에 그들을 매핑할 것입니다. 

 

다음으로는 위의 Model 1Luong Attention을 구성해봅시다. 그리고 Dot 방식을 사용하여 정렬 점수를 계산해봅시다. 

 

입력층 (The Input layer) 

Model 1 과 같습니다. : 

n_hidden = 100
input_train = Input(shape=(X_input_train.shape[1], X_input_train.shape[2]-1))
output_train = Input(shape=(X_output_train.shape[1], X_output_train.shape[2]-1))

 

LSTM 인코더 (The encoder LSTM) 

이부분은 다소 Model 1 과는 다릅니다 : 마지막 은닉 상태(hidden state)와 마지막 셀 상태(cell state)를 리턴하는 것이외에도, 정렬 점수 (alignment score) 계산을 위한 누적 은닉 상태(stacked hidden state)를 리턴할 필요가 있습니다. 

 

encoder_stack_h, encoder_last_h, encoder_last_c = LSTM(
    n_hidden, activation='relu', 
    dropout=0.2, recurrent_dropout = 0.2,
    return_state = True, return_sequences=True)(input_train)

print(encoder_stack_h)
print(encoder_last_h)
print(encoder_last_c)

다음으로, 경사 폭발을 피하기 위해 배치정규화를 적용합시다. 

encoder_last_h = BatchNormalization(momentum=0.6)(encoder_last_h)
encoder_last_c = BatchNormalization(momentum=0.6)(encoder_last_c)

 

LSTM 디코더 (The Decoder LSTM) 

다음으로, 인코더의 마지막 은닉 상태를 20번 반복하고, LSTM 디코더 그것들을 input으로 사용합시다. 

 

decoder_input = RepeatVector(output_train.shape[1])(encoder_last_h)
print(decoder_input)

또한, 정렬 점수 (alignment score) 계산을 위한 디코더의 누적 히든 상태도 얻을 필요가 있습니다. 

decoder_stack_h = LSTM(n_hidden, activation = 'relu',
                       dropout=0.2, 
                       recurrent_dropout=0.2,
                       return_state =False , 
                       return_sequences = True)(decoder_input, initial_state=[encoder_last_h, encoder_last_c])
print(decoder_stack_h)

 

어텐션 층 (Attention Layer) 

어텐션 층을 구성하기 위해, 첫번째로 할일은 정렬 점수를 계산하는 것입니다. 그리고 그 위에다 소프트맥스 활성화 함수를 적용합니다. 

attention = dot([decoder_stack_h, encoder_stack_h], axes = [2,2])
attention = Activation('softmax')(attention)
print(attention)

그리고나서, 문맥 벡터(context vector)를 계산할 수 있습니다. 또한 그 위에다 배치정규화를 적용합니다. 

context = dot([attention, encoder_stack_h], axes = [2,1])
context = BatchNormalization(momentum = 0.6)(context)
print(context)

이제, 문맥 벡터(context vector) 와 디코더의 누적 은닉 상태를 결합합니다. 그리고 마지막 dense layer에 입력값으로 사용합니다. 

decoder_combined_context = concatenate([context, decoder_stack_h])
print(decoder_combined_context)

out = TimeDistributed(Dense(output_train.shape[2]))(decoder_combined_context)
print(out)

이제, 모델을 컴파일 할 수 있습니다. 파라미터는 두 모델의 성능을 비교하기 위해, 파라미터는 Model 1 과 동일합니다. 

model = Model(inputs=input_train, outputs = out)
opt = Adam(lr=0.01, clipnorm=1)
model.compile(loss='mean_squared_error', optimizer = opt, metrics = ['mae'])
model.summary()

모델을 통해 데이터가 어떻게 흘러가는지 볼 수 있습니다 : 

트레이닝과 평가 과정은 Model1에서 설명된 것과 동일합니다. 

100 epoch 학습한 이후 ( Model1 과 동일한 트레이닝 epoch 횟수 입니다 ) 결과를 평가해볼 수 있습니다. 

아래는 sample MAE vs. sample order for train set 과 test set 에 대한 그림입니다. 

다시한번 말하자면, 여전히 주기적인 패턴을 보이기에 모델은 학습되기에 충분하지 않습니다. 하지만 2 모델의 쉬운 비교를 위해서 지금부터 더 학습을 시키진 않겠습니다. 전반적인 두 트레인 셋과 테스트 셋의 MAE가 근소하게 Model 1과 비교하여 향상되었다는 사실을 주목해봅시다. 

 

attention layer를 추가한 이후 : 

  • 트레이닝 셋의 MAE는 5.7851 에서 5.7381 로 감소하였습니다.  (난...증가)
  • 테스트 셋의 MAE는 6.1495 에서 5.9392 로 감소하였습니다.  (난...증가)
train_pred_detrend = model.predict(X_input_train[:,:,:2])*x_train_max[:2]
test_pred_detrend = model.predict(X_input_test[:,:,:2])*x_train_max[:2]
print(train_pred_detrend.shape, test_pred_detrend.shape)

train_true_detrend = X_output_train[:,:,:2]*x_train_max[:2]
test_true_detrend = X_output_test[:,:,:2]*x_train_max[:2]
print(train_true_detrend.shape, test_true_detrend.shape)

>>> (600, 20, 2) (181, 20, 2)
    (600, 20, 2) (181, 20, 2)
train_pred_detrend = np.concatenate([train_pred_detrend, 
                                     np.expand_dims(X_output_train[:,:,2], axis=2)], axis=2)
test_pred_detrend = np.concatenate([test_pred_detrend, 
                                     np.expand_dims(X_output_test[:,:,2], axis=2)], axis=2)
print(train_pred_detrend.shape, test_pred_detrend.shape)

train_true_detrend = np.concatenate([train_true_detrend, 
                                     np.expand_dims(X_output_train[:,:,2], axis=2)], axis=2)
test_true_detrend = np.concatenate([test_true_detrend, 
                                     np.expand_dims(X_output_test[:,:,2], axis=2)], axis=2)
print(train_true_detrend.shape, test_true_detrend.shape)

>>> (600, 20, 3) (181, 20, 3)
    (600, 20, 3) (181, 20, 3)
data_final = dict()
for dt, lb in zip([train_pred_detrend, train_true_detrend, test_pred_detrend, test_true_detrend],
                 ['train_pred','train_true','test_pred','test_true']):
    dt_x1 = dt[:,:,0] + (dt[:,:,2]**2)*x1_trend_param[0] + dt[:,:,2]*x1_trend_param[1] + x1_trend_param[2]
    dt_x2 = dt[:,:,1] + dt[:,:,2]*x2_trend_param[0] + x2_trend_param[1]
    data_final[lb] = np.concatenate([
        np.expand_dims(dt_x1, axis=2), np.expand_dims(dt_x2, axis=2)
    ], axis=2)
    print(lb+": {}".format(data_final[lb].shape))
    
>>> train_pred: (600, 20, 2)
    train_true: (600, 20, 2)
    test_pred: (181, 20, 2)
    test_true: (181, 20, 2)
for lb in ['train', 'test']:
    plt.figure(figsize=(15,4))
    plt.hist(data_final[lb+'_pred'].flatten(), bins=100, color ='orange', alpha=0.5, label = lb+' pred')
    plt.hist(data_final[lb+'_true'].flatten(), bins=100, color ='green', alpha=0.5, label = lb+' true')
    plt.legend()
    plt.title('value distribution: '+lb)
    plt.show()

for lb in ['train','test']:
    MAE_overall = abs(data_final[lb+ '_pred'] - data_final[lb+ '_true']).mean()
    MAE_ =  abs(data_final[lb+ '_pred'] - data_final[lb+ '_true']).mean(axis=(1,2))
    plt.figure(figsize=(15,3))
    plt.plot(MAE_)
    plt.title("MAE "+lb+": overall MAE = "+str(MAE_overall))
    plt.show()

ith_timestep = random.choice(range(data_final[lb+'_pred'].shape[1]))
plt.figure(figsize=(15,5))
train_start_t = 0 
test_start_t = data_final['train_pred'].shape[0]
for lb, tm, clrs in zip(['train','test'], [train_start_t, test_start_t],[['green','red'],['blue','orange']]):
    for i, x_lbl in zip([0,1], ['x1','x2']):
        plt.plot(range(tm, tm+data_final[lb+'_pred'].shape[0]),
                data_final[lb+'_pred'][:, ith_timestep, i],
                linestyle="--", linewidth = 1, color = clrs[0], label='pred '+x_lbl)
        plt.plot(range(tm, tm+data_final[lb+'_true'].shape[0]),
                data_final[lb+'_true'][:, ith_timestep, i],
                linestyle="--", linewidth = 1, color = clrs[1], label='true '+x_lbl)
plt.title('{}th time step in all samples'.format(ith_timestep))
plt.legend(loc='upper center', bbox_to_anchor=(0.5,-0.15), fancybox=True, shadow=False, ncol=8)
plt.show()

lb = 'test'
plt.figure(figsize=(15,5))
for i, x_lbl, clr in zip([0,1], ['x1', 'x2'],['green','blue']):
    plt.plot(data_final[lb+'_pred'][:, ith_timestep, i], linestyle='--',
            color=clr, label='pred '+x_lbl)
    plt.plot(data_final[lb+'_true'][:, ith_timestep, i], linestyle='-',
            color=clr, label='true '+x_lbl)
    
plt.title('({}): {}th time step in all samples'.format(lb, ith_timestep))
plt.legend(loc = 'upper center', bbox_to_anchor=(0.5,-0.15), fancybox=True, shadow=False, ncol=2)
plt.show()