LSTM 감성 분류(Sentiment Analysis) - IMDB 영화 리뷰 데이터

IMDB 영화 리뷰 데이터

- 영화 사이트 IMDB의 리뷰 데이터로 리뷰가 긍정인 경우 1, 부정인 경우 0으로 표시한 레이블로 구성된 데이터

 

 

 

IMDB 영화 리뷰 데이터 확인

환경 및 데이터 준비

import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.datasets import imdb

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

 

 

데이터 수 확인

- 카테고리 : 긍정 1, 부정 0

category = len(set(y_train))
print('카테고리 :', category)
print('훈련용 리뷰 개수 :', len(X_train))
print('테스트용 리뷰 개수 :', len(X_test))
카테고리 : 2
훈련용 리뷰 개수 : 25000
테스트용 리뷰 개수 : 25000

 

 

첫 번째 훈련 데이터 X_train[0]의 내용 및 레이블 확인

- IMDB 리뷰 데이터에서 토큰화와 정수 인코딩 끝난 상태

print('첫번째 훈련용 리뷰 :', X_train[0])
print('첫번째 훈련용 리뷰의 레이블 :', y_train[0])
첫번째 훈련용 리뷰 : [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, 22665, 9, 35, 480, 284, 5, 150, 4, 172,
<--생략-->
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

 

 

리뷰의 길이 분포 시각화

review_length = [len(review) for review in X_train]

print('리뷰의 최대 길이 :', np.max(review_length))
print('리뷰의 평균 길이 :', np.mean(review_length))

plt.subplot(1,2,1)
plt.boxplot(review_length)
plt.subplot(1,2,2)
plt.hist(review_length, bins=50)
plt.show()
리뷰의 최대 길이 : 2494
리뷰의 평균 길이 : 238.71364

대체적으로 1000 이하의 길이를 갖고, 100~500의 길이를 가진 데이터가 많음

 

 

레이블 분포 확인

- 총 25000개의 리뷰 중 레이블 0과 1은 각각 12500개로 균등하게 분포

unique_elements, counts_elements = np.unique(y_train, return_counts=True)
print('각 레이블에 대한 빈도수 :')
print(np.asarray((unique_elements, counts_elements)))
각 레이블에 대한 빈도수 :
[[    0     1]
 [12500 12500]]

 

 

빈도수 n 번째 단어 확인

- 정수로부터 단어를 알 수 있도록 get_word_index()에서 key와 value를 반대로 저장한 index_to_word 생성

- 숫자 0은 패딩을 의미하는 토큰인 pad, 숫자 1은 문장의 시작을 의미하는 sos, 숫자 2는 OOV를 위한 토큰인 unk라는 특별 토큰에 맵핑되어져야 함

- imdb.get_word_index()에 저장된 값에 +3을 해야 실제 맵핑되는 정수가 됨

 

 

  • get_word_index()

: 각 단어와 그 단어에 부여된 인덱스 반환

word_to_index = imdb.get_word_index()
index_to_word = {}
for key, value in word_to_index.items():
    index_to_word[value + 3] = key

 

 

- 인덱스 입력하면 단어 확인. +3 해야 원하는 빈도수 선택 가능

print('빈도수 상위 1등 단어 :', index_to_word[4])
빈도수 상위 1등 단어 : the
print('빈도수 상위 2000위 단어 :', index_to_word[2003])
빈도수 상위 2000위 단어 : behavior

 

 

첫 번째 훈련용 데이터 X_train[0]의 실제 내용 확인

for index, token in enumerate(("<pad>", "<sos>", "<unk>")):
    index_to_word[index] = token
    
print(' '.join([index_to_word[index] for index in X_train[0]]))

 

 

 

1. 환경 및 데이터 준비

- IMDB 리뷰 데이터에서는 훈련 데이터와 테스트 데이터가 50:50 비율로 이미 설정된 상태

import re
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.datasets import imdb
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Embedding
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
from tensorflow.keras.models import load_model

vocab_size = 10000  # 단어 집합 크기
max_len = 500 # 리뷰 최대 길이

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

 

 

 

2. 데이터 전처리 - 패딩(padding)

X_train = pad_sequences(X_train, maxlen=max_len)
X_test = pad_sequences(X_test, maxlen=max_len)

 

 

 

3. 모델 설정

- LSTM, 단어 임베딩 층 포함

- 로지스틱 회귀를 수행하는 모델이므로 활성화 함수로 시그모이드 함수를 사용

embedding_dim = 100  # 임베딩 벡터 차원
hidden_units = 128  # 은닉 상태 크기

model = Sequential()
model.add(Embedding(vocab_size, embedding_dim))
model.add(LSTM(hidden_units))
model.add(Dense(1, activation='sigmoid'))

 

 

 

4. 모델 실행 환경 설정 및 최적화

- 로지스틱 회귀를 수행하는 모델이므로 손실 함수로 교차 엔트로피 함수를 사용

# 모델의 실행 옵션 설정
model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])

 

 

