PyTorch SSDで転移学習（2/2）

PyTorchで実装された物体検出モデルSSD（Single Shot Multibox Detector）の転移学習をやってみました。検出対象は、車とナンバープレートです。今回はその第２弾です。

# 目次

本記事の内容は、前記事の続きで、以下（★）です。

1. 作成したいモデル
1. PyTorch環境構築
1. SSD実装(PyTorch)
1. 学習データ作成、学習実行（★）
1. 検知（推論）実行（★）

前記事は以下を参照ください。

https://qiita.com/shima202102/items/a92a28ff277bcea9202f

また、実装は、以下githubに公開してますので、ダウンロードしてご利用ください。

https://github.com/Shimasaki2021/pytorch_ssd_trial

# 4. 学習データ作成、学習実行

SSDモデルを作るための学習データ（画像、正解データ）を収集／作成します。

## 4.1 画像収集

検出対象が、ナンバープレートと車なので、それらが映る映像から画像を収集しました。幸い、動画公開用に撮りためたドラレコ映像が手元に大量にあるので、それらからナンバープレートと車が映る画像を収集しました。

画像収集で留意した点は以下です。

- (a) 色々な車種の車、ナンバープレートを集める
- (b) 画像サイズ

### (a) 色々な車種の車、ナンバープレートを集める

SSDに限らず、機械学習の学習データは、検出対象のデータをできるだけ幅広く集めることが重要です。それによって、検出対象の多様な姿に共通する特徴をモデルが学習してくれるので、検出対象の未知の姿にも対応できるようになる、いわゆる **汎化性能が向上** します。

今回も、乗用車タイプの車だけでなく、ワンボックス、トラックなど、なるべく幅広く集めることを心掛けました。検出対象のタイプだけでなく、昼、夜、晴れ、雨等の外的環境も多様なものが集められれば、それに越したことはないと思います。

ただ、学習のアルゴリズムには大抵、上述した多様性を自動で作り出すデータオーギュメンテーション（データ拡張）という、データを水増しする処理が含まれています。今回のSSD実装にも、以下が含まれていました（図はデータオーギュメンテーションで生成された画像例）。

- ```RandomContrast``` : コントラストをランダムに変更
- ```RandomSaturation```、```RandomHue``` : 彩度、色相をランダムに変更
- ```Expand```、```RandomSampleCrop``` : スケーリング、切り抜き
- ```RandomMirror``` : ミラーリング（左右反転）

![data_augumentation例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/75386ad1-02e1-483e-a291-d762486075ad.png)

なので、これらで自動生成されるものは、そんなにバリエーションを増やさなくても大丈夫かと思います。上記だと、車の色や大きさ、位置（左か右か）は、そんなに色んなものを集めなくてもよいです。

今回の学習では、自分が法令順守で運転しているので、左車線の車の画像がかなり少ないのですが、右車線の車ばかりでも、ミラーリングで左右反転してくれるので、たまに現れる左車線の車にも対応できてます。

### (b) 画像サイズ

今回のSSDモデルは、入力画像の解像度が```300x300```とかなり小さく、FHD（```1920x1080```）のような大きな画像を学習の入力として与えてしまうと、前処理で```1920x1080 → 300x300```というかなり強い縮小がかかってしまいます。

画像を縮小すると、当然、中に映るものも縮小されてしまい、特徴が消失するリスクがあります。特に、今回の検出対象であるナンバープレートは小さいので、そのリスクが大きいです。
なので、画像サイズは、等倍の```300x300```や、少し大きめの```350x350```で収集（映像から切り出し）しました。
ただ、このサイズばかりだと、トラックとかバスなどの大きな車両は１枚に収まらないことが多いので、大きな車両用に```600x500```も加えました。

![画像サイズ例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/cd731d69-6782-4578-b307-2e0899db32ab.png)


## 4.2 正解付け（アノテーション）

4.1で集めた画像に対し、どういう検出対象がどこにいるかを教える作業です。これをアノテーション（正解付け）といいます。

具体的には、画像上の検出対象の領域を枠（外接矩形）で囲み、何なのかをラベル（文字列）で与えていきます（下図例。車を枠で囲み「car」というラベルを付与）。

今回は、この作業をlabelImg（下図例）というツールを使って行いました。前記事の「2.PyTorch環境構築」では、このlabelImgのインストールもしてますので、2章で環境構築を行えば、labelImgが使える状態になってます。

![labelImg_open_od_cars_sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/7ad68bbf-b743-4e3b-af65-f17b3ae95564.png)

正解付けした結果はxmlファイルに保存されます。画像1枚につき、１つのxmlファイルが作成されます。上図例の結果は、下記xmlファイルになります。

