Ch8

2024. 5. 20. 22:51딥러닝

8.1 합성곱 신경망 - CNN 소개

  • 다음 코드는 2장에서 밀집 연결 신경망(densely connected network)으로 풀었던 MNIST 숫자 이미지 분류에 CNN(컨브넷)을 사용한 예제
  • 간단하게 만들었음에도 기존의 밀집 연결 신경망층의 정확도인 97%의 정확도를 99.3% 테스트 정확도로 앞지름
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)
  • Conv2D와 MaxPooling2D 층을 쌓아 올렸음
  • 함수형 API를 사용해서 CNN모델을 만든 예
  • 컨브넷이 배치 차원을 제외하고 (image_height, image_width, image_channels) 크기의 입력 텐서를 사용한다는 점이 중요
  • 이 예제에서는 MNIST 이미지 포맷인 (28,28,1) 크기의 입력을 처리하도록 컨브넷을 설정
  • 모델의 summary() 메서드 출력
model.summary()

 

  • Conv2D와 MaxPooling2D 층의 출력은 (height, width, channels) 크기의 랭크-3 텐서
  • 높이와 너비 차원은 모델이 깊어질수록 작아지는 경향이 있음
  • 채널의 수는 Conv2D 층에 전달된 첫 번째 매개변수에 의해 조절(32개, 64개 또는 128개)
  • 마지막 Conv2D 층의 출력 크기는 (3,3,128)
  • 즉, 128개의 채널을 가진 3 * 3 크기의 특성 맵(feature map)
  • 다음 단계는 이 출력을 [밀집 연결 분류기]Dense로 주입하는 것
  • Dense 층 이전에 Flatten 층으로 먼저 3D 출력을 1D 텐서로 펼쳐야 함
  • 마지막으로 10개의 클래스를 분류하기 위해 마지막 층의 출력(뉴런) 개수를 10으로 하고 소프트맥스 활성화 함수를 사용 확률값으로 변환
  • 소프트맥스 활성화 함수의 출력을 바탕으로 10개의 클래스를 분류하기 때문에 범주형 크로스엔트로피 손실을 사용
  • 레이블이 정수이므로 희소한 크로스엔트로피 손실인 sparse_categorical_crossentropy를 사용
  • 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)
  • 테스트 데이터로 모델을 평가

 

  • 2장의 완전 연결 네트워크는 97.8%의 테스트 정확도를 얻은 반면 기본적인 CNN(컨브넷)은 99.3%의 테스트 정확도를 얻었음
  • 에러율이 (상대적으로) 60%나 줄었음

합성곱 연산

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

 

이미지는 에지(edge), 질감(texture) 등 지역 패턴으로 분해될 수 있다

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

 

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

 

응답 맵의 개념: 입력의 각 위치에서 한 패턴의 존재에 대한 2D 맵

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

 

합성곱 작동 방식

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

경계 문제와 패딩 이해하기

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

 

55 입력 특성 맵에서 가능한 33 패치 위치

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

 

25개의 33 패치를 뽑기 위해 55 입력에 패딩 추가하기

  • Conv2D 층에서 패딩은 padding 매개변수로 설정할 수 있음
  • 2개의 값이 가능
  • "valid"는 패딩을 사용하지 않는다는 뜻(윈도우를 놓을 수 있는 위치만 사용)
  • "same"은 "입력과 동일한 높이와 너비를 가진 출력을 만들기 위해 패딩한다."라는 뜻
  • padding 매개변수의 기본값은 "valid"

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

  • 출력 크기에 영향을 미치는 다른 요소는 스트라이드
  • 지금까지 합성곱에 대한 설명은 합성곱 윈도우의 중앙 타일이 연속적으로 지나간다고 가정한 것
  • 두 번의 연속적인 윈도우 사이의 거리가 스트라이드라고 불리는 합성곱의 파라미터
  • 스트라이드의 기본값은 1
  • 스트라이드가 1보다 큰 스트라이드 합성곱도 가능

 

22 스트라이드를 사용한 33 합성곱의 패치

  • 스트라이드 2를 사용했다는 것은 특성 맵의 너비와 높이가 2의 배수로 다운샘플링되었다는 뜻 (경계 문제가 있다면 더 줄어듦)
  • 스트라이드 합성곱은 분류 모델에서 드물게 사용
  • 일부 유형의 모델에서는 유용
  • 분류 모델에서는 특성 맵을 다운샘플링하기 위해 스트라이드 대신에 첫 번째 컨브넷 예제에 사용된 최대 풀링(max pooling) 연산을 사용하는 경우가 많음

