본문 바로가기
Study

[NLP] 9. Sentiment Analysis

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

 

  • 감정 분석은 SNS, 리뷰 분석에 유용하게 사용할 수 있다.
  • 특정 이슈에 대한 사람들의 감정을 실시간으로 분석한다면, 그에 대해 신속하게 대처가 가능하다.

 

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

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

감정 사전 준비

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

데이터 준비

  • Scikit-learn에 내장되어 있는 뉴스그룹 데이터를 사용한다.
from sklearn.datasets import fetch_20newsgroups

newsdata=fetch_20newsgroups(subset='train')
newsdata.data[0]
-----------------------------------------------
From: lerxst@wam.umd.edu (where's my thing)\nSubject: WHAT car is this!?\nNntp-Posting-Host: rac3.wam.umd.edu\nOrganization: University of Maryland, College Park\nLines: 15\n\n I was wondering if anyone out there could enlighten me on this car I saw\nthe other day. It was a 2-door sports car, looked to be from the late 60s/\nearly 70s. It was called a Bricklin. The doors were really small. In addition,\nthe front bumper was separate from the rest of the body. This is \nall I know. If anyone can tellme a model name, engine specs, years\nof production, where this car is made, history, or whatever info you\nhave on this funky looking car, please e-mail.\n\nThanks,\n- IL\n   ---- brought to you by your neighborhood Lerxst ----\n\n\n\n\n

감정 상태 분류 및 시각화

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

afinn=Afinn()
for i in range(10):
  print(afinn.score(newsdata.data[i]))
--------------------------------------
7.0
11.0
16.0
5.0
-23.0
-25.0
7.0
3.0
16.0
-20.0

 

  • 모든 뉴스에 대한 감정을 시각화
  • 긍정과 부정에 대한 개수를 시각화
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'])
plt.show()

긍정, 부정, 중립에 대한 막대그래프


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

  • 한국어 자연어 처리 konlpy와 형태소 분석기 MeCab 설치
!set -x \
&& pip install konlpy \
&& curl -s https://raw.githubusercontent.com/konlpy/konlpy/master/scripts/mecab.sh | bash -x
import re
import urllib.request
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('seaborn-white')

from konlpy.tag import Mecab
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences

네이버 영화 리뷰 데이터

데이터 로드

  • 데이터를 웹에서 바로 받아오기 위해 urllib.request를 사용한다.
  • 받아온 데이터를 dataframe으로 변환하고 데이터를 확인한다.
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)

train_data[:10]

네이버 영화 리뷰 dataframe


중복 및 결측치 처리

  • 데이터 개수를 확인한다.
  • 데이터에 중복이 존재한다면 이를 제거한다.
print(train_data['document'].nunique())
print(train_data['label'].nunique())

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

 

print(train_data.isnull().sum())

train_data=train_data.dropna(how='any')
---------------------------------------
id          0
document    1
label       0
dtype: int64

데이터 정제

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

train_data[:10]

정제한 dataframe

 

train_data['document'].replace('',np.nan,inplace=True)
print(len(train_data))
print(train_data.isnull().sum())
------------------------------------------------------
146182
id            0
document    391
label         0
dtype: int64
train_data=train_data.dropna(how='any')
print(len(train_data))
---------------------------------------
145791

 

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')

토큰화 및 불용어 제거

  • 단어들을 분리하고 불용어를 제거한다.
  • 불용어 사전
    • '의', '가', '이', '은', '들', '는', '좀', '잘' ,'걍', '과', '도', '를', '으로', '자', '에', '와', '한', '하다'
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[:1])
------------------------------------------------------------------------------------
[['아', '더', '빙', '진짜', '짜증', '나', '네요', '목소리']]

 

x_test=[]
for sentence in train_data['document']:
  x_test.append([word for word in mecab.morphs(sentence) if not word in stopwords])
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))
----------------------------------------------------------------------
전체 단어 수: 49946
빈도가 2 이하인 희귀 단어 수: 28320
희귀 단어 비율: 56.70123733632323
희귀 단어 등장 빈도 비율: 1.7606762208782198
vocab_size=words_cnt-rare_cnt+2
print(vocab_size)
-------------------------------
21628

 

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))
print(len(y_train))
--------------------------------------------------------------------------------
145380
145380

패딩

  • 리뷰의 전반적인 길이를 확인한다.
  • 모델의 입력을 위해 동일한 길이로 맞춰준다.
print('리뷰 최대 길이:',max(len(l) for l in x_train))
print('리뷰 평균 길이:',sum(map(len,x_train))/len(x_train))
----------------------------------------------------------
리뷰 최대 길이: 83
리뷰 평균 길이: 13.801382583574082

 

plt.hist([len(s) for s in x_train],bins=50)
plt.xlabel('Length of Samples')
plt.ylabel('Number of Samples')
plt.show()

Sample의 길이와 개수의 히스토그램 그래프

 

max_len=60

x_train=pad_sequences(x_train,maxlen=max_len)
x_test=pad_sequences(x_test,maxlen=max_len)

모델 구축 및 학습

  • 감정 상태 분류 모델을 선언하고 학습한다.
  • 모델은 일반적인 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()
--------------------------------------------------------
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding (Embedding)       (None, None, 100)         2162800   
                                                                 
 lstm (LSTM)                 (None, 128)               117248    
                                                                 
 dense (Dense)               (None, 1)                 129       
                                                                 
=================================================================
Total params: 2,280,177
Trainable params: 2,280,177
Non-trainable params: 0
_________________________________________________________________
history=model.fit(x_train,y_train,epochs=15,batch_size=60,validation_split=0.2)

시각화

hist_dic=history.history
loss=hist_dic['loss']
val_loss=hist_dic['val_loss']
acc=hist_dic['acc']
val_acc=hist_dic['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()

Accuracy Loss


감정 예측

def sentiment_predict(new_sentence):
  new_token=[word for word in mecab.morphs(new_sentence) if not word 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.52%)
배우 연기력이 대박입니다. -> 긍정(91.98%)
분위가가 어둡고 스토리가 복잡해요. -> 긍정(91.34%)

 

'Study' 카테고리의 다른 글

[Computer Vision] 학습 데이터 디렉토리 클래스 구축  (0) 2024.08.28
[NLP] 10. Named Entity Recognition  (0) 2024.08.25
[NLP] 8. 스팸 메일 분류  (0) 2024.08.25
[NLP] 7. Embedding  (0) 2024.08.21
[NLP] 6. Topic Modeling  (0) 2024.08.21