안 쓰던 블로그

사전 훈련된 CNN 사용하기 (Using a pretrained CNN) 본문

머신러닝/머신러닝

사전 훈련된 CNN 사용하기 (Using a pretrained CNN)

proqk 2021. 7. 16. 15:30
반응형

kaggle - Dogs vs. Cats 

1. 소규모 데이터셋에서 CNN 훈련하기(Dogs vs. Cats 데이터셋) https://foxtrotin.tistory.com/473

2. 사전 훈련된 CNN 사용하기 (ImageNet 데이터셋, VGG16 모델) https://foxtrotin.tistory.com/486

3. 미세 조정 https://foxtrotin.tistory.com/507

 

케라스 창사자에게 배우는 딥러닝 5장을 개인적으로 공부한 내용을 덧붙여 정리한 글입니다.


이전 글에서 캐글의 cats vs. dogs 데이터셋을 가지고 소규모 데이터셋에서 컨브넷을 훈련해 보았다. 데이터 증식 방법을 적용하여 과대 적합을 줄였지만, 전체 데이터 자체가 적기 때문에 컨브넷을 처음부터 훈련해서 더 높은 정확도를 달성하기는 어려웠다. 사전 훈련된 네트워크를 사용하면 소규모 데이터셋에서 효과적으로 딥러닝을 적용할 수 있다.

사전 훈련된 네트워크는 일반적으로 대규모 이미지 분류 문제를 위해 대량의 데이터셋에서 미리 훈련되어 저장된 네트워크이다. 원본 데이터셋이 충분히 크고 일반적이라면 사전 훈련된 네트워크에 의해 학습된 특성의 계층 구조는 실제 세상에 대한 일반적인 모델로 효율적인 역할을 할 수 있다. 새로운 문제가 원래 작업과 완전히 다른 클래스에 대한 것이더라도 이런 특성은 많은 컴퓨터 비전 문제에 유용하다. 예를 들어 동물과 생활 용품이 같이 있는 이미지 데이터셋에 훈련된 네트워크라면, 이 네트워크를 가지고 가구 아이템을 식별하는 등의 다른 용도로 사용할 수 있다.

 

여기에서는 (1.4백만 개의 레이블된 이미지와 1,000개의 클래스로 이루어진) ImageNet 데이터셋에서 훈련된 대규모 컨브넷을 사용해 보겠다. ImageNet 데이터셋은 다양한 종의 강아지와 고양이를 포함해 많은 동물들을 포함하고 있으므로 강아지 vs. 고양이 분류 문제에 좋은 성능을 낼 것 같다.

2014년에 개발된 VGG16 라는 CNN 구조를 사용한다. 조금 오래되었지만 공부하는 입장에서 이해하기 쉽기 때문에 선택하겠다.

 

사전 훈련된 네트워크를 사용하는 두 가지 방법에는 특성 추출미세 조정이 있다. 이 두 가지를 모두 다루어 보겠다.

 

특성 추출 (Feature extract)

특성 추출은 사전에 학습된 네트워크의 표현을 사용해 새로운 샘플에서 흥미로운 특성을 뽑아내는 것이다. 이런 특성을 사용하여 새로운 분류기를 처음부터 훈련한다.

컨브넷은 이미지 분류를 위해 두 부분으로 구성되는데, 연속된 합성곱 + 풀링 층으로 시작해서 완전 연결 분류기로 끝난다. 첫 번째 부분을 모델의 합성곱 기반층(convolutional base)이라고 부른다. 컨브넷의 경우 특성 추출은 사전에 훈련된 네트워크의 합성곱 기반층을 선택해 새로운 데이터를 통과시키고 그 출력으로 새로운 분류기를 훈련한다.

 

일반적으로 합성곱 층만 재사용하고 완전 연결 분류기는 재사용하지 않는다. 합성곱 층에 의해 학습된 표현이 더 일반적이어서 재사용 가능하기 때문이다.

