Keras를 통한 LSTM 구현


/* 2017.8.29 by. 3months */


- LSTM에 대한 개념 학습

http://blog.naver.com/kmkim1222/221069000196

https://www.youtube.com/watch?v=6niqTuYFZLQ&list=PL3FW7Lu3i5JvHM8ljYj-zLfQRF3EO8sYv


- 코드 및 데이터 출처  -https://github.com/Dataweekends/zero_to_deep_learning_udemy


LSTM 개념


LSTM은 기존 vanilla RNN을 개선한 모델로써 데이터의 long-term dependency를 학습하는데 효과적인 모델입니다. 또한 내부적으로 구성요소간에 additive, elementwise interaction을 통해 backpropagation 할 때, gradient explosion, vanishing 문제를 기존 RNN에 비해 매우 줄인 것이 중요한 개선점입니다.


코드 실습을 통해 LSTM을 학습하기 위해서는 어떠한 데이터 구조가 필요한지, 즉, INPUT은 어떤 모양이어야 하고, OUTPUT이 어떻게 생성되며, 네트워크 구조를 코드 상으로 어떻게 정의하는지 알아보겠습니다.


사용할 데이터


cansim-0800020-eng-6674700030567901031.csv


데이터 임포트


데이터를 임포트합니다. 데이터는 날짜별로 특정한 값이 있는 데이터입니다. Unadjusted이 Raw 값이고, Seaonally adjusted는 Season의 효과를 보정한 값입니다. 보통 주식과 같은 시계열 데이터는 Season에 의한 효과가 있는데, Seasonally adjusted는 이 Season에 의한 효과를 보정하여 전체적인 추세만 보고자할 때 사용하는 변수입니다. 우리가 학습할 변수는 Season에 의한 효과를 보정하지 않은 Unadjusted입니다.

import pandas as pd
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt

df = pd.read_csv('data/cansim-0800020-eng-6674700030567901031.csv',
                 skiprows=6, skipfooter=9,
                 engine='python')
df.head()

Adjustments Unadjusted Seasonally adjusted
0 Jan-1991 12588862 15026890
1 Feb-1991 12154321 15304585
2 Mar-1991 14337072 15413591
3 Apr-1991 15108570 15293409
4 May-1991 17225734 15676083



전처리

from pandas.tseries.offsets import MonthEnd df['Adjustments'] = pd.to_datetime(df['Adjustments']) + MonthEnd(1) df = df.set_index('Adjustments') print(df.head()) df.plot()

우선 문자열로된 Adjustments 변수를 datetime 객체로 만들어주고, MonthEnd(1)을 더하면 해당 월의 맨 끝 날짜가 지정되게 됩니다. 그리고 이 날짜를 index로 만들어줍니다. 이후에 plot을 하면 날짜를 index로 해서 Unadjusted와 Seasonally adjusted 변수의 그래프를 그릴 수 있습니다.



Unadjusted Seasonally adjusted
Adjustments

1991-01-31 12588862 15026890
1991-02-28 12154321 15304585
1991-03-31 14337072 15413591
1991-04-30 15108570 15293409
1991-05-31 17225734 15676083


인덱스가 0,1,2,3 ... 에서 날짜로 변했음을 알 수 있습니다. (가장 왼쪽열. 또한 index의 이름이 Adjustements가 되었음을 알 수 있음.)



우리가 학습할 변수는 파란색 선 Unadjusted 입니다.



트레이닝셋 테스트셋 SPLIT


2011/1/1 을 기준으로 트레이닝셋과 테스트셋으로 구분합니다. 그리고 학습과 테스트에 사용할 Unadjusted 변수만 남겨놓습니다.

split_date = pd.Timestamp('01-01-2011')
# 2011/1/1 까지의 데이터를 트레이닝셋.
# 그 이후 데이터를 테스트셋으로 한다.

train = df.loc[:split_date, ['Unadjusted']]
test = df.loc[split_date:, ['Unadjusted']]
# Feature는 Unadjusted 한 개

ax = train.plot()
test.plot(ax=ax)
plt.legend(['train', 'test'])



변수 Scaling


MinMax Scaling을 통해 변수를 Scaling합니다. MinMax Scaling : http://3months.tistory.com/167

