본문 바로가기
ML & DL/tensorflow

[Tensorflow][Kaggle] Cats vs. Dogs Classification(수정 : 2020-12-07)

by 별준 2020. 12. 4.

www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition

 

Dogs vs. Cats Redux: Kernels Edition

Distinguish images of dogs from cats

www.kaggle.com

딥러닝 연습으로 Kaggle의 Dogs vs. Cats Classification을 진행해보겠습니다.

기존 사이트는 www.kaggle.com/c/dogs-vs-cats 이지만, 현재 결과 제출이 되지 않는 상태이기 때문에 위 사이트에서 진행하였습니다.

 

학습 모델은 간단한 CNN 모델과 Pre-trained된 VGG16 모델을 사용해서 진행해보도록 하겠습니다.

 

1. Data 전처리

우선 training에 사용되는 데이터는 총 25,000의 고양이와 개의 이미지가 있으며, test를 위한 데이터는 총 12,500개의 이미지가 있습니다. 

train.zip 압축을 해제하면, cat.i.jpg와 dog.i.jpg의 이름을 가진 이미지 파일들이 있으며, i는 0부터 12499까지의 인덱스입니다. 

개 이미지 12,500개, 고양이 이미지 12,500개를 train/validation/test 용으로 분리하고, tensorflow의 ImageDataGenerator를 사용할 것이기 때문에 각 라벨(cat,dog)를 폴더별로 따로 분리하도록 하겠습니다.

import matplotlib.pyplot as plt
import os
import shutil
import zipfile
import glob
import cv2

import tensorflow as tf
import numpy as np
import pandas as pd

# zip파일 경로 설정
data_zip_dir = '/kaggle/input/dogs-vs-cats-redux-kernels-edition'
train_zip_dir = os.path.join(data_zip_dir, 'train.zip')
test_zip_dir = os.path.join(data_zip_dir, 'test.zip')

# 압축해제
with zipfile.ZipFile(train_zip_dir, 'r') as z:
    z.extractall()
with zipfile.ZipFile(test_zip_dir, 'r') as z:
    z.extractall()

train_dir = os.path.join(os.getcwd(), 'train')
test_dir = os.path.join(os.getcwd(), 'test')

# train용 폴더 생성
train_set_dir = os.path.join(train_dir, 'train_set')
os.mkdir(train_set_dir)
train_dog_dir = os.path.join(train_set_dir, 'dog')
os.mkdir(train_dog_dir)
train_cat_dir = os.path.join(train_set_dir, 'cat')
os.mkdir(train_cat_dir)
# valid용 폴더 생성
valid_set_dir = os.path.join(train_dir, 'valid_set')
os.mkdir(valid_set_dir)
valid_dog_dir = os.path.join(valid_set_dir, 'dog')
os.mkdir(valid_dog_dir)
valid_cat_dir = os.path.join(valid_set_dir, 'cat')
os.mkdir(valid_cat_dir)
# test용 폴더 생성
test_set_dir = os.path.join(train_dir, 'test_set')
os.mkdir(test_set_dir)
test_dog_dir = os.path.join(test_set_dir, 'dog')
os.mkdir(test_dog_dir)
test_cat_dir = os.path.join(test_set_dir, 'cat')
os.mkdir(test_cat_dir)

# image file name list 생성
dog_files = [f'dog.{i}.jpg' for i in range(12500)]
cat_files = [f'cat.{i}.jpg' for i in range(12500)]

# 각 폴더로 image 이동
for file in dog_files[:10000]:
    src = os.path.join(train_dir, file)
    dst = os.path.join(train_dog_dir, file)
    shutil.move(src, dst)
    
for file in dog_files[10000:12000]:
    src = os.path.join(train_dir, file)
    dst = os.path.join(valid_dog_dir, file)
    shutil.move(src, dst)

for file in dog_files[12000:12500]:
    src = os.path.join(train_dir, file)
    dst = os.path.join(test_dog_dir, file)
    shutil.move(src, dst)

for file in cat_files[:10000]:
    src = os.path.join(train_dir, file)
    dst = os.path.join(train_cat_dir, file)
    shutil.move(src, dst)
    
for file in cat_files[10000:12000]:
    src = os.path.join(train_dir, file)
    dst = os.path.join(valid_cat_dir, file)
    shutil.move(src, dst)

for file in cat_files[12000:12500]:
    src = os.path.join(train_dir, file)
    dst = os.path.join(test_cat_dir, file)
    shutil.move(src, dst)

