[Keras, CoreML] CNN 이미지 분류 알고리즘으로 카메라 이미지 인식하기

스터디하면서 진행한 내용을 아래 목차대로 포스팅 해보겠습니다.

  1. 케라스 설치 및 환경설정하기
  2. 컨셉 정의
  3. 학습 데이터 수집, 전처리, 분류
  4. 케라스 학습 코드 작성
  5. 학습 및 최적화 과정
  6. CoreML 모델 뽑기
  7. IOS 코드에 적용하기(XCode)
  8. 테스트, 평가
2. 컨셉 정의

– 관광 명소를 이미지로 분류해보자
– 일단 명소, 건축물을 가리지 않고 분류하도록 해보자
– 난 초보니까 애매하게 비슷한 것들은 섞지 않겠어.
– 그래서 에펠탑, 자유의 여신상, 나이아가라 폭포, 콜로세움, 피라미드 5개로 정함

3. 학습 데이터 수집, 전처리, 분류

일단 icrawler 설치해주자. (urllib3은 SSL우회때문에 넣었다. 빼도 무관)

pip install icrawler
pip install urllib3

아래는 간단한 이미지 크롤링 코드
data_orign폴더를 만들어주고, 그안에 키워드별로 쌓도록 했다.

from icrawler.builtin import GoogleImageCrawler

import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

attractionList = ['tower eiffel', 'statue of liberty', 'niagara falls', 'colosseum', 'pyramid']
attractionFolderList = ['eiffel', 'liberty', 'niagara', 'colosseum', 'pyramid']

for idx, val in enumerate(attractionList):
    google_crawler = GoogleImageCrawler(
        feeder_threads=10,
        parser_threads=10,
        downloader_threads=10,
        storage={'root_dir': 'data_orign/'+attractionFolderList[idx]})
    google_crawler.session.verify = False
    filters = dict(type='photo') #사진만
    # 키워드로 돌면서 1000장 크롤링
    google_crawler.crawl(keyword=val, filters=filters, max_num=1000, file_idx_offset=0)

대략 400장~500장 정도 뽑혔다.
이제 같지도 않은 이상한 이미지들을 걸러줘야한다. (사람의 손으로)
아래와 같은 기준을 적용했다.
– 배경이 아닌 피사체 중심인 사진이어야 한다.
– 이상한 각도에서 찍어서 딱봤을때 모르겠는건 다 삭제
– 일부만 나오지 않을 것.
– 복잡한 구도가 아니어야한다.

빠르게 필터링을 거치니 평균 150장 정도의 이미지가 남았다.

전처리

전처리 코드 작성을 위해 필요한 pillow 라이브러리를 일단 설치

pip install image

폴더에서 데이터를 읽어서 일관된 224x224xRGB 형식으로 치환하여 jpg확장자에 맞춰 data폴더에 저장하도록 했다.
rorate한 이미지까지 준비를 했으나, 관광명소를 뒤집어 찍을일은 없으니 사용할 일은 그닥 없을 것 같다.
이미지가 정방형으로 잘리기때문에 잘린 뒤 형태를 알아보기 힘든 이미지가 나올 수 있다.
그만큼 Fit정책은 중요한 것으로 보이며, 변환 중 일부 읽을 수 없는 이미지가 걸러지는 것을 확인할 수 있다.

from PIL import Image, ImageOps
import os,sys
import glob

attractionList = ['tower eiffel', 'statue of liberty', 'niagara falls', 'colosseum', 'pyramid']
attractionFolderList = ['eiffel', 'liberty', 'niagara', 'colosseum', 'pyramid']
for attractionFolder in attractionFolderList:
    image_dir = "./data_orign/"+attractionFolder+"/"
    target_resize_dir = "./data/"+attractionFolder+"/"
    target_rotate_dir = "./data_rotate/"+attractionFolder+"/"
    if not os.path.isdir(target_resize_dir):
        os.makedirs(target_resize_dir)
    if not os.path.isdir(target_rotate_dir):
        os.makedirs(target_rotate_dir)
    files = glob.glob(image_dir+"*.*")
    print(len(files))
    count = 1;
    size = (224, 224)
    for file in files:
        im = Image.open(file)
        im = im.convert('RGB')
        print("i: ", count, im.format, im.size, im.mode, file.split("/")[-1])
        count+=1
        im = ImageOps.fit(im, size, Image.ANTIALIAS, 0, (0.5, 0.5))
        im.save(target_resize_dir+file.split("/")[-1].split(".")[0]+".jpg", quality=100)
        im.rotate(90).save(target_rotate_dir+"resize_"+file.split("/")[-1].split(".")[0]+".jpg", quality=100)

 

 