최대 풀링 연산

  • 앞선 컨브넷 예제에서 특성 맵의 크기가 MaxPooling2D 층마다 절반으로 줄어들었음
  • 예를 들어 첫 번째 MaxPooling2D 층 이전에 특성 맵의 크기는 2626이었는데 최대 풀링 연산으로 1313으로 줄어들었음
  • 스트라이드 합성곱과 매우 비슷하게 강제적으로 특성 맵을 다운샘플링하는 것이 최대 풀링의 역할
  • 최대 풀링은 입력 특성 맵에도 윈도우에 맞는 패치를 추출하고 각 채널별로 최댓값을 출력
  • 합성곱과 개념적으로 비슷하지만 추출할 패치에 학습된 선형 변환(합성곱 커널)을 적용하는 대신 하드코딩된 최댓값 추출 연산을 사용
  • 합성곱과 가장 큰 차이점은 최대 풀링은 보통 2*2윈도우와 스트라이드 2를 사용하여 특성 맵을 절반 크기로 다운샘플링한다는 것
  • 이에 반해, 합성곱은 전형적으로 3*3 윈도우와 스트라이드 1을 사용
  • 왜 최대 풀링 층을 빼고 큰 특성 맵을 계속 유지하지 않을까?
  • 이런 방식을 한 번 테스트해 보자
  • 최대 풀링 층이 빠진 잘못된 구조의 컨브넷
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)
  • 이 모델의 구조는 다음과 같음

 

  • 이 설정에서 무엇이 문제일까?
  • 두 가지가 있음
    • 특성의 공간적 계층 구조를 학습하는 데 도움이 되지 않음
      • 세 번째 층의 33 윈도우는 초기 입력의 77 윈도우 영역에 대한 정보만 담고 있음
      • 컨브넷에 의해 학습된 고수준 패턴은 초기 입력에 관한 정보가 아주 적어 숫자 분류를 학습하기에 충분하지 않을 것(7*7 픽셀 크기의 창으로 숫자를 보고 분류해 보자!)
      • 마지막 합성곱 층의 특성이 전체 입력에 대한 정보를 가지고 있어야 함
    • 최종 특성 맵은 2222128=61,952개의 원소를 가짐
      • 아주 많음
      • 이 컨브넷을 펼친 후 10개의 유닛을 가진 Dense 층과 연결한다면 50만 개의 가중치 파라미터가 생김
      • 작은 모델치고는 너무 많은 가중치고, 심각한 과대적합이 발생할 것
  • 간단히 말해서 다운샘플링을 사용하는 이유는 처리할 특성 맵의 가중치 개수를 줄이기 위해서임
  • 또한, 연속적인 합성곱 층이 (원본 입력에서 커버되는 영역 측면에서) 점점 커진 윈도우를 통해 바라보도록 만들어 필터의 공간적인 계층 구조를 구성
  • 최대 풀링이 다운샘플링을 할 수 있는 유일한 방법은 아님
  • 이미 알고 있듯이 앞서 합성곱 층에서 스트라이드를 사용할 수 있음
  • 최댓값을 취하는 최대 풀링 대신에 입력 패치의 채널별 평균값을 계산하여 변환하는 평균 풀링(average pooling)을 사용할 수도 있음
  • 최대 풀링이 다른 방법들보다 더 잘 작동하는 편
  • 그 이유는 특성이 특성 맵의 각 타일에서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문임(그래서 특성의 지도(맵))
  • 특성의 평균값보다 여러 특성 중 최댓값을 사용하는 것이 더 유용
  • 가장 납득할 만한 서브샘플링(subsampling) 전략은 먼저 (스트라이드가 없는 합성곱으로) 조밀한 특성 맵을 만들고 그다음 작은 패치에 대해 최대로 활성화된 특성을 고르는 것
  • 이런 방법이 입력에 대해 (스트라이드 합성곱으로) 듬성듬성 윈도우를 슬라이딩하거나 입력 패치를 평균해서 특성 정보를 놓치거나 희석시키는 것보다 나음

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

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

  • 보통 '적은' 샘플이란 수백 개에서 수만 개 사이를 의미
  • 실용적인 예제로 5,000개의 강아지와 고양이 사진(2,500개는 강아지, 2,500개는 고양이)으로 구성된 데이터셋에서 강아지와 고양이 이미지를 분류해 보자
  • 훈련을 위해 2,000개의 사진을 사용하고 검증에 1,000개와 테스트에 2,000개의 사진을 사용
  • 2,000개의 훈련 샘플에서 작은 컨브넷을 어떤 규제 방법도 사용하지 않고 훈련하여 기준이 되는 기본 성능을 만들겠음
  • 이 방법은 약 70%의 분류 정확도를 달성할 것
  • 이 방법의 주요 이슈는 과대적합이 될 것
  • 그다음 컴퓨터 비전에서 과대적합을 줄이기 위한 강력한 방법인 데이터 증식(data augmentation)을 소개하겠음
  • 데이터 증식을 통해 네트워크의 성능을 80~85% 정확도로 향상시킬 것
  • 사전 훈련된 네트워크로 특성을 추출하는 것(97.5%의 정확도를 얻게 됨)과 사전 훈련된 네트워크를 세밀하게 튜닝하는 것(최종 모델은 98.5% 정확도를 얻을 것)
  • 이런 세 가지 전략(처음부터 작은 모델 훈련하기, 사전 훈련된 모델을 사용하여 특성 추출하기, 사전 훈련된 모델을 세밀하게 튜닝하기)은 작은 데이터셋에서 이미지 분류 문제를 수행할 때 기본적인 워크플로로 알고있어야 함

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

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

데이터 내려받기

  • 여기에서 사용할 강아지 vs 고양이 데이터셋(Dogs vs Cats dataset)은 케라스에 포함되어 있지 않음
  • 캐글 API를 사용하여 이 데이터셋을 코랩으로 내려받을 수 있음

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

  • 캐글은 프로그램을 사용하여 캐글에 호스팅된 데이터셋을 내려받을 수 있도록 사용하기 쉬운 API를 제공
  • 이 API는 kaggle 패키지로 제공되며 코랩에는 이미 설치되어 있음
  • 코랩 셀에서 다음 명령을 실행하여 데이터셋을 쉽게 내려받을 수 있음
