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

합성곱 신경망 소개

  • 이론적 배경을 알아보기 앞서 2장에서 완전 연결 네트워크로 풀었던 MNIST 숫자 이미지 분류에 컨브넷을 사용해 보자. 기본적인 컨브넷도 완전 연결 모델의 성능을 훨씬 앞지른다.
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32, (3,3), activation='relu', input_shape=(28, 28, 1)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
  • 컨브넷이 (image_height, image_width, image_channels) 크기의 입력 텐서를 사용한다는 점이 중요하다 (배치 차원은 포함하지 않는다.)
    • 이 예제에서는 MNIST 이미지 포맷인 (28, 28, 1) 크기의 입력을 처리하도록 컨브넷을 설정해야 한다. 이 때문에 첫 번째 층의 매개변수로 input_shape(28, 28, 1)을 전달했다.
  • 컨브넷의 구조를 출력해 보자.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
  • Conv2D와 MaxPooling2D 층의 출력은 (height, width, channels) 크기의 3D 텐서이다. 높이와 너비 차원은 네트워크가 깊어질수록 작아지는 경향이 있다. 채널의 수는 Conv2D 층에 전달된 첫 번째 매개변수에 의해 조절된다 (32개 또는 64개)
  • 다음 단계에서 마지막 층의 ((3, 3, 64) 크기인) 출력 텐서를 완전 연결 네트워크에 주입한다. 이 네트워크는 이미 익숙하게 보았던 Dense 층을 쌓은 분류기이다. 이 분류기는 1D 벡터를 처리하는데, 이전 층의 출력이 3D 텐서이다. 그래서 3D 출력을 1D 텐서로 펼쳐야 한다. 그 다음 몇 개의 Dense 층을 추가한다.
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
  • 10개의 클래스를 분류하기 위해 마지막 층의 출력 크기를 10으로 하고 소프트맥스 활성화 함수를 사용한다. 전체 네트워크는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 11, 11, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 5, 5, 64)          0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 3, 3, 64)          36928     
_________________________________________________________________
flatten_1 (Flatten)          (None, 576)               0         
_________________________________________________________________
dense_1 (Dense)              (None, 64)                36928     
_________________________________________________________________
dense_2 (Dense)              (None, 10)                650       
=================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
  • 여기에서 볼 수 있듯이 (3, 3, 64) 출력이 (576,) 크기의 벡터로 펼쳐진 후 Dense 층으로 주입되었다.
  • 이제 MNIST 숫자 이미지에 이 컨브넷을 훈련하자.
from keras.datasets import mnist
from keras.utils import to_categorical

(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

train_labels = to_categorical(train_labels)
test_labels = to_categorical(test_labels)

model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)
  • 테스트 데이터에서 모델을 평가해 보자.
test_loss, test_acc = model.evaluate(test_images, test_labels)
test_acc
---
0.9927999973297119
  • 2장의 완전 연결 네트워크는 97.8%의 테스트 정확도를 얻은 반면, 기본적인 컨브넷은 99.2%의 테스트 정확도를 얻었다.
  • 완전 연결된 모델보다 왜 간단한 컨브넷이 더 잘 작동할까? 이에 대해 알아보기 위해 Conv2D와 MaxPooling2D 층이 어떤 일을 하는지 살펴보자.

합성곱 연산

  • 완전 연결 층과 합성곱 층 사이의 근본적인 차이는 다음과 같다.
    • Dense 층은 입력 특성 공간에 있는 전역 패턴(예컨대 MNIST 숫자 이미지에서는 모든 픽셀에 걸친 패턴)을 학습하지만 합성곱 층은 지역 패턴을 학습한다.
    • 이미지일 경우 작은 2D 윈도우로 입력에서 패턴을 찾는다. 앞선 예에서 윈도우는 모두 3×3 크기였다.

  • 이 핵심 특징은 컨브넷에 두 가지 흥미로운 성질을 제공한다.
    • 학습된 패턴은 평행 이동 불변성(translation invariant)을 갖는다. 
      • 컨브넷이 이미지의 오른쪽 아래 모서리에서 어떤 패턴을 학습했다면 다른 곳에서도 이 패턴을 학습할 수 있다. 완전 연결 네트워크는 새로운 위치에 나타난 것을 새로운 패턴으로 학습해야 한다.
      • 이런 성질은 컨브넷이 이미지를 효율적으로 처리하게 만들어진다. (근본적으로 우리가 보는 세상은 평행 이동으로 인해 다르게 인식되지 않는다) 적은 수의 훈련 샘플을 사용해서 일반화 능력을 가진 표현을 학습할 수 있다.
      • (회전은 안 되는 것 같다)
    • 컨브넷은 패턴의 공간적 계층 구조를 학습할 수 있다.
      • 첫 번째 합성곱 층이 에지 같은 작은 지역 패턴을 학습한다. 두 번째 합성곱 층은 첫 번째 층의 특성으로 구성되는 더 큰 패턴을 학습하는 식이다.
      • 이런 방식을 이용하여 컨브넷은 매우 복잡하고 추상적인 시각적 개념을 효과적으로 학습할 수 있다. (근본적으로 우리가 보는 세상은 공간적 계층 구조를 가진다)
  • 합성곱 연산은 특성 맵(feature map)이라고 부르는 3D 텐서에 적용된다. 이 텐서는 2개의 공간축(높이와 너비)과 깊이 축(채널 축이라고도 한다)으로 구성된다.
    • RGB 이미지는 3개의 컬러 채널을 가지므로 깊이 축의 차원이 3이 되고, MNIST 숫자처럼 흑백이미지는 깊이 축의 차원이 1이다.
    • 합성곱 연산은 입력 특성 맵에서 작은 패치들을 추출하고 이런 모든 패치에 같은 변환을 적용하여 출력 특성 맵(output feature amp)을 만든다.

  • 출력 특성 맵도 높이와 너비를 가진 3D 텐서이다. 출력 텐서의 깊이는 층의 매개변수로 결정되기 때문에 상황에 따라 다르다.
    • 이렇게 되면 깊이 축의 채널은 더 이상 RGB 입력처럼 특정 컬러를 의미하지 않는다. 그 대신 일종의 필터(filter)를 의미한다.
    • 필터는 입력 데이터의 어떤 특성을 인코딩한다. 예컨대 고수준으로 보면 하나의 필터가 ‘입력에 얼굴이 있는지’를 인코딩 할 수 있다.
  • MNIST 예제에서 첫 번째 합성곱 층이 (28, 28, 1) 크기의 특성 맵을 입력으로 받아 (26, 26, 32) 크기의 특성 맵을 출력한다. 즉 입력에 대해 32개의 필터를 적용한다.
    • 32개의 출력 채널 각각은 26×26 크기의 배열 값을 가진다. 이 값은 입력에 대한 필터의 응답 맵(response map)이다. 입력의 각 위치에서 필터 패턴에 대한 응답을 나타낸다.
    • 특성 맵이란 말이 의미하는 것은 다음과 같다. 깊이 축에 있는 각 차원은 하나의 특성(또는 필터)이고, 2D 텐서 output[:, :, n]은 입력에 대한 이 필터 응답을 나타내는 2D 공간상의 맵이다.

  • 합성곱은 핵심적인 2개의 파라미터로 정의된다.
    • 입력으로부터 뽑아낼 패치의 크기: 전형적으로 3×3 또는 5×5 크기를 사용한다. 이 예에서는 3×3을 사용했다.
    • 특성 맵의 출력 깊이: 합성곱으로 계산할 필터의 수이다. 이 예에서는 깊이 32로 시작해서 깊이 64로 끝났다.
  • 케라스의 Conv2D 층에서 이 파라미터는 Conv2D(output_depth, (window_height, window_width)) 처럼 첫 번째와 두 번째 매개변수로 전달된다.
  • 3D 입력 특성 맵 위를 3×3 또는 5×5 크기의 윈도우가 슬라이딩(sliding)하면서 모든 위치에서 3D 특성 패치(window_height, window_width, input_depth) 크기)를 추출하는 방식으로 합성곱이 작동한다.
    • 이런 3D 패치는 (output_depth, ) 크기의 1D 벡터로 변환된다. (합성곱 커널 (convolution kernel)이라고 불리는 하나의 학습된 가중치 행렬과의 텐서 곱셈을 통하여 변환된다.)
    • 변환된 모든 벡터는 (height, width, output_depth) 크기의 3D 특성 맵으로 재구성된다. 
    • 출력 특성 맵의 공간상 위치는 입력 특성 맵의 같은 위치에 대응된다. (예컨대 출력의 오른쪽 아래 모서리는 입력의 오른쪽 아래 부근에 해당하는 정보를 담고 있다)
    • 3×3 윈도우를 사용하면 3D 패치 input[i-1:i+2, j-1:j+2, :]로부터 벡터 output[i, j, :]가 만들어진다.
    • 아래 그림에 전체 과정이 표현되어 있다.
      • (입력의 깊이는 2차원인데, 출력의 깊이는 3차원이다)

  • 출력 높이와 너비는 입력의 높이, 너비와 다를 수 있는데, 여기에는 두 가지 이유가 있다.
    • 경계 문제, 입력 특성 맵에 패딩을 추가하여 대응할 수 있다.
    • 잠시 후에 설명할 스트라이드(stride)의 사용 여부에 따라 다르다.

