본문 바로가기
Computer/ML·DL·NLP

[이수안컴퓨터연구소] 감정 분석

by injeolmialmond 2022. 1. 31.

https://youtu.be/7GUoDHxN5NM

https://colab.research.google.com/drive/1CFBtnM5W7bGOp0SVhZeHymb78dMgeSBu?usp=sharing 

 

_10 감정 분석(Sentiment Analysis).ipynb

Colaboratory notebook

colab.research.google.com

 

감정 분석(Sentiment Analysis)

  • 감정 분석은 텍스트에 등장하는 단어들을 통해 어떤 감정이 드러나는지 분석하는 기법
  • 감정 분석은 오피니언 마이닝으로도 불리며, 텍스트에 담긴 의견, 태도 등을 알아보는데 유용한 기법
  • 감정 분석을 하기 위해선 미리 정의된 감정 어휘 사전이 필요
  • 감정 어휘 사전에 포함된 어휘가 텍스트에 얼마나 분포하는지에 따라 해당 텍스트의 감정이 좌우
  • 토픽 모델링이 텍스트의 주제를 찾아낸다면, 감정 분석은 텍스트의 의견을 찾아냄
  • 텍스트는 주제(토픽)와 의견(감정)의 결합으로 이루어졌다고 볼 수 있음

  • 감정 분석은 SNS, 리뷰 분석에 유용하게 사용할 수 있음
  • 특정 이슈에 대한 사람들의 감정을 실시간으로 분석한다면, 그에 대해 신속하게 대처 가능
 
  • 파이썬으로 감정 분석하는 방법은 크게 두 가지로 구분
    • 감정 어휘 사전을 이용한 감정 상태 분류
      • 미리 분류해둔 감정어 사전을 통해 분석하고자 하는 텍스트의 단어들을 사전에 기반해 분류하고, 그 감정가를 계산
      • 이 때 사용되는 감정어 사전에는 해당 감정에 해당되는 단어를 미리 정의해둬야 함
    • 기계학습을 이용한 감정 상태 분류
      • 분석 데이터의 일부를 훈련 데이터로 사용해 그로부터 텍스트의 감정 상태를 분류
      • 이 때 사용되는 훈련 데이터는 사용자가 분류한 감정 라벨이 포함되어 있어야 하며,
        이를 인공 신경망, 의사 결정 트리 등의 기계 학습 알고리즘을 사용하여 분류

 

1. 감정 어휘 사전을 이용한 감정 상태 분류

1.1. 감정 사전 준비

  • 감정 사전 라이브러리를 설치
  • afinn은 영어에 대한 긍정, 부정에 대한 감정 사전을 제공
$ pip install afinn

 

1.2. 데이터 준비

  1. 사용할 데이터를 구성
  2. 데이터는 사이킷런에 내장되어 있는 뉴스그룹 데이터를 이용
from sklearn.datasets import fetch_20newsgroups

newsdata = fetch_20newsgroups(subset='train')
newsdata.data[0]

1.3. 감정 상태 분류 및 시각화

  • 감정 사전을 구성하고 감정 스코어를 측정
  • afinn 라이브러리는 감정 사전과 더불어 편리하게 감정가를 계산할 수 있는 함수를 제공
# 감정사전 구성, 점수측정
from afinn import Afinn

afinn = Afinn()
for i in range(10):
    print(afinn.score(newsdata.data[i])) # 뉴스 당 점수 계산
  • 모든 뉴스에 대한 감정을 시각화
  • 긍정과 부정에 대한 갯수를 시각화
import numpy as np
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')

positive = 0
neutral = 0
negative = 0

for i in newsdata.data:
    score = afinn.score(i)
    if score > 0:
        positive += 1
    elif score == 0:
        neutral += 1
    else :
        negative += 1

plt.bar(np.arange(3), [positive, neutral, negative])
plt.xticks(np.arange(3), ['positive', 'neutral', 'negative'])

2. 기계학습을 이용한 감정 분석

