데이터분석/예시코드

네이버 영화 리뷰 데이터로 감성분류

이규승 2022. 6. 2. 12:33
728x90

참고 출처 : https://wikidocs.net/44249

 

6) 네이버 영화 리뷰 감성 분류하기(Naver Movie Review Sentiment Analysis)

이번에 사용할 데이터는 네이버 영화 리뷰 데이터입니다. 총 200,000개 리뷰로 구성된 데이터로 영화 리뷰에 대한 텍스트와 해당 리뷰가 긍정인 경우 1, 부정인 경우 0을 ...

wikidocs.net

불용어처리, 모델적용 오래걸린다. > 결과값만 보도록한다

 

 

colab 에서 konlpy를 사용하여 실행한다.

!pip install konlpy

 

필요한 것 import 

import numpy as np
import matplotlib.pyplot as plt
import re
from konlpy.tag import Okt
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
import pandas as pd

 

데이터 불러오기 (라벨링 완료된 데이터이다)

train_data = pd.read_table('https://raw.githubusercontent.com/pykwon/python/master/testdata_utf8/ratings_train.txt')
test_data = pd.read_table('https://raw.githubusercontent.com/pykwon/python/master/testdata_utf8/ratings_test.txt')

print(set(test_data.label)) # {0, 1} 부정 , 긍정

 

train 데이터를 전처리 해준다.

# 중복 데이터 확인 후 삭제
print(train_data.document.nunique()) # 146182

train_data.drop_duplicates(subset=['document'], inplace=True)

# null 확인 후 해당 행 삭제
print(train_data.isnull().sum()) # document    1
train_data = train_data.dropna(how='any')

# 한글과 공백만 데이터로 처리
train_data.document = train_data.document.str.replace("[^가-힣 ]", "")
train_data.document = train_data.document.str.replace('^ +','') # 하나 이상의 공백 데이터를 empty value로 변경
train_data.document.replace('',np.nan, inplace=True)

# document 열 중 NaN인 행 삭제
train_data = train_data.dropna(how='any')

 

test 데이터도 전처리 해준다

test_data.drop_duplicates(subset=['document'], inplace=True)
test_data.document = test_data.document.str.replace("[^가-힣 ]", "")
test_data.document = test_data.document.str.replace('^ +','')
test_data.document.replace('',np.nan, inplace=True)
test_data = test_data.dropna(how='any')

 

형태소분석, 불용어 제외

# 불용어(stopwords) - 문장에 자주 등장하는 자주 등장하는 단어이지만 문맥에 영향을 주지 않는 단어들
stopwords = ['와','하다','한','은','를','는','의','좀','잘','과','으로','에는','에','하는','이지만','조차','아','그러나','그리고','그래서']

# 형태소 분석
okt = Okt()

# train data
x_train = []
for sentence in train_data['document']:
  imsi = []
  imsi = okt.morphs(sentence, stem=True) # 형태소단위로 텍스트를 나눠준다. step=True : 어간추출
  imsi = [word for word in imsi if not word in stopwords] # 불용어 제거
  x_train.append(imsi)

print(x_train[:3])

# test data
x_test = []
for sentence in test_data['document']:
  imsi = []
  imsi = okt.morphs(sentence, stem=True) 
  imsi = [word for word in imsi if not word in stopwords] 
  x_test.append(imsi)

print(x_test[:3])

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
[['더빙', '진짜', '짜증나다', '목소리'], ['흠', '포스터', '보고', '초딩', '영화', '줄', '오버', '연기', '가볍다', '않다'], ['너', '무재', '밓었', '다그', '래서', '보다', '추천', '다']]
[['굳다'], ['뭐', '야', '이', '평점', '들', '나쁘다', '않다', '점', '짜다', '리', '더', '더욱', '아니다'], ['지루하다', '않다', '완전', '막장', '임', '돈', '주다', '보기']]

 

워드 임베딩

# word embedding : 정수 인코딩
tok = Tokenizer()
tok.fit_on_texts(x_train)
print(tok.word_index)

