본문 바로가기
머신러닝, 딥러닝/딥러닝

CNN 사전학습 모델을 이용한 이미지 분류

by 장찐 2022. 1. 18.

 

📚 CNN 사전학습 모델을 사용해서 이미지 분류하기 

 

✅ 이미지 전처리 - Input 형식 변경 

📌 이미지 크기 변경  

 사전학습 모델마다 입력받는 이미지의 형태가 다르다. VGG 16는 224x224 형태의 컬러 이미지를 입력받기 때문에 이에 맞춰서 학습할 이미지 크기를 변환해야 한다. 대부분의 사전학습 모델은 정사각형 이미지 형태를 입력받는다. 이미지를 정사각형으로 만드는 방식은 다음과 같다. 

 

① Cropping : 정사각형 형태로 가운데를 중심으로 자르기 

 

② Warping : 가로세로 비율이 다른 이미지를 확대 또는 축소에서 변경하는 방법 → 이미지의 왜곡 발생 

 

③ Padding : 이미지 일부를 특정 색상으로 채우는 방법. 주로 검정색(0인 값)으로 채우는 zero padding이 자주 사용된다. 

 

import tensorflow as tf
from tensorflow.keras.preprocessing import image
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.image import pad_to_bounding_box
from tensorflow.image import central_crop
from tensorflow.image import resize

 필요한 라이브러리를 불러온다. 이미지 전처리에는 주로 텐서플로에서 제공하는 image 모듈이 사용된다. 

 

#이미지 불러오기
bgd = image.load_img('C:/Users/Yeong/Desktop/CDSS강의_심화/심화_강의자료1/python_code/들3.jpg')
bgd_vector = np.asarray(image.img_to_array(bgd))
bgd_vector = bgd_vector/255

#이미지 형태 확인 
bgd_vector.shape

#이미지 확인 
plt.imshow(bgd_vector)
plt.show()

우주대스타견 방구들

친구네 강아지 방구들(이름이 방구들이다)의 이미지를 불러온다. 들이 표정이 띠꺼워 보이는데 귀찮게 한 거 아니고 항상 놀러가면 저렇게 쳐다본다. 불러온 이미지는 3024x40323 형태이다. CNN은 numpy array를 입력받기 때문에 이미지를 numpy array 형태로 변환한다

 

 

① Padding : 가장자리 채우기 

#이미지의 변경할 크기 설정 
target_height = 4500
target_width = 4500

#현재 이미지의 크기 지정 
source_height = bgd_vector.shape[0]
source_width = bgd_vector.shape[1]

#padding 실시 : pad_to_bounding_box 사용 
bgd_vector_pad = pad_to_bounding_box(bgd_vector, 
                                     int((target_height-source_height)/2), 
                                     int((target_width-source_width)/2), 
                                     target_height, 
                                     target_width)
                                     
 #이미지 형태 확인 
bgd_vector_pad.shape

#이미지 확인 
plt.imshow(bgd_vector_pad)
plt.show()

#이미지 저장 
image.save_img(r'C:\Users\Yeong\Desktop\CDSS강의_심화\심화_강의자료1\python_code\cat1_pad.png', cat_vector_pad)

padding 실시

 변경할 이미지의 크기를 설정한다. 여기서는 4500x4500 형태로 지정했다. 그리고 pad_to_bounding_bos 함수를 사용해서 패딩을 실시한다. 결과를 살펴보면 원본 이미지의 주변부가 값이 0인 픽셀로 패딩이 실시되어 정사각형이 된 것을 확인할 수 있다. 

 

 

② Cropping : 이미지 중심으로 자르기 

 

Cropping 실시

#가운데를 중심으로 50%만 crop 
bgd_vector_crop = central_crop(bgd_vector, .5)

bgd_vector_crop.shape

plt.imshow(bgd_vector_crop)
plt.show()

 central_crop 함수를 사용해서 간단하게 cropping을 할 수 있다. 0.5는 가운데로부터 50%만 자른다는 의미이다. 들이의 띠꺼운 표정이 한층 더 확대된다. 하지만 이 방식은 가로세로 비율이 그대로 유지되기 때문에, 사전학습 모델에 사용하기에는 적절하지 않다. 대부분의 사전학습 모델은 input 값으로 정사각형 형태의 이미지를 받는다. 

 

w, h = bgd.size

s = min(w, h) #둘 중에 작은 것 기준으로 자름 
y = (h - s) // 2
x = (w - s) // 2

