실전 딥러닝! 딥러닝을 이용한 예제를 풀어봅시다.

본 문서는 [케라스 창시자에게 배우는 딥러닝] 책을 기반으로 하고 있으며, subinium(본인)이 정리하고 추가한 내용입니다. 생략된 부분과 추가된 부분이 있으니 추가/수정하면 좋을 것 같은 부분은 댓글로 이야기해주시면 감사하겠습니다.

  • 책에서는 일부 부분에서 width를 넓이라고 번역합니다. 매우 불편하니 너비로 수정하여 정리하겠습니다.

들어가기전 : Google Colab 소개

이번 장부터는 Google Colaboratory에서 코드를 진행합니다. Google Colab의 컴퓨터 사양은 다음과 같습니다.

  • CPU : Intel Xeon 2.2GHz
  • RAM : 13GB
  • 저장공간 : 33GB
  • GPU : Nvidia Tesla K80
  • 12시간 연속 사용가능

TPU도 지원한다고 적혀있는데 GPU보다 느립니다. GPU 자체는 시중에서 300만원 정도 합니다. 아마 공부용으로는 유용할 것 같습니다.

Colaboratory를 사용하는 방법은 따로 포스팅하지 않겠습니다. 다음 링크에서 너무 쉽고 간단하게 알려주니 다음과 같이 사용하면 됩니다.

google colab 사용하기

자금에 여유가 된다면 AWS와 비슷한 구글 클라우드 서비스를 사용해도 괜찮을 것 같습니다. 다음은 GCP를 사용하는 좋은 글이 있어 가져와봤습니다.

google cloud 사용하기

Colab을 GPU로 세팅한 결과, MNIST_CNN예제 코드에서는 epoch에 9초/85초로 한 10배정도 빠릅니다. 하지만 3장 주택 가격 예측에서 K-겹 검증에서는 현재 노트북에서 5~6배정도 더 빠릅니다.

CPU의 차이인 것 같습니다. 앞으로 주피터 노트북과 Colab의 속도를 소규모에서 비교한 후 코드를 돌리는 것이 최적의 방법인 것 같습니다.

하지만 일단은 Colab이 편하니 대부분은 Colab에서 진행할 예정입니다.

5.1 합성곱 신경망 소개

컨브넷 정의와 컨브넷이 컴뷰터 비전 관련 작업에 잘 맞는 이유에 대해 이론적 배경을 알아봅시다.

2장에서 진행한 MNIST문제는 완전 연결 네트워크로 문제를 해결했습니다. 이번에는 기본적인 컨브넷을 이용해서 문제를 해결해봅시다. 컨브넷의 기본적인 모습은 다음과 같습니다. Conv2D와 MaxPooling2D 층을 쌓은 코드입니다.

# 코드 5-1 간단한 컨브넷 만들기
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)임을 주의 해주세요.

지금까지 컨브넷의 구조를 summary 메서드로 확인해보면 다음과 같습니다.

>> model.summary()
_________________________________________________________________
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의 첫 매개변수에 따라 결정됩니다.

마지막 단계에서 현재는 3D텐서로 출력을 줍니다. 분류를 위해 이제 1D텐서로 펼치고, 완전 연결 네트워크로 마무리를 합니다. 다음은 펴는 과정과 추가하는 층입니다.

# 코드 5-2 컨브넷 위에 분류기 추가하기
model.add(layers.Flatten())
models.add(layers.Dense(64, activation='relu'))
models.add(layers.Dense(10, activation='softmax'))

펼치게 되면 (3,3,64) -> (576,) 크기의 벡터로 변경됩니다. 이제 훈련을 진행합니다. 2장 코드를 그대로 사용하면 됩니다.

# 코드 5-3 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)

각 epoch 별로 10s가 소모되었습니다. 이제 테스트 데이터에서 모델을 평가하면 다음과 같은 정확도를 가집니다.

>> test_loss, test_acc = model.evaluate(test_images, test_labels)
>> print(test_loss, test_acc)
0.030514104746288195 0.9912

완전 연결 네트워크만 사용했을때 97.8%정도의 정확도를 가졌는데, 이번엔 99.1%까지 정확도가 증가했습니다.

또한 에러율이 상대적으로 60%이상 줄었습니다.

이제 왜 컨브넷이 더 좋은 결과를 산출하는지 알아봅시다.

5.1.1 합성곱 연산

