IMDb 영화 리뷰 감성 분석 완전 가이드 — DistilBERT LoRA와 TF-IDF 베이스라인을 한 번에 비교하기
1. 이 글에서 다루는 것
Stanford NLP의 IMDb 대규모 영화 리뷰 데이터셋으로 "긍정/부정" 감성 분류 모델을 처음부터 끝까지 만들어 봅니다. 단순히 트랜스포머 정확도만 보는 게 아니라, 고전적인 TF-IDF 베이스라인과 DistilBERT + LoRA 파인튜닝을 직접 비교합니다. 또한 모델이 얼마나 확신 있게 틀리는지, 긴 리뷰의 어디에서 감성 신호를 읽는지, 라벨 없는 데이터까지 활용해 성능을 끌어올리는 방법까지 다룹니다.
본문에서 등장하는 핵심 개념 10가지를 짧은 박스로 정리해 두었으니, 모르는 용어가 나와도 흐름이 끊기지 않습니다.
2. 왜 영화 리뷰 감성 분석인가?
감성 분석은 자연어 처리(NLP)의 대표 문제입니다. 제품 리뷰, SNS 반응, 고객 VOC 어디서나 쓰이죠. IMDb 데이터셋은 �정 12,500개 + 부정 12,500개로 구성된 균형 잡힌 이진 분류 데이터라, 분류 모델 성능을 비교하는 표준 벤치마크로 오랫동안 사용됩니다.
- 고전 ML(TF-IDF)과 최신 트랜스포머(DistilBERT)의 격차는 어느 정도인가?
- 정확도 외에, 모델이 얼마나 잘 맞는지 확신하는지는 어떻게 측정하는가?
- 긴 리뷰에서 모델이 어디를 보고 판단하는가?
이 글은 바로 이 세 가지 질문에 답하기 위한 튜토리얼을 따라가며, 동시에 실무에서 흔히 놓치는 캘리브레이션·견고성·해석 가능성까지 점검합니다.
3. 환경 준비와 데이터셋 감사
먼저 Google Colab 환경을 구성하고, 필요한 라이브러리를 설치합니다. PEFT와 torchao 사이 호환성 패치도 함께 적용해야 GPU에서 LoRA가 깨지지 않습니다. 재현 가능한 실험을 위해 결정론적 시드(seed=42)를 고정하는 것도 잊지 마세요.
!pip install -q datasets peft accelerate torchao
import torch, random, numpy as np
SEED = 42
random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
torch.cuda. available() and torch.cuda.manual_seed_all(SEED)
Stanford IMDb 데이터셋을 다운로드한 뒤, train/test를 섞어 서브샘플링합니다. 그 다음 반드시 확인해야 할 것 4가지가 있습니다.
- 클래스 균형: 긍정/부정 비율이 비슷한지 (불균형이면 정확도 지표가 왜곡됩니다)
- 리뷰 길이 분포: 평균·중앙값·최댓값, 그리고 너무 긴 리뷰가 얼마나 있는지
- 중복 누출: 같은 리뷰가 train/test 양쪽에 들어가면 정확도가 부풀려짐
- HTML 아티팩트: 원본 HTML 태그(
<br>등)가 남아 있지 않은지
모델 만들기 전에 분포를 시각화해 두면, 나중에 "왜 이런 오류가 났지?"를 추적할 때 큰 도움이 됩니다.
대규모 사전학습 모델의 미세조정 시, 거대한 가중치 행렬 전체를 업데이트하는 대신 저랭크 행렬 두 개만 추가해 학습 파라미터 수를 수십만 배로 줄이는 기법입니다. GPU 메모리와 학습 시간을 크게 아낄 수 있어, DistilBERT 같은 트랜스포머를 Colab에서도 굴릴 수 있게 만들어 줍니다.
Hugging Face가 지식 증류(knowledge distillation)로 만든 BERT의 경량 버전입니다. 크기는 약 40%, 추론 속도는 약 60% 더 빠르며, 성능은 BERT의 약 97%를 유지합니다. 감성 분석 같은 실무 태스크에서 비용 대비 성능이 매우 좋아 표준처럼 쓰입니다.
4. TF-IDF + 로지스틱 회귀 베이스라인
트랜스포머를 쓰기 전에, 먼저 "단순한 방법으로도 어디까지 가능한가"를 확인하는 게 중요합니다. TF-IDF로 리뷰를 벡터화하고, 로지스틱 회귀를 학습시키면 5분도 안 걸려 90% 가까운 정확도가 나옵니다.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
vec = TfidfVectorizer(ngram_range=(1,2), min_df=2, max_df=0.9, sublinear_tf=True)
Xtr = vec.fit_transform(train['text']); Xte = vec.transform(test['text'])
clf = LogisticRegression(C=4.0, max_iter=1000, n_jobs=-1).fit(Xtr, train['label'])
베이스라인을 만드는 진짜 목적은 성능 숫자 비교뿐 아니라, 학습된 계수를 보고 어떤 단어/구문이 긍정·부정을 이�는지 사람이 해석할 수 있다는 데 있습니다. 예를 들면 "disappointing plot twist" 같은 부정 bigram이 높은 가중치를 가지는 식입니다.
단어 빈도(TF) × 역 문서 빈도(IDF)로, 어떤 단어가 그 문서에서 얼마나 중요한지를 수치화합니다. 모든 문서에 흔하게 나오는 "the", "a" 같은 단어는 자동으로 가중치가 낮아져, 실제 의미를 가진 단어에 집중하게 만들어 줍니다.
매개변수 효율적 미세조정. 모델 전체를 업데이트하지 않고 소수의 파라미터만 학습해 메모리·연산 비용을 크게 줄이는 기법 군의 총칭입니다. LoRA가 가장 대표적인 PEFT 방식 중 하나입니다.
5. DistilBERT에 LoRA 어댑터 붙이기
이제 DistilBERT를 IMDb에 맞춰 파인튜닝합니다. 다만, 6,600만 개 매개변수 전체를 업데이트하면 Colab T4 GPU에서는 메모리가 빠듯합니다. 그래서 LoRA 어댑터만 추가해, 원본 가중치는 얼려두고(freeze) 작은 행렬만 학습시킵니다.
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForSequenceClassification
base = AutoModelForSequenceClassification.from_pretrained("distilbert-base-uncased", num_labels=2)
lora_cfg = LoraConfig(r=8, lora_alpha=16, lora_dropout=0.1,
target_modules=["q_lin","v_lin"], task_type="SEQ_CLS")
model = get_peft_model(base, lora_cfg)
model.print_trainable_parameters() # 전체의 1% 미만만 학습
학습은 Hugging Face Trainer로 진행합니다. dynamic padding(리뷰 길이에 맞춰 동적 패딩)으로 불필요한 연산을 줄이고, early stopping으로 과적합을 방지합니다.
GPU에서 계산을 FP16(반 정밀도)으로 빠르게 하고, 가중치 업데이트 같은 민감한 부분은 FP32(단 정밀도)로 유지하는 기법입니다. 메모리를 절반 가까이 줄이면서 정확도 손실은 거의 없습니다.
6. 모델 평가와 캘리브레이션
정확도(Accuracy)만 보면 트랜스포머가 항상 "더 좋다"고 결론 내기 쉽습니다. 하지만 실제로는 어떤 지표를 보느냐가 결론을 바꿉니다. 본 튜토리얼에서는 아래 4가지를 모두 측정합니다.
| 지표 | 의미 | 왜 중요한가 |
|---|---|---|
| Accuracy | 전체 정답률 | 가장 직관적이지만 클래스 불균형에 약함 |
| Macro-F1 | 클래스별 F1의 평균 | 부정/긍정 어느 쪽이든 잘 맞추는지 균형 있게 평가 |
| ROC-AUC | ROC 곡선 아래 면적 | 임계값에 무관한 모델 분리 능력 (0~1) |
| ECE | 예측 캘리브레이션 오차 | "95% 확신"이라 했으면 실제로 95%가 맞아야 함 |
ROC 곡선(Receiver Operating Characteristic) 아래 면적. 이진 분류에서 0~1 사이 값을 가지며, 1에 가까울수록 모델이 두 클래스를 잘 분리한다는 뜻입니다. 데이터가 균형 잡힐 때 모델 간 성능 비교에 특히 유용합니다.
다중(혹은 이진) 분류에서 각 클래스별 F1 점수를 단순 평균낸 값입니다. 클래스 비율에 가중치를 두지 않으므로, "어느 한쪽만 잘 맞추고 다른 쪽은 외면하는" 모델을 제대로 감점합니다.
특히 중요한 건 분류 임계값(threshold) 스�입니다. 기본값인 0.5가 정말 최적인지, 0.45나 0.55로 옮기면 정확도가 더 올라가는지를 평가 데이터에서 직접 확인합니다. 그리고 마지막으로 신뢰도 다이어그램(reliability diagram)을 그려 모델이 "자신감 있게" 틀리는 사례가 얼마나 되는지 파악합니다.
기대 캘리브레이션 오차. 모델이 예측한 확신도(confidence)와 실제 정확도 사이의 차이를 평균낸 값입니다. 0에 가까울수록 잘 보정(calibrated)된 모델이며, 0.05 이하면 실무적으로 충분히 신뢰할 만한 수준으로 봅니다.
7. 오류 분석과 해석 가능성
평가지표를 다 찍어봤다면, 이제는 "왜 틀렸는가"를 봐야 합니다. 두 가지 분석을 합니다.
7-1. 가장 확신 있는 오류와 길이별 성능
모델이 "99% 부정"이라 했는데 실제는 긍정인 사례를 찾아 모으면, 보통 리뷰가 매우 길거나 비꼬는 표현(아이러니/반어법)을 담고 있습니다. 리뷰를 0~128, 128~256, 256+ 토큰 구간으로 나눠 정확도를 비교하면, DistilBERT의 512 토큰 컨텍스트 한계가 어디서 정확도를 깎아내리는지 보입니다.
7-2. Occlusion Saliency로 단어 기여도 추정
LoRA 어댑터를 기본 모델에 병합한 뒤, 한 번에 한 단어씩 가려(occlude) 보고 예측이 어떻게 바뀌는지 측정합니다. "excellent"를 가리면 긍정 확률이 크게 떨어지고, "boring"을 가리면 부정 확률이 크게 떨어지는 식입니다. 이게 바로 모델이 그 단어에 의존해 판단을 내렸다는 증거입니다.
입력에서 한 단어/구간을 가려서 예측이 얼마나 변하는지를 측정하는 해석 가능성 기법입니다. 예측이 크게 흔들린 단어일수록 그 단어가 모델 판단에 큰 영향을 미친다고 해석합니다.
7-3. Head vs Tail: 어디에 감성 정보가 집중되는가?
긴 리뷰에서 앞부분 128 토큰만 본 예측과 뒷부분 128 토큰만 본 예측을 비교합니다. IMDb 리뷰에서는 보통 결론이 뒤에 나오기 때문에 뒷부분이 더 강한 신호를 줍니다. 이 사실을 알면 "긴 리뷰는 어디부터 읽게 할까" 같은 후처리 정책에 반영할 수 있습니다.
8. 준지도 학습으로 성능 더 끌어올리기
IMDb 데이터셋에는 라벨이 없는(unlabeled) 분할 50,000개가 추가로 제공됩니다. 파인튜닝된 DistilBERT에 이 데이터를 넣어 높은 신뢰도(예: 0.95 이상)를 받은 예측만 가짜 라벨(pseudo-label)로 채택해 TF-IDF 학습 데이터에 섞습니다. 그 뒤 다시 학습한 분류기의 정확도가 올라가는지 비교합니다.
from datasets import load_dataset
unlab = load_dataset("stanfordnlp/imdb", split="unsupervised")
pseudo_texts, pseudo_labels = [], []
for ex in unlab.select(range(8000)):
probs = infer(ex["text"]) # softmax 확률
if probs.max() >= 0.95: # 높은 확신만 채택
pseudo_texts.append(ex["text"])
pseudo_labels.append(int(probs.argmax()))
준지도 학습(Semi-Supervised Learning)의 대표 기법. 모델이 라벨 없는 데이터에 대해 예측한 결과를 가짜(pseudo) 정답으로 사용하고, 이를 라벨 있는 데이터와 합쳐 다시 학습시킵니다. 신뢰도가 낮은 예측은 버리는 자기 학습(self-training) 방식이 가장 안정적입니다.
📌 결과적으로 의사 라벨링 후 TF-IDF 분류기의 정확도가 1~2%p 정도 추가 상승하는 것을 확인할 수 있습니다. 라벨 비용 없이 성능을 끌어올리는 실용적인 방법입니다.
9. 결론 및 다음 실험
단순히 트랜스포머를 파인튜닝하고 정확도를 보고하는 데서 끝내지 않고, TF-IDF 베이스라인과 직접 비교하고, LoRA로 메모리를 아꼈으며, 캘리브레이션·길이·절단·매우 확신 있는 오류가 실제 성능에 미치는 영향을 정량적으로 살펴봤습니다. 또한 occlusion 기반 saliency로 개별 예측을 해석하고, 라벨 없는 IMDb 데이터의 의사 라벨로 지도 학습을 확장했습니다.
마지막으로 통합된 DistilBERT 모델과 토크나이저를 저장해 두면, 사용자 정의 영화 리뷰에 대해 즉시 감성 추론을 돌릴 수 있습니다.
- 실제 25k/25k 풀 벤치마크:
FULL_RUN = True로 두면 T4 GPU에서 약 40분 소요 - 8k 컨텍스트 윈도우:
MODEL_NAME = "answerdotai/ModernBERT-base"로 교체해 절단 없이 학습 - Head + tail 절단: 앞 128 + 뒤 128 토큰만 모아 학습해 성능 비교
- LoRA 랭크(r)를
{4, 8, 16, 64}로 바꿔가며 학습 가능 파라미터 수 대비 정확도 그래프 작성 - 의사 라벨 teacher를 앙상블(DistilBERT + RoBERTa)로 교체해 self-training 반복
huggingface_hub.login()후infer_model.push_to_hub(...)로 모델 공유
10. 자주 묻는 질문 (FAQ)
Q1. 왜 굳이 TF-IDF 베이스라인을 먼저 만들까요?
비교 기준점(reference)이 있어야 DistilBERT가 정말 더 낫다고 말할 수 있습니다. 또 TF-IDF 계수는 사람이 읽을 수 있어서, 트랜스포머가 놓치는 쉬운 패턴을 발견하는 데 도움이 됩니다.
Q2. LoRA 랭크(r)는 어떻게 정하나요?
작게 시작(r=4~8)해 점진적으로 키우는 게 안전합니다. 데이터가 적거나 태스크가 단순하면 r=4로도 충분하고, 복잡한 도메인에서는 r=16~64까지 시도해 볼 만합니다.
Q3. 정확도가 비슷한데 어떤 모델을 선택해야 하나요?
ECE(캘리브레이션 오차)가 낮고, Macro-F1이 더 균형 잡힌 쪽이 실무에서 더 좋습니다. 단순 정확도는 라벨 비율이 한쪽으로 치우친 데이터에서 착시가 큽니다.
Q4. Colab 무료 GPU에서도 가능한가요?
서브샘플링(예: train 4,000 / test 4,000) 모드라면 T4 무료 등급에서 약 15~20분이면 LoRA 파인튜닝을 끝낼 수 있습니다. 풀 25k/25k는 유료 GPU 권장.
Q5. 의사 라벨링은 언제 효과적인가요?
라벨 데이터가 부족하고, 라벨 없는 데이터가 풍부하며, 모델이 높은 신뢰도로 예측할 수 있는 사례가 많을 때 효과적입니다. 신뢰도가 애매한 데이터까지 끌어오면 오히려 노이즈가 됩니다.
Q6. 만든 모델을 어떻게 배포하나요?
LoRA 어댑터를 베이스 모델에 병합해 저장한 뒤 push_to_hub()로 Hugging Face Hub에 올리거나, ONNX로 내보내 모바일/엣지 디바이스에 배포할 수 있습니다.