print(w, h, x, y, s)

# 좌, 위, 오른쪽, 아래 픽셀 설정 
bgd = bgd.crop((x, y, x+s, y+s))
plt.imshow(np.asarray(bgd))
bgd.size

위와 같이 직접 이미지 픽셀의 가로세로를 설정해서 정사각형 형태로 크롭할 수 있다. 원본 이미지에서 길이가 짧은 곳을 기준으로 크롭 된 것을 확인할 수 있다. 

 

 

③ Warping 

bgd_vector_resize = resize(bgd_vector, (300,300))

bgd_vector_resize.shape

plt.imshow(bgd_vector_resize)

워핑은 resize 함수를 이용해서 매우 간단하게 수행할 수 있다. 이미지의 비율이 왜곡되기 때문에 성능에 영향을 미칠 수도 있으므로 유의해야 한다. 

 


 

✅ VGG 16 모델 적용 - 그대로 사용

 

📌 모델 구조 확인하기 및 라이브러리 불러오기 

#사전학습 모델 불러오기 : 케라스에서 클래스 형태로 제공함 
from tensorflow.keras.applications.vgg16 import VGG16

#weight, include_top 파라미터 설정 
model = VGG16(weights='imagenet', include_top=True)
model.summary()

① weight 파라미터 

• imagenet = 이미지넷에 최적화된 기본 파라미터로 사용

• none = 초기화된 상태로 

② include_top 파라미터 : 사전학습 모델에서 top 부분에 해당하는 것을 사용할지 여부를 결정함 

위 그림에서 dense layer가 있는 노란색 부분을 top 부분이라고 함. True로 설정할 경우 해당 부분을 포함해서 불러온다. 여기서는 사전학습 모델을 그대로 사용하기 때문에 true로 설정 

 

 

왼 : include_top=True / 우 : include_top=False

 model.summary()를 실행하면 좌측의 경우 원래 VGG 16과 동일한 구조임을 알 수 있다. 반대로 우측에서는 flatten부분과 Dense 부분이 생략되어 있는 것을 알 수 있다. 

 

from tensorflow.keras.applications.vgg16 import preprocess_input
from tensorflow.keras.preprocessing import image

import matplotlib.pyplot as plt
from PIL import Image
from tensorflow.keras.applications.imagenet_utils import decode_predictions
from tensorflow.keras.applications.imagenet_utils import preprocess_input
import numpy as np
from google.colab import drive
drive.mount('/content/gdrive')

cd/content/gdrive/MyDrive/CDSS 딥러닝 강의/심화_1

img = Image.open('들3.jpg')
img.size
plt.imshow(np.asarray(img))

코랩에서 경로를 설정하고 분류할 이미지를 불러온다. 위에서 사용한 4032x3024 크기의 들이 이미지를 사용한다. 

 

 

📌 이미지 전처리 

w, h = img.size
s = min(w, h)
y = (h - s) // 2
x = (w - s) // 2

print(w, h, x, y, s)
img = img.crop((x, y, x+s, y+s))
# 4-tuple defining the left, upper, right, and lower pixel coordinate
plt.imshow(np.asarray(img))
img.size

 ceter crop 방식을 사용해서 짧은 변을 기준으로 정사각형 형태로 만든다. 크롭 후의 크기를 확인해보면 3024 x 3024 형태임을 알 수 있다. 

 

 

#VGG16이 입력받는 이미지크기 확인
model.layers[0].input_shape

#이미지 리사이즈
target_size = 224
img = img.resize((target_size, target_size)) # resize from 280x280 to 224x224
plt.imshow(np.asarray(img))

img.size #변경된 크기 확인

 VGG16 모델은 224x224의 칼라이미지를 입력받는다. 따라서 크롭한 이미지를 224 크기로 줄여서 리사이징 한다. 

 

 

#numpy array로 변경
np_img = image.img_to_array(img)
np_img.shape  #(224, 224, 3) 

#4차원으로 변경 
img_batch = np.expand_dims(np_img, axis=0)
img_batch.shape

 CNN모델은 4차원 넘파이 형태로 데이터를 입력받기 때문에 이미지를 numpy array로 변경하고, 1차원을 추가한다. 

여기서 shape의 결과는 (이미지 수, 채널 수, 이미지 높이, 이미지 가로) 이다 

 

 