컨브넷의 특성 맵은 사진에 대한 일반적인 컨셉의 존재 여부를 기록한 맵이다. 그래서 주어진 컴퓨터 비전 문제에 상관없이 유용하게 사용할 수 있다. 하지만 분류기에서 학습한 표현은 모델이 훈련된 클래스 집합에 특화되어 있다. 분류기는 전체 사진에 어떤 클래스가 존재할 확률에 관한 정보만을 담고 있어서 일반적이지 않다.

 

특정 합성곱 층에서 추출한 표현의 일반성(그리고 재사용성)의 수준은 모델에 있는 층의 깊이에 달려 있다. 모델의 하위 층은 (에지, 색깔, 질감 등과 같이) 지역적이고 매우 일반적인 특성 맵을 추출한다. 반면 상위 층은 ('강아지 눈'이나 '고양이 귀'와 같이) 좀 더 추상적인 개념을 추출한다. 만약 새로운 데이터셋이 원본 모델이 훈련한 데이터셋과 많이 다르다면 전체 합성곱 기반층을 사용하는 것보다는 모델의 하위 층 몇 개만 특성 추출에 사용하는 것이 좋다.

 

ImageNet의 클래스 집합에는 여러 종류의 강아지와 고양이를 포함하고 있으므로, 이런 경우 원본 모델의 완전 연결 층에 있는 정보를 재사용하는 것이 도움이 될 것이다. 하지만 새로운 문제의 클래스가 원본 모델의 클래스 집합과 겹치지 않는 좀 더 일반적인 경우를 다루기 위해서 여기서는 완전 연결 층을 사용하지 않겠다.


ImageNet 데이터셋

ImageNet 데이터셋에 훈련된 VGG16 네트워크의 합성곱 기반층을 사용하여 강아지와 고양이 이미지에서 유용한 특성을 추출해 본다. 그런 다음 이 특성으로 강아지 vs. 고양이 분류기를 훈련한다.

 

VGG16 모델

VGG16 모델은 케라스에 패키지로 포함되어 있으며, keras.applications 모듈에서 임포트할 수 있다. keras.applications 모듈에서는 여러 이미지 분류 모델을 사용할 수 있다. (모두 ImageNet 데이터셋에서 훈련됨)

 

https://keras.io/api/applications/vgg/#vgg16-function

 

Keras documentation: VGG16 and VGG19

VGG16 and VGG19 VGG16 function tf.keras.applications.VGG16( include_top=True, weights="imagenet", input_tensor=None, input_shape=None, pooling=None, classes=1000, classifier_activation="softmax", ) Instantiates the VGG16 model. Reference For image classifi

keras.io

import tensorflow as tf
import keras

from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.vgg16 import preprocess_input
import numpy as np

conv_base = VGG16(weights='imagenet',
                  include_top=False,
                  input_shape=(150, 150, 3))

 

VGG16 함수에 세 개의 매개변수를 전달한다

- weights는 모델을 초기화할 가중치 체크포인트를 지정한다.

- include_top은 네트워크의 최상위 완전 연결 분류기를 포함할지 안할지를 지정한다. 기본값은 ImageNet의 1,000개의 클래스에 대응되는 완전 연결 분류기를 포함한다. 강아지와 고양이 두 개의 클래스를 구분하는 별도의 완전 연결 층을 추가할 것이므로 포함시키지 않는다는 False로 설정한다.

- input_shape은 네트워크에 주입할 이미지 텐서의 크기이다. 이 매개변수는 선택사항으로, 값을 지정하지 않으면 네트워크가 어떤 크기의 입력도 처리할 수 있다. (참고로 완전 연결 분류기를 포함하기로 했지만 원본 모델과 동일한 크기여야 한다)

 

아래는 VGG16 합성곱 기반층의 자세한 구조이다. 이 구조는 이전 글에서 다룬 간단한 CNN과 비슷하다.

conv_base.summary()

최종 특성 맵의 크기는 (4, 4, 512)이다. 이 특성 위에 완전 연결 층을 놓을 것이다. 이 지점에서 두 가지 방식이 가능하다.

 

