Day 2에서 정제한 Twitter 텍스트를 이제 숫자 벡터로 바꾸고 분류 모델을 학습한다. BOW와 TF-IDF 두 가지 벡터화 방식이 성능에 어떤 차이를 만드는지, 그리고 정확도 하나만으로 모델을 평가하면 왜 부족한지를 배웠다.
1. 텍스트 벡터화: BOW vs TF-IDF
기계학습 모델은 숫자만 이해한다. 정제된 텍스트를 어떻게 숫자로 바꾸느냐에 따라 성능이 달라진다.
Bag of Words (BOW)
이게 뭔지: 각 단어가 문서에 몇 번 등장했는지 세는 방식.
from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer(
analyzer='word', # 단어 단위 분석
token_pattern=r'\w{1,}', # 1글자 이상만 포함
max_features=30000 # 최빈도 상위 30,000개 단어만 사용
)
train_bow = count_vect.fit_transform(merge['Clean_Tweets'])
print(train_bow.shape) # (49159, 30000) — 49,159 문서 × 30,000 단어
결과: 49,159개 트윗 × 30,000개 단어 행렬. 각 칸은 해당 트윗에서 그 단어가 나온 횟수.
한계: “the”, “is” 같은 흔한 단어도 높은 값을 가진다. 중요한 단어와 흔한 단어를 구별하지 못한다.
TF-IDF
이게 뭔지: 단어 빈도(TF)와 희소성(IDF)을 곱해 “이 문서에서 특별히 중요한 단어”에 높은 가중치를 부여하는 방식.
TF = 문서 내 단어 빈도 / 문서 총 단어 수
IDF = log(전체 문서 수 / 해당 단어를 포함한 문서 수)
TF-IDF = TF × IDF
“the”는 모든 문서에 등장 → IDF 낮음 → TF-IDF 낮음
”love”는 긍정 트윗에 집중 등장 → IDF 높음 → TF-IDF 높음
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vect = TfidfVectorizer(
analyzer='word',
token_pattern=r'\w{1,}',
max_features=1000, # BOW보다 적은 단어 수로도 충분
stop_words='english' # 불용어 자동 제거
)
train_tfidf = tfidf_vect.fit_transform(merge['Clean_Tweets'])
print(train_tfidf.shape) # (49159, 1000)
특성 수: 30,000
가중치 기준: 등장 횟수
흔한 단어 처리: 높은 가중치
불용어 자동 제거: 불가
특성 수: 1,000
가중치 기준: 빈도 × 희소성
흔한 단어 처리: 낮은 가중치
불용어 자동 제거: 가능
2. 데이터 분할
왜 필요한지: 같은 데이터로 학습하고 평가하면 “외운” 것과 “이해한” 것을 구별할 수 없다. 보지 못한 데이터로 평가해야 실제 성능을 안다.
from sklearn.model_selection import train_test_split
# BOW 데이터 분할
x_train_bow, x_test_bow, y_train_bow, y_test_bow = train_test_split(
train_bow, # 특성(X)
merge['label'], # 레이블(y)
test_size=0.3, # 30%를 테스트 세트로
random_state=42 # 재현 가능한 결과를 위한 시드
)
# TF-IDF 데이터 분할
x_train_tfidf, x_test_tfidf, y_train_tfidf, y_test_tfidf = train_test_split(
train_tfidf, merge['label'], test_size=0.3, random_state=42
)
print(x_train_bow.shape) # (34411, 30000) — 학습용 70%
print(x_test_bow.shape) # (14748, 30000) — 평가용 30%
여기서 함께 알아둘 점은 데이터 누수(data leakage) 다. 실제 프로젝트에서는 학습 데이터에만 fit을 하고, 테스트 데이터에는 같은 벡터화 기준을 그대로 적용하는 transform만 써야 한다. 테스트 데이터의 정보를 학습 단계에서 미리 보면 평가 점수가 부풀려질 수 있다.
3. 로지스틱 회귀 모델 학습
이게 뭔지: 텍스트 벡터를 받아 긍정(1)/부정(0) 확률을 출력하는 선형 분류기.
왜 로지스틱 회귀인가: 텍스트 분류의 베이스라인 모델로 자주 쓰인다. 계산이 빠르고, 어떤 단어가 예측에 영향을 미쳤는지 해석할 수 있다.
내부 동작: 확률 = 1 / (1 + e^(-z)) 시그모이드 함수로 0~1 확률 출력 → 0.5 이상이면 긍정 분류
from sklearn.linear_model import LogisticRegression
# BOW 기반 모델
lr_bow = LogisticRegression(
solver='liblinear', # 소규모/중규모 데이터에 적합한 최적화 방식
max_iter=100
)
lr_bow.fit(x_train_bow, y_train_bow)
# TF-IDF 기반 모델
lr_tfidf = LogisticRegression(solver='liblinear', max_iter=100)
lr_tfidf.fit(x_train_tfidf, y_train_tfidf)
로지스틱 회귀가 텍스트 분류 기준선으로 많이 쓰이는 이유도 정리할 필요가 있다.
- 학습 속도가 빠르다.
- sparse한 TF-IDF 벡터와 궁합이 좋다.
- 어떤 단어가 긍정 또는 부정 쪽으로 작용하는지 해석이 가능하다.
즉, 복잡한 딥러닝 모델보다 먼저 로지스틱 회귀를 돌려 보는 것은 단순해서가 아니라, 문제 구조를 빠르게 확인하는 데 효율적이기 때문이다.
4. 모델 평가
왜 정확도만으로 부족한가: 부정 트윗이 1%뿐인 데이터에서 모든 것을 “긍정”으로 예측하면 정확도 99%가 나온다. 하지만 쓸모없는 모델이다. 여러 지표를 함께 봐야 한다.
from sklearn import metrics
y_pred_bow = lr_bow.predict(x_test_bow)
y_pred_tfidf = lr_tfidf.predict(x_test_tfidf)
정확도 (Accuracy)
정확도 = 맞은 예측 수 / 전체 예측 수
acc_bow = metrics.accuracy_score(y_test_bow, y_pred_bow)
acc_tfidf = metrics.accuracy_score(y_test_tfidf, y_pred_tfidf)
정밀도(Precision)와 재현율(Recall)
정밀도 = 긍정으로 예측한 것 중 실제 긍정 비율
= TP / (TP + FP)
재현율 = 실제 긍정 중 모델이 긍정으로 맞춘 비율
= TP / (TP + FN)
- 정밀도가 중요한 경우: 스팸 필터 — 일반 메일을 스팸으로 잘못 분류(FP)하면 중요 메일을 놓침
- 재현율이 중요한 경우: 의료 진단 — 실제 환자를 놓치는 것(FN)이 위험
precision_tfidf = metrics.precision_score(y_test_tfidf, y_pred_tfidf)
recall_tfidf = metrics.recall_score(y_test_tfidf, y_pred_tfidf)
F1 점수
이게 뭔지: 정밀도와 재현율의 조화평균. 둘 중 하나가 낮으면 F1도 낮아진다.
F1 = 2 × (정밀도 × 재현율) / (정밀도 + 재현율)
f1_bow = metrics.f1_score(y_test_bow, y_pred_bow)
f1_tfidf = metrics.f1_score(y_test_tfidf, y_pred_tfidf)
ROC-AUC
이게 뭔지: 모델이 긍정과 부정을 얼마나 잘 구분하는지 나타내는 단일 지표. 0.5 = 동전 던지기 수준, 1.0 = 완벽.
# predict_proba: 분류 결과(0/1)가 아닌 확률(0.0~1.0) 반환
y_pred_proba_tfidf = lr_tfidf.predict_proba(x_test_tfidf)[:, 1] # 긍정 확률만
roc_auc_tfidf = metrics.roc_auc_score(y_test_tfidf, y_pred_proba_tfidf)
어떤 지표를 더 중요하게 볼 것인가
정확도, 정밀도, 재현율, F1, AUC를 모두 배우는 이유는 “좋은 모델”의 기준이 문제마다 다르기 때문이다.
- 거짓 경보(False Positive)가 비싼 문제면 정밀도가 중요하다.
- 놓치면 안 되는 문제면 재현율이 중요하다.
- 둘을 균형 있게 보고 싶으면 F1이 적합하다.
- 임곗값 변화 전체를 보고 싶으면 ROC-AUC를 본다.
예를 들어 재난 감지 시스템에서는 실제 재난 트윗을 놓치는 것이 더 치명적일 수 있으므로, 단순 정확도보다 재현율을 더 중요하게 볼 수 있다.
기준:
5. 최종 예측 및 제출
test_original = pd.read_csv('test.csv')
# 주의: 테스트 데이터는 fit_transform이 아니라 transform만 사용
# fit_transform: 어휘를 새로 만들고 변환 (학습 데이터에만)
# transform: 기존 어휘 기준으로 변환 (테스트 데이터에 사용)
test_tfidf = tfidf_vect.transform(test_original['tweet'])
y_pred_final = lr_tfidf.predict(test_tfidf)
submission = pd.DataFrame({'id': test_original['id'], 'label': y_pred_final})
submission.to_csv('submission.csv', index=False)
테스트 데이터에 fit_transform()을 쓰면 안 되는 이유: 테스트 데이터의 단어 분포로 어휘 사전을 다시 만들면, 학습 때와 다른 기준으로 변환된다. 이를 **데이터 누수(data leakage)**라고 한다.
Day 3에서 실제로 배운 핵심
이번 수업은 “모델을 하나 돌렸다”보다, 전처리된 텍스트를 숫자로 만들고, 그 숫자 위에서 분류기를 학습하고, 여러 평가 지표로 해석하는 전체 흐름을 익힌 날이었다.
- 텍스트를 벡터화한다.
- 학습용/평가용 데이터를 분리한다.
- 분류 모델을 학습한다.
- 확률과 예측값을 뽑는다.
- 여러 지표로 성능을 해석한다.
핵심 정리
정제된 텍스트
↓ CountVectorizer 또는 TfidfVectorizer
숫자 행렬 (문서 × 단어)
↓ train_test_split
학습셋 / 테스트셋
↓ LogisticRegression.fit()
모델
↓ predict() / predict_proba()
평가: Accuracy, Precision, Recall, F1, ROC-AUC
정확도 하나로 모델을 판단하면 안 된다는 것이 이날의 핵심이다. 어떤 실수가 더 비싼지(FP vs FN)를 먼저 정의하고, 그에 맞는 지표를 선택해야 한다.
Community
Comments
Comments appear immediately. Use report if something needs review.
No comments yet.