Yoloを用いたオリジナルデータでの物体検知モデルの作成手順

# はじめに
物体検知について学び、yoloを用いてモデル作成を行なったので、備忘録もかねて残しておこうと思う。
今回のyoloのモデルはKerasを用いて作成しています。

# データの準備・前処理
まず、画像から検出したいものをアノテーションし、学習を行うのに必要なアノテーションファイルを作成する必要があります。
精度の高いモデルを作成するためには、アノテーションをする前に、アノテーションする画像の大きさをリサイズして大きさを揃えておくことが重要となります。
今回、画像のサイズは192×192を想定しているので、以下のようなコードを用いて、全ての画像をリサイズして保存しておきます。

```py
import glob
from PIL import Image

image_paths = glob.glob('./画像データディレクトリ/*')
for image_path in image_paths:
    # 画像のリサイズ
    img = Image.open(image_path)
    img_resize = img.resize((192, 192))
    img_name = './リサイズした画像を保存するディレクトリ/' + image_path.split('/')[-1]
    img_resize.save(img_name)
```

その後、リサイズした画像のアノテーションを行います。
アノテーションには、[labelImg](https://github.com/tzutalin/labelImg)を用います。
こちらのツールは、面倒なアノテーション作業を多少なりとも楽にしてくれるもので、
アノテーションファイルを出力してくれます。
使い方は
[Yolo学習用データセットの作成ツール：labelImg](https://demura.net/misc/14350.html)
など、調べればたくさん出てくるので割愛します。
こちらのツールでアノテーションに関する情報が詰まった`annotation.txt`が得られます。

次に、アノテーションの時に用いたラベルを以下のように`classes.txt`というテキストファイルに保存します。

（human,car,cat,dogの４つをラベルにした場合の`classes.txt`の例）

```classes.txt
human
car
cat
dog
```


## データが少ないのであればオーグメンテーション（画像の水増し）
もしも画像が極端に少ないなどの問題がある場合は画像の水増しをする必要があります。
画像の水増しには、あらゆる手法がありますが、多くの場合、[Kerasのライブラリ](https://keras.io/ja/preprocessing/image/)を用いると解決できます。
また、使い方は以下のサイトを参考にすると簡単にわかると思うので、割愛します。
* [Keras - Keras の ImageDataGenerator を使って学習画像を増やす](https://pynote.hatenablog.com/entry/keras-image-data-generator)
* [Kerasでデータ拡張(Data Augmentation)後の画像を表示する](https://qiita.com/takurooo/items/c06365dd43914c253240) 

(追記)
オーグメンテーションについての記事を書きましたので、こちらを参考にしていただければと思います！
[ImageDataGenaratorを用いた画像の水増し](https://qiita.com/yassyyuki/items/f1ecddea04ca728c765c)

# 実装
今回は[こちら](https://github.com/qqwweee/keras-yolo3)のソースコードを用いてモデル作成を行いました。
この中で僕らが変更を加えるべきファイルは`train.py`です。
`train.py`の中の`main()`の部分を編集する必要があります。

```py:train.py
def _main():
    annotation_path = 'annotatino.txtのパス'  # ここ！！！！！！
    log_dir = 'logs/000/'
    classes_path = 'classes.txtのパス'  # ここ！！！！！！
    anchors_path = 'model_data/yolo_anchors.txt'
    class_names = get_classes(classes_path)
    num_classes = len(class_names)
    anchors = get_anchors(anchors_path)

    input_shape = (192, 192) # multiple of 32, hw  # 画像データを192×192にリサイズした理由


    is_tiny_version = len(anchors)==6 # default setting
    if is_tiny_version:
        model = create_tiny_model(input_shape, anchors, num_classes,
            freeze_body=2, weights_path='model_data/tiny_yolo_weights.h5')
    else:
        model = create_model(input_shape, anchors, num_classes,
            freeze_body=2, weights_path='model_data/yolo.h5') # make sure you know what you freeze

    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=20)
    reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=3, verbose=1)
    early_stopping = EarlyStopping(monitor='val_loss', min_delta=0, patience=5, verbose=1)

    val_split = 0.2  # ここも変更可能！！！
    with open(annotation_path) as f:
        lines = f.readlines()
    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

    # Train with frozen layers first, to get a stable loss.
    # Adjust num epochs to your dataset. This step is enough to obtain a not bad model.
    if True:
        model.compile(optimizer=Adam(lr=1e-3), loss={
            # use custom yolo_loss Lambda layer.
            'yolo_loss': lambda y_true, y_pred: y_pred})

        batch_size = 128  # ここも変更可能！！！！
        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=10,
                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=0,
            callbacks=[logging, checkpoint, reduce_lr, early_stopping])
        model.save_weights(log_dir + 'trained_weights_final.h5')

```
また、`train.py`の中の`create_model()`の部分の`load_pretrained=False`を`True`に変えると、学習済みモデルを用いて学習をすることができます。ただ、オリジナルモデルを作成するにあたっては、学習済みモデルを使うことで精度が増すとは限りません。
適宜ハイパーパラメータチューニングをして調べて下さい。

# 学習
オリジナルモデル作成の準備が整ったので、学習を開始します。
学習をするには、以下のコマンドを実行するのみです。

```
$ python train.py
```
学習が終わると、`logs/000/trained_weights_final.h5`というファイルが作成されます。

# テスト
学習が終わったらテストを行います。
テストは、作成したモデルに画像を入力して、物体検知が行われ、
バウンディングボックスが表示された画像が出力されます。
検知精度はその画像を目視で確認することになります。
テストの実行方法は、以下のコマンドを実行し、その後に入力したい画像のパスを入力すると可能です。

```
$ python yolo_video.py --image
Input image filename: 入力したい画像のpath
```
`yolo_video.py`の中身を編集すればいちいち画像のpathを入力しなくてもテストが可能になりますので、少し書き換えても良いと思います。

以上が一通りの流れとなります。
物体検知のオリジナルモデルを作成するには、なんといってもアノテーション作業が大変です。
大量のデータが必要なのにも関わらず、アノテーション作業は手作業で行わなければなりません。
根気強く頑張りましょう。

# さいごに
今回はkerasで実装されたyoloを用いて、オリジナルの物体検知モデルを作成する手順を書きました。
少しでも誰かの役に立てればと思います。
また、間違いの指摘やもっと良い手法の提案、疑問など、なんでも良いのでコメントいただけると嬉しいです。
最後まで読んでいただき、ありがとうございました。