경계 문제와 패딩 이해하기

  • 5×5 크기의 특성 맵을 생각해 보자. (총 25개의 타일이 있다고 가정)
    • 3×3 크기인 윈도우의 중앙을 맞출 수 있는 타일은 3×3 격차를 형성하는 9개 뿐이다. 따라서 출력 특성 맵은 3×3 크기가 된다.
    • 크기가 조금 줄어 들었다. 여기에서 높이와 너비 차원을 따라 정확히 2개의 타일이 줄어들었다.
    • 앞선 예에서도 이런 경계 문제를 볼 수 있는데, 첫 번째 합성곱 층에서 28×28 크기의 입력이 26×26 크기가 되었다.

  • 입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶다면 패딩(padding)을 사용할 수 있다.
    • 패딩은 입력 특성 맵의 가장자리에 적절한 개수의 행과 열을 추가한다. 그래서 모든 입력 타일에 합성곱 윈도우의 중앙을 위치시킬 수 있다.
    • 3×3 윈도우라면 위아래에 하나의 행을 추가하고 오른쪽, 왼쪽에 하나의 열을 추가한다. 5×5 윈도우라면 2개의 행과 열을 추가한다.

  • Conv2D 층에서 패딩은 padding 매개변수로 설정할 수 있다. 2개의 값이 가능한데, “valid”는 패딩을 사용하지 않는다는 뜻이고, “same”은 입력과 동일한 높이와 너비를 가진 출력을 만들기 위해 패딩한다는 뜻이다. 기본값은 “valid”이다.

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

  • 출력 크기에 영향을 미치는 다른 요소는 스트라이드이다.
    • 지금까지 합성곱에 대한 설명은 합성곱 윈도우의 중앙 타일이 연속적으로 지나간다고 가정한 것이다.
    • 두 번의 연속적인 윈도우 사이의 거리가 스트라이드라고 불리는 합성곱의 파라미터이다. 스트라이드의 기본값은 1이다.
    • 스트라이드가 1보다 큰 스트라이드 합성곱도 가능하다. 아래의 그림은 5×5 크기의 입력에 스트라이드 2를 사용한 3×3 크기의 윈도우로 합성곱하여 추출한 패치를 나타낸다.

  • 스트라이드를 2를 사용했다는 것은 특성 맵의 너비와 높이가 2의 배수로 다운샘플링되었다는 뜻이다. (경계 문제가 있다면 더 줄어든다)
    • 스트라이드 합성곱은 실전에서는 드물게 사용된다. 하지만 어떤 모델에서는 유용하게 사용될 수 있으므로 잘 알아둘 필요가 있다.
  • 특성 맵을 다운샘플링하기 위해 스트라이드 대신 첫 번째 컨브넷 예제에 사용된 최대 풀링(max pooling) 연산을 사용하는 경우가 많으므로 알아보자.

최대 풀링 연산

  • 앞선 컨브넷 예제에서 특성 맵의 크기가 MaxPooling2D 층마다 절반으로 줄어들었다.
    • 예컨대 MaxPooling2D 층 이전에 특성 맵의 크기는 26×26이었는데, 최대 풀링 연산으로 13×13으로 줄어들었다.
    • 스트라이드 합성곱과 매우 비슷하게 강제적으로 특성 맵을 다운샘플링하는 것이 최대 풀링의 역할이다.
  • 최대 풀링은 입력 특성 맵에서 윈도우에 맞는 패치를 추출하고 각 채널별로 최댓값을 출력한다.
    • 합성곱과 개념적으로 비슷하지만 추출한 패치에 학습된 선형 변환(합성곱 커널)을 적용하는 대신 하드코딩된 최댓값 추출 연산을 사용한다.
    • 합성곱과 가장 큰 차이점은 최대 풀링은 보통 2×2 윈도우와 스트라이드 2를 사용하여 특성 맵을 절반 크기로 다운샘플링한다는 것이다.
    • 이에 반해 합성곱은 전형적으로 3×3 윈도우와 스트라이드 1을 사용한다.
  • 왜 이런 식으로 특성 맵을 다운샘플링할까? 왜 최대 풀링 층을 빼고 큰 특성 맵을 계속 유지하지 않을까? 이런 방식을 테스트해 보자. 합성곱만으로 이루어진 모델은 다음과 같다.
model_no_max_pool = models.Sequential()
model_no_max_pool.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
model_no_max_pool.add(layers.Conv2D(64, (3, 3), activation='relu'))
  • 모델의 구조는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 24, 24, 64)        18496     
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 22, 22, 64)        36928     
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
  • 이 설정에서 무엇이 문제일까? 두 가지가 있다.
    • 특성의 공간적 계층 구조를 학습하는데 도움이 되지 않는다. 세 번째 층의 3×3 윈도우는 초기 입력의 7×7 윈도우 영역에 대한 정보만 담고 있다. 컨브넷에 의해 학습된 고수준 패턴은 초기 입력에 관한 정보가 아주 적어 숫자 분류를 학습하기에 충분하지 않을 것이다(7×7 픽셀 크기의 창으로 숫자를 보고 분류해 보라) 마지막 학성곱 층의 특성이 전체 입력에 대한 정보를 가지고 있어야 한다.
    • 최종 특성 맵은 22 x 22 x 64 = 30,976개의 가중치를 가지는데, 아주 많다. 이 컨브넷을 펼친 후 512 크기의 Dense 층과 연결한다면 약 15.8백만 개의 가중치 파라미터가 생긴다. 작은 모델치고 너무 많은 가중치고, 심각한 과대적합이 발생할 것이다.
  • 간단히 말해서 다운샘플링을 사용하는 이유는 처리할 특성 맵의 가중치 개수를 줄이기 위해서다. 또 연속적인 합성곱 층이 (원본 입력에서 커버된느 영역 측면에서) 점점 커진 윈도우를 통해 바라보도록 만들어 필터의 공간적인 계층 구조를 구성한다.
  • 최대 풀링이 다운샘플링을 할 수 있는 유일한 방법은 아니다. 이미 알고 있듯 앞선 합성곱 층에서 스트라이드를 사용할 수 있다.
    • 최댓값을 취하는 최대 풀링 대신 입력 패치의 채널별 평균값을 계산하여 변환하는 평균 풀링(average pooling)을 사용할 수도 있다.
    • 하지만 최대 풀링이 다른 방법들보다 더 잘 작동하는 편이다. 그 이유는 특성이 특성 맵의 각 타일에서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문이다. (그래서 특성의 지도이다)
    • 따라서 특성의 평균값보다 여러 특성 중 최댓값을 사용하는 것이 더 유용하다. 가장 납득할 만한 서브샘플링(subsampling) 전략은 먼저 (스트라이드가 없는 합성곱으로) 조밀한 특성 맵을 만들고 그 다음 작은 패치에 대해 최대로 활성화된 특성을 고르는 것이다.
    • 이런 방법이 입력에 대해 (스트라이드 합성곱으로) 듬성듬성 윈도우를 슬라이드하거나 입력 패치를 평균해서 특성 정보를 놓치거나 희석시키는 것보다 낫다.

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

  • 적은 데이터를 이용하여 이미지 분류 모델을 훈련하는 일은 흔한 경우이다. 보통 적은 샘플이란 수백 개에서 수만 개 사이를 의미한다.
    • 실용적인 예제로 4,000개의 강아지와 고양이 사진으로 구성된 데이터셋에서 강아지와 고양이 이미지를 분류해 보자. 2,000개의 사진을 훈련 데이터로, 검증과 테스트 데이터로 1,000개씩 사용하겠다.
  • 문제를 해결하기 위해 기본적인 전략 하나를 살펴보자. 보유한 소규모 데이터셋을 사용하여 처음부터 새로운 모델을 훈련하는 것이다.
    • 2,000개의 훈련 샘플에서 작은 컨브넷을 어떤 규제 방법도 사용하지 않고 훈련하여 기준이 되는 기본 성능을 만들겠다. 이 방법은 71%의 분류 정확도를 달성할 것이다. 이 방법의 주요 이슈는 과대적합이 될 것이다.
    • 그 다음 컴퓨터 비전에서 과대적합을 줄이기 위한 강력한 방법인 데이터 증식(data augmentation)을 소개하겠다. 데이터 증식을 통해 네트워크의 성능을 82%로 향상시킬 수 있다.
  • 다음 절에서 작은 데이터셋에 딥러닝을 적용하기 위한 핵심적인 기술 두 가지를 살펴보겠다.
    • 사전 훈련된 네트워크로 특성을 추출하는 것(90%의 정확도를 얻게 된다)과 사전 훈련된 네트워크를 세밀하게 튜닝하는 것(최종 모델은 92% 정확도를 얻게 될 것이다). 
    • 이 세 가지 전략 (처음부터 작은 모델을 훈련하기, 사전 훈련도니 모델을 사용하여 특성 추출하기, 사전 훈련된 모델을 세밀하게 튜닝하기)은 작은 데이터셋에서 이미지 분류 문제를 수행할 때 여러분의 도구 상자에 포함되어 있어야 한다.

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

  • 딥러닝은 데이터가 풍부할 때만 작동한다는 말을 듣는데 부분적으로만 맞는 이야기다.
    • 딥러닝의 근본적인 특징은 훈련 데이터에서 특성 공학의 수작업 없이 흥미로운 특성을 찾을 수 있는 것이다. 이는 훈련 샘플이 많아야만 가능하다. 입력 샘플이 이미지처럼 매우 고차원인 문제에서는 특히 그렇다.
  • 하지만 많은 샘플이 의미하는 것은 상대적이다. 우선 훈련하려는 네트워크의 크기와 깊이에 상대적이다. 
    • 복잡한 문제를 푸는 컨브넷을 수십 개의 샘플만 사용해서 훈련하는 것은 불가능하다. 하지만 모델이 작고 규제가 잘 되어 있으며 간단한 작업이라면 수백 개의 샘플로도 충분할 수 있다.
    • 컨브넷은 지역적이고 평행 이동으로 변하지 않는 특성을 학습하기 때문에 지각에 관한 문제에서 매우 효율적으로 데이터를 사용한다.
    • 매우 작은 데이터셋에서 어떤 종류의 특성 공학을 사용하지 않고 컨브넷을 처음부터 훈련해도 납득할만한 결과를 만들 수 있다.
  • 거기에 더해 딥러닝 모델은 태생적으로 매우 다목적이다. 말하자면 대규모 데이터셋에서 훈련시킨 이미지 분류 모델이나 스피치-투-텍스트(speech-to-text) 모델을 조금만 변경해서 완전히 다른 문제에 재사용할 수 있다.
    • 특히 컴퓨터 비전에서는 (보통 ImageNet 데이터셋에서 훈련 된) 사전 훈련된 모델들이 내려받을 수 있도록 많이 공개되어 있어서 매우 적은 데이터에서 강력한 비전 모델을 만드는데 사용할 수 있다.

