이 튜토리얼은 이미지 데이터 고양이와 개를 분류하는 방법을 보여줍니다. tf.keras.Sequential
모델을 사용하여 이미지 분류기인 합성곱 신경망을 만들고 tf.keras.preprocessing.image.ImageDataGenerator
를 사용하여 데이터를 불러옵니다. 세부적으로 다음 과정을 진행합니다.
tf.keras.preprocessing.image.ImageDataGenerator
클래스를 사용하여 데이터 입력 파이프라인을 구축합니다.이 튜토리얼은 다음과 같은 기본적인 머신러닝 워크플로우(workflow)를 따릅니다:
먼저 필요한 패키지를 가져오겠습니다. os
패키지는 파일과 디렉토리(directory) 구조를 읽는 데 사용되고, 넘파이(NumPy)는 파이썬(python) 리스트를 넘파이 배열로 변환하고 필요한 매트릭스 작업을 수행하는데 필요하며, matplotlib.pyplot
으로 그래프를 그리고 학습 및 검증 데이터의 이미지를 보여줍니다.
추가적으로 모델을 만드는데 필요한 텐서플로우와 케라스 클래스들을 임포트합니다.
from __future__ import absolute_import, division, print_function, unicode_literals
import warnings
warnings.simplefilter('ignore')
import os
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Conv2D, Flatten, Dropout, MaxPooling2D
from tensorflow.keras.preprocessing.image import ImageDataGenerator
데이터셋을 다운로드하는 것으로 시작합니다. 이 튜토리얼에서는 Kaggle에서 필터링된 버전의 Dogs vs Cats 데이터셋을 사용합니다. 데이터셋의 아카이브(archive) 버전을 다운로드하여 “/tmp/” 디렉토리에 저장합니다.
_URL = 'https://storage.googleapis.com/mledu-datasets/cats_and_dogs_filtered.zip'
path_to_zip = tf.keras.utils.get_file('cats_and_dogs.zip', origin=_URL, extract=True)
PATH = os.path.join(os.path.dirname(path_to_zip), 'cats_and_dogs_filtered')
데이터셋에는 다음과 같은 디렉토리 구조가 있습니다:
cats_and_dogs_filtered |__ train |______ cats: [cat.0.jpg, cat.1.jpg, cat.2.jpg ....] |______ dogs: [dog.0.jpg, dog.1.jpg, dog.2.jpg ...] |__ validation |______ cats: [cat.2000.jpg, cat.2001.jpg, cat.2002.jpg ....] |______ dogs: [dog.2000.jpg, dog.2001.jpg, dog.2002.jpg ...]
내용을 추출한 후 학습 및 검증 세트에 적합한 파일 경로를 가진 변수를 할당합니다.
train_dir = os.path.join(PATH, 'train')
validation_dir = os.path.join(PATH, 'validation')
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') # 검증을 위한 개 사진 디렉토리
학습 및 검증 디렉토리에 얼마나 많은 고양이와 개의 이미지가 있는지 살펴보겠습니다.
num_cats_tr = len(os.listdir(train_cats_dir))
num_dogs_tr = len(os.listdir(train_dogs_dir))
num_cats_val = len(os.listdir(validation_cats_dir))
num_dogs_val = len(os.listdir(validation_dogs_dir))
total_train = num_cats_tr + num_dogs_tr
total_val = num_cats_val + num_dogs_val
print('학습 고양이 이미지:%d개' %num_cats_tr)
print('학습 개 이미지:%d개' %num_dogs_tr)
print('검증 고양이 이미지:%d개' %num_cats_val)
print('검증 개 이미지:%d개' %num_dogs_val)
print("--")
print("전체 학습 이미지:%d개" %total_train)
print("전체 검증 이미지:%d개" %total_val)
편의를 위해 데이터셋을 전처리하고 네트워크를 학습하는 동안 사용할 변수를 미리 설정합니다.
batch_size = 128 # 배치 사이즈
epochs = 10 # 학습을 위한 에포크 설정
IMG_HEIGHT = 150 # 이미지 높이
IMG_WIDTH = 150 # 이미지 너비
네트워크에 전달하기 전에 다음 순서를 통해 이미지를 적절히 전처리된 부동 소수점(floating point) 텐서로 변환합니다:
다행히 이 모든 작업은 tf.keras
에서 제공하는 ImageDataGenerator
클래스로 수행할 수 있습니다. 이 클래스는 디스크에서 이미지를 읽고 적절한 텐서로 전처리할 수 있도록 하며, 네트워크를 학습시킬 때 이러한 이미지를 텐서 배치(batch)로 변환하는 생성자를 설정할 수 있습니다.
train_image_generator = ImageDataGenerator(rescale=1./255) # 훈련데이터를 위한 Generator
validation_image_generator = ImageDataGenerator(rescale=1./255) # 검증데이터를 위한 Generator
훈련 및 검증 이미지에 대한 생성자를 정의한 후 flow_from_directory
메서드를 통해 디스크에서 이미지를 로드하고, 리스케일링(rescaling)을 적용하고,이미지의 크기를 필요한 크기로 조정합니다.
train_data_gen = train_image_generator.flow_from_directory(batch_size=batch_size,
directory=train_dir,
shuffle=True,
target_size=(IMG_HEIGHT, IMG_WIDTH),
class_mode='binary')
val_data_gen = validation_image_generator.flow_from_directory(batch_size=batch_size,
directory=validation_dir,
target_size=(IMG_HEIGHT, IMG_WIDTH),
class_mode='binary')
학습 생성자에서 32개의 이미지를 추출하여 학습 이미지를 시각화합니다. 이번 예시에서는 32개의 이미지를 추출한 다음, 그 중 5개의 이미지를 matplotlib
로 그려 확인해보겠습니다.
sample_training_images, _ = next(train_data_gen)
next
함수는 데이터셋에서 배치를 반환합니다. next
함수의 반환값은 (x_train, y_train)
의 형태로, 여기서 x_train은 학습 피쳐이고, 이에 대한 레이블은 y_train입니다. 학습 이미지만을 시각화하기 위해 레이블을 삭제합니다.
# 이 함수는 1개의 행과 5개의 열을 가지고 각 열에 이미지가 존재하는
# 그리드 형식으로 이미지를 보여줍니다.
def plotImages(images_arr):
fig, axes = plt.subplots(1, 5, figsize=(20,20))
axes = axes.flatten()
for img, ax in zip( images_arr, axes):
ax.imshow(img)
ax.axis('off')
plt.tight_layout()
plt.show()
plotImages(sample_training_images[:5])
모델은 각각 max pool 레이어가 있는 세 개의 컨볼루션 블록으로 구성됩니다. 512개의 유닛이 있는 완전 연결(fully connected) 레이어가 있고, 그 위에 활성화 함수 relu
가 있습니다. 모델은 sigmoid
활성화 함수의 이진 분류에 기반한 각 클래스 확률을 출력합니다.
model = Sequential([
Conv2D(16, 3, padding='same', activation='relu', input_shape=(IMG_HEIGHT, IMG_WIDTH ,3)),
MaxPooling2D(),
Conv2D(32, 3, padding='same', activation='relu'),
MaxPooling2D(),
Conv2D(64, 3, padding='same', activation='relu'),
MaxPooling2D(),
Flatten(),
Dense(512, activation='relu'),
Dense(1, activation='sigmoid')
])
이번 튜토리얼에서는 ADAM 옵티마이저(optimizer)와 이진 교차 엔트로피(binary cross entropy) 손실 함수를 사용하겠습니다. 각 훈련 에포크마다 학습 및 검증 정확도를 보려면 metrics
인자에 전달하여 확인할 수 있습니다.
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
summary
메서드를 사용하여 네트워크의 모든 레이어를 확인합니다.
model.summary()
ImageDataGenerator
클래스의 fit_generator
메서드를 사용하여 네트워크를 학습시킵니다.
history = model.fit_generator(
train_data_gen,
steps_per_epoch=total_train // batch_size,
epochs=epochs,
validation_data=val_data_gen,
validation_steps=total_val // batch_size
)
이제 학습을 마친 네트워크의 결과를 시각화해보겠습니다.
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs_range = range(epochs)
plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
그림에서 볼 수 있듯이 학습 데이터셋에 대한 정확도와 검증 데이터셋에 대한 정확도는 큰 차이가 나며 모델은 검증 데이터셋에 대해 약 68%의 정확도를 얻는데에 그쳤습니다.
무엇이 잘못되었는지 살펴본 후 모델의 전반적인 성능을 향상시켜봅시다.
위의 그림에서 학습 데이터의 정확도는 시간이 지남에 따라 선형적으로 증가하는 반면, 검증 데이터의 정확도는 학습 프로세스에서 약 68%에 머무릅니다. 또한 학습 데이터와 검증 데이터에 대한 정확도의 차이가 눈에 띕니다. 이는 과대적합 즉, 오버피팅의 징후입니다.
훈련 데이터가 적은 경우 모델은 때때로 훈련 샘플의 노이즈(noise)나 원치 않는 세부 사항들을 학습하여 새로운 데이터에 대한 모델의 성능에 부정적인 영향을 미칩니다. 이러한 현상을 오버피팅이라고 합니다. 이는 새로운 데이터셋에서는 모델을 일반화하는 데 어려움이 있다는 것을 의미합니다.
훈련 과정에서 오버 피팅을 막는 여러가지 방법이 있습니다. 이번 튜토리얼에서는 데이터 어그멘테이션(data augmentation)을 사용하고 드롭아웃(dropout) 을 모델에 추가해 보겠습니다.
일반적으로 오버피팅은 훈련 데이터가 적은 경우에 발생합니다. 이 문제를 해결하는 한 가지 방법은 충분한 양의 훈련 데이터를 학습할 수 있도록 데이터셋을 늘리는 것입니다. 데이터 어그멘테이션(data augmentation)은 신뢰할 수 있는 이미지를 생성하는 랜덤 변환을 사용하여 샘플을 증가시킴으로써 기존 훈련 샘플에서 더 많은 훈련 샘플을 생성합니다. 이렇게 하는 이유는 이 모델이 똑같은 사진을 두 번 학습하지 않기 위해서입니다. 이를 통해 모델이 데이터의 다양한 측면을 학습할 수 있게 됩니다.
이를 ImageDataGenerator 클래스를 사용하여 tf.keras
에서 구현합니다. 다양게 변환된 데이터를 데이터셋에 전달하면 학습 과정에서 이를 적용할 수 있을 것입니다.
먼저 데이터셋에 random horizontal flip (랜덤 좌우 반전) 어그멘테이션을 적용하고 변환한 후 개별 이미지가 어떻게 변화되었는지 확인해봅시다.
horizontal_flip
을 ImageDataGenerator
클래스의 인자로 전달하고 True
로 설정하여 어그멘테이션을 적용합니다.
image_gen = ImageDataGenerator(rescale=1./255, horizontal_flip=True)
train_data_gen = image_gen.flow_from_directory(batch_size=batch_size,
directory=train_dir,
shuffle=True,
target_size=(IMG_HEIGHT, IMG_WIDTH))
훈련 데이터에서 하나의 샘플 이미지를 추출하고 어그멘테이션을 5번 반복하여 동일한 이미지에 5번 적용합니다.
augmented_images = [train_data_gen[0][0][0] for i in range(5)]
# 위에서 사용된 것과 동일한 사용자 정의 플롯 함수를 재사용하여 훈련 이미지를 시각화합니다.
plotImages(augmented_images)
회전이라는 또다른 어그멘테이션을 알아본 후 훈련 데이터를 45도씩 무작위로 회전시켜보겠습니다.
image_gen = ImageDataGenerator(rescale=1./255, rotation_range=45)
train_data_gen = image_gen.flow_from_directory(batch_size=batch_size,
directory=train_dir,
shuffle=True,
target_size=(IMG_HEIGHT, IMG_WIDTH))
augmented_images = [train_data_gen[0][0][0] for i in range(5)]
plotImages(augmented_images)
데이터셋에 확대/축소 어그멘테이션 함수를 적용하여 이미지를 50% 수준까지 무작위로 확대/축소합니다.
# zoom_range는 0에서 1사이의 값으로 설정합니다. 1은 100%를 의미합니다.
image_gen = ImageDataGenerator(rescale=1./255, zoom_range=0.5) #
train_data_gen = image_gen.flow_from_directory(batch_size=batch_size,
directory=train_dir,
shuffle=True,
target_size=(IMG_HEIGHT, IMG_WIDTH))
augmented_images = [train_data_gen[0][0][0] for i in range(5)]
plotImages(augmented_images)
이전의 어그멘테이션을 모두 적용합니다. 여기서는 리스케일링과 45도 회전, 너비 바꾸기, 높이 바꾸기, horizontal flip(좌우 반전)과 확대/축소 어그멘테이션을 훈련 이미지에 적용합니다.
image_gen_train = ImageDataGenerator(
rescale=1./255,
rotation_range=45,
width_shift_range=.15,
height_shift_range=.15,
horizontal_flip=True,
zoom_range=0.5
)
train_data_gen = image_gen_train.flow_from_directory(batch_size=batch_size,
directory=train_dir,
shuffle=True,
target_size=(IMG_HEIGHT, IMG_WIDTH),
class_mode='binary')
이 어그멘테이션을 데이터셋으로 랜덤하게 전달할 때 단일 이미지가 어떻게 각각 다르게 나타나는지 시각화해봅시다.
augmented_images = [train_data_gen[0][0][0] for i in range(5)]
plotImages(augmented_images)
일반적으로 데이터 어그멘테이션은 훈련 데이터에만 적용됩니다. 이 경우 검증 데이터 이미지만 리스케일링하고 ImageDataGenerator
를 사용하여 배치(batch)로 변환합니다.
image_gen_val = ImageDataGenerator(rescale=1./255)
val_data_gen = image_gen_val.flow_from_directory(batch_size=batch_size,
directory=validation_dir,
target_size=(IMG_HEIGHT, IMG_WIDTH),
class_mode='binary')
오버피팅을 줄이는 또 다른 방법은 드롭아웃(dropout)을 네트워크에 도입하는 것입니다. 이는 정규화의 한 종류로, 네트워크의 가중치가 작은 값만 취하게 함으로써 가중치 분포가 보다 더 정규화되어 데이터 수가 적은 훈련 데이터에서의 오버피팅을 줄일 수 있습니다.
레이어에 드롭아웃을 적용하면 훈련 과정 중 적용된 레이어에서 출력 유닛의 수가 무작위로 삭제됩니다(0으로 설정). 드롭아웃은 0.1, 0.2, 0.4 등의 형식으로 작은 수를 입력 값으로 사용합니다. 즉, 적용된 레이어의 출력 유닛의 10%, 20% 또는 40%를 무작위로 삭제합니다.
0.1의 드롭아웃을 특정 레이어에 적용하면 각 학습 에포크에서 출력 유닛의 10%를 무작위로 삭제하는 것입니다.
이 새로운 드롭아웃 기능을 사용하여 네트워크 구조를 생성하고 이를 서로 다른 컨볼루션과 완전히 연결된(fully-connected) 레이어에 적용합니다.
여기서는 첫 번째와 마지막의 max pool 레이어에 드롭아웃을 적용합니다. 드롭아웃(0.2)을 적용하면 각 훈련 에포크에서 무작위로 20%의 뉴런을 0으로 설정합니다. 이를 통해 학습 데이터셋에 오버피팅되는 것을 방지할 수 있습니다.
model_new = Sequential([
Conv2D(16, 3, padding='same', activation='relu',
input_shape=(IMG_HEIGHT, IMG_WIDTH ,3)),
MaxPooling2D(),
Dropout(0.2),
Conv2D(32, 3, padding='same', activation='relu'),
MaxPooling2D(),
Conv2D(64, 3, padding='same', activation='relu'),
MaxPooling2D(),
Dropout(0.2),
Flatten(),
Dense(512, activation='relu'),
Dense(1, activation='sigmoid')
])
네트워크에 드롭아웃을 적용한 후 모델을 컴파일하고 네트워크를 확인해보겠습니다.
model_new.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
model_new.summary()
훈련 데이터에 데이터 어그멘테이션을 도입하고 네트워크에 드롭아웃을 추가한 새로운 네트워크를 학습해봅시다.
history = model_new.fit_generator(
train_data_gen,
steps_per_epoch=total_train // batch_size,
epochs=epochs,
validation_data=val_data_gen,
validation_steps=total_val // batch_size
)
학습한 후 새 모델을 시각화해보면 이전보다 오버 피팅이 상당히 적다는 것을 알 수 있습니다. 더 많은 에포크로 모델을 학습시키면 정확도가 더 높아져야 합니다.
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs_range = range(epochs)
plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.