4. 케라스 학습 코드 작성

여차저차 Keras를 설치하고,, (환경 설치에 맘고생하지 말고 이왕이면 FloydHub 혹은 Azure Notebook과 같은 서비스를 이용하는게 더 좋습니다)
[딥러닝] FloydHub 플랫폼에서 Tensorflow, Keras 학습하기

아래와 같이 학습 코드를 작성하였다.
(여기서 Tensorflow로 학습하지 않는 이유는 케라스가 CoreML컨버팅이 더 쉽기 때문입니다)

ImageDataGenerator를 사용하여 로테이션, 플립, 크롭 등 테스트 데이터를 증분
또한 validation_split을 통해 validation데이터가 셔플하여 자동으로 30%정도 나누도록 하였다.

train_datagen = ImageDataGenerator(rescale=1./255, 
                                   rotation_range=10,
                                   width_shift_range=0.2,
                                   height_shift_range=0.2,
                                   shear_range=0.7,
                                   zoom_range=[0.9, 2.2],
                                   horizontal_flip=True,
                                   vertical_flip=True,
                                   fill_mode='nearest',
                                   validation_split=0.33)


training_set = train_datagen.flow_from_directory('/floyd/input/data',
                                                 shuffle=True, 
                                                 seed=13,
                                                 target_size = (224, 224),
                                                 batch_size = 15,
                                                 class_mode = 'categorical',
                                                 subset="training")
validation_set = train_datagen.flow_from_directory('/floyd/input/data',
                                                 shuffle=True, 
                                                 seed=13,
                                                 target_size = (224, 224),
                                                 batch_size = 10,
                                                 class_mode = 'categorical',
                                                 subset="validation")

주피터 노트북이 껐다 키면 진행상황이 보이지 않기때문에 fit_generator에 로깅 콜백을 추가.

from keras.callbacks import CSVLogger

csv_logger = CSVLogger('./log.csv', append=True, separator=';')

hist = model.fit_generator(training_set,
                         steps_per_epoch = 20,
                         epochs = 1000,
                         validation_data = validation_set,
                         validation_steps = 10,
                         callbacks=[csv_logger])

생성된 모델을 잘 세이브

from keras.models import load_model

model.save('cnn_attraction_keras_model.h5')

 

아래는 풀코드

# Importing the Keras libraries and packages
from keras.models import Sequential
from keras.layers import Conv2D
from keras.layers import MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras.callbacks import ModelCheckpoint
from keras.layers.normalization import BatchNormalization
import numpy as np


# Initialising the CNN
# classifier = Sequential()
model = Sequential()

# Step 1 - Convolution
# classifier.add(Conv2D(16, (3, 3), padding='same', activation='relu', input_shape=(224, 224, 3)))
# classifier.add(Conv2D(16, (3, 3), activation='relu'))
# classifier.add(MaxPooling2D(pool_size=(2, 2)))
# classifier.add(Dropout(0.25))

# classifier.add(Conv2D(64, (3, 3), padding='same', activation='relu'))
# classifier.add(Conv2D(64, (3, 3), activation='relu'))
# classifier.add(MaxPooling2D(pool_size=(2, 2)))
# classifier.add(Dropout(0.25))

# classifier.add(Conv2D(32, (3, 3), padding='same', activation='relu'))
# classifier.add(Conv2D(32, (3, 3), activation='relu'))
# classifier.add(MaxPooling2D(pool_size=(2, 2)))
# classifier.add(Dropout(0.25))

# classifier.add(Flatten())
# classifier.add(Dense(512, activation='relu'))
# classifier.add(Dropout(0.5))
# classifier.add(Dense(5, activation='softmax'))

model.add(Conv2D(16, (3, 3), padding='same', use_bias=False, input_shape=(224, 224, 3)))
model.add(BatchNormalization(axis=3, scale=False))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(4, 4), strides=(4, 4), padding='same'))
model.add(Dropout(0.2))

model.add(Conv2D(32, (3, 3), padding='same', use_bias=False))
model.add(BatchNormalization(axis=3, scale=False))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(4, 4), strides=(4, 4), padding='same'))
model.add(Dropout(0.2))

model.add(Conv2D(64, (3, 3), padding='same', use_bias=False))
model.add(BatchNormalization(axis=3, scale=False))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(4, 4), strides=(4, 4), padding='same'))
model.add(Dropout(0.2))

model.add(Conv2D(128, (3, 3), padding='same', use_bias=False))
model.add(BatchNormalization(axis=3, scale=False))
model.add(Activation("relu"))
model.add(Flatten())
model.add(Dropout(0.2))