데이터 내려받기

  • 여기서 사용할 강아지, 고양이 데이터셋은 케라스에 포함되어 있지 않고 캐글에 있기 때문에 캐글 계정이 필요하다. 다음 주소에서 데이터를 내려 받자.

  • 내려 받고 압축 해제 한 원본 데이터 파일을 다음 코드를 이용해서 분리한다.
import os, shutil

original_dataset_dir = './datasets/cats_and_dogs/train/' #원본 데이터셋 경로
base_dir = '.datasets/cats_and_dogs/cats_and_dogs_small/'
os.mkdir(base_dir)

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

os.mkdir(train_dir)
os.mkdir(validation_dir)
os.mkdir(test_dir)

train_cats_dir = os.path.join(train_dir, 'cats')
train_dogs_dir = os.path.join(train_dir, 'dogs')

validation_cats_dir = os.path.join(validation_dir, 'cats')
validation_dogs_dir = os.path.join(validation_dir, 'dogs')

test_cats_dir = os.path.join(test_dir, 'cats')
test_dogs_dir = os.path.join(test_dir, 'dogs')

os.mkdir(train_cats_dir)
os.mkdir(train_dogs_dir)
os.mkdir(validation_cats_dir)
os.mkdir(validation_dogs_dir)
os.mkdir(test_cats_dir)
os.mkdir(test_dogs_dir)

for fname in ['cat.{}.jpg'.format(i) for i in range(1000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_cats_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(train_dogs_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(validation_dogs_dir, fname)
shutil.copyfile(src, dst)

for fname in ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]:
src = os.path.join(original_dataset_dir, fname)
dst = os.path.join(test_dogs_dir, fname)
shutil.copyfile(src, dst)

네트워크 구성하기

  • 이전보다 이미지가 크고 복잡하기 때문에 네트워크를 좀 더 만들겠다. Conv2D + MaxPooling2D 단계를 하나 더 추가한다.
    • 이렇게 하면 네트워크의 용량을 늘리고 Flatten 층의 크기가 너무 커지지 않도록 특성 맵의 크기를 줄일 수 있다.
    • 150×150 크기의 입력으로 싲가해서 Flatten 층 이전에 7×7 크기의 특성 맵으로 줄어든다.
  • 이진 분류 문제이므로 네트워크는 하나의 유닛(크기가 1인 Dense 층)과 sigmoid 활성화 함수로 끝난다. 이 유닛은 한 클래스에 대한 확률을 인코딩할 것이다.
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 층들을 거치면서 특성 맵의 차원이 어떻게 변하는지 살펴보자
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_5 (Conv2D)            (None, 148, 148, 32)      896       
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 74, 74, 32)        0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 72, 72, 64)        18496     
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 36, 36, 64)        0         
_________________________________________________________________
conv2d_7 (Conv2D)            (None, 34, 34, 128)       73856     
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 17, 17, 128)       0         
_________________________________________________________________
conv2d_8 (Conv2D)            (None, 15, 15, 128)       147584    
_________________________________________________________________
max_pooling2d_8 (MaxPooling2 (None, 7, 7, 128)         0         
_________________________________________________________________
flatten_2 (Flatten)          (None, 6272)              0         
_________________________________________________________________
dense_2 (Dense)              (None, 512)               3211776   
_________________________________________________________________
dense_3 (Dense)              (None, 1)                 513       
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
  • 컴파일 단계에서 이전과 같이 RMSprop 옵티마이저를 선택하겠다. 네트워크의 마지막이 하나의 시그모이드 유닛이기 때문에 이진 크로스엔트로피를 손실로 사용한다.
from keras import optimizers

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

데이터 전처리

  • 데이터는 네트워크에 주입되기 전에 부동 소수 타입의 텐서로 전처리 되어 있어야 한다. 지금은 데이터가 JPEG 파일로 되어 있으므로 네트워크에 주입하려면 다음 과정을 따라야 한다.
    1. 사진 파일을 읽는다.
    2. JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩한다.
    3. 그 다음 부동 소수 타입의 텐서로 변환한다.
    4. 픽셀 값(0-255)의 스케일을 [0, 1] 사이로 조정한다.
  • 케라스에는 이런 단계를 자동으로 처리하는 유틸리티가 있다. 또 케라스에는 keras.preprocessing.image에 이미지 처리를 위한 헬퍼 도구들도 있다. 특히 ImageDataGenerator는 디스크에 있는 이미지 파일을 전처리된 배치 텐서로 자동으로 바꾸어 주는 파이썬 제너레이터를 만들어 준다.
from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=20, class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(150,150), batch_size=20, class_mode='binary')
  • 이 제너레이터의 출력은 150×150 RGB 이미지의 배치((20, 150, 150, 3) 크기)와 이진 레이블의 배치 ((20, ) 크기)이다. 각 배치에는 20개의 샘플(배치 크기)이 있다. 
    • 제너레이터는 이 배치를 무한정 만들어낸다. 타깃 폴더에 있는 이미지를 끝없이 반복하기 때문. 따라서 반복 루프 안 어디에선가 break 문을 사용해야 한다.
  • 제너레이터를 사용한 데이터에 모델을 훈련시켜 보겠다.
    • fit_generator 메서는 fit 메서드와 동일하되 데이터 제너레이터를 사용할 수 있다. 이 메서드는 첫 번째 매개변수로 입력과 타깃의 배치를 끝없이 반환하는 파이썬 제너레이터를 기대한다.
    • 데이터가 끝없이 생성되기 때문에 케라스 모델에 하나의 에포크를 정의하기 위해 제너레이터로부터 얼마나 많은 샘플을 뽑을 것인지 알려주어야 한다. steps_per_epoch 매개변수에서 이를 설정한다.
    • steps_per_epoch개의 배치만큼 뽑은 후, 즉 steps_per_epoch 횟수만큼 경사 하강법 단계를 실행한 후에 훈련 프로세스는 다음 에포크로 넘어간다.
    • 여기서는 20개의 샘플이 하나의 배치이므로 2,000개의 샘플을 모두 처리할 때까지 100개의 배치를 뽑을 것이다.
  • fit_generator를 사용할 때 fit 메서드와 마찬가지로 validation_data 매개변수를 전달할 수 있다. 이 매개변수에는 데이터 제너레이터도 가능하지만 넘파이 배열의 튜플도 가능하다.
    • validation_data로 제너레이터를 전달하면 검증 데이터의 배치를 끝없이 반환하기 때문에 검증 데이터 제너레이터에서 얼마나 많은 배치를 추출하여 평가할지 validation_steps 매개변수에 지정해야 한다.
history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=30, validation_data=validation_generator, validation_steps=50)
  • 훈련이 끝나면 항상 모델을 저장하는 습관을 갖자
model.save('cats_and_dogs_samll_1.h5')
  • 훈련 데이터와 검증 데이터에 대한 모델의 손실과 정확도를 그래프로 나타내보자
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(1, len(acc) + 1)

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()

  • 이 그래프는 과대적합의 특성을 보여준다.
    • 훈련 정확도가 시간이 지남에 따라 선형적으로 증가해서 거의 100%에 도달하는 반면 검증 정확도는 70-72% 에서 멈추었다.
    • 검증 손실은 다섯 번의 에포크만에 최솟값에 다다른 후 더 진전되지 않았고 훈련 손실은 거의 0에 도달할 때까지 선형적으로 계속 감소한다.
  • 비교적 훈련 샘플의 수(2,000개)가 적기 때문에 과대적합이 가장 중요한 문제이다.
    • 드롭아웃이나 가중치 감소(L2 규제)처럼 과대적합을 감소시킬 수 있는 여러 기법들을 배웠다. 여기서는 컴퓨터 비전에 특화되어 있어서 딥러닝으로 이미지를 다룰 때 일반적으로 사용되는 새로운 방법인 데이터 증식을 시도해 보겠다.

