DeepLearning/Tensorflow

케라스 창시자에게 배우는 딥러닝 - 8장 컴퓨터 비전을 위한 딥러닝

Dobby-HJ 2023. 8. 4. 08:43

1. 합성공 신경망 소개

from tensorflow import keras
from tensorflow.keras import layers
inputs = keras.Input(shape=(28, 28, 1))
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(inputs)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(10, activation="softmax")(x)
model = keras.Model(inputs=inputs, outputs=outputs)

코드 8-2 모델의 summary() 메서드 출력

model.summary()
'''
출력 결과
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 28, 28, 1)]       0         

 conv2d (Conv2D)             (None, 26, 26, 32)        320       

 max_pooling2d (MaxPooling2D  (None, 13, 13, 32)       0         
 )                                                               

 conv2d_1 (Conv2D)           (None, 11, 11, 64)        18496     

 max_pooling2d_1 (MaxPooling  (None, 5, 5, 64)         0         
 2D)                                                             

 conv2d_2 (Conv2D)           (None, 3, 3, 128)         73856     

 flatten (Flatten)           (None, 1152)              0         

 dense (Dense)               (None, 10)                11530     

=================================================================
Total params: 104,202
Trainable params: 104,202
Non-trainable params: 0
_________________________________________________________________
'''

Conv2D와 MaxPooling2D 층의 출력은 (height, width, channels) 크기의 랭크-3 텐서입니다. 높이와 너비 차원은 모델이 깊어질수록 작아지는 경향이 있습니다. 채널의 수는 Conv2D 층에 전달된 첫 번째 매개변수에 의해 조절됩니다.

마지막 Conv2D 층의 출력은 (3, 3, 128)입니다. 즉, 128개의 채널을 가진 3x3 크기의 특성 맵(Feature map)입니다. 다음 단계는 이 출력을 밀집 연결 분류기로 주입하는 것입니다. 이 분류기는 Dense 층을 쌓은 것으로 이미 익숙한 구조입니다. 이 분류기는 1D 벡터를 처리하는데, 이전 층의 출력이 랭크-3 텐서입니다. 그래서 Dense 층 이전에 Flatten 층으로 먼저 3D 출력을 1D 텐서로 펼쳐야 합니다.

마지막으로 10개의 클래스를 분류하기 위해 마지막 층의 출력 크기를 10으로 하고 소프트 맥스 활성화 함수를 사용합니다.

이제 MNIST 숫자 이미지에 이 컨브넷을 훌련합니다. 2장의 MNIST 예제 코드를 많이 재사용하겠습니다.

코드 8-3 MNIST 이미지에서 컨브넷 훈련하기

from tensorflow.keras.datasets import mnist

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000, 28, 28, 1))
train_images = train_images.astype("float32") / 255
test_images = test_images.reshape((10000, 28, 28, 1))
test_images = test_images.astype("float32") / 255
model.compile(optimizer="rmsprop",
    loss="sparse_categorical_crossentropy",
    metrics=["accuracy"])
model.fit(train_images, train_labels, epochs=5, batch_size=64)

테스트 데이터에서 모델을 평가해 보죠.

코드 8-4 컨브넷 평가하기

test_loss, test_acc = model.evaluate(test_images, test_labels)
print(f"테스트 정확도: {test_acc:.3f}")
'''
출력 결과
313/313 [==============================] - 1s 4ms/step - loss: 0.0308 - accuracy: 0.9904
테스트 정확도: 0.990
'''

1.1 합성곱 연산

완전 연결 층과 합성곱 층 사이의 근본적인 차이는 다음과 같습니다. Dense 층은 입력 특성 공간에 있는 전역 패턴(예를 들어 MNIST 숫자 이미지에서는 모든 픽셀에 걸친 패턴)을 학습하지만 합성곱 층은 지역 패턴을 학습합니다. 앞의 예에서는 이 윈도우는 모두 3x3 크기였습니다.

이 핵심 Feature는 컨브넷에 두 가지 흥미로운 성질을 제공합니다.

  • 학습된 패턴은 평행 이동 불변성(translation invariant)을 가집니다. 컨브넷이 이미지의 오른쪽 아래 모서리에서 어떤 패턴을 학습했다면 다른 곳(예를 들어 왼쪽 위 모서리)에서도 이 패턴을 인식할 수 잇습니다. 완전 연결 네트워크는 새로운 위치에 나타난 것은 새로운 패턴으로 학습해야 합니다. 이런 성질은 컨브넷이 이미지를 효율적으로 처리하게 만들어줍니다.(근본적으로 우리가 보는 세상은 평행 이동으로 인해 다르게 인식되지 않습니다). 적은 수의 훈련 샘플을 사용해서 일반화 능력을 가진 표현을 학습할 수 있습니다.
  • 컨브넷은 패턴의 공간적 계층 구조를 학습할 수 있습니다. 첫 번째 합성곱 층이 에지 같은 작은 지역 패턴을 학습합니다. 두 번째 합성곱 층은 첫 번째 층의 특성으로 구성된 더 큰 패턴을 학습하는 식입니다. 이런 방식을 사용하여 컨브넷은 매우 복잡하고 추상적인 시각적 개념을 효과적으로 학습할 수 있습니다. 근본적으로 우리가 보는 세상은 공간적 계층 구조를 가지고 있기 때문입니다.