model.add(Dense(512, activation='relu'))
model.add(Dense(5, activation='softmax'))
model.summary()

# Compiling the CNN
model.compile(optimizer = 'adam', loss = 'categorical_crossentropy', metrics = ['accuracy'])


# Part 2 - Fitting the CNN to the images
from keras.preprocessing.image import ImageDataGenerator

# train_datagen = ImageDataGenerator(rescale = 1./255)

# 데이터셋 불러오기
train_datagen = ImageDataGenerator(rescale=1./255,
                                   rotation_range=10,
                                   width_shift_range=0.2,
                                   height_shift_range=0.2,
                                   shear_range=0.7,
                                   zoom_range=[0.9, 2.2],
                                   horizontal_flip=True,
                                   vertical_flip=True,
                                   fill_mode='nearest',
                                   validation_split=0.33)

training_set = train_datagen.flow_from_directory('/floyd/input/data',
                                                 shuffle=True,
                                                 seed=13,
                                                 target_size = (224, 224),
                                                 batch_size = 15,
                                                 class_mode = 'categorical',
                                                 subset="training")
validation_set = train_datagen.flow_from_directory('/floyd/input/data',
                                                 shuffle=True,
                                                 seed=13,
                                                 target_size = (224, 224),
                                                 batch_size = 10,
                                                 class_mode = 'categorical',
                                                 subset="validation")

from keras.callbacks import CSVLogger

csv_logger = CSVLogger('./log.csv', append=True, separator=';')

hist = model.fit_generator(training_set,
                         steps_per_epoch = 20,
                         epochs = 1000,
                         validation_data = validation_set,
                         validation_steps = 10,
                         callbacks=[csv_logger])


from keras.models import load_model

model.save('cnn_attraction_keras_model.h5')

# output = classifier.predict_generator(test_set, steps=5)
# print(test_set.class_indices)
# print(output)

# 모델 평가하기
print("-- Evaluate --")

scores = model.evaluate_generator(
            validation_set,
            steps = 10)

print("%s: %.2f%%" %(model.metrics_names[1], scores[1]*100))

# 모델 예측하기
print("-- Predict --")

output = model.predict_generator(
            validation_set,
            steps = 10)
print(validation_set.class_indices)
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})

print(output)
print(validation_set.filenames)
5. 학습 및 최적화 과정

floydhub에서 신용카드 등록하면 주는 무료GPU 크레딧을 사용해 학습했는데, 역시나 CPU보다 매우 빠른시간에
학습이 되었다.
배치사이즈나 epoch 수치를 경험적으로 잘~ 정해야 학습이 잘된다..
하드웨어 성능이 좋으니까 1000에포크 정도 학습을 시켰다.

아래 코드를 돌려 학습이 잘되었는지 확인하자.

from keras.models import load_model
model = load_model('./cnn_attraction_keras_model.h5')

# 모델 평가하기
print("-- Evaluate --")

scores = model.evaluate_generator(
            validation_set,
            steps = 10)

print("%s: %.2f%%" %(model.metrics_names[1], scores[1]*100))

# 모델 예측하기
print("-- Predict --")

output = model.predict_generator(
            validation_set,
            steps = 10)
# print(output)
# print(validation_set.class_indices)
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})

# print(validation_set.filenames)


# 5. 학습과정 살펴보기
import matplotlib.pyplot as plt

fig, loss_ax = plt.subplots()

acc_ax = loss_ax.twinx()

loss_ax.plot(hist.history['loss'], 'y', label='train loss')
loss_ax.plot(hist.history['val_loss'], 'r', label='val loss')
#loss_ax.set_ylim([0.0, 0.5])

acc_ax.plot(hist.history['acc'], 'b', label='train acc')
acc_ax.plot(hist.history['val_acc'], 'g', label='val acc')
#acc_ax.set_ylim([0.8, 1.0])

loss_ax.set_xlabel('epoch')
loss_ax.set_ylabel('loss')
acc_ax.set_ylabel('accuray')

loss_ax.legend(loc='upper left')
acc_ax.legend(loc='lower left')

plt.show()

이 정도면,, 꽤나 성공적??
학습이 잘 안되었으면, 배치 사이즈를 변경하여 학습을 달리해보려했으나, 일단 패스.

6. Core ML 모델 뽑기

사실 tensorflow로 학습을 진행했으나, coreML로 변환하는게 어렵고 잘 모르겠어서 Keras로 바꾸었다..-ㅇ-;;
coremltool을 설치하고.

pip install coremltools

그런데 아까 모델 생성할때 어떤 라벨순서로 class output이 만들어지는지 확인을 안했다.
label map을 다시 좀 출력해봐야겠다.

