YOLOv4 - condaでのクイックセットアップとGPUで学習する

> 依存関係はシステム全体ではなく、conda 環境のみでインストールされます。

- [English Version (英語版)](https://qiita.com/sebastianrettig/items/33ead90d3bde4cc9b6b0)

## 前書き
エッジデバイスでの高速物体検出のためにYOLOv4モデルを学習させようと考え、YOLOv3 Darknetリポジトリからフォークされた[YOLOv4公式リポジトリ](https://github.com/AlexeyAB/darknet)を見つけました 。
DarknetはC言語とCUDAで書かれたニューラルネットワークフレームワークです。GPU用のセットアップは明示的に説明されていませんが、OpenCVとCUDA-Toolkitの公式サイトにリンクされ、システム全体にインストールするためのセットアップガイドが提供されています。私はPC1台で複数ニューラルネットワークのフレームワーク（Pytorch、Tensorflow 1と2など）を使用しているため、依存関係のあるライブラリをシステム全体にインストールすると、バージョンの競合を引き起こす可能性が高くなります。それにシステム全体にインストールされたライブラリーのアンインストール方法が統一されておらず、正しいアンインストール方法の調査時間も必要です。Anacondaを使用するとセットアップ環境を分離できます。conda環境でのGPU学習のためのDarknetのセットアップに関するガイドを見つけることができなかったため、このガイドを書くことにしました。

## conda について
conda は [公式 Web ページ](https://docs.conda.io/en/latest/) にあるように、多くのプログラミング言語用のパッケージ、依存関係、環境管理ツールです。python の場合、 [pip](https://pip.pypa.io/en/stable/) に比べて、 conda は python ライブラリとその依存パッケージだけでなく、CUDA-Toolkit や cuDNN ライブラリのような他の言語で書かれた依存パッケージも環境にインストールすることが可能です。パッケージは環境にインストールされるため、1つのパッケージの複数のバージョンを環境毎に分けておき、切り替えて使えるという利点があります。conda のパッケージの削除は、アンインストールスクリプトなどが必要なく、以下のコマンドで特定の環境を削除するだけで十分です。

```
conda env remove -n <環境名>
```


## セットアップ
Ubuntu 20.04 LTSに[Miniconda](https://docs.conda.io/en/latest/miniconda.html)をインストールしたPCでセットアップをテストしました。セットアップは非常に簡単で、インターネット環境にもよりますが、10分程度で完了します。また、CUDA-ToolkitとcuDNNライブラリには最低2GBのメモリが必要です。Darknetのニューラルネットワークフレームワークを構築するための[要件](https://github.com/AlexeyAB/darknet#requirements-for-windows-linux-and-macos)にはOpenCVが含まれており、これもcondaを使用してインストールします。

### conda環境の作成

`YOLOv4_conda` という conda 環境を作成し、ターミナルで以下のコマンドを実行して python 3.7 と OpenCV 3.x をインストールします。

```
conda create -n YOLOv4_conda python=3.7 opencv=3
```

[CUDA-Toolkit ドキュメント](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#conda)の説明に従って、以下のターミナルコマンドで`nvidia`チャンネルからCUDA-ToolkitとcuDNNライブラリをインストールします。

```
conda install cuda cudnn -c nvidia -n YOLOv4_conda
```

`cuda` パッケージは、GPU で動作する Darknet CUDA コードをビルドするための **CUDA-Toolkit** と **NVCC** (NVIDIA CUDA Compiler) をインストールするパッケージです。
> `nvidia`チャンネルには`cuda-toolkit`パッケージもあり、一般にTensorflowやPytorchなどのpythonベースの機械学習フレームワークのGPU学習に使用されますが、このパッケージにはcuda-toolkitだけが含まれ、NVCCは含まれません。

最後に、新しく作成した環境に切り替えます。

```
conda activate YOLOv4_conda
```

### Darknet YOLOv4 プロジェクトをクローンします。
ターミナルでクローンしてリポジトリに切り替えます。

```
git clone https://github.com/AlexeyAB/darknet.git
cd darknet
```

今後の環境構築のために、ターミナルでクローンしたバージョンを確認します。

```
git --no-pager log --oneline -1
```

以下のようなコミットを返します。

```
8a0bf84 (HEAD -> master, origin/master, origin/HEAD) various fixes (#8398)
```

### Darknetの設定
テキストエディタに `Makefile` を読み込んで、以下の3箇所を修正します。

1. **1~4行目**：GPU ビルド、OpenCVとcuDNN を有効にする。
<pre>
GPU=<b>1</b>
CUDNN=<b>1</b>
CUDNN_HALF=<b>1</b>
OPENCV=<b>1</b>
</pre>
2. **27~58行目**：GPUの計算能力をアンコメントする（例：RTX 3090 GPU で学習する場合、以下をアンコメントしてください）。
<pre>
`#` GeForce RTX 3070, 3080, 3090
<b>ARCH= -gencode arch=compute_86,code=[sm_86,compute_86]</b>
</pre>
3. **80行目**: conda環境にインストールされているヘッダーファイルのパスを追加する ([CONDA_PREFIX](https://docs.conda.io/projects/conda-build/en/latest/user-guide/environment-variables.html#environment-variables-that-affect-the-build-process) 変数に格納されます)
<pre>
COMMON= -Iinclude/ -I3rdparty/stb/include <b>-I$(CONDA_PREFIX)/include</b>
</pre>

### Darknet のビルド
Darknet をビルドするのに必要なパッケージを `pkgconfig` で見つけられるように、ターミナルで検索パス（また[CONDA_PREFIX](https://docs.conda.io/projects/conda-build/en/latest/user-guide/environment-variables.html#environment-variables-that-affect-the-build-process) 変数を使います）に環境変数を一時的に追加します (現在のターミナルセッションでのみ)。

```
export PKG_CONFIG_PATH=$CONDA_PREFIX/lib/pkgconfig:$PKG_CONFIG_PATH
```

ビルドコマンドを実行する。

```
make
```

ビルド中にエラーが発生した場合、以下のような表示でビルド処理が停止します。

```
make: *** [Makefile:176: darknet] Error 1
```
ビルドエラーを修正するための解決策については、[トラブルシューティング](#トラブルシューティング)を参照してください。

## データセットの準備
[Darknet のドキュメント](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)では、データセットは `build/darknet/x64/data` フォルダに作成することになっていますが、実際には `darknet` リポジトリフォルダ内の任意の場所に配置することができます。ドキュメントには、`./darknet`実行ファイルとデータセットを格納するフォルダの場所についての説明に矛盾があります。私の環境の場合、`./darknet`実行ファイルはリポジトリのルートフォルダにあり、それに関しては、リポジトリのルートフォルダ内の`data`フォルダにデータセットを作成します。テスト用に、[COCO](https://cocodataset.org/)データセットから収集した画像で、小さなカスタムデータセット `street_views_yolo` を作成しました。

### アノテーションについて
データセットフォルダ内に、データセットで使われるすべてのクラス名を含むファイル `classes.txt` を作成します。今回のテストデータセットでは、3つのクラスを使っているので、`classes.txt`ファイルは次のような内容になっています。

```
car
bus
person
```

画像のアノテーションは、画像のファイル名と同じで、末尾が`.txt`のファイルに保存されます。アノテーションは、画像が保存されているフォルダに保存してください。アノテーションのフォーマットは、1行に1バウンディングボックスで、以下のような構造になっています。

```
<object-class> <x_center> <y_center> <width> <height>
```
`<object-class>` は， `classes.txt` ファイル内の0から (`car`) 始まるクラス番号です．`<x_center>` と `<y_center>` は、0 から 1 の間で正規化されたバウンディングボックスの中心点を含みます（それぞれ画像の幅と高さで割る）。`<width>` と `<height>` は、バウンディングボックスの幅と高さを表し、0から1の間で正規化されます。

![bbox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/3cc23c92-459e-0951-51f1-d7f2e390ac4c.png)

例として、2つのバウンディングボックス（`car`と`bus`）を持つ画像 `street1.jpg` に対するアノテーションは、以下の内容を持つファイル `street1.txt` に格納されます。

```
0 0.5072916666666667 0.54453125 0.43125 0.3484375
1 0.5135416666666667 0.2875 0.40625 0.26875
```

アノテーションは [Labelimg](https://github.com/tzutalin/labelImg) のようなアノテーションツールで行うことができ、すでに Darknet のアノテーションフォーマットが提供されています。インストールは、以下のコマンドを使用して `conda` で簡単に行うことができます。

```
conda install labelimg -c conda-forge
```

### データセットの分割
学習用画像と検証用画像は `train` と `val` フォルダに格納されます。すべての学習画像を一覧するファイル `train.txt` と検証用を一覧するファイル `val.txt` が必要です。一覧（JPEG画像の場合）は以下のコマンドで作成することができます。

```
ls train/*.jpg > train.txt
ls val/*.jpg > val.txt
```

データセットに関するすべての情報をまとめるためのインデックスファイル `index.txt` を以下の内容で作成します。

```
classes = 3
train  = train.txt
valid  = val.txt
names = classes.txt
backup = backup
```

`classes` は検出するクラスの数、 `train` と `valid` は学習画像と検証画像の一覧ファイルの場所、 `names` はクラス名を含むファイルの場所と `backup` はトレーニング中にモデルの重みが保存されるフォルダを指します。データセットのフォルダ構造は以下のようになっているはずです。

```
street_views_yolo/
├── classes.txt
├── index.txt
├── train/
│   ├── street1.jpg
│   ├── street1.txt
│   ├── ...
│   ├── street50.jpg
│   └── street50.txt
├── train.txt
├── val/
│   ├── street2.jpg
│   ├── street2.txt
│   ├── ...
│   ├── street10.jpg
│   └── street10.txt
├── val.txt
└── yolov4-custom.cfg
```

## 学習
モデルをゼロから学習させるのではなく、既に学習させたモデルの微調整をします。これは*転移学習*と呼ばれ、学習時間を大幅に短縮することができる。
上記のデータセットでYOLOv4モデルを学習させます。この[学習マニュアル](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)によると、検出するカスタムクラスの数に合わせて、学習済みモデルのいくつかのレイヤーを変更する必要があります。カスタムデータセットでYOLOv4を学習するための設定ファイル `yolov4-custom.cfg` が `cfg` フォルダの中に既に用意されているので、それを修正して使用します。

### ネットワークの修正
まず、`yolov4-custom.cfg`ファイルを以下のコマンドでデータセットフォルダにコピーします。

```
cp cfg/yolov4-custom.cfg data/street_views_yolo/.
```
次に、コピーした `yolov4-custom.cfg` の行を[学習マニュアル](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)に説明されているように調整します。
基本的には、設定ファイルにある3つのYOLO-layerを`yolo`というキーワードで検索するだけです。そして、3つの `[yolo]` レイヤーのそれぞれで、 `classes=80` を `classes=3` の3つのクラスの上記データセットを合わせて変更します。そしてそれぞれの `[yolo]` レイヤーの上にある `[convolutional]` レイヤーでは、 `filters=255` を `filters=24` ((クラス数 + 5) * 3) に変更します。最後に、必要であれば、 `[net]` 層の画像入力 `width` と `height` を、 `width=608` と `height=608` を 32 で割り切れるサイズ（例えば、 `width=416` と `height=416` ）に変更することも可能です。

### 学習済みの重みファイルのダウンロード
YOLOv4の場合、[学習マニュアル](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)でリンクされた学習済みの重みファイル `yolov4.conv.137` をダウンロードし、親フォルダ `data` に保存しておけば、他のデータセットで学習する際に学習済みの重みファイルを再利用できます。ファイルとフォルダの構成は以下のようになります。

```
darknet/
├── build/
├── cfg/
│   ├── ...
│   └── yolov4-custom.cfg
├── darknet
├── ...
└── data/
    ├── ...
    ├── street_views_yolo/
    │   ├── classes.txt
    │   ├── index.txt
    │   ├── train/
    │   ├── train.txt
    │   ├── val/
    │   ├── val.txt
    │   └── yolov4-custom.cfg
    └── yolov4.conv.137
```

### 学習の開始
`darknet` が conda 環境にインストールされた CUDA と cuDNN ライブラリを見つけるために、一時的に（現在のターミナルセッションでのみ）ターミナル内のライブラリパスに環境変数を追加します（ここでも [CONDA_PREFIX](https://docs.conda.io/projects/conda-build/en/latest/user-guide/environment-variables.html#environment-variables-that-affect-the-build-process) 変数をを使います）。

```
export LD_LIBRARY_PATH=$CONDA_PREFIX/lib:$LD_LIBRARY_PATH
```
データセット内の画像へのパスは `darknet` 実行ファイルからの相対パスに合わせる必要があります。現在、データセット内の画像へのパスは `street_views_yolo` のデータセットフォルダからの相対パスになっていますが、`darknet` 実行ファイルはリポジトリのルートフォルダに配置されています。
実行ファイルがデータセットのインデックスファイルに記載したデータセットの画像を見つけるために、データセットフォルダ内で実行します。まず、dataset フォルダに移動します。

```
cd data/street_views_yolo
```

そして、datasetのフォルダに相対パスで以下のコマンドで学習を開始します。

```
../../darknet detector train \
  index.txt \
  yolov4-custom.cfg \
  ../yolov4.conv.137 \
  -dont_show -map -mjpeg_port 8090
```

学習実行中にエラーが発生した場合は、[トラブルシューティング](#トラブルシューティング)を参照して対処してください。

`-mjpeg` のパラメータにより、ブラウザで URL `http://ip-address:8090` にアクセスすると、以下のようなグラフが表示され、学習の進捗確認ができます。

![chart_yolov4-custom.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1167699/cf8d8a2b-ed45-9bec-1773-16e29ae65f28.png)

> 青線は損失、赤線は mean Average Precision (mAP) を表す。

### 学習の成果

学習終了後、結果はデータセット内の `backup` フォルダに保存されます。

```
street_views_yolo/
└── backup/
    ├── yolov4-custom_10000.weights
    ├── yolov4-custom_best.weights
    ├── yolov4-custom_final.weights
    └── yolov4-custom_last.weights
```

`yolov4-custom_best.weights` は、平均精度が最も高い重みファイルになります。

## クリーンアップ
CUDA、cuDNN、OpenCVなどのインストールされているパッケージやライブラリを全てアンインストールするには、以下のコマンドで作成したconda環境を削除するだけです。

```
conda env remove -n YOLOv4_conda
```

そして、ローカルにクローンした `darknet` リポジトリフォルダを削除します。
これで完了です。

## トラブルシューティング
### Darknetの構築について
**問題**: Darknetのビルドが以下のようなエラーで停止する。

```
No package 'opencv' found
/usr/bin/ld: cannot find -lcudart
/usr/bin/ld: cannot find -lcublas
/usr/bin/ld: cannot find -lcurand
collect2: error: ld returned 1 exit status
make: *** [Makefile:176: darknet] Error 1
```

**解決方法**: 以下のコマンドで、`PKG_CONFIG_PATH`にconda環境を追加してください。この設定は、現在のターミナルセッションを閉じると消えます。新しいセッションでは再度コマンド実行することを忘れないでください。

```
export PKG_CONFIG_PATH=$CONDA_PREFIX/lib/pkgconfig:$PKG_CONFIG_PATH
```

### Darknetの学習について
**問題**: 学習が以下のようなエラーで停止する。

```
error while loading shared libraries: libopencv_dnn.so.<version>: cannot open shared object file: No such file or directory
``` 

**解決方法**: 以下のコマンドで、conda 環境を `LD_LIBRARY_PATH` に追加します。この設定は、現在のターミナルセッションを閉じると消えます。新しいセッションでは再度コマンド実行することを忘れないでください。

```
export LD_LIBRARY_PATH=$CONDA_PREFIX/lib:$LD_LIBRARY_PATH
```

**問題**: 学習が以下のようなエラーで停止する。
```
 Error in load_data_detection() - OpenCV Cannot load image <path_to_image>
```
**解決方法**: 
`train.txt`または`val.txt`の画像のパスが正しく設定されていません。画像を保存するフォルダ構造が両方のファイルの画像パスと一致しているかを確認してください。パスはデータセットフォルダからの相対パスになります。