# 등장 빈도수가 3회 미만인 단어의 비중 확인
threshold = 3
total_cnt = len(tok.word_index)
rare_cnt = 0
total_freq = 0
rare_freq = 0 

for key, value in tok.word_counts.items():
  total_freq = total_freq + value
  if value < threshold:
    rare_cnt = rare_cnt + 1
    rare_freq = rare_freq + value

print('단어 집합 크기 : ', total_cnt)
print('희귀 단어 수 : ', rare_cnt)
print('단어 집합 크기 :', (rare_cnt / total_cnt) * 100)
print('전체 등장 빈도에서 희귀단어 비율 :', (rare_freq / total_freq) * 100)

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
{'이': 1, '영화': 2, '보다': 3, '가': 4, '을': 5, ...
단어 집합 크기 :  43069
희귀 단어 수 :  23850
단어 집합 크기 : 55.37625670435812
전체 등장 빈도에서 희귀단어 비율 : 1.7389526217056424

 

OOV란

https://acdongpgm.tistory.com/223

 

[NLP] . OOV 를 해결하는 방법 - 1. BPE(Byte Pair Encoding)

컴퓨터가 자연어를 이해하는 기술은 크게 발전했다. 그 이유는 자연어의 근본적인 문제였던 OOV문제를 해결했다는 점에서 큰 역할을 했다고 본다. 사실 해결이라고 보긴 어렵고 완화가 더 맞는

acdongpgm.tistory.com

 

희귀단어(2이하의 단어)제거 후 토큰화

# 희귀단어 비율이 1.7389이므로 희귀단어 갯수는 제거 (2글자 이하 단어)
vocab_size = total_cnt - rare_cnt + 2 # +2 하는 이유는 pad와 oov 토큰을 사용할 예정.
print('단어사전크기:', vocab_size) # 19221

# 토큰화 되어 있지 않은 경우 (단어 사전에 등록되지 않는 단어)에는 oov(out of vocabulary)로 처리. oov는 보통 1로 할당
tok = Tokenizer(vocab_size, oov_token='OOV')
tok.fit_on_texts(x_train)
x_train = tok.texts_to_sequences(x_train)
x_test = tok.texts_to_sequences(x_test)
print(x_train[:3])

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
단어사전크기: 19221
[[448, 21, 256, 651], [921, 450, 46, 594, 3, 211, 1427, 29, 666, 24], [380, 2404, 1, 2224, 5608, 4, 219, 14]]

 

label 보관, 빈 샘플 제거

# label 별도 보관
y_train = np.array(train_data['label'])
y_test = np.array(test_data['label'])
print(y_train[:3])

# 빈 샘플(empty sample) 제거
# 전체 데이터에서 빈도수가 낮은 단어가 삭제되었다는 것은 빈도수가 낮은 단어만으로 구성되었던 샘플들은 빈(empty) 샘플이 되었다는 것을 의미합니다.
# 빈 샘플들은 어떤 레이블이 붙어있던 의미가 없으므로 빈 샘플들을 제거해주는 작업을 하겠습니다. 
# 각 샘플들의 길이를 확인해서 길이가 0인 샘플들의 인덱스를 받아오겠습니다
drop_train = [index for index, sentence in enumerate(x_train) if len(sentence) < 1]
print(drop_train)

# 빈 샘플들을 제거
X_train = np.delete(x_train, drop_train, axis=0)
y_train = np.delete(y_train, drop_train, axis=0)
print(len(X_train))
print(len(y_train))

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
[0 1 0]

 

전체 샘플 중 길이가 max_len 이하인 샘플의 비율이 몇 %인지 확인하는 함수를 만듭니다.

def below_threshold_len(max_len, nested_list):
  count = 0
  for sentence in nested_list:
    if(len(sentence) <= max_len):
        count = count + 1
  print('전체 샘플 중 길이가 %s 이하인 샘플의 비율: %s'%(max_len, (count / len(nested_list))*100))

max_len = 30
below_threshold_len(max_len, X_train)

# 전체 훈련 데이터 중 약 94%의 리뷰가 30이하의 길이를 가지는 것을 확인했습니다. 모든 샘플의 길이를 30으로 맞추겠습니다.

x_train = pad_sequences(x_train, maxlen=max_len)
x_test = pad_sequences(x_test, maxlen=max_len)
print(x_train[:10])

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
전체 샘플 중 길이가 30 이하인 샘플의 비율: 93.19852941176471
[[    0     0     0     0     0     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0     0     0
      0     0   448    21   256   651]
 [    0     0     0     0     0     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0   921   450    46   594
      3   211  1427    29   666    24]
 [    0     0     0     0     0     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0   380  2404
      1  2224  5608     4   219    14]
 [    0     0     0     0     0     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0  6416   105
   7631   214    59     9    31  3551]
 [    0     0     0     0     0     0     0     0     0     0     0     0
   1006     1    34  9060    29     5   819     3  2538    26  1089   237
  14101     1     5  1059   249   237]
 [    0     0     0     0     0     0     0     0     0     0     0     0
      0     0   709  5609   979  1366   424   141  1670  1605 11443   222
      3   124  1068     7    50   241]
 [    0     0     0     0     0     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0     0     0
      0   210   307     6   316   474]
 [  121  1562     2   361   118   223    15   790    22   569   566   511
    468  3073  8038    19  1367  1367     2    42   278     7     9    29
     40    45    19   695  1053    70]
 [    0     0     0     0     0     0     0     0     0     0     0     0
      0     0     0     0     0     0     0     0     0     0    94     2
      9    59    11   361    97     3]
 [    0     0     0     0     0     0     0     0     0     0     0     0
      0     0     0     0  1510    31     2   198   528    85    19   387
   1418   354   658    13  5610    11]]

 

모델적용

# model
from keras.layers import Embedding, Dense, LSTM
from keras.models import Sequential
from keras.models import load_model
from keras.callbacks import EarlyStopping, ModelCheckpoint

embedding_dim = 100
hidden_units = 128

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

es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=4)
mc = ModelCheckpoint('best_model.h5', monitor='val_acc', mode='max', verbose=1, save_best_only=True)

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
history = model.fit(x_train, y_train, epochs=15, callbacks=[es, mc], batch_size=64, validation_split=0.2)

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