```path```タグに、ファイルのフルパスが書き込まれていますが、今回のSSD実装ソースではここは参照しないので、xml生成後にディレクトリを移動しても大丈夫です。labelImgも参照してないようで、別のディレクトリに移動しても問題なくロードできてました。

```od_cars_add8_2_urban_exp_580_200_300x300_F09660.xml
<annotation>
	<folder>od_cars</folder>
	<filename>od_cars_add8_2_urban_exp_580_200_300x300_F09660.jpg</filename>
	<path>/home/simasaki/work/pytorch_ssd_trial/data/od_cars/od_cars_add8_2_urban_exp_580_200_300x300_F09660.jpg</path>
	<source>
		<database>Unknown</database>
	</source>
	<size>
		<width>300</width>
		<height>300</height>
		<depth>3</depth>
	</size>
	<segmented>0</segmented>
	<object>
		<name>car</name>
		<pose>Unspecified</pose>
		<truncated>0</truncated>
		<difficult>0</difficult>
		<bndbox>
			<xmin>124</xmin>
			<ymin>87</ymin>
			<xmax>184</xmax>
			<ymax>145</ymax>
		</bndbox>
	</object>
	<object>
		<name>car</name>
		<pose>Unspecified</pose>
		<truncated>0</truncated>
		<difficult>0</difficult>
		<bndbox>
			<xmin>43</xmin>
			<ymin>104</ymin>
			<xmax>80</xmax>
			<ymax>134</ymax>
		</bndbox>
	</object>
</annotation>
```
画像、xmlファイルはすべて、１つのディレクトリにフラットにおいてください（下図例）。階層構造にしてしまうと読めないのでご注意ください。

![学習データ格納ディレクトリ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/bf385e2b-4809-4b71-aaf3-58632b63377b.png)

このアノテーションは、モデルの出来栄えを左右する重要な工程です。
枠とラベルを付ける作業自体は、labelImg等のツールを使えば簡単ですが、数が多くなってくると、付け間違いなども起こってしまいます。

それをチェックするツールの１つとして、今回のSSD実装に、学習データに対して検知（推論）を行い、正解枠／ラベルと検知した枠／ラベルを重畳描画する機能を付けてみました（```predict_ssd.py```、結果例は下図）。数値は、検知にどれだけ自信があるかを示す確信度confです（```（自信なし） 0.0 ～ 1.0（自信あり）```）。緑が正解枠／ラベルで、黄色が検知枠／ラベルです。

![アノテーション確認機能_実行例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/b15fff6d-06c1-4dab-a87d-5c40acfc2a7d.png)

学習データそのものを検知させるので、confは本来、```1.0（自信あり）```に近くなってしかるべきですが、ラベル／枠のつけ間違い等、まずい点があると、低い値（0.6付近）になっているものや、未検知（緑枠しかない）も出てきます。そういう正解枠に対して、

- ラベル／枠のつけ間違いを修正する
- 付け間違い以外で、どうしても検知したいものなら、数を増やす
- どちらでもよいものなら、消去する

などを行い、アノテーションの精度を高めていきます。こういう機能があれば、それを幾分効率的に実施できるかと思います。

ただ、「どちらでもよいものなら、消去する」に関しては、１点念頭に置いておくべきことがあります。それは以下です。

:::note info
正解枠を付けていない箇所は「背景」として学習される
:::

これを念頭に置かずに適当に正解枠を消していると、似たような画像に対して、正解枠を付けたものとつけないものが混在してしまいがちですが、この状態が、学習で大きなマイナスになります。


SSDモデルの学習は、```300x300```の画像全体に大量の矩形をちりばめ、その矩形毎に、中の絵がどのクラス（ラベル）に属するのか、どういうサイズなのかを学習していきます。正解枠との重なりが小さい矩形は、正解＝「背景」クラスの物体として誤差が算出され、その誤差が少なくなる方向にパラメータが調整されていきます。

正解枠を付けるのは、画像1枚につきせいぜい数個なので、正解枠との重なりが小さい矩形の数は、重なりのある矩形と比べ、桁違いに多くなります。それだと背景ばかりを学習することになってしまうので、背景の矩形の数を絞り込む処理が行われます（Hard Negative Mining）。絞り込みの際は、誤検出した矩形、つまり、背景を「背景でない」と判断した矩形が優先されます。

ということは、似た画像に対して、正解枠を付けたものとつけないものを混在させてしまうと、<ins>つけなかったほうが誤検出となってしまい、Hard Negative Miningで背景として優先的に選ばれてしまうので、違いを学習しようと必死に努力してしまいます。</ins>