!kaggle competitions download -c dogs-vs-cats
  • 캐글 사용자만 이 API를 사용할 수 있음
  • 앞의 명령을 실행하기 위해서는 먼저 사용자 인증이 필요함
  • kaggle 패키지는 JSON 파일인 ~/.kaggle/kaggle.json에서 로그인 정보를 찾음
  • 이 파일을 만들어 보자
  1. 먼저 캐글 API키를 만들어 로컬 컴퓨터로 내려받아야 함
    • 웹 브러우저로 캐글 웹 사이트에 접속하여 로그인한 후 Account 페이지로 이동

https://www.kaggle.com/#

Kaggle: Your Machine Learning and Data Science Community Kaggle is the world’s largest data science community with powerful tools and resources to help you achieve your data science goals. www.kaggle.com

!https://blog.kakaocdn.net/dn/bj61Dz/btsoXx8CV3D/Sl5FHkNNlfKfkxFQ4TkS3K/img.png

  • 이 페이지에서 API 섹션을 찾음
  • 그다음 Create New API Token 버튼을 누리면 kaggle.json 파일이 생성되고 컴퓨터로 내려받기 됨

 

  1. 그다음 코랩 노트북으로 이동한 후 셀에서 다음 명령을 실행하여 API키가 담긴 JSON 파일을 현재 코랩 세션(session)에 업로드
# kaggle.json 파일을 업로드하세요.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
  • 이제 사용할 데이터를 내려받을 수 있음
!kaggle competitions download -c dogs-vs-cats
  • 데이터를 처음 내려받을 때 '403 Forbidden' 에러가 발생할 수 있음
  • 데이터를 내려받기 전에 이 데이터셋에 연관된 규칙에 동의해야 하기 때문임
  • (캐글 계정에 로그인한 상태에서) www.kaggle.com/c/dogs-vs-cats/rules 페이지로 이동한 후 I Understand and Accept 버튼을 누름
  • 약관 동의는 한 번만 하면 됨

 

  • 훈련 데이터는 dogs-vs-cats.zip 이름의 압축 파일
  • 이 파일의 압축을 해제한 후 생성된 train.zip 파일의 압축도 품
  • 압축 해제할 때(unzip) 메시지가 출력되지 않도록 옵션을 추가(-qq)
!unzip -qq dogs-vs-cats.zip
!unzip -qq train.zip
  • 데이터셋에 있는 사진들은 중간 정도의 해상도를 가진 컬러 JPEG 파일

 

강아지vs고양이 데이터셋의 샘플로 이 샘플들은 원본 크기 그대로이며 샘플들은 크기, 배경 등이 제각각이다

  • 당연히 2013년 강아지 vs 고양이 캐글 경연은 컨브넷을 사용한 참가자가 우승
  • 최고 성능은 95%의 정확도를 달성
  • 이 예제로 (다음 절에서) 참가자들이 사용했던 데이터의 10%보다 적은 양으로 모델을 훈련하고도 이와 아주 근접한 정확도를 달성해 보자
  • 이 데이터셋은 2만 5,000개의 강아지와 고양이 이미지(클래스마다 1만 2,500개)를 담고 있고 (압축해서) 543MB 크기
  • 데이터를 내려받아 압축을 해제한 후 3개의 서브셋이 들어 있는 새로운 데이터셋을 만들 것
  • 클래스마다 1,000개의 샘플로 이루어진 훈련 세트, 클래스마다 500개의 샘플로 이루어진 검증 세트, 클래스마다 1000개의 샘플로 이루어진 테스트 세트
  • 많은 데이터가 있으면 문제가 더 쉬워짐
  • 배울 때는 작은 데이터셋을 사용하는 것이 좋음
  • 앞으로 사용할 3개의 서브셋은 다음과 같은 디렉토리 구조를 가짐

 

  • 이미지를 훈련, 검증, 테스트 디렉토리로 복사하기
import os, shutil, pathlib

original_dir = pathlib.Path("train")# 원본 데이터셋이 압축 해제되어 있는 디렉토리 경로
new_base_dir = pathlib.Path("cats_vs_dogs_small")# 서브셋 데이터를 저장할 디렉토리# start_index에서 end_index까지의 고양이와 강아지 이미지를 new_base_dir/(subset_name)/cat(또는 /dog)으로 복사하기 위한 유틸리티 함수 subset_name은 "train", "validation", "test"중 하나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)# 카테고리마다 처음 1,000개의 이미지를 훈련 서브셋으로 만든다
make_subset("validation", start_index=1000, end_index=1500)# 카테고리마다 그다음 500갸의 이미지를 검증 서브셋으로 만든다.
make_subset("test", start_index=1500, end_index=2500)# 카테고리마다 그다음 1,000개의 이미지를 테스트 서브셋으로 만든다.
  • 이제 2,000개의 훈련 이미지, 1,000개의 검증 이미지, 2,000개의 테스트 이미지가 준비
  • 분할된 각 데이터는 클래스마다 동일한 개수의 샘플을 포함
  • 균형 잡힌 이진 분류 문제이므로 정확도를 사용하여 성능을 측정

