keras-yolo3 opensource package

2021. 1. 22. 15:06Computer vision ღ'ᴗ'ღ

728x90

※ 본 글은 <딥러닝 컴퓨터비전 완벽가이드> 강의 및 여러 자료를 참고하여 쓰여진 글입니다. ※

github: github.com/chaeyeongyoon/ComputerVision_Study

 

chaeyeongyoon/ComputerVision_Study

Contribute to chaeyeongyoon/ComputerVision_Study development by creating an account on GitHub.

github.com

 

코드와 관련된 것은 github에서만 다루려고 했는데 keras-yolo에 대한 것은 정리해두는게 좋을 것 같아 정리해봅니다.

 

keras에 yolo 공식 API가 없기 때문에 opensource package를 사용합니다.

github.com/qqwweee/keras-yolo3

 

qqwweee/keras-yolo3

A Keras implementation of YOLOv3 (Tensorflow backend) - qqwweee/keras-yolo3

github.com

다른 opensource package(github.com/experiencor/keras-yolo3)도 있지만 csv format의 데이터를 지원하기 때문에 이 opensource package를 선택하였습니다. 

기회가 된다면 다른 open source package도 정리해보겠습니다.

 

keras 기반으로 yolo opensource를 이용하면, 

  • 보다 쉽게 환경설정을 할 수 있다.
  • keras의 callbacks, Tensorboard, Preprocessing등의 다양한 기능 활용
  • source code가 보다 이해하기 쉽고 customization도 비교적 쉽게 가능
  • Darknet C/C++로 구현된 original yolo package보다는 속도가 느립니다.
  • 이 opensource 에서는 이미지 처리에 PIL을 사용합니다(치명적 단점...)

일단 위의 github을 clone해줍니다.

keras-yolo3은 setup을 제공하지 않아 local directory 상에서 바로 package를 import 해야합니다.

이를 위해 clone해준 keras-yolo3폴더를 system path에 추가해줍니다.

keras-yolo3 directory의 yolo.py의 YOLO class를 import해 inference에 사용합니다.

weigths 파일은 darknet 홈페이지에서 다운받은 후 keras-yolo3용으로 변환해주는데, convert.py를 수행하여 변환합니다.

 

!python convert.py yolov3.cfg yolov3.weights model_data/yolo.h5

LOCAL_PACKAGE_DIR = os.path.abspath("./keras-yolo3")
sys.path.append(LOCAL_PACKAGE_DIR)

from yolo import YOLO

from PIL import Image

yolo = YOLO(model_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/yolo.h5',
            anchors_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/yolo_anchors.txt',
            classes_path='~/DLCV/Detection/yolo/keras-yolo3/model_data/coco_classes.txt')
            
img = Image.open(os.path.join('../../data/image/beatles01.jpg'))
detected_img = yolo.detect_image(img)

inference 는 이와같이 비교적 간단합니다.

다음은 training 하는 방법에 대해 알아보겠습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

● Train 

github.com/experiencor/raccoon_dataset  

 

experiencor/raccoon_dataset

The dataset is used to train my own raccoon detector and I blogged about it on Medium - experiencor/raccoon_dataset

github.com

이 데이터 셋을 사용하였습니다.

<colab으로 실습>

raccoon data set을 다운 받으면 다음과 같은 구조를 가집니다.

 

 

 

 

 

 

 

 

 

 

 

 

annotation폴더에는 다음과 같은 xml파일들이 있는데, 사진과 같은 이름으로 매칭되어 있습니다. 

yolo로 학습시켜 주기 위해서는 xml파일들을 csv파일로 변환시켜주어야합니다.

import glob
import xml.etree.ElementTree as ET

