AICE Professional 2번 문제 완전 정복 — Text 다중 분류

들어가며: 2번은 패턴만 잡으면 만점입니다

AICE Professional 시험의 2번 문제는 텍스트 데이터를 분류하는 NLP 문제입니다. 영문, 한글, 특수문자가 섞인 텍스트가 주어지고, 이것이 어떤 카테고리에 속하는지 예측해야 합니다.

저는 이 2번 문제에서 만점을 받았습니다. 1번 Tabular도 만점이었지만, 3번 Image에서 불합격한 경험이 있습니다. 2번은 텍스트 전처리와 LSTM 모델 구성이라는 정해진 패턴이 있어서, 이 패턴을 확실히 익혀 두면 안정적으로 만점을 받을 수 있습니다.


AICE Professional 시험 구조 (복습)

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

합격 기준은 80점 이상입니다. 2번은 35점 배점으로, 1번(30점)보다 비중이 큽니다. 확실히 가져가야 할 점수입니다.


2번 문제: Text 다중 분류 — 출제 유형 분석

예상 문제 형태

텍스트 컬럼(예: 문의 메시지, 리뷰, 문장 등)과 카테고리 라벨이 주어지고, 테스트 텍스트의 카테고리를 예측하는 다중 분류 문제가 출제됩니다.

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

  • 텍스트에 영문, 숫자, 한글, 특수문자가 섞여 있습니다
  • 전처리로 특수문자를 제거하고 영문/숫자/한글/공백만 남깁니다
  • 모델은 Embedding + BiLSTM + Dense 구조를 사용합니다
  • 평가 기준은 val_accuracy 56% 이상 (문제에 따라 다를 수 있음)
  • 제출 파일: 예측 CSV + .keras 모델 파일

1번 Tabular과 달리 sklearn이 아닌 TensorFlow/Keras를 사용해야 하고, 토크나이저와 시퀀스 패딩이라는 NLP 특유의 전처리가 필요합니다. 처음 접하면 복잡하게 느껴지지만, 각 단계의 역할을 이해하면 기계적으로 적용할 수 있습니다.


풀이 전략: 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, models, 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                  # 문장 길이 95% 분위수 사용

# ── 모델 하이퍼파라미터 ──
EMBED_DIM  = 128
LSTM_UNITS = 64
DROPOUT_R  = 0.3
BATCH_SIZE = 64
EPOCHS     = 15
PATIENCE   = 3
SEED       = 42

하이퍼파라미터 각각의 의미를 정리해 두겠습니다.

파라미터역할
NUM_WORDS30000빈도 상위 30,000개 단어만 사전에 포함
OOV_TOKEN‘<OOV>’사전에 없는 단어를 이 토큰으로 대체
PCT_LEN0.95상위 5% 긴 문장은 잘라내고, 95%까지만 커버
EMBED_DIM128단어 임베딩 벡터 차원
LSTM_UNITS64LSTM 은닉 상태 크기
DROPOUT_R0.3과적합 방지를 위한 드롭아웃 비율

Step 1. 데이터 로드

python

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

# 테스트 ID 컬럼 찾기 (없으면 index 사용)
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))

팁: 테스트 CSV의 ID 컬럼명은 문제마다 다를 수 있습니다. 여러 후보를 미리 리스트에 넣어 두고 자동으로 찾는 방식이 시험장에서 안전합니다.

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)

전처리는 이 한 함수로 충분합니다. 핵심은 영문, 숫자, 한글, 공백 외의 모든 문자를 제거하는 것입니다. 정규식 [^0-9a-z가-힣\s]가 이 역할을 합니다.

만약 문제에서 “한글만 남기세요”라고 지시하면, 정규식을 [^가-힣 ]으로 변경하면 됩니다.

python

# 한글만 남기는 경우
s = re.sub(r'[^가-힣 ]', '', s)

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

python

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

# 학습/검증 분리 (stratify 필수)
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
)

1번 Tabular 다중 분류와 마찬가지로, LabelEncoder로 문자열 라벨을 정수로 변환하고 stratify=y로 클래스 비율을 유지합니다.

Step 4. 토크나이저 & 시퀀스 패딩

python

# 토크나이저: 학습 텍스트로만 fit
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)

# 시퀀스 최대 길이 결정: 95% 분위수 기준
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))    # 16~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')

이 단계가 Text 문제의 핵심입니다. 각 과정의 역할을 정리하면 이렇습니다.

과정입력출력역할
Tokenizer.fit_on_texts원문 텍스트(내부 사전 구축)단어별 정수 인덱스 생성
texts_to_sequences원문 텍스트정수 리스트“안녕 세계” → [5, 12]
pad_sequences가변 길이 리스트고정 길이 배열짧으면 0으로 채움, 길면 자름

padding='post'를 사용하는 이유: 문장 앞부분(의미가 있는 부분)을 그대로 두고, 뒤쪽에 0을 채웁니다. LSTM이 문장을 앞에서부터 읽으므로, 의미 있는 토큰이 앞에 있는 것이 학습에 유리합니다.

fit_on_texts는 학습 데이터로만: 검증/테스트 데이터의 단어까지 사전에 포함하면 데이터 누출(leakage)이 발생합니다. 토크나이저는 반드시 학습 텍스트로만 fit하세요.

Step 5. 모델 구성 — Embedding + BiLSTM

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(num_classes, activation='softmax'))

model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy']
)
model.summary()

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

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

각 레이어의 역할도 알아 두면 좋습니다.

레이어역할
Embedding정수 인덱스를 고정 차원 벡터로 변환 (단어의 의미를 학습)
SpatialDropout1D임베딩 차원 단위로 드롭아웃 (단어 수준 정규화)
Bidirectional(LSTM)문장을 앞→뒤, 뒤→앞 양방향으로 읽어 문맥 파악
Dense(64, relu)비선형 변환으로 표현력 확보
Dropout과적합 방지
Dense(softmax)클래스별 확률 출력