각 용도에 맞는 폴더를 라벨별로 생성해주고, 압축해제한 이미지 파일들을 각 폴더로 옮겨주었습니다.

print(f'the number of train set : {len(os.listdir(train_dog_dir)) + len(os.listdir(train_cat_dir))}')
print(f'the number of validn set : {len(os.listdir(valid_dog_dir)) + len(os.listdir(valid_cat_dir))}')
print(f'the number of test set : {len(os.listdir(test_dog_dir)) + len(os.listdir(test_cat_dir))}')

training set으로 총 20,000장의 이미지(cat:10,000/dog:10,000), cross-validation set으로 4,000장의 이미지(cat:2,000/dog:2,000), test set으로 1,000장의 이미지(cat:500/dog:500)으로 구성하였습니다.

 

그리고 data genertor를 생성해주는데, 각 이미지를 읽을 때마다 랜덤하게 변형을 주어서 읽도록 설정하겠습니다.

train_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255,
                                                      rotation_range=40,
                                                      width_shift_range=0.2,
                                                       height_shift_range=0.2,
                                                       shear_range=0.2,
                                                       zoom_range=0.2,
                                                       horizontal_flip=True,
                                                       fill_mode='nearest')
valid_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)
test_datagen = tf.keras.preprocessing.image.ImageDataGenerator(rescale=1./255)

train_generator = train_datagen.flow_from_directory(train_set_dir,
                                                   target_size=(150,150),
                                                   batch_size=32,
                                                   class_mode='binary')
valid_generator = valid_datagen.flow_from_directory(valid_set_dir,
                                                   target_size=(150,150),
                                                   batch_size=32,
                                                   class_mode='binary')
test_generator = test_datagen.flow_from_directory(test_set_dir,
                                                 target_size=(150,150),
                                                 batch_size=32,
                                                 class_mode='binary')

train_step = train_generator.n // 32
valid_step = valid_generator.n // 32
test_step = test_generator.n // 32

ImageDataGenerator를 통해서 data generator를 생성해주는데 rescale로 normalization을 적용합니다. 그리고, 랜덤 변환을 적용하기 위해서 사용되는 파라미터는 다음과 같습니다.

  • rotation_range - 사진 회전각도 범위; 0~180사이값
  • width_shift_range, height_shift_range - 수평과 수직으로 평행 이동시킬 범위; 전체 너비와 높이에 대한 비율값
  • shear_range - shearing transformation(전단 변환)을 적용할 각도 범위; 사진을 3D로 기울임
  • zoom_range - 사진을 확대할 범위
  • horizontal_flip - 랜덤하게 이미지를 수평으로 뒤집음
  • fill_mode - 회전이나 이동을 통해 빈 곳이 생기면 픽셀을 채우는 방법(nearest는 인접한 픽셀을 사용함)

그리고 mini-batch size는 32로 설정하고, 1 epoch를 위한 총 step수를 계산해둡니다.(fit_generator에서 사용)

 

2. 간단한 CNN Model 구현

우선 간단한 ConvNet을 구성해보도록 하겠습니다.