1. 새로운 데이터셋에서 합성곱 기반층을 실행하고 출력을 넘파이 배열로 디스크에 저장한다. 그다음 이 데이터를 독립된 완전 연결 분류기에 입력으로 사용한다.

합성곱 연산은 전체 과정 중에서 가장 비싼 부분이다. 이 방식은 모든 입력 이미지에 대해 합성곱 기반층을 한 번만 실행하면 되기 때문에 빠르고 비용이 적게 든다. 하지만 이런 이유 때문에 이 기법에는 데이터 증식을 사용할 수 없다.

 

2. 준비한 모델(conv_base) 위에 Dense 층을 쌓아 확장한다. 그다음 입력 데이터에서 엔드 투 엔드로 전체 모델을 실행한다.

모델에 노출된 모든 입력 이미지가 매번 합성곱 기반층을 통과하기 때문에 데이터 증식을 사용할 수 있다. 하지만 이런 이유로 이 방식은 첫 번째 방식보다 훨씬 비용이 많이 든다.

 

두 가지 방식을 모두 다루어 보겠다.

 

1. 데이터 증식을 사용하지 않는 특성 추출 (빠름, 비용 적음, 과대적합 위험)

먼저 첫 번째 방식을 구현하자. conv_base에 데이터를 주입하고 출력을 기록한다.

이 출력을 새로운 모델의 입력으로 사용하겠다.

 

일단 ImageDataGenerator를 사용해 이미지와 레이블을 넘파이 배열로 추출한다. conv_base 모델의 predict 메서드를 호출하여 이 이미지에서 특성을 추출할 수 있다.

import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator

base_dir = '/content/gdrive/My Drive/Kaggle/Keras/datasets/cats_and_dogs_small'

train_dir = os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir = os.path.join(base_dir, 'test')

datagen = ImageDataGenerator(rescale=1./255)
batch_size = 20

def extract_features(directory, sample_count):
    features = np.zeros(shape=(sample_count, 4, 4, 512))
    labels = np.zeros(shape=(sample_count))
    generator = datagen.flow_from_directory(
        directory,
        target_size=(150, 150),
        batch_size=batch_size,
        class_mode='binary')
    i = 0
    for inputs_batch, labels_batch in generator:
        features_batch = conv_base.predict(inputs_batch)
        features[i * batch_size : (i + 1) * batch_size] = features_batch
        labels[i * batch_size : (i + 1) * batch_size] = labels_batch
        i += 1
        if i * batch_size >= sample_count:
            #제너레이터는 루프 안에서 무한하게 데이터를 만들어내므로 모든 이미지를 한 번씩 처리하고 나면 중지해야 한다
            break
    return features, labels

