YOLOv4（Darknet）で異常検知モデル作成挑戦（GoogleColab）

# YOLOv4(Darknet) で異常部位の Object Detection
![28467666-b7d4-4d18-a234-3270addb25bc-960x387r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/61a198e3-8a91-cbaa-beb1-ad0c10bcbd23.png)

## 実行環境
- Google Colab（2020 年 9 月現在）

## データセット
[MVTec Anomaly Detection Dataset (MVTec AD)](https://www.mvtec.com/company/research/datasets/mvtec-ad/) を使用します。
screw（ネジ）のデータを使用します。

- train : good（異常なし良品）
- test : 不良品
    - good
    - thread_top
    - thread_side
    - scratch_neck
    - scratch_head
    - manipulated_front

本来は良品のみで学習させるためのデータセットですが、test に用意されている不良品データを学習にも使用してモデルを作成したいと思います。
（製造ラインで不良品が滅多にでないことは重々承知。）

傷の種類は分けず全て `anomaly` としてラベル付けしました。

このような画像データです。（傷部分にマークつけてます。）

![248d45c6-e50f-4d9e-afff-2e08893e7219-960x283r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/099f9468-0297-8848-20b8-2c9863441bc5.png)

![0e3db855-f455-4e57-8fde-bdc699292a40-960x283r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/d30709f6-4ae3-e8d0-a94a-f2da6eac6b84.png)

![1f0ef881-5795-416f-9634-469b9b6ce917-960x282r (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/23a1df31-202a-c963-37cd-7c160bfc85f8.png)

![d9852fe4-a1ef-405b-931e-aecb91b65802-960x283r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/f742734d-5d18-0b05-d0c3-55bcd59a0526.png)

![f047f553-ec09-4938-b737-5a632a0754b5-960x283r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/06375dee-7f48-5074-4772-ccb16b0cb07f.png)

![0688c4e5-86e5-4d4d-83ea-ad4d6e20b979-960x283r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/1bdd84df-f1ea-9a86-bee4-45b6c45fc5c5.png)

## アノテーション
割愛します。
VOTT を使って PascalVOC 形式から YOLO 形式へ変換を行いました。
簡単に説明すると、xml 形式から txt 形式に変換します。
また、
[ `object-class` `x_center` `y_center` `width` `height` ]
の順番かつ、ボックス座標は 0 ~ 1 に正規化された値でなければいけません。
こんな感じ。
![f988cc81-cd22-47eb-813c-a9e3adec9e60-960x174r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/d072b342-eecb-3e1b-7d50-3a1206faae03.png)



## モデル学習準備

下記 2 つを参考にして進めました。
- https://github.com/pjreddie/darknet
- https://github.com/AlexeyAB/darknet

特にこれに忠実に実装しました。
[How to train (to detect your custom objects)](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)

```python:darknetの取得
!git clone https://github.com/AlexeyAB/darknet
%cd darknet
```
### 重みの取得（YOLOv4）
ここからダウンロードして環境上にアップロードします。
[How to train (to detect your custom objects)](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)

raining Yolo v4 (and v3):

For training cfg/yolov4-custom.cfg download the pre-trained weights-file (162 MB): yolov4.conv.137 (Google drive mirror yolov4.conv.137 )


ここからオリジナルデータで学習させるための準備をしていきます。
`custom` というディレクトリで作業を進めることにしました。

```python:customディレクトリを作成
!mkdir custom
%cd custom/
```
```python:各データファイル作成
!mkdir data
!touch train.txt
!touch test.txt
!touch custom.data
!touch custom.names
!touch custom.cfg
!mkdir backup
```
- `data`：画像データ + バウンディングボックスデータ
- `train.txt`：学習用画像データパス
- `test.txt`：テスト用画像データパス
- `custom.data`：各データ参照
- `custom.names`：class 名（今回は 'anomaly' のみ）
- `custom.cfg`：モデル構造
- `backup`：学習後の重みの保存場所

### data フォルダに画像とアノテーション txt を配置します。
こんな感じ。

![0138fc81-f82c-45ad-8da1-700455e7f400-960x984r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/1378cceb-0365-a785-d8a8-ba6854149f29.png)

### train.txt, test.txt に書き込み
```python
# 画像パスの取得
from glob import glob
images = glob('custom/data/*.png')

# 学習用とテスト用に分割
n_train = int(len(images) * 0.8)
train = images[: n_train]
test = images[n_train :]

# train.txt, test.txt に書き込み
with open('custom/train.txt', 'w') as f:
    for line in train:
        f.write('%s\n' % line)

with open('custom/test.txt', 'w') as f:
    for line in test:
        f.write('%s\n' % line)
```
こんな感じになっているはず。（順不同で OK）
順番揃えたければ `sorted(glob(''))` で。

![74db2d37-592f-4a09-a0c1-a3498d5b2b13-960x464r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/bbf8be02-864f-b092-0ed8-a731a857787d.png)



### custom.data 作成
- classes：タスク（学習データ）に合わせて
- その他はパスを指定

```python
classes = 1
train  = /content/darknet/custom/train.txt
valid  = /content/darknet/custom/test.txt
names = /content/darknet/custom/custom.names
backup = /content/darknet/custom/backup/
```

### custom.names
- タスク（学習データ）に合わせて

```
anomaly
```
![2f5dfcee-5bb4-4acf-bef8-9f55440daa69-960x382r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/e906fd08-1d86-056f-ccda-de9259cd94c7.png)



### custom.cfg 作成
モデル構造のファイルです。
こちらは `darknet/cfg/yolov4-custom.cfg` をコピーしてきて必要箇所を書き換えましょう。

- `max_batches` を学習させたいデータの class 数（物体数）× 2000 にします。
ただし、最低 6000 。
- `steps` を `max_batches` の 80%, 90%

（今回は 'anomaly' 1 クラスなので `max_batches` =  1* 2000 = 2000 となるが 6000、`steps` = 4800, 5400）

```python:custom.cfg内
earning_rate=0.001
burn_in=1000
max_batches = 6000
policy=steps
steps=4800,5400
scales=.1,.1
```
- タスクに合わせて`[yolo]`, `[convolutional]` 変更 : **合計 6 箇所あります！！**
- `classes`：物体数
- `filters`：(class + 5 ) × 3

```python
[convolutional]
size=1
stride=1
pad=1
filters=18
activation=linear


[yolo]
mask = 0,1,2
anchors = 12, 16, 19, 36, 40, 28, 36, 75, 76, 55, 72, 146, 142, 110, 192, 243, 459, 401
classes=1
num=9
jitter=.3
ignore_thresh = .7
truth_thresh = 1
scale_x_y = 1.2
iou_thresh=0.213
cls_normalizer=1.0
iou_normalizer=0.07
iou_loss=ciou
nms_kind=greedynms
beta_nms=0.6
max_delta=5
```

### Make
darknet/Makefile の書き換え

```python:Makefileの下記部分書き換え
GPU=1
CUDNN=1
CUDNN_HALF=1
OPENCV=1
AVX=0
OPENMP=0
LIBSO=0
ZED_CAMERA=0
ZED_CAMERA_v2_8=0
```

```python:Make
%cd /content/darknet
!make

# make 複数回してしまったらクリーンしてから !make
!make clean
```
## 学習
```python
!./darknet detector train custom/custom.data custom/custom.cfg custom/yolov4.conv.137
```
このように `backup` フォルダに重みが保存されていきます。
デフォルトでは、1000 回ごとに重みが保存されます。（画像は 100 回ごとにコードを変更しています。）
学習後はこの重みを使って推論を行います。

![12f4888f-36c4-4121-883e-16cb702a1095-960x519r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/576b52ed-4035-a81c-41f0-4ac093436d2f.png)

## 結果
loss が落ち着いたので 3500 エポックで終了しました。

テストデータに対して結果を確認します。(backup に保存されている `_last.weights` を使用します。)

```python
!./darknet detector map custom/custom.data custom/custom.cfg custom/backup/custom_last.weights
```

```python
| 24 |
| --- |
| detections_count = 23, unique_truth_count = 25 |
| class_id = 0, name = anomaly, ap = 79.28% | (TP = 16, FP = 0) |
| for conf_thresh = 0.25, precision = 1.00, recall = 0.64, F1-score = 0.78 |
| for conf_thresh = 0.25, TP = 16, FP = 0, FN = 9, average IoU = 76.83 % |
| IoU threshold = 50 %, used Area-Under-Curve for each unique Recall |
| mean average precision (mAP@0.50) = 0.792783, or 79.28 % |
| Total Detection Time: 2 Seconds |
```

テスト用の 24 枚に対して上記の結果です。まずまずの結果です。
2 秒で推論できているので、v4 速いのかな。

### テスト 24 枚の結果
![12b6601b-1e56-4ce3-af48-cb307baf8dd5-960x651r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/5eea7048-4eb3-e40b-4800-d26e72d44810.png)

![60c5eb0a-b35d-409e-b2d4-6f5a1fea1714-960x653r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/605917/7fc2120a-3bf5-9386-4d22-2f4dd55495cc.png)

## 結果考察
検出したオブジェクトの信頼度は 0.99 付近で高い。
ネジの先端の曲がりなどは検出しにくかった。
そもそも先端の曲がりに、どうアノテーションすればよいのかわからない。
今回のデータ傷の場所や大きさ、形状が似たような物が多かったので
**おそらく学習した 5 種類以外の傷に対しては検出できないはず。**
ある意味過学習モデルな気がする。