[Python] 감성 분석 실습

0ㅑ채
|2024. 3. 14. 14:48

1. IMDB 영화평을 이용한 지도학습 기반 감성분석

 

- https://www.kaggle.com/c/word2vec-nlp-tutorial/data

- 데이터 구조

  • id : 유저 아이디
  • sentiment : 감성. 긍정 1, 부정 0
  • review : 리뷰

  • 레이블이 있는 데이터를 가지고 범주를 예측하는 것과 동일하지만, 자연어는 피처가 문장으로 주어지기 때문에 문장을 피처 벡터화 작업해줘야 하는 것이 다르다.
  • 모든 단어를 각각의 피처로 만들고 각 문장은 피처의 존재 여부를 데이터로 소유한다. 

 

1. 데이터 읽어오기

 

import pandas as pd

review_df = pd.read_csv('./data/IMDB/labeledTrainData.tsv/labeledTrainData.tsv', header=0, sep="\t", quoting=3)
review_df.head(3)

 

 

2. 데이터 전처리

- <br />를 제거: replace

- 영문만 남겨두기: 정규식 모듈의 sub 메소드 활용

#정규식 모듈
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) )

print(review_df['review'].head())
0     With all this stuff going down at the moment ...
1       The Classic War of the Worlds   by Timothy ...
2     The film starts with a manager  Nicholas Bell...
3     It must be assumed that those who praised thi...
4     Superbly trashy and wondrously unpretentious ...
Name: review, dtype: object
  • a-zA-Z가 아닌 글자를 공백으로 치환
  • 한글을 제외한 글자 제거: [^가-힣]

 

3. 훈련/테스트 데이터 분리

- 지도학습 기반의 분류이므로 훈련 데이터를 이용해서 훈련하고, 테스트 데이터로 확인하는 것을 권장

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, 1), (7500, 1))

 

4. 피처벡터화

- CountVectorizer로 피처벡터화 수행 후 분류 모델 훈련

- TdidfVectorizer로 비처벡터화 수행 후 분류 모델 훈련

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, filtering, ngram은 (1,2)로 설정해 CountVectorization수행. 
# LogisticRegression의 C는 10으로 설정. 
pipeline = Pipeline([
    ('cnt_vect', CountVectorizer(stop_words='english', ngram_range=(1,2) )),
    ('lr_clf', LogisticRegression(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)))
예측 정확도는 0.8860      , ROC-AUC는 0.9503
# 스톱 워드는 english, filtering, ngram은 (1,2)로 설정해 TF-IDF 벡터화 수행. 
# LogisticRegression의 C는 10으로 설정. 
pipeline = Pipeline([
    ('tfidf_vect', TfidfVectorizer(stop_words='english', ngram_range=(1,2) )),
    ('lr_clf', LogisticRegression(C=10))])

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)))
예측 정확도는 0.8939,      ROC-AUC는 0.9596
ngram을 설정하면 하나의 단어를 하나로 인식하지 않고, n개의 단어까지 하나의 단어로 인지한다.
영어는 2나 3을 설정: 사람 이름 등 (I am a boy  -> I am, am a, a boy)
정확하게 일치하는 것이라면 예측도 가능하다. (I am 다음은 a/an 이구나)
그래서 생성형 AI는 ngram을 설정하는게 좋다. 

 

 

 

 

2. 비지도학습 기반 감성분석

- 레이블이 없는 경우 사용

- Lexicon이라는 감성 분석에 관련된 어휘집을 이용하는 방식

- 한글 버전의 Lexicon이 현재는 제공되지 않음

 

 

 

 

3. 네이버 식당 리뷰 데이터를 이용한 감성분석

  • 이진분류는 LogisticRegression이면 충분
  • 크롤링한 데이터 
  • score: 별점
  • y: 감성, score가 4이상이면 1, 아니면 0

 

1. 데이터 읽어오기

df = pd.read_csv("./python_machine_learning-main/data/review_data.csv")
print(df.head())
   score                      review  y
0      5            친절하시고 깔끔하고 좋았습니다  1
1      5                  조용하고 고기도 굿  1
2      4      갈비탕과 냉면, 육회비빔밥이 맛있습니다.  1
3      4  대체적으로 만족하나\n와인의 구성이 살짝 아쉬움  1
4      5       고기도 맛있고 서비스는 더 최고입니다~  1

 

 

 

2. 데이터 전처리

한글 추출: (가-힣)

  • 모음과 자음만으로 구성된 텍스트도 추출하려면 (ㄱ-ㅣ, 가-힣)

 

#한글을 제외한 글자 전부 제거

