RaspberryPiで物体検出してみる ~その2：YOLOv3でねこ画像の学習~

この記事は「[【マイスター・ギルド】本物の Advent Calendar 2020](https://adventar.org/calendars/5454)」10日目の記事です。

こんにちは、マイスター・ギルド入社5か月のSwitchBladeです。
マイスター・ギルド アドカレの猫連載2日目になります。[前の方の記事](https://note.com/hkt_mgild/n/n57e7b65eb86a)で保護猫に関して記載されていましたが、我が家の4匹は近所に落ちていたものを保護しました。


# はじめに
独自の学習データを使ったyolov3-tinyによる物体検出の学習をしていきます。
物体検出の対象は我が家の4匹の猫です。柄は4匹とも違っています。

※なお。今回の作業にRaspberryPiは使用しません。

以下の順序で作業します。
1. 学習用画像の準備
2. アノテーションデータ作成
3. アノテーションデータをYoLo形式に変換
4. 学習

# 1.学習用画像の準備
デジカメ撮影で230枚用意しました。(多い方がいいことは分かっていましたが、2か月でこの辺りが限界でした。)

## 猫紹介
|なまえ |タグ|写真 |備考
|:-----------|:------------:|:------------:|:------------:|
にゃー| mike |  ![nyaP1100696.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/96b6eb08-d935-45e5-a6e3-d7e82f6679cf.jpeg) |三毛。他の3匹とは別部屋。|
ちゃちゃ| cha |  ![chaP1100768.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/ba129b4d-5094-0e7a-4278-dfec97c44a43.jpeg) |茶トラ(白)。さばの兄弟|
さば| saba | ![sabaP1100847.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/81d2a375-f33b-62ce-f47b-cedec1c833df.jpeg)|キジトラ(白)。ちゃちゃの兄弟。|
ゆき| kiji |  ![yukiP1100613.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/b48b02f9-c14f-9276-c04f-8d9d9309a9c9.jpeg)| キジトラ(ペルシャ風)。一人愚連隊。|


# 2.アノテーションデータ作成
物体検出用の教師データを作成していきます。
具体的には、写真のここに何がありますという注釈(アノテーション)をつけていきます。
アノテーションツールはMicrosoft VoTTを使用しました。

## 2.1. VoTTインストール(Windows)
https://github.com/Microsoft/VoTT/releases からダウンロードして、インストールします。

 ![Microsoft VoTT- v2.2.0 ](https://img.shields.io/badge/Microsoft VoTT- v2.2.0 - brightgreen) 

## 2.2.新規プロジェクト
起動して、[新規プロジェクト]を選択します。

## 2.3.プロジェクト設定
![VoTT1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/bc7dbca7-6db8-dfb0-5fb4-a99077fb8080.png)
![スクリーンショット 2020-12-14 104401.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/9f166c59-5bb8-054f-6586-8a7bb6cbb7e2.png)

* 表示名：任意
* セキュリティ トークン：設定不要
* ソース接続：画像フォルダ
* ターゲット接続：プロジェクトやエクスポートの保存先
* 説明：任意
* ビデオ設定：今回ビデオは使わないのでデフォルトのまま
* タグ：必要なタグを指定(今回は4つのタグを指定します)


## 2.4.エクスポート設定
![VoTT5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/bd87618f-703c-738a-1e5a-14960ae26b60.png)

* プロバイダー：Pascal VOC (Yolo形式のエクスポートには対応していないので、一旦Pascal VOC形式にします)
* アセットの状態：訪問済みのアセットのみ
* テスト/トレーニング分割：キャプチャの状態で訓練データが80%、検証データが20%になります。

## 2.5.アクティブ ラーニング
![VoTT3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/d9fa8631-35ec-0215-6287-ed70333e1240.png)

* モデル プロバイダー：「事前トレーニング済みのCoCo SSD」を選択すれば [Microsoft CoCOデータセット](https://cocodataset.org/) に分類があるものは、物体がある場所を自動で範囲選択してくれるので、アノテーション作成にかかる時間を短縮できます。
* 予測タグ：ONにしておけば、Cocoデータセットと同じタグが自動で付与されます。今回はオリジナルのタグを付けるためOFFにしています。

上記の設定で、画像読み込み時にこのように自動で範囲選択されます。猫に関してはかなりの精度で検知してくれていました。
![VoTT4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/d006b61c-6208-5320-6589-a4bc89b461b3.png)

## 2.6.タグ付け
用意した画像分 地道にタグ付けをしていきます。。。230枚で1時間ほどかかりました。
## 2.7.エクスポート
タグを付け終わったら[プロジェクトをエクスポート]ボタンを押下して、エクスポートします

# 3.アノテーションデータをYoLo形式に変換
上に書いたようにVoTTはYoLo形式のエクスポートに対応していないので、Pascal VOC形式をYoLo形式に変換する必要があります。
[VoTTでエクスポートしたPascal VOC形式のデータをYolo V3形式に変換](https://yasuraka.dip.jp/?p=601) を参考にさせてもらいました。

一部ソースの修正が必要で、classes変数 をVoTTで作成したタグに書き換える必要があります
。その際、エクスポートされた`pascal_label_map.pbtxt`の順序で指定する必要があります。

``` 例)voc_label.py
classes = ["cha", "saba", "mike", "kiji"]  
```

このPythonの実行は、Google Colaboratoryで作業しました。

実行すると、`train.txt`(訓練用の画像ファイルのセット)と`val.txt`(検証用の画像ファイルのセット)と`export/labels`の下に"画像のファイル名.txt"(YoLo形式のアノテーションファイル)ができます。

# 4.学習
学習は[前回の記事](https://qiita.com/SwitchBlade/items/c2fd76b01a621578a464)でも使用した[AlexeyAB/darknet](https://github.com/AlexeyAB/darknet)をGoogle Colaboratory上で実行しました。Google ColaboratoryのランタイムのタイプはGPUを指定します。

## 4.1.インストール・ビルドなど
```Colaboratory：yolov3-tiny_train.ipynb
!apt install cuda-10-0 -y
import os
os.environ['PATH'] += ':/usr/local/cuda/bin'
os.environ['LD_LIBRARY_PATH'] += ':usr/local/cuda/lib64'
```
CUDAを使用するため、インストールします。バージョンは10.0以上です。


```Colaboratory：yolov3-tiny_train.ipynb
!git clone https://github.com/AlexeyAB/darknet
```
githubからcloneします。


```Colaboratory：yolov3-tiny_train.ipynb
%%bash
sed -i 's/GPU=0/GPU=1/g' Makefile
sed -i 's/CUDNN=0/CUDNN=1/g' Makefile
sed -i 's/OPENCV=1/OPENCV=0/g' Makefile
make
```
AlexeyABはC言語なので、Makefileを修正してmake(ビルド実行)します。Makefileの詳しい説明は、AlexeyABのgithubに書かれています。

* `%%bash` は、Linuxコマンドを使うための、おまじないです。
* sedコマンド(Linuxの文字列置換コマンド)でMakefileを書き換えます。
    * `GPU=1`、`CUDNN=1`：GPUを使用できるようにします。
    * `OPENCV=0`：Google Colaboratory上ではOPENCVをOFFにしないと、学習の実行に失敗しました。


```Colaboratory：yolov3-tiny_train.ipynb
%%bash
./darknet partial yolov3-tiny_obj_train.cfg  yolov3-tiny.weights yolov3-tiny.conv.15 15
```
一から学習すると時間がかかるため、学習済みの重みの前半のLayerを取り出して、そこから学習を始めます。
学習済みの重みは[pjreddie/darknet](https://pjreddie.com/darknet/yolo/)からダウンロードできます。
上のコマンドでは15Layerを取り出しています。

## 4.2.画像ファイル・アノテーションファイル等の配置
画像ファイルとYoLo形式のアノテーションファイルを`darknet`フォルダの下に移動します。今回は`data/images`フォルダとします。
同じくYoLo形式のアノテーションファイル作成時に出力される`train.txt`と`val.txt`を`darknet`フォルダの下に移動します。今回は`data`フォルダとします。`train.txt`と`val.txt`には画像のパスが書かれていますが、darknet実行ファイルからの相対パスに変更します。今回の場合だと、`data/images/XXXX.jpg`に変更します。

以下の2ファイルを作成します。

```data/obj.names
cha
saba
mike
kiji
```
タグ名称を`pascal_label_map.pbtxt`の順序で記載します。

```data/obj.data
classes= 4
train = data/train.txt
valid = data/test.txt
names = data/obj.names
backup = backup/
```
classes：タグの数を指定
train：訓練用の画像ファイルセットが記載されたファイル
valid：検証用の画像ファイルセットが記載されたファイル
names：タグ名称が記載されたファイル
backup：学習結果の重みが格納されるパス

## 4.3.configファイル修正
configファイルを[pjreddie/darknet](https://pjreddie.com/darknet/yolo/)からダウンロードして、学習したい内容に合わせて修正します。

```yolov3-tiny_train.cfg
# Testing
#batch=1
#subdivisions=1
# Training
batch=32
subdivisions=8
・・・
max_batches = 8000
・・・
steps=6400,7200
```
学習用に設定を変更します。

* batch：学習時のバッチサイズ。大きくすると時間がかかります。
* subdivisions：バッチを何分割するか。小さいと`Out of memory`が発生する可能性があります。
* max_batches：バッチの繰り返し回数。タグ数×2000が目安。
* steps： max_batchesの80%と90%を指定します。

```yolov3-tiny_train.cfg
[convolutional]
size=1
stride=1
pad=1
filters=27  ★
activation=linear

[yolo]
mask = 3,4,5
anchors = 10,14,  23,27,  37,58,  81,82,  135,169,  344,319
classes=4  ★
num=6
jitter=.3
ignore_thresh = .7
truth_thresh = 1
random=1
```
★の位置を修正します。

* classes：[yolo]レイヤーのclassesをタグ数に変えます。
* filters：[yolo]レイヤーの前の[convolutional]レイヤーのfiltersを(タグ数 + 5)x3 に変えます。

yolov3-tiny.cfgにはそれぞれ2か所あります。


## 4.4.学習実行
```Colaboratory：yolov3-tiny_train.ipynb
%%bash
chmod u+x darknet

./darknet detector train data/obj.data yolov3-tiny_train.cfg yolov3-tiny.conv.15
```
学習時間はColaboratoryのGPUガチャにもよりますが、Tesla P100で60分程度かかりました。



# 学習結果の確認
学習結果の確認もColaboratoryで行います。

```Colaboratory：yolov3-tiny_test.ipynb
%%bash
sed -i 's/GPU=0/GPU=1/g' Makefile
sed -i 's/CUDNN=0/CUDNN=1/g' Makefile
sed -i 's/OPENCV=0/OPENCV=1/g' Makefile
make
```
Makefileを書き換えてmakeし直します。OPENCVを有効にします。

```yolov3-tiny_test.cfg
# Testing
batch=1
subdivisions=1
# Training
#batch=32
#subdivisions=8
```
yolov3-tiny_train.cfg をファイルコピーして上の部分を修正します。

```Colaboratory：yolov3-tiny_test.ipynb
%%bash
chmod u+x darknet
./darknet detector test data/obj.data  yolov3-tiny_test.cfg  backup/yolov3-tiny_train_final.weights data_test/XXXXXX.JPG -ext_output -thresh 0.01 -dont_show
```

* -ext_output ：結果画像が`predictions.jpg`で出力されます。
* -thresh XXX ：検出する確度の閾値
* -dont_show ： 実行時の結果画像表示を抑止する。Colaboratory上では付けないとエラーになります。

何枚か結果を載せます。判定の枠にタグ名と判定確度が出力されます。
![predictions_P1100738_8000_re.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/55752db4-2b93-5740-411c-deabf02fc84e.jpeg)
![predictions_DSC_0221_6000_re.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/350b5455-9834-8ea5-4b53-04343e8e5ab1.jpeg)
この辺りはうまく検出できています！特に1枚目は高い確度になっていますが、ほぼ同じ構図の写真を学習に使用していた影響と思います。

![predictions_DSC_0134_8000_re.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/7ece9bbb-c915-46c5-c690-47c0fb3e8a44.jpeg)
![predictions_DSC_0146_6000_re.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438288/76e6b7cc-46b1-826b-2321-5c14f1be0e82.jpeg)
この辺りは閾値を0.01にしているためぎりぎり検出できていますが、確度も低いですし誤判定もあります。全般的に「ちゃちゃ」はうまく検出できてない傾向が多かったです。学習用の画像が230枚とかなり少なかったこともありますが、見る角度によっては白の割合が多いため「にゃー」や「さば」と混同して学習されている可能性があります。

#メモ
役に立つか分かりませんが、気づいたことをメモしておきます。

* 学習時のloss(損失)が0.2程度まで下がっていない場合は、収束していない可能性が高いです。訓練データを用いて学習結果の確認を行っても、検出できません。
* VoTTの訓練データ、検証データの振り分けはVoTTで読み込んだファイル順である程度連続して振り分けるっぽいです。自分で撮った写真を使う場合は似た写真が連続するため、訓練データ、検証データの偏りが大きいです。収束しない場合は、`train.txt`と`val.txt`のファイル名をランダムに入れ替えるとうまくいくかもしれません。
* 学習時は1000ステップごとにバックアップファイルが作成されます。もし、Colaboratoryの時間切れなどで途中で学習が途切れた場合は、学習実行コマンドの`yolov3-tiny.conv.15` をバックアップファイルに変えることで学習を途中から再開することができます。

# 今後の方向性
[【物体検出】vol.3 ：YOLOv3の独自モデル学習の勘所](https://www.nakasha.co.jp/future/ai/yolov3train.html) によると、学習データは1タグにつき1000枚程度が必要ということなので学習用の写真を増やして検出の精度を上げる方法を試しつつ、RaspberryPiで検出できるようにしていきます。