데이터 증식 사용하기

  • 과대적합은 학습할 샘플이 너무 적어 새로운 데이터에 일반화할 수 있는 모델을 훈련시킬 수 없기 때문에 발생한다. 무한히 많은 데이터가 주어지면 데이터 분포의 모든 가능한 측면을 모델이 학습할 수 있을 것이다.
  • 데이터 증식은 기존 훈련 샘플로부터 더 많은 훈련 데이터를 생성하는 방법으로 이 방법은 그럴듯한 이미지를 생성하도록 여러 가지 랜덤한 변환을 적용하여 샘플을 늘린다.
    • 훈련할 때 모델이 정확히 같은 데이터를 두 번 만나지 않도록 하는 것이 목표이다. 모델이 데이터의 여러 측면을 학습하면 일반화에 도움이 될 것이다.
  • 케라스에서는 ImageDataGenerator가 읽은 이미지에 여러 종류의 랜덤 변환을 적용하도록 설정할 수 있다.
datagen = ImageDataGenerator(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')
  • 추가적인 매개변수가 몇 가 더 있다. 이 코드를 살펴보면 다음과 같다.
    • rotation_range는 랜덤하게 사진을 회전시킬 각도 범위이다. (0-180 사이)
    • width_shift_range와 height_shift_range는 사진을 수평과 수직으로 랜덤하게 평행 이동 시킬 범위이다.
    • shear_range는 랜덤하게 전단 변환(shearing transformation)을 적용할 각도 범위이다.
    • zoom_range는 랜덤하게 사진을 확대할 범위이다.
    • horizontal_flip은 랜덤하게 이미지를 수평으로 뒤집는다. 수평 대칭을 가정할 수 있을 때 사용한다. (풍경/ 인물 사진 등)
    • fill_mode는 회전이나 가로/세로 이동으로 인해 새롭게 생성해야 할 픽셀을 채울 전략이다.
  • 증식된 이미지 샘플은 아래 그림과 같다.

  • 데이터 증식을 사용하여 새로운 네트워크를 훈련시킬 때 네트워크에 같은 입력 데이터가 두 번 주입되지 않는다. 하지만 적은 수의 원본 이미지에서 만들어졌기 때문에 여전히 입력 데이터들 사이에 상호 연관성이 크다.
    • 즉 새로운 정보를 만들어 낼 수 없고 단지 기존 정보의 재조합만 가능하다. 그렇기 때문에 완전히 과대적합을 제거하기에 충분하지 않을 수 있다.
    • 과대적합을 더 억제하기 위해 완전 연결 분류기 직전에 Dropout 층을 추가하겠다.
model = models.Sequential()
model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(150, 150, 3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128, (3, 3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])
  • 데이터 증식과 드롭아웃을 사용하여 이 네트워크를 훈련시켜 보자.
train_datagen = ImageDataGenerator(rescale=1./255, rotation_range=40, width_shift_range=0.2, height_shift_range=0.2, shear_range=0.2, zoom_range=0.2, horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=32, class_mode='binary')
validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(150,150), batch_size=32, class_mode='binary')

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)
  • 모델을 저장하자.
model.save('cats_and_dogs_samll_2.h5')
  • 결과 그래프를 다시 그려보자. 데이터 증식과 드롭아웃 덕분에 더는 과대적합되지 않는다. 훈련 곡선이 검증 곡선에 가깝꼐 따라가고 있다.
    • 검증 데이터에서 82% 정확도를 달성했는데, 규제하지 않은 모델과 비교했을 때 15% 정도 향상되었다.

  • 다른 규제 기법을 더 사용하고 네트워크의 파라미터를 튜닝하면 86%나 87% 정도까지 더 높은 정확도를 얻을 수 있다.
    • 하지만 데이터가 적기 때문에 컨브넷을 처음부터 훈련해서 더 높은 정확도를 달성하기는 어렵다.
    • 이런 상황에서 정확도를 높이기 위한 다음 단계는 사전 훈련된 모델을 사용하는 것이다.

사전 훈련된 컨브넷 사용하기

  • 작은 이미지 데이터셋에 딥러닝을 적용하는 일반적이고 매우 효과적인 방법은 사전 훈련된 네트워크를 사용하는 것이다.
    • 사전 훈련된 네트워크(pretrained network)는 일반적으로 대규모 이미지 분류 문제를 위해 대량의 데이터셋에서 미리 훈련되어 저장된 네트워크이다.
    • 원본 데이터셋이 충분히 크고 일반적이라면 사전 훈련된 네트워크에 의해 학습된 특성의 계층 구조는 실제 세상에 대한 일반적인 모델로 효율적인 역할을 할 수 있다. 새로운 문제가 원래 작업과 완전히 다른 클래스에 대한 것이라도 이런 특성은 많은 컴퓨터 비전 문제에 유용하다.
    • 예컨대 대부분 동물이나 생활 용품으로 이루어진 ImageNet 데이터셋에 네트워크를 훈련한다. 그 다음 이 네트워크를 이미지에서 가구 아이템을 식별하는 것 같은 다른 용도로 사용할 수 있다.
    • 학습된 특성을 다른 문제에 적용할 수 있는 이런 유연성은 이전의 많은 얕은 학습 방법과 비교했을 때 딥러닝의 핵심 장점이다. 이런 방식으로 작은 데이터셋을 가진 문제에도 딥러닝이 효율적으로 작동할 수 있다.
  • 여기서는 ImageNet 데이터셋에서 훈련된 대규모 컨브넷을 사용해 보겠다. 캐런 시몬연(Karen Simonyan)과 앤드류 지서먼(Andrew Zisserman)이 2014년 개발한 VGG16 구조를 사용하겠다.
    • VGG16은 조금 오래되었고 최고 수준의 성능에는 못 미치며 최근의 다른 모델보다는 조금 무겁다. 하지만 이 모델의 구조기 이전에 보았던 것과 비슷해서 선택했다.
    • 아마 VGG가 처음 보는 모델 애칭일지 모르겠으나 이런 이름에는 VGG, ResNet, Inception, Inception_ResNet, Xception 등이 있다. 컴퓨터 비전을 위해 딥러닝을 계속 공부하다 보면 이런 이름을 자주 만나게 될 것이다.
  • 사전 훈련된 네트워크를 사용하는 두 가지 방법이 있다. 특성 추출(feature extraction)과 미세 조정(fine tuning)이다.

특성 추출

  • 특성 추출은 사전에 학습된 네트워크의 표현을 사용하여 새로운 샘플에서 흥미로운 특성을 뽑아내는 것이다. 이런 특성을 사용하여 새로운 분류기를 처음부터 훈련한다.
  • 앞서 보앗듯이 컨브넷은 이미지 분류를 위해 두 부분으로 구성된다. 먼저 연속된 합성곱과 풀링 층으로 시작해서 완전 연결 분류기로 끝난다.
    • 첫 번째 부분을 모델의 합성곱 기반 층(convolutional base)이라 부르겠다.
    • 컨브넷의 경우 특성 추출은 사전에 훈련된 네트워크의 합성곱 기반 층을 선택하여 새로운 데이터를 통과시키고, 그 출력으로 새로운 분류기를 훈련한다.

  • 왜 합성곱 층만 재사용할까? 완전 연결 분류기도 재사용할 수 있을까?
    • 일반적으로 권장하지 않는데, 합성곱 층에 의해 학습된 표현이 더 일반적이어서 재사용이 가능하기 때문이다.
    • 컨브넷의 특성 맵은 사진에 대한 일반적인 콘셉트의 존재 여부를 기록한 맵이다. 주어진 컴퓨터 비전 문제에 상관없이 유용하게 사용할 수 있다.
    • 하지만 분류기에서 학습한 표현은 모델이 훈련된 클래스 집합에 특화되어 있다. 분류기는 전체 사진에 어떤 클래스가 존재할 확률에 관한 정보만 담고 있다.
    • 더군다나 완전 연결 층에서 찾은 표현은 더 이상 입력 이미지에 있는 객체의 위치 정보를 가지고 있지 않다. 완전 연결 층들은 공간 개념을 제거하지만 합성곱의 특성맵은 객체 위치를 고려한다. 객체 위치가 중요한 문제라면 완전 연결 층에서 만든 특성느 크게 쓸모가 없다.
  • 특정 합성곱 층에서 추출한 표현의 일반성(그리고 재사용성) 수준은 모델에 있는 층의 깊이에 달려 있다.
    • 모델의 하위 층은(에지, 색깔, 질감 등) 지역적이고 매우 일반적인 특성 맵을 추출한다. 반면 상위 층은 (강아지 눈, 고양이 귀 처럼) 좀 더 추상적인 개념을 추출한다.
    • 새로운 데이터셋이 원본 모델이 훈련한 데이터셋과 많이 다르다면 전체 합성곱 기반 층을 사용하는 것보다는 모델의 하위 층 몇 개만 특성 추출에 사용하는 것이 좋다.
  • ImageNet의 클래스 집합에는 여러 종류의 강아지와 고양이를 포함하고 있다. 이런 경우 원본 모델의 완전 연결 층에 있는 정보를 재사용하는 것이 도움이 될 것이다.
    • 하지만 새로운 문제의 클래스가 원본 모델의 클래스 집합과 겹치지 않는 좀 더 일반적인 경우를 다루기 위해 여기서는 완전 연결 층을 사용하지 않겠다.
    • ImageNet 데이터셋에 훈련된 VGG16 네트워크의 합성곱 기반 층을 사용하여 강아지와 고양이 이미지에 유용한 특성을 추출해 보겠다. 그런 다음 이 특성으로 강아지 vs 고양이 분류기를 훈련한다.
  • VGG16 모델은 케라스 패키지로 포함되어 있다. keras.applications 모듈에서 임포트 할 수 있는데, keras.applications 모듈에서 사용 가능한 이미지 분류 모델은 다음과 같다. (모두 ImageNet 데이터셋에서 훈련되었다.)
    • Xception
    • Inception V3
    • ResNet50
    • VGG16
    • VGG19
    • MobileNet
  • VGG16 모델을 만들어 보자.
