본문 바로가기

AI/딥러닝(Deep Learning)

[딥러닝/DL]9. LSTM과 GRU 셀

728x90
반응형

Pupbani는 저번에 만든 순환 신경망의 성능을 더 끌어 올리기 위한 방법을 찾던 중
LSTM과 GRU 셀을 알게 되었다.
Pupbani는 이것들을 사용해 순환 신경망 모델을 만들어보려고 한다.

LSTM 구조

LSTM(Long Short-Term Memory)는 단기 기억을 오래 기억하기 위해 고안되었다.
LSTM은 구조가 복잡하므로 딘계적으로 따라가보자.

은닉상태 만들기

입력과 이전 타임스텝의 은닉 상태를 가중치에 곱한 후 활성화 함수를 통과 시켜 다음 은닉 상태를 만든다.
이 때 기본 순환층과 달리 "Sigmoid" 함수를 활성화 함수로 사용한다.

이 그림에서는 가중치 wx와 wh를 통틀어 wo(입력과 은닉상태를 가중치에 곱한 값)라고 부른다.
파란색 원은 tanh 함수, 빨간색 원은 시그모이드 함수, x는 곱셈을 가리킨다.
LSTM은 순환되는 상태가 2개이다.

  • 은닉 상태
  • 셀 상태(cell state) : 다음 층으로 전달되지 않고 LSTM 셀에서만 순환만 되는 값이다.

셀 상태 만들기

셀 상태를 구하는 과정(셀상태 : c)

  • 1. 입력과 은닉 상태를 또 다른 가중치 wf에 곱한 다음 시그모이드 함수를 통과 시킨다.
  • 2. 이전 타임스텝의 셀 상태와 곱하여 새로운 셀 상태를 만든다.

이 셀 상태가 오른쪽에서 tanh 함수를 통과하여 새로운 은닉 상태를 만드는 데 기여한다.
LSTM은 마치 작은 셀을 여러 개(4개) 포함하고 있는 큰 셀과 같다.
※ 은닉 상태에 곱해지는 가중치 wo와 wf가 다르고 두 셀은 각기 다른 기능을 위해 훈련된다.

  • 3. 입력과 은닉 상태를 각기 다른 가중치에 곱한 다음, 하나는 (wi)시그모이드, 하나는 (wj)tanh 함수를 통과 시킨다.
  • 4. 시그모이드를 통과한 wi와 tanh를 통과한 wj를 곱한 후 셀 상태와 더한다.
  • 5. 이 결과가 최종적인 다음 셀 상태가 되고, 다음 은닉 상태에도 사용된다.

그림에 있는 "X(곱셈)"을 위치에 따라 삭제, 입력, 출력 게이트(gate)라고 부른다.

  • 삭제 게이트 : 셀 상태에 있는 정보를 제거하는 역할을 한다.
  • 입력 게이트 : 새로운 정보를 셀 상태에 추가하는 역항을 한다.
  • 출력 게이트 : 이 셀 상태가 다음 은닉 상태로 출력 된다.

LSTM 신경망 훈련하기

keras는 LSTM 클래스를 지원한다.
기존 SimpleRNN 클래스 자리를 LSTM 클래스로 바꾸기만 하면된다.

  • 데이터 가져오기
from tensorflow.keras.datasets import imdb
from sklearn.model_selection import train_test_split

(train_input, train_target), (test_input, test_target) = imdb.load_data(
    num_words=500)

train_input, val_input, train_target, val_target = train_test_split(
    train_input, train_target, test_size=0.2, random_state=42)
  • 데이터 전처리(시퀀스)
from tensorflow.keras.preprocessing.sequence import pad_sequences

train_seq = pad_sequences(train_input, maxlen=100)
val_seq = pad_sequences(val_input, maxlen=100)
  • 모델 생성
from tensorflow import keras

model = keras.Sequential()

model.add(keras.layers.Embedding(500, 16, input_length=100))
model.add(keras.layers.LSTM(8))
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.summary()
  • Embedding 층: 500 x 16 = 8000개 파라미터, 어휘 사전 크기(토큰 개수) x 임베딩 벡터 크기
  • LSTM 층: (16 x 8 + 8 x 8 + 8) x 4 = 800, 한개의 셀 파라미터 x 4개
  • Dense 층 : 8 + 1 = 9, 이전 층 출력 개수 + 절편

※ LSTM 셀의 모델 파라미터 수는 RNN 셀의 4배

  • 모델 컴파일
rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model.compile(optimizer=rmsprop, loss='binary_crossentropy', 
              metrics=['accuracy'])
  • 모델 학습
checkpoint_cb = keras.callbacks.ModelCheckpoint('best-lstm-model.h5')
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3,
                                                  restore_best_weights=True)

history = model.fit(train_seq, train_target, epochs=100, batch_size=64,
                    validation_data=(val_seq, val_target),
                    callbacks=[checkpoint_cb, early_stopping_cb])

44번째 에포크에서 조기 종료 되었다.
훈련 정확도 82%, 검증 정확도 79% 이다.

  • 훈련 손실과 검증 손실 그래프
import matplotlib.pyplot as plt

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

그래프를 보면 기본 순환층보다 LSTM이 과대적합을 억제하면서 훈련을 잘 수 행한 것을 볼 수 있다.
하지만 과대적합을 더 강하게 제어할 필요 있다.
그러기 위해 드롭아웃을 순환층에도 적용해보자.

  • 순환 신경망 모델들은 자체적으로 드롭아웃 기능을 제공한다.
  • dropout 매개변수 : 셀의 입력에 드롭아웃을 적용, 드롭 아웃 비율을 지정한다.
  • recurrent_dropout 매개변수 : 순환되는 은닉상태에 드롭아웃을 적용, GPU를 사용하여 속도가 매우 느려 진다.

