Google Colab上でYOLOv3の独自データ学習をする

世の中にはもうすでにYOLOをcolab上で動かす情報が転がっているけど，備忘録も兼ねて．

# 実行環境
記事執筆中のcolabの環境はこんな感じ．ローカルはMacOS, python3．

```Bash
! nvcc -V
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2019 NVIDIA Corporation
Built on Sun_Jul_28_19:07:16_PDT_2019
Cuda compilation tools, release 10.1, V10.1.243

! dpkg -l | grep "cudnn"
hi  libcudnn7                              7.6.5.32-1+cuda10.1                               amd64        cuDNN runtime libraries
ii  libcudnn7-dev                          7.6.5.32-1+cuda10.1                               amd64        cuDNN development libraries and headers

import tensorflow as tf; print(tf.__version__)
2.2.0
```

# 学習データ準備(local)
## darknetをcloneする
まだGoogle colabは用いずにローカルで作業をおこなう．私は作業しやすいからローカルでcloneしたけど，colab上でもできる．

```Bash
git clone https://github.com/AlexeyAB/darknet.git
cd darknet
```
ちなみに，こちらは御本家ではないがいろいろ機能が追加されていて，オプションをつけると学習状況をプロットしてくれたりする．また，さらに強くしたyolov4作ったで〜みたいなことも書いてあるので，興味があればそっちでやってもいいと思う，今回はv3で．一応，[公式](https://pjreddie.com/darknet/yolo/)に載っている通りやる場合は以下．

```Bash
git clone https://github.com/pjreddie/darknet
cd darknet
```
とりあえず，私はこの中に今回のtask用のディレクトリを作った．さらにその下に画像とか入れる場所をdataという名前で作成し，この中に学習データとなるjpgファイルを入れる．

```Bash
mkdir task1 | cd task1
mkdir data 
```

## 必要ファイルの準備
YOLOで学習させるのに必要になってくるファイルは以下．

* yolov3-obj.cfg
* obj.names
* train.txt
* test.txt
* obj.data

### yolov3-obj.cfg
まずはcfgファイルから準備する．今回は元々あるyolov3.cfgをコピーして書き換えていく．

```Bash
cp cfg/yolov3.cfg task1/yolov3-obj.cfg
```
[README](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)にある様に自分の学習させたいクラスの数とかに従って書き換えていく．基本的には以下．

- Trainingのコメントアウトを外してTestingをコメントに(2-7行目)．batch=64, subdivisions=16とする．
- 20行目max_batchesをclasses*2000の数に．ただし，学習させる画像の数や6000回を下回らない様に．
- 22行目にstepsをmax_batchesの80%と90%に．
- 610, 696, 783行目のclassesをクラス数に変える．
- 603, 689, 776行目のfilters=255を(classes + 5)x3の数に変える．yolo層の前のconvolutional層にある

私はクラスが1個なので，max_batches=6000, steps = 4800,5400, classes=1, filters=18となる.

### obj.names
クラスの名前をきちんとクラスの数だけ書く．私はクラスが1個なので1個だけ．

```bash:obj.names
target
```

### train.txtとtest.txt
これはそれぞれに画像へのパスを書いてあげれば良い．ただ，面倒なので[こちら](https://timebutt.github.io/static/how-to-train-yolov2-to-detect-custom-objects/)に載っているprocess.pyを使ってtrain.txtとtest.txtを作成した．colabで再度実行することを考えて少し手を加えたものが以下．テストデータの割合を変えたい場合はpercentage_testを変える．

```python:process.py
import glob, os

# Current directory
current_dir = os.path.dirname(os.path.abspath(__file__))
task_name = os.path.join(current_dir, 'task1/')

# Directory where the data will reside, relative to 'darknet.exe'
path_data = task_name + 'data/'

# Percentage of images to be used for the test set
percentage_test = 20;

# Create and/or truncate train.txt and test.txt
file_train = open(task_name + 'train.txt', 'w')  
file_test = open(task_name + 'test.txt', 'w')

# Populate train.txt and test.txt
counter = 1  
index_test = round(100 / percentage_test)  
for pathAndFilename in glob.iglob(os.path.join(path_data, "*.jpg")):  
    title, ext = os.path.splitext(os.path.basename(pathAndFilename))

    if counter == index_test:
        counter = 1
        file_test.write(path_data + title + '.jpg' + "\n")
    else:
        file_train.write(path_data + title + '.jpg' + "\n")
        counter = counter + 1
```

```bash
python process.py
```

### obj.data
以下の様なファイルを作る．

```bash:onj.data
classes=1
train=task1/train.txt
valid=task1/test.txt
names=task1/obj.names
backup = task1/backup/
```

### その他もろもろ
task1下にbackupディレクトリを作る．

```bash
mkdir task1/backup
```
また，学習に使うので使うのでdarknet53モデルを持ってきておく．

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

最後に，GPUを使うためにMakefileの1~4行目を書き換える．

```Bash
GPU=1
CUDNN=1
CUDNN_HALF=1
OPENCV=1
```
また，これはお好みだが，デフォルトでweightは1000回ごとに保存される様になっている．これを変えたい場合はsrc/detector.cの363行目を変更してあげれば良い．私は1000回以下の時は100回ごと，1000回を超えると1000回ごと保存される様にした．

```c:detector.c
// if (iteration >= (iter_save + 1000) || iteration % 1000 == 0) {
if (iteration % 1000 == 0 || (iteration < 1000 && iteration % 100 == 0)) {
```

現在，こんな感じになっているはず．

```
darknet
├── task1
│   ├── backup
│   ├── yolov3-obj.cfg
│   ├── train.txt
│   ├── test.txt
│   ├── obj.names
│   ├── fugaobj.data
│   └── data
│       ├── 1.jpg
│       ├── 2.jpg
│       ├── ...
│
├── cfg
│   ├── yolov3.cfg
│   ├── ...
│
├── src
│   ├── detector.c
│   ├── ...
│
├── Makefile
├── process.py
├── Makefile
├── darknet53.conv.74
├── ...
```

## アノテーション
今回はアノテーションツールとして，labelImgを使った．pipで入れれる．pipじゃなくても入れれる．pipを使うと以下の様な感じ．

```Bash
pip install pyqt5 lxml # Install qt and lxml by pip
make qt5py3
python labelImg.py task1/data task1/obj.names
```
labelImgの使い方は割と直感でななんとかなると思う．わかんなくても調べたら情報はたくさん出てくるはず．また，起動したままアノテーションするとYOLO形式では保存されないので，左側にあるラベルの種類をYOLOに変える．dataの中にtxtができていればおけ．

```Bash
darknet
├── task1
│   ├── ...
│   ├──data
│       ├── 1.jpg
│       ├── 1.txt
│       ├── 2.jpg
│       ├── 2.txt
│       ├── ...
```

これでローカルでの準備は完了．

# 学習させる
## 学習準備 on colab
今作ったdarknet以下を丸ごとgoogle driveにあげる．画像がたくさんあったら時間がかかってしまうかも．私は十数枚(しかない)ので割と早めにアップロード完了．

次にgoogle colabの新規ノートブックを立ち上げ，ランタイムのタイムをGPUにする．google driveをマウントするのはとても簡単で，左側に並んでいるアイコンのフォルダをクリックすると"ドライブをマウントする"が現れるのでクリック．マウントできたら先ほどアップロードしたdarknetの中に入る．

```python
% cd '/content/drive/My Drive/darknet' 
```
そしたらmakeする．最初はmakeだけで大丈夫だけど，2回目とかだとmake cleanが必要になるので注意．ちなみに結構warningが出てきて心配になるが，問題はなさそう．

```python
# ! make clean
! make 
chmod +x *.sh
g++ -std=c++11 -std=c++11 -Iinclude/ -I3rdparty/stb/include -DGPU -I/usr/local/cuda/include/ -DCUDNN -Wall -Wfatal-errors -Wno-unused-result -Wno-unknown-pragmas -fPIC -Ofast -DGPU -DCUDNN -I/usr/local/cudnn/include -c ./src/image_opencv.cpp -o obj/image_opencv.o
g++ -std=c++11 -std=c++11 -Iinclude/ -I3rdparty/stb/include -DGPU -I/usr/local/cuda/include/ -DCUDNN -Wall -Wfatal-errors -Wno-unused-result -Wno-unknown-pragmas -fPIC -Ofast -DGPU -DCUDNN -I/usr/local/cudnn/include -c ./src/http_stream.cpp -o obj/http_stream.o
./src/http_stream.cpp: In member function ‘bool JSON_sender::write(const char*)’:
./src/http_stream.cpp:249:21: warning: unused variable ‘n’ [-Wunused-variable]
                 int n = _write(client, outputbuf, outlen);
                     ^
gcc -Iinclude/ -I3rdparty/stb/include -DGPU -I/usr/local/cuda/include/ -DCUDNN -Wall -Wfatal-errors -Wno-unused-result -Wno-unknown-pragmas -fPIC -Ofast -DGPU -DCUDNN -I/usr/local/cudnn/include -c ./src/gemm.c -o obj/gemm.o
./src/gemm.c: In function ‘convolution_2d’:
...
```

次に，google driveにアップロードした関係でパスが変化したので，process.pyを実行する．~~結局，python実行するならファイルにじゃなくてセルにそのまま貼り付けてしまっても良かったな~~

```python
! python process.py
```
これで，train.txtとtest.txtが更新されたはずなので，気になる人はgoogle driveの方から覗いてみると良いかも．

準備はできた．

## 学習開始 on colab
以下のコマンドを実行すると学習が開始される．もしpermission deniedとかでたら，make cleanしてmakeしなおしてみて．また，MakefileにOPENCV=1を入れておくと，100iterationごとにLoss-chart，-mapをつけるとmAP & Loss-chartがchar.pngとして保存される．

```python
! ./darknet detector train task1/crater.data task1/yolov3.cfg darknet53.conv.74 -dont_show -map
```

weightはtask1/backupの中に保存されていく．仮に途中で終わってしまって学習を再開させたい場合は以下の様にすればおけ．(この時，-mapつけてきちんと続きからplotされるのかは試していないのでわからない，つけて損はなさそうだけど)

```python
! ./darknet detector train task1/crater.data task1/yolov3.cfg task1/backup/yolov3_last.weights
```

# 確認
モデルができたらcolab上で試しても良いし，driveからダウンロードしてローカルで使っても．
~~ただ，検出の時にはcfgファイルの最初のところを少し書き換える必要があるらしい．~~
cfgは書き換えなくても良いかもしれない．学習に使ったcfgを使った方が良いかも．

```yolov3-obj.cfg
# Testing
batch=1
subdivisions=1
# Training
# batch=64
# subdivisions=8
```

test.jpgをcolab上で確認するにはこんな感じ．

```python
!./darknet detector test task1/obj.data task1/obj.cfg yolov3_6000.weights test.jpg
```
predictions.pngができているはずなので，そちらを確認する．

# 参考サイト
参考にさせていただいたサイト
公式: https://pjreddie.com/darknet/yolo/
今回使ったコード元: https://github.com/AlexeyAB/darknet
process.pyの元: https://timebutt.github.io/static/how-to-train-yolov2-to-detect-custom-objects/