완전 연결 층과 합성곱 층의 가장 큰 차이점은 학습하는 내용이 다릅니다. 두 층 모두 패턴을 학습한다는 점은 같습니다. 하지만 완전 연결 층은 전역 패턴을 학습하고, 합성곱 층은 지역 패턴을 합성합니다. 합성곱 층은 다음과 같이 부분들의 패턴을 파악합니다.

conv layer image

Conv2D에서는 이미지를 작은 윈도우로 쪼개서 봅니다. 여기서는 입력값에서 (3,3) 즉 3*3 윈도우로 나누어 본 것 입니다.

이 핵심 특징은 컨브넷에 두 가지 성질을 제공합니다.

  1. 학습된 패턴은 평행 이동 불변성(translation invariant) 을 가집니다. 컨브넷이 이미지 오른쪽 아래에서 패턴을 학습했다면 왼쪽 위에서도 패턴을 인식할 수 있습니다. (완전 연결 네트워크는 다른 위치의 패턴은 다르게 인식합니다.) 이는 학습 속도를 빠르게 하며 적은 샘플로도 학습을 가능하게 만듭니다.
  2. 컨브넷은 패턴의 공간적 계층 구조 를 학습할 수 있습니다. 분할정복과 같이 부분적으로 학습 패턴을 넓혀갑니다.

합성곱 연산은 특성 맵(feature map) 이라고 부르는 3D 텐서에 적용됩니다. 이 텐서는 2개의 공간 축(높이와 너비)과 깊이(채널) 축으로 구성됩니다. RGB 이미지는 3개의 컬러 채널이므로 깊이 축은 3차원이 됩니다. MNIST는 흑백 이미지이므로 1차원입니다.

합성곱 연산은 입력 특성 맵에서 작은 패치(patch)들을 추출하고 이런 모든 패치에 같은 변환을 적용하여 출력 특성 맵(output feature map) 을 만듭니다.

출력 특성 맵도 높이와 너비를 가진 3D 텐서입니다. 하지만 깊이 축의 채널은 더 이상 전에 입력으로 들어온 채널과 다릅니다. 깊이 축의 채널은 일종의 필터 역할을 하게 됩니다. 각각의 특성을 지니는 채널로 변경되는 것입니다.

MNIST문제의 첫번째 층을 살펴보면 다음과 같습니다. (28,28,1) -> (26,26,32)은 32개의 채널값이 26*26에 기존 데이터를 필터링하여 매핑되어 있다는 것을 알 수 있습니다. 이를 응답 맵(response map) 이라고 합니다.

코드에서는 첫 번째 층에서 (3,3,1)크기의 필터 32개를 적용하고, 두번째 Conv2D에서 (3,3,32)크기 필터를 64개 적용합니다.

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

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

3D 입력 특성 맵 위를 패치 크기 만큼의 윈도우가 슬라이딩하면서 패치에서 특성 패치를 추출하는 구조입니다. 출력 높이와 너비는 입력의 높이와 너비와 다를 수 있는데 여기에는 두 가지 이유가 있습니다.

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

스트라이드 및 내용에 대해서 좀 더 알아봅시다.

경계 문제와 패딩 이해하기

특성 맵을 윈도우로 출력 특성 맵을 하게 되면 맵의 크기는 줄어듭니다. (5, 5)를 (3, 3)로 출력특성맵을 만들면 (5-3+1) * (5-3+1), 즉 (3, 3)크기의 출력 특성 맵을 만들어집니다. 하지만 계속 맵을 줄여나가게 되면 특성을 찾기 힘들 수 있고, 입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶을 수 있습니다. 그럴때 암호에서도 자주 사용하는 패딩(padding) 을 사용할 수 있습니다. 패딩은 적절하게 가장자리에 행과 열을 추가하면 됩니다. 보통 0으로 채우게 되기에 제로 패딩이라고도 부릅니다.

Conv2D 층에서 패딩은 padding 매개변수로 설정할 수 있습니다. 2개의 값이 가능합니다. valid는 패딩을 사용하지 않고, same은 입력과 동일한 크기의 출력을 위해 패딩하는 것입니다.

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

출력 크기에 영향을 미치는 다른 요소는 스트라이드 입니다. 위의 문제의 경우는 합성곱 윈도우가 연속적으로 지나간다고 가정한 것입니다. (스트라이드 = 1) 두 번의 연속적인 윈도우 사이의 거리가 스트라이드라고 불리는 합성곱의 파라미터입니다. 즉 윈도우간의 거리를 스트라이드라고 할 수 있습니다. 출력을 o, 입력을 i, 필터(윈도우) f, 스트라이드를 s라 하면, 다음과 같은 수식을 만족하게 됩니다.