2.0. 한국어 자연어 처리 konlpy와 형태소 분석기 MeCab 설치

- 저는 윈도우에서 Mecab을 사용하기 위해 eunjeon을 사용했습니다. 

관련 포스트 : [이수안컴퓨터연구소] 자연어 처리 Natural Language Processing 기초 2 한국어 NLP (Feat. Windows에서 Mecab 사용하기!) (tistory.com)

2.1. 네이버 영화 리뷰 데이터

2.1.1. 데이터 로드

받아온 데이터를 dataframe으로 변환하고 데이터를 확인

import re
import urllib.request
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')

from eunjeon import Mecab
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
# 네이버 영화 리뷰 데이터
train_file = urllib.request.urlopen('https://raw.githubusercontent.com/e9t/nsmc/master/ratings_train.txt')
test_file = urllib.request.urlopen('https://raw.githubusercontent.com/e9t/nsmc/master/ratings_test.txt')

train_data = pd.read_table(train_file)
test_data = pd.read_table(test_file)

2.1.2. 중복 및 결측치 처리

  • 데이터 개수 확인
  • 데이터에 중복이 존재한다면 이를 제거
# 중복, 결측치 제거
print(train_data['document'].nunique()) # 146182
print(train_data['label'].nunique()) # 2
train_data.drop_duplicates(subset=['document'], inplace=True)

print(train_data.isnull().sum())
# id          0
# document    1
# label       0
# dtype: int64

train_data = train_data.dropna(how='any')

2.1.3. 데이터 정제

  • 데이터에서 한글과 공백을 제외하고 모두 제거
# 데이터 정제
# 한글, 공백 제외하고 모두 제거
train_data['document'] = train_data['document'].str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣 ]', '')
train_data[:10]

숫자나 특수문자가 없어져서 단어끼리 붙은 것들이 보이는데, 이건 토큰화하면서 알아서 조정되니 걱정하지 말 것!

train_data['document'].replace('', np.nan, inplace=True)
print(len((train_data))) # 146182

print(train_data.isnull().sum())
# id            0
# document    391
# label         0
# dtype: int64

train_data = train_data.dropna(how='any')
print(len(train_data)) # 145791
# test_data도 동일하게 해줍니다
test_data.drop_duplicates(subset=['document'], inplace=True)
test_data['document'] = test_data['document'].str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣 ]', '')
test_data['document'].replace('', np.nan, inplace=True)
test_data = test_data.dropna(how='any')

 

2.1.4. 토큰화 및 불용어 제거

  • 단어들을 분리하고 불용어를 제거함
  • 불용어 사전: '의','가','이','은','들','는','좀','잘','걍','과','도','를','으로','자','에','와','한','하다'
# 토큰화, 불용어 제거
stopwords = ['의','가','이','은','들','는','좀','잘','걍','과','도','를','으로','자','에','와','한','하다']
mecab = Mecab()

X_train = []
for sentence in train_data['document']:
    X_train.append([word for word in mecab.morphs(sentence) if not word in stopwords])
print(X_train[:3]) # konlpy보다 정확한 것 같은데요?
#[['아', '더', '빙', '진짜', '짜증', '나', '네요', '목소리'], ['흠', '포스터', '보고', '초딩', '영화', '줄', '오버', '연기', '조차', '가볍', '지', '않', '구나'], ['너무', '재', '밓었', '다', '그래서', '보', '것', '을', '추천', '한다']]
X_test = []
for sentence in test_data['document']:
    X_test.append([word for word in mecab.morphs(sentence) if not word in stopwords])
print(X_test[:3])
# [['굳', 'ㅋ'], ['뭐', '야', '평점', '나쁘', '진', '않', '지만', '점', '짜리', '더더욱', '아니', '잖아'], ['지루', '하', '지', '않', '은데', '완전', '막장', '임', '돈', '주', '고', '보', '기']]
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)

 

