[딥러닝] 순환 신경망

3 분 소요

순차 데이터

순차데이터는 텍스트나 시계열 데이터와 같이 순서에 의미가 있는 데이터를 말한다.

완전 연결 신경망이나 합성곱 신경망은 이전에 입력한 데이터를 기억하는 장치가 없다. 하나의 샘플을 사용해 정방향 계산을 수행하고 나면 그 샘플은 버려지고 다음 샘플을 처리할 때 재사용하지 않는다.

이렇게 입력 데이터의 흐림이 앞으로만 전달되는 신경망을 피드포워드 신경망(feedforward neural network, FFNN)이라고 한다.

순환 신경망

순환 신경망(recurrent neural network, RNN)은 일반적인 완전 연결 신경망과 거의 비슷하다. 완전 연결 신경망에 이전 데이터의 처리 흐름을 순환하는 고리 하나만 추가하면 된다.

샘플을 처리하는 한 단계를 타임스텝이라고 말한다.

순환 신경망에서는 특별히 층을 셀(cell)이라고 부른다. 한 셀에는 여러 개의 뉴런이 있지만 완전 연결 신경망과 달리 뉴런을 모두 표시하지 않고 하나의 셀로 층을 표현한다. 또 셀의 출력을 은닉 상태(hidden state)라고 부른다.

일반적으로 은닉층의 활성화 함수로는 하이퍼볼릭 탄젠트 함수인 tanh가 많이 사용된다.

거시적인 구조는 다른 신경망과 크게 다르지 않다. 입력에 가중치를 곱하고 절편을 더한 다음 활성화 함수를 통과시켜 다음 층으로 전달한다. 다만 순환층은 이전 타임스텝의 출력을 입력으로 함께 사용한다. 또 마지막 타임스텝의 출력만 다음 층으로 전달한다.

IMDB 리뷰 데이터셋

imdb 리뷰 데이터셋은 유명한 인터넷 영화 데이터베이스인 imdb.com에서 수집한 리뷰를 감상평에 따라 긍정과 부정으로 분류해 놓은 데이터셋이다. 총 50000개의 샘플로 이루어져 있고 훈련 데이터와 테스트 데이터에 각각 25000개씩 나누어져 있다.

텍스트 자체를 신경망에 전달하지 않는다. 텍스트를 숫자로 변환하는데 이때 정수값 사이에는 어떤 관계도 없다. 더 큰 숫자라고해서 더 중요하다는 의미가 아니다. 일반적으로 영어 문장은 모두 소문잘 바꾸고 구두점을 삭제한 다음 공백을 기준으로 분리한다. 이렇게 분리되 단어를 토큰이라고 부른다. 하나의 샘플은 여러 개의 토큰으로 이루어져 있고 1개의 토큰이 하나의 타임스탬프에 해당한다.

from tensorflow.keras.datasets import imdb

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

텐서플로에는 이미 정수로 바꾼 imdb 리뷰 데이터가 있다. 여기에서는 전체 데이터셋에서 가장 자주 등장하는 단어 500개만 사용한다.

각 리뷰의 길이는 다르다.

토큰에 할당하는 정수 중 몇개는 특정한 용도로 예약되어 있는 경우가 많다. 0은 패딩, 1은 문장의 시작, 2는 어휘 사전에 없는 토큰 등이다.

타깃값은 부정 0, 긍정 1이다.

from sklearn.model_selection import train_test_split

train_input, val_input, train_target, val_target = train_test_split(
    train_input, train_target, test_size=0.2, random_state=42)

20%를 검증 세트로 떼어 놓는다.

from tensorflow.keras.preprocessing.sequence import pad_sequences

train_seq = pad_sequences(train_input, maxlen=100)

print(train_seq.shape)
(20000, 100)

리뷰를 길이가 100이 되도록 맞춘다.

pad_sequences 함수는 기본적으로 maxlen 보다 긴 시퀀스의 앞부분을 자른다. 이렇게 하는 이유는 일반적으로 시퀀스의 뒷부분의 정보가 더 유용하다고 기대하기 때문이다.

val_seq = pad_sequences(val_input, maxlen=100)

검증 세트 역시 길이를 100으로 맞춘다.

순환 신경망 만들기

from tensorflow import keras

model = keras.Sequential()

model.add(keras.layers.SimpleRNN(8, input_shape=(100, 500)))
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
simple_rnn (SimpleRNN)       (None, 8)                 4072      
_________________________________________________________________
dense (Dense)                (None, 1)                 9         
=================================================================
Total params: 4,081
Trainable params: 4,081
Non-trainable params: 0
_________________________________________________________________
train_oh = keras.utils.to_categorical(train_seq)
val_oh = keras.utils.to_categorical(val_seq)

print(train_oh.shape)
(20000, 100, 500)

순환 신경망 입력층의 (100,500)에서 100은 샘플의 길이이다.

train_seq, val_seq에는 한가지 문제가 있다. 토큰을 정수로 변환한 이 데이터를 신경망에 주입하면 큰 정수가 큰 활성화 출력을 만든다. 정수값에 있는 크기 속성을 없애고 각 정수를 고유하게 표현하는 원-핫 인코딩으로 바꿔야 한다. 이 배열의 크기는 500개 단어만 사용했으므로 500이다.

글서 훈련, 검증 세트를 모두 원-핫 인코딩 한다.

훈련하기

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

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

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

이 모델은 원-핫 인코딩을 했기때문에 입력 데이터가 엄청 커진다. 토큰 1개를 500차원으로 늘렸기 때문에 대략 500배가 커진다. 훈련 데이터가 커질수록 더 문제가 된다. 이러한 문제를 해결하기 위해 단어 임베딩을 한다.

단어 임베딩

순환 신경망에서 텍스트를 처리할 때 많이 사용하는 방법으 단어 임베딩이다. 단어 임베딩은 각 단어를 고정된 크기의 실수 벡터로 바꾼다.

이런 단어 임베딩으로 만들어진 벡터는 원-핫 인코딩된 벡터보다 훨씬 의미 있는 값으로 채워져 있기 때문에 자연어 처리에서 더 좋은 성능을 내는 경우가 많다.

model2 = keras.Sequential()

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

model2.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
embedding_1 (Embedding)      (None, 100, 16)           8000      
_________________________________________________________________
simple_rnn_2 (SimpleRNN)     (None, 8)                 200       
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 9         
=================================================================
Total params: 8,209
Trainable params: 8,209
Non-trainable params: 0
_________________________________________________________________

앞서 원-핫 인코딩은 샘플 하나를 500차원으로 늘렸다. 이와 비슷하게 임베딩도 (100,)을 (100,20)과 같이 2차원 배열로 늘리지만 원-핫 인코딩과 비교해 훨씬 작은 크기로도 단어를 잘 표현할 수 있다.

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

checkpoint_cb = keras.callbacks.ModelCheckpoint('best-embedding-model.h5', 
                                                save_best_only=True)
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])