모델 만들기

  • 첫 번째 예제에서 보았던 일반적인 모델 구조를 동일하게 재사용
  • Conv2D(relu 활성화 함수 사용)와 MaxPooling2D 층을 번갈아 쌓은 컨브넷
  • 이전보다 이미지가 크고 복잡한 문제이기 때문에 모델을 좀 더 크게 만들겠음
  • Conv2D와 MaxPooling2D 단계를 하나 더 추가
  • 이렇게 하면 모델의 용량을 늘리고 Flatten 층의 크기가 너무 커지지 않도록 특성 맵의 크기를 줄일 수 있음
  • 특성 맵의 깊이는 모델에서 점진적으로 증가하지만(32에서 256까지), 특성 맵의 크기는 감소 (180180에서 77까지)
  • 이는 거의 모든 컨브넷에서 볼 수 있는 전형적인 패턴
  • 이진 분류 문제이므로 모델은 하나의 유닛(크기가 1인 Dense 층)과 sigmoid 활성화 함수로 끝남
  • 이 유닛은 모델이 보고 있는 샘플이 한 클래스에 속할 확률을 인코딩할 것
  • 마지막 작은 차이점 하나는 Rescaling 층으로 모델이 시작되는 것
  • 이 층은 (원래 [0, 255] 범위의 값인) 이미지 입력을 [0,1] 범위로 스케일 변환
  • 강아지 vs 고양이 분류를 위한 소규모 컨브넷 만들기
from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(180, 180, 3))# 이 모델은 180*180 크기의 RGB 이미지를 기대한다.
x = layers.Rescaling(1./255)(inputs)# 입력을 255로 나누어 [0,1]범위로 스케일을 조정한다.
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)

 

  • 컴파일 단계에서 이전과 같이 RMSprop 옵티마이저를 선택
  • 모델의 마지막이 하나의 시그모이드 유닛이기 때문에 이진 크로스엔트로피(binary crossentropy)를 손실로 사용
  • 모델 훈련 설정하기
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])

데이터 전처리

  • 데이터는 네트워크에 주입되기 전에 부동 소수점 타입의 텐서로 적절하게 전처리되어 있어야 함
  • 지금은 데이터가 JPEG 파일로 되어 있으므로 네트워크에 주입하려면 대략 다음 과정을 따름
    1. 사진 파일을 읽음
    2. JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩
    3. 그다음 부동 소수점 타입의 텐서로 변환
    4. 동일한 크기의 이미지로 바꿈(여기에서는 180*180을 사용)
    5. 배치로 묶음 (하나의 배치는 32개의 이미지로 구성)
  • 좀 복잡하게 보일 수 있지만 다행히 케라스는 이런 단계를 자동으로 처리하는 유틸리티가 있음
  • 특히 케라스는 image_dataset_from_directory() 함수를 제공
  • 이 함수를 사용하면 디스크에 있는 이미지 파일을 자동으로 전처리된 텐서의 배치로 변환하는 데이터 파이프라인을 빠르게 구성할 수 있음
  • image_dataset_from_directory(directory)를 호출하면서 먼저 directory의 서브디렉터리를 찾음
  • 각 서브디렉터리에는 한 클래스에 해당하는 이미지가 담겨 있다고 가정
  • 그다음 각 서브디렉토리에 있는 이미지 파일을 인덱싱
  • 마지막으로 이런 파일을 읽고, 순서를 섞고, 텐서로 디코딩하고, 동일 크기로 변경하고 배치로 묶어 주는 tf.data.Dataset 객체를 만들어 반환
  • 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 객체를 만들어 보자
  • 샘플 1,000개를 만들겠음
  • 각 샘플은 크기가 16인 벡터
import numpy as np
import tensorflow as tf
random_numbers = np.random.normal(size=(1000, 16))
# from_tensor_slices() 클래스 메서드를 사용하여 하나의 넘파이 배열 또는 넘파이 배열의 튜플이나 딕셔너리에서 Dataset을 만들 수 있음
dataset = tf.data.Dataset.from_tensor_slices(random_numbers)
  • 처음에는 이 데이터셋이 하나의 샘플을 반환

 

  • .batch() 메서드를 사용하면 데이터의 배치가 반환
  • 일반적으로 다음과 같은 유용한 메서드를 사용할 수 있음
    • .shuffle(buffer_size): 버퍼 안의 원소를 섞음
    • .prefetch(buffer_size): 장치 활용도를 높이기 위해 GPU 메모리에 로드할 데이터를 미리 준비
    • .map(callable): 임의의 변환을 데이터셋의 각 원소에 적용(callable 함수는 데이터셋이 반환하는 1개의 원소를 입력으로 기대함)
  • 특히 .map() 메서드는 자주 사용
  • 예를 들어 예제 데이터셋의 원소 크기를 (16,)에서 (4,)로 변환해 보겠음

 

  • fit() 메서드의 validation_data 매개변수를 사용하여 별도의 Dataset 객체로 검증 지표를 모니터링하겠음
  • 또한, ModelCheckpoint 콜백을 사용하여 에포크가 끝날 때마다 모델을 저장
  • 콜백에 파일을 저장할 경로와 매개변수 save_best_only=True와 monitor="val_loss"를 지정할 것
  • 훈련하는 동안 val_loss 값이 이전보다 더 낮을 때만 콜백이 (이전 파일을 덮어쓰는 식으로) 새로운 파일을 저장할 것
  • 이렇게 하면 저장된 파일에는 언제나 검증 데이터의 성능이 가장 좋은 훈련 에포크의 모델 상태가 들어 있게 됨
  • 결과적으로 과대적합이 시작되는 에포크 횟수로 새로운 모델을 다시 훈련할 필요가 없음
  • 저장된 파일에서 바로 모델을 로드할 수 있음
  • Dataset을 사용하여 모델 훈련하기
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)
  • 훈련 과정의 정확도와 손실 그래프 그리기
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()

 

  • 이 그래프는 과대적합의 특성을 보여 줌
  • 훈련 정확도가 시간이 지남에 따라 선형적으로 증가해서 거의 100%에 도달
  • 반면 검증 정확도는 75% 정도가 최고
  • 검증 손실은 열 번의 에포크 만에 최솟값에 다다른 이후 더 이상 진전되지 않았음
  • 반면 훈련 손실은 훈련이 진행됨에 따라 선형적으로 계속 감소
  • 테스트 정확도를 확인해 보자
  • 과대적합되기 전의 상태를 평가하기 위해 저장된 파일에서 모델을 로드
  • 테스트 세트에서 모델 평가하기