2.1.5. 빈도 수가 낮은 단어 제거

  • 빈도 수가 낮은 단어는 학습에 별로 영향을 주지 않음
  • 처리를 통해 빈도 수가 낮은 단어들은 제거
# 빈도수 낮은 단어 제거
threshold = 3
words_cnt = len(tokenizer.word_index)
rare_cnt = 0
words_freq = 0
rare_freq = 0

for key, value in tokenizer.word_counts.items():
    words_freq = words_freq + value
    if value < threshold :
        rare_cnt += 1
        rare_freq = rare_freq + value
        
print('전체 단어 수:', words_cnt)
print('빈도가 {} 이하인 희귀 단어 수 : {}'.format(threshold-1, rare_cnt))
print('희귀 단어 비율 : {}'.format(rare_cnt/words_cnt*100))
print('희귀 단어 등장 빈도 비율 : {}'.format((rare_freq/words_freq)*100))

# 전체 단어 수: 49590
# 빈도가 2 이하인 희귀 단어 수 : 28110
# 희귀 단어 비율 : 56.684815486993344
# 희귀 단어 등장 빈도 비율 : 1.7454099037762576
vocab_size = words_cnt - rare_cnt + 2 # 2 : 0번째 padding, out of vocabulary token
print(vocab_size) # 21482
tokenizer = Tokenizer(vocab_size, oov_token='OOV')
tokenizer.fit_on_texts(X_train)
X_train = tokenizer.texts_to_sequences(X_train)
X_test = tokenizer.texts_to_sequences(X_test)
y_train = np.array(train_data['label'])
y_test = np.array(test_data['label'])
drop_train = [index for index, sentence in enumerate(X_train) if len(sentence) < 1]

X_train = np.delete(X_train, drop_train, axis = 0)
y_train = np.delete(y_train, drop_train, axis = 0)

print(len(X_train)) # 145380

 

 

2.1.6. 패딩

  • 리뷰의 전반적인 길이를 확인
  • 모델의 입력을 위해 동일한 길이로 맞춰줌
# 패딩 : 리뷰의 길이 맞춰주기
print('리뷰 최대 길이 :', max(len(l) for l in X_train))
print('리뷰 평균 길이 :', sum(map(len, X_train)) / len(X_train))