train_features, train_labels = extract_features(train_dir, 2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels = extract_features(test_dir, 1000)

 

추출된 특성의 크기는 (samples, 4, 4, 512)이다. 완전 연결 분류기에 주입하기 위해서 먼저 (samples, 8192) 크기로 펼쳐야 한다.

train_features = np.reshape(train_features, (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features = np.reshape(test_features, (1000, 4 * 4 * 512))

 

그런 다음 완전 연결 분류기를 정의하고(규제를 위해 드롭아웃 사용) 저장된 데이터와 레이블을 사용해 훈련한다.

from keras import models
from keras import layers
from keras import optimizers

model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
              loss='binary_crossentropy',
              metrics=['acc'])

history = model.fit(train_features, train_labels,
                    epochs=30,
                    batch_size=20,
                    validation_data=(validation_features, validation_labels))

 

두 개의 Dense 층만 처리하면 되기 때문에 훈련이 매우 빠르다. 훈련 손실과 정확도 곡선을 출력해 본다.

import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

90%의 검증 정확도에 도달했다. 이는 이전 글(https://foxtrotin.tistory.com/486)에서 처음 훈련시킨 작은 모델보다 훨씬 나은 값이다. 하지만 드롭아웃을 많이 사용한 것치고는 훈련을 시작하자마자 거의 바로 과대적합되었다. 작은 이미지 데이터셋에서 과대적합을 막기 위해 필수적으로 해야 하는 데이터 증식을 사용하지 않았기 때문이다.

 

2. 데이터 증식을 사용한 특성 추출 (느림, 비용 큼, 과대적합 적음)

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

conv_base = VGG16(weights='imagenet', include_top=False, input_shape=(150, 150, 3))

model = keras.Sequential()
model.add(keras.Input(shape=(150,150,3)))
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

1번에서 했던 conv_base 모델을 확장한다. 모델은 층과 동일하게 작동하므로 Sequential 모델에 다른 모델을 층처럼 추가할 수 있다.

 

이 부분에서 책에서는 model.add(keras.Input(shape=(150,150,3))) 부분이 없는데, shape가 없거나 build를 하지 않았다는 ValueError가 났다.  (ValueError: This model has not yet been built. Build the model first by calling `build()` or calling `fit()` with some data, or specify an `input_shape` argument in the first layer(s) for automatic build.)

conv_base 이름의 VGG16 모델을 만들면서 input_shape를 해 준 것을 Sequential 모델에 add했기 때문에 괜찮을 것 같았는데, VGG16 모델 따로, Sequential 모델 따로 해 주어야 하나 보다. (추측)

 

아무튼 model 쪽에서도 shape를 지정해 주면 해결된다. 다음은 모델의 구조이다.

VGG16의 합성곱 기반 층은 14714688개의 파라미터를 가진 반면, 합성곱 기반 층 위에 추가한 분류기는 2097408개의 파라미터로 차이가 크다.

 

Pre-trained, 즉 사전 훈련된 모델을 사용할 것이기 때문에, 지금부터 진행하는 훈련에서는 가중치가 업데이트되면 안 된다. 그래서 모델을 컴파일하고 훈련하기 전에 Convolutional Layer를 Freeze해야 한다. Freeze(동결)된 층은 훈련 중 가중치 업데이트가 되지 않는다. 여기서 Freeze를 하지 않으면 Pre-trained 모델을 가져다 쓰는 게 소용이 없어진다.

 

conv_base.trainable = False

케라스에서는 trainable 속성을 False로 주어서 네트워크를 동결한다.

 

print('conv_base를 동결한 후 훈련되는 가중치의 수:', len(model.trainable_weights))

 

False를 하기 전에는 30개, 이후에는 4개만 훈련된다. 추가한 2개의 Dense층마다 2개씩 있는 가중치(가중치 행렬과 편향 벡터) 총 4개의 텐서가 훈련되는 것이다. 변경 사항을 적용하기 위해 모델을 컴파일 한다. (trainable 속성을 변경하면 컴파일 해야 적용된다)

 

컴파일까지 했으면 이전 글(https://foxtrotin.tistory.com/486)에서 했던 데이터 증식을 사용하여 모델을 훈련한다. 

from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(
      rescale=1./255,
      rotation_range=20,
      width_shift_range=0.1,
      height_shift_range=0.1,
      shear_range=0.1,
      zoom_range=0.1,
      horizontal_flip=True,
      fill_mode='nearest')

#검증 데이터는 증식 X
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
        #타깃 디렉터리
        train_dir,
        #모든 이미지의 크기를 150 × 150로 변경한다
        target_size=(150, 150),
        batch_size=20,
        #binary_crossentropy 손실을 사용하므로 이진 레이블을 사용
        class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
        validation_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=2e-5),
              metrics=['acc'])

history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=30,
      validation_data=validation_generator,
      validation_steps=50,
      verbose=2)

 

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()

plt.show()

그래프를 출력해 보면 검증 정확도 자체는 이전 CNN 그래프와 비슷하다.

하지만 아래 그래프를 보면 과대적합이 약간 줄은 것을 볼 수 있다.

 

model.save('cats_and_dogs_small_3.h5')

훈련이 끝나면 항상 모델을 저장하는 습관을 가지자.

반응형
Comments