오늘의 수업 요약

VGG19를 활용한 이미지 분류 및 전이학습

기존의 똑똑한 모델(VGG19)을 빌려와서 내 데이터를 빠르게 학습시키는 핵심 기법 배우기

무엇을 배울까요?

💡 쉬운 예시: 이미 '사진을 잘 보는 눈'을 가진 모델(VGG19)의 눈은 그대로 쓰고, 마지막 분류 판단(입)만 내 데이터에 맞춰 새로 배우게 하는 거예요.

1

필요한 도구(라이브러리) 가져오기

왜 이 코드를 실행하나요? 딥러닝 모델(VGG19)을 구성하고 데이터를 처리하기 위한 필수 도구들을 미리 세팅해요. 특히 불필요한 경고 메시지는 무시하도록 설정해 학습 로그를 깨끗하게 유지합니다.
# vgg16 : 16개의 레이어, vgg19 : 19개의 레이어
import warnings
warnings.filterwarnings("ignore")
# 학습 도중 출력되는 지저분한 경고 메시지들을 숨겨요.
import tensorflow as tf
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.applications.vgg19 import VGG19
from tensorflow.keras.datasets import cifar10
from tensorflow.keras import layers
import numpy as np
import matplotlib.pyplot as plt
실제 출력
(출력 없음)
출력 해설: 라이브러리 로드 단계에서는 별도의 화면 출력이 없는 것이 정상이에요.
2

CIFAR10 이미지 데이터셋 불러오기

왜 이 코드를 실행하나요? 학습에 사용할 10가지 종류의 이미지 데이터(CIFAR10)를 인터넷에서 다운로드하거나 캐시에서 가져와 훈련용과 테스트용으로 분리합니다.
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
# 인터넷을 통해 6만 장의 이미지를 다운로드하고 (훈련/테스트) 변수에 나눠 담아요.
예상 출력
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz 170498071/170498071 [==============================] - 2s 0us/step
출력 해설: 최초 실행 시에는 데이터를 내려받는 진행 표시줄(ProgressBar)이 나타나요.
3

학습 속도를 위해 데이터 샘플링(줄이기)

왜 이 코드를 실행하나요? VGG19는 매우 무거운 모델이라 5만 장을 다 쓰면 시간이 너무 오래 걸려요. 100장당 1장씩만 골라내어(슬라이싱) 전체 학습 흐름이 정상인지 빠르게 검증하기 위해 데이터를 줄입니다.
x_train = x_train[::100]
# 훈련 이미지를 100개 간격으로 건너뛰며 뽑아서 개수를 줄여요.
y_train = y_train[::100]
# 훈련 정답(라벨)도 이미지와 똑같은 개수가 되도록 줄여요.
x_test = x_test[::100]
# 테스트 이미지도 1/100 수준으로 가볍게 만들어요.
y_test = y_test[::100]
# 테스트 정답도 이미지와 짝을 맞춰 줄여요.
실제 출력
(데이터 크기 조정 완료)
출력 해설: 변수의 내용만 바뀌고 화면에 찍히는 값은 없지만, 메모리 효율이 급격히 좋아진 상태예요.
4

샘플링된 데이터의 모양(Shape) 확인

왜 이 코드를 실행하나요? 데이터를 줄인 후 이미지의 개수와 해상도가 의도한 대로(N, 32, 32, 3) 유지되고 있는지 최종 점검합니다. 이 크기를 알아야 나중에 VGG 입력 크기와 비교할 수 있어요.
print("Shape of x_train: ", x_train.shape)
# 훈련 이미지의 (개수, 높이, 너비, 채널) 정보를 출력해요.
print("Shape of y_train: ", y_train.shape)
# 훈련 라벨의 (개수, 1) 정보를 확인해요.
print("Shape of x_test: ", x_test.shape)
# 평가용 이미지의 모양을 확인해요.
print("Shape of y_test: ", y_test.shape)
# 평가용 라벨의 모양을 확인해요.
실제 출력
Shape of x_train: (500, 32, 32, 3) Shape of y_train: (500, 1) Shape of x_test: (100, 32, 32, 3) Shape of y_test: (100, 1)
출력 해설: 앞에서 [::100]으로 줄였기 때문에 훈련 데이터가 500개로 줄어든 것을 확인할 수 있어요.
5

정답 데이터(라벨) 전처리: 원-핫 인코딩

왜 이 코드를 실행하나요? 컴퓨터는 '3번 클래스'를 단순 숫자로 이해하기보다 '3번 자리에만 1이 있는 리스트'로 이해할 때 더 잘 배워요. 클래스 개수를 파악해 모든 정답을 0과 1의 벡터 형태로 변환합니다.
y_train[:5]
# 변환 전의 정답(0~9 숫자) 5개를 먼저 구경해요.
numberOfClass = len(np.unique(y_train))
# 전체 클래스가 총 몇 종류(10가지)인지 자동으로 계산해요.
y_train = to_categorical(y_train, numberOfClass)
# 훈련 정답을 [0,0,1,0...] 식의 원-핫 벡터로 싹 바꿔요.
y_test = to_categorical(y_test, numberOfClass)
# 테스트 정답도 똑같이 변환해 딥러닝 모델의 출력 형식과 맞춰요.
y_train[:5]
# 변환 후 정답이 어떻게 바뀌었는지 다시 5개를 확인해요.
실제 출력
변환 전: [[6], [8], [6], [2], [2]] 변환 후: [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]
출력 해설: 단순 숫자가 10칸짜리 리스트로 펼쳐진 것을 볼 수 있어요. 이게 딥러닝이 계산하기 가장 좋은 형태예요.
6