학성곱 연산은 특성 맵(Feature map)이라고 부르는 랭크-3 텐서에 적용됩니다. 이 텐서는 2개의 공간 축(높이와 너비)과 깊이 축(채널 축이라고도 합니다)으로 구성됩니다. RGB 이미지는 3개의 컬러 채널을 가지므로 깊이 축의 차원이 3이 됩니다. MNIST 숫자처럼 흑백 이미지는 깊이 축의 차원이 1입니다. 합성곱 연산은 입력 특성 맵에서 작은 패치(patch)들을 추출하고 이런 모든 패치에 같은 변환을 적용하여 출력 특성 맵(output feature map)을 만듭니다.

출력 특성 맵도 높이와 너비를 가진 랭크-3 텐서입니다. 출력 텐서의 깊이는 층의 매개변수로 결정되기 때문에 상황에 따라 다릅니다. 이렇게 되면 깊이 축의 채널은 더 이상 RGB 입력처럼 특정 컬러를 의미하지 않습니다. 그 대신 일종의 **필터(filter)**를 의미합니다. 필터는 입력 데이터의 어떤 특성을 인코딩합니다. 예를 들어 고수준으로 보면 하나의 필터가 “입력에 얼굴이 있는지”를 인코딩할 수 있습니다.

MNIST 예제에서는 첫 번째 합성공 층이(28, 28, 1) 크기의 특성 맵을 입력으로 받아 (26, 26, 32) 크기의 Feature map을 출력합니다. 이 값은 입력에 대한 필터의 **응답 맵(response map)**입니다.

**특성 맵(Feature map)**이란 말이 의미하는 것은 다음과 같습니다. 깊이 축에 있는 각 차원은 하나의 특성(또는 필터)이고, 랭크-2 텐서인 output[:, :, n]은 입력에 대한 이 필터의 응답을 나타내는 2D 공간상의 맵입니다.

합성곱의 핵심적인 2개의 파라미터로 정의됩니다.

  • 입력으로부터 뽑아낼 패치의 크기 : 전형적으로 3x3 또는 5x5를 사용합니다.
  • 특성 맵의 출력 깊이 : 합성곱으로 계산할 필터의 개수입니다. 이 예에서는 깊이 32로 시작해서 깊이 128로 끝났습니다.

케라스의 Conv2D 층에서 이 파라미터는 Conv2D(output_depth, (window_height, window_width)) 처럼 첫 번째와 두 번째 매개변수로 전달됩니다.

3D 입력 특성 맵 위를 3 x 3 또는 5 x 5 크기의 윈도우가 슬라이딩(sliding)하면서 모든 위치에서 3D 특성 패치((window_height, window_width, input_depth) 크기)를 추출하는 방식으로 합성곱이 작동합니다. 이런 3D 패치는 합성곱 커널(convolution kernel)이라고 불리는 하나의 학습된 가중치 행렬과 텐서 곱셈을 통해(output_depth,) 크기의 1D 벡터로 변환됩니다. 동일한 커널이 모든 패치에 걸쳐서 재사용됩니다. 변환된 모든 벡터는 (height, width, output_depth) 크기의 3D 특성 맵으로 재구성됩니다. 출력 특성 맵의 공간상 위치는 입력 특성 맵의 같은 위치에 대응됩니다.(예를 들어 출력 오른쪽 아래 모서리는 입력의 오른쪽 아래 부근에 해당하는 정보를 담고 있습니다). 3 x 3 윈도우를 사용하면 3D 패치 input[i-1:i+2, j-1:j+2, :]로 부터 벡터 output[i, j, :]가 만들어집니다.

두 가지 이유로 출력 높이와 너비는 입력의 높이, 너비와 다를 수 있습니다.

  • 경계 문제. 입력 특성 맵에 패딩을 추가하여 대응할 수 있습니다.
  • 잠시 후에 설명할 스트라이드(stride)의 사용 여부에 따라 다릅니다.

경계 문제와 패딩 이해하기

입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶다면 패딩(padding)을 사용할 수 있습니다. 패딩은 입력 특성 맵의 가장자리에 적절한 개수의 행과 열을 추가합니다.

합성곱 스트라이드 이해하기

출력 크기에 영향을 미치는 다른 요소는 스트라이드입니다. 지금까지 합성곱에 대한 설명은 합성 곱 윈도우의 중앙 타일이 연속적으로 지나간다고 가정한 것입니다. 당연히 연속적으로 지나가지 않을 수도 있습니다.

1.2 최대 풀링 연산(MaxPooling)

왜 최대값을 이용해서 특성 맵을 다운 샘플링할까요?

inputs = keras.Input(shape=(28, 28, 1))
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(inputs)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(10, activation="softmax")(x)
model_no_max_pool = keras.Model(inputs=inputs, outputs=outputs)

위 모델에서의 문제는 무엇일까요?

  • 특성의 공간적 계층 구조를 학습하는데 도움이 되지 않습니다. 세 번째 층의 3 x 3 윈도우는 초기 입력의 7 x 7 윈도우 영역에 대한 정보만 담고 있습니다. 컨브넷에 의해 학습된 고수준 패턴은 초기 입력에 관한 정보가 아주 적어 숫자 분류를 학습하기에 충분하지 않을 것입니다.
  • 최종 특성 맵은 굉장히 많은 수의 원소를 가집니다. 이는 과대적합을 발생시킬 수 있습니다.