\[o_{width} = \lfloor \frac{i_{width}-f_{width}}{s_{width}}\rfloor + 1\] \[o_{height} = \lfloor \frac{i_{height}-f_{height}}{s_{height}}\rfloor + 1\]

스트라이드 합성곱은 실전에서 드물게 사용되지만 모델에 따라 유용한 방법이므로 알아두어야 합니다. 다운샘플링하기 위해서는 스트라이드 대신에 최대 풀링 연산을 사용하는 경우가 많습니다. 바로 알아봅시다.

5.1.2 최대 풀링 연산

앞의 컨브넷 예제에서 특성 맵의 크기가 MaxPooling2D 층마다 절반으로 줄어들었습니다. 최대 풀링 연산은 이처럼 특성 맵을 강제적으로 다운 샘플링하는 것입니다. 최대 풀링은 입력 특성 맵에서 윈도우에 맞는 패치를 추출하고 각 채널별로 최댓값을 출력합니다. 자동적으로 채널 수는 줄어듭니다.

사실 여기까지는 합성곱 스트라이드와 큰 차이가 없다고 생각할 수 있습니다. 하지만 합성곱은 추출한 패치에서 학습된 선형 변환(합성곱 커널)을 적용하는 대신 하트코딩된 최댓값 추출 연산을 사용합니다. 그림을 보면 이해가 될 것 같습니다.

max pooling

가장 큰 차이점은 최대풀링은 2 * 2 윈도우와 스트라이드 2를 사용하고, 합성곱은 3 * 3 윈도우와 스트라이드 1을 사용합니다. 왜 합성곱만 이용하지 않고, 최대 풀링 층을 이용할까요? 그 이유는 다음과 같이 설명할 수 있습니다.

  • 합성곱 층으로만 이루어진 네트워크는 특성의 공간적 계층 구조를 학습하는데 도움이 되지 않습니다. 합성곱을 하며 줄어든 윈도우는 특성에 대한 정보가 부족합니다.
  • 최종 가중치가 너무 많아 작은 모델에 비해 오버피팅이 발생할 가능성이 큽니다.

간단하게 다운샘플링의 이유는 특성 맵의 가중치 개수를 줄이는 것입니다. 또한 점점 커진 윈도우를 통해 입력 데이터를 공간적 계층 구조로 확인하기 위하여 사용합니다.

평균 풀링(average pooling) 도 이용할 수 있지만, 최대 풀링이 다른 방법보다 더 잘 작동하는 편입니다. 그 이유는 특성이 특성 맵의 각 타일에 따라서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문입니다. 그렇기에 합리적인, 납득할만한 서브샘플링(subsampling) 전략은 먼저 스트라이드 없는 합성곱으로 조밀한 특성 맵을 만들고, 최대 풀링 연산으로 최대 활성화된 특성을 고르는 것입니다.

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

이번 장에서는 다음과 같은 이미지 분류 문제를 해결하고자 합니다.

  • 전문적이나 기본적인 컴퓨터 비전 예제
  • 적은 데이터(적은 = 수백개에서 수만개 사이)
  • 4000개의 강아지와 고양이 사진 (각각 2000개)
  • 훈련에 2000개
  • 검증과 테스트에 각각 1000개

이 문제에서는 기본적인 전략은 다음과 같습니다.

  1. 보유한 소규모 데이터셋을 사용하여 처음부터 새로운 모델 훈련
  2. 사전 훈련된 네트워크로 특성을 추출
  3. 사전 훈련된 네트워크를 세밀하게 튜닝

소규모 데이터셋을 사용하여 처음부터 새로운 모델을 훈련하는 것입니다.

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

딥러닝은 데이터가 많을 때 유용합니다. 하지만 ‘많다’는 상대적입니다. 그렇다고 복잡한 문제에서 수십 개의 데이터만으로는 훈련이 불가능합니다. 하지만 모델이 작고 규제가 잘 되어 있다면 수백 개의 샘플로도 충분할 수 있습니다.

컨브넷은 지역적이고 평행 이동으로 변하지 않는 특성을 학습하기 때문에 지각에 관한 문제에서 효율적으로 데이터를 사용합니다. 매우 작은 이미지 데이터셋에서는 특성 공학을 사용하지 않고 컨브넷을 처음부터 훈련해도 납득할 만한 결과를 얻을 수 있습니다.