이미지 확인 및 모델 입력 규격 파악

왜 이 코드를 실행하나요? 데이터가 깨지지 않고 잘 들어왔는지 실제 이미지를 한 장 그려보고, 모델에 집어넣을 이미지 한 장의 가로/세로/채널 규격(input_shape)을 확정합니다.
input_shape = x_train.shape[1:]
# 이미지 한 장의 (32, 32, 3) 규격을 변수에 담아요.
input_shape
# 변수에 담긴 규격을 화면에 찍어봐요.
plt.figure(figsize=(1.5, 1.5))
# 작은 사이즈(1.5인치)의 도화지를 준비해요.
plt.imshow(x_train[3].astype(np.uint8))
# 4번째 이미지(index 3)를 실제 픽셀값으로 그려요.
plt.axis("off")
# 그래프의 눈금(숫자)들을 치워서 이미지만 보이게 해요.
plt.show()
# 준비된 그림을 화면에 출력합니다.
실제 출력
input_shape: (32, 32, 3)
출력 해설: 32x32 크기의 아주 작은 CIFAR10 샘플 이미지가 나타납니다. VGG19에 넣기엔 너무 작은 크기예요.
샘플 이미지 (x_train[3])
7

VGG19 모델 규격에 맞게 리사이즈

왜 이 코드를 실행하나요? VGG19 모델은 기본적으로 32x32보다 큰 이미지를 선호해요(최소 48x48 이상). 모델이 제대로 특징을 뽑아낼 수 있도록 모든 이미지의 해상도를 48x48로 키워주는 전처리를 수행합니다.
#vgg19 모형에 맞게 48x48로 resize
import cv2
def resize_img(img):
# 데이터 이미지 장수를 먼저 체크해요.
numberOfImage = img.shape[0]
# 48x48 크기의 빈 결과통(배열)을 미리 준비해요.
new_array = np.zeros((numberOfImage, 48, 48, 3))
# 모든 이미지에 대해 하나씩 리사이즈 작업을 반복해요.
for i in range(numberOfImage):
# OpenCV를 이용해 32x32를 48x48로 확대해요.
new_array[i] = cv2.resize(img[i, :, :, :], (48, 48))
# 작업이 끝난 뭉치를 반환해요.
return new_array
# 훈련 데이터 전체를 확대해서 변수에 덮어써요.
x_train = resize_img(x_train)
# 테스트 데이터 전체도 똑같이 확대해요.
x_test = resize_img(x_test)
# 확대된 데이터의 최종 모양을 확인해봐요.
print(x_train.shape, x_test.shape)
실제 출력
(500, 48, 48, 3) (100, 48, 48, 3)
출력 해설: 가로 세로가 32에서 48로 커진 것을 알 수 있어요. 이제 VGG19에 넣을 준비가 끝났습니다.
8

전이학습(Transfer Learning)을 위한 VGG19 로드

왜 이 코드를 실행하나요? 처음부터 모델을 만들지 않고, 이미 똑똑하게 훈련된 VGG19 모델을 가져옵니다. 이때 마지막 분류층(top)은 떼어내고, ImageNet에서 배운 '사물 인식 능력'만 활용합니다.
vgg = VGG19(include_top=False, weights="imagenet", input_shape=(48, 48, 3))
# ImageNet 지식을 가진 VGG19를 불러오되, 분류기 부분은 빼고 가져와요.
vgg.summary()
# 가져온 VGG19가 어떤 층들로 이루어져 있는지 표로 확인해요.
예상 출력
Model: "vgg19" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) [(None, 48, 48, 3)] 0 block1_conv1 (Conv2D) (None, 48, 48, 64) 1792 ... =================================================================
출력 해설: 수천만 개의 파라미터를 가진 거대한 신경망 구조가 출력됩니다. 이 구조는 이미 '그림을 보는 법'을 알고 있어요.
9

모델 결합: VGG 기초 + 새로운 분류기