Maxpooling이 다운 샘플링을 할 수 있는 유일한 방법은 아닙니다. 이미 알고 있듯이 앞선 합성곱 층에서 스트라이드를 사용할 수 있습니다. 최대값을 취하는 최대 풀링 대신에 입력 패치의 채널별 평균값을 계산하여 변환하는 평균 풀링(average pooling)을 사용할 수도 있습니다. 하지만 최대 풀링이 다른 방법들보다 더 잘 작동하는 편입니다. 그 이유는 특성이 특성 맵의 각 타일에서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문입니다.(그래서 특성의 지도(맵)입니다.) 따라서 특성의 평균값보다 여러 특성 중 최댓값을 사용하는 것이 더 유용합니다.

2. 소규모 데이터셋에서 밑바닥부터 컨브넷 훈련하기

컴퓨터 비전에서 과대적합을 줄이기 위한 강력한 방법인 데이터 **증식(data augmentation)**을 소개 하겠습니다.

데이터 증식을 통해 네트워크의 성능을 80%~85% 정확도로 향상시킬 것입니다.

다음 절에서 작은 데이터셋에 딥러닝을 적용하기 위한 핵심적인 기술 두 가지를 살펴보겠습니다. 사전 훈련된 네트워크로 특성을 추출하는 것과 사전 훈련된 네트워크를 세밀하게 튜닝하는 것입니다.

2.1 작은 데이터셋 문제에서 딥러닝의 타당성

2.2 데이터 내려받기

구글 코랩에서 캐글 데이터셋 내려받기

!kaggle competitions downlad -c dogs-vs-cats

하지만 캐글 사용자만 이 API를 사용할 수 있습니다. 따라서 앞의 명령을 실행하기 위해서는 먼저 사용자 인증이 필요합니다. kaggle 패키지는 JSON 파일인 ~/.kaggle/kaggle.json에서 로그인 정보를 찾습니다. 이 파일을 만들어보겠습니다.

  1. 먼저 캐글 API 키를 만들어 로컬 컴퓨터로 내려받아야 합니다. 웹 브라우저로 캐글 웹 사이트로 접속하여 로그인한 후 Account 페이지로 이동합니다. 이 페이지에서 API 섹션을 찾으세요. 그 다음 Create New API Token 버튼을 누르면 Kaggle.json 파일이 생성되고 컴퓨터로 내려받기 됩니다.
  2. 그 다음 코랩 노트북으로 이동한 후 셀에서 다음 명령을 실행하여 API 키가 담긴 JSON 파일을 현재 코랩 세션(session)에 업로드하겠습니다.
from google.colab import files
files.upload()

이 셀을 실행하면 파일 선택 버튼이 나옵니다. 버튼을 누르고 방금 전에 내려받은 kaggle.json 파일을 선택합니다. 그러면 이 파일이 현재 코랩 런타임에 업로드됩니다.

  1. 마지막으로 ~/.kaggle 폴더를 만들고 (mkdir ~/.kaggle) 키 파일을 이 폴더로 복사합니다.(cp kaggle. json ~/.kaggle/) 보안을 위해 현재 사용자만 이 파일을 읽을 수 있게 하빈다.(chmod 600)
!mkdir ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json

이제 사용할 데이터를 내려받을 수 있습니다.

!unzip -qq dogs-vs-cats.zip
!unzip -qq train.zip

shutile 패키지를 사용하여 이런 구조를 만들어 보겠습니다.

코드 8-6 이미지를 훈련, 검증, 테스트 디렉터리로 복사하기

import os, shutil, pathlib

original_dir = pathlib.Path("train")
new_base_dir = pathlib.Path("cats_vs_dogs_small")

def make_subset(subset_name, start_index, end_index):
    for category in ("cat", "dog"):
        dir = new_base_dir / subset_name / category
        os.makedirs(dir)
        fnames = [f"{category}.{i}.jpg" for i in range(start_index, end_index)]
        for fname in fnames:
            shutil.copyfile(src=original_dir / fname, dst=dir / fname)

make_subset("train", start_index=0, end_index=1000)
make_subset("validation", start_index=1000, end_index=1500)
make_subset("test", start_index=1500, end_index=2500)

2.3 모델 만들기