test_model = keras.models.load_model("convnet_from_scratch.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"테스트 정확도: {test_acc:.3f}")

 

  • 테스트 정확도는 71.5%를 얻었음(신경망의 랜덤한 초기화 때문에 1퍼센트 포인트 정도 차이가 날 수 있음)
  • 비교적 훈련 샘플의 개수(2,000개)가 적기 때문에 과대적합이 가장 중요한 문제
  • 드롭아웃이나 가중치 감소(L2 규제)처럼 과대적합을 감소시킬 수 있는 여러 가지 기법을 배웠음
  • 여기에서는 컴퓨터 비전에 특화되어 있어 딥러닝으로 이미지를 다룰 때 매우 일반적으로 사용되는 새로운 방법인 데이터 증식을 시도해 보자
  • 과대적합은 학습할 샘플이 너무 적어 새로운 데이터에 일반화할 수 있는 모델을 훈련시킬 수 없기 때문에 발생
  • 무한히 많은 데이터가 주어지면 데이터 분포의 모든 가능한 측면을 모델이 학습할 수 있을 것
  • 데이터 증식은 기존 훈련 샘플로부터 더 많은 훈련 데이터를 생성하는 방법
  • 이 방법은 그럴듯한 이미지를 생성하도록 여러 가지 랜덤한 변환을 적용하여 샘플을 늘림
  • 훈련할 때 모델이 정확히 같은 데이터를 두 번 만나지 않도록 하는 것이 목표
  • 모델이 데이터의 여러 측면을 학습하도록 더 잘 일반화할 수 있음
  • 케라스에서는 모델 시작 부분에 여러 개의 데이터 증식 층(data augmentation layer)을 추가할 수 있음
  • 예를 들어 보자
  • 다음 Sequential 모델은 몇 개의 랜덤한 이미지 변환을 수행
  • 컨브넷의 Rescaling 층 바로 이전에 이 모델을 추가하겠음
  • 컨브넷에 추가할 데이터 증식 단계 정의하기
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도]에 해당)
    • RandomZoom(0.2): [-20%, +20%] 범위 안에서 랜덤한 비율만큼 이미지를 확대 또는 축소
  • 랜덤하게 증식된 훈련 이미지 출력하기
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("uint8"))
        plt.axis("off")

랜덤한 데이터 증식으로 생성한 고양이 이미지

  • 데이터 증식을 사용하여 새로운 모델을 훈련시킬 때 모델에 같은 입력 데이터가 두 번 주입되지 않음
  • 적은 수의 원본 이미지에서 만들어졌기 때문에 여전히 입력 데이터들 사이에 상호 연관성이 큼
  • 즉, 새로운 정보를 만들어 낼 수 없고 단지 기존 정보의 재조합만 가능
  • 그렇기 때문에 완전히 과대적합을 제거하기에 충분하지 않을 수 있음
  • 과대적합을 더 억제하기 위해 밀집 연결 분류기직전에 Dropout 층을 추가
  • 랜덤한 이미지 증식 층에 대해 마지막으로 알아야 할 한 가지는 Dropout 층처럼 추론할 때(predict()나 evaluate() 메서드를 호출할 때)는 동작하지 않는다는 것
  • 즉, 모델을 평가할 때는 데이터 증식과 드롭아웃이 없는 모델처럼 동작
  • 이미지 증식과 드롭아웃을 포함한 컨브넷 만들기
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 에포크 동안 훈련
  • 규제를 추가한 컨브넷 훈련하기
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)

 

  • 테스트 세트에서 모델 훈련하기
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}")

 

8.3 사전 훈련된 모델 활용하기

