본문 바로가기

Lecture AI/6장.텍스트와 시퀀스를 위한 딥러닝

3. 순환 신경망의 이해와 SimpleRNN과 LSTM의 차이

반응형

완전 연결 네트워크나 컨브넷처럼 지금까지 본 모든 신경망의 특징은 메모리가 없다는 것입니다. 네트워크에 주입되는 입력은 개별적으로 처리되며 입력 간에 유지되는 상태가 없습니다. 이런 네트워크로 시퀀스나 시계열 데이터 포인트를 처리하려면 네트워크에 전체 시퀀스를 주입해야 합니다. 

 

전체 시퀀스를 하나의 데이터 포인트로 변환해야 합니다. 예를 들어 IMDB문제에서 영화 리뷰 하나를 큰 벡터 하나로 변환하여 처리했습니다. 이런 네트워크를 피드포워드 네트워크라고 합니다. 

 

순환신경망(RNN)은 같은 원리를 적용한 것입니다. 시퀀스의 원소를 순회하면서 지금까지 처리한 정보를 상태(state)에 저장합니다. 사실 RNN은 내부에 루프(loop)를 가진 신경망의 한 종류입니다. RNN의 상태는 2개의 다른 시퀀스를 처리하는 사이에 재설정됩니다. 하나의 시퀀스가 여전히 하나의 데이터 포인트로 간주됩니다. 즉 네트워크에 하나의 입력을 주입한다고 가정합니다. 이 데이터 포인트가 한 번에 처리되지 않는다는 것이 다릅니다. 그 대신 네트워크는 시퀀스의 원소를 차례대로 방문합니다.

 


1. 케라스의 순환 층

 

SimpleRNN에 대해 알아보겠습니다.

SimpleRNN이 한 가지 다른 점은 넘파이 예제처럼 하나의 시퀀스가 아니라 다른 케라스 층과 마찬가지로 시퀀스 배치를 처리한다는 것입니다. 즉 (timesteps, input_features)크기가 아니라 (batch_size, timesteps, input_features)크기의 입력을 받습니다.

 

케라스에 있는 모든 순환층과 마찬가지로 SimpleRNN은 두 가지 모드로 실행할 수 있습니다. 각 타임스텝의 출력을 모은 전체 시퀀스를 반환하거나(크기가 (batch_size, timesteps, output_features)인 3D 텐서), 입력 시퀀스에 대한 마지막 출력만 반환할 수 있습니다.(크기가 batch_size, output_features)인 2D 텐서). 이 모드는 객체를 생성할 때 return_sequences 매개변수로 선택할 수 있습니다. SimpleRNN을 사용하여 마지막 타임스텝의 출력만 얻는 예제를 살펴보죠.

 

from keras.models import Sequential
from keras.layers import Embedding, SimpleRNN

model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32))
model.summary()

 

다음 예는 전체 상태 시퀀스를 반환합니다.

model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32, return_sequences=True))
model.summary()

 

네트워크의 표현력을 증가시키기 위해 여러 개의 순환 층을 차례대로 쌓는 것이 유용할 때가 있습니다. 이런 설정에서는 중간층들이 전체 출력 시퀀스를 반환하도록 설정해야 합니다.

model = Sequential()
model.add(Embedding(10000, 32))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32, return_sequences=True))
model.add(SimpleRNN(32))
model.summary()

 

 

이 전장에서 확인했던 영화 리뷰 분류 문제에 적용해 보겠습니다. 먼저 데이터를 전처리합니다.

from keras.datasets import imdb
from keras.preprocessing import sequence

max_features = 10000
maxlen = 500
batch_size = 32

print('Loading data...')
(input_train, y_train), (input_test, y_test) = imdb.load_data(num_words=max_features)
print(len(input_train), 'train sequences')
print(len(input_test), 'test sequences')