첫 번째 예제에서 보았던 일반적인 모델 구조를 동일하게 재사용하게씃ㅂ니다. Conv2D(relu 활성화 함수)와 MaxPooling2D` 층을 번갈아 쌓은 컨브넷입니다.

Note 특성 맵의 깊이는 모델에서 점진적으로 증가하지만(32에서 256까지), 특성맵의 크기는 감소합니다.(180 x 180에서 7 x 7까지). 이는 거의 모든 컨브넷에서 볼 수 있는 전형적인 패턴입니다.

이진 분류 문제이므로 모델은 하나의 유닛(크기가 1인 Dense 층)과 sigmoid 활성화 함수로 끝납니다. 이 유닛은 모델이 보고 있는 샘플이 한 클래스에 속할 확률은 인코딩할 것입니다.

마지막 작은 차이점은 Rescaling 층으로 모델이 시작되는 것입니다. 이층은 원래 [0, 255] 범위의 값인)이미지 입력을 [0, 1] 범위로 스케일 변환합니다.

코드 8-7 강아지 vs 고양이 분류를 위한 소규모 컨브넷 만들기

from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(180, 180, 3))
x = layers.Rescaling(1./255)(inputs)
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs=inputs, outputs=outputs)
model.summary()
'''
출력 결과
Model: "model_3"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_4 (InputLayer)        [(None, 180, 180, 3)]     0         

 rescaling_1 (Rescaling)     (None, 180, 180, 3)       0         

 conv2d_11 (Conv2D)          (None, 178, 178, 32)      896       

 max_pooling2d_7 (MaxPooling  (None, 89, 89, 32)       0         
 2D)                                                             

 conv2d_12 (Conv2D)          (None, 87, 87, 64)        18496     

 max_pooling2d_8 (MaxPooling  (None, 43, 43, 64)       0         
 2D)                                                             

 conv2d_13 (Conv2D)          (None, 41, 41, 128)       73856     

 max_pooling2d_9 (MaxPooling  (None, 20, 20, 128)      0         
 2D)                                                             

 conv2d_14 (Conv2D)          (None, 18, 18, 256)       295168    

 max_pooling2d_10 (MaxPoolin  (None, 9, 9, 256)        0         
 g2D)                                                            

 conv2d_15 (Conv2D)          (None, 7, 7, 256)         590080    

 flatten_3 (Flatten)         (None, 12544)             0         

 dense_3 (Dense)             (None, 1)                 12545     

=================================================================
Total params: 991,041
Trainable params: 991,041
Non-trainable params: 0
_________________________________________________________________
'''

2.4 데이터 전처리

데이터는 네트워크에 주입되기 전에 부동 소수점 타입의 텐서로 적절하게 전처리되어 있어야 합니다. 지금은 데이터가 JPEG 파일로 되어 있으므로 네트워크에 주입하려면 대략 다음 과정을 따릅니다.

  1. 사진 파일을 읽습니다.
  2. JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩합니다.
  3. 그 다음 부동 소수점 타입의 텐서로 변환합니다.
  4. 동일한 크기의 이미지로 바꿉니다(여기에서는 180 x 180을 사용합니다.)
  5. 배치로 묶습니다(하나의 배치는 32개의 이미지로 구성됩니다.

좀 복잡하게 보일 수 있지만 다행히 케라스는 이런 단계를 자동으로 처리하는 유틸리티가 있습니다. image_dataset_from_directory() 함수를 제공합니다. 이 함수를 사용하면 디스크에 있는 이미지 파일을 자동으로 전처리된 텐서의 배치로 변환하는 데이터 파이프라인을 빠르게 구성할 수 있습니다. 여기에서 이 함수를 사용해 보겠습니다.

코드 8-9 image_dataset_from_directory를 사용하여 이미지 읽기

from tensorflow.keras.utils import image_dataset_from_directory

train_dataset = image_dataset_from_directory(
    new_base_dir / "train",
    image_size=(180, 180),
    batch_size=32)
validation_dataset = image_dataset_from_directory(
    new_base_dir / "validation",
    image_size=(180, 180),
    batch_size=32)
test_dataset = image_dataset_from_directory(
    new_base_dir / "test",
    image_size=(180, 180),
    batch_size=32)

텐서플로 Dataset 객체 이해하기

텐서플로는 머신 러닝 모델을 위한 효율적인 입력 파이프라인을 만들 수 있는 tf.data.API를 제공합니다. 핵심 클래스는 tf.data.Dataset입니다.

Dataset 객체는 반복자(iterator)입니다. 즉, for 루프에 사용할 수 있으며 일반적으로 입력 데이터와 레이블의 배치를 반환합니다. Dataset 객체를 바로 케라스 모델의 fit() 메서드에 전달할 수 있습니다.

Dataset 클래스는 직접 구현하기 어려운 여러 가지 핵심 기능을 처리해 줍니다. 특히 비동기 데이터 프리페칭(prefetching)입니다.(디전 배치를 모델이 처리하는 동안 다음 배치 데이터를 전처리하기 때문에 중단 없이 모델을 계속 실행할 수 있습니다.

Dataset 클래스는 데이터셋을 조작하기 위한 함수형 스타일의 API도 제공합니다. 다음은 간단한 예입니다. 랜덤한 넘파이 배열을 사용해서 Dataset 객체를 만들어보죠. 샘플 1000개를 만들겠습니다. 각 샘플은 크기가 16인 벡터입니다.

import numpy as np
import tensorflow as tf
random_numbers = np.random.normal(size=(1000, 16))
dataset = tf.data.Dataset.from_tensor_slices(random_numbers)`

처음에는 이 데이터셋이 하나의 샘플을 반환합니다.

.batch() 메서드를 사요앟면 데이터의 배치가 반환됩니다.

일반적으로 다음과 같은 유용한 메서드를 사용할 수 있습니다.

  • .shuffle(buffer_size) : 버퍼 안의 원소를 섞습니다.
  • .prefetch(buffer_size) : 장치 활용도를 높이기 위해 GPU 메모리에 로드할 데이터를 미리 준비합니다.
  • map(callable) : 임의의 변환을 데이터셋의 각 원소에 적용합니다.(callable 함수는 데이터셋이 반환하는 1개의 원소를 입력으로 기대합니다.)

특히 .map() 메서드는 자주 사용합니다. 예를 들어 예제 데이터셋의 원소 크기를 (16.)에서 (4,4)로 변환해 보겠습니다.