사전 훈련된 모델 활용하기

  • 작은 이미지 데이터셋에 딥러닝을 적용하는 일반적이고 매우 효과적인 방법은 사전 훈련된 모델을 사용하는 것
  • 사전 훈련된 모델(pretrained model)은 일반적으로 대규모 이미지 분류 문제를 위해 대량의 데이터셋에서 미리 훈련된 모델
  • 원본 데이터셋이 충분히 크고 일반적이라면 사전 훈련된 모델에 의해 학습된 특성의 계층 구조는 실제 세상에 대한 일반적인 모델로 효율적인 역할을 할 수 있음
  • 새로운 문제가 원래 작업과 완전히 다른 클래스에 대한 것이더라도 이런 특성은 많은 컴퓨터 비전 문제에 유용
  • 예를 들어 (대부분 동물이나 생활용품으로 이루어진) ImageNet 데이터셋에 모델을 훈련
  • 그다음 이 모델을 이미지에서 가구 아이템을 식별하는 것 같은 다른 용도로 사용할 수 있음
  • 학습된 특성을 다른 문제에 적용할 수 있는 이런 유연성은 이전의 많은 얕은 학습 방법과 비교했을 때 딥러닝의 핵심 장점
  • 이런 방식으로 작은 데이터셋을 가진 문제에도 딥러닝이 효율적으로 작동할 수 있음
  • 여기에서는 (1,400만 개의 레이블된 이미지와 1,000개의 클래스로 이루어진) ImageNet 데이터셋에서 훈련된 대규모 컨브넷을 사용해 보자
  • ImageNet데이터셋은 다양한 종의 강아지와 고양이를 비롯하여 많은 동물을 포함하고 있음
  • 강아지 vs 고양이 분류 문제에 좋은 성능을 낼 것 같음
  • 캐런 시몬연(Keran Simonyan)과 앤드류 지서먼(Andrew Zisserman)이 2014년에 개발한 VGG16 구조를 사용
  • VGG16은 조금 오래되었고 최고 수준의 성능에는 못 미치며 최근의 다른 모델보다는 조금 무거움
  • 사전 훈련된 모델을 사용하는 두 가지 방법이 있음
  • 특성 추출(feature extraction)과 미세 조정(fine tuning)
  • 특성 추출은 사전에 학습된 모델의 표현을 사용하여 새로운 샘플에서 흥미로운 특성을 뽑아내는 것
  • 이런 특성을 사용하여 새로운 분류기를 처음부터 훈련
  • 앞서 보았듯이 컨브넷은 이미지 분류를 위해 두 부분으로 구성
  • 먼저 연속된 합성곱과 폴링 층으로 시작해서 밀집 연결 분류기로 끝남
  • 첫 번째 부분을 모델의 합성곱 기반 층(convolution base)이라고 부르겠음
  • 컨브넷의 경우 특성 추출은 사전에 훈련된 모델의 합성곱 기반 층을 선택하여 새로운 데이터를 통과시키고, 그 출력으로 새로운 분류기를 훈련

 

같은 합성곱 기반 층을 유지하면서 분류기 바꾸기

  • 왜 합성곱 층만 재사용할까?
  • 밀집 연결 분류기도 재사용할 수 있을까?
  • 일반적으로 권장하지 않음
  • 합성곱 층에 의해 학습된 표현이 더 일반적이어서 재사용이 가능하기 때문임
  • 컨브넷의 특성 맵은 이미지에 대한 일반적인 콘셉트의 존재 여부를 기록한 맵
  • 주어진 컴퓨터 비전 문제에 상관없이 유용하게 사용할 수 있음
  • 분류기에서 학습한 표현은 모델이 훈련된 클래스 집합에 특화되어 있음
  • 분류기는 전체 사진에 어떤 클래스가 존재할 확률에 관한 정보만 담고 있음
  • 더군다나 밀집 연결 층에서 찾은 표현은 더 이상 입력 이미지에 있는 객체의 위치 정보를 가지고 있지 않음
  • 밀집 연결 층들은 공간 개념을 제거하지만 합성곱의 특성 맵은 객체 위치를 고려
  • 객체 위치가 중요한 문제라면 밀집 연결 층에서 만든 특성은 크게 쓸모없음
  • 특정 합성곱 층에서 추출한 표현의 일반성(그리고 재사용성) 수준은 모델에 있는 층의 깊이에 달려 있음
  • 모델의 하위 층은 (에지, 색깔, 질감 등) 지역적이고 매우 일반적인 특성 맵을 추출
  • 반면 상위 층은 ('강아지 눈'이나 '고양이 귀'처럼) 좀 더 추상적인 개념을 추출
  • 새로운 데이터셋이 원본 모델이 훈련한 데이터셋과 많이 다르다면 전체 합성곱기반 층을 사용하는 것보다는 모델의 하위 층 몇 개만 특성 추출에 사용하는 것이 좋음
  • ImageNet의 클래스 집합에는 여러 종류의 강아지와 고양이를 포함하고 있음
  • 이런 경우 원본 모델의 완전 연결 층에 있는 정보를 재사용하는 것이 도움이 될 것 같음
  • 새로운 문제의 클래스가 원본 모델의 클래스 집합과 겹치지 않는 좀 더 일반적인 경우를 다루기 위해 여기에서는 완전 연결 층을 사용하지 않겠음
  • ImageNet 데이터셋에 훈련된 VGG16 네트워크의 합성곱 기반 층을 사용하여 강아지와 고양이 이미지에서 유용한 특성을 추출해 보자
  • 그런 다음 이 특성으로 강아지 vs 고양이 분류기를 훈련
  • VGG16 모델은 케라스에 패키지로 포함되어 있음
  • keras.applications 모듈에서 임포트 할 수 있음
  • keras.applications 모듈에서 사용 가능한 이미지 분류 모델은 다음과 같음(모두 ImageNet 데이터셋에서 훈련)
    • Xception
    • ResNet
    • MobileNet
    • Efficientnet
    • DenseNet
    • 그 외
  • VGG16 합성곱 기반 층 만들기