import re
# 텍스트 정제 함수 : 한글 이외의 문자는 전부 제거
def text_cleaning(text):
    # 한글의 정규표현식으로 한글만 추출합니다.
    hangul = re.compile('[^ ㄱ-ㅣ가-힣]+')
    result = hangul.sub('', text)
    return result
df['ko_text'] = df['review'].apply(lambda x: text_cleaning(x))
del df['review'] 
df.head()
  • 데이터 용량을 줄이기 위해 바로바로 del

 

 

 

3. 형태소분석

from konlpy.tag import Okt

# konlpy라이브러리로 텍스트 데이터에서 형태소를 '단어/품사'로 추출
def get_pos(x):
    tagger = Okt()
    pos = tagger.pos(x) # PartOfSpeech
    pos = ['{}/{}'.format(word,tag) for word, tag in pos]
    return pos
# 형태소 추출 동작을 테스트합니다.
result = get_pos(df['ko_text'][0])
print(result)
['친절하시고/Adjective', '깔끔하고/Adjective', '좋았습니다/Adjective']

 

 

 

4. 피처 벡터화

  • 등장하는 모든 단어를 수치화

# 등장 횟수만을 고려한 피처 벡터화를 위한 사전 생성하고 데이터를 피처 벡터화

from sklearn.feature_extraction.text import CountVectorizer

# 형태소를 벡터 형태의 학습 데이터셋(X 데이터)으로 변환
index_vectorizer = CountVectorizer(tokenizer = lambda x: get_pos(x))
X = index_vectorizer.fit_transform(df['ko_text'].tolist())

X.shape
(545, 3030)
  • 3030: 단어의 개수
  • 545: 행의 개수
print(df['ko_text'][0])
print(X[0])
친절하시고 깔끔하고 좋았습니다
  (0, 2647) 1
  (0, 428) 1
  (0, 2403) 1
  • 다 한번씩만 등장!

 

#CounterVectorizer를 이용해서 만든 피처 벡터를 Tfidf 기반으로 변경

from sklearn.feature_extraction.text import TfidfTransformer

# TF-IDF 방법으로, 형태소를 벡터 형태의 학습 데이터셋(X 데이터)으로 변환합니다.
tfidf_vectorizer = TfidfTransformer()
X = tfidf_vectorizer.fit_transform(X)

print(X.shape)
print(X[0])
(545, 3030)
  (0, 2647) 0.5548708693511647
  (0, 2403) 0.48955631270748484
  (0, 428) 0.6726462183300624
  • 가중치로 나타남

 

-  필요하다면 데이터를 제거하거나 추가해도 된다.

  • 제거는 많이 하고 추가는 예측할 데이터가 들어오면 그 데이터를 추가하는 형태로 작업

 

 

5. 훈련 데이터와 검증 데이터 생성

from sklearn.model_selection import train_test_split

y = df['y']
x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.30)
print(x_train.shape)
print(x_test.shape)
(381, 3030)
(164, 3030)

 

 

6. 모델 생성 및 훈련과 평가

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

# 로지스틱 회귀모델을 학습합니다.
lr = LogisticRegression(random_state=0)
lr.fit(x_train, y_train)
y_pred = lr.predict(x_test)
y_pred_probability = lr.predict_proba(x_test)[:,1]

# 로지스틱 회귀모델의 성능을 평가합니다.
print("accuracy: %.2f" % accuracy_score(y_test, y_pred))
print("Precision : %.3f" % precision_score(y_test, y_pred))
print("Recall : %.3f" % recall_score(y_test, y_pred))
print("F1 : %.3f" % f1_score(y_test, y_pred))
accuracy: 0.90
Precision : 0.896
Recall : 1.000
F1 : 0.945
  • 1이 나오는 경우는 굉장히 드물다. 의심을 해봐야 한다.
from sklearn.metrics import confusion_matrix

# Confusion Matrix를 출력합니다.
confmat = confusion_matrix(y_true=y_test, y_pred=y_pred)
print(confmat)
[[  0  17]  #0을 0으로, 0을 1로 분류
 [  0 147]]  #0을 1로, 1을 1로 분류
  • 실제 1인 데이터는 전부 1로 분류했으므로 정확도가 1이 된 것
  • 감성 분석이나 신용 카드 부정 거래 탐지 등을 수행할 때 주의할 점은 샘플의 비율이 비슷하지 않은 것
  • 일정한 비율로 샘플링하는 것이 중요

이런상황

df['y'].value_counts()

 

 

#타겟의 분포 확인

