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)을 가짐
- 컨브넷이 이미지의 오른쪽 아래 모서리에서 어떤 패턴을 학습했다면 다른 곳 (예를 들어 왼쪽 위 모서리)에서도 이 패턴을 인식할 수 있음
- 완전 연결 네트워크는 새로운 위치에 나타난 것은 새로운 패턴으로 학습해야 함
- 이런 성질은 컨브넷이 이미지를 효율적으로 처리하게 만들어 줌(근본적으로 우리가 보는 세상은 평행 이동으로 인해 다르게 인식되지 않음)
- 적은 수의 훈련 샘플을 사용해서 일반화 능력을 가진 표현을 학습할 수 있음
- 컨브넷은 패턴의 공간적 계층 구조를 학습할 수 있음
- 첫 번째 합성곱 층이 에지 같은 작은 [지역 패턴]을 학습
- 두 번째 합성곱 층은 첫 번째 층의 특성으로 구성된 더 큰 패턴을 학습하는 식
- 이런 방식을 사용하여 컨브넷은 매우 복잡하고 추상적인 시각적 개념을 효과적으로 학습할 수 있음
- 근본적으로 우리가 보는 세상은 공간적 계층 구조를 가지고 있기 때문임(사진, 이미지 등)
- 학습된 패턴은 평행 이동 불변성(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에서 로그인 정보를 찾음
- 이 파일을 만들어 보자
- 먼저 캐글 API키를 만들어 로컬 컴퓨터로 내려받아야 함
- 웹 브러우저로 캐글 웹 사이트에 접속하여 로그인한 후 Account 페이지로 이동
!https://blog.kakaocdn.net/dn/bj61Dz/btsoXx8CV3D/Sl5FHkNNlfKfkxFQ4TkS3K/img.png
- 이 페이지에서 API 섹션을 찾음
- 그다음 Create New API Token 버튼을 누리면 kaggle.json 파일이 생성되고 컴퓨터로 내려받기 됨
- 그다음 코랩 노트북으로 이동한 후 셀에서 다음 명령을 실행하여 API키가 담긴 JSON 파일을 현재 코랩 세션(session)에 업로드
# kaggle.json 파일을 업로드하세요.from google.colab import files
files.upload()
이 셀을 실행하면 파일 선택 버튼이 나타남. 버튼을 누르고 방금 전에 내려받은 kaggle.json 파일을 선택. 그러면 이 파일이 현재 코랩 런타임에 업로드
- 마지막으로 ~/.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 파일로 되어 있으므로 네트워크에 주입하려면 대략 다음 과정을 따름
- 사진 파일을 읽음
- JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩
- 그다음 부동 소수점 타입의 텐서로 변환
- 동일한 크기의 이미지로 바꿈(여기에서는 180*180을 사용)
- 배치로 묶음 (하나의 배치는 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로 지정하면 층이나 모델의 훈련 가능한 가중치 리스트가 텅 비게 됨
- 동결하기 전과 후에 훈련 가능한 가중치 리스트 출력하기
- 이제 다음을 연결하여 새로운 모델을 만들 수 있음
- 데이터 증식 단계
- 동결된 합성곱 기반 층
- 밀집 분류기
- 데이터 증식 단계와 밀집 분류기를 합성곱 기반 층에 추가하기
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의 합성곱 기반 층을 동결해야 한다고 말했음
- 같은 이유로 맨 위에 있는 분류기가 훈련된 후 합성곱 기반의 상위 층을 미세조정할 수 있음
- 분류기가 미리 훈련되지 않으면 훈련되는 동안 너무 큰 오차 신호가 네트워크에 전파
- 이는 미세 조정될 층들이 사전에 학습한 표현들을 망가뜨리게 될 것
- 네트워크를 미세 조정하는 단계는 다음과 같음
- 사전에 훈련된 기반 네트워크 위에 새로운 네트워크를 추가
- 기반 네트워크를 동결
- 새로 추가한 네트워크를 훈련
- 기반 네트워크에서 일부 층의 동결을 해제
- ("배치 정규화(batch normalization)" 층은 동결 해제하면 안 되고 VGG16에는 이런 층이 없기 때문에 여기에서는 해당되지 않음)
- 동결을 해제한 층과 새로 추가한 층을 함께 훈련
- 처음 세 단계는 특성 추출을 할 때 이미 완료
- 네 번째 단계를 진행해 보자
- 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개의 샘플에서 훈련하는 것 사이에는 아주 큰 차이점이 있음!