Programming

감성분석(Sentiment Analysis) 정리: 지도학습 vs 감성사전(SentiWordNet/VADER) + IMDB 실습

Lucas.Kim 2026. 2. 14. 13:50
반응형

Overview

감성분석은 텍스트에 담긴 주관적인 감정/의견/평가(긍정·부정 등) 를 자동으로 판별하는 기술입니다.
대표 활용처는 소셜미디어 반응 분석, 여론조사, 제품/영화 리뷰 분석, 고객 VOC 분석 등이 있습니다.

이번 글은 아래 2가지 축을 “입문자 관점에서” 확실히 구분해 정리합니다.

  1. 지도학습 기반 감성분석(분류 문제로 풀기)
  2. 감성 어휘 사전 기반 감성분석(룰/사전 기반)
    • SentiWordNet
    • VADER(소셜미디어 최적화 룰 기반)

그리고 IMDB 리뷰 데이터로 전처리 → 학습/평가 → 사전기반 평가까지 흐름을 한 번에 연결합니다.

1) 감성분석이란?

  • 감성분석은 문서/문장/단어에 포함된 감정(positive/negative), 의견(opinion), 태도(attitude) 를 추정합니다.
  • 가장 흔한 형태는 이진 분류(긍정=1, 부정=0) 이지만,
    • 다중 감정(기쁨/분노/슬픔 등),
    • 점수 회귀(감성 점수),
    • 문장 단위 vs 문서 단위
      로도 확장됩니다.

2) 감성분석 접근 2가지

2-1) 지도학습 기반(ML 분류)

핵심 아이디어:
“정답 라벨이 붙은 리뷰(긍정/부정)를 학습 → 새 리뷰의 라벨 예측”
즉, 텍스트 분류와 구조가 거의 동일합니다.

  • 장점: 데이터만 충분하면 일반적으로 성능이 좋음
  • 단점: 라벨 데이터 구축 비용이 큼(사람이 달아야 함), 도메인 바뀌면 성능 저하 가능

2-2) 감성 어휘 사전 기반(lexicon/rule)

핵심 아이디어:
“단어(또는 표현)마다 감성 점수/극성이 들어있는 사전(lexicon)을 이용해 문서 점수를 계산”

  • 장점: 라벨 데이터 없이도 동작, 빠르게 베이스라인 구축 가능
  • 단점: 문맥(반어/부정/강조/도메인 특화 표현) 처리 한계, 정확도가 지도학습보다 낮아질 수 있음

3) IMDB 영화 리뷰 긍/부정 예측(지도학습) — 데이터 로딩/전처리

아래 코드는 IMDB 라벨 데이터(labeledTrainData.tsv)를 읽고, HTML/특수문자/숫자 등을 제거하는 정규화 전처리입니다.

# 지도학습 기반 감성 분석 실습
import pandas as pd

# ✅ (목적) IMDB 라벨 데이터 로드: review(텍스트), sentiment(정답 라벨) 포함
review_df = pd.read_csv('./IMDB/labeledTrainData.tsv',
                        sep='\t')

review_df.head(5)

# ✅ (목적) 원문 리뷰 예시 출력: HTML 태그, 기호, 숫자 등이 섞여 있어 전처리가 필요함을 확인
print(review_df['review'][1]) # -> html 및 숫자 삭제

전처리는 정규표현식을 사용합니다.

import re #정규화

# ✅ (목적) <br /> 같은 HTML 개행 태그 제거(공백으로 치환)
review_df['review'] = review_df['review'].str.replace('<br />',' ')

# ✅ (핵심) 영어 알파벳(a-zA-Z)만 남기고 나머지는 공백 처리
# - 숫자/특수문자/기호 제거로 토큰의 잡음을 줄임
review_df['review'] = review_df['review'].apply( lambda x : re.sub("[^a-zA-Z]", " ", x) )

# ✅ (확인) 전처리 후 결과 출력: HTML과 숫자가 제거된 것을 확인
print(review_df['review'][1]) # -> html 및 숫자 삭제