from keras.models import load_model
model = load_model('./cnn_attraction_keras_model.h5')

from keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator(rescale=1./255, 
                                   rotation_range=10,
                                   width_shift_range=0.2,
                                   height_shift_range=0.2,
                                   shear_range=0.7,
                                   zoom_range=[0.9, 2.2],
                                   horizontal_flip=True,
                                   vertical_flip=True,
                                   fill_mode='nearest',
                                   validation_split=0.33)

training_set = train_datagen.flow_from_directory('/floyd/input/data',
                                                 shuffle=True, 
                                                 seed=13,
                                                 target_size = (224, 224),
                                                 batch_size = 15,
                                                 class_mode = 'categorical',
                                                 subset="training")
label_map = (training_set.class_indices)
print(label_map)
             

Keras -> CoreML 컨버팅은 아래 코드처럼 무지 쉽다.
확인된 라벨을 아래 class_labels에 맞춰서 넣어주고,, 돌린다.
처음에 파라미터에 ‘image’ 넣는걸 몰라서 찾느라 좀 헤맸다.
나중에 IOS코드 짤때 input을 Image형식으로 받는 모델로 만들어준다.

from keras.models import load_model
import coremltools
model = load_model('./cnn_attraction_keras_model.h5')
# Saving the Core ML model to a file.
class_labels = ['colosseum', 'eiffel', 'liberty', 'niagara', 'pyramid']
coreml_model = coremltools.converters.keras.convert(model, input_names='image', image_input_names='image', class_labels=class_labels, is_bgr=True)  
coreml_model.save('./cnn_attraction_model.mlmodel')

파일이 잘 생성되었네!

7. IOS코드에 적용하기

이제 IOS Swift코드를 짜줄건데,, 
공식 레퍼런스에서 예제코드를 받아다가 내가 만든 모델을 사용하도록 살짝 수정을 할거다.
https://developer.apple.com/documentation/vision/classifying_images_with_vision_and_core_ml

프로젝트 받고 > 압축풀어서 >  Xcode로 열어본다.

이런 프로젝트 구조에서 아까 만들었던, mlmodel을 넣는 것부터 시작.

모델 파일을 누르면 아래와 같이 정보가 뜨는데, 아까 모델 컨버팅할때 image로 적었기 때문에
저 inputs부분이 RGB Image로 셋팅이 된거다. (복잡하게 타입 맞춰줄 필요가 없다)
output1은 얼마나 같은지 수치가 나올거고
classLabel은 위에서 뽑아봤던 label map이 들어있겠지…

ImageClassificationViewController.swift 파일에서 주석된 곳 수정

    /// - Tag: MLModelSetup
    lazy var classificationRequest: VNCoreMLRequest = {
        do {
            /*
             Use the Swift class `MobileNet` Core ML generates from the model.
             To use a different Core ML classifier model, add it to the project
             and replace `MobileNet` with that model's generated Swift class.
             */
            
//            let model = try VNCoreMLModel(for: MobileNet().model)
              // 여기 내가 만든 모델로 수정하기
              let model = try VNCoreMLModel(for: cnn_attraction_model().model)
            
            let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
                self?.processClassifications(for: request, error: error)
            })
            request.imageCropAndScaleOption = .centerCrop
            return request
        } catch {
            fatalError("Failed to load Vision ML model: \(error)")
        }
    }()

끝, 이제 애뮬레이터에 올리든 폰에 올려보든 실행해보자. 쉽네

8. 테스트, 평가

뭔가 잘해버렸다.

그러나,, 이상한게..
하나의 결과에 100%확신을 때려버리고, 틀린 답도 계속 나온다…

학습이 잘못된건지,, 적용이 잘못된건지,, 다시 한번 확인이 필요할 것 같다..

모든 소스 및 파일 구조는 아래 github에 공개되어 있습니다.
https://gitlab.com/htrucci/attraction_cnn_classification

aaa

You may also like...

2 Responses

  1. 감사 댓글:

    https://gitlab.com/htrucci/attraction_cnn_classification

    에서
    cnn_attraction_keras.py
    cnn_attraction_keras_classification.py
    cnn_attraction_keras_evaluate.py
    cnn_attraction_model.mlmodel
    convert_keras_mlcore.py

    파일들이 있는데, 위의 예제는 어느 것인가요?

    • htrucci 댓글:

      모두 사용하긴하나 모델 학습코드를 말씀하시는 거라면 아래 두 파일입니다. 비교해보시면 거의 동일하고 튜닝인자등 일부 값이 다른 내용입니다.
      cnn_attraction_keras.py
      cnn_attraction_keras_classification.py

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.