텍스트 마이닝을 활용하여 문자 대화내용 분석

NSMC 감정분석하기

HeyTeddy 2023. 4. 25. 17:12
반응형

감정분석의 정의는 무엇일까?

  • 데이터 만드는 자, 데이터 제공자, 데이터 분석가의 주관에 따라 감정이 다르다.
  • 부정/중립/긍정
  • 일반적으로 텍스트 안에 있는 의미를 뽑아낸다.
  • 감정 분석은 디지털 텍스트를 분석하여 메시지의 감정적 어조가 긍정적인지, 부정적인지 또는 중립적인지를 확인하는 프로세스
  • 오늘날 회사는 이메일, 고객 지원 채팅 트랜스크립트, 소셜 미디어 댓글 및 리뷰와 같은 대량의 텍스트 데이터를 보유하고 있습니다. 감정 분석 도구는 이 텍스트를 스캔하여 주제에 대한 글쓴이의 태도를 자동으로 확인할 수 있습니다. 기업은 감정 분석의 인사이트를 활용하여 고객 서비스를 개선하고 브랜드 평판을 높입니다.
  • 크게 감정분석은 Knowledge-based approach, Machine Learning-based approach가 있다.
    • Knowledge-based approach : 알려진 어구, 어미, 관용 표현등을 활용하여 이미 문서들을 human expert가 평가한 데이터를 가져와 평가하는 방법이다.
    • ML-based approach는 supervised, unsupervised 방법이 있다.

오피니언 마이닝

  • 감정분석 중 하나로 기업이 제품과 서비스를 개선하는데 도움이 되는 중요한 BI(Business Intelligence)이다.

텍스트 분류

  • 벡터 형태로 표현된 텍스트를 분류하는 방법
  • 현재 CNN, LSTM을 분류 모델로 가장 많이 사용한다.
  • 실제로 텍스트 분류 작업은 큰 범위에서 대부분의 NLP downstream task를 포함한다.
  • 하나의 예로, Siamese Network라는 걸 통해서 Question-Answering pair를 학습하게 되면, 분류의 기준이 "특정 질문에 맞는 정답을 잘 골랐는가 아닌가(0 / 1)"를 해결하는 문제로 바뀌게 된다.

따라서, 가장 많이 쓰이는 방법인 text classification으로서의 sentiment analysis를 공부해보고, 추가적으로 생각해볼 여러 가지 이슈에 대해 고민해보는 시간을 가진다.

 

scikit-learn으로 NSMC 감정분석을 해보았다. 전에 포스팅했던 형태소 분석 코드를 가져온 후, NSMC를 불러온다.

def read_documents(filename):
    with open(filename, encoding='utf-8') as f:
        documents = [line.split('\t') for line in f.read().splitlines()]
        documents = documents[1:]
        
    return documents
    
train_docs = read_documents("data/nsmc/ratings_train.txt")
test_docs = read_documents("data/nsmc/ratings_test.txt")
print(len(train_docs))
print(len(test_docs))

# 결과

150000
50000

ratings_train.txt에 150000개 리뷰데이터 존재, ratings_test.txt에 50000개 리뷰데이터 존재한다.

칼럼에는 (아이디, 리뷰, 라벨)로 분류되고, 라벨에 0은 부정(Negative), 1은 긍정(Positive)를 뜻한다.

def text_cleansing(doc): # 한국어 제외하고 글자를 제거하는 함수
    doc = re.sub("[^ㄱ-ㅎㅏ-ㅣ가-힣]", "", doc)
    
    return doc

def define_stopwords(path):
    SW = set()
    
    with open(path, encoding='utf-8') as f:
        for word in f:
            SW.add(word)
            
    return SW

def text_tokenizing(doc):
    
    return [word for word in mecab.morphs(doc) if word not in SW and len(word) > 1]
from konlpy.tag import Mecab
# from konlpy.tag import Okt
import json
import os
import re
from pprint import pprint
import MeCab