왜 이런 전처리를 하나요?

  • <br />, ", 숫자, 괄호 같은 것들은 “감성”과 직접 관련이 적고
  • 단어 토큰을 쓸데없이 늘려서 벡터 공간만 커지고 희소성이 증가합니다.
  • 특히 BoW/TF-IDF에서는 불필요 토큰이 늘어날수록 성능과 효율이 떨어질 수 있습니다.

4) 학습/테스트 데이터 분리

#학습 / 테스트 데이터 분리
from sklearn.model_selection import train_test_split

# ✅ (목적) sentiment가 정답(y), review 텍스트가 입력(X)
class_df = review_df['sentiment']

# ✅ (목적) id와 sentiment를 제외하고 feature(텍스트)만 남김
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))

포인트

  • (17500, 1)은 “리뷰 문서 17500개, 컬럼 1개(review)”라는 뜻입니다.
  • 실전에서는 train/test를 먼저 나눈 뒤, 벡터라이저는 반드시 train으로 fit 해야 합니다.

5) Pipeline으로 CountVectorizer + LogisticRegression 학습/평가

이 블록은 “전처리된 텍스트 → CountVectorizer로 벡터화 → 로지스틱 회귀 분류”를 파이프라인으로 묶습니다.

#pipe라인을 통해 Count 기반 피처 백터화 및 머신러닝 학습/예측/평가
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

# ✅ (목적) stop_words='english'로 흔한 불용어 제거
# ✅ (목적) ngram_range=(1,2)로 unigram+bigram을 함께 사용(표현력 증가)
# ✅ (목적) LogisticRegression C=10은 규제를 약하게 해서 더 복잡한 결정경계를 허용
pipeline = Pipeline([
    ('cnt_vect', CountVectorizer(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'])

# ✅ (확률) ROC-AUC 계산을 위해 positive(1) 확률값 추출
pred_probs = pipeline.predict_proba(X_test['review'])[:, 1]

# ✅ (평가) Accuracy: 단순 맞춘 비율 / ROC-AUC: 임계치 변화에도 강건한 분류 품질 지표
print(f'Accuracy : {accuracy_score(y_test, pred)}')
print(f'ROC-AUC : {roc_auc_score(y_test, pred_probs)}')

Accuracy : 0.8848
ROC-AUC : 0.9509112508536308

Accuracy vs ROC-AUC (입문자 필수 개념)

  • Accuracy(정확도): 0/1을 임계치 기준으로 잘 맞췄는지
  • ROC-AUC: 임계치를 바꿔가며 “양성/음성 분리 능력” 자체를 평가
    → 특히 데이터가 불균형하거나, “확률의 품질”을 보고 싶을 때 유용합니다.

6) TF-IDF 기반 Pipeline (주의: 코드에 오타/실수가 있습니다)

사용자가 공유한 코드에는 아래 라인이 있습니다.

pipeline = Pipeline([
    ('tfidf_vect', CountVectorizer(stop_words='english', ngram_range=(1,2))),
    ('lr_clf',LogisticRegression(C=10))
])

여기서 단계 이름은 tfidf_vect인데 실제로 CountVectorizer를 쓰고 있습니다.
그래서 결과가 위 CountVectorizer와 동일하게 나옵니다(출력도 동일했죠).

✅ 의도대로 “TF-IDF”를 쓰려면 아래처럼 TfidfVectorizer가 들어가야 합니다.

(요청하신 “코드 순서 유지” 원칙 때문에 원문 코드는 그대로 설명하되, 정확한 개념을 위해 수정 방향을 함께 명시합니다.)

  • 올바른 형태(개념상 정답):
pipeline = Pipeline([
    ('tfidf_vect', TfidfVectorizer(stop_words='english', ngram_range=(1,2))),
    ('lr_clf', LogisticRegression(C=10))
])

TF-IDF를 쓰면 뭐가 달라지나요?

  • “the, movie, film”처럼 거의 모든 리뷰에 등장하는 단어의 영향력이 줄고
  • 특정 리뷰에서만 강하게 나타나는 단어(“masterpiece”, “terrible” 등)가 상대적으로 돋보입니다.
    → 특히 감성 분석에서 TF-IDF는 자주 좋은 베이스라인이 됩니다.

7) 감성 어휘 사전 기반 분석

지도학습이 “데이터로 학습”하는 방식이라면, 사전 기반은 “이미 구축된 감성 지식”을 사용합니다.

7-1) SentiWordNet 개념

  • WordNet의 synset(동의어 집합) 개념에 감성 점수를 붙인 형태
  • 각 synset은 보통 다음 점수를 가집니다.
    • pos_score() : 긍정 성향
    • neg_score() : 부정 성향
    • obj_score() : 객관 성향

즉, 단어 하나가 아니라 단어+의미(sense) 단위로 감성이 정의됩니다.

7-2) WordNet synset 확인(“present” 예시)

