자연어처리(NLP) 18일차 (IMDB 리뷰 감성 분류하기)

정민수
13 min readJun 24, 2019

--

2019.06.24

출처 : https://wikidocs.net/book/2155

핵심키워드

  • LSTM

IMDB 리뷰 감성 분류하기(IMDB Movie Review Sentiment Analysis)

머신 러닝에서 텍스트 분류. 그 중에서도 특히, 감성 분류를 연습하기 위해 자주 사용되는 훈련 데이터가 있다. 바로 영화 사이트 IMDB의 리뷰 데이터이다. 이 데이터는 리뷰에 대한 텍스트와 해당 리뷰가 긍정인 경우 1을 부정인 경우 0으로 표시한 레이블로 구성된 데이터이다.

스탠포드 대학교에서 2011년에 낸 논문에서 이 데이터를 소개하였으며, 당시 논문에서는 이 데이터를 훈련 데이터와 테스트 데이터를 50:50 비율로 분할하여 88.98%의 정확도를 얻었다고 소개하고 있다.

논문 링크 : http://ai.stanford.edu/~amaas/papers/wvSent_acl2011.pdf

케라스에서는 해당 IMDB 영화 리뷰 데이터를 imdb.load_data() 함수를 통해 바로 다운로드 할 수 있도록 지원하고 있다. 케라스로부터 해당 데이터를 다운로드 받아 감성 분류를 수행하는 모델을 만들어보자.

1. IMDB 리뷰 데이터에 대한 이해

from keras.datasetstasets import imdb
(X_train, y_train), (X_test, y_test) = imdb.load_data(num_words=10000)
# 영화 리뷰는 X_train에, 감성 정보는 y_train에 저장

영화 리뷰 데이터를 가져온다. 케라스에서 제공하는 IMDB 리뷰 데이터는 앞서 배운 로이터 뉴스 데이터에서 훈련 데이터와 테스트 데이터를 우리가 직접 비율을 조절했던 것과는 달리 이미 훈련 데이터와 테스트 데이터를 50:50 비율로 구분해서 제공한다. 로이터 뉴스 데이터에서 사용했던 test_split과 같은 테스트 데이터의 비율을 조절하는 파라미터는 imdb.load_data에서는 지원하지 않는다.

여기서 num_words는 이 데이터에서 등장 빈도 순위로 몇 번째에 해당하는 단어까지를 갖고 올 것인지 조절하는 것이다. 예를 들어서 10,000이란 값을 넣으면, 등장 빈도 순위가 1~10,000에 해당하는 단어만 갖고오게 된다. 즉, 단어의 종류는 10,000개가 되므로 단어 집합의 크기는 10,000이 된다.

print('훈련 데이터: {}'.format(len(X_train)))
print('테스트 데이터: {}'.format(len(X_test)))
num_classes = max(y_train) + 1
print('카테고리: {}'.format(num_classes))
훈련 데이터: 25000
테스트 데이터: 25000
카테고리: 2

y_train은 0부터 시작해서 레이블을 부여하므로, y_train에 들어 있는 가장 큰 수에 +1을 하여 출력하면 카테고리가 총 몇 개인지를 알 수 있다. 훈련 데이터는 25,000개, 테스트 데이터는 25,000개, 카테고리는 2개인 것을 확인할 수 있다.

print(X_train[0])
print(y_train[0])

훈련 데이터가 어떻게 구성되어있는지 확인하기 위해 첫번째 훈련 데이터를 출력해본다. 다시 말해, 25,000개의 영화 리뷰 중 첫번째 리뷰 텍스트와 그 리뷰에 대한 레이블을 출력해보는 것이다.

[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]
1

첫번째 훈련 데이터(X_train[0], y_train[0])에서 리뷰 데이터에 해당하는 X_train[0]에는 숫자들이 들어있다. 이 데이터는 토큰화와 단어에 인덱스를 부여하여 텍스트를 숫자로 바꿔 표현하는 텍스트 전처리가 끝난 상태이다.

IMDB 리뷰 데이터는 전체 25,000개의 훈련 데이터에서 각 단어들이 몇 번 등장하는 지의 빈도에 따라서 인덱스를 부여했다. 1이라는 숫자는 이 단어가 이 데이터에서 등장 빈도가 1등이라는 뜻이다. 973이라는 숫자는 이 단어가 데이터에서 973번째로 빈도수가 높은 단어라는 뜻이다.

첫번째 훈련 데이터에서 레이블에 해당하는 y_train[0]은 1이라는 값이 들어있다. 이 숫자는 첫번째 훈련 데이터가 2개의 카테고리 중 1에 해당하는 카테고리임을 의미한다. 이 예제의 경우 감성 정보로서 0 또는 1의 값을 가지는데, 이 경우에는 긍정을 의미하는 1의 값을 가진다.

25,000개의 X_train 데이터의 각 길이는 전부 다른데, X_train 데이터의 각 길이가 대체적으로 어떤 크기를 가지는지 그래프를 통해 확인해보도록 한다.

import matplotlib.pyplot as pltplt.hist([len(s) for s in X_train], bins=50)
plt.xlabel('length of Data')
plt.ylabel('number of Data')
plt.show()

대체적으로 500이하의 길이를 가지며, 특히 100~300길이를 가진 데이터가 많은 것을 확인할 수 있다. 반면, 가장 긴 길이를 가진 데이터는 길이가 1,000이 넘는 것도 확인할 수 있다.