reshaped_dataset = dataset.map(lambda x : tf.reshape(x, (4, 4))

코드 8-10 Dataset이 반환하는 데이터와 레이블 크기 확인하기

for data_batch, labels_batch in train_dataset:
    print("데이터 배치 크기:", data_batch.shape)
    print("레이블 배치 크기:", labels_batch.shape)
    break
'''
출력 결과
데이터 배치 크기: (32, 180, 180, 3)
레이블 배치 크기: (32,)
'''

이 데이터셋에서 모델을 훈련해보죠. fit() 메서드의 validation_data 매개변수를 사용하여 별도의 Dataset 객체로 검증 지표를 모니터링하겠습니다.

또한, ModelCheckpoint 콜백을 사용하여 에포크가 끝날 때마다 모델을 저장하겠습니다. 콜백에 파일을 저장할 경로와 매개변수 save_best_only=True와 monitor=’val_loss’를 지정할 것입니다.

callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="convnet_from_scratch.keras",
        save_best_only=True,
        monitor="val_loss")
]
history = model.fit(
    train_dataset,
    epochs=30,
    validation_data=validation_dataset,
    callbacks=callbacks)

코드 8-12 훈련 과정의 정확도와 손실 그래프 그리기.

import matplotlib.pyplot as plt
accuracy = history.history["accuracy"]
val_accuracy = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]
epochs = range(1, len(accuracy) + 1)
plt.plot(epochs, accuracy, "bo", label="Training accuracy")
plt.plot(epochs, val_accuracy, "b", label="Validation accuracy")
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()

코드 8-13 테스트 세트에서 모델 평가하기

test_model = keras.models.load_model("convnet_from_scratch.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"테스트 정확도: {test_acc:.3f}")
'''
출력 결과
63/63 [==============================] - 3s 37ms/step - loss: 0.6083 - accuracy: 0.6985
테스트 정확도: 0.698
'''

2.5 데이터 증식(augmentation) 사용하기

케라스에서는 모델 시작 부분에 여러 개의 데이터 증식 층(data augmentation layer)을 추가할 수 있습니다.

코드 8-14 컨브넷에 추가할 데이터 증식 단계 정의하기

data_augmentation = keras.Sequential(
    [
        layers.RandomFlip("horizontal"),
        layers.RandomRotation(0.1),
        layers.RandomZoom(0.2),
    ]
)

사용할 수 있는 층은 이보다 더 많습니다. 아래에서 코드를 간단히 살펴보겠습니다.

  • RandomFlip("horizontal") : 랜덤하게 50% 이미지를 수평으로 뒤집습니다.
  • RandomRotation(0.1) : [-10%, +10%] 범위 안에서 랜덤한 값만큼 입력 이미지를 외전합니다.(전체 원에 대한 비율입니다. 각도로 나타내면 [-36도, +36도]에 해당합니다.
  • RondomZoom(0.2) : [-20%, +20%] 범위 안에서 랜덤한 비율만큼 이미지를 확대 또는 축소합니다.

즉싱된 이미지를 확인해 보겠습니다.

코드 8-15 랜덤하게 증식된 훈련 이미지 출력하기

plt.figure(figsize=(10, 10))
# take(N)을 사용하여 데이터셋에서 (N)개의 배치만 샘플링합니다. 
# 이는 N번째 배치 후에 루프를 중단하는 것과 같습니다.
for images, _ in train_dataset.take(1): 
        for i in range(9):
                augmented_images = data_augmentation(images)
                ax = plt.subplot(3, 3, i + 1)
                plt.imshow(augmented_images[0].numpy().astype("unit8")
                plt.axis("off")

데이터 증식을 사용하면 새로운 모델을 훈련시킬 때 모델에 같은 입력 데이터가 두 번 주입되지 않습니다. 하지만 적은 수의 원본 이미지에서 만들어졌기 때문에 여전히 입력 데이터들 사이에 상호 연관성이 큽니다. 즉, 새로운 정보를 만들어 낼 수 없고 단지 기존 정보의 재조합만 가능합니다. 그렇기 때문에 완전히 과대적합을 제거하기에 충분하지 않을 수 있습니다. 과대적합을 더 억제하기 위해 밀집 연결 분류기 직전에 Dropout 층을 추가하겠습니다.

랜덤한 이미지 증식 층에 대해 마지막으로 알아야 할 한가지는 Dropout 층처럼 추론할 때 (predict()나 evaluate() 메서드를 호출할 때)는 동작하지 않는다는 것입니다. 즉, 모델을 평가할 때는 데이터 증식과 드롭아웃이 없는 모델처럼 동작합니다.

코드 8-15 이미지 증식과 드롭아웃을 포함한 컨브넷 만들기

inputs = keras.Input(shape=(180, 180, 3))
x = data_augmentation(inputs)
x = layers.Rescaling(1./255)(x)
x = layers.Conv2D(filters=32, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=64, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=128, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.MaxPooling2D(pool_size=2)(x)
x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs=inputs, outputs=outputs)

model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])

데이터 증식과 드롭아웃을 사용해서 모델을 훈련해 보겠습니다. 훈련에서 과대적합이 훨씬 늦게 일어날 것으로 기대되기 때문에 3배 많은 100 epoch 동안 훈련하겠습니다.

callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="convnet_from_scratch_with_augmentation.keras",
        save_best_only=True,
        monitor="val_loss")
]
history = model.fit(
    train_dataset,
    epochs=100,
    validation_data=validation_dataset,
    callbacks=callbacks)

코드 8-18 테스트 세트에서 모델 성능 측정하기

