들어가며: 3번에서 떨어진 경험을 공유합니다
AICE Professional 시험에 직접 도전해 본 경험을 바탕으로, 이번에는 3번 Image 이진 분류 문제의 예상 풀이를 정리합니다. 저는 1, 2번을 만점으로 통과했지만, 바로 이 3번 Image 문제에서 불합격했습니다.
실제 재검토 결과를 공개하자면, Accuracy 95% 이상이 달성 목표였는데 제 답안은 **55.56%**로 측정되어 부분 점수 없이 0점 처리되었습니다. 거의 랜덤 수준의 정확도였던 셈입니다.
원인을 분석해 보니, 테스트 예측 시 전처리 파이프라인 적용에 문제가 있었습니다. val_accuracy는 높게 나왔는데 실제 제출 결과가 55%라면, 학습과 추론의 전처리가 일치하지 않았다는 의미입니다. 이 실수를 반면교사로 삼아, 이번 글에서는 전처리 일관성에 특히 주의를 기울여 풀이를 정리했습니다.
AICE Professional 시험 구조 (복습)
| 문제 | 유형 | 배점 |
|---|---|---|
| 1번 | Tabular (정형 데이터) | 30점 |
| 2번 | Text (텍스트 데이터) | 35점 |
| 3번 | Image (이미지 데이터) | 35점 |
합격 기준은 80점 이상입니다. 한 문제라도 완전히 틀리면 사실상 불합격이므로, 세 문제 모두 확실하게 마무리해야 합니다. 특히 3번은 Accuracy 95% 이상이라는 높은 기준이 요구되므로, 파인튜닝까지 반드시 진행해야 합니다.
3번 문제: Image 이진 분류 — 출제 유형 분석
예상 문제 형태
학습 이미지가 두 개의 클래스 폴더로 분류되어 제공되고(예: clean/, not_clean/), 테스트 이미지가 둘 중 어느 클래스에 속하는지 예측하는 이진 분류(Binary Classification) 문제입니다.
핵심 포인트는 다음과 같습니다.
- 학습 데이터:
train/clean/,train/not_clean/형태의 폴더 구조 - 테스트 데이터:
test/폴더에 이미지 파일,03_test_x.csv에 파일명 목록 - 달성 목표: Accuracy 95% 이상
- 제출 형식:
image(파일명)와label(예측 클래스명) 두 컬럼의 CSV +.keras모델 파일
1번 Tabular, 2번 Text와 비교해서 3번의 가장 큰 변수는 학습 시간입니다. 코드에 오류가 있어서 처음부터 다시 돌려야 하는 상황이 오면 시간 안에 끝내기 어렵습니다. 검증된 코드 템플릿을 반드시 미리 준비해 두세요.
풀이 전략: 7단계 접근법
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.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'
# ── 하이퍼파라미터 ──
IMAGE_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'
TARGET_COL = 'label'
tf.random.set_seed(SEED)
팁: 경로와 파일명을 한 블록에 모아 두면, 시험 당일에 이 부분만 수정하고 나머지 코드를 그대로 실행할 수 있습니다.
Step 1. 학습/검증 데이터 로딩
python
train_ds = tf.keras.utils.image_dataset_from_directory(
TRAIN_DIR,
labels='inferred',
label_mode='binary', # 이진 분류: 0 또는 1
validation_split=0.2,
subset='training',
seed=SEED,
image_size=IMAGE_SIZE,
batch_size=BATCH,
)
val_ds = tf.keras.utils.image_dataset_from_directory(
TRAIN_DIR,
labels='inferred',
label_mode='binary',
validation_split=0.2,
subset='validation',
seed=SEED,
image_size=IMAGE_SIZE,
batch_size=BATCH,
)
class_names = train_ds.class_names # 예: ['clean', 'not_clean'] (알파벳 순)
print('CLASS:', class_names)
# 성능 최적화
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds.cache().shuffle(1024).prefetch(AUTOTUNE)
val_ds = val_ds.cache().prefetch(AUTOTUNE)
label_mode='binary'로 설정하면 라벨이 0 또는 1로 들어옵니다. 클래스 이름의 알파벳 순서대로 0, 1이 할당되므로, class_names를 출력해서 어떤 클래스가 0이고 어떤 클래스가 1인지 반드시 확인하세요.
Step 2. 전처리 & 데이터 증강
python
data_augmentation = tf.keras.Sequential([
layers.RandomFlip('horizontal'),
layers.RandomRotation(0.05),
layers.RandomZoom(0.1),
], name='augmentation')
preproc = tf.keras.Sequential([
layers.Rescaling(1./255), # 0~255 → 0~1
data_augmentation, # 증강
layers.Lambda(lambda x: x * 2.0 - 1.0) # 0~1 → -1~1 (MobileNetV2 입력 범위)
], name='preproc')
여기서 전처리 파이프라인을 모델 내부에 포함시키는 것이 핵심입니다. 전처리를 모델 밖에서 별도로 처리하면, 학습과 추론에서 전처리가 달라지는 실수가 발생하기 쉽습니다.
전처리 흐름을 정리하면 이렇습니다.
원본 이미지(0~255) → Rescaling(0~1) → 증강 → Lambda(-1~1) → MobileNetV2
MobileNetV2는 -1 ~ 1 범위의 입력을 기대합니다. Rescaling으로 0~1로 만든 뒤, Lambda(x * 2.0 - 1.0)으로 -1~1로 변환합니다.
주의: 다중 분류 코드에서는
preprocess_input()함수를 사용했지만, 이진 분류 코드에서는Rescaling+Lambda로 동일한 변환을 직접 구현합니다. 어느 방식이든 결과는 같지만, 모델 내부에 포함시키는 이 방식이 추론 시 전처리 누락을 방지하는 데 더 안전합니다.
Step 3. 모델 구성 — MobileNetV2 전이학습
python
base_model = MobileNetV2(
input_shape=IMAGE_SIZE + (3,),
include_top=False,
weights='imagenet'
)
base_model.trainable = False # 1단계에서는 베이스 고정
inputs = layers.Input(IMAGE_SIZE + (3,))
x = preproc(inputs) # 전처리를 모델 안에 포함
x = base_model(x, training=False)
x = layers.GlobalAveragePooling2D()(x)
x = layers.Dropout(0.2)(x)
outputs = layers.Dense(1, activation='sigmoid')(x) # 이진 분류
model = models.Model(inputs, outputs)
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3),
loss='binary_crossentropy',
metrics=['accuracy']
)
model.summary()
모델 구조를 한눈에 정리하면 이렇습니다.
Input → Rescaling → Augmentation → -1~1 변환 → MobileNetV2(고정) → GlobalAvgPool → Dropout → Dense(sigmoid)
다중 분류와의 핵심 차이점: 마지막 출력층이 Dense(num_classes, activation='softmax')가 아니라 Dense(1, activation='sigmoid')입니다. 손실 함수도 binary_crossentropy를 사용합니다.
Step 4. 1단계 학습 — Head만 학습
python
callbacks_stage1 = [
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_stage1,
verbose=1
)
1단계에서는 MobileNetV2 가중치를 고정한 채 새로 추가한 Dense 층만 학습합니다. 이 단계만으로 val_accuracy가 90% 근처까지 올라가는 경우가 많습니다. 하지만 목표가 95% 이상이므로, 여기서 멈추면 안 됩니다.
Step 5. 2단계 파인튜닝 — 반드시 진행하세요
python
base_model.trainable = True
for i, layer in enumerate(base_model.layers):
layer.trainable = (i >= FINE_TUNE_AT)
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),
loss='binary_crossentropy',
metrics=['accuracy']
)
callbacks_stage2 = [
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_stage2,
verbose=1
)
파인튜닝은 선택이 아니라 필수입니다. Head만 학습한 1단계로는 95%를 넘기기 어려운 경우가 많습니다. 제 실패 경험에서도, 파인튜닝을 주석 처리한 채로 제출한 것이 패인 중 하나였습니다.
파인튜닝 시 반드시 지켜야 할 두 가지가 있습니다.
- 학습률을
1e-5로 크게 낮출 것: 사전학습된 가중치를 미세하게만 조정해야 합니다. 학습률이 높으면 좋은 특징들이 망가집니다. FINE_TUNE_AT이전 레이어는 고정할 것: MobileNetV2의 앞쪽 레이어는 엣지, 텍스처 같은 범용 특징을 담고 있어 건드릴 필요가 없습니다.
Step 6. 모델 저장 및 테스트 예측
python
# 모델 저장
model.save(OUTPUT_MODEL)
print(f"Saved model -> {OUTPUT_MODEL}")
# 테스트 데이터 로딩
df_test = pd.read_csv(TEST_CSV)
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, IMAGE_SIZE)
return img
test_ds = (tf.data.Dataset.from_tensor_slices(test_filepaths)
.map(load_img, num_parallel_calls=AUTOTUNE)
.batch(BATCH)
.prefetch(AUTOTUNE))
# 예측: sigmoid 출력 → 0.5 기준으로 0/1 → 클래스명 변환
proba = model.predict(test_ds, verbose=1).ravel()
pred_idx = (proba >= 0.5).astype(int)
pred_lbl = [class_names[i] for i in pred_idx]
submit = pd.DataFrame({
"image": df_test["image"],
TARGET_COL: pred_lbl
})
submit.to_csv(OUTPUT_CSV, index=False, encoding='utf-8-sig')
print(f"Saved predictions -> {OUTPUT_CSV}")
이진 분류의 예측 후처리는 다중 분류와 다릅니다. sigmoid 출력이 0~1 사이의 확률값이므로, 0.5를 기준으로 0 또는 1로 변환한 뒤 class_names로 매핑합니다.
여기서 중요한 점이 하나 있습니다. 전처리 파이프라인(preproc)을 모델 내부에 포함시켰기 때문에, 테스트 예측 시에는 별도의 전처리가 필요 없습니다. model.predict()를 호출하면 모델이 자동으로 Rescaling과 -1~1 변환을 수행합니다. 단, 이때 증강(RandomFlip, RandomRotation 등)은 training=False 모드에서 자동으로 비활성화됩니다.
이것이 제가 실패했던 핵심 원인입니다. 전처리를 모델 밖에서 따로 처리하는 방식을 사용하면, 학습 때는 적용했는데 추론 때 빠뜨리거나, 반대로 이중 적용하는 실수가 생깁니다. 모델 안에 전처리를 포함시키면 이런 불일치를 원천 차단할 수 있습니다.
이진 분류 vs 다중 분류 — 차이점 한눈에 보기
시험에서 어떤 유형이 나올지 모릅니다. 둘 다 대비해 두세요.
| 항목 | 이진 분류 | 다중 분류 |
|---|---|---|
label_mode | 'binary' | 'int' |
| 출력층 | Dense(1, sigmoid) | Dense(num_classes, softmax) |
| 손실 함수 | binary_crossentropy | sparse_categorical_crossentropy |
| 예측 변환 | p >= 0.5 → 0/1 → 클래스명 | argmax → 클래스 인덱스 → 클래스명 |
| 제출 라벨 | 클래스명 2종 | 클래스명 K종 |
이 다섯 가지만 바꾸면 나머지 코드 구조는 동일합니다.
95%가 안 나올 때 — 간단 튜닝 가이드
Accuracy 95%는 꽤 높은 기준입니다. 1단계 학습만으로 부족하다면 아래 순서로 조정해 보세요.
| 우선순위 | 조정 항목 | 방법 | 기대 효과 |
|---|---|---|---|
| 1 | 파인튜닝 활성화 | 2단계 학습을 반드시 실행 | 가장 큰 성능 향상 |
| 2 | 파인튜닝 범위 확대 | FINE_TUNE_AT를 80~50으로 낮춤 | 더 많은 레이어 미세조정 |
| 3 | 에포크 수 증가 | EPOCHS_STAGE1/2를 15~20으로 | 학습 부족 해소 |
| 4 | 모델 변경 | MobileNetV3-Large 사용 | 더 높은 모델 성능 |
| 5 | Dropout 조정 | 0.2 → 0.3~0.4 | 과적합 완화 |
| 6 | 증강 강도 조절 | RandomBrightness(0.1) 추가 | 일반화 향상 |
| 7 | 배치 크기 | 32 → 64 (메모리 여유 시) | 학습 안정성 |
실제 재검토 피드백에서도 MobileNetV3-Large 활용이나 파인튜닝 적용이 성능 개선 방안으로 제시되었습니다. 시간이 된다면 모델을 MobileNetV3로 교체하는 것도 좋은 선택입니다.
내가 틀린 이유 — 실패에서 배운 교훈
제 실제 시험 결과를 다시 정리하면 이렇습니다.
- 달성 목표: Accuracy 95% 이상
- 제출 결과: 55.56% (부분 점수 없음, 0점 처리)
- val_accuracy는 높게 나왔으나, 실제 테스트 정확도는 랜덤 수준
원인은 크게 두 가지였습니다.
첫째, 파인튜닝을 하지 않았습니다. 코드에 파인튜닝 부분을 주석 처리한 채로 제출했습니다. Head만 학습한 상태로는 95%를 넘기기 어렵습니다.
둘째, 학습과 추론의 전처리가 불일치했습니다. 학습 시에는 전처리가 적용되었지만, 테스트 예측 시 전처리 파이프라인이 제대로 적용되지 않아 모델이 엉뚱한 입력을 받았습니다. val_accuracy가 높아도 실제 제출 결과가 55%라면, 이 불일치가 원인일 가능성이 높습니다.
이 문제의 해결책이 바로 전처리를 모델 내부에 포함시키는 것입니다. 이렇게 하면 model.predict()만 호출하면 전처리가 자동으로 적용되므로, 추론 시 실수할 여지가 사라집니다.
실전 체크리스트
시험장에서 제출 전, 이 순서대로 확인하세요.
TRAIN_DIR,TEST_DIR,TEST_CSV경로가 정확한가?label_mode='binary'와loss='binary_crossentropy'가 일치하는가?- 출력층이
Dense(1, sigmoid)인가? (다중 분류와 혼동하지 않았는가?) - 파인튜닝을 실행했는가? (주석 처리되어 있지 않은가?)
- val_accuracy가 95% 이상인가?
- 전처리가 모델 내부에 포함되어 있어, 테스트 예측 시 별도 전처리가 필요 없는 구조인가?
- 예측 결과가 0/1 숫자가 아닌 **클래스명(문자열)**으로 변환되었는가?
- 제출 CSV 컬럼이
image,label로 되어 있는가? .keras모델 파일이 정상 저장되었는가?
마무리
3번 Image 문제는 AICE Professional에서 가장 까다롭고, 실제로 많은 분들이 이 문제에서 막혀 불합격합니다. 저도 그중 한 명이었습니다.
하지만 실패 원인을 분석해 보면, 코드 자체의 문제보다 파인튜닝 미실행과 전처리 불일치 같은 실수가 대부분입니다. 풀이 패턴을 확실히 익혀 두고, 제출 전 체크리스트를 꼼꼼히 확인하면 충분히 95%를 넘길 수 있습니다.
핵심을 한 문장으로 요약하면 이렇습니다. 전처리는 모델 안에 넣고, 파인튜닝은 반드시 하세요.