본문 바로가기
인공지능/Deep Learning

[DL] RNN으로 IMDB 리뷰 분류하기+단어 임베딩

by 유일리 2023. 11. 5.

순환 신경망을 영화 리뷰 데이터셋에 적용해서 리뷰를 긍정과 부정으로 분류해 볼 것이다.

데이터셋을 단어 임베딩으로 변형하여 순환 신경망에 주입해볼 것이다.

 

IMDB 리뷰 데이터셋은 imdb.com에서 수집한 리뷰를 감상평에 따라 긍정과 부정으로 분류해놓은 데이터셋이다. 총 50,000개의 샘플로 이루어져 있고 훈련 데이터와 테스트 데이터에 각각 25,000개씩 나누어져 있다.

 

텍스트 데이터의 경우, 단어를 숫자 데이터로 바꾸는 일반적인 방법은 데이터에 등장하는 단어마다 고유한 정수를 부여하는 것이다. 정숫값 사이에는 어떤 관계도 없다. 일반적으로 영어 문장은 모두 소문자로 바꾸고 구둣점을 삭제한 다음 공백을 기준으로 분리한다. 이렇게 분리된 단어는 토큰이라고 한다. (한글은 조사가 발달되어 있기에 공백이 아닌 형태소 분석을 통해 토큰을 만든다.) 토큰에 할당하는 정수 중에 몇 개는 특정한 용도로 예약되어 있는 경우가 많다. 예를 들어 0은 패딩, 1은 문장의 시작, 2는 어휘 사전에 없는 토큰을 나타낸다. 

 

실제 IMDB 리뷰 데이터셋은 영어로 된 문장이지만 편리하게도 텐서플로에는 이미 정수로 바꾼 데이터가 포함되어 있다. 

 

우선 필요한 라이브러리를 import해준다.

import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf

from tensorflow.keras.datasets import imdb

 

데이터셋을 다운로드하고 준비한다.

(train_input, train_target), (test_input, test_target) = imdb.load_data(num_words=300)
#훈련 세트와 테스트 세트의 크기 확인
print(train_input.shape, test_input.shape)
#첫 번째 리뷰에 담긴 내용 출력
print(train_input[0])
#타깃 데이터 출력 (0은 부정, 1은 긍정)
print(train_target[0])

 

#훈련 세트의 20%를 검증 세트로 떼기
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)
#훈련 세트 조사-각 리뷰의 길이를 계산해 넘파이 배열에 담기 (평균 리뷰 길이, 가장 짧고 긴 리뷰 길이 파악)
lengths = np.array([len(x) for x in train_input])
print(np.mean(lengths), np.median(lengths))
plt.hist(lengths)
plt.xlabel('length')
plt.ylabel('frequency')
plt.show()

리뷰의 평균 단어 개수는 239개이고, 중간값이 178인 것을 보아 리뷰 길이 데이터는 한쪽에 치우친 분포이다. (히스토그램으로 확인) 대부분의 리뷰 길이는 300 미만이다. 우리는 리뷰의 max 길이를 100으로 맞추자. 

from tensorflow.keras.preprocessing.sequence import pad_sequences
train_seq = pad_sequences(train_input, maxlen=100)
print(train_seq.shape)

 

maxlen에 원하는 길이를 지정하면 이보다 긴 경우는 잘라내고 짧은 경우는 0으로 패딩한다. train_input은 파이썬 리스트 배열이었지만 길이를 100으로 맞춘 train_seq는 이제 2차원 배열이 된다. pad_sequences() 함수는 기본으로 maxlen보다 긴 시퀀스의 앞부분을 자른다. 이렇게 하는 이유는 일반적으로 시퀀스의 뒷부분의 정보가 더 유용하리라 기대하기 때문이다. 만약 뒷부분을 잘라내고 싶다면 pad_sequences() 함수의 truncating 매개변수의 값을 기본값 'pre'가 아닌 'post'로 바꾸면 된다. 

train_seq에 있는 첫 번째 리뷰는 앞뒤에 패딩값 0이 없는 것으로 보아 100보다는 긴 리뷰이다. 그 밑의 리뷰는 앞부분에 0이 있는 것으로 보아 샘플의 길이가 100이 안될 것이다. 패딩 토큰은 시퀀스의 앞부분에 추가된다. 시퀀스의 마지막에 있는 단어가 셀의 은닉 상태에 가장 큰 영향을 미치게 되므로 마지막에 패딩을 추가하는 것은 일반적으로 선호하지 않는다.

 

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,300)))
#model.add(keras.layers.Dense(1, activation='sigmoid'))

 

첫 번째 차원이 100인 것은 앞에서 샘플의 길이를 100으로 지정했기 때문이다. 두번째 차원인 300은 

토큰을 정수로 변환한 데이터를 신경망에 주입하면 큰 정수가 큰 활성화 출력을 만든다. 그러나 정수 사이에 어떤 관계도 없기 때문에 단순한 정숫값을 신경망에 입력하기 위해서는 다른 방식을 찾아야 한다. 우리는 원핫 인코딩 대신 단어 임베딩을 사용해보자. 단어 임베딩은 각 단어를 고정된 크기의 실수 벡터로 바꿔준다. 단어 임베딩의 장점은 입력으로 정수 데이터를 받는 것이기 때문에 메모리를 훨씬 효율적으로 사용할 수 있다.

from tensorflow import keras
model = keras.Sequential()
model.add(keras.layers.Embedding(300, 16, input_length=100))
model.add(keras.layers.SimpleRNN(8))
model.add(keras.layers.Dense(1, activation='sigmoid'))

 

먼저 Embedding 클래스의 첫 번째 매개변수(300)는 어휘 사전의 크기이다. 앞서 리뷰 데이터셋에서 300개의 단어만 사용하도록 (num_words=300) 했기 때문이다. 두 번째 매개변수(16)은 임베딩 벡터의 크기이다.

Embedding 클래스는 300개의 각 토큰을 크기가 16인 벡터로 변경하기 때문에 총 300x16 = 4,800 개의 모델 파라미터를 가진다. 그 다음 SimpleRNN 층은 임베딩 벡터의 크기가 16이므로 8개의 뉴런과 곱하기 위해 필요한 가중치 16x8 = 128개를 가진다. 또한 은닉 상태에 곱해지는 가중치 8x8 = 64개가 있다. 마지막으로 8개의 절편이 있으므로 이 순환층에 있는 전체 모델 파라미터의 개수는 200개이다. 마지막 Dense 층의 가중치 개수는 이전과 동일하게 9개이다. 

 

모델을 훈련해보자.

 

#모델 컴파일(RMSprop 최적화 알고리즘 사용)
rmsprop = keras.optimizers.RMSprop(learning_rate=1e-4)
model.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 = model.fit(train_seq, train_target, epochs=100, batch_size=64, validation_data=(val_seq, val_target),callbacks=[checkpoint_cb, early_stopping_cb])

 

이 훈련은 27번째 에포크에서 조기 종료되었다. 검증 세트에 대한 정확도는 약 78% 정도이다. 매우 뛰어난 성능은 아니지만 감상평을 분류하는데 어느 정도 성과를 내고 있다고 판단할 수 있다. 그럼 훈련 손실과 검증 손실을 그래프로 그려서 훈련 과정을 살펴보겠다.

 

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

검증 손실이 더 감소되지 않아 훈련이 적절히 조기 종료된 것 같다.


*이 글은 '혼자 공부하는 머신러닝+딥러닝' 책을 기반으로 작성한 것입니다.

댓글