test_model = keras.models.load_model(
    "convnet_from_scratch_with_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"테스트 정확도: {test_acc:.3f}")
'''
출력 결과
63/63 [==============================] - 3s 36ms/step - loss: 0.4682 - accuracy: 0.8315
테스트 정확도: 0.831
'''

3. 사전 훈련된 모델 활용하기

사전 훈련된 모델(pretrained model)은 일반적으로 대규모 이미지 분류 문제를 위해 대량의 데이터셋에서 미리 훈련된 모델입니다.

사전 훈련된 모델을 사용하는 두 가지 방법이 있습니다. 특성 추출(Feature Extraction)과 미세 조정(fine tuning)입니다. 이 두가지를 모두 다루어 보겠습니다. 먼저 특성 추출(Feature Extracion)입니다.

3.1 사전 훈련된 모델을 사용한 특성 추출

왜 합성곱 층만 재사용할까요? 밀집 연결 분류기도 재사용할 수 있을까요? 일반적으로 권장하지 않습니다. 합성곱 층에 의해 학습된 표현이 더 일반적이어서 재사용이 가능하기 때문입니다. 컨브넷의 특성 맵은 이미지에 대한 일반적인 콘셉트의 존재 여부를 기록한 맵입니다. 주어진 컴퓨터 비전 문제에 상관없이 유용하기 사용할 수 있습니다.

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

코드 8-19 VGG16 기반 합성곱 기반 층 만들기

conv_base = keras.applications.vgg16.VGG16(
        weights="imagenet", # 어떤 dataset으로 학습한 모델 가중치인지 결정
        include_top=False, # 네트워크 맨 위(마지막)에 놓인 dense layer를 포함할 지 안 할지 결정
        input_shape=(180, 180, 3)),  # summary()를 위해 input shape을 결정함
conv_base.summary()
'''
출력 결과
Model: "vgg16"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_5 (InputLayer)        [(None, 180, 180, 3)]     0         

 block1_conv1 (Conv2D)       (None, 180, 180, 64)      1792      

 block1_conv2 (Conv2D)       (None, 180, 180, 64)      36928     

 block1_pool (MaxPooling2D)  (None, 90, 90, 64)        0         

 block2_conv1 (Conv2D)       (None, 90, 90, 128)       73856     

 block2_conv2 (Conv2D)       (None, 90, 90, 128)       147584    

 block2_pool (MaxPooling2D)  (None, 45, 45, 128)       0         

 block3_conv1 (Conv2D)       (None, 45, 45, 256)       295168    

 block3_conv2 (Conv2D)       (None, 45, 45, 256)       590080    

 block3_conv3 (Conv2D)       (None, 45, 45, 256)       590080    

 block3_pool (MaxPooling2D)  (None, 22, 22, 256)       0         

 block4_conv1 (Conv2D)       (None, 22, 22, 512)       1180160   

 block4_conv2 (Conv2D)       (None, 22, 22, 512)       2359808   

 block4_conv3 (Conv2D)       (None, 22, 22, 512)       2359808   

 block4_pool (MaxPooling2D)  (None, 11, 11, 512)       0         

 block5_conv1 (Conv2D)       (None, 11, 11, 512)       2359808   

 block5_conv2 (Conv2D)       (None, 11, 11, 512)       2359808   

 block5_conv3 (Conv2D)       (None, 11, 11, 512)       2359808   

 block5_pool (MaxPooling2D)  (None, 5, 5, 512)         0         

=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________
'''

최종 특성 맵의 크기는 (5, 5, 512)입니다. 이 특성 위에 밀집 연결 층을 놓을 것입니다.

이 지점에서 두 가지 방식이 가능합니다.

  • 사로운 데이터셋에서 합성곱 기반 층을 실행하고 출력을 넘파이 배열로 디스크에 저장합니다. 그 다음 이 데이터를 이 책의 4장에서 보았던 것과 비슷한 독립된 밀집 연결 분류기에 입력으로 사용합니다. 합성곱 연산은 전체 과정 중 가장 비싼 부분입니다. 이 방식은 모든 입력 이미지에 대해 합성곱 기반 층을 한 번만 실행하면 되기 때문에 빠르고 비용이 적게 듭니다. 하지만 이런 이유 때문에 이 기법에는 데이터 증식을 사용할 수 없습니다.
  • 준비한 모델(conv_base) 위에 Dense 층을 쌓아 확장합니다. 그 다음 입력 데이터에서 엔드-투-엔드로 전체 모델을 실행합니다. 모델에 노출된 모든 입력 이미지가 매번 합성곱 기반 층을 통과하기 때문에 데이터 증식을 사용할 수 있습니다. 하지만 이런 이유로 이 방식은 첫 번째 방식보다 훨씬 비용이 많이 듭니다.

데이터 증식을 사용하지 않는 빠른 특성 추출

먼저 훈련, 검증, 테스트 데이터셋에서 conv_base 모델의 predict() 메서드를 호출하여 넘파이 배열로 추출한 특성을 저장하겠습니다.

코드 8-20 VGG16을 이용해 특성과 해당 label 추출하기

import numpy as np

def get_features_and_labels(dataset):
        all_features = []
        all_label = []
        for images, labeles in dataset:
                preprocessed_images = keras.applications.vgg16.preprocess_input(images)
                features = conv_base.predict(preprocessed_images)
                all_features.append(features)
                all_labels.append(labels)
        return np.concatenate(all_features), np.concatenate(all_labels)
train_features, train_labels =  get_features_and_labels(train_dataset)
val_features, val_labels =  get_features_and_labels(validation_dataset)
test_features, test_labels =  get_features_and_labels(test_dataset)
train_features.shape
'''
출력 결과
(2000, 5, 5, 512)
'''

중요한 점은 predict() 메서드가 레이블은 제외하고 이미지만 기대한다는 것입니다. 하지만 현재 데이터셋은 이미지와 레이블을 함께 담고 있는 배치를 반환합니다. 또한 VGG16 모델은 적절한 범위로 픽셀 값을 조정해 주는 keras.applications.vgg16.preprocess_input 함수로 전처리된 입력을 기대합니다.

추출된 특성의 크기는 (samples, 5, 5, 512)입니다.

이제 (규제를 위한 dropout 적용을 사용한) 밀집 연결 분류기를 정의하고 방금 저장한 데이터와 레이블에서 훌련할 수 있습니다.

inputs = keras.Input(shape=(5, 5, 512)
x = layers.Flatten()(inputs) # Dense 층에 특성을 주입하기 전에 Flatten 층을 사용합니다.
x = layers.Dense(256)(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activations="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(loss="binary_crossentropy",
                            optimizer="rmsprop",
                            metrics=["accuracy"])
callbacks = [
        keras.callbacks.ModelCheckpoint(
                filepath="feature_extraction.keras",
            save_best_only=True,
            monitor="val_loss")
]
history = model.fit(
    train_features, train_labels,
    epochs=20,
    validation_data=(val_features, val_labels),
    callbacks=callbacks
)

2개의 dense 층만 처리하면 되므로 훈련이 매우 빠릅니다. CPU를 사용하더라도 에포크에 걸리는 시간이 1초 미만입니다.

코드 8-22 결과를 그래프로 나타내기.

import matplotlib.pyplotas plt
acc= history.history["accuracy"]
val_acc= history.history["val_accuracy"]
loss= history.history["loss"]
val_loss= history.history["val_loss"]
epochs= range(1, len(acc)+ 1)
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
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()

데이터 증식을 사용한 특성 추출

이제 특성 추출을 위해 두 번째로 언급한 방법을 살펴보겠습니다. 이 방법은 훨씬 느리고 비용이 많이 들지만 훈련하는 동안 데이터 증식 기법을 사용할 수 있습니다. conv_base와 새로운 밀집 분류기를 연결한 모델을 만들고 입력 데이터를 사용하여 엔드-투-엔드로 실행합니다.

이렇게 하려면 먼저 합성곱 기반 층을 “동결(Freeze)” 해야합니다. 하나 이상의 층을 동결한다는 것은 훈련하는 동안 가중치 업데이트 되지 않도록 막는다는 뜻입니다. 이렇게 하지 않으면 합성곱 기반 층에 의해 사전에 학습된 표현이 훈련하는 동안 수정될 것입니다. 맨 위의 Dense 층은 랜덤하게 초기화 되었기 때문에 매우 큰 가중치 업ㄹ데이트 값이 네트워크에 전파될 것입니다. 이는 사전에 학습된 표현을 크게 훼손하게 됩니다.

케라스에서는 trainable 속성을 Fase로 설정하여 층이나 모델을 동결할 수 있습니다.

코드 8-23 VGG16 합성곱 기반 층을 만들고 동결하기

conv_base = keras.applications.vgg16.VGG16(
        weights="imagenet",
        include_top=False
)
**conv_base.trainable = False**

trainable 속성을 False로 지정하면 층이나 모델의 훈련 가능한 가중치 리스트가 텅 비게 됩니다.

conv_base.trainable = True
print("합성곱 기반 층을 동결하기 전의 훈련 가능한 가중치 개수:", len(conv_base.trainable_weights))
# 합성곱 기반 층을 동결하기 전의 훈련 가능한 가중치 개수: 26
conv_base.trainable = False
print("합성곱 기반 층을 동결한 후의 훈련 가능한 가중치 개수:", len(conv_base.trainable_weights))
# 합성곱 기반 층을 동결한 후의 훈련 가능한 가중치 개수: 0

이제 다음을 연결하여 새로운 모델을 만들 수 있습니다.

코드 8-25 데이터 증식 단계와 밀집 분류기를 합성곱 기반 층에 추가하기

data_augmentation = keras.Sequential( # augmentation process를 keras.Sequential API를 이용해 간단히 구현할 수 있다.
    [
        layers.RandomFlip("horizontal"),
        layers.RandomRotation(0.1),
        layers.RandomZoom(0.2),
    ]
)

inputs = keras.Input(shape=(180, 180, 3))
x = data_augmentation(inputs)
x = keras.applications.vgg16.preprocess_input(x)
x = conv_base(x)
x = layers.Flatten()(x)
x = layers.Dense(256)(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])

앞서 conv_base.trainable = False로 했기에 이렇게 설정하면 추가한 2개의 Dense 층 가중치만 훈련될 것입니다.

변경사항을 적용하려면 모델을 컴파일 해야합니다. 그렇지 않으면 변경 사항이 적용되지 않습니다.

callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="feature_extraction_with_data_augmentation.keras",
        save_best_only=True,
        monitor="val_loss")
]
history = model.fit(
    train_dataset,
    epochs=50,
    validation_data=validation_dataset,
    callbacks=callbacks)