#feature normalization
pre_processed = preprocess_input(img_batch)

 정규화 전  img_batch는 왼쪽과 같이 0~255 사이의 값으로 구성되어 있다. 이미지 인식 모델에서 학습 속도와 모델 성능을 확보하기 위해서는 정규화를 실시해야 한다. preprocess_input 함수를 통해서 간단하게 실시할 수 있는데, 모형마다 이 함수의 역할이 다르므로 유의해야 한다. VGG16 모델에서는 원래 픽셀 값들이 0을 기준으로 centering 된다. 

 

 

 

📌 예측하기 

y_preds = model.predict(pre_processed)

y_preds.shape  # 종속변수가 취할 수 있는 값의 수 = 1000

np.set_printoptions(suppress=True, precision=10)
y_preds

#가장 확률이 높은 값
np.max(y_preds)

 이 사례에서는 VGG16을 그대로 사용하는 것이기 때문에 별도의 학습 과정이 필요 없다. 또한, VGG16은 이미지넷 데이터의 1000개 클래스를 분류하는데, 여기에는 개와 고양이 등의 동물 이미지도 포함되어 있기 때문에 들이 이미지도 분류할 수 있다. 위 화면은 y_preds의 출력 결과인데, 총 1000개 클래스에 대해서 각 클래스로 분류될 확률을 나타낸다. 

 

 

decode_predictions(y_preds, top=10)

가장 확률이 높은 상위 10개 클래스를 출력하면 위와 같다. 들이는 보더콜리인데 완벽하게 일치하지는 않지만 대부분 유사한 견종이 상위 랭크에 위치하고 있음을 알 수 있다. 

 

 


 

📚 전이학습 (Transfer Learning) 

 

 전이 학습은 사전학습모델의 구조를 변경하거나 파라미터의 일부를 새롭게 학습해서 사용하는 방식을 의미한다. 전이학습도 세부 방식에 따라서 크게 3가지로 분류할 수 있다. 

✅ 전이학습 방식 

① 머신러닝 사용 

사전학습 모델이 출력하는 값을 feature로 보고, 전통적인 머신러닝 알고리즘을 적용하는 방법 (logistic regression, SVM, LightGBM  등). 하지만 최근에는 이러한 방식이 많이 사용되지 않음 

 

② 모델 구조 변경 

기존 모형의 일부 레이어를 제거하고 (일반적으로 top layer 부분) 다른 레이어를 추가해서 분류 실시하는 방법.

항상  top 레이어를 제거하는 것은 아니고 앞부분도 변경할 수 있다. 

 

③ Fine Tuning 

 사전학습모델의 파라미터 일부나 전체를 새로운 학습 데이터를 이용해서 새롭게 학습하는 방식. 이 방식이 대부분 결과가 좋기 때문에 최근에는 더 많이 사용된다.

 모델을 수정하기 위해서는 별도의 학습/테스트 데이터가 필요하고 위와 같이 별도의 폴더에 저장해야 한다. 각 이미지들을 클래스에 따라서 개별 하위 폴더에 저장해야 한다. 

 


 

✅ 모델 구조 변경 방식(2)으로 이미지 분류하기 

 여기서는 기존 VGG16 모델을 사용하되, 추가로 몇 개의 레이어를 더 쌓고 새로운 데이터로 모델을 학습한다. 즉, 위 그림에서 top layers 만을 unfreeze 하고 기존 모델 부분은 freezing 한다. 

📌 라이브러리, 데이터 불러오기 

import tensorflow as tf
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.preprocessing import image
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Flatten, Dense, GlobalAveragePooling2D
from tensorflow.keras.applications.vgg16 import VGG16, preprocess_input
import math
import numpy as np

#코랩 연결 
from google.colab import drive
drive.mount('/content/gdrive')

cd/content/gdrive/MyDrive/CDSS 딥러닝 강의/심화_1
TRAIN_DATA_DIR = './cats_and_dogs_small/train'
VALIDATION_DATA_DIR = './cats_and_dogs_small/validation'
TEST_DATA_DIR = './cats_and_dogs_small/test'

TRAIN_SAMPLES = 800*2
VALIDATION_SAMPLES = 400*2 
NUM_CLASSES = 2
IMG_WIDTH, IMG_HEIGHT = 224, 224
BATCH_SIZE = 64

 개와 고양이 이미지를 분류하는 모델을 만든다. train, validation 이미지를 클래스에 따라서 폴더별로 정리해 둔 데이터셋을 사용한다. 모든 이미지는 224x224 형태이다. 

 

