Google ColabでYOLOの学習を行う

[前回](https://qiita.com/shoji9x9/items/74ee46d3b39b16220f16)の続きです。予告通りGoogle ColabでYOLOの学習を行いたいと思います。かなり苦労しました・・・。

# 実行環境
* Google Colaboratory
* YOLOv3

# コード
## 準備
前回同様まずはDarknetのダウンロード

```Python
!git clone https://github.com/pjreddie/darknet
```

前回と違うのはGPUを利用したいので、Makefileを書き換える必要があります。Google Colabのノートブックの設定変更（GPU利用できるように）も忘れずに。

```Python
%cd darknet
!sed -i 's/GPU=0/GPU=1/g' Makefile
!make
```

次にデータセットを取得します。自分でも作れるようですが大変なので、今回は世の中の資産を活用します。PASCAL VOCというものを使うことにしました。なぜPASCAL VOCかと言うと[公式](https://pjreddie.com/darknet/yolo/)に**Training YOLO on VOC**としてここから先の手順が全部載ってて、なぞるだけで実装できるからです（COCOの手順も載っています）。ということで、データセットをダウンロードします。

```Python
!mkdir VOCDataset
%cd VOCDataset
!wget https://pjreddie.com/media/files/VOCtrainval_11-May-2012.tar
!wget https://pjreddie.com/media/files/VOCtrainval_06-Nov-2007.tar
!wget https://pjreddie.com/media/files/VOCtest_06-Nov-2007.tar
!tar xf VOCtrainval_11-May-2012.tar
!tar xf VOCtrainval_06-Nov-2007.tar
!tar xf VOCtest_06-Nov-2007.tar
```

次にDarknet用のLabelファイルを作る必要がありますが、自分で作る必要はありません。加工ツール（voc_label.py）が提供されています。ただ、実行権限がなかったため、先に権限の変更が必要でした。

```Python
!chmod 755 ../scripts/voc_label.py
!python ../scripts/voc_label.py
```

この処理が終わると以下のようなファイルが作成されます。

```Python
!pwd
!ls

/content/darknet/VOCDataset
2007_test.txt	2012_val.txt   VOCtest_06-Nov-2007.tar
2007_train.txt	train.all.txt  VOCtrainval_06-Nov-2007.tar
2007_val.txt	train.txt      VOCtrainval_11-May-2012.tar
2012_train.txt	VOCdevkit
```

次に設定ファイルのtrainとvalidのパスを変更します。元のパスを先ほど作成したtrain.txtと2007_test.txtに変更します（↓のコードはエスケープが入っているので見づらくなっていますがご容赦下さい）。

```Python
%cd ../
!sed -i 's/\/home\/pjreddie\/data\/voc\/train.txt/VOCDataset\/train.txt/g' cfg/voc.data
!sed -i 's/\/home\/pjreddie\/data\/voc\/2007_test.txt/VOCDataset\/2007_test.txt/g' cfg/voc.data
```

最後に事前学習済の重みを取得します。これで準備は完了です。

```Python
!wget https://pjreddie.com/media/files/darknet53.conv.74
```

## 訓練
ということで、いざ訓練！先ほど変更した設定ファイルや重みを引数に与えています。yolov3-voc.cfgはモデルやハイパーパラメーターが定義されたファイルですが、今回は何も変更していません。中身を見てみるとResNetっぽい（ショートカットがある）モデルですが、Darknetと呼ぶそうです。重みを更新するかどうかは書かれていないように見えましたが、全てのレイヤーの重みを更新しているんでしょうか・・・。であれば、事前学習済の重みは必要だったのか？よくわかりません。ご存知の方コメント頂けると嬉しいです。

```Python
!./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg darknet53.conv.74
```

ここからはGoogle Colabに任せ待つしかありません。
・・・1時間後・・・
出力エリアがクラッシュ（？）してる！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/337041/89f126d0-b096-6f5c-aca8-7f8d832e4150.png)
なんかやたらログ出てるなぁと思ってたらバッチサイズ1で学習してました。これはまずそうなので、先ほどのyolov3-voc.cfgを変更します。さらにmax_batchesも大きすぎるようなので変更します。置換の順番は注意が必要です。詳細割愛しますが、うまく置換できずはまっていました。

```Python
!sed -i 's/ batch=1/# batch=1/g' cfg/yolov3-voc.cfg
!sed -i 's/# subdivisions=16/ subdivisions=32/g' cfg/yolov3-voc.cfg
!sed -i 's/ subdivisions=1/# subdivisions=1/g' cfg/yolov3-voc.cfg
!sed -i 's/# batch=64/ batch=128/g' cfg/yolov3-voc.cfg
!sed -i 's/max_batches = 50200/max_batches = 10000/g' cfg/yolov3-voc.cfg
```

そして、再度学習コマンドを実行！
・・・（12時間後）・・・
全く終わりません。このままではせっかく学習した重みが消えてなくなるため、別ノートブックを使いbackupディレクトリ以下の学習済みの重みを定期的（今回は1時間毎）にGoogle Driveにコピーします。

```Python
import sys
import os
import glob
import time
import datetime
from time import sleep

def save_file(file_name):
    !cp $file_name /content/drive/'My Drive'/temp/

def epoch_to_datetime(epoch):
    return datetime.datetime(*time.localtime(epoch)[:6])

JST = datetime.timezone(datetime.timedelta(hours=+9), 'JST')

while True:
    now = datetime.datetime.now()
    now_jst = datetime.datetime.now(JST)
    print(now_jst)
    file_list = glob.glob('backup/*')
    for file in file_list:
        sys.stdout.write(file)
        created_time = epoch_to_datetime(os.path.getctime(file))
        if created_time >= now - datetime.timedelta(hours=1):
            try:                
                save_file(file)
                print(' was uploaded!')
            except IOError:
                print(' had IOError!')
        else:
            print(' was ignored.')
                
    print('waiting...')            
    sleep(60 * 60 * 1)
```

そして12時間後にはdarknet53.conv.74の代わりに上記でコピーしておいた学習した重みを使い学習再開ということを繰り返すこと約2週間（仕事が忙しくて学習しない日もあったり）ようやく10000エポック終わりました！[こちらに](https://github.com/AlexeyAB/darknet)以下のように書いてありましたが、最終的にlossは0.7くらいになっていたと思います。

> When you see that average loss 0.xxxxxx avg no longer decreases at many iterations then you should stop training. The final avgerage loss can be from 0.05 (for a small model and easy dataset) to 3.0 (for a big model and a difficult dataset).

## テスト
ようやく学習も終わり、効果のほどを確認しようと思い、コマンド実行してみます（正確には前回同様以下のコマンドの後に`predictions.jpg`を表示しています）。

```
!./darknet detect cfg/yolov3-voc.cfg backup/yolov3-voc_final.weights VOCDataset/VOCdevkit/VOC2007/JPEGImages/000028.jpg
```

すると・・・
![image.png](https://qiita-image-store.s3.amazonaws.com/0/337041/1ab221b9-ffeb-38fc-6e56-d1dd20887e35.png)

なるほど、この茶色い生物っぽいものはtruckと呼ぶのか・・・（しかも精度100%と自信満々の回答）。他の画像のテストをしてみても軒並み分類結果が異なります。しかし物体検出自体はできていました。これはさすがに何かおかしいと思い色々と調べていると、どうも`data/coco.names`を参照しラベル表示しているようでした。同じようなことが[こちら](https://groups.google.com/forum/#!topic/darknet/kzlNoXwIxa8)にも書いてあり、`./darknet detector test`を実行してみたか？というようなコメントがありました。よく見てみると自分の実行したコマンドは`./darknet detect`でちょっと違います。ということで・・・

```Python
!./darknet detector test cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc_final.weights VOCDataset/VOCdevkit/VOC2007/JPEGImages/000028.jpg
```

を実行！すると・・・

![image.png](https://qiita-image-store.s3.amazonaws.com/0/337041/bfe7bc92-63b7-57f3-5d94-ee152a8e1089.png)

ついに正しく表示されました！長かった・・・。その他の画像も概ねうまく物体検出できているようでした。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/337041/b4fa1e0f-0705-07a1-5075-bb6313d61aa9.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/337041/944b7b71-17d5-59a0-5c9a-5f568ee1c171.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/337041/ffdf90ef-e8c0-5dd2-e5b4-30c82cb88965.png)

# まとめ
* batch、subdivisions、max_batchesの値を忘れずに変更しましょう
* 学習は12時間では終わらないため、重みをGoogle Driveにコピーし、次はそれを元に学習を再開しましょう
* テストは`./darknet detector test`を使いましょう

# 最後に
どなたかのお役に立てれば幸いです。コードはGitHubに~~公開したいなぁとは思っています。~~[公開しました](https://github.com/shoji9x9/train-yolov3-on-google-colaboratory)。

# 参考にさせて頂いたページ
* [YOLO](https://pjreddie.com/darknet/yolo/)
* [AlexeyAB/darknet](https://github.com/AlexeyAB/darknet)
* [YOLOv3を試してみる(2)](http://mirai-tec.hatenablog.com/entry/2018/12/26/221327)
* [最新の物体検出YOLO v3 (Keras2.2.0+TensorFlow 1.8.0)を独自データで学習できるようにしてみた](http://arkouji.cocolog-nifty.com/blog/2018/07/yolo-v3-keras22.html)
* [darknetでMNISTを学習する](https://qiita.com/ashitani/items/7744954f5dbb0419d955)
* [Windows 10上のDarknetでYolo v3をトレーニングしOpenCVから使ってみる](https://nixeneko.hatenablog.com/entry/2018/08/15/000000)