X_train에 들어있는 숫자들이 각자 어떤 단어들을 나타내고 있는 지 궁금하기 때문에, 이를 확인해보도록 한다.

word_index = imdb.get_word_index()
index_to_word={}
for key,value in word_index.items():
index_to_word[value] = key
Downloading data from https://s3.amazonaws.com/text-datasets/imdb_word_index.json
1646592/1641221 [==============================] - 2s 1us/step
print(index_to_word[1])
print(index_to_word[3941])
the
journalist

이 데이터에서 빈도가 가장 높은 단어는 the이고, 단어가 3941번째로 높은 단어는 journalist임을 알 수 있다.

print(' '.join([index_to_word[X] for X in X_train[0]]))the as you with out themselves powerful lets loves their becomes reaching had journalist of lot from anyone to have after out atmosphere never more room and it so heart shows to years of every never going and help moments or of every chest visual movie except her was several of enough more with is now current film as you of mine potentially unfortunately of you than him that with out themselves her get for was camp of you movie sometimes movie that with scary but and to story wonderful that in seeing in character to of 70s musicians with heart had shadows they of here that with her serious to have does when from why what have critics they is you that isn't one will very to as itself with other and in of seen over landed for anyone of and br show's to whether from than out themselves history he name half some br of and odd was two most of mean for 1 any an boat she he should is thought frog but of script you not while history he heart to real at barrel but when from one bit then have two of script their with her nobody most that with wasn't to with armed acting watch an for with heartfelt film want an

첫번째 훈련 데이터의 X_train[0]이 인덱스로 바뀌기 전에 어떤 단어들이었는지 확인해보자. (물론 인덱스로 바꾸기 전에도 어느 정도 전처리가 된 상태라서 제대로 된 문장이 나오지는 않는다.)

2. LSTM으로 IMDB 리뷰 감성 분류하기

from keras.datasets import imdb
from keras.models import Sequential
from keras.layers import Dense, LSTM, Embedding
from keras.preprocessing import sequence

우선 필요한 패키지를 가져온다.

(X_train, y_train), (X_test, y_test) = imdb.load_data(num_words=5000)

훈련 데이터는 빈도 순위가 5,000 이상인 단어들만 갖고 오도록 한다.

X_train = sequence.pad_sequences(X_train, maxlen=500)
X_test = sequence.pad_sequences(X_test, maxlen=500)

각 훈련 데이터는 문장의 길이가 다르기 때문에, 모델이 처리할 수 있도록 길이를 동일하게 해주어야 한다. 이때 사용하는 것이 pad_sequences()이다. 길이는 maxlen에 넣는 값으로 정해진다. 훈련 데이터가 정한 길이를 초과하면 초과분을 삭제하고, 부족하면 0으로 채운다.

model = Sequential()
model.add(Embedding(5000, 120))
model.add(LSTM(120))
model.add(Dense(1, activation='sigmoid'))

Embedding()은 두 개의 인자를 받는데, 첫번째 인자는 단어 집합의 크기이며 두번째 인자는 임베딩 후의 벡터 크기이다. 여기서는 120을 선택했다. 즉, 입려 데이터에서 모든 단어는 120 차원을 가진 임베딩 벡터로 표현된다.

출력층은 활서와 함수로 시그모이드 함수를 갖는 뉴런 하나를 사용한다. 시그모이드 함수를 사용하면 0 또는 1의 결과를 도출해낼 수 있기 때문에 분류해야하는 카테고리가 두 개일 경우에 출력층에서 사용된다.

model.compile(loss='binary_crossentropy', optimizer='adam',
metrics=['accuracy'])
model.fit(X_train, y_train, validation_data=(X_test, y_test),
epochs=5, batch_size=64)
Train on 25000 samples, validate on 25000 samples
Epoch 1/5
25000/25000 [==============================] - 368s 15ms/step - loss: 0.4395 - acc: 0.7940 - val_loss: 0.3828 - val_acc: 0.8538
Epoch 2/5
25000/25000 [==============================] - 360s 14ms/step - loss: 0.3018 - acc: 0.8804 - val_loss: 0.3217 - val_acc: 0.8722
Epoch 3/5
25000/25000 [==============================] - 362s 14ms/step - loss: 0.2672 - acc: 0.8917 - val_loss: 0.3311 - val_acc: 0.8621
Epoch 4/5
25000/25000 [==============================] - 367s 15ms/step - loss: 0.2306 - acc: 0.9095 - val_loss: 0.3375 - val_acc: 0.8617
Epoch 5/5
25000/25000 [==============================] - 366s 15ms/step - loss: 0.1813 - acc: 0.9323 - val_loss: 0.3713 - val_acc: 0.8431
scores = model.evaluate(X_test, y_test, verbose=0)
# 텍스트 데이터에 대해서 정확도 평가
print('정확도 : %.2f%%' % (scores[1]*100))
정확도 : 84.31%

앞서 로이터 뉴스 분류하기 챕터에서 언급했듯이, validation_data에 테스트 데이터인 X_test 와 y_test를 사용할 경우에는 마지막 에포크에서의 val_acc가 결국 테스트 데이터의 정확도이다. 테스트 데이터에 대해서 87%의 정확도가 나왔다.

--

--

정민수
정민수

No responses yet