TRAIN_DATA_DIR = './cats_and_dogs_small/train'
VALIDATION_DATA_DIR = './cats_and_dogs_small/validation'
TEST_DATA_DIR = './cats_and_dogs_small/test'

TRAIN_SAMPLES = 800*2
VALIDATION_SAMPLES = 400*2 
NUM_CLASSES = 2
IMG_WIDTH, IMG_HEIGHT = 224, 224
BATCH_SIZE = 64

 

 

📌 데이터 생성 

train_datagen = ImageDataGenerator(preprocessing_function=preprocess_input,
                                   rotation_range=20,
                                   width_shift_range=0.2,
                                   height_shift_range=0.2,
                                   zoom_range=0.2)

val_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)

 ImageDataGenerator를 이용해서 학습용 이미지를 추가로 더 생성한다. 학습 데이터가 많지 않은 경우 과적합 가능성이 높아지는데 실제로 데이터를 충분히 많이 수집하기 어려운 경우가 많다. 따라서 여기서는 원본 이미지를 변형하여 새로운 이미지를 생성하는 data augmentation을 실시한다. 단, validation 이미지는 정규화만 실시하고 데이터 augmentaion은 실시하지 않는다. 주요 데이터 증식 방법은 아래와 같다. 

 

 

케라스 imagegenerator : 

https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/image/ImageDataGenerator

 

 

train_generator = train_datagen.flow_from_directory(TRAIN_DATA_DIR,
                                                    target_size=(IMG_WIDTH,
                                                                 IMG_HEIGHT),
                                                    batch_size=BATCH_SIZE,
                                                    shuffle=True,
                                                    seed=12345,
                                                    class_mode='categorical')

validation_generator = val_datagen.flow_from_directory(
    VALIDATION_DATA_DIR,
    target_size=(IMG_WIDTH, IMG_HEIGHT),
    batch_size=BATCH_SIZE,
    shuffle=False,
    class_mode='categorical')

 각각 데이터를 불러오면 train set은 1600개 이미지, validation set은 800개 이미지임을 알 수 있다. 

 

 

📌 모델 정의 

def model_maker():
    base_model = VGG16(include_top=False, input_shape=(IMG_WIDTH, IMG_HEIGHT, 3))
    print(len(base_model.layers))

    for layer in base_model.layers[:]:
        layer.trainable = False

    input = Input(shape=(IMG_WIDTH, IMG_HEIGHT, 3))
    custom_model = base_model(input)
    custom_model = GlobalAveragePooling2D()(custom_model)
    custom_model = Dense(32, activation='relu')(custom_model)
    predictions = Dense(NUM_CLASSES, activation='softmax')(custom_model)
    
    return Model(inputs=input, outputs=predictions)
model_final = model_maker()
model_final.summary()

VGG16 모델을 불러오고, include_top=False로 설정한다. 라인 5,6에서 기본 모델에 포함되어 있는 레이어는 학습을 진행하지 않도록 설정한다. 그리고 기본 모델 위에 추가 레이어를 쌓는다. 최종 생성된 모델의 구조는 아래와 같다. 

 

 

📌 모델 학습 

model_final.compile(loss='categorical_crossentropy',
              optimizer=tf.keras.optimizers.Adam(0.001),
              metrics=['acc'])

history = model_final.fit(
    train_generator,
    steps_per_epoch=TRAIN_SAMPLES // BATCH_SIZE, # number of updates
    epochs=10,
    validation_data=validation_generator,
    validation_steps=VALIDATION_SAMPLES // BATCH_SIZE)

모델 컴파일과 학습을 진행한다. 이미지 용량이 크지 않았음에도 학습하는데에 시간이 꽤 걸렸다. 

 

 

import matplotlib.pyplot as plt
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.xlabel('epoch')
plt.ylabel('loss')
plt.legend(['train','val'])
plt.show()

import matplotlib.pyplot as plt
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.legend(['train','val'])
plt.show()

전반적으로 validation 에서 손실 함수가 일정하게 감소하고, 정확도도 증가하고 있으므로 학습이 잘 진행되었다고 볼 수 있다. 

 

 

📌 모델 성능 평가 

test_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)

test_generator = val_datagen.flow_from_directory(
    TEST_DATA_DIR,
    target_size=(IMG_WIDTH, IMG_HEIGHT),
    batch_size=BATCH_SIZE,
    shuffle=False,
    class_mode='categorical')