# 리뷰 최대 길이 : 83
# 리뷰 평균 길이 : 13.805839867932315
plt.hist([len(s) for s in X_train], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

리뷰 길이 분포표

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

 

2.1.7. 모델 구축 및 학습

  • 감정 상태 분류 모델을 선언하고 학습
  • 모델은 일반적인 LSTM 모델을 사용
# 모델 구축 및 학습
from tensorflow.keras.layers import Embedding, Dense, LSTM
from tensorflow.keras.models import Sequential
model = Sequential()
model.add(Embedding(vocab_size, 100))
model.add(LSTM(128))
model.add(Dense(1, activation='sigmoid'))

model.compile(optimizer='rmsprop', loss='binary_crossentropy', metrics=['acc'])
model.summary()

2.1.8. 시각화

model.evaluate(X_test, y_test) # [0.49036523699760437, 0.8441269397735596]
# 시각화
hist_dict = history.history
loss = hist_dict['loss']
val_loss = hist_dict['val_loss']
acc = hist_dict['acc']
val_acc = hist_dict['val_acc']

plt.plot(loss, 'b--', label='training loss')
plt.plot(val_loss, 'r:', label='validation loss')
plt.legend()
plt.grid()

plt.figure()
plt.plot(acc, 'b--', label='training accuracy')
plt.plot(val_acc, 'r:', label='validation accuracy')
plt.legend()
plt.grid()

plt.show()

 

2.1.9. 감정 예측

# 감정 예측
def sentiment_predict(new_sentence):
    new_token = [word for word in mecab.morphs(new_sentence) if word not in stopwords]
    new_sequences = tokenizer.texts_to_sequences([new_token])
    new_pad = pad_sequences(new_sequences, maxlen=max_len)
    score=float(model.predict(new_pad))
    
    if score > 0.5:
        print("{} -> 긍정({:.2f}%)".format(new_sentence, score*100))
    else:
        print("{} -> 부정({:.2f}%)".format(new_sentence, (1-score)*100))
sentiment_predict('정말 재미있고 흥미진진 했어요.')
sentiment_predict('어떻게 이렇게 지루하고 재미없죠?')
sentiment_predict('분위기가 어둡고 스토리가 어려워요')
sentiment_predict('배우 연기력이 대박입니다')

# 정말 재미있고 흥미진진 했어요. -> 긍정(99.88%)
# 어떻게 이렇게 지루하고 재미없죠? -> 부정(99.98%)
# 분위기가 어둡고 스토리가 어려워요 -> 부정(89.66%)
# 배우 연기력이 대박입니다 -> 긍정(95.29%)

2.2. 네이버 쇼핑 리뷰 데이터

2.2.1. 훈련 데이터와 테스트 데이터 분리

# 네이버 쇼핑 리뷰 데이터
# 데이터 로드
urllib.request.urlretrieve("https://raw.githubusercontent.com/bab2min/corpus/master/sentiment/naver_shopping.txt", filename="shopping.txt")
total_data = pd.read_table('shopping.txt', names=['ratings', 'reviews'])
print(len(total_data)) # 200000

# 훈련, 테스트 데이터 분리
# 3점 초과면 1, 이하면 0
total_data['label'] = np.select([total_data.ratings > 3], [1], default=0)

total_data['ratings'].nunique(), total_data['reviews'].nunique(), total_data['label'].nunique() # (4, 199908, 2)
total_data.drop_duplicates(subset=['reviews'], inplace=True)
print(len(total_data)) # 199908
# 학습 데이터 나누기
from sklearn.model_selection import train_test_split

train_data, test_data = train_test_split(total_data, test_size=0.25, random_state=111)
print(len(train_data), len(test_data)) # 149931 49977

2.2.2. 레이블의 분포 확인

# 레이블의 분포 확인
train_data['label'].value_counts().plot(kind='bar')

train_data.groupby('label').count() # 거의 균등!

2.2.3. 데이터 정제

# 데이터 정제
train_data['reviews'] = train_data['reviews'].str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣 ]', '')
train_data['reviews'].replace('', np.nan, inplace=True)
print(len(train_data)) # 149931
test_data['reviews'] = test_data['reviews'].str.replace('[^ㄱ-ㅎㅏ-ㅣ가-힣 ]', '')
test_data['reviews'].replace('', np.nan, inplace=True)
print(len(test_data)) # 49977

 

 

2.2.4. 토큰화 및 불용어 제거

  • 불용어 사전: '도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게'
# 토큰화, 불용어 제거
stopwords = ['도', '는', '다', '의', '가', '이', '은', '한', '에', '하', '고', '을', '를', '인', '듯', '과', '와', '네', '들', '듯', '지', '임', '게']
mecab = Mecab()
train_data['reviews'] =  train_data['reviews'].apply(mecab.morphs)
train_data['reviews'] =  train_data['reviews'].apply(lambda x : [item for item in x if item not in stopwords])
test_data['reviews'] =  test_data['reviews'].apply(mecab.morphs)
test_data['reviews'] =  test_data['reviews'].apply(lambda x : [item for item in x if item not in stopwords])
X_train = train_data['reviews'].values
y_train = train_data['label'].values
X_test = test_data['reviews'].values
y_test = test_data['label'].values

print(X_train.shape, y_train.shape, X_test.shape, y_test.shape) # (149931,) (149931,) (49977,) (49977,)

 

2.2.5. 빈도 수가 낮은 단어 제거

  • 빈도 수가 낮은 단어는 학습에 별로 영향을 주지 않음
  • 처리를 통해 빈도 수가 낮은 단어들은 제거
# 빈도수 낮은 단어 제거
tokenizer = Tokenizer()
tokenizer.fit_on_texts(X_train)
threshold = 3
words_cnt = len(tokenizer.word_index)
rare_cnt = 0
words_freq = 0
rare_freq = 0