from keras.application import VGG16

conv_base = VGG16(weights='imagenet', include_top=False, input_shape=(150, 150, 3))
  • VGG 함수에 3개의 매개 변수를 전달한다.
    • weights는 모델을 초기화할 가중치 체크포인트를 지정한다.
    • include_top은 네트워크의 최상위 완전 연결 분류기를 포함할지 안 할지 지정한다. 기본값은 ImageNet의 클래스 1000개에 대응되는 완전 연결 분류기를 포함한다. 별도의 완전 연결층을 추가하려 하므로 이를 포함시키지 않는다.
    • input_shape은 네트워크에 주입할 이미지 텐서의 크기이다. 이 매개변수는 선택사항인데, 이 값을 지정하지 않으면 네트워크가 어떤 크기의 입력도 처리할 수 있다.
  • 다음은 VGG16 합성곱 기반 층의 자세한 구조이다.
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 150, 150, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 150, 150, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 150, 150, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 75, 75, 64)        0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 75, 75, 128)       73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 75, 75, 128)       147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 37, 37, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 37, 37, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 37, 37, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 18, 18, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 18, 18, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 18, 18, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 9, 9, 512)         0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 9, 9, 512)         2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 4, 4, 512)         0         
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
  • 최종 특성 맵의 크기는 (4, 4, 512)이다. 이 특성 위에 완전 연결 층을 놓을 것이다.) 이 지점에서 두 가지 새로운 방식이 가능하다.
    • 새로운 데이터셋에서 합성곱 기반 층을 실행하고 출력을 넘파이 배열로 디스크에 저장한다. 그 다음 이 데이터를 독립된 완전 연결 분류기에 입력으로 사용한다. 합성곱 연산은 전체 과정 중에서 가장 비싼 부분이다. 이 방식은 모든 입력 이미지에 대해 합성곱 기반 층을 한 번만 실행하면 되기 때문에 빠르고 비용이 적게 든다. 하지만 이런 이유로 이 기법에는 데이터 증식을 사용할 수 없다.
    • 준비한 모델(conv_base) 위에 Dense 층을 쌓아 확장한다. 그 다음 입력 데이터에서 엔드-투-엔드로 전체 모델을 실행한다. 모델에 노출된 모든 입력 이미지가 매번 합성곱 기반층을 통과하기 때문에 데이터 증식을 사용할 수 있다. 하지만 이런 이유로 첫 번째 방식보다 훨씬 많은 비용이 든다.

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

  • ImageDataGenerator를 이용해서 이미지와 레이블을 넘파이 배열로 추출하자. conv_base 모델의 predict 메서드를 호출하여 이 이미지에서 특성을 추출한다.
import os
import numpy as np
from keras.preprocessing.image import ImageDataGenerator

base_dir = 'E:/Study/Keras Deep Learning/datasets/cats_and_dogs/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(train_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))
  • 2개의 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(1, len(acc) + 1)

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%의 검증 정확도에 도달했다. 이전 절에서 처음부터 훈련시킨 작은 모델에서 얻은 것보다 훨씬 좋다. 
    • 하지만 이 그래프는 많은 비율로 드롭아웃을 사용했음에도 훈련을 시작하면서 거의 바로 과대적합되고 있다는 것을 보여준다. 작은 이미지 데이터셋에서는 과대적합을 막기 위해 필수적인 데이터 증식을 사용하지 않았기 때문이다.

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

  • 이 방법은 훨씬 느리고 비용이 많이 들지만 훈련하는 동안 데이터 증식 기법을 사용할 수 있다. conv_base 모델을 확장하고 입력 데이터를 사용하여 엔드-투-엔드로 실행한다.
  • 모델은 층과 동일하게 작동하므로 층을 추가하듯이 Sequential 모델에 다른 모델을 추가할 수 있다.
from keras import models
from keras import layers

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))
  • 이 모델의 구조는 다음과 같다.
Layer (type)                 Output Shape              Param #   
=================================================================
vgg16 (Model)                (None, 4, 4, 512)         14714688  
_________________________________________________________________
flatten_1 (Flatten)          (None, 8192)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 256)               2097408   
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 257       
=================================================================
Total params: 16,812,353
Trainable params: 16,812,353
Non-trainable params: 0
  • 여기서 볼 수 있듯이 VGG16의 합성곱 기반 층은 14,714,688개의 매우 많은 파라미터를 갖고 있다. 합성곱 기반 층 위에 추가한 분류기는 200만개의 파라미터를 가진다.
  • 모델을 컴파일 하고 훈련하기 전에는 합성곱 기반 층을 동결하는 것이 아주 중요하다. 하나 이상의 층을 동결(freezing) 한다는 것은 훈련하는 동안 가중치가 업데이트되지 않도록 막는다는 뜻이다.
    • 이렇게 하지 않으면 합성곱 기반 층에 의해 사전에 학습된 표현이 훈련하는 동안 수정될 것이다.
    • 맨 위의 Dense 층은 랜덤하게 초기화되었기 때문에 매우 큰 가중치 업데이트 값이 네트워크에 전파될 것이다. 이는 사전에 학습된 표현을 크게 훼손하게 된다.
  • 케라스에서는 trainable 속성을 False로 설정하여 네트워크를 동결할 수 있다.
print('conv_base를 동결하기 전 훈련되는 가중치의 수: ', len(model.trainable_weights))
conv_base.trainable = False
print('conv_base를 동결한 후 훈련되는 가중치의 수: ', len(model.trainable_weights))
---
conv_base를 동결하기 전 훈련되는 가중치의 수: 30 conv_base를 동결한 후 훈련되는 가중치의 수: 4
  • 이렇게 설정하면 추가한 2개의 Dense 층 가중치만 훈련될 것이다. 층마다 2개씩 총 4개의 텐서가 훈련된다.
    • 변경 사항을 적용하려면 먼저 모델을 컴파일 해야 한다. 컴파일 단계 후에 trainable 속성을 변경하면 반드시 모델을 다시 컴파일해야 한다. 그렇지 않으면 변경 사항이 적용되지 않는다.
  • 앞서 사용했던 데이터 증식을 사용하여 모델 훈련을 시작할 수 있다.
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers

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

test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_dir, target_size=(150,150), batch_size=20, 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)
  • 결과 그래프는 다음과 같다. 검증 정확도가 이전과 비슷하지만 처음부터 훈련시킨 소규모 컨브넷보다 과대적합이 줄었다.

미세 조정

  • 미세 조정(fine tuning)은 특성 추출에 사용했던 동결 모델의 상위 층 몇 개를 동결에서 해제하고 모델에 새로 추가한 층(여기서는 완전 연결 분류기)과 함께 훈련하는 것이다.
    • 주어진 문제에 조금 더 밀접하게 재사용 모델의 표현을 일부 조정하기 때문에 미세 조정이라고 한다.

  • 앞서 랜덤하게 초기화된 상단 분류기를 훈련하기 위해 VGG16의 합성곱 기반 층을 동결해야 한다고 했다. 같은 이유로 맨 위에 있는 분류기가 훈련된 후에 합성곱 기반의 상위 층을 미세 조정할 수 있다.
    • 분류기가 미리 훈련되지 않으면 훈련되는 동안 너무 큰 오차 신호가 네트워크에 전파된다. 이는 미세 조정될 층들이 사전에 학습한 표현을 망가뜨리게 될 것이다.
  • 미세 조정하는 단계는 다음과 같다.
    • 사전에 훈련된 기반 네트워크 위에 새로운 네트워크를 추가한다.
    • 기반 네트워크를 동결한다.
    • 새로 추가한 네트워크를 훈련한다.
    • 기반 네트워크에서 일부 층의 동결을 해제한다.
    • 동결을 해제한 층과 새로 추가한 층을 함께 훈련한다.
  • 처음 세 단계는 특성 추출을 할 때 이미 완료했다. 네 번째 단계를 진행해 보자. conv_base의 동결을 해제하고 개별 층을 동결하겠다.
    • 앞서 보았던 합성곱 기반 층의 구조를 다시 확인해 보자.
Layer (type) Output Shape Param # ================================================================= 
input_1 (InputLayer) (None, 150, 150, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 150, 150, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 150, 150, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 75, 75, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 75, 75, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 75, 75, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 37, 37, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 37, 37, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 37, 37, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 37, 37, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 18, 18, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 9, 9, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 4, 4, 512) 0 =================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
  • 마지막 3개의 합성곱 층을 미세 조정하겠다. 즉 block4_pool까지 모든 층은 동결되고 block5_conv1, block5_conv2, block5_conv3 층은 학습 대상이 된다.
  • 왜 더 많은 층을 미세 조정하지 않을까? 그렇게 할 수도 있지만 다음 사항을 고려해야 한다.
    • 합성곱 기반 층에 있는 하위 층들을 좀 더 일반적이고 재사용 가능한 특성들을 인코딩한다. 반면 상위 층은 좀 더 특화된 특성을 인코딩 한다. 새로운 문제에 재활용하도록 수정이 필요한 것은 구체적인 특성이므로 이들을 미세 조정하는 것이 유리하다. 하위 층으로 갈수록 미세 조정에 대한 효과가 감소한다.
    • 훈련해야 할 파라미터가 많을수록 과대적합의 위험이 커진다. 합성곱 기반 층은 1,500만개의 파라미터를 가지고 있으므로 작은 데이터셋으로 전부 훈련하려고 하면 매우 위험하다.
  • 앞선 예제 코드에 이어 미세 조정을 해보자.