그러면 아래와 같이 Scaling 된 것을 볼 수 있고, scaling의 결과는 2차원의 numpy ndarray 타입으로 변환이 되게 됩니다.

from sklearn.preprocessing import MinMaxScaler

sc = MinMaxScaler()

train_sc = sc.fit_transform(train)
test_sc = sc.transform(test)

train_sc
(240, 1)
[[ 0.01402033]
 [ 0.        ]
 [ 0.0704258 ]
 [ 0.09531795]
 [ 0.16362761]
 [ 0.13514108]
 [ 0.12395846]
 [ 0.12617398] 
......


Pandas Dataframe으로 변환


이를 다시 pandas dataframe 데이터 타입으로 변환해봅시다. Dataframe에서 작업을 해야지 Window를 만들기 유용하기 때문입니다. Window는 RNN을 트레이닝 하기위한 단위로 Window size는 Timestep과 같다고 생각하시면 됩니다.

train_sc_df = pd.DataFrame(train_sc, columns=['Scaled'], index=train.index)
test_sc_df = pd.DataFrame(test_sc, columns=['Scaled'], index=test.index)
train_sc_df.head()



Scaled
Adjustments
1991-01-31 0.014020
1991-02-28 0.000000
1991-03-31 0.070426
1991-04-30 0.095318
1991-05-31 0.163628



pandas shift를 통해 Window 만들기


shift는 이전 정보 다음 row에서 다시 쓰기 위한 pandas의 함수입니다. 이를 통해 아래와 같이 과거의 값들을 shift_s 와 같은 형태로 저장할 수 있습니다. 과거값은 총 12개를 저장하며, timestep은 12개가 됩니다. 우리의 목적은 과거값 shift1~12를 통해 현재값 Scaled를 예측하는 것입니다.

for s in range(1, 13):
    train_sc_df['shift_{}'.format(s)] = train_sc_df['Scaled'].shift(s)
    test_sc_df['shift_{}'.format(s)] = test_sc_df['Scaled'].shift(s)

train_sc_df.head(13)



Scaled shift_1 shift_2 shift_3 shift_4 shift_5 shift_6 shift_7 shift_8 shift_9 shift_10 shift_11 shift_12
Adjustments












1991-01-31 0.014020 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1991-02-28 0.000000 0.014020 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1991-03-31 0.070426 0.000000 0.014020 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1991-04-30 0.095318 0.070426 0.000000 0.014020 NaN NaN NaN NaN NaN NaN NaN NaN NaN
1991-05-31 0.163628 0.095318 0.070426 0.000000 0.014020 NaN NaN NaN NaN NaN NaN NaN NaN
1991-06-30 0.135141 0.163628 0.095318 0.070426 0.000000 0.014020 NaN NaN NaN NaN NaN NaN NaN
1991-07-31 0.123958 0.135141 0.163628 0.095318 0.070426 0.000000 0.014020 NaN NaN NaN NaN NaN NaN
1991-08-31 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426 0.000000 0.014020 NaN NaN NaN NaN NaN
1991-09-30 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426 0.000000 0.014020 NaN NaN NaN NaN
1991-10-31 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426 0.000000 0.014020 NaN NaN NaN
1991-11-30 0.131738 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426 0.000000 0.014020 NaN NaN
1991-12-31 0.200913 0.131738 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426 0.000000 0.01402 NaN
1992-01-31 0.030027 0.200913 0.131738 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426 0.00000 0.01402



트레이닝셋과 테스트셋 만들기


dropna를 통해 NaN이 있는 데이터를 제거하고, shift_1 ~ shift_12는 X로 Scaled는 Y로 지정을 합니다.

X_train = train_sc_df.dropna().drop('Scaled', axis=1)
y_train = train_sc_df.dropna()[['Scaled']]

X_test = test_sc_df.dropna().drop('Scaled', axis=1)
y_test = test_sc_df.dropna()[['Scaled']]


최종 트레이닝셋과 테스트셋의 모양은 아래와 같습니다.


X_train.head()



shift_1 shift_2 shift_3 shift_4 shift_5 shift_6 shift_7 shift_8 shift_9 shift_10 shift_11 shift_12
Adjustments











1992-01-31 0.200913 0.131738 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426 0.000000 0.014020
1992-02-29 0.030027 0.200913 0.131738 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426 0.000000
1992-03-31 0.019993 0.030027 0.200913 0.131738 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318 0.070426
1992-04-30 0.065964 0.019993 0.030027 0.200913 0.131738 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628 0.095318
1992-05-31 0.109831 0.065964 0.019993 0.030027 0.200913 0.131738 0.111395 0.092309 0.126174 0.123958 0.135141 0.163628


y_train.head()



Scaled
Adjustments
1992-01-31 0.030027
1992-02-29 0.019993
1992-03-31 0.065964
1992-04-30 0.109831
1992-05-31 0.149130



다시 ndarray로 변환하기


데이터구조를 다시 ndarray로 변환시켜줍니다. 실제 deep learning 모델의 트레이닝과 테스트로 사용되는 데이터는 일반적으로 numpy의 ndarray입니다. 아래와 같이 .values를 통해 ndarray 값을 얻을 수 있습니다. 트레이닝 데이터는 timestep=12, size = 228 입니다.

X_train = X_train.values
X_test= X_test.values

y_train = y_train.values
y_test = y_test.values
print(X_train.shape)
print(X_train)
print(y_train_shape)
print(y_train)
(228, 12)
[[ 0.20091289  0.13173822  0.11139526 ...,  0.0704258   0.          0.01402033]
 [ 0.03002688  0.20091289  0.13173822 ...,  0.09531795  0.0704258   0.        ]
 [ 0.01999285  0.03002688  0.20091289 ...,  0.16362761  0.09531795
   0.0704258 ]
 ..., 
 [ 0.79916654  0.81439355  0.86398323 ...,  0.92972161  0.71629034
   0.77368724]
 [ 0.80210057  0.79916654  0.81439355 ...,  0.59734863  0.92972161
   0.71629034]
 [ 0.81482896  0.80210057  0.79916654 ...,  0.53166512  0.59734863
   0.92972161]]
(228, 1)
[[ 0.03002688]
 [ 0.01999285]
 [ 0.06596369]
 [ 0.10983126]
 [ 0.14912986]
....


최종 트레이닝셋과 테스트셋의 X 만들기


이부분이 중요한데, keras에서는 RNN 계열의 모델을 트레이닝할 대 요구되는 데이터의 형식이 있습니다. 바로 3차원 데이터여야하며 각각의 차원은 (size, timestep, feature) 을 순서대로 나타내주어야하는 것입니다. 따라서 이 형태로 데이터를 reshape 해주어야합니다. 일반적인 MLP 모델에서는 size와 feature만 있기 때문에 2차원이지만, RNN에서는 "시간" 이라는 개념이 있기 때문에 차원이 한 차원 늘어나게 된 것입니다. 합리적인 데이터 구조라고 볼 수 있습니다.


아래 코드를 통해 Training set과 Test set의 X를 RNN 학습에 맞는 형태로 reshape 해줍니다.

X_train_t = X_train.reshape(X_train.shape[0], 12, 1) X_test_t = X_test.reshape(X_test.shape[0], 12, 1)

print("최종 DATA")
print(X_train_t.shape)
print(X_train_t)
print(y_train)
최종 DATA
(228, 12, 1)
[[[ 0.20091289]
  [ 0.13173822]
  [ 0.11139526]
  ..., 
  [ 0.0704258 ]
  [ 0.        ]
  [ 0.01402033]]

 [[ 0.03002688]
  [ 0.20091289]
  [ 0.13173822]
  ..., 
  [ 0.09531795]
  [ 0.0704258 ]
  [ 0.        ]]


LSTM 모델 만들기


아래와 같이 keras를 통해 LSTM 모델을 만들 수 있습니다. input_shape=(timestep, feature)으로 만들어줍니다. size는 모델 설계시에는 중요하지 않으므로, feature, timestep만 모델에 알려주면 됩니다. 또 예측하고자하는 target의 갯수가 1이므로 마지막에 Dense(1)을 하나 추가해줍니다. 또한 실제 연속적인 값을 예측하는 것이기 때문에 loss function은 mean squared error가 됩니다. 또한 일반적으로 optimizer는 adam을 자주 사용합니다.

from keras.layers import LSTM from keras.models import Sequential from keras.layers import Dense import keras.backend as K from keras.callbacks import EarlyStopping K.clear_session() model = Sequential() # Sequeatial Model model.add(LSTM(20, input_shape=(12, 1))) # (timestep, feature) model.add(Dense(1)) # output = 1 model.compile(loss='mean_squared_error', optimizer='adam') model.summary()


모델 Fitting


모델을 Fitting한다는 것은 Training data set으로 optimization 과정을 통해 모델의 weight를 찾는 것입니다. early stopping 객체를 이용해 epoch마다 early stopping을 체크합니다. 아래와 같은 결과를 내면서 트레이닝이 잘 되는 것을 확인할 수 있습니다.

early_stop = EarlyStopping(monitor='loss', patience=1, verbose=1)

model.fit(X_train_t, y_train, epochs=100,
          batch_size=30, verbose=1, callbacks=[early_stop])
Epoch 1/100
228/228 [==============================] - 0s - loss: 0.3396     
Epoch 2/100
228/228 [==============================] - 0s - loss: 0.2958     
Epoch 3/100
228/228 [==============================] - 0s - loss: 0.2551     
Epoch 4/100
228/228 [==============================] - 0s - loss: 0.2175   
...


학습된 모델을 통해 테스트셋 Test 하기


테스트셋은 2011/1/1 이후의 데이터를 나타냅니다. 트레이닝에는 1991년~ 2010년 까지 데이터가 이용되었기 때문에, 이 실습은 1991년~2010년 데이터를 통해 2011년 이후의 데이터를 예측하는 것으로 볼 수 있습니다.


테스트셋의 모양은 아래와 같이 생겼습니다. 당연한 이야기지만 트레이닝셋과 구조가 같습니다.

print(X_test_t)
[[[ 1.06265011]
  [ 0.87180554]
  [ 0.84048091]
  [ 0.86220767]
  [ 0.88363094]
  [ 0.89302107]
  [ 0.92552046]
  [ 0.89993326]
  [ 0.83505683]
  [ 0.77259579]
  [ 0.56926634]
  [ 0.61423187]]


....


model.predict를 통해 테스트셋의 X에 대한 예측값 y_hat을 얻을 수 있습니다.

y_pred = model.predict(X_test_t)
print(y_pred)
[[ 0.82410765]
 [ 0.85221326]
 [ 0.88795143]
 [ 0.90008551]
 [ 0.90281236]
 [ 0.90063977]
 [ 0.89379823]
 [ 0.88957471]
 [ 0.88779473]

...


  • 이민호 2018.03.28 23:10

    안녕하세요 저번에 질문 드렸던 학생입니다. 제가 모르는 부분이있어서 그러는데 질문좀 할게요 ㅠㅠㅠㅠㅠ 죄송해요

    마지막에 예측값 y_hat까지 얻으셨는데 예측값과 실제 테스트 값과 차이가 얼마나 있고 확인하기 위해서 위에 matplotlib함수를 이용해서 그래프를 나타낸것처럼 확인하는 방법이 있을까요? 하려면 데이터 전처리 한 부분을 다시 복구해야하는건가요?

    • 사용자 Deepplay 2018.03.30 01:49 신고

      예측값 y_hat에 대한 실제 정답값은 y_train 변수에 저장되어있으니 이 두개를 matplotlib를 통해 그래프로 나타내고 비교하시면 될 것 같습니다~ㅎㅎ

  • 이민호 2018.03.30 19:15

    안녕하세요 Deepp님
    y_train인가요? y_test에 실제 정답값이 저장되어있는거 아닌가요?? 그리고 에측값이 y_pred변수에 저장되어있는거 아닌가요?
    제가 해보았는데
    plt.figure()
    plt.plot(y_pred)
    plt.plot(y_test)
    2개의 그래프를 해보았는데 차이가 많이나고 일단 y축의 범위가 다르게 나옵니다.
    혹시 제가 잘못 비교한것은 아닌지 모르겠습니다

    • 사용자 Deepplay 2018.04.01 04:33 신고

      아 y_test가 맞습니다. 잘못적었네요.

      음 그런경우에는 optimization이 잘 안됐을 수 있어서 우선은 실제 값을 프린트해봐서 비슷하게 나오는지 확인해보면 좋을 것 같습니다. 그리고 maplotlib를 이용해 두 데이터가 하나의 그래프에 겹쳐서 나오도록 비교하시고 그래도 차이가 많이난다면 모델 튜닝을 잘 해보셔야합니다. (epoch을 늘리거나 learning rate를 조정하거나 등) 위 예제는 예제이기 때문에 optimization이 잘 안되었을 수도 있습니다.

  • 절간스님 2018.05.15 17:33

    y_pred = model.predict(X_test_t)
    y_pred = sc.inverse_transform(y_pred)
    y_pred_df = pd.DataFrame(y_pred, columns=['prediction'], index=test.index[:-12])

    y_pred_df.plot(ax=ax)
    plt.show()
    이민호님은 inverse_transform을 생략해서 그런 게 아닐까 싶습니다.
    이렇게 해보니 prediction이 매우 잘됬다고 보기 어려운데
    epoch나 learning rate 말고 다른 방법으로 optimization시키는 방법이 있을까요?

    • 사용자 Deepplay 2018.10.15 03:46 신고

      안녕하세요. 말씀해주신 방법 이외에도 LSTM 모델 자체의 파라미터를 변경하거나 (cell의 수), optimizer 변경 등을 해볼 수 있습니다.

  • 2018.08.31 20:49

    비밀댓글입니다

    • 2018.10.15 04:00

      비밀댓글입니다

  • 김종화 2018.10.13 18:20

    안녕하세요 예제 따라하는 중간에
    X_train_t = X_train.reshape(X_train.shape[0], 12, 1)

    ---------------------------------------------------------------------------
    ValueError Traceback (most recent call last)
    <ipython-input-24-994d220ffde8> in <module>()
    ----> 1 X_train_t = X_train.reshape(X_train.shape[0], 12, 1)

    ValueError: cannot reshape array of size 357 into shape (119,12,1)
    이런 에러가 뜨는 어떻게 수정해야 할까요?

    • 사용자 Deepplay 2018.10.15 04:00 신고

      안녕하세요. 앞부분에서 데이터 핸들링이 제대로 되지 않은듯 합니다. X_train이 (119,3)의 모양을 갖고 있어서 이를 (119,12,1)로 바꾸려고하니 numpy array의 element 갯수 차이 때문에 에러가 난 것입니다.

      앞선 데이터 처리 과정을 다시 하시어 X-train의 shape를 (228,12) 모양으로 만든 후, X_train.reshape(X_train.shape[0], 12, 1) 를 실행하시면 (228,12,1)의 모양이 될 듯합니다!

  • 학생 2019.07.22 00:36

    안녕하세요! 글 잘 읽었습니다. 다름이 아니라 데이터셋을 Test와 Train으로 나누는 과정에서 오류가 계속 발생해서 여쭤봅니다.
    저는 원래는 아래와같은 샘플 코드를 실행하려했습니다.

    datas = df['inputs'].value
    labels = df['outputs'].value

    training_size = int(0.8* datas.shape[0])
    training_datas = datas[:training_size,:]
    training_labels = labels[:training_size,:,0]
    validation_datas = datas[training_size:,:]
    validation_labels = labels[training_size:,:,0]

    그런데 csv파일에서 불러온 데이터프레임 형태의 df가 3가지 인덱스로 구성되어있지않아 datas[:training_size,:] 를 할때 계속 오류가 났었습니다.
    혹시 이때 어떻게 해야하는지 혹시 아시나요?

    즉 제 질문은 데이터 프레임을 values함수를 통해 np.array로 만들고 shape함수를 해보면 (1150, 7) 이런식으로 나오는데
    이것을 3차원으로 reshape하는 방법을 조언 주시면 감사하겠습니다!

  • Jun 2019.10.26 00:43

    LSTM관련 서적을 아직 구매하지 않아서 해당 포스팅 덕분에 좋은 공부를 하였습니다.

    하지만 해당 예제 결과는 저같은경우 실제값과 예측값의 차이가 매우 크게 나오는데 다른분들은 어떤신지 궁금하네요

    제 개인적인 판단으로는 EarlyStopping의 patience가 1로 잡혀서

    적은 Epoch 횟수에 학습을 멈추기에 성능이 좋지 못하게 나오는게 아닌가 생각됩니다.

    만약 보다 좋은 성능을 보고싶은분이 계신다면 patience를 3~7 정도 잡고 돌리면 괜찮치 않을까 싶네요