import nltk
nltk.download('all')

from nltk.corpus import wordnet as wn

term = 'present'

# ✅ (목적) 단어 'present'가 가지는 의미(sense) 목록(synsets)을 가져옴
synsets = wn.synsets(term)

print('synsets() 반환 type :', type(synsets))
print('synsets() 반환 값 갯수:', len(synsets))
print('synsets() 반환 값 :', synsets)

왜 synset이 중요하죠?

“present”는

  • 명사: 선물(present)
  • 동사: 제시하다(present)
  • 형용사: 현재의(present)
    처럼 의미가 여러 개입니다.
    감성 사전도 의미별로 점수가 달라질 수 있으므로 어떤 의미(synset)를 택하느냐가 결과에 영향을 줍니다.

7-3) SentiWordNet 점수 확인 예시

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

해석

  • “father”는 감성 중립에 가까워 obj_score가 높게 나올 수 있고
  • “fabulous”는 긍정 단어라 pos_score가 크게 나옵니다.

8) SentiWordNet 기반 문서 감성 분류 함수(swn_polarity)

이 함수는 전체 리뷰 텍스트를:

  • 문장 분리 → 단어 토큰화 → 품사 태깅 → WordNet 품사 매핑 → 표제어화 → synset 선택 → senti 점수 합산
    의 흐름으로 점수를 계산합니다.
from nltk.corpus import wordnet as wn

# ✅ (목적) NTLK PennTreebank POS 태그를 WordNet POS 태그로 변환
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
    return 

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)

    # ✅ (핵심 흐름) 문장 단위로 처리 → 단어 토큰 → 품사 태깅 → 표제어화 → SentiWordNet 점수 합산
    for raw_sentence in raw_sentences:
        tagged_sentence = pos_tag(word_tokenize(raw_sentence))
        for word , tag in tagged_sentence:
            
            wn_tag = penn_to_wn(tag)

            # ✅ (목적) 감성에 유의미한 품사(명사/형용사/부사) 중심으로 사용
            if wn_tag not in (wn.NOUN , wn.ADJ, wn.ADV):
                continue                   

            # ✅ (목적) 표제어(lemma)로 변환해 사전 매칭 정확도 향상
            lemma = lemmatizer.lemmatize(word, pos=wn_tag)
            if not lemma:
                continue

            # ✅ (목적) lemma와 품사로 synset 후보를 찾음
            synsets = wn.synsets(lemma , pos=wn_tag)
            if not synsets:
                continue

            # ✅ (단순화) 첫 번째 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
    
    # ✅ (결정 규칙) 0 이상이면 긍정(1), 아니면 부정(0)
    if sentiment >= 0 :
        return 1
    
    return 0

입문자 관점 “왜 성능이 떨어질 수 있나?”

  • synsets[0]만 쓰면 의미 중의성이 제대로 처리되지 않습니다.
  • “not good”(부정) 같은 부정어 처리가 약합니다.
  • 리뷰 도메인(영화) 특유의 표현을 사전에 다 반영하지 못합니다.

8-1) SentiWordNet 기반 예측 및 평가