왜 이 코드를 실행하나요? VGG가 가진 기존 지식은 변하지 않도록 고정(Freezing)하고, 그 뒤에 우리가 가진 10종류의 이미지를 맞힐 수 있는 새로운 층들을 덧붙여 나만의 모델을 완성합니다.
for layer in vgg.layers:
# VGG의 기존 레이어들을 하나씩 꺼내요.
layer.trainable = False
# 이미 배운 능력(가중치)이 바뀌지 않게 학습 불가능 상태로 잠가요.
layer = [
# VGG 뒤에 붙일 나만의 층(분류기) 리스트를 만들어요.
layers.Flatten(),
# 2D 특징 데이터를 1D 일렬로 쭉 펼쳐요.
layers.Dense(128),
# 중간에서 특징을 한 번 더 거르는 필터를 128개 둬요.
layers.Dense(numberOfClass, activation="softmax"),
# 최종적으로 10가지 클래스 확률을 내보내는 출구를 만들어요.
]
# VGG 레이어들과 새 레이어들을 일렬로 쭉 이어붙여요.
model = tf.keras.Sequential(vgg.layers + layer)
# 최종 합체된 모델의 구조를 다시 한 번 요약해서 봐요.
model.summary()
예상 출력
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= vgg19 (Functional) (None, 1, 1, 512) 20024384 flatten (Flatten) (None, 512) 0 dense (Dense) (None, 128) 65664 dense_1 (Dense) (None, 10) 1290 =================================================================
출력 해설: 전체 파라미터 중 VGG 부분은 잠겨 있고(Non-trainable), 뒤에 붙인 일부만 학습 가능한 상태인 것을 확인해야 해요.
10

모델 컴파일 및 메모리 정리

왜 이 코드를 실행하나요? 학습을 시작하기 전 '어떻게 틀린 정도를 잴지(loss)'와 '어떻게 정답을 찾아갈지(optimizer)'를 설정합니다. 반복 실행 시 발생할 수 있는 메모리 누수를 막기 위해 불필요한 데이터를 강제로 비워줍니다.
model.compile(loss="categorical_crossentropy",
# 다중 분류에 필수인 손실 함수와 최적화 도구를 정해요.
optimizer="rmsprop",
# 남아있는 딥러닝 세션을 깨끗이 지워 메모리 에러를 방지해요.
metrics=["accuracy"])
# 파이썬의 쓰레기 수집기(GC)를 호출해 안 쓰는 메모리를 진짜로 반납해요.
import gc
tf.keras.backend.clear_session()
gc.collect()
실제 출력
(설정 완료 및 메모리 확보)
출력 해설: 화면 변화는 없지만, 모델이 학습할 준비가 완벽히 끝난 가장 가벼운 상태가 되었어요.
11

본격적인 모델 학습 시작

왜 이 코드를 실행하나요? 준비한 훈련 데이터를 모델에 집어넣어 학습을 진행합니다. 훈련 중간에 별도로 떼어놓은 검증 데이터(30%)를 통해 모델이 처음 보는 데이터도 잘 맞히고 있는지 실시간으로 기록(history)합니다.
hist = model.fit(x_train, y_train, validation_split=0.3, epochs=5, batch_size=1)
# 500장의 데이터를 5번 반복해서 가르쳐요. 30%는 시험용으로 따로 빼서 체크합니다.
예상 출력
Epoch 1/5 350/350 [==============================] - 15s 39ms/step - loss: 2.214 - accuracy: 0.185 - val_loss: 1.956 - val_accuracy: 0.280 ... Epoch 5/5 350/350 [==============================] - 14s 40ms/step - loss: 1.250 - accuracy: 0.520 - val_loss: 1.540 - val_accuracy: 0.450
출력 해설: 에폭이 진행될수록 loss는 떨어지고 accuracy는 올라가는지 눈여겨봐야 해요. 이게 학습의 성적표입니다.
12

학습 결과 시각화 및 최종 평가

왜 이 코드를 실행하나요? 학습 기록(hist)을 그래프로 그려 훈련 과정에서 오버피팅(과적합)이 발생했는지 분석합니다. 마지막으로 한 번도 보지 못한 테스트 데이터를 통해 이 모델의 진짜 실력을 검증합니다.
print(hist.history.keys())
# 어떤 지표들이 기록되었는지(loss, accuracy 등) 이름을 확인해요.
plt.plot(hist.history["loss"], label="training loss")
# 훈련 오차가 줄어드는 과정을 그래프로 그려요.
plt.plot(hist.history["val_loss"], label="validation loss")
# 검증 오차도 같이 그려서 두 선이 같이 잘 내려가는지 봐요.
plt.legend()
# 선이 무엇을 뜻하는지 범례(label)를 달아줘요.
plt.show()
# 손실 그래프 도표를 화면에 띄워요.
plt.plot(hist.history["accuracy"], label="training accuracy")
# 맞히는 확률(정확도)이 올라가는 과정을 그려요.
plt.plot(hist.history["val_accuracy"], label="validation accuracy")
# 검증 정확도도 함께 그려서 실전 성능을 비교해요.
plt.legend()
# 정확도 범례를 달아줘요.
plt.show()
# 정확도 그래프 도표를 화면에 띄워요.
model.evaluate(x_test, y_test)
# 최종적으로 테스트 데이터 100장을 풀어보게 해서 진짜 실력을 점수 매겨요.
실제 출력
dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy']) 4/4 [==============================] - 1s 180ms/step - loss: 1.623 - accuracy: 0.420 [1.6234, 0.4200]
출력 해설: 그래프 두 개를 통해 학습이 안정적인지 판단하고, evaluate의 최종 정확도로 모델의 성능을 확정 짓습니다.
학습 결과 그래프 (Loss & Accuracy)