Epoch 9/15
1816/1816 [==============================] - ETA: 0s - loss: 0.1952 - acc: 0.9239
Epoch 9: val_acc did not improve from 0.85735
1816/1816 [==============================] - 155s 85ms/step - loss: 0.1952 - acc: 0.9239 - val_loss: 0.3895 - val_acc: 0.8467
Epoch 9: early stopping

 

예측 함수 만들기

def sentiment_predict(new_sentence):
  new_sentence = re.sub(r'[^ㄱ-ㅎㅏ-ㅣ가-힣 ]','', new_sentence)
  new_sentence = okt.morphs(new_sentence, stem=True) # 토큰화
  new_sentence = [word for word in new_sentence if not word in stopwords] # 불용어 제거
  encoded = tok.texts_to_sequences([new_sentence]) # 정수 인코딩
  pad_new = pad_sequences(encoded, maxlen = max_len) # 패딩
  score = float(model.predict(pad_new)) # 예측
  if(score > 0.5):
    print("{:.2f}% 확률로 긍정 리뷰입니다.\n".format(score * 100))
  else:
    print("{:.2f}% 확률로 부정 리뷰입니다.\n".format((1 - score) * 100))

 

 

예측해보기

sentiment_predict('이 영화 개꿀잼 ㅋㅋㅋ')
sentiment_predict('이 영화 핵노잼 ㅠㅠ')
sentiment_predict('이딴게 영화냐 ㅉㅉ')
sentiment_predict('감독 뭐하는 놈이냐?')
sentiment_predict('와 개쩐다 정말 세계관 최강자들의 영화다')

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
92.77% 확률로 긍정 리뷰입니다.
98.74% 확률로 부정 리뷰입니다.
99.94% 확률로 부정 리뷰입니다.
99.77% 확률로 부정 리뷰입니다.
58.45% 확률로 부정 리뷰입니다.
728x90