자연어 처리로 문서 분류하기: TF-IDF·로지스틱 회귀 기준선 만들기

반응형

문서 분류의 가장 단순하고 강한 기준선은 텍스트를 TF-IDF 벡터로 바꾸고 선형 분류기를 학습하는 것이다. 형태소 분석이나 거대한 언어 모델부터 시작하지 않아도, 데이터 분할과 평가를 제대로 하면 뉴스 주제·문의 유형·스팸 분류 같은 문제의 첫 결과를 빠르게 만들 수 있다.

기존 노트에는 형태소, 코사인 유사도, 로지스틱 회귀, 혼동 행렬까지 관련 용어를 거의 모두 넣었다. 다시 보니 실제로 모델을 만드는 순서와 각 단계의 선택 이유가 잘 보이지 않았다. 이번에는 데이터 확인 → 분할 → 벡터화 → 학습 → 오류 분석 흐름으로 정리했다.

문서 분류는 어떤 문제인가

입력은 문서이고 출력은 미리 정한 카테고리다. 예를 들어 고객 문의를 결제, 배송, 환불로 나누거나 뉴스를 정치, 경제, 스포츠로 분류할 수 있다.

지도학습이므로 각 문서에는 정답 라벨이 필요하다. 모델보다 먼저 확인할 것은 데이터다.

  • 카테고리 정의가 서로 겹치지 않는가
  • 라벨을 붙인 사람마다 기준이 같은가
  • 클래스별 문서 수가 지나치게 불균형하지 않은가
  • 같은 기사나 템플릿 문장이 학습·테스트에 동시에 들어가지 않았는가
  • 시간 순서가 중요한 문제인데 무작위로 섞지는 않았는가

“정확도는 보통 70~90%”처럼 일반적인 숫자를 기대값으로 두면 안 된다. 라벨의 난이도, 데이터 중복, 클래스 수와 평가 방식에 따라 점수의 의미가 완전히 달라진다.

텍스트를 숫자로 바꾸는 가장 단순한 방법

Bag-of-Words는 문서에 어떤 토큰이 몇 번 나타났는지를 센다. TF-IDF는 여기에 전체 문서에서 흔한 단어의 가중치를 낮추고, 특정 문서에서 상대적으로 두드러지는 단어의 가중치를 높인다.

직관만 남기면 다음과 같다.

TF-IDF = 문서 안에서의 단어 빈도 × 전체 문서에서의 희귀성

scikit-learn의 TfidfVectorizer는 토큰 수 집계, IDF 가중치와 정규화를 한 번에 처리해 희소 행렬을 만든다. 기본 L2 정규화를 사용하면 두 문서 벡터의 내적이 코사인 유사도와 연결된다.

한국어는 토큰화를 어떻게 할까

한국어 텍스트라고 항상 형태소 분석기가 필수인 것은 아니다. 기준선을 만들 때는 몇 가지 선택을 비교해 볼 수 있다.

방식 장점 주의점
띄어쓰기 기반 word n-gram 빠르고 설명하기 쉽다. 조사·어미와 띄어쓰기 변형에 민감하다.
character n-gram 오탈자와 활용형 변화에 비교적 강하다. 특성 수가 커지고 설명이 어려워질 수 있다.
형태소 분석 어간·품사 단위로 다룰 수 있다. 사전·설치·도메인 용어 관리가 필요하다.

처음부터 하나를 정답으로 두기보다 동일한 데이터 분할에서 word n-gram, character n-gram, 형태소 토큰을 비교하는 편이 낫다. 형태소 분석을 쓴다면 학습과 예측에서 같은 전처리기와 사전 버전을 사용해야 한다.

데이터 누수를 막는 분할 순서

TF-IDF의 vocabulary와 IDF는 학습 데이터에서만 배워야 한다. 전체 문서로 벡터라이저를 먼저 학습한 뒤 테스트 세트를 나누면 테스트 문서의 단어 분포가 학습 단계에 섞인다.

안전한 순서는 다음과 같다.

  1. 원문과 라벨을 학습·테스트로 먼저 나눈다.
  2. 학습 문서로만 TfidfVectorizer.fit을 수행한다.
  3. 같은 벡터라이저로 테스트 문서를 transform한다.
  4. 학습 세트로 분류기를 학습하고 테스트 세트는 마지막 평가에만 쓴다.

Pipeline을 사용하면 이 경계를 코드로 묶을 수 있다.

TF-IDF와 로지스틱 회귀 기준선 만들기

CSV에 text, label 열이 있다고 가정한 최소 예제다.

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix

df = pd.read_csv("documents.csv").dropna(subset=["text", "label"])

X_train, X_test, y_train, y_test = train_test_split(
    df["text"],
    df["label"],
    test_size=0.2,
    random_state=42,
    stratify=df["label"],
)

model = Pipeline([
    ("tfidf", TfidfVectorizer(
        ngram_range=(1, 2),
        min_df=2,
        sublinear_tf=True,
    )),
    ("classifier", LogisticRegression(max_iter=1000)),
])

model.fit(X_train, y_train)
pred = model.predict(X_test)

print(classification_report(y_test, pred))
print(confusion_matrix(y_test, pred))

이 코드는 완성된 정답이 아니라 비교 기준이다. 데이터가 적다면 min_df=2 때문에 중요한 희귀 단어가 사라질 수 있고, 클래스가 크게 불균형하다면 class_weight="balanced"가 후보가 될 수 있다. 설정은 반드시 검증 결과와 오류 사례를 보고 정한다.

정확도만 보면 놓치는 것

클래스 비율이 불균형하면 accuracy가 높아도 필요한 문서를 거의 찾지 못할 수 있다.

  • Precision: 해당 클래스로 예측한 문서 중 실제로 맞은 비율
  • Recall: 실제 해당 클래스 문서 중 모델이 찾아낸 비율
  • F1: precision과 recall의 조화 평균
  • Confusion matrix: 어떤 클래스를 어떤 클래스로 잘못 분류하는지 보여 주는 표

다중 클래스에서는 macro F1weighted F1의 차이도 본다. macro 평균은 각 클래스를 같은 비중으로 보므로 소수 클래스의 실패가 드러난다. weighted 평균은 문서 수가 많은 클래스의 영향이 더 크다.

R²는 회귀 문제의 지표이므로 문서 카테고리 분류 평가 목록에 섞지 않는다.

점수를 올리기 전에 오류를 읽는다

모델을 바꾸기 전에 오분류 문서를 직접 읽으면 다음 실험이 구체적이 된다.

  1. 라벨 기준이 애매하면 데이터 정의부터 고친다.
  2. 도메인 복합어가 잘리면 n-gram이나 사용자 사전을 바꾼다.
  3. 제목과 본문이 다른 역할을 하면 특성을 분리한다.
  4. 중복 템플릿이 점수를 부풀리면 그룹 단위로 분할한다.
  5. 문맥과 어순이 핵심이라면 그때 임베딩이나 Transformer 계열 모델을 비교한다.

고전적인 TF-IDF 모델은 최신 모델보다 단순하지만 빠르고, 중요한 단어를 확인하기 쉬우며, 학습·서빙 비용이 작다. 복잡한 모델이 실제로 얼마나 더 나은지 판단할 기준선으로 가치가 있다. AI 학습 범위 전체는 ML 개념부터 NLP 모델까지의 공부 지도에서 이어서 볼 수 있다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글