print('Pad sequences (samples x time)')
input_train = sequence.pad_sequences(input_train, maxlen=maxlen)
input_test = sequence.pad_sequences(input_test, maxlen=maxlen)
print('input_train shape:', input_train.shape)
print('input_test shape:', input_test.shape)

 

Embedding층과 SimpleRNN층을 사용하여 간단한 순환 네트워크를 훈련시켜 보겠습니다.

from keras.layers import Dense

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(SimpleRNN(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(input_train, y_train, epochs=10, batch_size=128, validation_split=0.2)

 

이제 훈련과 검증의 손실과 정확도를 확인합니다.

import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) + 1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

이 모델은 성능이 높지 않습니다. 원인은 전체 시퀀스가 아니라 순서대로 500개의 단어만 입력에 사용했기 때문입니다. 이 RNN은 기준 모델보다 얻은 정보가 적습니다. 다른 이유는 SimpleRNN이 텍스트처럼 긴 시퀀스를 처리하는 데 적합하지 않기 때문입니다. 더 잘 작동하는 다른 순환 층이 있습니다. 조금 더 고급 순환 층을 살펴보죠.

 


2. LSTM과 GRU층 이해하기

 

케라스에는 SimpleRNN외에 다른 순환 층도 있습니다. LSTM과 GRU 2개입니다. 실전에서는 항상 이 둘중에 하나를 사용할 것입니다. SimpleRNN은 실전에 쓰기에는 너무 단순하기 때문입니다. SimpleRNN은 이론적으로 t에서 이전의 모든 타임스텝의 정보를 유지할 수 있습니다.

 

실제로는 긴 시간에 걸친 의존성은 학습할 수 없는 것이 문제입니다. 층이 많은 일반 네트워크(피드포워드 네트워크)에서 나타나는 것과 비슷한 현상인 그래디언트 소실 문제때문입니다. 피드포워드 네트워크에 층을 많이 추가할수록 훈련하기 어려운 것과 같습니다. 이 문제를 해결하기 위해 고안된 것이 LSTM과 GRU층입니다.

 

LSTM(Long Short-Term Memory, LSTM)알고리즘은 그래디언트 소실 문제에 대한 연구의 결정체입니다. SimpleRNN의 한 변종으로 정보를 여러 타임스텝에 걸쳐 나르는 방법이 추가됩니다. 처리할 시퀀스에 나란히 작동하는 컨베이어 벨트를 생각하면 됩니다. 시퀀스의 어느 지점에서 추출된 정보가 컨베이어 벨트 위로 올라가 필요한 시점의 타임스텝으로 이동하여 떨굽니다. 이것이 LSTM이 하는 일입니다. 나중을 위해 정보를 저장함으로써 처리 과정에서 오래된 시그널이 점차 소실되는 것을 막아 줍니다.

 

from keras.layers import LSTM

model = Sequential()
model.add(Embedding(max_features, 32))
model.add(LSTM(32))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
history = model.fit(input_train, y_train,epochs=10,batch_size=128,validation_split=0.2)

이번에는 SimpleRNN보다 높은 정확도를 보입니다. LSTM이 그래디언트 소실 문제로부터 영향을 덜 받기 때문입니다. 하지만 생각처럼 높지 않은 정확도를 보인 이유는 임베딩 차원이나 LSTM출력 차원 같은 하이퍼파라미터를 전혀 튜닝하지 않았기 때문입니다. 또 하나는 규제가 없기 때문입니다. 

 

가장 큰 이유는 리뷰를 전체적으로 길게분석하는 것은 감성 분류 문제에 도움이 되지 않기 때문입니다. 이런 간단한 문제는 각 리뷰에 어떤 단어가 나타나고 얼마나 등장하는지를 보는 것이 낮습니다. 바로 첫 번째 완전 연결 네트워크가 사용한 방법입니다. 하지만 훨씬 더 복잡한 자연어 처리 문제들에서는 LSTM능력이 드러납니다. 특히 질문-응담과 기계 번역 분야입니다. 

반응형