텍스트 분석이란?
이전 내용
감성 분석(Sentiment Analysis)
◆ 감성 분석:
문서의 주관적인 감성/의견/감정/기분 등을 파악하기 위한 방법으로 소셜 미디어, 여론조사, 온라인 리뷰, 피드백 등 다양한 분야에서 활용되고 있으며, 문서 내 텍스트가 나타내는 여러 가지 주관적인 단어와 문맥을 기반으로 감성(Sentiment) 수치를 계산하는 방법을 이용한다.
감성 지수는 긍정 감성 지수와 부정 감성 지수로 구성되며 이들 지수를 합산해 긍정 감성 또는 부정 감성을 결정한다.
[감성 분석의 지도 학습, 비지도 학습]
- 지도학습: 학습 데이터와 타깃 레이블 값을 기반으로 감성 분석 학습을 수행한 뒤 이를 기반으로 다른 데이터의 감성 분석을 예측하는 방법으로 일반적인 텍스트 기반의 분류와 거의 동일. (텍스트 기반의 이진 분류)
- 비지도학습: 'Lexicon' 이라는 일종의 감성 어휘 사전을 이용하여 문서의 긍정적, 부정적 감성 여부 판단.
※ Lexicon: 감성 분석을 위한 용어와 문맥에 대한 다양한 정보를 가지고 있는 어휘 사전. 주로 감성만을 분석하기 위해 지원하는 감성 어휘 사전이다. 감성 사전은 긍정 감성 또는 부정 감성의 정도를 의미하는 수치를 가지고 있으며 이를 감성 지수(Polarity Score) 라고 한다.
감성 지수는 단어의 위치나 주변 단어, 문맥, POS(Part of Speech) 등을 참고해 결정된다.
NLTK는 감성 사전을 구현한 대표적인 패키지이며, 많은 서브 모듈을 가지고 있는데 그중 Lexicon 모듈도 포함되어 있다.
지도학습 기반 감성 분석 - IMDB 영화평
IMDB의 영화 사이트의 영화평의 텍스트를 분석해 감성 분석 결과가 긍정 또는 부정인지를 예측하는 모델을 만들어 본다.
하단의 링크 (캐글)에 접속하여 데이터를 다운로드 한다. (캐글 로그인 필수, 캐글 경연 규칙 준수 필수)
※ 'labeledTrainData.tsv.zip' 파일을 다운로드 한 뒤, 압축을 푼다. 참고로, tsv 파일은 탭(\t) 문자로 분리된 파일이다.
[주피터 노트북에서 파일 로드하기]
tsv 파일은 판다스의 reas_csv() 를 이용하면 탭으로 칼럼이 분리된 파일도 DataFrame으로 쉽게 로딩할 수 있는데, read_csv()의 인자로 sep="\t"를 명시해주면 된다.
import pandas as pd
review_df = pd.read_csv("../Data/08. labeledTrainData.tsv", header=0, sep="\t", quoting=3)
review_df.head()
데이터 필드는 다음과 같다.
- id - 각 리뷰의 고유 ID
- 감정 - 리뷰의 감정; 긍정적인 리뷰는 1, 부정적인 리뷰는 0
- 리뷰 - 리뷰의 텍스트
텍스트 구성을 보기 위해 review 칼럼의 텍스트 값을 하나만 살펴본다.
print(review_df['review'][0])
[텍스트 사전 처리]
- HTML 형식의 <br /> 태그 문자열 삭제: replace()를 str에 적용해 <br /> 태그를 모두 공백으로 변환
- 영어가 아닌 숫자/특수문자: 감성분석을 위한 피처로는 별 의미 없어 보이므로 정규 표현식을 사용하여 공란으로 변경
import re
# <br> html 태그는 공백으로
review_df['review'] = review_df['review'].str.replace('<br />', ' ')
# 파이썬의 정규표현식 모듈 re를 이용해 영어 문자열이 아닌 문자는 모두 공백으로 변환
review_df['review'] = review_df['review'].apply(lambda x : re.sub("[^a-zA-Z]", " ", x))
※ 정규 표현식 관련 글
※ lambda 식 관련 글
[학습용과 테스트용 데이터 세트 분리 작업]
결정 값 클래스인 sentiment 칼럼을 별도로 추출해 결정 값 데이터 세트를 만들고, 원본 데이터 세트에서 id와 sentiment 칼럼을 삭제해 피처 데이터 세트를 생성한다. 그리고 train_test_split()을 이용해 학습용과 테스트용 데이터 세트로 분리한다.
from sklearn.model_selection import train_test_split
class_df = review_df['sentiment']
feature_df = review_df.drop(['id', 'sentiment'], axis=1, inplace=False)
X_train, X_test, y_train, y_test = train_test_split(feature_df, class_df, test_size=0.3, random_state=156)
X_train.shape, X_test.shape
▶ 학습용 데이터는 17500개의 리뷰, 테스트용 데이터는 7500개의 리뷰로 구성되었다.
[피처 벡터화, ML 분류 알고리즘 적용 - 예측 성능 측정]
Pipeline 객체를 이용해 감상평(Review) 텍스트를 피처 벡터화한 후에 ML 분류 알고리즘을 적용해 예측 성능을 측정하는 작업을 한 번에 진행한다.
- Count 벡터화 적용하여 예측 성능 측정
- TF-IDF 벡터화 적용하여 예측 성능 측정
- Classifier (분류기)는 LogisitcRegression 이용
- 예측 성능 평가는 이진 분류임을 고려해 테스트 데이터 세트의 정확도(accuracy score)와 ROC-AUC 측정
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, roc_auc_score
# 스톱 워드는 English, ngram은 (1,2)로 설정해 CountVectorization 수행
# LogisticRegression의 C는 10으로 설정
pipeline = Pipeline([
('cnt_vect', CountVectorizer(stop_words='english', ngram_range=(1,2) )),
('lr_clf', LogisticRegression(solver='liblinear', C=10))
])
# Pipeline 객체를 이용해 fit(), predict() 로 학습/예측 수행. predict_proba()는 roc_auc 를 위해 수행
pipeline.fit(X_train['review'], y_train)
pred = pipeline.predict(X_test['review'])
pred_probs = pipeline.predict_proba(X_test['review'])[:, 1]
print('예측 정확도는 {0:.4f}, ROC-AUC는 {1:.4f}'.format(accuracy_score(y_test, pred),
roc_auc_score(y_test, pred_probs)))
이번에는 TF-IDF 벡터화를 적용해 예측 성능을 측정해 본다.
# 스톱 워드는 English, ngram은 (1,2)로 설정해 TF-IDF 벡터화 수행
# LogisticRegression의 C는 10으로 설정
pipeline = Pipeline([
('cnt_vect', TfidfVectorizer(stop_words='english', ngram_range=(1,2) )),
('lr_clf', LogisticRegression(solver='liblinear', C=10))
])
# Pipeline 객체를 이용해 fit(), predict() 로 학습/예측 수행. predict_proba()는 roc_auc 를 위해 수행
pipeline.fit(X_train['review'], y_train)
pred = pipeline.predict(X_test['review'])
pred_probs = pipeline.predict_proba(X_test['review'])[:, 1]
print('예측 정확도는 {0:.4f}, ROC-AUC는 {1:.4f}'.format(accuracy_score(y_test, pred),
roc_auc_score(y_test, pred_probs)))
▶ TF-IDF 기반 피처 벡터화의 예측 성능이 보다 좋다.
비지도학습 기반 감성 분석 소개
대부분의 감성 분석용 데이터는 위의 실습처럼 결정된 레이블 값을 가지고 있지 않아 이러한 경우 Lexicon은 유용하게 사용될 수 있다. (단, 한글 버전은 없다)
NLTK에서 제공하는 WordNet 모듈은 시맨틱 분석을 제공하는 방대한 영어 어휘 사전이며, 다양한 상황에서 같은 어휘라도 다르게 사용되는 어휘의 시맨틱 정보를 제공하며, 이를 위해 각각의 품사(명사, 동사, 형용사, 부사 등)로 구성된 개별 단어를 Synset(Sets of cognitive synonyms)이라는 개념을 이용해 표현한다.
Synset은 단순한 하나의 언어가 아닌 그 단어가 가지는 문맥, 시맨틱 정보를 제공하는 WordNet의 핵심 개념이다.
※ 시맨틱(semantic): 문맥상 의미. 동일한 단어나 문장이라도 다른 환경과 문맥에서는 다르게 표현되거나 이해될 수 있으며, 언어학에서는 이런 시맨틱을 표현하기 위해서 여러 가지 규칙을 정해왔으며, NLP 패키지는 시맨틱을 프로그램적으로 인터페이스 할 수 있는 다양한 방법을 제공한다.
NLTK의 감성 사전이 감성에 대한 훌륭한 사전 역할을 제공한 장점은 있으나, 예측 성능은 그리 좋지 못하다는 단점이 있다. 그리하여 실제 업무의 적용은 NLTK 패키지가 아닌 다른 감성 사전을 적용하는 것이 일반적이며, 대표적인 감성 사전들은 아래와 같다.
- SentiWordNet: NLTK 패키지의 WordNet과 유사하게 감성 단어 전용의 WordNet 구현. (WordNet의 Synset 개념을 감성 분석에 적용) WordNet의 Synset 별로 3가지 감성 점수(긍정 감성 지수, 부정 감성 지수, 객관성 지수)를 할당하여 긍정 감성 지수와 최종 감성 지수를 합산하여 최종 감성 지수를 계산하고 이에 기반해 감성이 긍정인지 부정인지 결정.
- VADER: 주로 소셜 미디어의 텍스트에 대한 감성 분석을 제공하기 위한 패키지. 뛰어난 감성 분석 결과를 제공하며, 비교적 빠른 수행 시간을 보장해 대용량 텍스트 데이터에 잘 사용되는 패키지.
- Pattern: 예측 성능 측면에서 가장 주목받는 패키지.
※ 객관성 지수: 긍정/부정 감성 지수와 완전히 반대되는 개념으로 단어가 감성과 관계없이 얼마나 객관적인지를 수치로 나타낸 것.
[그 외에도 다양한 감성 분석을 위한 라이브러리 소개 글]
SentiWordNet을 이용한 감성 분석
SentiWordNet은 WordNet의 하위 모듈로서 감성 분석을 위한 다양한 프레임워크를 제공한다.
WordNet을 이용하기 위해서는 NLTK를 셋업한 후에 WordNet 서브패키지와 데이터 세트를 내려받아야 한다.
import nltk
nltk.download('all')
NLTK의 모든 데이터 세트를 내려받은 뒤에 WordNet 모듈을 임포트하여 'present' 단어에 대한 Synsets 추출. synsets()는 파라미터로 지정된 단어에 대해 WordNet에 등재된 모든 Synsets 객체를 반환한다.
from nltk.corpus import wordnet as wn
term = 'present'
# 'present' 라는 단어로 wordnet의 synsets 생성
synsets = wn.synsets(term)
print('synsets() 반환 type :', type(synsets))
print('synsets() 반환 값 개수 :', len(synsets))
print('synsets() 반환 값 :', synsets)
▶ 내용 분석
- synsets() 호출 시 반환되는 것은 여러 개의 Synset 객체를 가지는 리스트이다.
- 총 18개의 서로 다른 semantic을 가지는 synset 객체가 반환되었다.
- Synset('present.n.01')와 같이 Synset 객체의 파라미터 present.n.01은 POS 태그를 나타낸다.
- present.n.01 에서 present는 의미 / n은 명사 품사 / 01은 present가 명사로서 가지는 의미가 여러 가지 있어서 이를 구분하는 인덱스이다.
[synset 객체가 가지는 여러 가지 속성]
Synset은 POS(품사), 정의(Definition), 부명제(Lemma) 등으로 시맨틱적인 요소를 표현할 수 있다.
for synset in synsets:
print('### Synset name: ', synset.name(), '###')
print('POS :', synset.lexname())
print('Definition :', synset.definition())
print('Lemmas :', synset.lemma_names())
▶ synset은 하나의 단어가 가질 수 있는 여러 가지 시맨틱 정보를 개별 클래스로 나타낸 것이다.
[어휘 간의 유사도 표현]
synset 객체는 단어 간의 유사도를 나타내기 위해서 path_similarity() 메서드를 제공한다.
# synset 객체를 단어별로 생성
tree = wn.synset('tree.n.01')
lion = wn.synset('lion.n.01')
tiger = wn.synset('tiger.n.02')
cat = wn.synset('cat.n.01')
dog = wn.synset('dog.n.01')
entities = [tree, lion, tiger, cat, dog]
similarities = []
entity_names = [entity.name().split('.')[0] for entity in entities]
# 단어별 synset 반복하며 다른 단어의 synset과 유사도 측정
for entity in entities:
similarity = [round(entity.path_similarity(compared_entity), 2)
for compared_entity in entities]
similarities.append(similarity)
# 개별 단어별 sysnet 과 다른 단어의 synset과의 유사도를 DataFrame 형태로 저장
similarity_df = pd.DataFrame(similarities, columns=entity_names, index=entity_names)
similarity_df
- tree는 lion과 tiger와 유사도가 가장 낮고, dog와 유사도가 가장 높음.
- lion은 tree와 유사도가 가장 낮고, tiger와 유사도가 가장 높음.
- tiger는 tree와 유사도가 가장 낮고, lion과 유사도가 가장 높음.
- cat은 tree와 유사도가 가장 낮고, lion과 tiger와 유사도가 가장 높음.
- dog는 tree와 유사도가 가장 낮고, cat과 유사도가 가장 높음.
[SentiWordnet의 Senti_Synset 클래스]
SentiWordNet은 WordNet의 Synset과 유사한 Senti_Synset 클래스를 가지고 있어 synsets()와 비슷하게 Senti_Synset 클래스를 리스트 형태로 반환한다.
import nltk
from nltk.corpus import sentiwordnet as swn
senti_synsets = list(swn.senti_synsets('slow'))
print('senti_synset() 반환 type: ', type(senti_synsets))
print('senti_synset() 반환 값 개수 :', len(senti_synsets))
print('senti_synset() 반환 값 :', senti_synsets)
SentiSynset 객체는 단어의 감성을 나타내는 감성 지수와 객관성을 나타내는 객관성 지수를 가지고 있으며, 감성 지수는 다시 긍정 감성 지수와 부정 감성 지수로 나뉜다.
어떤 단어가 전혀 감성적이지 않으면 객관성 지수는 1이 되고, 감성 지수는 0이 된다.
['father' 와 'fabulous'의 감성 지수와 객관성 지수]
import nltk
from nltk.corpus import sentiwordnet as swn
father = swn.senti_synset('father.n.01')
print('father 긍정 감성 지수 :', father.pos_score())
print('father 부정 감성 지수 :', father.neg_score())
print('father 객관성 지수 :', father.obj_score())
print('\n')
fabulous = swn.senti_synset('fabulous.a.01')
print('fabulous 긍정 감성 지수 :', fabulous.pos_score())
print('fabulous 부정 감성 지수 :', fabulous.neg_score())
print('fabulous 객관성 지수 :', fabulous.obj_score())
SentiWordNet을 이용한 영화 감상평 감성 분석
해당 감성 분석의 대략적인 순서
- 문서(Document)를 문장(Sentence) 단위로 분해
- 다시 문장을 단어(Word) 단위로 토큰화하고 품사 태깅
- 품사 태깅된 단어 기반으로 synset 객체와 senti_synset 객체 생성
- senti_synset 객체에서 긍정/부정 감성 지수를 구하고 이를 모두 합산해 특정 임계치 값 이상일 때 긍정 감성으로, 그렇지 않으면 부정 감성으로 결정
[품사 태깅을 수행하는 내부 함수 생성]
from nltk.corpus import wordnet as wn
# 간단한 NLTK PennTreebank Tag를 기반으로 WordNet 기반의 품사 Tag로 변환
def penn_to_wn(tag):
if tag.startswith('J'):
return wn.ADJ
elif tag.startswith('N'):
return wn.NOUN
elif tag.startswith('R'):
return wn.ADV
elif tag.startswith('V'):
return wn.VERB
[문서 → 문장 → 단어 토큰 → 품사 태깅 후 Polarity Score를 합산하는 함수 생성]
품사 태깅 후에 SentiSynset 클래스를 생성하고 Polarity Score를 합산하는 함수를 생성한다. 각 단어의 긍정 감성 지수와 부정 감성 지수를 모두 합한 총 감성 지수가 0 이상일 경우 긍정 감성, 그렇지 않을 경우 부정 감성으로 예측한다.
from nltk.stem import WordNetLemmatizer
from nltk.corpus import sentiwordnet as swn
from nltk import sent_tokenize, word_tokenize, pos_tag
def swn_polarity(text):
# 감성 지수 초기화
sentiment = 0.0
tokens_count = 0
lemmatizer = WordNetLemmatizer()
raw_sentences = sent_tokenize(text)
# 분해된 문장별로 단어 토큰 > 품사 태깅 후에 SentiSynset 생성 > 감성 지수 합산
for raw_sentence in raw_sentences:
# NLTK 기반의 품사 태깅 문장 추출
tagged_sentence = pos_tag(word_tokenize(raw_sentence))
for word, tag in tagged_sentence:
# WordNet 기반 품사 태깅과 어근 추출
wn_tag = penn_to_wn(tag)
if wn_tag not in (wn.NOUN, wn.ADJ, wn.ADV):
continue
lemma = lemmatizer.lemmatize(word, pos=wn_tag)
if not lemma:
continue
# 어근을 추출한 단어와 WordNet 기반 품사 태깅을 입력해 Synset 객체 생성
synsets = wn.synsets(lemma, pos=wn_tag)
if not synsets:
continue
# sentiwordnet의 감성 단어 분석으로 감성 synset 추출
# 모든 단어에 대해 긍정 감성 지수는 +로 부정 감성 지수는 -로 합산해 감성 지수 계산
synset = synsets[0]
swn_synset = swn.senti_synset(synset.name())
sentiment += (swn_synset.pos_score() - swn_synset.neg_score())
tokens_count += 1
if not tokens_count:
return 0
# 총 score가 0 이상일 경우 긍정(Positive) 1, 그렇지 않을 경우 부정(Negative) 0 반환
if sentiment >= 0:
return 1
return 0
[swn_polarity(text) 함수로 IMDB 감상평의 개별 문서에 적용해 긍정/부정 감성 예측]
- 앞서 생성한 함수를 IMDB 감상평의 개별 문서에 적용해 긍정 및 부정 감성을 예측해 본다.
- 판다스의 apply lambda 구문을 이용해 swn_polarity(text)를 개별 감상평 텍스트에 적용한다.
- 지도학습 기반의 감성 분석에서 생성한 review_df DataFrame을 그대로 이용하고, review_df의 새로운 칼럼으로 'preds'를 추가해 이 칼럼에 swn_polarity(text)로 반환된 감성 평가를 담는다.
- 실제 감성 평가인 'sentiment' 칼럼과 swn_polarity(text)로 반환된 결과의 정확도, 정밀도, 재현율 값을 측정한다.
review_df['preds'] = review_df['review'].apply(lambda x : swn_polarity(x))
y_target = review_df['sentiment'].values
preds = review_df['preds'].values
[SentiWordNet의 감성 분석 예측 성능 확인하기]
from sklearn.metrics import accuracy_score, confusion_matrix, precision_score
from sklearn.metrics import recall_score, f1_score, roc_auc_score
import numpy as np
print(confusion_matrix(y_target, preds))
print('정확도:', np.round(accuracy_score(y_target, preds), 4))
print('정밀도:', np.round(precision_score(y_target, preds), 4))
print('재현율:', np.round(recall_score(y_target, preds), 4))
VADER를 이용한 감성 분석
VADER는 소셜 미디어의 감성 분석 용도로 만들어진 룰 기반의 Lexicon 이며, SentimentIntensityAnalyzer 클래스를 이용해 쉽게 감성 분석을 제공한다. 또한, VADER는 NLTK 패키지의 서브 모듈로 제공될 수도 있고 단독 패키지로 제공될 수도 있다.
[VADER의 간단한 사용법 알아보기]
NLTK 서브 모듈로 SentimentIntensityAnalyzer를 임포트하고 간략하게 IMDB의 감상평 한 개만 감성 분석을 수행해 결과를 살펴본다.
from nltk.sentiment.vader import SentimentIntensityAnalyzer
senti_analyzer = SentimentIntensityAnalyzer()
senti_scores = senti_analyzer.polarity_scores(review_df['review'][0])
print(senti_scores)
▶ SentimentIntensityAnalyzer 객체를 생성한 뒤에 문서별로 polarity_score() 메서드를 호출해 감성 점수를 산출
- 해당 문서의 감성 점수가 특정 임계값 이상이면 긍정, 그렇지 않으면 부정으로 판단
※ SentimentIntensityAnalyzer 객체의 polarity_score() 메서드는 딕셔너리 형태의 감성 점수를 반환한다.
- neg: 부정 감성 지수
- neu: 중립적인 감성 지수
- pos: 긍정 감성 지수
- compound: neg, neu, pos score를 적절히 조합해 -1에서 1사이의 감성 지수를 표현한 값. 보통 0.1 이상이면 긍정 감성, 그 이하이면 부정 감성으로 판단하나 상황에 따라 이 임계값을 조정해 예측 성능 조절.
[VADER를 이용한 IMDB 감성 분석 수행]
vader_polarity() 함수를 생성.
vader_polarity() 함수는 입력 파라미터로 영화 감상평 텍스트와 긍정/부정을 결정하는 임곗값(threshold)을 가지고, SentimentIntensityAnalyzer 객체의 polarity_score() 메서드를 호출해 감성 결과를 반환한다.
- review_df DataFrame의 apply lambda 식을 통해 vader_polarity() 함수를 호출
- 각 문서별로 감성 결과를 vader_preds 라는 review_df의 새로운 칼럼으로 저장
- 저장된 감성 분석 결과를 기반으로 VADER의 예측 성능 측정
def vader_polarity(review, threshold=0.1):
analyzer = SentimentIntensityAnalyzer()
scores = analyzer.polarity_scores(review)
# compound 값에 기반해 threshold 입력값보다 크면 1, 그렇지 않으면 0 반환
agg_score = scores['compound']
final_sentiment = 1 if agg_score >= threshold else 0
return final_sentiment
# apply lambda 식을 이용해 레코드별로 vader_polarity() 를 수행하고 결과를 'vader_preds'에 저장
review_df['vader_preds'] = review_df['review'].apply(lambda x : vader_polarity(x, 0.1))
y_target = review_df['sentiment'].values
vader_preds = review_df['vader_preds'].values
print(confusion_matrix(y_target, vader_preds))
print('정확도:', np.round(accuracy_score(y_target, vader_preds), 4))
print('정밀도:', np.round(precision_score(y_target, vader_preds), 4))
print('재현율:', np.round(recall_score(y_target, vader_preds), 4))
▶ SentiWordNet 에 비해 정확도가 향상됐고, 특히 재현율은 85%로 크게 향상되었다.
다음 내용
[출처]
파이썬 머신러닝 완벽 가이드
Unite.AI
'[파이썬 Projects] > <파이썬 머신러닝>' 카테고리의 다른 글
[머신러닝] 텍스트 분석: 문서 군집화 (2) | 2024.10.31 |
---|---|
[머신러닝] 텍스트 분석: 토픽 모델링 (4) | 2024.10.31 |
[머신러닝] 텍스트 분석: 분류 실습 (0) | 2024.10.30 |
[머신러닝] 텍스트 분석: BOW(Bag of Words) (1) | 2024.10.30 |
[머신러닝] 텍스트 분석: 텍스트 정규화 (0) | 2024.10.29 |