드롭아웃 비율 30%을 적용한 모델을 작성하자.

model2 = keras.Sequential()

model2.add(keras.layers.Embedding(500, 16, input_length=100))
model2.add(keras.layers.LSTM(8, dropout=0.3))
model2.add(keras.layers.Dense(1, activation='sigmoid'))

모델을 훈련해보자.

rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model2.compile(optimizer=rmsprop, loss='binary_crossentropy', 
               metrics=['accuracy'])

checkpoint_cb = keras.callbacks.ModelCheckpoint('best-dropout-model.h5')
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3,
                                                  restore_best_weights=True)

history = model2.fit(train_seq, train_target, epochs=100, batch_size=64,
                     validation_data=(val_seq, val_target),
                     callbacks=[checkpoint_cb, early_stopping_cb])

훈련 손실과 검증 손실 그래프를 그려보자.

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

2개의 층을 연결하자

순환층을 연결할 때는 주의할 점이 있다.

  • 순환층의 은닉 상태는 샘플의 마지막 타임스텝에 대한 은닉 상태만 다음 층으로 전달한다.
  • 순환층을 쌓게 되면 모든 순환층에 순차 데이터가 필요하다.
  • 앞쪽의 순환층이 모든 타임스텝에 대한 은닉 상태를 출력해야 한다.
  • 오직 마지막 순환층만 마지막 타임스텝의 은닉 상태를 출력해야 한다.

모델을 만들어보자.

model3 = keras.Sequential()

model3.add(keras.layers.Embedding(500, 16, input_length=100))
model3.add(keras.layers.LSTM(8, dropout=0.3, return_sequences=True))
model3.add(keras.layers.LSTM(8, dropout=0.3))
model3.add(keras.layers.Dense(1, activation='sigmoid'))

model3.summary()

모델을 학습해보자.

rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model3.compile(optimizer=rmsprop, loss='binary_crossentropy', 
               metrics=['accuracy'])

checkpoint_cb = keras.callbacks.ModelCheckpoint('best-2rnn-model.h5')
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3,
                                                  restore_best_weights=True)

history = model3.fit(train_seq, train_target, epochs=100, batch_size=64,
                     validation_data=(val_seq, val_target),
                     callbacks=[checkpoint_cb, early_stopping_cb])

일반적으로 순환층을 쌓으면 성능이 높아지나 이 예제에서는 그렇게 높은 성능이 나오지 않았다.

과대적합이 잘 제어 되었는지 확인하기 위해 훈련 손실, 검증 손실 그래프를 그려보자.

plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

그래프를 보면 과대적합을 제어하면서 손실을 최대한 낮춘 것 같다.

GRU(Gated Recurrent Unit)구조

GRU는 뉴욕 대학교 조경현 교수가 발명한 셀로 유명하다.
LSTM을 간소화한 버전으로 생각할 수 있다.
LSTM 처럼 셀 상태를 계산하지 않고 은닉 상태 하나만 포함하고 있다.

  • GRU의 셀

GRU 셀에는 은닉 상태와 입력에 가중치를 곱하고 절편을 더하는 작은 셀이 3개 들어 있다.
2개는 Sigmoid 함수(wr, wz)를 통과하고 한 개는 tanh 함수(wg)를 통과한다.

  • wz 셀: 출력이 은닉 상태에 바로 곱해져 삭제 게이트 역할을 수행한다.
  • wr 셀: 출력된 값은 wg 셀이 사용할 은닉 상태의 정보를 제어한다.
  • wg 셀: 출력을 1 뺀다음 가장 오른쪽 wg를 사용하는 셀의 출력에 곱한다.

GRU 셀은 LSTM보다 가중치가 적기 때문에 계산량이 적지만 LSTM 못지않은 좋은 성능을 낸다.

신경망을 훈련해보자.

  • 모델 만들기
model4 = keras.Sequential()

model4.add(keras.layers.Embedding(500, 16, input_length=100))
model4.add(keras.layers.GRU(8))
model4.add(keras.layers.Dense(1, activation='sigmoid'))

model4.summary()

파라미터 개수

  • Embedding : 500 x 16 = 8000개
  • GRU : (16 x 8 + 8) + (8 x 8 + 8) x 3 = 624 
    • 입력에 곱하는 가중치 : 16 x 8 = 128개
    • 은닉 상태에 곱하는 가중치 : 8 x 8 = 64개
    • 절편 : 8개 

케라스에서는 작은 셀마다 은닉 상태 가중치를 별도의 '더 작은 셀'로 계산후 곱한다.

  • 작은 셀의 파라미터 : (16 x 8 + 8) + (8 x 8 + 8) = 208
  • Dense : 8 + 1 = 9

  • 모델 학습하기
rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model4.compile(optimizer=rmsprop, loss='binary_crossentropy', 
               metrics=['accuracy'])

checkpoint_cb = keras.callbacks.ModelCheckpoint('best-gru-model.h5')
early_stopping_cb = keras.callbacks.EarlyStopping(patience=3,
                                                  restore_best_weights=True)

history = model4.fit(train_seq, train_target, epochs=100, batch_size=64,
                     validation_data=(val_seq, val_target),
                     callbacks=[checkpoint_cb, early_stopping_cb])

LSTM과 비슷한 성능이 나온다.

  • 손실 그래프 그리기
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train', 'val'])
plt.show()

드롭아웃을 사용하지 않았기 때문에 이전보다 훈련 손실과 검증 손실 사이에 차이가 있지만 훈련 과정이 잘 수렴되고 있는 것을 확인할 수 있다.

Pupbani는 이제 LSTM과 GRU를 사용할 수 있게 되었다!

728x90
반응형