review_df['preds'] = review_df['review'].apply( lambda x : swn_polarity(x) )
y_target = review_df['sentiment'].values
preds = review_df['preds'].values

from sklearn.metrics import accuracy_score, confusion_matrix, precision_score 
from sklearn.metrics import recall_score, f1_score, roc_auc_score

print(confusion_matrix( y_target, preds))
print("정확도:", accuracy_score(y_target , preds))
print("정밀도:", precision_score(y_target , preds))
print("재현율:", recall_score(y_target, preds))

[[7669 4831]
 [3635 8865]]
정확도: 0.66136
정밀도: 0.6472692757009346
재현율: 0.7092

해석(핵심만)

  • 정확도 ~0.66 수준으로, 지도학습(0.88+)보다 낮습니다.
  • 사전 기반은 “라벨 없이도 돌아간다”는 장점이 있지만, 문맥 처리 한계로 성능이 제한될 수 있습니다.

9) VADER 기반 감성 분석

VADER는 특히 소셜미디어 문장(짧고 감탄/대문자/이모티콘/강조가 많은 문장)에 강한 룰 기반 분석기입니다.

  • polarity_scores()가 다음을 반환합니다.
    • neg, neu, pos : 각 비율
    • compound : 최종 감성 점수(-1 ~ +1)
#Vader lexicon을 이용한 Sentimen Anlysis
from nltk.sentiment.vader import SentimentIntensityAnalyzer

senti_analyzer = SentimentIntensityAnalyzer()
senti_scores = senti_analyzer.polarity_scores(review_df['review'][0])
print(senti_scores)
{'neg': 0.13, 'neu': 0.743, 'pos': 0.127, 'compound': -0.7943}

임계값(threshold)로 긍/부정을 나누는 함수:

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

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('#### VADER 예측 성능 평가 ####')
from sklearn.metrics import accuracy_score, confusion_matrix, precision_score 
from sklearn.metrics import recall_score, f1_score, roc_auc_score

print(confusion_matrix( y_target, vader_preds))
print("정확도:", accuracy_score(y_target , vader_preds))
print("정밀도:", precision_score(y_target , vader_preds))
print("재현율:", recall_score(y_target, vader_preds))

VADER의 특징(실전 감각)

  • “완전한 문장 규칙 + 감성 사전” 조합이라 빠르고 간편합니다.
  • 다만 IMDB처럼 긴 리뷰(서술형)에서는 지도학습에 비해 한계가 있을 수 있습니다.
  • threshold(예: 0.1)는 데이터에 따라 조정할 수 있습니다.

10) 정리: 언제 무엇을 쓰면 좋을까?

지도학습이 유리한 경우

  • 라벨 데이터가 있고(또는 만들 수 있고)
  • 목표 도메인(영화/쇼핑/정치 등)이 명확하며
  • 높은 정확도가 필요할 때

✅ 추천 조합(베이스라인)

  • TfidfVectorizer + LogisticRegression
  • 필요 시 ngram, max_df, min_df, C 튜닝

감성사전/룰 기반이 유리한 경우

  • 라벨이 없고 빠르게 “대략적 감성 흐름”만 보고 싶을 때
  • 프로토타입/대시보드/실시간 모니터링 같은 상황

✅ 추천

  • 짧은 문장/소셜 텍스트: VADER
  • WordNet 기반 실험/연구용: SentiWordNet(단, 중의성 처리 주의)

감성분석은 크게

  • 지도학습 기반(텍스트 분류)
  • 감성사전 기반(룰/lexicon) 으로 나뉘며,

IMDB 리뷰처럼 “라벨이 존재하고 문장이 긴 리뷰”에서는
대부분 지도학습 기반(TF-IDF + 로지스틱 회귀) 이 강력한 베이스라인이 됩니다.

반면, 라벨 없이도 감성 흐름을 빠르게 보고 싶다면
SentiWordNet/VADER 같은 사전 기반 방법이 유용한 출발점이 됩니다.

반응형