conv_base.trainable = True

set_trainable = False

for layer in conv_base.layers:
if layer.name == 'block5_conv1':
set_trainable = True
if set_trainable:
layer.trainable = True
else:
layer.trainable = False
  • 이제 네트워크의 미세 조저을 시작해보자. 학습률을 낮춘 RMSProp 옵티마이저를 사용한다. 학습률을 낮추는 이유는 미세 조정하는 3개의 층에서 학습된 표현을 조금씩 수정하기 위해서다. 변경량이 너무 크면 학습된 표현에 나쁜 영향을 끼칠 수 있다.
model.compile(loss='binary_crossentropy', optimizer=optimizers.RMSprop(lr=1e-5), metrics=['acc'])

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)
  • 이전과 동일한 코드로 결과를 그려보면 다음과 같다.

  • 그래프가 불규칙하게 보이는데, 그래프를 보기 쉽게 하기 위해 지수 이동 평균(exponential moving averages)으로 정확도와 손실 값을 부드럽게 표현할 수 있다.
def smooth_curve(points, factor=0.8):
smoothed_points = []
for point in points:
if smoothed_points:
previous = smoothed_points[-1]
smoothed_points.append(previous * factor + point * (1-factor))
else:
smoothed_points.append(point)
return smoothed_points

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

plt.figure()

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

plt.show()

  • 검증 정확도 곡선이 훨씬 깨끗하게 보인다. 정확도가 대략 1% 이상 향상되었다.
  • 손실 곡선은 실제 어떤 향상을 얻지 못했다(사실 악화되었다). 손실이 감소되지 않았는데 어떻게 정확도가 안정되거나 향상될 수 있었을까?
    • 답은 간단한데, 그래프는 개별적인 손실 값의 평균을 그린 것이다. 하지만 정확도에 영향을 미치는 것은 손실 값의 분포이지 평균이 아닌다.
    • 정확도는 모델이 예측한 클래스 확률이 어떤 임계 값을 넘었는지에 대한 결과이기 때문이다. 모델이 더 향상되더라도 평균 손실에 반영되지 않을 수 있다.
  • 마지막으로 테스트 데이터에서 이 모델을 평가해보자.
test_generator = test_datagen.flow_from_directory(test_dir, target_size=(150, 150), batch_size=20, class_mode='binary')

test_loss, test_acc = model.evaludate_generator(test_generator, steps=50)
print('test acc:', test_acc)
  • 92%의 테스트 정확도를 얻을 것이다. 이 데이터셋을 사용한 원래 캐글 경연 대회에서 꽤 높은 결과이다. 하지만 최신 딥러닝 기법으로 훈련 데이터의 일부분(약 10%)만 사용해서 이런 결과를 달성했다.

정리

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

컨브넷 학습 시각화

  • 딥러닝 모델을 ‘블랙 박스(black box)’ 같다고 자주 이야기하는데, 학습된 표현에서 사람이 이해하기 쉬운 형태를 뽑아내거나 제시하기 어렵기 때문이다.
    • 일부 딥러닝 모델에서는 이 말이 어느 정도 맞지만 컨브넷에서는 전혀 아니다. 컨브넷에서는 시각적인 개념을 학습한 것이기 때문에 시각화하기 아주 좋다.
  • 컨브넷의 표현을 시각화하고 해석하는 기법들은 다음과 같다.
    • 컨브넷 중간층의 출력(중간층에 있는 활성화)을 시각화하기: 연속된 컨브넷 층이 입력을 어떻게 변형시키는지 이해하고 개별적인 컨브넷 필터의 의미를 파악하는데 도움이 된다.
    • 컨브넷 필터를 시각화하기: 컨브넷의 필터가 찾으려는 시각적인 패턴과 개념이 무엇인지 상세하게 이해하는데 도움이 된다.
    • 클래스 활성화에 대한 히트맵(heatmap)을 이미지에 시각화하기: 이미지의 어느 부분이 주어진 클래스에 속하는데 기여했는지 이해하고 이미지에서 객체 위치를 추정(localization)하는데 도움이 된다.

중간층의 활성화 시각화하기

  • 중간층의 활성화 시각화는 어떤 입력이 주어졌을 때 네트워크에 있는 여러 합성곱과 풀링 층이 출력하는 특성 맵을 그리는 것이다. (층의 출력이 활성화 함수의 출력이라서 종종 활성화(activation)라고 부른다.)
    • 이 방법은 네트워크에 의해 학습된 필터들이 어떻게 입력을 분해하는지 보여준다.
    • 너비, 높이, 깊이(채널) 3개의 차원에 대해 특성 맵을 시각화하는 것이 좋다.
    • 각 채널은 비교적 독립적인 특성을 인코딩하므로 특성 맵의 각 채널 내용을 독립적인 2D 이미지로 그리는 것이 괜찮은 방법이다.
  • 앞서 만들었던 모델을 로드해서 시작해 보자.
from keras.models import load_model

model = load_model('cats_and_dogs_small_2.h5')
model.summary()

------
Layer (type) Output Shape Param # ================================================================= conv2d_9 (Conv2D) (None, 148, 148, 32) 896 _________________________________________________________________ max_pooling2d_9 (MaxPooling2 (None, 74, 74, 32) 0 _________________________________________________________________ conv2d_10 (Conv2D) (None, 72, 72, 64) 18496 _________________________________________________________________ max_pooling2d_10 (MaxPooling (None, 36, 36, 64) 0 _________________________________________________________________ conv2d_11 (Conv2D) (None, 34, 34, 128) 73856 _________________________________________________________________ max_pooling2d_11 (MaxPooling (None, 17, 17, 128) 0 _________________________________________________________________ conv2d_12 (Conv2D) (None, 15, 15, 128) 147584 _________________________________________________________________ max_pooling2d_12 (MaxPooling (None, 7, 7, 128) 0 _________________________________________________________________ flatten_3 (Flatten) (None, 6272) 0 _________________________________________________________________ dropout_1 (Dropout) (None, 6272) 0 _________________________________________________________________ dense_5 (Dense) (None, 512) 3211776 _________________________________________________________________ dense_6 (Dense) (None, 1) 513 ================================================================= Total params: 3,453,121 Trainable params: 3,453,121 Non-trainable params: 0
  • 그 다음 네트워크를 훈련할 때 사용했던 이미지에 포함되지 않은 고양이 사진 하나를 입력 이미지로 선택한다.
img_path = '.datasets/cats_and_dogs/cats_and_dogs_small/test/cats/cat.1700.jpg'

from keras.preprocessing import image
import numpy as np

img = image.load_img(img_path, target_size=(150,150))
img_tensor = image.img_to_array(img)
img_tensor = np.expand_dims(img_tensor, axis=0)
img_tensor /= 255.

print(img_tensor.shape)
  • 사진을 출력해 보자
import matplotlib.pyplot as plt

plt.imshow(img_tensor[0])
plt.show()

  • 확인하고 싶은 특성 맵을 추출하기 위해 이미지 배치를 입력으로 받아 모든 합성곱과 출링 층의 활성화를 출력하는 케라스 모델을 만들자. 
    • 이를 위해 케라스의 Model 클래스를 사용하겠다. 모델 객체를 만들 때 2개의 매개변수가 필요한데, 입력 텐서(또는 입력 텐서의 리스트)와 출력 텐서(또는 출력 텐서의 리스트)이다.
    • 반환되는 객체는 Sequential과 같은 케라스 모델이지만 특정 입력과 특정 출력을 매핑한다.
    • Model 클래스를 사용하면 Sequential과는 달리 여러 개의 출력을 가진 모델을 만들 수 있다.
from keras import models

layer_outputs = [layer.output for layer in model.layers[:8]] --상위 8개 층의 출력을 추출한다.
activation_model = models.Model(inputs=model.input, outputs=layer_outputs) --입력에 대해 8개 층의 출력을 반환하는 모델을 만든다.
  • 입력 이미지가 주입될 때 이 모델은 원본 모델의 활성화 값을 반환한다. 이 모델이 이 책에서는 처음 나오는 다층 출력 모델이다.
    • 지금까지 본 모델은 정확히 하나의 입력과 하나의 출력만을 가졌는는데, 일반적으로 모델은 몇 개의 입력과 출력이라도 가질 수 있다.
    • 이 모델은 입력과 층의 활성화마다 하나씩 총 8개의 출력을 가진다.
activations = activation_model.predict(img_tensor)  --층의 활성화마다 하나씩 8개의 넘파이 배열로 이루어진 리스트를 반환한다.
  • 예컨대 다음이 고양이 이미지에 대한 첫 번째 합성곱 층의 활성화 값이다.
