들어가며: 3번이 가장 까다롭습니다
AICE Professional 시험에 직접 도전해 본 경험을 바탕으로, 이번에는 3번 Image 다중 분류 문제의 예상 풀이를 정리합니다. 솔직히 말씀드리면, 저도 이 3번 문제에서 막혀 불합격했습니다. 1, 2번은 만점이었지만 3번 하나를 놓치면서 80점을 넘기지 못했습니다.
그만큼 3번은 세 문제 중 난이도가 가장 높고, 실수하기도 쉬운 문제입니다. 하지만 풀이 패턴 자체는 정형화되어 있어서, 구조를 확실히 익혀 두면 충분히 만점을 노릴 수 있습니다.
AICE Professional 시험 구조 (복습)
| 문제 | 유형 | 배점 |
|---|---|---|
| 1번 | Tabular (정형 데이터) | 30점 |
| 2번 | Text (텍스트 데이터) | 35점 |
| 3번 | Image (이미지 데이터) | 35점 |
합격 기준은 80점 이상입니다. 부분 점수가 있긴 하지만, 거의 다 맞아야 의미 있는 점수가 나옵니다. 한 문제라도 완전히 틀리면 사실상 불합격이므로, 세 문제 모두 확실하게 마무리하는 전략이 필요합니다.
3번 문제: Image 다중 분류 — 출제 유형 분석
예상 문제 형태
학습 이미지가 클래스별 폴더로 분류되어 제공되고, 테스트 이미지의 클래스명을 예측하는 다중 분류(Multi-class Classification) 문제가 출제됩니다.
핵심 포인트는 다음과 같습니다.
- 학습 데이터:
train/<class_1>,train/<class_2>, … 형태의 폴더 구조 - 테스트 데이터:
test/폴더에 이미지 파일,03_test_x.csv에 파일명 목록 - 제출 형식:
image(파일명)와label(예측 클래스명) 두 컬럼의 CSV - 모델 파일:
.keras형식으로 별도 저장
1번 Tabular, 2번 Text와 달리 학습 시간이 오래 걸린다는 점이 가장 큰 변수입니다. 코드에 오류가 있어서 처음부터 다시 돌려야 하는 상황이 오면 시간 안에 끝내기 어렵습니다. 그래서 검증된 코드 템플릿을 미리 준비해 두는 것이 중요합니다.
풀이 전략: 8단계 접근법
Step 0. 환경 설정 — 경로와 하이퍼파라미터를 먼저 정리하세요
python
import os
import pandas as pd
import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, models
from tensorflow.keras.applications import MobileNetV2
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint
# ── 경로 설정 ──
BASE_DIR = '/content/drive/MyDrive/Colab Notebooks/data/'
PHONE = "01012345678" # 본인 전화번호로 변경
TRAIN_DIR = BASE_DIR + "03_image/train"
TEST_DIR = BASE_DIR + "03_image/test"
TEST_CSV = BASE_DIR + "03_test_x.csv"
# ── 하이퍼파라미터 ──
IMG_SIZE = (224, 224)
BATCH = 32
SEED = 42
EPOCHS_STAGE1 = 10 # Head만 학습
EPOCHS_STAGE2 = 10 # Fine-tuning
FINE_TUNE_AT = 100 # 이 레이어 이후부터 학습 허용
# ── 출력 파일 ──
OUTPUT_CSV = BASE_DIR + f"{PHONE}_3.csv"
OUTPUT_MODEL = BASE_DIR + f"{PHONE}_3.keras"
tf.random.set_seed(SEED)
팁: 시험 당일에는 경로와 파일명이 달라질 수 있습니다. 이 설정 블록만 수정하면 나머지 코드가 그대로 돌아가도록, 경로를 한 곳에 모아 두는 습관이 중요합니다.
Step 1. 학습/검증 데이터 로딩
python
train_ds = tf.keras.utils.image_dataset_from_directory(
TRAIN_DIR,
labels='inferred',
label_mode='int', # 정수 라벨 (0, 1, 2, ...)
validation_split=0.2,
subset='training',
seed=SEED,
image_size=IMG_SIZE,
batch_size=BATCH
)
val_ds = tf.keras.utils.image_dataset_from_directory(
TRAIN_DIR,
labels='inferred',
label_mode='int',
validation_split=0.2,
subset='validation',
seed=SEED,
image_size=IMG_SIZE,
batch_size=BATCH
)
class_names = train_ds.class_names
num_classes = len(class_names)
print("Class names:", class_names)
print("Class num:", num_classes)
# 성능 최적화
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds.shuffle(1024).prefetch(AUTOTUNE)
val_ds = val_ds.prefetch(AUTOTUNE)
여기서 핵심적으로 이해해야 할 부분은 label_mode 설정입니다.
| label_mode | 라벨 형태 | 손실 함수 |
|---|---|---|
'int' | 정수 (0, 1, 2, …) | sparse_categorical_crossentropy |
'categorical' | 원-핫 인코딩 | categorical_crossentropy |
'binary' | 0 또는 1 | binary_crossentropy |
다중 분류에서는 'int'를 사용하는 것이 가장 간편합니다. 원-핫 인코딩 변환 없이 정수 라벨을 그대로 쓸 수 있고, 이에 맞춰 손실 함수만 sparse_categorical_crossentropy로 지정하면 됩니다.
Step 2. 데이터 증강 (Augmentation)
python
data_augmentation = tf.keras.Sequential([
layers.RandomFlip("horizontal"),
layers.RandomRotation(0.05),
layers.RandomZoom(0.1),
layers.RandomBrightness(0.1),
], name="augmentation")
이미지 분류에서 데이터 증강은 과적합을 막는 가장 효과적인 방법입니다. 다만 시험에서는 과도한 증강보다 가볍고 안정적인 수준이 적절합니다. 위 네 가지 정도면 충분합니다.
Step 3. 모델 구성 — MobileNetV2 전이학습
python
base_model = MobileNetV2(
input_shape=IMG_SIZE + (3,),
include_top=False,
weights="imagenet"
)
base_model.trainable = False # 1단계에서는 베이스 고정
inputs = layers.Input(shape=IMG_SIZE + (3,))
x = data_augmentation(inputs)
x = preprocess_input(x)
x = base_model(x, training=False)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dropout(0.2)(x)
outputs = layers.Dense(num_classes, activation='softmax')(x)
model = models.Model(inputs, outputs)
모델 구조를 한눈에 정리하면 이렇습니다.
Input → Augmentation → MobileNetV2 전처리 → MobileNetV2(고정) → GlobalAvgPool → Dropout → Dense(softmax)
이진 분류와의 핵심 차이점: 마지막 출력층이 Dense(1, activation='sigmoid')가 아니라 Dense(num_classes, activation='softmax')입니다. 이 부분을 혼동하면 학습 자체가 제대로 되지 않으니 반드시 확인하세요.
MobileNetV2를 선택한 이유는 명확합니다.
- 모델이 가벼워서 시험 환경에서도 학습 시간이 짧습니다
- ImageNet 사전학습 가중치 덕분에 적은 데이터로도 높은 성능을 냅니다
- ResNet50이나 EfficientNet도 좋지만, 시험에서는 속도와 안정성이 우선입니다
Step 4. 1단계 학습 — Head만 학습
python
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
callbacks = [
EarlyStopping(
monitor="val_accuracy", patience=3,
mode="max", restore_best_weights=True
),
ReduceLROnPlateau(
monitor="val_loss", factor=0.5,
patience=2, verbose=1
),
ModelCheckpoint(
"tmp_best_stage1.keras",
monitor="val_accuracy", mode="max",
save_best_only=True, save_weights_only=False
)
]
history1 = model.fit(
train_ds,
validation_data=val_ds,
epochs=EPOCHS_STAGE1,
callbacks=callbacks,
verbose=1
)
1단계에서는 MobileNetV2의 가중치를 고정한 채, 새로 추가한 Dense 층(Head)만 학습합니다. 학습률은 1e-3으로 비교적 크게 잡아 빠르게 수렴시킵니다.
콜백 세 가지의 역할도 정리해 두겠습니다.
| 콜백 | 역할 |
|---|---|
EarlyStopping | val_accuracy가 3 에포크 연속 개선 안 되면 조기 종료 |
ReduceLROnPlateau | val_loss 정체 시 학습률을 절반으로 줄임 |
ModelCheckpoint | 가장 좋은 모델을 자동 저장 |
Step 5. 2단계 파인튜닝 — 상위 레이어 해제
python
base_model.trainable = True
for i, layer in enumerate(base_model.layers):
layer.trainable = (i >= FINE_TUNE_AT) # 100번째 레이어 이후만 학습
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
callbacks_ft = [
EarlyStopping(
monitor="val_accuracy", patience=3,
mode="max", restore_best_weights=True
),
ReduceLROnPlateau(
monitor="val_loss", factor=0.5,
patience=2, verbose=1
),
ModelCheckpoint(
"tmp_best_stage2.keras",
monitor="val_accuracy", mode="max",
save_best_only=True, save_weights_only=False
)
]
history2 = model.fit(
train_ds,
validation_data=val_ds,
epochs=EPOCHS_STAGE2,
callbacks=callbacks_ft,
verbose=1
)
2단계의 핵심은 학습률을 1e-5로 크게 낮추는 것입니다. 사전학습된 가중치를 미세하게만 조정해야 하기 때문입니다. 학습률이 너무 높으면 이미 학습된 좋은 특징들이 망가져 오히려 성능이 떨어집니다.
FINE_TUNE_AT = 100은 MobileNetV2의 뒤쪽 레이어부터만 풀겠다는 의미입니다. 앞쪽 레이어는 엣지, 텍스처 같은 범용적인 특징을 담고 있어 건드릴 필요가 없습니다.
Step 6. 모델 저장
python
model.save(OUTPUT_MODEL)
print(f"모델 저장 완료: {OUTPUT_MODEL}")
주의: 시험에서 요구하는 확장자가
.keras인지.h5인지 반드시 확인하세요. 포맷이 다르면 채점이 안 될 수 있습니다.
Step 7. 테스트 예측 및 CSV 저장
python
df_test = pd.read_csv(TEST_CSV)
assert 'image' in df_test.columns, "CSV에 'image' 컬럼이 필요합니다."
test_filepaths = [os.path.join(TEST_DIR, fn) for fn in df_test['image'].tolist()]
def load_img(path):
img = tf.io.read_file(path)
img = tf.io.decode_image(img, channels=3, expand_animations=False)
img = tf.image.resize(img, IMG_SIZE)
return img
test_ds = tf.data.Dataset.from_tensor_slices(test_filepaths)
test_ds = test_ds.map(load_img, num_parallel_calls=AUTOTUNE)
test_ds = test_ds.map(lambda x: preprocess_input(x))
test_ds = test_ds.batch(BATCH).prefetch(AUTOTUNE)
# 예측: softmax 확률 → argmax → 클래스명 변환
proba = model.predict(test_ds, verbose=1)
pred_idx = np.argmax(proba, axis=1)
pred_lbl = [class_names[i] for i in pred_idx]
submit = pd.DataFrame({
"image": df_test["image"],
"label": pred_lbl
})
submit.to_csv(OUTPUT_CSV, index=False, encoding='utf-8-sig')
print(f"예측 CSV 저장 완료: {OUTPUT_CSV}")
예측 과정에서 가장 실수하기 쉬운 부분은 클래스 인덱스를 클래스명으로 변환하는 단계입니다. argmax로 얻은 정수 인덱스를 class_names 리스트로 매핑해야 합니다. 이 변환을 빠뜨리고 숫자 그대로 제출하면 0점입니다.
또 하나 주의할 점은, 테스트 이미지에도 학습 때와 동일한 전처리(preprocess_input)를 반드시 적용해야 한다는 것입니다. 이걸 빠뜨리면 예측 정확도가 크게 떨어집니다.
이진 분류 vs 다중 분류 — 차이점 한눈에 보기
시험에서 이진 분류가 나올지 다중 분류가 나올지 모릅니다. 둘 다 대비해 두세요.
| 항목 | 이진 분류 | 다중 분류 |
|---|---|---|
label_mode | 'binary' | 'int' |
| 출력층 | Dense(1, sigmoid) | Dense(num_classes, softmax) |
| 손실 함수 | binary_crossentropy | sparse_categorical_crossentropy |
| 예측 변환 | 0.5 기준 threshold | argmax → 클래스명 매핑 |
이 네 가지만 바꾸면 나머지 코드는 동일합니다.
성능이 안 나올 때 — 간단 튜닝 가이드
| 조정 항목 | 방법 | 기대 효과 |
|---|---|---|
| 에포크 수 | EPOCHS_STAGE1/2를 15~20으로 증가 | 학습 부족 해소 |
| 파인튜닝 범위 | FINE_TUNE_AT를 80~50으로 낮춤 | 더 많은 레이어 미세조정 |
| 이미지 크기 | IMG_SIZE를 (256, 256) 이상으로 | 디테일 보존 향상 |
| 클래스 불균형 | class_weight 파라미터 추가 | 소수 클래스 학습 강화 |
| 증강 강도 | RandomContrast, RandomTranslation 추가 | 과적합 완화 |
다만 시험 시간이 한정되어 있으므로, 에포크 수 조정과 파인튜닝 범위 변경 정도만 시도하는 것이 현실적입니다.
실전 체크리스트
시험장에서 제출 전, 이 순서대로 확인하세요.
TRAIN_DIR,TEST_DIR,TEST_CSV경로가 정확한가?label_mode='int'와loss='sparse_categorical_crossentropy'가 일치하는가?- 출력층이
Dense(num_classes, softmax)인가? (이진 분류와 혼동하지 않았는가?) - val_accuracy가 합리적인 수준인가? (클래스 수에 따라 다르지만, 랜덤보다 확실히 높은지)
- 테스트 이미지에
preprocess_input을 적용했는가? - 예측 결과가 클래스 인덱스(숫자)가 아닌 **클래스명(문자열)**으로 변환되었는가?
- 제출 CSV 컬럼이
image,label로 되어 있는가? .keras모델 파일이 정상 저장되었는가?
마무리
3번 Image 문제는 AICE Professional에서 가장 까다로운 문제입니다. 학습 시간이 길어서 실수를 만회할 여유가 부족하고, 이진 분류와 다중 분류의 설정 차이를 혼동하기 쉽습니다.
하지만 풀이 패턴은 명확합니다. MobileNetV2 전이학습 → 2단계 파인튜닝 → argmax로 클래스명 변환. 이 흐름을 몸에 익혀 두면, 시험장에서 당황하지 않고 안정적으로 풀 수 있습니다.
핵심은 코드를 시험 전에 충분히 연습해서, 시험장에서는 경로와 파라미터만 바꿔 넣으면 되는 상태로 만들어 두는 것입니다.
다음 글에서는 2번 Text 문제 풀이를 다루겠습니다.