conv_base = keras.applications.vgg16.VGG16(
    weights="imagenet",
    include_top=False,
    input_shape=(180, 180, 3))
  • VGG16 함수에 3개의 매개변수를 전달
    • weights는 모델을 초기화할 가중치 체크포인트(checkpoint)를 지정
    • include_top은 네트워크 맨 위에 놓인 밀집 연결 분류기를 포함할지 안 할지 지정
      • 기본값은 ImageNet의 클래스 1,000개에 대응되는 밀집 연결 분류기를 포함
      • 별도의 (강아지와 고양이 2개의 클래스를 구분하는) 밀집 연결 층을 추가하려고 하므로 이를 포함시키지 않음
    • input_shape은 네트워크에 주입할 이미지 텐서의 크기
      • 이 매개변수는 선택 사항
      • 이 값을 지정하지 않으면 네트워크가 어떤 크기의 입력도 처리할 수 있음
      • 여기에서는 합성곱 층과 풀링 층을 거치면서 특성 맵이 어떻게 줄어드는지(이더지는 summary() 메서드로) 시각화하기 위해 입력 크기를 지정
  • 다음은 VGG16 합성곱 기반 층의 자세한 구조
  • 이 구조는 앞서 보았던 간단한 컨브넷과 비슷함

 

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

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

  • 먼저 훈련, 검증, 테스트 데이터셋에서 conv_base 모델의 predict() 메서드를 호출하여 넘파이 배열로 특성을 추출
  • 데이터셋을 순회하면서 VGG16의 특성을 추출해 보자
  • VGG16 특성과 해당 레이블 추출하기
import numpy as np

def get_features_and_labels(dataset):
    all_features = []
    all_labels = []
    for images, labels 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)
  • 중요한 점은 predict() 메서드가 레이블은 제외하고 이미지만 기대한다는 것
  • 현재 데이터셋은 이미지와 레이블을 함께 담고 있는 배치를 반환
  • 또한, VGG16 모델은 적절한 범위로 픽셀 값을 조정해 주는 keras.applications.vgg16.preprocess_input 함수로 전처리된 입력을 기대
  • 추출된 특성의 크기는 (samples, 5, 5, 512)
  • 이제 (규제를 위해 드롭아웃을 사용한) 밀집 연결 분류기를 정의하고 방금 저장한 데이터와 레이블에서 훈련할 수 있음
  • 밀집 연결 분류기 정의하고 훈련하기
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, activation="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초 미만
  • 훈련 과정의 손실과 정확도를 그래프로 나타내면 다음과 같음

 

  • 약 97%의 검증 정확도에 도달했음
  • 이전 절에서 처음부터 훈련시킨 작은 모델에서 얻은 것보다 훨씬 좋음
  • ImageNet에는 개와 고양이 샘플이 많기 때문에 약간 공정하지 않은 비교
  • 다시 말하면 사전 훈련된 모델이 현재 주어진 작업에 딱 맞는 지식을 이미 가지고 있음
  • 사전 훈련된 특성을 사용할 때 항상 이렇지는 않음
  • 이 그래프는 많은 비율로 드롭아웃을 사용했음에도 훈련을 시작하면서 거의 바로 과대적합되고 있다는 것을 보여 줌
  • 작은 이미지 데이터셋에서는 과대적합을 막기 위해 필수적인 데이터 증식을 사용하지 않았기 때문임

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

  • 이제 특성 추출을 위해 두 번째로 언급한 방법을 살펴보겠음
  • 이 방법은 훨씬 느리고 비용이 많이 들지만 훈련하는 동안 데이터 증식 기법을 사용할 수 있음
  • conv_base와 새로운 밀집 분류기를 연결한 모델을 만들고 입력 데이터를 사용하여 엔드-투-엔드로 실행
  • 이렇게 하려면 먼저 합성곱 기반 층을 동결해야 함
  • 하나 이상의 층을 동결(freezing)한다는 것은 훈련하는 동안 가중치가 업데이트되지 않도록 막는다는 뜻
  • 이렇게 하지 않으면 합성곱 기반 층에 의해 사전에 학습된 표현이 훈련하는 동안 수정될 것
  • 맨 위의 Dense 층은 랜덤하게 초기화되었기 때문에 매우 큰 가중치 업데이트 값이 네트워크에 전파될 것
  • 이는 사전에 학습된 표현을 크게 훼손하게 됨
  • 케라스에서는 trainable 속성을 False로 설정하여 층이나 모델을 동결할 수 있음
  • VGG16 합성곱 기반 층을 만들고 동결하기
conv_base  = keras.applications.vgg16.VGG16(
    weights="imagenet",
    include_top=False)
conv_base.trainable = False
  • trainable 속성을 False로 지정하면 층이나 모델의 훈련 가능한 가중치 리스트가 텅 비게 됨
  • 동결하기 전과 후에 훈련 가능한 가중치 리스트 출력하기

 

  • 이제 다음을 연결하여 새로운 모델을 만들 수 있음
    1. 데이터 증식 단계
    2. 동결된 합성곱 기반 층
    3. 밀집 분류기
  • 데이터 증식 단계와 밀집 분류기를 합성곱 기반 층에 추가하기