first_layer_activation = activations[0]
print(first_layer_activation.shape)
----
(1, 148, 148, 32)
  • 32개의 채널을 가진 148×148 크기의 특성 맵이다. 원본 모델의 첫 번째 활성화 중에서 20번째 채널을 그려보자.
import matplotlib.pyplot as plt

plt.matshow(first_layer_activation[0, :, :, 19], cmap='viridis')

  • 이 채널은 대각선 에지를 감지하도록 인코딩 된 것 같다.
    • 합성곱 층이 학습한 필터는 결정적이지 않기 때문에 채널 이미지가 책과 다를 수 있다.
    • (실제 내가 돌려본 이미지는 전혀 다름)
  • 16번째 채널을 그려보자.

  • (마찬가지로 내가 돌려본 결과는 전혀 다르다)
  • 이제 네트워크의 모든 활성화를 시각화해 보자.
    • 8개의 활성화 맵에서 추출한 모든 채널을 그리기 위해 하나의 큰 이미지 텐서에 추출한 결과를 나란히 쌓겠다.
layer_names = []
for layer in model.layers[:8]:
layer_names.append(layer.name)

images_per_row = 16

for layer_name, layer_activation in zip(layer_names, activations):
n_features = layer_activation.shape[-1]
size = layer_activation.shape[1]
n_cols = n_features // images_per_row
display_grid = np.zeros((size*n_cols, images_per_row*size))

for col in range(n_cols):
for row in range(images_per_row):
channel_image = layer_activation[0, :, :, col * images_per_row + row]
channel_image -= channel_image.mean()
channel_image /= channel_image.std()
channel_image *= 64
channel_image += 128
channel_image = np.clip(channel_image, 0, 255).astype('uint8')
display_grid[col*size : (col+1) * size, row*size : (row+1)*size] = channel_image

scale = 1. / size
plt.figure(figsize=(scale * display_grid.shape[1], scale * display_grid.shape[0]))
plt.title(layer_name)
plt.grid(False)
plt.imshow(display_grid, aspect='auto', cmap='viridis')

plt.show()

 

  • 몇 가지 주목해야 할 내용이 있다.
    • 첫 번째 층은 여러 종류의 에지 감지기를 모아 놓은 것 같다. 이 단계의 활성화에는 초기 사진에 있는 거의 모든 정보가 유지된다.
    • 상위 층으로 갈수록 활성화는 점점 더 추상적으로 되고 시각적으로 이해하기 어려워진다. ‘고양이 귀’, ‘고양이 눈’ 처럼 고수준 개념을 인코딩하기 시작한다. 상위 층의 표현은 이미지의 시각적 콘텐츠에 관한 정보가 점점 줄어들고 이미지의 클래스에 관한 정보가 점점 증가한다.
    • 비어 있는 활성화가 층이 깊어짐에 따라 늘어난다. 첫 번째 층에서는 모든 필터가 입력 이미지에 활성화 되었지만 층을 올라가면서 활성화되지 않은 필터들이 생긴다. 필터에 인코딩된 패턴이 입력 이미지에 나타나지 않았다는 것을 의미한다.
  • 심층 신경망이 학습한 표현에서 일반적으로 나타나는 중요한 특징을 확인했다. 층에서 추출한 특성은 층의 깊이를 따라 점점 더 추상적이 된다.
    • 높은 층의 활성화는 특정 입력에 관한 시각적 정보가 점점 더 줄어들고 타깃에 관한 정보(이 경우에는 강아지 또는 고양이 이미지의 클래스)가 점점 더 증가한다.
    • 심층 신경망은 입력되는 원본 데이터(여기서는 RGB 포맷의 사진)에 대한 정보 정제 파이프라인처럼 작동한다.
    • 반복적인 변환을 통해 관계없는 정보(예컨대 이미지에 있는 특정 요소)를 걸러내고 유용한 정보는 강조되고 개선된다(여기서는 이미지의 클래스)
  • 사람과 동물이 세상을 인지하는 방식이 이와 비슷하다.
    • 사람은 몇 초 동안 한 장면을 보고 난 후 그 안에 있었던 추상적인 물체(자전거, 나무)를 기억할 수 있다. 하지만 물체의 구체적인 모양은 기억하지 못한다.
    • 사실 기억을 더듬어 일반적인 자전거를 그려보면 조금이라도 비슷하게 그릴 수 없다.
    • 우리 뇌는 시각적 입력에서 관련성이 적은 요소를 필터링하여 고수준 개념으로 변환한다. 이렇게 완전히 추상적으로 학습하기 때문에 눈으로 본 것을 자세히 기억하기는 어렵다.
    • (결국 인간도 특징점을 찾아 추상화 하여 기억한다는 뜻. 사실 세상의 정보는 무한한데, 그 정보를 다루려면 그런 식으로 압축해서 다루는게 맞다. 이래서 CNN이 이미지 분석을 잘 하는 듯)

컨브넷 필터 시각화하기

  • 컨브넷이 학습한 필터를 조사하는 또 다른 방법은 각 필터가 반응하는 시각적 패턴을 그려 보는 것이다.
    • 빈 입력 이미지에서 시작해서 특정 필터의 응답을 최대화하기 위해 컨브넷 입력 이미지에 경사 상승법을 적용한다.
    • 결과적으로 입력 이미지는 선택된 필터가 최대로 응답하는 이미지가 될 것이다.
  • 전체 과정은 간단하다. 특정 합성곱 츠으이 한 필터 값을 최대화하는 손실 함수를 정의한다.
    • 이 활성화 값을 최대화하기 위해 입력 이미지를 변경하도록 확률적 경사 상승법을 사용한다.
    • 예컨대 여기에서는 ImageNet에 사전 훈련된 VGG16 네트워크에서 block3_conv1 층 필터 0번의 활성화를 손실로 정의한다.
from keras.applications import VGG16
from keras import backend as K

model = VGG16(weights='imagenet', include_top=False)

layer_name = 'block3_conv1'
filter_index = 0

layer_output = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])
  • 경사 상승법을 구현하기 위해 모델의 입력에 대한 손실의 그래디언트가 필요하다. 이를 위해 케라스의 backend 모듈에 있는 gradients 함수를 사용하겠다.
grads = K.gradients(loss, model.input)[0]
  • 경사 상승법 과정을 부드럽게 하기 위해 사용하는 한 가지 기법은 그래디언트 텐서를 L2 노름(텐서에 있는 값을 제곱한 합의 제곱근)으로 나누어 나누어 정규화하는 것이다. 이렇게 하면 입력 이미지에 적용할 수정량의 크기를 항상 일정 범위 안에 놓을 수 있다.
grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)
  • 이제 주어진 입력 이미지에 대한 손실 텐서와 그래디언트 텐서를 계산해야 한다. 케라스 백엔드 함수를 사용하여 처리하겠다.
    • iterate는 넘파이 텐서(크기가 1인 텐서의 리스트)를 입력으로 받아 손실과 그래디언트 2개의 넘파이 텐서를 반환한다.
iterate = K.function([model.input], [loss, grads])

import numpy as np
loss_value, grads_value = iterate([np.zeros((1, 150, 150, 3))])
  • 여기에서 파이썬 루프를 만들어 확률적 경사 상승법을 구성한다.
input_img_data = np.random.random((1, 150, 150, 3)) * 20 + 128. --잡음이 섞인 회색 이미지로 시작한다.

step = 1.

# 경사 상승법을 40회 실행
for i in range(40):
loss_value, grads_value = iterate([input_img_data]) --손실과 그래디언트를 계산
input_img_data += grads_value * step --손실을 최대화하는 방향으로 입력 이미지를 수정
  • 결과 이미지 텐서는 (1, 150, 150, 3) 크기의 부동 소수 텐서이다. 이 텐서 값은 [0, 255] 사이의 정수가 아니다. 따라서 출력 가능한 이미지로 변경하기 위해 후처리를 할 필요가 있다. 
    • 이를 위해 간단한 함수를 정의하여 사용하자.
def deprocess_image(x):
# 텐서의 평균이 0, 표준편차가 0.1이 되도록 정규화
x -= x.mean()
x /= (x.std() + 1e-5)
x *= 0.1

# [0, 1]로 클리핑
x += 0.5
x = np.clip(x, 0, 1)

# RGB 배열로 변환
x *= 255
x = np.clip(x, 0, 255).astype('uint8')

return x
  • 이제 모든 코드가 준비되었다. 이 코드를 모아서 층의 이름과 필터 번호를 입력으로 받는 함수를 만들겠다. 이 함수는 필터 활성화를 최대화하는 패턴을 이미지 텐서로 출력한다.
def generate_pattern(layer_name, filter_index, size=150):
layer_ouput = model.get_layer(layer_name).output
loss = K.mean(layer_output[:, :, :, filter_index])

grads = K.gradients(loss, model.input)[0]

grads /= (K.sqrt(K.mean(K.square(grads))) + 1e-5)

iterate = K.function([model.input], [loss, grads])

input_img_data = np.random.random((1, size, size, 3)) * 20 + 128.

step = 1.
for i in range(40):
loss_value, grads_value = iterate([input_img_data])
input_img_data += grads_value * step

img = input_img_data[0]
return deprecss_image(img)
  • 이 함수를 실행한 결과는 다음과 같다.
import matplotlib.pyplot as plt