model = tf.keras.models.Sequential([
    tf.keras.layers.Input(shape=(150,150,3)),
    tf.keras.layers.Conv2D(filters=32, kernel_size=(3,3), strides=1, padding='same', activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(filters=64, kernel_size=(3,3), strides=1, padding='same', activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(filters=128, kernel_size=(3,3), strides=1, padding='same', activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(filters=128, kernel_size=(3,3), strides=1, padding='same', activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(1024, activation='relu'),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(512, activation='relu'),
    tf.keras.layers.Dropout(0.1),
    tf.keras.layers.Dense(1, activation='sigmoid')
])

model.summary()

Conv2D와 MaxPooling2D로 구성된 Convolution layer와 마지막에 벡터 텐서로 변환하여서 Fully Connected 층을 구성하였습니다. 그리고 overfitting 문제를 줄이기 위해서 FC 층에서 Dropout을 적용하였습니다.

model.compile(optimizer=tf.keras.optimizers.Adam(1e-3),
             loss='binary_crossentropy',
              metrics=['acc'])
              
model.fit_generator(train_generator,
                   steps_per_epoch=train_step,
                   epochs=10,
                   validation_data=valid_generator,
                   validation_steps=valid_step)

최적화 알고리즘으로는 Adam(learning rate=0.001)을 사용하고, loss로는 이진분류이기 때문에 'binary_crossentropy'로 설정하였습니다. 

우선 10회만 학습해보도록 하죠.

처음에 50%를 약간 넘는 수치의 정확도로 시작하여서 10회 학습 후에 77%의 train acc와 82%의 valid acc를 얻었습니다. 아무래도 데이터가 많기 때문에 더 많은 학습이 필요한 것 같습니다.

 

10회 더 반복한 결과입니다.

계속 학습하면, 더 좋은 성능을 얻을 수 있을 것 같습니다. 학습된 모델을 우선 한 번 저장하고, 테스트용 데이터로 평가를 해보도록 하겠습니다.

model.save('CNN_epoch_20.h5')

test_loss, test_acc = model.evaluate_generator(test_generator,
                                               steps=test_step,
                                               workers=4)

print(f'test loss : {test_loss:.4f} / test acc : {test_acc*100:.2f} %')

88%의 test acc를 얻었습니다. overfitting은 확실하게 피한 것으로 보입니다.

 

추가로 20 epoch 더 학습을 진행했고, 마지막 10 epoch의 결과와 test set의 결과가 다음과 같습니다.

 

저의 목표는 처음부터 VGG16 Network를 사용해서 학습하는 것이었기 때문에.... 여기까지 진행하도록 하고, VGG16 network를 기반으로 FC layer를 추가해서 학습해보도록 하겠습니다. 

vgg_base = tf.keras.applications.VGG16(include_top=False, input_shape=(150, 150, 3))
vgg_base.summary()

tensorflow에서 제공하는 사전 학습된 VGG16 model을 불러오면, 위와 같은 구조의 network를 가진 모델을 얻을 수 있습니다.  자세한 구조는 아래와 같습니다. 

출처 : https://medium.com/@mjbhobe/cats-vs-dogs-kaggle-challenge-achieve-97-test-accuracy-3295636c77a6

Prediction Layers는 다시 학습하도록 제거한 model을 가지고 왔으며, pine-tuning까지 진행하도록 VGG16의 마지막 3개의 층은 학습을 하도록 설정하고, 나머지 VGG16 layer들은 동결시키고 학습을 진행하도록 하겠습니다.

trainable = False

for layer in vgg_base.layers:
    if layer.name == 'block5_conv2':
        trainable = True
    layer.trainable = trainable

print(vgg_base.summary())

마지막 block5_conv2, block5_conv3, block5_pool layer만 trainable이 True로 설정되어서 Non-trainable params가 증가한 것을 볼 수 있습니다.

 

그리고 Prediction layer를 추가해주고(첫번째 모델과 동일합니다.), Adam optimizer, binary_crossentropy loss로 compile하고 학습을 진행합니다. 학습은 총 150 epoch 반복 수행합니다.

(매우 오래 걸리기 때문에 저는 학습 진행시켜놓고.. 한숨 잤습니다)

vgg_model = tf.keras.models.Sequential([
	vgg_base,
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(1024, activation='relu'),
    tf.keras.layers.Dropout(0.2),
    tf.keras.layers.Dense(512, activation='relu'),
    tf.keras.layers.Dropout(0.1),
    tf.keras.layers.Dense(1, activation='sigmoid')
])

vgg_model.compile(optimizer=tf.keras.optimizers.Adam(1e-4),
                 loss='binary_crossentropy',
                 metrics=['acc'])
                 
history = vgg_model.fit_generator(train_generator,
                                 steps_per_epoch=train_step,
                                 epochs=150,
                                 validation_data=valid_generator,
                                 validation_steps=valid_step)

마지막 5 epoch의 결과가 위와 같습니다. 조금 overfitting의 느낌이 있긴하지만, 꽤 좋은 결과를 나타내는 것 같습니다.

이제 모델을 저장하고, test set으로 평가해보도록 하죠.

vgg_model.save('vgg_model_epoch_150.h')

test_loss, test_acc = vgg_model.evaluate_generator(test_generator,
                                               steps=test_step,
                                               workers=4)

print(f'test loss : {test_loss:.4f} / test acc : {test_acc*100:.2f} %')

약 95%의 test acc를 얻었습니다.

 

(참고로 vgg_base는 모두 동결시키고, prediction 부분만 학습시키도록 했을 때에는 다음과 같은 결과를 얻었습니다.)

 

이제 test.zip에 있던 이미지들을 예측해서 kaggle에 제출해보도록 하겠습니다.

우선 평가할 데이터(test_dir에 있는 이미지파일들)를 ImageDataGenerator로 읽어야하는데, ImageDataGenerator의 from_flow_directory 메소드는 root path에 하위 폴더가 꼭 존재해야 합니다.

따라서, test_dir 경로 아래에 폴더를 하나 만들어주고, 이미지들을 모두 옮기는 작업이 우선되어야 합니다.

temp_dir = os.path.join(test_dir, 'test')
os.mkdir(temp_dir)

for file in os.listdir(test_dir):
    if file != 'test':
        src = os.path.join(test_dir, file)
        dst = os.path.join(temp_dir, file)
        shutil.move(src, dst)

그리고 ImageDataGenerator를 만들어줍니다. test_datagen은 우리가 학습 때 사용했던 generator 객체이며 어짜피 이미지 사이즈만 (150,150)으로 변경하기 때문에 재사용되었습니다. 주의해야할 점은 우리는 y_true를 알지 못하는 데이터에 대해서 generator를 생성하므로, class_mode=None, shuffle=False 로 파라미터를 설정해주어야 합니다.

※결과 순서가 1부터 12500으로 순서대로 읽는지 확인을 해야합니다.. ! 아래는 이미지가 id대로 정렬되지 않아서 결과 제출시에 높은 score가 나오고 있습니다 ㅠㅠ 자세한 내용은 pytorch로 수행한 아래결과를 참조하시기 바랍니다...ㅠ

2020/12/07 - [ML & DL/pytorch] - [Pytorch][Kaggle] Cats vs. Dogs Classification

 

eval_generator = test_datagen.flow_from_directory(test_dir,
                                                 target_size=(150,150),
                                                 batch_size=32,
                                                 class_mode=None,
                                                 shuffle=False)

그럼 Generator 생성은 완료했고, model의 predict를 사용해서 제출할 12500개의 이미지를 예측해보도록 하겠습니다. steps는 예측할 데이터의 갯수라고 보시면 됩니다.

pred = vgg_model.predict(eval_generator, steps=12500)

 

이제 제출할 파일을 생성해야하는데, 결과는 csv파일로 제출합니다. 아래와 같은 폼을 가져야하고 각 label의 값은 label이 dog(1)일 확률입니다. 따라서 pred를 후처리할 필요없이 그대로 사용하면 됩니다.

다만, 평가가 log loss로 이루어지기 때문에... 0%나 100%로 예측해버리면 loss가 엄청 커지게 됩니다.

따라서 np.clip 함수를 사용해서 min, max 값을 설정해줍니다.

pred = np.clip(pred, 0.02, 0.98)

편리하게 pandas를 사용해서 csv파일을 만들어보도록 하겠습니다.

id = np.array([[i for i in range(1, len(pred)+1)]], dtype=np.int32).reshape(-1,)

submission = pd.DataFrame()
submission['id'] = id
submission['label'] = pred
submission.head()

 

 

이제 csv 파일로 저장하고 제출해보도록 하겠습니다.

submission.to_csv('submission.csv', index=False)

to_csv로 저장을 해주고, 아래의 Late Submission을 클릭해서, 파일을 첨부해주고 제출을 하면 됩니다 !

 

1.92622의 score가 나왔네요....

리더보드에 상위권 점수를 보면 0.03점대의 score를 갖던데... 거의 99%의 정확도를 가져야될 것 같습니다.

 

참고로 min/max 값 처리를 하지 않으면, 아래와 같이 엄청 큰 score를 얻게 됩니다.(낮을수록 좋은거에요...)

추가로 min - 0.05, max - 0.95로 설정하니, 조금 더 작은 loss를 얻었습니다.

 

위에서 언급한대로 제출 이미지가 순서대로 정렬하지 않고 예측을 했기 때문에 score는 조금 이상합니다 ㅠㅠ 만약 따라서 진행할 시에는 제출용 이미지를 파일 순서대로 읽어서 예측하는지 확인해야합니다 !!

정상적인 결과는 pytorch로 수행한 아래 게시글을 참조하시길 바랍니다.. !

2020/12/07 - [ML & DL/pytorch] - [Pytorch][Kaggle] Cats vs. Dogs Classification

 

[Pytorch][Kaggle] Cats vs. Dogs Classification

(torch version : 1.7.0) 지난번 tensorflow에서 Cats vs. Dogs Classification에 이어서, Pytorch를 사용해서 해당 문제를 구현해보도록 하겠습니다. 2020/12/04 - [ML & DL/tensorflow] - [Tensorflow][Kaggle]..

junstar92.tistory.com

 

댓글