AICE Professional 2번 문제 완전 정복 — Text 이진 분류

들어가며: 다중 분류 코드에서 네 군데만 바꾸면 됩니다

AICE Professional 시험의 2번 Text 문제는 다중 분류가 나올 수도 있지만, **이진 분류(Binary Classification)**가 출제될 가능성도 있습니다. 이전 글에서 Text 다중 분류를 다뤘으니, 이번에는 이진 분류 유형을 정리합니다.

저는 2번 문제에서 만점을 받았습니다. 이진 분류든 다중 분류든 전처리와 토크나이저 과정은 완전히 동일하고, 출력층, 손실 함수, 예측 후처리, 임계값 설정 네 가지만 달라집니다. 다중 분류 코드를 확실히 익혀 둔 분이라면, 이진 분류 전환은 5분이면 충분합니다.


AICE Professional 시험 구조 (복습)

문제유형배점
1번Tabular (정형 데이터)30점
2번Text (텍스트 데이터)35점
3번Image (이미지 데이터)35점

합격 기준은 80점 이상입니다. 2번은 35점 배점으로 비중이 큰 만큼, 확실히 가져가야 할 점수입니다.


2번 문제: Text 이진 분류 — 출제 유형 분석

예상 문제 형태

텍스트 컬럼(문의 메시지, 리뷰, 문장 등)과 이진 라벨(긍정/부정, 스팸/정상 등)이 주어지고, 테스트 텍스트가 두 클래스 중 어디에 속하는지 예측하는 문제입니다.

핵심 포인트는 다음과 같습니다.

  • 전처리와 토크나이저 과정은 다중 분류와 완전히 동일합니다
  • 출력층이 Dense(1, sigmoid)로 변경됩니다
  • 손실 함수가 binary_crossentropy로 변경됩니다
  • 예측 시 argmax 대신 0.5 임계값으로 0/1을 판정합니다

이진 분류 vs 다중 분류 — 차이점 한눈에 보기

Text 문제에서 이진/다중 전환 시 바꿔야 할 부분은 정확히 네 곳입니다.

항목이진 분류다중 분류
출력층Dense(1, sigmoid)Dense(num_classes, softmax)
손실 함수binary_crossentropysparse_categorical_crossentropy
예측 변환proba >= 0.5 → 0/1argmax → 클래스 인덱스
확률 출력 형태(N, 1) → .ravel() 필요(N, K) → 그대로 사용

나머지는 전부 동일합니다. 전처리, 토크나이저, 패딩, Embedding + BiLSTM 구조, 콜백 설정, LabelEncoder, inverse_transform 모두 그대로 사용합니다.


풀이 전략: 8단계 접근법

Step 0. 환경 설정

python

import re
import numpy as np
import pandas as pd
from pathlib import Path

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

import tensorflow as tf
from tensorflow.keras.preprocessing.text import Tokenizer
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras import layers, callbacks
from tensorflow.keras.models import Sequential

# ── 경로 설정 ──
DATA_DIR = Path('.')
TRAIN_CSV = DATA_DIR / '02_train.csv'
TEST_CSV  = DATA_DIR / '02_test_x.csv'

PHONE = '01012345678'
OUT_CSV   = DATA_DIR / f'{PHONE}_2.csv'
OUT_KERAS = DATA_DIR / f'{PHONE}_2.keras'

TEXT_COL   = 'reqmsg'              # 텍스트 컬럼명 (문제에 따라 변경)
TARGET_COL = 'category'            # 라벨 컬럼명 (문제에 따라 변경)

# ── 토크나이저/시퀀스 설정 ──
NUM_WORDS  = 30000
OOV_TOKEN  = '<OOV>'
PCT_LEN    = 0.95

# ── 모델 하이퍼파라미터 ──
EMBED_DIM  = 128
LSTM_UNITS = 64
DROPOUT_R  = 0.3
BATCH_SIZE = 64
EPOCHS     = 15
PATIENCE   = 3
SEED       = 42
THRESHOLD  = 0.5                   # 이진 분류 판정 임계값

다중 분류 코드와 거의 동일하지만, THRESHOLD = 0.5가 추가되었습니다. sigmoid 출력이 이 값 이상이면 양성(1), 미만이면 음성(0)으로 판정합니다.

Step 1. 데이터 로드

python

train_df = pd.read_csv(TRAIN_CSV)
test_df  = pd.read_csv(TEST_CSV)

# 테스트 ID 컬럼 찾기
ID_COL_CANDIDATES = ['id', 'ID', 'index', 'sample_id', 'reqid']
test_id_col = None
for c in ID_COL_CANDIDATES:
    if c in test_df.columns:
        test_id_col = c
        break
if test_id_col is None:
    test_id_col = 'id'
    test_df = test_df.copy()
    test_df[test_id_col] = np.arange(len(test_df))

Step 2. 텍스트 전처리 — 다중 분류와 동일

python

def clean_text(s: str) -> str:
    if not isinstance(s, str):
        s = '' if pd.isna(s) else str(s)
    s = s.lower()
    s = re.sub(r'[^0-9a-z가-힣\s]', ' ', s)
    s = re.sub(r'\s+', ' ', s).strip()
    return s