plt.imshow(generate_pattern('block3_conv1', 0))

  • block3_conv1 층의 필터 0은 물방울 패턴에 반응하는 것 같다. 이제 재미있는 것을 만들어 보자. 모든 층에 있는 필터를 시각화해 보겠다.
    • 간단히 만들기 위해 각 층에서 처음 64개의 필터만 사용하겠다.
    • 또 각 합성곱 블록의 첫 번째 층만 살펴보겠다(block1_conv1, block2_conv1, block3_conv1, block4_conv1, block5_conv1)
    • 여기서 얻은 출력을 64×64 필터 패턴의 8×8 그리드로 정렬한다. 각 필터 패턴 사이에 검은색 마진을 둔다.
layer_name = 'block1_conv1'
size = 64
margin = 5

results = np.zeros((8*size + 7*margin, 8*size + 7*margin, 3), dtype='uint8')

for i in range(8):
for j in range(8):
filter_img = generate_pattern(layer_name, i + (j*8), size=size)
horizontal_start = i * size + i * margin
horizontal_end = horizontal_start + size
vertical_start = j * size + j * margin
vertical_end = vertical_start + size
results[horizontal_start: horizontal_end, vertical_start:vertical_end, :] = filter_img

plt.figure(figsize=(20,20))
plt.imshow(results)

  • 이런 필터 시각화를 통해 컨브넷 츠잉 바라보는 방식을 이해할 수 있다. 컨브넷의 각 층은 필터의 조합으로 입력을 표현할 수 있는 일련의 필터를 학습한다.
  • 이는 푸리에 변환(Fourier transform)을 사용하여 신호를 일련의 코사인 함수로 분해할 수 있는 것과 비슷하다. 이 컨브넷 필터들은 모델의 상위 층으로 갈 수록 점점 더 복잡해지고 개선된다.
    • 모델에 있는 첫 번째 층(block1_conv1)의 필터는 간단한 대각선 방향의 에지와 색깔(또는 어떤 색깔이 있는 에지)을 인코딩한다.
    • block2_conv1의 필터는 에지나 색깔의 조합으로 만들어진 간단한 질감을 인코딩한다.
    • 더 상위 층의 필터는 깃털, 눈, 나뭇잎 등 자연적인 이미지에서 찾을 수 있는 질감을 닮아가기 시작한다.

클래스 활성화의 히트맵 시각화하기

  • 한 가지 시각화 기법을 더 소개하겠다. 이 방법은 이미지의 어느 부분이 컨브넷의 최종 분류 결정에 기여하는지 이해하는데 유용하다.
    • 분류에 실수가 있는 경우 컨브넷의 결정 과정을 디버깅하는데 도움이 된다. 또 이미지에 특정 물체가 있는 위치를 파악하는데 사용할 수도 있다.
  • 이 기법의 종류를 일반적으로 클래스 활성화 맵(Class Activation Map, CAM) 시각화라고 한다.
    • 입력 이미지에 대한 클래스 활성화의 히트맵을 만든다. 클래스 활성화 히트맵은 특정 출력 클래스에 대해 입력 이미지의 모든 위치를 계산한 2D 점수 그리드이다.
    • 클래스에 대해 각 위치가 얼마나 중요한지 알려준다. 예컨대 강아지 vs 고양이 컨브넷에 한 이미지를 주입하면 CAM 시각화는 고양이 클래스에 대한 히트맵을 생성하여 이미지에서 고양이와 비슷한 부분을 알려준다.
  • 여기서 사용할 구체적인 구현은 “Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization”에 기술되어 있는 것이다.
    • 방법은 간단하다. 입력 이미지가 주어지면 합성곱 층에 있는 특성 맵의 출력을 추출한다.
    • 그 다음 특성 맵의 모든 채널 출력에 채널에 대한 클래스의 그래디언트 평균을 곱한다.
    • 이 기법을 직관적으로 이해하는 방법은 다음과 같다. ‘입력 이미지가 각 채널을 활성화하는 정도’에 대한 공간적인 맵을 ‘클래스에 대한 각 채널의 중요도’로 가중치를 부여하여 ‘입력 이미지가 클래스를 활성화하는 정도’에 대한 공간적인 맵을 만드는 것이다.
  • 사전 훈련된 VGG16 네트워크를 다시 사용하여 시연해 보겠다.
from keras.applications.vgg16 import VGG16

model = VGG16(weights='imagenet')
  • 다음 그림의 초원을 걷는 어미와 새끼 아프리카 코끼리의 이미지를 적용해 보겠다. 이 이미지를 VGG16 모델이 인식할 수 있도록 변환하다.
    • 이 모델은 224×224 크기의 이미지에서 훈련되었고 keras.applications.vgg16.preprocess_input 함수에 있는 몇 가지 규칙에 따라 전처리 되었다.

from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input, decode_predictions
import numpy as np

img_path = '.\datasets\creative_commons_elephant.jpg'

img = image.load_img(img_path, target_size=(224, 224))

x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
  • 이제 이 이미지에서 사전 훈련된 네트워크를 실행하고 예측 벡터를 이해하기 쉽게 디코딩한다.
preds = model.predict(x)
print('Predicted:', decode_predictions(preds, top=3)[0])
---
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/imagenet_class_index.json 40960/35363 [==================================] - 0s 0us/step Predicted: [('n02504458', 'African_elephant', 0.90942144), ('n01871265', 'tusker', 0.08618243), ('n02504013', 'Indian_elephant', 0.0043545766)]
  • 이 이미지에 대한 상위 3개의 예측 클래스는 다음과 같다.
    • 아프리카 코끼리 90.9% 
    • 코끼리(tusker) 8.6%
    • 인도 코끼리 0.4%
    • (책에 있는 결과와 조금 다름)
  • 네트워크는 이 이미지가 아프리카 코끼리를 담고 있다고 인식했다. 예측 벡터에서 최대로 활성화된 항목은 ‘아프리카 코끼리’클래스에 대한 것으로 386번 인덱스이다.
np.argmax(preds[0])
---
386
  • 이 이미지에서 가장 아프리카 코끼리와 같은 부위를 시각화 하기 위해 Grad-CAM 처리 과정을 구현해 보자.
from keras import backend as K
import matplotlib.pyplot as plt

# 예측 벡터의 '아프리카 코끼리' 항목
african_elephant_output = model.output[:, 386]

# VGG16의 마지막 합성곱 층인 block5_conv3 층의 특성맵
last_conv_layer = model.get_layer('block5_conv3')

# block5_conv3의 특성 맵 출력에 대한 '아프리카 코끼리' 클래스의 그래디언트
grads = K.gradients(african_elephant_output, last_conv_layer.output)[0]

# 특성 맵 채널별 그래디언트 평균값이 담긴 (512, ) 크기의 벡터
pooled_grads = K.mean(grads, axis=(0, 1, 2))

# 샘플 이미지가 주어졌을 때 방금 전 정의한 pooled_grads와 block5_conv3의 특성맵 출력을 구한다.
iterate = K.function([model.input], [pooled_grads, last_conv_layer.output[0]])

# 두 마리 코끼리가 있는 샘플 이미지를 주입하고 2개의 넘파이 배열을 얻는다.
pooled_grads_value, conv_layer_output_value = iterate([x])

# '아프리카 코끼리' 클래스에 대한 '채널의 중요도'를 특성 맵 배열의 채널에 곱한다.
for i in range(512):
conv_layer_output_value[:, :, i] *= pooled_grads_value[i]

# 만들어진 특성 맵에서 채널 축을 따라 평균한 값이 클래스 활성화의 히트맵이다.
heatmap = np.mean(conv_layer_output_value, axis=-1)
heatmap = np.maximum(heatmap, 0)
heatmap /= np.max(heatmap)

plt.matshow(heatmap)

  • 마지막으로 OpenCV를 사용하여 히트맵에 원본 이미지를 겹친 이미지를 만들어보자.
import opencv

img = cv2.imread(img_path)

heatmap = cv2.resize(heatmap, (img.shape[1], img.shape[0]))
heatmap = np.uint8(255 * heatmap)
heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)

superimposed_img = heatmap * 0.4 + img

cv2.imwrite('.\datasets\elephant_cam.jpg', superimposed_img)

  • 이 시각화 기법은 2개의 중요한 질문에 대한 답을 준다.
    • 왜 네트워크가 이 이미지에 아프리카 코끼리가 있다고 생각하는가?
    • 아프리카 코끼리 사진은 어디 있는가?
  • 코끼리 새끼의 귀가 강하게 활성화 된 점이 흥미로운데, 아마도 이것이 네트워크가 아프리카 코끼리와 인도 코끼리의 차이를 구분하는 방법일 것이다.

요약

  • 컨브넷은 시각적인 분류 문제를 다루는데 최상의 도구이다.
  • 컨브넷은 우리가 보는 세상을 표현하기 위한 패턴의 계층 구조와 개념을 학습한다.
  • 학습된 표현은 쉽게 분석할 수 있다. 컨브넷은 블랙 박스가 아니다.
  • 이미지 분류 문제를 풀기 위해 자신만의 컨브넷을 처음부터 훈련시킬 수 있다.
  • 과대적합을 줄이기 위해 데이터 증식하는 방법을 배웠다.
  • 사전 훈련된 컨브넷을 사용하여 특성 추출과 미세 조정하는 방법을 배웠다.
  • 클래스 활성화 히트맵을 포함하여 컨브넷이 학습한 필터를 시각화 할 수 있다.
[ssba]

The author

Player가 아니라 Creator가 되려는 자/ suyeongpark@abyne.com

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.