# conver xml file to csv file
def xml_to_csv(path, output_filename):
  xml_list = []
  # all absolute path of xml files 
  with open(output_filename, "w") as train_csv_file:
    for xml_file in glob.glob(path + '/*.xml'):
      # parsing xml files and create Element Tree (XML Element)
      # object informations
      tree = ET.parse(xml_file)
      root = tree.getroot()
      # fine all object Element in file
      full_image_name = os.path.join(IMAGE_DIR, root.find('filename').text)
      value_str_list = ' '
      for obj in root.findall('object'):
        xmlbox = obj.find('bndbox')
        x1 = int(xmlbox.find('xmin').text)
        y1 = int(xmlbox.find('ymin').text)
        x2 = int(xmlbox.find('xmax').text)
        y2 = int(xmlbox.find('ymax').text)
        class_id = 0 # just one id for 'raccoon'
        value_str = (f'{x1},{y1},{x2},{y2},{class_id}')
        value_str_list = value_str_list + value_str + ' '
      train_csv_file.write(full_image_name + ' ' + value_str_list + '\n')

csv파일은 다음과 같이 만들어집니다. 이미지 절대경로 (띄어쓰기) xmin,ymin,xmax,ymax,class id(띄어쓰기 없음을 유의)

raccoon_anno.csv

import numpy as np
import keras.backend as K
from keras.layers import Input, Lambda
from keras.models import Model
from keras.optimizers import Adam
from keras.callbacks import TensorBoard, ModelCheckpoint, ReduceLROnPlateau, EarlyStopping
import sys
import os

default_dir = '/content'
default_yolo_dir = os.path.join(default_dir, 'yolo')

LOCAL_PACKAGE_DIR = os.path.abspath(os.path.join(default_yolo_dir, 'keras-yolo3'))
print(LOCAL_PACKAGE_DIR)
# append keras-yolo path to system path
sys.path.append(LOCAL_PACKAGE_DIR)

from yolo3.model import preprocess_true_boxes, yolo_body, tiny_yolo_body, yolo_loss
from yolo3.utils import get_random_data

inference시와 같이 systempath에 yolo경로를 추가해주고

학습에 필요한 package들을 import해줍니다.

!wget https://pjreddie.com/media/files/yolov3.weights

!python convert.py yolov3.cfg yolov3.weights model_data/yolo.h5

어김없이 공식페이지에서 다운받은 weights를 .h5파일로 convert하고 font폴더를 상위폴더로 옮겨줍니다(코드 실행장소와 동일하게 만들어줌)

 

여기서부터 본격적인 training을 위한 코드입니다.

# modify raccoon_class.txt
BASE_DIR = '/content/yolo/keras-yolo3'
classes_path = os.path.join(BASE_DIR, 'model_data/raccoon_class.txt')
with open(classes_path, 'w') as f:
  f.write('raccoon')

!cat /content/yolo/keras-yolo3/model_data/raccoon_class.txt

raccoon에 대해서만 detection할 것이기 때문에 class텍스트 파일을 새로 만들어줍니다.

이후 keras-yolo패키지의 train.py에서 몇몇 함수를 불러와 학습을 진행해주겠습니다.

from train import get_classes, get_anchors
from train import create_model, create_tiny_model, data_generator, data_generator_wrapper

# settings for training
# annotation files path, model file path, object class file path, anchor file path
annotation_path = os.path.join(ANNO_DIR, 'raccoon_anno.csv')
log_dir = os.path.join(BASE_DIR, 'snapshots/000/')
classes_path = os.path.join(BASE_DIR, 'model_data/raccoon_class.txt')
anchors_path = os.path.join(BASE_DIR,'model_data/yolo_anchors.txt')

class_names = get_classes(classes_path)
num_classes = len(class_names)
anchors = get_anchors(anchors_path)

model_weigths_path = os.path.join(BASE_DIR, 'model_data/yolo.h5')

input_shape = (416, 416)

# tiny yolo has 6 anchors
is_tiny_version = len(anchors)==6
if is_tiny_version:
  model = create_tiny_model(input_shape, anchors, num_classes, freeze_body=2, weights_path=model_weigths_path)
else:
  model = create_model(input_shape, anchors, num_classes, freeze_body=2, weights_path=model_weigths_path)

# callback
logging = TensorBoard(log_dir=log_dir)
checkpoint = ModelCheckpoint(log_dir + 'ep{epoch:03d}-loss{loss:.3f}-val_loss{val_loss:.3f}.h5',
                             monitor='val_loss', save_weights_only=True, save_best_only=True, period=3)
reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1)
early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1)

val_split = 0.1
with open(annotation_path) as f:
  lines = f.readlines()