5.2.2 데이터 내려받기

강아지와 고양이 데이터셋은 케라스에는 없습니다. 2013년 캐글에서 컴퓨터 비전 경연 대회의 일환으로 이 데이터셋을 만들었고, 이는 캐글에서 내려받을 수 있습니다. 이 사진들은 중간 정도의 해상도를 가진 JPEG 파일이며 크기가 다 다릅니다.

2013년, 본 데이터를 사용한 경연에서 우승자는 컨브넷을 사용했습니다. 최고 성능은 95%의 정확도를 달성했습니다. 책에서는 10%보다 적은 양으로 훈련합니다.

캐글에는 별도의 테스트 데이터가 있지만 책에서는 완전한 예제를 위해 훈련 데이터에서 훈련, 검증, 테스트 세트를 만듭니다. 저는 코랩에서 진행하고 있고, 캐글에는 다운로드 할 수 있게 API가 준비되어 있으니, 그걸 이용해봅시다.

코랩에서 kaggle API 사용하기

여기서 저는 코랩의 단점을 하나 느꼈습니다. 시간이 지난 후에 캐글 세팅 및 다운로드를 다시 해야한다는 점입니다.

일단 코랩에서 디렉토리를 다 설정하고 이제 분류작업을 해봅시다. 저는 코랩에서 기본적으로 있는 sample_data 디렉토리에서 진행했습니다. 파이썬을 공부해야 겠습니다. 아직 모르는 코드가 너무 많아서 어렵네요. 본 코드는 코드 예제 정리 에서 가져왔습니다.

unzip명령으로 압축을 풀고, 디렉토리만 잘 설정하면 다음코드를 쉽게 진행할 수 있습니다.

# 코드 5-4 훈련, 검증, 테스트 폴더로 이미지 복사하기
import os, shutil

# 원본 데이터셋을 압축 해제한 디렉터리 경로
original_dataset_dir = './datasets/cats_and_dogs/train'

# 소규모 데이터셋을 저장할 디렉터리
base_dir = './datasets/cats_and_dogs_small'
if os.path.exists(base_dir):  # 반복적인 실행을 위해 디렉토리를 삭제합니다.
    shutil.rmtree(base_dir)   # 이 코드는 책에 포함되어 있지 않습니다.
os.mkdir(base_dir)

# 훈련, 검증, 테스트 분할을 위한 디렉터리
train_dir = os.path.join(base_dir, 'train')
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation')
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test')
os.mkdir(test_dir)

# 훈련용 고양이 사진 디렉터리
train_cats_dir = os.path.join(train_dir, 'cats')
os.mkdir(train_cats_dir)

# 훈련용 강아지 사진 디렉터리
train_dogs_dir = os.path.join(train_dir, 'dogs')
os.mkdir(train_dogs_dir)

# 검증용 고양이 사진 디렉터리
validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)

# 검증용 강아지 사진 디렉터리
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)

# 테스트용 고양이 사진 디렉터리
test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)

# 테스트용 강아지 사진 디렉터리
test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)

