Skip to main content

텍스트 분류 (Text Classification)

텍스트 분류는 입력 텍스트에 미리 정의된 **레이블(Label)**을 부여하는 태스크입니다. 스팸 필터링, 감성 분석, 주제 분류, 의도 인식 등 가장 널리 사용되는 NLP 태스크 중 하나입니다. 이 문서에서는 한국어 감성 분석 데이터셋을 사용하여 BERT 모델을 Fine-tuning하는 전체 과정을 실습합니다.

학습 목표

이 문서를 완료하면 다음을 수행할 수 있습니다.
  • HuggingFace datasets 라이브러리로 한국어 데이터셋을 로딩하고 전처리할 수 있습니다
  • BERT 기반 Sequence Classification 모델을 구성할 수 있습니다
  • HuggingFace Trainer를 사용하여 Fine-tuning을 수행할 수 있습니다
  • Accuracy, Precision, Recall, F1-Score로 모델을 평가할 수 있습니다

텍스트 분류의 유형

유형설명예시
이진 분류 (Binary)2개 클래스긍정/부정, 스팸/정상
다중 클래스 (Multi-class)N개 클래스 중 하나주제 분류 (정치, 경제, 스포츠, …)
다중 레이블 (Multi-label)여러 클래스 동시태그 부여 (영화 장르: 액션+SF)
**NSMC(Naver Sentiment Movie Corpus)**는 네이버 영화 리뷰 20만 건(훈련 15만, 테스트 5만)으로 구성된 이진 감성 분류 데이터셋입니다. 한국어 텍스트 분류의 사실상 표준 벤치마크로, 이 문서에서도 NSMC를 사용합니다.

실습: BERT로 감성 분석

1

환경 설정 및 데이터 로딩

필요한 라이브러리를 설치하고, NSMC 데이터셋을 로딩합니다.
pip install transformers datasets evaluate accelerate
from datasets import load_dataset

# NSMC 데이터셋 로딩
dataset = load_dataset("nsmc")
print(dataset)
# DatasetDict({
#     train: Dataset({features: ['id', 'document', 'label'], num_rows: 150000})
#     test:  Dataset({features: ['id', 'document', 'label'], num_rows: 50000})
# })

# 데이터 확인
print(dataset["train"][0])
# {'id': '9976970', 'document': '아 더빙.. 진짜 짜증나네요 목소리', 'label': 0}
label이 0이면 부정, 1이면 긍정입니다. 결측치(documentNone인 경우)를 제거합니다.
# 결측치 제거
dataset = dataset.filter(lambda x: x["document"] is not None)
print(f"훈련 데이터: {len(dataset['train']):,}건")
print(f"테스트 데이터: {len(dataset['test']):,}건")
2

토크나이저 적용

한국어 BERT 모델의 토크나이저를 사용하여 텍스트를 토큰화합니다.
from transformers import AutoTokenizer

model_name = "klue/bert-base"
tokenizer = AutoTokenizer.from_pretrained(model_name)

# 토크나이징 예시
sample = "이 영화 정말 재미있었어요!"
tokens = tokenizer(sample, return_tensors="pt")
print(tokenizer.convert_ids_to_tokens(tokens["input_ids"][0]))
# ['[CLS]', '이', '영화', '정말', '재미있', '##었', '##어요', '!', '[SEP]']
전체 데이터셋에 토크나이저를 일괄 적용합니다.
def tokenize_function(examples):
    """데이터셋 토크나이징 함수"""
    return tokenizer(
        examples["document"],
        padding="max_length",
        truncation=True,
        max_length=128,  # 리뷰 텍스트는 대부분 128 토큰 이내
    )

# 배치 단위 토크나이징 (num_proc으로 병렬 처리)
tokenized_dataset = dataset.map(
    tokenize_function,
    batched=True,
    num_proc=4,
    remove_columns=["id", "document"],  # 불필요한 컬럼 제거
)

# 모델이 기대하는 형식으로 포맷 설정
tokenized_dataset.set_format("torch")
print(tokenized_dataset["train"].column_names)
# ['label', 'input_ids', 'token_type_ids', 'attention_mask']
3

모델 준비 및 학습 설정

사전학습된 BERT에 분류 헤드(Classification Head)를 추가한 모델을 로딩합니다.
from transformers import AutoModelForSequenceClassification

model = AutoModelForSequenceClassification.from_pretrained(
    model_name,
    num_labels=2,  # 이진 분류
)
# BERT 본체 위에 Linear(768, 2) 분류 헤드가 추가됩니다
Trainer에 전달할 학습 인자를 설정합니다.
from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="./results",
    eval_strategy="epoch",          # 매 에포크마다 평가
    save_strategy="epoch",
    learning_rate=2e-5,             # BERT Fine-tuning 권장 lr
    per_device_train_batch_size=32,
    per_device_eval_batch_size=64,
    num_train_epochs=3,
    weight_decay=0.01,
    load_best_model_at_end=True,    # 최고 성능 모델 자동 복원
    metric_for_best_model="f1",
    logging_steps=500,
    fp16=True,                      # Mixed Precision 학습
)
4

평가 지표 정의 및 학습

분류 태스크의 핵심 평가 지표를 정의합니다.
import numpy as np
import evaluate

# 평가 지표 로딩
accuracy = evaluate.load("accuracy")
precision_metric = evaluate.load("precision")
recall_metric = evaluate.load("recall")
f1_metric = evaluate.load("f1")