sparse_categorical_crossentropy를 쓰는 이유: 라벨이 정수(0, 1, 2, …)이므로, 원-핫 인코딩 없이 바로 사용할 수 있는 sparse 버전을 씁니다. 원-핫으로 변환했다면 categorical_crossentropy를 사용해야 합니다.

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, save_format='keras'
    )
]

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} (target >= 0.56)')

콜백에서 주의할 점이 있습니다.

monitor 값mode의미
val_accuracymax정확도가 높을수록 좋음
val_lossmin손실이 낮을수록 좋음

monitormode를 잘못 맞추면 EarlyStopping이 엉뚱하게 동작합니다. val_accuracy를 모니터링하면서 mode=’min’으로 설정하면, 정확도가 떨어질 때 “좋아지고 있다”고 판단하는 실수가 생깁니다.

Step 7. 테스트 예측 및 제출 파일 저장

python

# 저장된 베스트 모델 로드 (안전)
best_model = tf.keras.models.load_model(OUT_KERAS)

# 예측: softmax 확률 → argmax → 원래 라벨명 복원
proba = best_model.predict(X_test, batch_size=BATCH_SIZE, verbose=1)
pred_idx = proba.argmax(axis=1)
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)

1번 Tabular 다중 분류와 마찬가지로, inverse_transform()으로 정수 인덱스를 원래 카테고리명으로 복원해야 합니다. 이 변환을 빠뜨리고 정수를 그대로 제출하면 채점에서 틀립니다.

ModelCheckpoint로 저장한 베스트 모델을 다시 로드하는 이유: 학습 중 val_accuracy가 가장 높았던 시점의 가중치를 사용하기 위해서입니다. EarlyStopping의 restore_best_weights=True가 있어 사실상 같은 결과이지만, 파일에서 로드하는 것이 더 확실합니다.


성능이 56%에 못 미칠 때 — 간단 튜닝 가이드

우선순위조정 항목시도 값기대 효과
1EPOCHS15 → 20 → 30학습 부족 해소
2LSTM_UNITS64 → 96 → 128모델 용량 증가
3EMBED_DIM128 → 192 → 256임베딩 표현력 강화
4NUM_WORDS30000 → 40000 → 60000더 많은 단어 커버
5max_len 범위256 상한 → 384긴 문장 정보 보존
6DROPOUT_R0.3 → 0.2 → 0.4과적합/과소적합 균형
7모델 구조 변경LSTM → GRUGRU가 더 빠르고 비슷한 성능

시간 여유가 있다면 LSTM을 2층으로 쌓는 것도 효과적입니다.

python

# 2층 Stacked LSTM
model.add(layers.Bidirectional(
    layers.LSTM(LSTM_UNITS, return_sequences=True)    # 첫 번째: return_sequences=True
))
model.add(layers.Bidirectional(
    layers.LSTM(LSTM_UNITS, return_sequences=False)   # 두 번째: return_sequences=False
))

return_sequences=True는 모든 시점의 출력을 다음 레이어로 전달합니다. 마지막 LSTM만 False로 설정하면 최종 시점의 출력만 Dense 층으로 넘깁니다.


실전 체크리스트

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

  1. TEXT_COL, TARGET_COL 컬럼명이 문제와 일치하는가?
  2. 텍스트 전처리(특수문자 제거)를 train과 test 모두에 적용했는가?
  3. Tokenizer.fit_on_texts학습 데이터로만 실행했는가?
  4. LabelEncoder로 라벨을 인코딩했는가?
  5. train_test_splitstratify=y를 넣었는가?
  6. monitormode가 일치하는가? (val_accuracy → max, val_loss → min)
  7. val_accuracy가 목표 기준(예: 56%)을 넘겼는가?
  8. inverse_transform()으로 예측값을 원래 카테고리명으로 복원했는가?
  9. 제출 CSV 컬럼명과 포맷이 문제 요구사항과 일치하는가?
  10. .keras 모델 파일이 정상 저장되었는가?

마무리

2번 Text 문제는 NLP라는 이름 때문에 어렵게 느껴질 수 있지만, 풀이 패턴은 명확하게 정해져 있습니다. 텍스트 정제 → 토크나이저 → 패딩 → Embedding + BiLSTM → 예측 → inverse_transform. 이 흐름을 한 번 확실히 익혀 두면, 시험장에서는 컬럼명과 하이퍼파라미터만 바꿔 넣으면 됩니다.

핵심을 세 가지로 요약합니다. 전처리는 정규식 한 줄, 토크나이저는 학습 데이터로만, 예측은 반드시 inverse_transform. 이 세 가지만 놓치지 않으면 35점은 확보할 수 있습니다.

관련 글 보기

  • AICE Professional 3번 문제 완전 정복 — Image 다중 분류

    AICE Professional 시험 3번 Image 다중 분류 문제 풀이법을 실전 경험을 바탕으로 정리했습니다. MobileNetV2 전이학습과 2단계 파인튜닝으로 접근하는 8단계 풀이 전략, 이진 분류와 다중 분류의 핵심 차이점, 제출 시 주의사항까지 코드와 체크리스트로 한 번에 확인하세요.

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

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

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

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

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

    AICE Professional 시험 2번 Text 이진 분류 문제 풀이법을 실전 만점 경험을 바탕으로 정리했습니다. 다중 분류 코드에서 출력층, 손실 함수, 예측 후처리 네 군데만 변경하면 됩니다. sigmoid 출력 처리, 임계값 설정, ravel() 주의사항까지 코드와 체크리스트로 확인하세요.