- 검증 데이터 손실(val_loss)이 4회 증가하면 정해진 에포크에 도달하지 못해도 학습 조기 중단

- 검증 데이터의 정확도(val_acc)가 이전보다 좋아질 경우에만 모델 저장

# 모델 최적화를 위한 설정 구간
es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4)
mc = ModelCheckpoint('GRU_model.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True)

 

 

 

5. 모델 실행

- 훈련 데이터의 20%를 검증 데이터로 분리해서 사용하고, 검증 데이터를 통해 훈련이 적절히 되고 있는지 확인

history = model.fit(X_train, y_train, epochs=15, callbacks=[es, mc], batch_size=64, validation_split=0.2)

에포크 9에서 조기 종료

 

 

 

6. 데이터 시각화

# 학습셋과 테스트셋의 오차 저장
y_vloss = history.history['val_loss']
y_loss = history.history['loss']

# 그래프로 표현
x_len = np.arange(len(y_loss))
plt.plot(x_len, y_vloss, marker='.', c="red", label='Testset_loss')
plt.plot(x_len, y_loss, marker='.', c="blue", label='Trainset_loss')

# 그래프에 그리드를 주고 레이블 표시
plt.legend(loc='upper right')
plt.grid()
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()

 

 

 

7. 저장된 모델 로드

- 훈련 과정에서 검증 데이터의 정확도가 가장 높았을 때 저장된 모델인 'LSTM_model.h5' 로드

loaded_model = load_model('LSTM_model.h5')
print("테스트 정확도: %.4f" % (loaded_model.evaluate(X_test, y_test)[1]))
782/782 [==============================] - 7s 9ms/step - loss: 0.3207 - acc: 0.8750
테스트 정확도: 0.8750

 

 

8. 임의의 데이터로 모델 테스트

  • sentiment_predict

: 입력된 문장에 대해 기본적인 전처리와 정수 인코딩, 패딩을 한 후에 모델의 입력으로 사용하여 예측값을 리턴하는 함수

word_to_index = imdb.get_word_index()
index_to_word = {}
for key, value in word_to_index.items():
    index_to_word[value+3] = key

def sentiment_predict(new_sentence):
  # 알파벳과 숫자를 제외하고 모두 제거 및 알파벳 소문자화
  new_sentence = re.sub('[^0-9a-zA-Z ]', '', new_sentence).lower()
  encoded = []

  # 띄어쓰기 단위 토큰화 후 정수 인코딩
  for word in new_sentence.split():
    try :
      # 단어 집합의 크기를 10,000으로 제한.
      if word_to_index[word] <= 10000:
        encoded.append(word_to_index[word]+3)
      else:
      # 10,000 이상의 숫자는 <unk> 토큰으로 변환.
        encoded.append(2)
    # 단어 집합에 없는 단어는 <unk> 토큰으로 변환.
    except KeyError:
      encoded.append(2)

  pad_sequence = pad_sequences([encoded], maxlen=max_len)
  score = float(loaded_model.predict(pad_sequence)) # 예측

  if(score > 0.5):
    print("{:.2f}% 확률로 긍정 리뷰입니다.".format(score * 100))
  else:
    print("{:.2f}% 확률로 부정 리뷰입니다.".format((1 - score) * 100))

 

  • 영화 블랙팬서의 1점 부정 리뷰 테스트
test_input = "This movie was just way too overrated. The fighting was not professional and in slow motion. I was expecting more from a 200 million budget movie. The little sister of T.Challa was just trying too hard to be funny. The story was really dumb as well. Don't watch this movie if you are going because others say its great unless you are a Black Panther fan or Marvels fan."

sentiment_predict(test_input)
1/1 [==============================] - 0s 353ms/step
94.28% 확률로 부정 리뷰입니다.

 

  • 영화 어벤져스 10점 긍정 리뷰 테스트
test_input = " I was lucky enough to be included in the group to see the advanced screening in Melbourne on the 15th of April, 2012. And, firstly, I need to say a big thank-you to Disney and Marvel Studios. \
Now, the film... how can I even begin to explain how I feel about this film? It is, as the title of this review says a 'comic book triumph'. I went into the film with very, very high expectations and I was not disappointed. \
Seeing Joss Whedon's direction and envisioning of the film come to life on the big screen is perfect. The script is amazingly detailed and laced with sharp wit a humor. The special effects are literally mind-blowing and the action scenes are both hard-hitting and beautifully choreographed."

sentiment_predict(test_input)
1/1 [==============================] - 0s 21ms/step
99.21% 확률로 긍정 리뷰입니다.