train, validation과 동일한 방식으로 test set을 불러온다. 단, test set에서도 data augmentation을 실시하지 않는다. 800개의 이미지가 포함되어 있고 마찬가지로 고양이, 개 2개의 클래스로 구성된다. 

 

 

model_final.evaluate(test_generator, steps=800 // BATCH_SIZE)

 test set에 대해서 학습한 모델로 예측을 진행한다. 

 

np.set_printoptions(suppress=True)
import matplotlib.pyplot as plt

img_path = 'cat.jpg'
img = image.load_img(img_path, target_size=(224, 224))
img_array = image.img_to_array(img)
plt.imshow(img_array/255)
expanded_img_array = np.expand_dims(img_array, axis=0)
preprocessed_img = expanded_img_array / 255

임의의 고양이 이미지를 불러오고 모델에 넣기 위해서 기본적인 전처리를 진행한다. 

 

prediction = model_final.predict(preprocessed_img)
print(np.array(prediction[0]))

 데이터를 불러올 때 첫 번째 폴더가 고양이 폴더였으므로, 고양이일 확률이 높게 나타나서 예측이 올바르게 이루어졌음을 알 수 있다.

 

  그런데 위에서 사용한 친구네 강아지 들이 이미지도 고양이로 예측했다;; 아무래도 학습한 데이터에 보더콜리 이미지가 많지 않았기 때문에 뾰족한 귀를 가진 들이를 고양이와 확실하게 구분하지 못한 듯 하다. 양질의 데이터의 중요성을 다시 한 번 느낄 수 있었고, 이미지넷 클래스에 포함되는 이미지를 예측하는 경우라면 기본 모델을 그대로 사용하는 것도 성능이 준수함을 알 수 있었다. 

 

 


✅ Fine Tuning 으로 이미지 분류하기 

 파인 튜닝 방식에서는 top layers 뿐만 아니라 기존 generic layers에 대해서도 unfreeze를 실시해서 모델을 학습한다. 가지고 있는 데이터에 대해서 많은 파라미터를 학습시킬 수 있기 때문에 좋은 성능을 내 수 있다. 

 

일반적으로 입력층에 가까울수록 이미지의 일반적인 정보를 추출하고, 출력층에 가까울수록 주어진 task에 특화된 세부적인 정보들을 추출한다. 따라서 출력층에서 가까운 파라미터부터 새롭게 학습한다. 

 

얼마나 많은 레이어를 fine tuning 할 것인지는 데이터의 양과 현재 task가 사전학습된 task와 얼마나 유사한 지에 따라서 결정한다. 

→ 학습 데이터가 많을수록, task 유사도가 작을수록 더 많은 레이어를 학습해야 한다. 

 

 

 

모델 정의 이전 부분의 코드는 '모델 구조 변경' 방식과 동일하다. 

📌 모델 정의 

def model_maker():
    base_model = MobileNet(include_top=False, input_shape=(IMG_WIDTH, IMG_HEIGHT, 3))

    for layer in base_model.layers[:-2]:
        layer.trainable = False # Top 층을 제외한 나머지 층에서 2개의 층을 새롭게 학습 

    input1 = Input(shape=(IMG_WIDTH, IMG_HEIGHT, 3))
    custom_model = base_model(input1)
    custom_model = GlobalAveragePooling2D()(custom_model)
    custom_model = Dense(64, activation='relu')(custom_model)
    predictions = Dense(NUM_CLASSES, activation='softmax')(custom_model)
    return Model(inputs=input1, outputs=predictions)
model = model_maker()

 라인4,5에서 몇 번째 레이어까지 다시 학습할 것인지를 정한다. 여기서는 top layer를 제외한 나머지 2개의 층을 새롭게 학습하고 나머지는 프리징한다. 

 

 

📌 모델 학습 

model.compile(loss='categorical_crossentropy',
              optimizer=tf.keras.optimizers.Adam(0.001),
              metrics=['acc'])
history=model.fit(
    train_generator,
    steps_per_epoch=math.ceil(float(TRAIN_SAMPLES) / BATCH_SIZE),
    epochs=10,
    validation_data=validation_generator,
    validation_steps=math.ceil(float(VALIDATION_SAMPLES) / BATCH_SIZE))

 

이후 평가 및 예측 부분도 '모델 구조 변경' 부분과 동일함. 

 

 

 

 


📚  Reference

• 연세대학교 디지털사회과학센터(CDSS) 파이썬을 활용한 딥러닝 기초 워크숍, 이상엽 교수님 

 

댓글