print(lines)

np.random.seed(10101)
np.random.shuffle(lines)
np.random.seed(None)
num_val = int(len(lines)*val_split)
num_train = len(lines) - num_val

if True:
  model.compile(optimizer=Adam(lr=1e-3), loss={'yolo_loss':lambda y_true, y_pred: y_pred})
  batch_size = 4
  print(f'train on {num_train} samples, val on {num_val} samples, with batch size {batch_size}')

  model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
            steps_per_epoch=max(1, num_train//batch_size),
            validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes),
            validation_steps=max(1, num_val//batch_size),
            epochs=50,
            initial_epoch=0,
            callbacks=[logging, checkpoint])
  model.save_weights(log_dir + 'trained_weights_stage_1.h5')

# Unfreeze and continue training, to fine-tune.
# Train longer if the result is not good.
if True:
    for i in range(len(model.layers)):
        model.layers[i].trainable = True
    model.compile(optimizer=Adam(lr=1e-4), loss={'yolo_loss': lambda y_true, y_pred: y_pred}) # recompile to apply the change
    print('Unfreeze all of the layers.')

    batch_size = 4 # note that more GPU memory is required after unfreezing the body
    print('Train on {} samples, val on {} samples, with batch size {}.'.format(num_train, num_val, batch_size))
    
    model.fit_generator(data_generator_wrapper(lines[:num_train], batch_size, input_shape, anchors, num_classes),
        steps_per_epoch=max(1, num_train//batch_size),
        validation_data=data_generator_wrapper(lines[num_train:], batch_size, input_shape, anchors, num_classes),
        validation_steps=max(1, num_val//batch_size),
        epochs=100,
        initial_epoch=50,
        callbacks=[logging, checkpoint, reduce_lr, early_stopping])
    model.save_weights(log_dir + 'trained_weights_final.h5')

get_classes와 get_anchors함수로 class와 anchor box정보를 가져왔습니다. (list로써 불러옵니다)
tiny_yolo는 anchor box 개수가 6개인 점을 이용해 tiny_yolo모델을 만들지 일반 yolo모델을 만들지 결정해줍니다.

keras의 callback 함수를 4가지 생성해주었습니다.
logging: TensorBoard확인-현재 경로로 이동하고 tensorboard --logdir=logs(로그가 저장되어 있는 폴더명) 으로 실행하여 tensorboard를 확인할 수 있습니다. (생략해도 괜찮습니다)
checkpoint: 지정된 경로로 weights를 중간저장해줍니다. 여기서는 val_loss값이 개선되었을 때 중간저장을 해줍니다.
reduce_lr: local minimum 에 빠지지 않도록 Learning rate를 조정하여 주는 효과를 기대할 수 있습니다.
early_stopping: 지정한 epoch수 도달 전에 (여기서는 val_loss가 10epochs동안 더 이상 줄어들지 않을때 학습을 중단해줍니다.)

 

이후 np.random을 통해 csv로 저장되어있는 data들을 불러와 shuffle해줍니다. 데이터의 순서를 섞어주는 작업, 학습의 성능을 높이기 위해서 하는 부분입니다.  사실 이 dataset에선 큰 의미가 없습니다.

그리고 두번의 반복문으로 학습을 실행해주는데, 첫번째는 위의 두 layer만 학습을 시켜주고(model생성시에 설정해주었음), 두번째는 모든 layer들을 unfreeze(trainable)하게 만든다음 모든 layer를 학습시킵니다.
이렇게 두 단계로 학습시키는 것이 효율이 더 좋기 때문입니다.(find-tuning)

학습시에는 keras의 fit_generator를 사용하는데, keras-yolo 패키지 내의 data_generator_wrapper를 이용해 인자를 넣어줍니다. data_generator_wrapper의 코드를 살펴보면(keras-yolo github참고) annotation의 정보가 없거나, batchsize가 음수나 0인 경우를 처리해준 후 data_generator함수를 불러냅니다. data_generator함수가 fit_generator에 맞는 인자를 생성해줍니다.

이후 중간저장된 weights파일을 확인해주고, 앞에서의 inference방법과 동일하게 detection을 수행해줍니다.

728x90