for key, value in tokenizer.word_counts.items():
    words_freq = words_freq + value
    if value < threshold :
        rare_cnt += 1
        rare_freq = rare_freq + value
        
print('전체 단어 수:', words_cnt)
print('빈도가 {} 이하인 희귀 단어 수 : {}'.format(threshold-1, rare_cnt))
print('희귀 단어 비율 : {}'.format(rare_cnt/words_cnt*100))
print('희귀 단어 등장 빈도 비율 : {}'.format((rare_freq/words_freq)*100))
# 전체 단어 수: 39733
# 빈도가 2 이하인 희귀 단어 수 : 22902
# 희귀 단어 비율 : 57.63974529987668
# 희귀 단어 등장 빈도 비율 : 1.2105580649176668
vocab_size = words_cnt - rare_cnt + 2
print(vocab_size) # 16833
tokenizer = Tokenizer(vocab_size, oov_token = 'OOV')
tokenizer.fit_on_texts(X_train)
X_train = tokenizer.texts_to_sequences(X_train)
X_test = tokenizer.texts_to_sequences(X_test)

2.2.6. 패딩

# 패딩 : 리뷰의 길이 맞춰주기
print('리뷰 최대 길이 :', max(len(l) for l in X_train))
print('리뷰 평균 길이 :', sum(map(len, X_train)) / len(X_train))

# 리뷰 최대 길이 : 85
# 리뷰 평균 길이 : 15.324502604531418
plt.hist([len(s) for s in X_train], bins=50)
plt.xlabel('length of samples')
plt.ylabel('number of samples')
plt.show()

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

 

2.2.7. 모델 구축 및 학습

# 모델 구축 및 학습
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding , Dense, GRU
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
model = Sequential()
model.add(Embedding(vocab_size, 100))
model.add(GRU(128))
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=60, validation_split=0.2)
# epoch=8에서 early stopping

 

2.2.8. 시각화

from keras.models import load_model
loaded_model = load_model('best_model.h5')
loaded_model.evaluate(X_test, y_test) # [0.20839178562164307, 0.9261460304260254]
# 시각화
hist_dict = history.history
loss = hist_dict['loss']
val_loss = hist_dict['val_loss']
acc = hist_dict['acc']
val_acc = hist_dict['val_acc']

plt.plot(loss, 'b--', label='training loss')
plt.plot(val_loss, 'r:', label='validation loss')
plt.legend()
plt.grid()

plt.figure()
plt.plot(acc, 'b--', label='training accuracy')
plt.plot(val_acc, 'r:', label='validation accuracy')
plt.legend()
plt.grid()

plt.show()

 

2.2.9. 감정 예측

# 감정 예측
def sentiment_predict(new_sentence):
    new_token = [word for word in mecab.morphs(new_sentence) if word not in stopwords]
    new_sequences = tokenizer.texts_to_sequences([new_token])
    new_pad = pad_sequences(new_sequences, maxlen=max_len)
    score=float(loaded_model.predict(new_pad))
    
    if score > 0.5:
        print("{} -> 긍정({:.2f}%)".format(new_sentence, score*100))
    else:
        print("{} -> 부정({:.2f}%)".format(new_sentence, (1-score)*100))
sentiment_predict('처음 써봤는데 대박 좋아요.')
sentiment_predict('원래 배송이 이렇게 늦나요?')
sentiment_predict('좋은 거 인정! 추가 구매 의향 있습니다.')
sentiment_predict('이건 정말 리뷰 쓰는 게 아깝네요.')

# 처음 써봤는데 대박 좋아요. -> 긍정(97.79%)
# 원래 배송이 이렇게 늦나요? -> 부정(97.71%)
# 좋은 거 인정! 추가 구매 의향 있습니다. -> 긍정(95.44%)
# 이건 정말 리뷰 쓰는 게 아깝네요. -> 부정(96.03%)

 

댓글