import matplotlib.pyplot as plt
acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
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()

코드 8-26 테스트 세트에서 모델 평가하기

test_model = keras.models.load_model(
    "feature_extraction_with_data_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"테스트 정확도: {test_acc:.3f}")
'''
출력 결과
63/63 [==============================] - 8s 114ms/step - loss: 2.4575 - accuracy: 0.9765
테스트 정확도: 0.976
'''

3.2 사전 훈련된 모델 미세 조정하기

모델을 재사용하는 데 널리 사용되는 또 하나의 기법은 특성 추출을 보완하는 미세조정입니다. 미세 조정은 특성 추출에 사용했던 동결 모델의 상위 층 몇개를 동결에서 해제하고 모델에 새로 추가한 층(Dense layer 등)과 함께 훈련하는 것입니다. 주어진 문제에서 조금 더 밀접하게 재사용 모델의 표현을 일부 조정하기 때문에 미세 조정이라고 부릅니다.

앞서 랜덤하게 초기화 된 상단 분류기를 훈련하기 위해 VGG16의 합성곱 기반 층을 동결 해야한다고 말했습니다. 같은 이유로 맨 위에 있는 분류기가 훈련된 후 합성곱 기반의 상위 층을 미세 조정할 수 있습니다. 분류기가 미리 훈련되지 않으면 훈련되느 동안 너무 큰 오차 신호가 네트워크에 전파됩니ㅏㄷ. 이는 미세 조정될 층들이 사전에 학습한 표현들을 망가뜨리게 될 것입니다. 네트워크를 미세 조정하는 단계는 다음과 같습니다.

  1. 사전에 훈련된 기반 네트워크 위에 새로운 네트워크를 추가합니다.
  2. 기반 네트워크를 동결합니다.
  3. 새로 추가한 네트워크를 훈련합니다.
  4. 기반 네트워크에서 일부 층의 동결을 해제합니다(”배치 정규화(batch normalization)” 층은 동결 해제하면 안됩니다. 배치 정규화와 미세 조정에 대한 영향은 다음 장에서 설명하겠습니다.
  5. 동결 해제한 층과 새로 추가한 층을 함께 훈련합니다.

처음 세 단계는 pre-trained model을 특성 추출만으로 추론할 때 이미 완료 했습니다. 네 번째 단계를 진행 해보죠. conv_base의 동결을 해제하고 개별 층을 동결하겠습니다.

마지막 3개의 층을 동결 해제해서 미세 조정에 사용해보겠습니다.

왜 더 많은 층을 미세 조정하지 않을까요? 왜 전체 합성곱 기반 층을 미세 조정하지 않을까요? 그렇게 할 수 있지만 다음 사항을 고려해야합니다.

  • 합성곱 기반 층에 있는 하위 층들은 좀 더 일반적이고 재사용 가능한 특성들을 “인코딩”합니다. 반면 상위 층은 좀 더 특화된 특성을 인코딩 합니다. 새로운 문제에 재활용하도록 수정이 필요한 것은 구체적인 특성이므로 이들을 미세 조정하는 것이 유리합니다. 하위 층으로 갈수록 미세조정에 대한 효과가 감소합니다.
  • 훈련해야 할 파라미터가 많을 수록 과대적합의 위험이 커집니다. 합성곱 기반 층은 1500만개의 파라미터를 가지고 잇습니다. 작은 데이터셋으로 전부 훈련하려고하면 매우 위험합니다.

코드 8-27 마지막에서 네 번째 층까지 모든 층 동결하기

conv_base.trainable=True
for layer in conv_base.layers[:-4]:
        layer.trainable = False

이제 이 모델의 미세 조정을 시작하겠습니다. 학습률을 낮춘 RMSProp 옵티마이저를 사용합니다.

학습률을 낮추는 이유는 미세 조정하는 3개의 층에서 학습된 표현을 조금씩 수정하기 위해서입니다.

변경량이 너무 크면 학습된 표현에 나쁜 영향을 끼칠 수 있습니다.

코드 8-28 모델 미세 조정하기

model.compile(loss="binary_crossentropy",
              optimizer=keras.optimizers.RMSprop(**learning_rate=1e-5**),
              metrics=["accuracy"])

callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="fine_tuning.keras",
        save_best_only=True,
        monitor="val_loss")
]
history = model.fit(
    train_dataset,
    epochs=30,
    validation_data=validation_dataset,
    callbacks=callbacks)

4. 요약

  • 컨브넷은 컴퓨터 비전 작업에 가장 뛰어난 머신 러닝 모델입니다. 아주 작은 데이터셋에서도 처음부터 훈련해서 괜찮은 성능을 낼 수 있습니다.
  • 컨브넷은 시각적 세상을 표현하기 위한 패턴과 개념의 계층 구조를 학습합니다.
  • 작은 데이터셋에서는 과대적합이 항상 큰 문제입니다. 데이터 증식(Data augmentation)은 이미지 데이터를 다룰 때 과대적합을 막을 수 있는 강력한 방법입니다.
  • 특성 추출 방식으로 새로운 데이터셋에 기존 컨브넷을 쉽게 재사용할 수 있습니다. 작은 이미지 데이터셋으로 작업할 때 효과적인 방법입니다.
  • 특성 추출을 보완하기 위해 미세 조정을 사용할 수 있습니다. 미세 조정은 기존 모델에서 사전에 학습한 표현의 일부를 새로운 문제에 적응시킵니다. 이 기법은 조금 더 성능을 끌어올리는 전략입니다.