# 처음 1,000개의 고양이 이미지를 train_cats_dir에 복사합니다
fnames = ['cat.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_cats_dir, fname)
    shutil.copyfile(src, dst)

# 다음 500개 고양이 이미지를 validation_cats_dir에 복사합니다
fnames = ['cat.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_cats_dir, fname)
    shutil.copyfile(src, dst)

# 다음 500개 고양이 이미지를 test_cats_dir에 복사합니다
fnames = ['cat.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_cats_dir, fname)
    shutil.copyfile(src, dst)

# 처음 1,000개의 강아지 이미지를 train_dogs_dir에 복사합니다
fnames = ['dog.{}.jpg'.format(i) for i in range(1000)]
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(train_dogs_dir, fname)
    shutil.copyfile(src, dst)

# 다음 500개 강아지 이미지를 validation_dogs_dir에 복사합니다
fnames = ['dog.{}.jpg'.format(i) for i in range(1000, 1500)]
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_dogs_dir, fname)
    shutil.copyfile(src, dst)

# 다음 500개 강아지 이미지를 test_dogs_dir에 복사합니다
fnames = ['dog.{}.jpg'.format(i) for i in range(1500, 2000)]
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_dogs_dir, fname)
    shutil.copyfile(src, dst)

본 코드를 실행하면 데이터는 다음과 같이 나눠집니다.

  • 2000개의 훈련 이미지
  • 1000개의 검증 이미지
  • 1000개의 테스트 이미지

분할된 각 데이터는 동일한 개수의 샘플을 포함합니다. 이진 분류 문제이므로 정확도를 이용해 성공을 측정합니다.

5.2.3 네트워크 구성하기

MNIST의 예제와 비슷하지만, 분제가 복잡하므로 층을 좀 더 많이 사용하여 150 * 150 크기의 입력 특성맵을 7 * 7의 특성맵으로 줄입니다. 이렇게 해야 Flatten층에서 크기가 너무 커지는 것을 방지할 수 있습니다.

# 코드 5-5 강아지 vs. 고양이 분류를 위한 소규모 컨브넷 만들기
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'))

이진 분류 문제이므로 네트워크는 하나의 유닛과 sigmoid 활성화 함수로 끝납니다. 이 유닛은 한 클래스에 대한 확률을 인코딩합니다. 모델을 확인해봅시다.

>> model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_1 (Conv2D)            (None, 148, 148, 32)      896       
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 74, 74, 32)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 72, 72, 64)        18496     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 36, 36, 64)        0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 34, 34, 128)       73856     
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 17, 17, 128)       0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 15, 15, 128)       147584    
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 7, 7, 128)         0         
_________________________________________________________________
flatten_1 (Flatten)          (None, 6272)              0         
_________________________________________________________________
dense_1 (Dense)              (None, 512)               3211776   
_________________________________________________________________
dense_2 (Dense)              (None, 1)                 513       
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
_________________________________________________________________

컴파일 단계에서는 RMSprop 옵티마이저를 사용합니다.

# 코드 5-6 모델의 훈련 설정하기
from keras import optimizers

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

5.2.4 데이터 전처리

데이터는 네트워크에 주입되기 전에 부동 소수의 타입의 텐서로 적절하게 전처리되어 있어야 합니다. JPEG파일이므로 다음과 같은 과정으로 전처리 합니다.

  1. 사진 파일을 읽습니다.
  2. JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩합니다.
  3. 그다음 부동 소수 타입의 텐서로 변환합니다.
  4. 픽셀 값(0~255의 정수값)의 스케일을 [0, 1] 사이로 조정합니다.

케라스에서는 이런 단계를 자동으로 처리하는 유틸리티가 있습니다. 케라스에는 keras.preprocessing.image 와 같이 이미지 처리를 위한 헬퍼 도구들도 있습니다. ImageDataGenerator 클래스는 디스크에 있는 이미지 파일을 전처리된 배치 텐서로 자동으로 바꾸어 주는 파이썬 제너레이터를 만들어 줍니다.

# 코드 5-7 ImageDataGenerator를 사용하여 디렉터리에서 이미지 읽기
from keras.preprocessing.image import ImageDataGenerator

# 모든 이미지를 1/255로 스케일을 조정합니다
train_datagen = ImageDataGenerator(rescale=1./255)
test_datagen = ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(
        # 타깃 디렉터리
        train_dir,
        # 모든 이미지를 150 × 150 크기로 바꿉니다
        target_size=(150, 150),
        batch_size=20,
        # binary_crossentropy 손실을 사용하기 때문에 이진 레이블이 필요합니다
        class_mode='binary')

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

제너레이터를 사용한 데이터에 모델을 훈련시켜보겠습니다. fit_generator 메서드는 fit 메서드와 동일하되 데이터 제너레이터를 사용할 수 있습니다.

# 코드 5-8 배치 제너레이터를 사용하여 모델 훈련하기
history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=30,
      validation_data=validation_generator,
      validation_steps=50)

각 매개변수는 다음과 같습니다.

  • 파이썬 제너레이터
  • 제너레이터의 끝이 없기 때문에 필요한 에포크 별 배치 개수 (배치가 20개이므로 100개 해야 2000개를 사용)
  • 에포크
  • 검증 제너레이터
  • 검증 에포크 별 배치 개수 (배치가 20개이므로 50개 있어야 1000개 사용)

위 코드는 코랩에서 GPU에서 각 에포크별 11초 소모되었습니다. 훈련 후에 모델을 저장합니다. (안해도 되는데 좋은 방법이라고 합니다. ^^)

# 코드 5-9 데이터 저장하기
model.save('cats_and_dogs_small_1.h5')