train_df[TEXT_COL] = train_df[TEXT_COL].apply(clean_text)
test_df[TEXT_COL]  = test_df[TEXT_COL].apply(clean_text)

전처리 함수는 다중 분류와 완전히 동일합니다. 영문/숫자/한글/공백만 남기고 나머지를 제거합니다. 문제에서 “한글만 남기세요”라고 지시하면 정규식을 [^가-힣 ]으로 변경하면 됩니다.

Step 3. 라벨 인코딩 및 학습/검증 분리

python

# 라벨 인코딩
le = LabelEncoder()
y = le.fit_transform(train_df[TARGET_COL].values)
classes = list(le.classes_)
print('Classes:', classes, '(binary)')

# 학습/검증 분리
X_train_text, X_val_text, y_train, y_val = train_test_split(
    train_df[TEXT_COL].values, y,
    test_size=0.2, random_state=SEED, stratify=y
)

이진 분류에서도 LabelEncoder를 사용합니다. 라벨이 이미 0/1이더라도 적용해 두면, 나중에 inverse_transform()으로 안전하게 원래 라벨명을 복원할 수 있습니다.

Step 4. 토크나이저 & 시퀀스 패딩 — 다중 분류와 동일

python

tk = Tokenizer(num_words=NUM_WORDS, oov_token=OOV_TOKEN)
tk.fit_on_texts(X_train_text)

seq_train = tk.texts_to_sequences(X_train_text)
seq_val   = tk.texts_to_sequences(X_val_text)
seq_test  = tk.texts_to_sequences(test_df[TEXT_COL].values)

lengths = np.array([len(s) for s in seq_train if len(s) > 0] + [1])
max_len = int(np.percentile(lengths, PCT_LEN * 100))
max_len = max(16, min(max_len, 256))
print('Max sequence length:', max_len)

X_train = pad_sequences(seq_train, maxlen=max_len, padding='post', truncating='post')
X_val   = pad_sequences(seq_val,   maxlen=max_len, padding='post', truncating='post')
X_test  = pad_sequences(seq_test,  maxlen=max_len, padding='post', truncating='post')

토크나이저와 패딩 과정은 이진/다중 구분 없이 완전히 동일합니다. 다중 분류 글에서 설명한 내용을 그대로 적용하면 됩니다.

Step 5. 모델 구성 — 여기서 차이가 생깁니다

python

vocab_size = min(NUM_WORDS, len(tk.word_index) + 1)

model = Sequential()
model.add(layers.Embedding(
    input_dim=vocab_size, output_dim=EMBED_DIM, input_length=max_len
))
model.add(layers.SpatialDropout1D(0.2))
model.add(layers.Bidirectional(layers.LSTM(LSTM_UNITS, return_sequences=False)))
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dropout(DROPOUT_R))
model.add(layers.Dense(1, activation='sigmoid'))        # ← 이진 분류

model.compile(
    optimizer='adam',
    loss='binary_crossentropy',                          # ← 이진 분류
    metrics=['accuracy']
)
model.summary()

다중 분류 코드에서 딱 두 줄만 변경했습니다.

  • 출력층: Dense(num_classes, softmax)Dense(1, sigmoid)
  • 손실 함수: sparse_categorical_crossentropybinary_crossentropy

나머지 Embedding, SpatialDropout1D, BiLSTM, Dense(64), Dropout 구조는 전부 동일합니다.

모델 구조를 한눈에 정리하면 이렇습니다.

Input(정수 시퀀스) → Embedding → SpatialDropout → BiLSTM → Dense(64, relu) → Dropout → Dense(1, sigmoid)

Step 6. 콜백 설정 및 학습

python

cb = [
    callbacks.EarlyStopping(
        monitor='val_accuracy', patience=PATIENCE,
        mode='max', restore_best_weights=True
    ),
    callbacks.ModelCheckpoint(
        filepath=str(OUT_KERAS),
        monitor='val_accuracy', mode='max',
        save_best_only=True
    )
]

hist = model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=EPOCHS,
    batch_size=BATCH_SIZE,
    callbacks=cb,
    verbose=1
)

val_loss, val_acc = model.evaluate(X_val, y_val, verbose=0)
print(f'[VAL] accuracy = {val_acc:.4f}')

콜백 설정도 다중 분류와 동일합니다. val_accuracy를 모니터링하면서 mode='max'로 설정하는 것, 잊지 마세요.

Step 7. 테스트 예측 및 제출 — 여기서도 차이가 있습니다

python

best_model = tf.keras.models.load_model(OUT_KERAS)

# sigmoid 출력: (N, 1) → 1차원으로 변환
proba = best_model.predict(X_test, batch_size=BATCH_SIZE, verbose=1).ravel()

# 임계값 0.5 기준으로 0/1 판정
pred_idx = (proba >= THRESHOLD).astype(int)

# 원래 라벨명으로 복원
pred_lbl = le.inverse_transform(pred_idx)

# 제출 CSV 저장
submit = pd.DataFrame({
    test_id_col: test_df[test_id_col],
    TARGET_COL: pred_lbl
})
submit.to_csv(OUT_CSV, index=False, encoding='utf-8')