# okt = Okt()
mecab = MeCab.Tagger()

SW = define_stopwords("data/stopwords-ko.txt")

if os.path.exists('train_docs.json'): # 경로가 존재하면 True/False
    with open("train_docs.json", encoding='utf-8') as f: # 불러오기
        train_data = json.load(f)
else: # 토큰화 후, 정제작업까지 진행
    # train_data를 2가지 버전으로 만들기. 1) 정제하기 2) 정제 안하기
    # train_data = [(text_tokenizing(line[1]), line[2]) for line in train_docs if text_tokenizing(line[1])] # 리뷰, 레이블 
    train_data = [(text_tokenizing(text_cleansing(line[1])), line[2]) for line in train_docs if text_tokenizing(text_cleansing(line[1]))]
    
    with open("train_docs.json", 'w', encoding='utf-8') as f:
        json.dump(train_data, f, ensure_ascii=False, indent='\t')

if os.path.exists('test_docs.json'):
    with open("test_docs.json", encoding='utf-8') as f:
        test_data = json.load(f)
else:
    test_data = [(text_tokenizing(text_cleansing(line[1])), line[2]) for line in test_docs if text_tokenizing(text_cleansing(line[1]))]
    
    with open("test_docs.json", 'w', encoding='utf-8') as f:
        json.dump(test_data, f, ensure_ascii=False, indent='\t')

pprint(train_data[0]) # pretty print
pprint(test_data[0])

여기서 중요한 점은 cleansing을 먼저하고 tokenizing을 진행해야 더 빠르다. 결과는 같지만 속도 면에서 cleansing을 먼저 하는게 좋다.

 

print(train_data[:3])
[[['진짜', '짜증', '네요', '목소리'], '0'],
[['..', '포스터', '보고', '초딩', '영화', '...', '오버', '연기', '조차', '가볍', '구나'], '1'],
[['너무', '밓었다그래서보는것을추천한다'], '0']]

cleansing을 진행하고 tokenizing을 한 결과값을 보면 토큰화가 잘 이루어진 경우도 있지만, 세번째 리뷰는 오타로 인해 잘 분류되지 않았다. 라벨은 0(부정)이지만 '추천한다'는 의미가 있어서 긍정으로 보이며 잘 분류되지 않은 것처럼 보인다.

 

cleansing 후 토큰의 개수를 살펴보았다.

print(len(train_data)) # 정제 후 150000개 -> 148051개, 1949개 줄어듦
print(len(test_data))  # 정제 후 50000개 -> 49359개, 641개 줄어듦

 

이제 NLTK를 활용하여 nsmc 텍스트 정제 후, 히스토그램 시각화하여 단어 분포를 파악해보았다.

NLTK

  • 자연어 처리 및 분석을 위한 파이썬 패키지이다.
  • 토큰생성, 형태소 분석, 품사 태깅 등 다양한 기능을 제공한다.
import nltk

total_tokens = [token for doc in train_data for token in doc[0]]
print(len(total_tokens)) # 총 토큰 개수
1206841
text = nltk.Text(total_tokens, name='NMSC')
print(len(set(text.tokens)))
pprint(text.vocab().most_common(10))
51722
[('영화', 57614),
 ('..', 22813),
 ('는데', 11543),
 ('너무', 11002),
 ('정말', 9783),
 ('으로', 9322),
 ('네요', 9053),
 ('재밌', 9022),
 ('지만', 8366),
 ('진짜', 8326)]

훈련데이터에 1206841개 토큰이 있고, set함수를 사용하여 중복된 토큰을 제거한 토큰은 51722개가 존재한다. 그리고 토큰 빈도수가 높은 상위 10개를 출력하였다. 결과값이 그리 탐탁지 못하고 큰 의미를 부여하지 않게 나왔다.