y
1    492
0     53
Name: count, dtype: int64
  • 1과 0의 비율이 10배 정도
  • 1을 언더샘플링 하든지, 0을 오버샘플링 해야 함

 

# 언더샘플링

# 1:1 비율로 랜덤 샘플링을 수행합니다.
positive_random_idx = df[df['y']==1].sample(50, random_state=30).index.tolist()
negative_random_idx = df[df['y']==0].sample(50, random_state=30).index.tolist()

print(positive_random_idx)
print(negative_random_idx)
[274, 315, 544, 31, 297, 153, 396, 444, 491, 446, 90, 0, 524, 516, 26, 172, 344, 27, 189, 406, 364, 307, 216, 334, 38, 147, 290, 121, 448, 218, 358, 215, 14, 72, 116, 400, 309, 137, 424, 223, 148, 288, 470, 108, 277, 357, 29, 273, 474, 131]

[151, 265, 425, 231, 488, 232, 321, 317, 54, 163, 353, 254, 333, 195, 238, 50, 389, 170, 36, 540, 249, 275, 45, 92, 523, 261, 403, 340, 89, 20, 497, 105, 55, 13, 248, 37, 112, 508, 349, 123, 325, 16, 40, 361, 19, 114, 17, 227, 79, 537]
  • 비율을 맞춘 데이터 생성
# 랜덤 데이터로 데이터셋을 나눔
random_idx = positive_random_idx + negative_random_idx
sample_X = X[random_idx, :]

y = df['y'][random_idx]
x_train, x_test, y_train, y_test = train_test_split(sample_X, y, test_size=0.30)
print(x_train.shape)
print(x_test.shape)
(70, 3030)
(30, 3030)
  • 데이터의 개수는 줄어들었지만 비율은 맞음
# 로지스틱 회귀모델을 다시 학습합니다.
lr = LogisticRegression(random_state=42)
lr.fit(x_train, y_train)
y_pred = lr.predict(x_test)
y_pred_probability = lr.predict_proba(x_test)[:,1]

# 학습한 모델을 테스트 데이터로 평가합니다.
print("accuracy: %.2f" % accuracy_score(y_test, y_pred))
print("Precision : %.3f" % precision_score(y_test, y_pred))
print("Recall : %.3f" % recall_score(y_test, y_pred))
print("F1 : %.3f" % f1_score(y_test, y_pred))
accuracy: 0.70
Precision : 0.818
Recall : 0.562
F1 : 0.667
  • 샘플링 비율을 맞추면서 데이터의 개수가 줄어드는 바람에 모든 평가지표가 내려갔다.
  • 분류 모델은 데이터를 많이 모으는게 중요하고 특히 비율이 안맞는 경우 비율이 낮은 쪽의 데이터를 많이 모아야 한다.

 

 

 

7. 피처의 중요도 확인

  • 트리 게열의 모델들은 feature_importance_라는 속성에 각 피처의 중요도를 가지고 잇음
  • 트리 계열이 아닌 모델들은 회귀 계수를 가지고 판단
  • 모델의 회귀계수는 coef_라는 속성에 저장됨
print(lr.coef_[0])
[ 0.          0.          0.18171898       ...          0.         -0.12358162            0.        ]
  • 각 피처에 대한 회귀 계수 = 단어
#회귀 계수 내림차순 정렬
coef_pos_index = sorted(((value, index) for index, value in enumerate(lr.coef_[0])),
                       reverse = True)

#단어와 매핑
invert_index_vectorizer = {v:k for k, v in index_vectorizer.vocabulary_.items()}
print(invert_index_vectorizer)
{2647: '친절하시고/Adjective', 428: '깔끔하고/Adjective', 2403: '좋았습니다/Adjective', 2356: '조용하고/Adjective', 233: '고기/Noun', 721: '도/Josa', 330: '굿/Noun', 120: '갈비탕/Noun', 260: '과/Josa', 528: '냉면/Noun', 2065: '육회/Noun', 1419: '비빔밥/Noun', 2082: '이/Josa', 1013: '맛있습니다/Adjective', 671: '대/Modifier', 2604: '체적/Noun', 2067: '으로/Josa', 956: '만족하나/Adjective', 1996: '와인/Noun', 2077: '의/Josa', 293: '구성/Noun', 1476: '살짝/Noun', 1705: '아쉬움/Noun', 1001: '맛있고/Adjective', 1508: '서비스/Noun', 589: '는/Josa', 701: '더/Noun', 2613: '최고/Noun', 2182: '입니다/Adjective', 24: '가/Josa', 2177: '입/Noun', 1897: '에서/Josa', 553: '녹아요/Verb',  ...