알쓸신잡으로 h5 확장자는 HDF 파일의 확장자로 계층적 데이터 형식이라고 합니다.

  • 모든 크기의 유형의 시스템에서 사용할 수 있음
  • 유연하고 효율적인 저장소 및 빠른 IO 제공
  • 많은 포맷이 HDF지원

이제 훈련 데이터와 검증 데이터 모델의 손실과 정확도 그래프로 그려봅시다.

# 코드 5-10 훈련의 정확도와 손실 그래프 그리기
import matplotlib.pyplot as plt
acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

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

plt.figure()

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

plt.show()
정확도
손실

그래프를 보면 검증 정확도는 70%에서 멈추고, 손실은 11번쯤 이내에서 최저점을 찍는 것을 알 수 있습니다. 훈련 샘플이 적기 때문에 과대적합은 중요한 문제입니다. 드롭아웃이나 가중치 감소처럼 과대적합을 감소시킬 수 있는 여러가지 방법이 있지만, 여기서는 컴퓨터 비전에서 사용하는 데이터 증식 을 시도해봅니다.

5.2.5 데이터 증식 사용하기

데이터 증식은 말 그대로 데이터를 늘리는 방법입니다. 데이터를 늘리는 방법은 여러 가지 랜덤한 변환을 적용하는 것입니다. 모델이 데이터의 여러 측면을 학습하면 일반화에 도움이 될 것입니다. 다음과 같은 방법으로 여러 종류의 랜덤 변환을 적용하도록 설정합니다.

# 코드 5-11 ImageDataGenerator를 사용하여 데이터 증식 설정하기
datagen = ImageDataGenerator(
      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')
  • rotation_range는 랜덤하게 사진을 회전시킬 각도 범위입니다(0-180 사이).
  • width_shift_rangeheight_shift_range는 사진을 수평과 수직으로 랜덤하게 평행 이동시킬 범위입니다(전체 넓이와 높이에 대한 비율).
  • shear_range는 랜덤하게 전단 변환을 적용할 각도 범위입니다.
  • zoom_range는 랜덤하게 사진을 확대할 범위입니다.
  • horizontal_flip은 랜덤하게 이미지를 수평으로 뒤집습니다. 수평 대칭을 가정할 수 있을 때 사용합니다(예를 들어, 풍경/인물 사진).
  • fill_mode는 회전이나 가로/세로 이동으로 인해 새롭게 생성해야 할 픽셀을 채울 전략입니다.

이렇게 데이터 증식을 사용하여 새로운 네트워크를 훈련시킬 때 네트워크에 같은 입력 데이터가 두 번 주입되지 않습니다. 하지만 적은 수의 원본 이미지에서 만들어졌기 때문에 아직은 입력 데이터들 사이에 상호 연관성이 큽니다.

그렇다면 기존 정보의 재조합만 이루어지고, 과대적합을 해결하기 위해서는 충분하지 못합니다. 그렇기에 dropout까지 추가해서 층을 새롭게 이루고, 네트워크를 훈련시켜보겠습니다. flatten 층 다음에 model.add(layers.Dropout(0.5)) 다음코드만 추가하고 전까지 과정을 반복하면 됩니다. 모델부터 다시 만들어야하는 것입니다. 그래프까지 그려보면 다음과 같습니다. 코드는 다음과 같습니다.

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,
        # 모든 이미지를 150 × 150 크기로 바꿉니다
        target_size=(150, 150),
        batch_size=32,
        # binary_crossentropy 손실을 사용하기 때문에 이진 레이블을 만들어야 합니다
        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_small_2.h5')

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

epochs = range(len(acc))

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

plt.figure()

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

plt.show()

이번에는 에포크별 31-32초 소모되었습니다. 100 epoch라 약 1시간 정도 소모되었습니다. 아마 제가 이 예제를 돌리는 동안 GPU를 지원안했던 게 아닌가라는 생각을 하고 있습니다.

인내의 시간… 그래프는 다음과 같습니다. 85% 정도의 정확도까지 향상시킬 수 있는 것을 볼 수 있습니다.

정확도
손실

규제 기법을 변경하고, 네트워크의 파라미터를 튜닝하면 더 높은 정확도를 얻을 수 있지만 데이터가 적기 때문에 컨브넷을 처음부터 훈련해서 더 높은 정확도를 달성하기는 어렵습니다.

다음에는 상황에서 정확도를 높이기 위해 사전 훈련된 모델을 사용하는 방법을 진행해봅시다.

Leave a Comment