data_augmentation = keras.Sequential(
    [
        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"])
  • 이렇게 설정하면 추가한 2개의 Dense 층 가중치만 훈련될 것
  • 층마다 2개씩 (가중치 행렬과 편향 벡터) 총 4개의 텐서가 훈련
  • 변경 사항을 적용하려면 먼저 모델을 컴파일해야 함
  • 컴파일 단계 후에 trainable 속성을 변경하면 반드시 모델을 다시 컴파일해야 함
  • 그렇지 않으면 변경 사항이 적용되지 않음
  • 모델을 훈련해 보자
  • 데이터 증식 덕분에 과대적합이 시작되기까지 훨씬 오래 걸릴 것
  • 더 많은 에포크 동안 훈련할 수 있음
  • 50번의 에포크를 시도해 보자
  • 이 기법은 연산 비용이 크기 때문에 GPU를 사용할 수 있을 때 시도해야 함(예를 들어 코랩의 무료 GPU 런타임)
  • CPU에서는 적용하기 힘듦
  • GPU를 사용할 수 없다면 첫 번째 방법을 사용
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)
  • 여기에서 볼 수 있듯이 검증 정확도가 98%에 도달
  • 이전 모델보다 크게 향상
  • 테스트 정확도를 확인해 보자
  • 테스트 세트에서 모델 평가하기

 

  • 테스트 정확도 97.8%를 얻었음
  • 이전에 얻은 테스트 정확도에 비해 조금만 향상된 것으로 검증 데이터의 좋은 결과를 감안할 때 약간 실망스러움
  • 모델의 정확도는 향상 평가하려는 샘플 세트에 따라 달라짐!
  • 일부 샘플 세트는 다른 세트에 비해 어려울 수 있으며 한 세트에서 좋은 결과가 다른 모든 세트에 항상 적용된다는 것은 아님
  • 모델을 재사용하는 데 널리 사용되는 또 하나의 기법은 특성 추출을 보완하는 미세 조정
  • 미세 조정은 특성 추출에 사용했던 동결 모델의 상위 층 몇 개를 동결에서 해제하고 모델에 새로 추가한 층(여기에서는 밀집 연결 분류기)과 함께 훈련하는 것
  • 주어진 문제에 조금 더 밀접하게 재사용 모델의 표현을 일부 조정하기 때문에 미세 조정이라고 부름

VGG16 네트워크에서 마지막 합성곱 블록에 대한 미세 조정

  • 앞서 랜덤하게 초기화된 상단 분류기를 훈련하기 위해 VGG16의 합성곱 기반 층을 동결해야 한다고 말했음
  • 같은 이유로 맨 위에 있는 분류기가 훈련된 후 합성곱 기반의 상위 층을 미세조정할 수 있음
  • 분류기가 미리 훈련되지 않으면 훈련되는 동안 너무 큰 오차 신호가 네트워크에 전파
  • 이는 미세 조정될 층들이 사전에 학습한 표현들을 망가뜨리게 될 것
  • 네트워크를 미세 조정하는 단계는 다음과 같음
    1. 사전에 훈련된 기반 네트워크 위에 새로운 네트워크를 추가
    2. 기반 네트워크를 동결
    3. 새로 추가한 네트워크를 훈련
    4. 기반 네트워크에서 일부 층의 동결을 해제
      • ("배치 정규화(batch normalization)" 층은 동결 해제하면 안 되고 VGG16에는 이런 층이 없기 때문에 여기에서는 해당되지 않음)
    5. 동결을 해제한 층과 새로 추가한 층을 함께 훈련
  • 처음 세 단계는 특성 추출을 할 때 이미 완료
  • 네 번째 단계를 진행해 보자
  • conv_base의 동결을 해제하고 개별 층을 동결
  • 마지막 3개의 합성곱 층을 미세 조정하겠음
  • 즉, block4_pool까지 모든 층은 동결되고 block5_conv1, block5_conv2, block5_conv3 층은 학습 대상이 됨
  • 왜 더 많은 층을 미세 조정하지 않을까?
  • 왜 전체 합성곱 기반 층을 미세 조정하지 않을까?
  • 그렇게 할 수도 있지만 다음 사항을 고려해야 함
    • 합성곱 기반 층에 있는 하위 층들은 좀 더 일반적이고 재사용 가능한 특성들을 인코딩
      • 반면 상위 층은 좀 더 특화된 특성을 인코딩
      • 새로운 문제에 재사용하도록 수정이 필요한 것은 구체적인 특성이므로 이들을 미세 조정하는 것이 유리함
    • 하위 층으로 갈수록 미세 조정에 대한 효과가 감소
      • 훈련해야 할 파라미터가 많을수록 과대적합의 위험이 커짐
      • 합성곱 기반 층은 1,500만 개의 파라미터를 가지고 있음
      • 작은 데이터셋으로 전부 훈련하려고 하면 매우 위험
  • 그러므로 이런 상황에서는 합성곱 기반 층에서 최상위 2~3개의 층만 미세 조정하는 것이 좋음
  • 마지막에서 네 번째 층까지 모든 층 동결하기
conv_base.trainable = True
for layer in conv_base.layers[:-4]:
    layer.trainable = False
  • 이제 이 모델의 미세 조정을 시작
  • 학습률을 낮춘 RMSProp 옵티마이저를 사용
  • 학습률을 낮추는 이뉴는 미세 조정하는 3개의 층에서 학습된 표현을 조금씩 수정하기 위해서임
  • 변경량이 너무 크면 학습된 표현에 나쁜 영향을 끼칠 수 있음
  • 모델 미세 조정하기
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)
  • 97.5% 정도의 테스트 정확도를 얻음
  • 이 데이터셋을 사용한 원래 캐글 경연 대회의 최상위 결과 중 하나에 해당
  • 강아지와 고양이에 대한 사전 지식이 이미 포함되어 있는 사전 훈련된 특성을 사용했기 때문에 공정한 비교는 아님
  • 긍정적으로 보면 최신 딥러닝 기법을 활용하여 대회에서 제공하는 훈련 데이터의 일부분(약 10%)만 사용해서 이런 결과를 달성
  • 2만 개의 샘플에서 훈련하는 것과 2,000개의 샘플에서 훈련하는 것 사이에는 아주 큰 차이점이 있음!

'딥러닝' 카테고리의 다른 글

Ch11  (0) 2024.06.30
Ch9  (0) 2024.05.23
Ch7  (0) 2024.05.12
Ch6  (0) 2024.05.05
Ch5  (0) 2024.04.07