def compute_metrics(eval_pred):
    """Trainer에 전달할 평가 함수"""
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)

    return {
        "accuracy": accuracy.compute(
            predictions=predictions, references=labels
        )["accuracy"],
        "precision": precision_metric.compute(
            predictions=predictions, references=labels
        )["precision"],
        "recall": recall_metric.compute(
            predictions=predictions, references=labels
        )["recall"],
        "f1": f1_metric.compute(
            predictions=predictions, references=labels
        )["f1"],
    }
Trainer를 생성하고 학습을 실행합니다.
from transformers import Trainer

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset["train"],
    eval_dataset=tokenized_dataset["test"],
    compute_metrics=compute_metrics,
)

# 학습 시작
trainer.train()
학습이 완료되면 자동으로 최고 성능 모델이 복원됩니다.
5

모델 평가 및 추론

테스트 데이터셋에서 최종 성능을 확인합니다.
# 테스트셋 평가
results = trainer.evaluate()
print(f"Accuracy: {results['eval_accuracy']:.4f}")
print(f"F1-Score: {results['eval_f1']:.4f}")
# klue/bert-base 기준 약 Accuracy 0.90, F1 0.90 수준
학습된 모델로 새로운 텍스트를 추론합니다.
from transformers import pipeline

# 추론 파이프라인 생성
classifier = pipeline(
    "text-classification",
    model=trainer.model,
    tokenizer=tokenizer,
    device=0,  # GPU 사용
)

# 새로운 리뷰 예측
texts = [
    "연기도 좋고 스토리도 탄탄해서 몰입감이 대단했습니다",
    "시간 아까운 영화. 다시는 안 봄",
    "그저 그런 영화였어요. 나쁘지도 좋지도 않은",
]

predictions = classifier(texts)
for text, pred in zip(texts, predictions):
    label = "긍정" if pred["label"] == "LABEL_1" else "부정"
    print(f"[{label} ({pred['score']:.2f})] {text}")
6

모델 저장 및 허브 업로드

학습된 모델을 로컬에 저장하거나 HuggingFace Hub에 업로드합니다.
# 로컬 저장
trainer.save_model("./nsmc-bert-classifier")
tokenizer.save_pretrained("./nsmc-bert-classifier")

# HuggingFace Hub 업로드 (선택 사항)
# trainer.push_to_hub("my-nsmc-classifier")
저장된 모델은 나중에 pipeline으로 바로 로딩할 수 있습니다.
# 저장된 모델 로딩
classifier = pipeline(
    "text-classification",
    model="./nsmc-bert-classifier",
)

평가 지표 이해

지표수식설명
Accuracy(TP + TN) / 전체전체 데이터 중 올바르게 분류한 비율
PrecisionTP / (TP + FP)긍정으로 예측한 것 중 실제 긍정 비율
RecallTP / (TP + FN)실제 긍정 중 올바르게 예측한 비율
F1-Score2 * P * R / (P + R)Precision과 Recall의 조화 평균
클래스 불균형 주의: 데이터셋의 클래스 비율이 불균형할 경우 Accuracy만으로는 모델 성능을 정확히 판단할 수 없습니다. 예를 들어 긍정 95%, 부정 5%인 데이터셋에서 모든 것을 긍정으로 예측해도 Accuracy는 95%입니다. F1-Score를 주요 지표로 사용하세요.

실전 팁: 다중 클래스 분류

KLUE-TC(Topic Classification)는 7개 주제(정치, 경제, 사회, 생활, 세계, IT/과학, 스포츠)로 분류하는 다중 클래스 데이터셋입니다.
# KLUE-TC 데이터셋 로딩
klue_tc = load_dataset("klue", "ynat")

# num_labels만 변경하면 동일한 파이프라인 사용 가능
model = AutoModelForSequenceClassification.from_pretrained(
    "klue/bert-base",
    num_labels=7,  # 7개 클래스
)
다중 클래스에서는 macro 또는 weighted 평균 F1-Score를 사용합니다.
f1_metric = evaluate.load("f1")

def compute_metrics_multiclass(eval_pred):
    logits, labels = eval_pred
    predictions = np.argmax(logits, axis=-1)
    return {
        "f1_macro": f1_metric.compute(
            predictions=predictions,
            references=labels,
            average="macro",  # 클래스별 F1의 단순 평균
        )["f1"],
        "f1_weighted": f1_metric.compute(
            predictions=predictions,
            references=labels,
            average="weighted",  # 클래스 비율 가중 평균
        )["f1"],
    }
per_device_train_batch_size를 줄이세요 (32 → 16 → 8). GPU 메모리에 따라 적절한 배치 크기가 다릅니다. fp16=True 설정으로 메모리 사용량을 약 절반으로 줄일 수 있습니다. gradient_accumulation_steps를 함께 늘려 실효 배치 크기를 유지하세요.
Learning rate가 너무 높거나 낮을 수 있습니다. BERT Fine-tuning에는 2e-5 ~ 5e-5 범위가 권장됩니다. 데이터 전처리 과정에서 label 컬럼이 올바르게 매핑되었는지 확인하세요.
과적합(Overfitting)의 징후입니다. weight_decay를 늘리거나, num_train_epochs를 줄이세요. load_best_model_at_end=True가 설정되어 있다면 최고 성능 모델이 자동으로 복원됩니다.
모델의 토크나이저가 한국어를 충분히 커버하지 못하는 경우입니다. bert-base-multilingual-cased 대신 klue/bert-basemonologg/kobert 같은 한국어 특화 모델을 사용하세요.

다음 문서