本当に違いを学習させたいならよいのですが、気まぐれでそうなっただけなら、この努力はパワーの無駄遣いですし、<ins>学習がなかなか収束してくれなくなります。</ins>

長くなりましたが、「どちらでもよいものなら、消去する」を実施する場合、上記のような混在状態にならないよう、正解枠を消去する/しないの線引きをちゃんと定義し、一貫した方針で徹底的にやったほうがよいです。ただ、実際にやってみるとわかりますが、一貫性を保つのは至難の業です。なので、おススメは以下です。

:::note info
どちらでもよいものは、正解枠だけでなく画像ごと消去する
:::

ファイルごと削除するのが簡単ですが、画像の中に学習させたいものも交じっている場合は、Windowsペイント等の画像編集ツールで部分的に消去すればよいです。最近のペイントは、下図のようなインテリジェンスな消し方が簡単にできるようになっているので、わざわざ高価な編集ツールを買わなくてもこれで十分です。

![ペイントでの消去例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/92d0705f-d6b5-4dea-96ce-b488e960dfe2.png)


## 4.3 学習実行

前節で作成した学習データを、2章で説明したSSD実装に学習させます。

- (a) VGG16の学習済みパラメータをダウンロード
- (b) 学習データの場所等をソースに設定
- (c) 学習ソース実行

### (a) VGG16の学習済みパラメータをダウンロード

今回は、一からモデルを学習するのではなく、あらかじめ大量のデータを学習させたパラメータをベースに、今回の検出対象（車、ナンバープレート）を学習させる、いわゆる転移学習を行います。これによって、一から学習よりも少ないデータで、モデルを作成することができます。

学習済パラメータとして、以下の```vgg16_reducedfc.pth```ファイルを使わせていただきました。以下URLからブラウザ等でダウンロードし、weightsディレクトリに配置します。

https://s3.amazonaws.com/amdegroot-models/vgg16_reducedfc.pth

### (b) 学習データの場所等をソースに設定

前節で作成した学習データの場所等を、学習ソースである```train_ssd.py```の以下に記述します。

```python:train_ssd.py （抜粋）
# 学習データを置いたディレクトリパス
data_path    = "./data/od_cars"

# 学習クラス名　※アノテーション工程で付与したラベル名を記載
voc_classes  = ["car","number"]

# SSDモデルでパラメータ（重み）を更新しないレイヤー（入力層～freeze_layerまでの重みは更新しない）
freeze_layer = 5

# epoch数（引数指定なしの場合のDefault値）
num_epochs = 500

# バッチサイズ
batch_size = 16

# 検証用画像の割合（全体のtest_rateを検証用画像にする）
test_rate  = 0.1
```

検出対象に合わせて編集が必須なのは上２つ（「学習データを置いたディレクトリパス」「学習クラス名」）で、あとは（おそらく）そのままでも使えるかと思います。

### (c) 学習ソース実行

ここまでできたら、いよいよ学習開始です。
ターミナルから以下入力すれば、学習を開始します。上述「epoch数（```num_epochs```）」回の実行が完了した時点で、学習が終了します。

```sh:Ubuntuターミナル
./train_ssd.py
```
```sh:Anaconda Powershellプロンプト
python train_ssd.py
```

学習中は、下図のように、ターミナルに進捗が表示されます。epoch毎（全学習データを1回参照する毎）に、```train_Loss```、```val_Loss```が出力されます。これは、今のパラメータで検知（推論）をかけたときの損失（誤差値）です。この数値が小さければ小さいほど、精度よく検知できている、ということになります。

学習がうまくいっていれば、epochを重ねるごとに、数値が小さくなります。```train_Loss```は、学習に使ったデータに対して検知をかけた値、```val_Loss```は、学習には使わず検証用に取っておいたデータに対して検知をかけた値になります。なので、```val_Loss```が小さいほど、未知のデータに対する精度が高い（汎化性能が高い）ということになります。

![学習時のターミナル例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/8874843a-95ad-4243-b28c-82f5edc1d934.png)

結果は、weightsディレクトリに、pthファイルと、ラベル（クラス）が書かれたtxtが出力されます。pthファイルが、学習したモデルのパラメータです。```val_Loss```最小のときのパラメータが出力されます。

学習中の```train_Loss```、```val_Loss```をグラフ化したのが下図です。```out weight```は、パラメータ出力時の値です。

![train_loss_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/be029db4-307b-49c1-8fc5-26f791f7cdbf.png)

epoch200以上ではほぼ横ばいに見えますが、よく見ると、少しずつではありますが、```train_Loss```、```val_Loss```ともに減少傾向が続いてます。もう少しepochを増やせば、少しだけ精度UPするかもしれません。