여기서 nltk.Text()는 자연어 데이터의 탐색을 편리하게 해주는 기능을 제공해준다.

 

주어진 결과를 시각화해보았다.

import matplotlib.pyplot as plt
import platform
from matplotlib import font_manager, rc
%matplotlib inline

path = "c:/Windows/Fonts/malgun.ttf"
if platform.system() == 'Darwin':
    rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
    font_name = font_manager.FontProperties(fname=path).get_name()
    rc('font', family=font_name)
else:
    print('Unknown system')

plt.figure(figsize=(16, 10))
text.plot(50)

 

선형 분류(Linear Classifier)와 서포트벡터머신(SVM)으로 NSMC 분류작업을 해보았다.

from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import SGDClassifier
from sklearn.svm import SVC
from sklearn.naive_bayes import MultinomialNB
import numpy as np

# 학습을 위해 데이터셋 모양 통일하기
x_train = [list_to_str(doc) for doc, _ in train_data]
x_test = [list_to_str(doc) for doc, _ in test_data]
y_train = [label for _, label in train_data]
y_test = [label for _, label in test_data]

# 학습 모델 파이프라인
# linear classifier(선형 분류)
learner = Pipeline([
    ('vect', CountVectorizer()),
    ('clf', SGDClassifier(loss='perceptron', penalty='l2',
                          alpha=1e-4, random_state=42,
                          max_iter=100))
])

# SVM Linear Kernel
learner2 = Pipeline([
    ('vect', CountVectorizer()),
    ('clf', SVC(kernel="linear"))
])

# SVM with polynomial kernel(다항 커널)
learner3 = Pipeline([
    ('vect', CountVectorizer()),
    ('clf', SVC(kernel="poly", degree=8)) # 8차원
])

# SVM with Radius Basis Function kernel(가우시안 커널)
learner4 = Pipeline([
    ('vect', CountVectorizer()),
    ('clf', SVC(kernel="rbf"))
])

# SVM with sigmoid kernel
learner5 = Pipeline([
    ('vect', CountVectorizer()),
    ('clf', SVC(kernel='sigmoid'))
])

# Naive Bayes Classifier
learner6 = Pipeline([
    ('vect', CountVectorizer()),
    ('clf', MultinomialNB())
])

# 학습기
classifier = learner

# 학습하기
classifier.fit(x_train, y_train)
train_predict = classifier.predict(x_train)
train_accuracy = np.mean(train_predict == y_train)

test_predict = classifier.predict(x_test)
test_accuracy = np.mean(test_predict == y_test)

# 결과
print("For %d test data" % len(x_test))
print("Training Accuracy : %.2f" % train_accuracy)
print("Test Accuracy : %.2f" % test_accuracy)
For 49359 test data
Training Accuracy : 0.84
Test Accuracy : 0.77

머신러닝에서 커널(kernel)은 결정 경계를 선형으로 만들기 어려운 상황에서 원본데이터를 고차원 매핑(변환)하여 선형 분류를 할 때 사용하는 기법이다. 어떤 커널을 써야 성능이 가장 좋은지는 알 수 없다. 그래서 linear classifier, SVM linear, SVM polynomial kernel, SVM RBF kernel, SVM sigmoid kernel, Naive Bayes Classifier을 불러왔다. 학습할 때 학습기만 변경하면서 학습하면 된다. 선형 분류 학습을 제외한 학습을 할 때, 시간이 꽤 오래걸린다는 점 참조하길 바란다.

 

위 코드는 선형 분류 커널을 이용하여 학습한 결과, 훈련데이터 정확도와 테스트데이터 정확도를 비교해보았다.

SVM Linear kernel을 돌려보면 0.81정도 측정되고 Naive Bayes를 사용하면 0.84정도로 정확도가 점점 높아진다.여러 커널을 사용해보면서 어떻게 하면 성능을 높일 수 있는지 고민해보면 좋을거 같다. 

반응형