sem / modules /sentiment.py
cyj-26's picture
Upload 25 files
26c3195 verified
# =============================================================================
# 감정분석 모듈 (한국어 + 영어)
# 방법 1: KNU 한국어 감성사전 (내장) — 오프라인
# 방법 2: HuggingFace transformers — 온라인/GPU 권장
# =============================================================================
import pandas as pd
import numpy as np
import re
# ── KNU 한국어 감성사전 (축약 내장 버전) ──────────────────────────────────────
# 원본: https://github.com/park1200656/KnuSentiLex
_KNU_DICT = {
# 긍정
"좋다":2,"훌륭하다":2,"뛰어나다":2,"만족":2,"감사":2,"행복":2,"기쁘다":2,
"즐겁다":2,"편리하다":2,"탁월하다":2,"우수하다":2,"친절":2,"빠르다":1,
"깨끗하다":1,"합리적":1,"저렴하다":1,"신뢰":2,"안전하다":1,"편하다":1,
"최고":2,"훌륭":2,"사랑":2,"좋아":2,"웃음":1,"밝다":1,"성공":2,"발전":1,
"유익":1,"효과적":1,"혁신":1,"창의":1,"희망":2,"긍정":2,"완벽":2,"탁월":2,
"추천":1,"칭찬":2,"흡족":2,"감동":2,"유용하다":1,"도움":1,"쉽다":1,
# 부정
"나쁘다":-2,"불만":-2,"싫다":-2,"실망":-2,"최악":-2,"불편":-1,"느리다":-1,
"비싸다":-1,"불안":-1,"위험":-1,"문제":-1,"불량":-2,"고장":-2,"오류":-2,
"짜증":-2,"화나다":-2,"슬프다":-2,"우울":-2,"부족":-1,"어렵다":-1,
"힘들다":-1,"복잡하다":-1,"불친절":-2,"무시":-2,"거짓":-2,"사기":-2,
"불신":-2,"두렵다":-1,"피곤":-1,"지루하다":-1,"부정":-2,"낭비":-1,
"손해":-2,"후회":-2,"걱정":-1,"아쉽다":-1,"부정적":-2,"억울":-2,
}
# 부정어 패턴
_NEG_PATTERNS = re.compile(r"(안|못|없|않|안됨|불|비|무|미)")
def _knu_score(text: str) -> float:
if not isinstance(text, str) or not text.strip():
return 0.0
tokens = re.findall(r'\w+', text)
score = 0.0
count = 0
for i, tok in enumerate(tokens):
for word, val in _KNU_DICT.items():
if word in tok:
# 앞 토큰에 부정어가 있으면 극성 반전
neg = (i > 0 and _NEG_PATTERNS.search(tokens[i-1]))
score += -val if neg else val
count += 1
return round(score / max(count, 1), 4)
def _label(score: float, pos_thr: float = 0.3, neg_thr: float = -0.3) -> str:
if score >= pos_thr: return "긍정"
if score <= neg_thr: return "부정"
return "중립"
# ── 방법 2: HuggingFace (선택적) ─────────────────────────────────────────────
def _hf_sentiment(texts: list, model_name: str = "snunlp/KR-FinBert-SC"):
try:
from transformers import pipeline
pipe = pipeline("text-classification", model=model_name,
tokenizer=model_name, truncation=True, max_length=512)
results = []
batch = 16
for i in range(0, len(texts), batch):
chunk = [str(t)[:512] for t in texts[i:i+batch]]
out = pipe(chunk)
results.extend(out)
return results
except Exception as e:
return [{"error": str(e)}] * len(texts)
# ── 메인 함수 ─────────────────────────────────────────────────────────────────
def run_sentiment(df: pd.DataFrame, text_col: str,
method: str = "dictionary", # "dictionary" | "transformer"
model_name: str = "snunlp/KR-FinBert-SC",
pos_threshold: float = 0.3,
neg_threshold: float = -0.3):
"""
Parameters
----------
text_col : 텍스트 컬럼명
method : 'dictionary' (KNU 사전, 빠름) | 'transformer' (딥러닝, 정확)
"""
texts = df[text_col].fillna("").tolist()
if method == "transformer":
try:
import transformers # noqa
except ImportError:
return None, "transformers 패키지가 설치되어 있지 않습니다. dictionary 방법을 사용하세요."
raw = _hf_sentiment(texts, model_name)
if "error" in raw[0]:
return None, f"Transformer 오류: {raw[0]['error']}"
labels = [r.get("label","").lower() for r in raw]
scores = [r.get("score", 0.0) for r in raw]
# 레이블 정규화
def norm_label(l):
if "pos" in l or "긍정" in l: return "긍정"
if "neg" in l or "부정" in l: return "부정"
return "중립"
df_out = df[[text_col]].copy()
df_out["감정레이블"] = [norm_label(l) for l in labels]
df_out["신뢰도"] = [round(s, 4) for s in scores]
else:
scores = [_knu_score(t) for t in texts]
labels = [_label(s, pos_threshold, neg_threshold) for s in scores]
df_out = df[[text_col]].copy()
df_out["감정점수"] = scores
df_out["감정레이블"] = labels
# 요약 통계
label_counts = df_out["감정레이블"].value_counts()
total = len(df_out)
summary = pd.DataFrame({
"감정": label_counts.index,
"빈도": label_counts.values,
"비율(%)": (label_counts.values / total * 100).round(1)
})
# 상위 긍정/부정 텍스트 샘플
if "감정점수" in df_out.columns:
pos_top = df_out.nlargest(5, "감정점수")[[text_col,"감정점수","감정레이블"]]
neg_top = df_out.nsmallest(5, "감정점수")[[text_col,"감정점수","감정레이블"]]
else:
pos_top = df_out[df_out["감정레이블"]=="긍정"].head(5)
neg_top = df_out[df_out["감정레이블"]=="부정"].head(5)
return {
"전체결과": df_out,
"요약": summary,
"긍정상위": pos_top.reset_index(drop=True),
"부정상위": neg_top.reset_index(drop=True)
}, None