学習にかかる時間は、PC環境や学習データ数に大きく依存します。
参考までに、以下PC環境、学習データ数で、学習するのに、約3時間かかりました。

- PC環境
  - CPU: AMD Ryzen 7 3700X (3.60 GHz)
  - GPU: NVIDIA GeForce GTX 1660 SUPER
  - OS: Windows 11 Home (24H2） , WSL2 + Ubuntu24.04

- 学習データ数（※検証用（1割）込み）
  - 画像数: 530
  - 物体数（枠の数）: 1394　（車：857、ナンバープレート：537）


# 5. 検知（推論）実行

上述4で学習したパラメータを使って、検知（推論）を行います。

- (a) 検知範囲をソースに設定
- (b) 検知実行

## (a) 検知範囲をソースに設定

ソース```predict_ssd.py```の末尾付近の以下で検知範囲を編集できます。ここは、検知させたい画像の解像度や、検出対象に合わせて設定します。
入力画像全域にすることもできます。

以下例では、```1280x720```の画像上で、検知対象である車やナンバープレートがありそうな領域を４つ設定しています。

```predict_ssd.py
# 検出範囲
#   (1280x720を)300x300/350x350に切り出し
img_procs = [ImageProc(180, 250, 530, 600), 
             ImageProc(480, 200, 780, 500), 
             ImageProc(730, 200, 1030, 500), 
             ImageProc(930, 250, 1280, 600)] 

# 入力画像全域を検出範囲にする場合は以下を有効化
# img_procs = [ImageProc()] 
```

## (b) 検知実行

ターミナルから以下入力すれば、検知（推論）を実行します。

```sh:Ubuntuターミナル
./predict_ssd.py [動画(mp4) or 画像ファイルパス]
```
```sh:Anaconda Powershellプロンプト
python predict_ssd.py [動画(mp4) or 画像ファイルパス]
```

実行結果例
```
./predict_ssd.py data/od_cars_org_F00000.jpg
```
![od_cars_org_F00000_result.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/6403cfe2-aadd-453a-8c33-e593e2d4ac80.jpeg)

上述4.2節で触れた、正解枠／ラベルと検知した枠／ラベルを重畳描画する機能も、```predict_ssd.py```でできます。

```sh:Ubuntuターミナル
./predict_ssd.py [学習データディレクトリ]
```
```sh:Anaconda Powershellプロンプト
python predict_ssd.py [学習データディレクトリ]
```

実行結果例
```
./predict_ssd.py data/od_cars
```

※緑：正解枠／ラベル、黄色：検知枠／ラベル
![アノテーション確認機能_実行例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299377/95e73e2a-2da0-412f-9fa1-a2181383cfff.png)
※ナンバープレート部分の塗りつぶしは記事掲載用です。検知実行時には塗りつぶしなしの画像を入力してます。

この結果も適宜参照しながら、アノテーションを修正し、学習を行う、というスパイラルで、モデルを完成形に近づけていきます。

ちなみに今回学習したモデルは、ナンバープレートの未検出が散見されてはいるのですが（上図例（右の黒い車のナンバープレート）など）、文字／数字がはっきり視認できるものはほぼ検出できているので、とりあえず現時点ではこれでよしとしました。ナンバープレートを自動でぼかすアプリを作る／使う過程で、アプリと一緒に適宜ブラッシュアップしていこうと思ってます。

（追記1）ナンバープレートを自動でぼかすアプリを作成しました。以下記事をぜひご覧ください。

https://qiita.com/shima202102/items/0bea767122c288021194

（追記2）SSDのベースネットワークがmobilenet-v2-liteのSSDモデルの転移学習にも挑戦しました。こちらもぜひご覧ください。

https://qiita.com/shima202102/items/e14468739ef7f93b8b77

# おわりに

今回、SSDモデルの転移学習に、初めて挑戦しました。環境構築（pythonバージョンエラー）にも悩まされたりしましたが、やはり一番苦戦したのは、正解付け（アノテーション）です。

はじめは、「正解枠を付けていない箇所は「背景」として学習される」という事実を知らず、多様性を気にして、同じ車に対して枠を付けたりつけなかったりといったことをして、何も検出できないモデルが作成されてしまったりしてました。

上述「正解枠を付けていない箇所は「背景」として学習される」もそうですが、実際にやってみて初めて気づかされることも色々あったように思います。やはり経験は大事ですね。読者のみなさまも、ぜひ、挑戦してみてください。

とはいえ、挑戦するにあたって、試行錯誤はできるだけ少なくなるに越したことはないので、本記事が、試行錯誤を減らす手助けになれば幸いです。