print('Saved:', OUT_CSV)
print('Saved:', OUT_KERAS)

이진 분류 예측의 핵심 차이점을 정리합니다.

단계다중 분류이진 분류
모델 출력 형태(N, K) 확률 행렬(N, 1) 확률값
평탄화불필요.ravel()로 1차원 변환 필요
클래스 결정argmax(axis=1)proba >= 0.5
결과클래스 인덱스 (0~K-1)0 또는 1

.ravel()을 잊으면 안 됩니다. sigmoid 출력은 shape이 (N, 1)이라서, 그대로 비교하면 차원 불일치가 발생할 수 있습니다. .ravel()(N,) 형태로 평탄화한 뒤 임계값 비교를 해야 합니다.


성능이 안 나올 때 — 간단 튜닝 가이드

우선순위조정 항목시도 값기대 효과
1EPOCHS15 → 20 → 30학습 부족 해소
2LSTM_UNITS64 → 96 → 128모델 용량 증가
3EMBED_DIM128 → 192 → 256임베딩 표현력 강화
4THRESHOLD0.5 → 0.4 → 0.6판정 기준 조정
5DROPOUT_R0.3 → 0.2 → 0.4과적합/과소적합 균형
6max_len 범위256 상한 → 384긴 문장 정보 보존

이진 분류에서 다중 분류와 다른 튜닝 포인트가 하나 있습니다. 임계값(THRESHOLD) 조정입니다. 기본값 0.5가 항상 최적은 아닙니다. 클래스 불균형이 있으면 0.4나 0.6으로 바꿔 보는 것만으로 Accuracy가 개선될 수 있습니다.


실전 체크리스트

시험장에서 제출 전, 이 순서대로 확인하세요.

  1. TEXT_COL, TARGET_COL 컬럼명이 문제와 일치하는가?
  2. 텍스트 전처리를 train과 test 모두에 적용했는가?
  3. Tokenizer.fit_on_texts학습 데이터로만 실행했는가?
  4. 출력층이 Dense(1, sigmoid)인가? (다중 분류와 혼동하지 않았는가?)
  5. 손실 함수가 binary_crossentropy인가?
  6. monitor='val_accuracy'mode='max'가 일치하는가?
  7. val_accuracy가 목표 기준을 넘겼는가?
  8. 예측 시 .ravel()로 평탄화한 뒤 임계값 비교를 했는가?
  9. inverse_transform()으로 원래 라벨명을 복원했는가?
  10. 제출 CSV 컬럼명과 .keras 파일이 정상 저장되었는가?

마무리

Text 이진 분류는 다중 분류 코드를 이미 익힌 분이라면 가장 빠르게 전환할 수 있는 유형입니다. 전처리부터 BiLSTM까지 전부 동일하고, 출력층과 손실 함수, 예측 후처리만 바꾸면 됩니다.

핵심을 한 문장으로 요약합니다. 출력은 sigmoid, 손실은 binary_crossentropy, 예측은 ravel() 후 0.5 기준. 이 세 가지만 기억하면 다중 분류 코드에서 2분 안에 이진 분류로 전환할 수 있습니다.

관련 글 보기

  • AICE Professional 3번 문제 완전 정복 — Image 이진 분류

    AICE Professional 시험 3번 Image 이진 분류 문제를 실전 경험과 실패 분석을 바탕으로 정리했습니다. MobileNetV2 전이학습, 2단계 파인튜닝, 전처리 일관성 확보까지 7단계 풀이 전략과 실전 체크리스트를 코드와 함께 제공합니다. Accuracy 95% 달성을 위한 튜닝 가이드도 포함되어 있습니다.

  • AICE Professional 1번 문제 완전 정복 — Tabular 다중 분류

    AICE Professional 시험 1번 Tabular 다중 분류 문제 풀이법을 실전 경험을 바탕으로 정리했습니다. RandomForestClassifier와 LabelEncoder를 활용한 6단계 접근법, 회귀와 분류의 핵심 차이점, 클래스 불균형 대응 튜닝 가이드까지 코드와 체크리스트로 한 번에 확인하세요.

  • AICE Professional 1번 문제 완전 정복 — Tabular 이진 분류

    AICE Professional 시험 1번 Tabular 이진 분류 문제 풀이법을 실전 경험을 바탕으로 정리했습니다. RandomForestClassifier를 활용한 6단계 접근법, 회귀·이진·다중 분류 유형별 비교표, ROC-AUC 계산 시 주의사항, 클래스 불균형 대응 튜닝 가이드까지 코드와 체크리스트로 확인하세요.

  • AICE Professional 1번 문제 완전 정복 — Tabular 회귀

    AICE Professional 시험 1번 Tabular 회귀 문제 풀이법을 실전 경험을 바탕으로 정리했습니다. RandomForestRegressor로 RMSE 83 이하를 달성하는 5단계 접근법, 하이퍼파라미터 튜닝 가이드, 제출 시 주의사항까지 한 번에 확인하세요. 시험 준비에 실질적으로 도움이 되는 코드와 체크리스트를 제공합니다.