OpenCVのdnnモジュールのクラス分類で信頼度を[0.0-1.0]にする

この記事は[OpenCV Advent Calendar 2022](https://qiita.com/advent-calendar/2022/opencv)の2日目の記事です。  

# 概要

今年は「[OpenCVではじめよう ディープラーニングによる画像認識](https://gihyo.jp/book/2022/978-4-297-12775-6)」（技術評論社）という書籍を出版しました。OpenCVのdnnモジュールを利用してディープラーニングによる画像認識を手軽に試してみようという内容になっています。（宣伝）  

この記事では書籍には間に合わなかったOpenCVのdnnモジュールのちょっとした問題の修正や問題を回避する方法を紹介します。あわせて、書籍では省略したPyTorchからONNXへエクスポートする方法などを軽く紹介します。  

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34834/cb1c5feb-8e89-ee6e-6e0b-644016d4846f.jpeg" width="350">

# サンプルプログラム

サンプルプログラムのノートブックは以下で公開しています。  
Google Colaboratoryですぐに動かして試すことができます。  

* [OpenCV Advent Calendar 2022 2022/12/02.ipynb](https://colab.research.google.com/drive/1cP2kJWZBfgLuxIUEva8C3qEWSByu2_qI?usp=sharing)

# PyTorch（torchvision）のクラス分類の学習済みモデルをONNXにエクスポートする

[torchvision](https://pytorch.org/vision/0.14/)の学習済みモデルをONNXにエクスポートします。  
まずは`torch`、`torchvision`をインポートします。  

```python
# インポート
import torch
import torchvision
from torchvision.models import mobilenet_v3_large, MobileNet_V3_Large_Weights
```

`torchvision.models`からモデルを重み付きで取得します。
torchvision v0.12.0まではpretrained=Trueという引数を渡していましたが、v0.13.0以降は[仕様が変更された](https://pytorch.org/vision/0.14/models.html#initializing-pre-trained-models)ようです。  
ここでは[MobileNet V3](https://pytorch.org/vision/0.14/models/generated/torchvision.models.mobilenet_v3_large.html#torchvision.models.mobilenet_v3_large)というクラス分類のモデルを利用しています。  

```python
# 学習済みモデルの取得
weights = torchvision.models.MobileNet_V3_Large_Weights.DEFAULT
model = torchvision.models.mobilenet_v3_large(weights=weights)
# model = torchvision.models.mobilenet_v3_large(pretrained=True) # 非推奨
```

`torch.onnx.export()`にダミーの入力テンソルなどを指定してONNXにエクスポートします。
入力テンソルの形状は各モデルのページに記載があります。  

```python
# 学習済みモデルをONNXにエクスポート
dummy_tensor = torch.randn((1, 3, 224, 224))
torch.onnx.export(model, dummy_tensor, 'mobilenet_v3_large.onnx', verbose=True, opset_version=13, input_names=['input'], output_names=['output'])
```

ラベルのリストは`torchvision.models.weights.meta['categories']`に含まれています。
テキストファイルに書き出しておきましょう。

```python
# ラベルリストを保存
with open('imagenet.names','w') as f:
    f.writelines([f"{label}\n" for label in weights.meta['categories']])
```

# モデルの構造を確認する

出力されたONNXを[Netron](https://netron.app/)で可視化します。  
以下の画像はネットワークの最終層付近の構造です。  
Softmaxが含まれていないことが今回のポイントです。

![mobilenet_v3_large.onnx.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34834/39aae4ff-f9dc-ceb7-dc15-e8fec46aa821.png)

# OpenCVのdnnモジュールで推論する

エクスポートしたONNXをOpenCVのdnnモジュールで推論してみます。（一部省略）  
詳しくは書籍「[OpenCVではじめよう ディープラーニングによる画像認識](https://amzn.to/3V84XXx)」を参照してください。 

```python
# インポート
import cv2

# 画像を読み込む
image = cv2.imread('yorkie.jpg')

# モデルを読み込む
model = cv2.dnn_ClassificationModel('mobilenet_v3_large.onnx')

# モデルの入力パラメーターを設定する
scale = 1.0 / 255.0
size = (224, 224)
mean = tuple(map(lambda x: x * 255.0, (0.406 , 0.456, 0.485)))
swap = True
crop = True
model.setInputParams(scale, size, mean, swap, crop)

# クラス分類して信頼度が最も高いクラスを取得する
class_id, confidence = model.classify(image)

# 結果を表示する
top_1 = f"{class_id} {classes[class_id]} ({confidence:.3f})"
print(top_1)
```

とても可愛いヨークシャーテリアの画像なので「187 Yorkshire terrier」と正しく推論できています。

![result1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34834/d755ac7b-0976-2c97-fd01-624c71d41ce0.png)

# 信頼度の出力範囲を[0.0-1.0]にする

ここからがこの記事の本題です。
OpenCVのdnnモジュールのクラス分類の出力結果を見るとTop1のラベルは正しそうです。
しかし、信頼度が[0.0-1.0]の範囲の値になっておらず、結果が信頼できるのか判断できません。
これは後処理にSoftmaxが組み込まれていないことが原因です。

```
187 Yorkshire terrier (8.077)
```

OpenCV 4.5.5のcv2.dnn_ClassificationModel()ではモデルにSoftmaxレイヤーが組み込まれていることを前提としていました。しかし、PyTorch（torchvision）のクラス分類のモデルにはSoftmaxが組み込まれていません。

これを解決する方法として2つのアプローチがあります。  

## 解決方法1. cv2.dnn_ClassificationModel.setEnableSoftmaxPostProcessing()

OpenCV 4.6.0では`cv2.dnn_ClassificationModel.setEnableSoftmaxPostProcessing()`が追加されました。`True`に設定することで、Softmaxレイヤーが組み込まれていないモデルでもSoftmaxが後処理された結果を得ることができます。

本来であればこの方法を書籍で紹介したかったのですが、この機能を追加する[Pull Request #21692](https://github.com/opencv/opencv/pull/21692)のMergeが間に合いませんでした。ちなみにこれは私が実装しました。書籍の執筆を通してこうした不足している機能の追加やバグの修正を行えたのはよかったです。  

### 1-1. OpenCVのdnnモジュールでSoftmaxの後処理を有効に設定して推論する

次の一行を追加して実行してみましょう。  
`model.setEnableSoftmaxPostProcessing(True)`とすることで後処理としてSoftmaxされた結果が出力されるようになります。

```python
# 後処理のSoftmaxを有効にする
model.setEnableSoftmaxPostProcessing(True)
```

結果を確認すると信頼度の値が[0.0-1.0]の範囲になっていることがわかります。
これでどれくらい結果が信頼できるのか判断ができるようになりました。  

![result2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34834/84a82f83-cd43-1c5c-98ec-c994bdf2420b.png)

## 解決方法2. ONNXにSoftmaxレイヤーを組み込む

さて、お気づきのようにモデルの最終層にSoftmaxレイヤーが含まれていれさえすればこの問題は解決しますね。（脳筋発想
ONNXにSoftmaxレイヤーを挿入する編集をしましょう。

最近ではPINTOさんが[simple-onnx-processing-tools](https://github.com/PINTO0309/simple-onnx-processing-tools)というツールを作成されています。  
この記事ではこのツールを使ってONNXを編集してSoftmaxレイヤーを挿入します。   
（昔はJSONに変換して編集したりしてた記憶があるけど、ツールが充実してきてずいぶん簡単になりました。） 

simple-onnx-processing-toolsは様々なツールが含まれています。  
この中からオペレーターを生成する[sog4onnx](https://github.com/PINTO0309/sog4onnx)とモデルを結合する[snc4onnx](https://github.com/PINTO0309/snc4onnx)を使います。  
sog4onnxでSoftmaxのみのONNXを生成、snc4onnxで対象のモデルの最終層にSoftmaxのみのONNXを結合します。  

### 2-1. SoftmaxのみのONNXを生成（sog4onnx）

まずは、sog4onnxでSoftmaxのみのONNXを生成します。  
[オペレーターの一覧](https://github.com/onnx/onnx/blob/main/docs/Operators.md)と対象のモデルの情報を確認しながら以下のオプションを指定します。  
オペレーターの名前や入出力の名前は対象のモデルと被らないようにしておきましょう。  

* **--op_type**：オペレーターの種類
* **--opset**：オペレーターのバージョン
* **--op_name**：オペレーターの名前
* **--input_variables**：オペレーターの入力（名前、型、形状）
* **--output_variables**：オペレーターの出力（名前、型、形状）
* **--output_onnx_file_path**：出力ファイルのパス
* **--non_verbose**：エラーログのみの表示

```bash
# SoftmaxのみのONNXを生成
sog4onnx --op_type Softmax --opset 13 --op_name Softmax --input_variables input_softmax float32 [1,1000] --output_variables output_softmax float32 [1,1000] --output_onnx_file_path Softmax.onnx --non_verbose
```

このようなSoftmaxのみのONNXが生成されます。

![Softmax.onnx.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34834/75bd0760-2f57-8dbd-91f8-b2f38a97be22.png)


### 2-2. モデルの最終層にSoftmaxのみのONNXを結合（snc4onnx）

つぎに、snc4onnxで対象のモデルの最終層にsna4onnxで生成したSoftmaxのみのONNXを結合します。  
モデルの情報を確認しながら以下のオプションを指定します。

* **--input_onnx_file_path**：入力ファイルのパス
* **--srcop_destop**：結合元と結合先の入出力の名前（input1 output1 input2 output2 ...の並び順で指定）
* **--output_onnx_file_path**：出力ファイルのパス

```bash
# モデルを結合する
snc4onnx --input_onnx_file_path mobilenet_v3_large.onnx Softmax.onnx --srcop_destop output input_softmax --output_onnx_file_path mobilenet_v3_large_with_softmax.onnx
```

### 2-3. Softmaxレイヤーを組み込んだモデルの構造を確認する

さて、Softmaxレイヤーを組み込んだモデルの構造をNetronで確認してみましょう。  
正しくレイヤーが挿入されていることが確認できます。  

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34834/b530d7f5-06a2-ecc0-786f-88990f5f20e6.png)

### 2-4. OpenCVのdnnモジュールでSoftmaxの後処理を無効に設定して推論する

それでは、Softmaxレイヤーを組み込んだモデルを使ってOpenCVのdnnモジュールで推論してみます。 

```python
# モデルを読み込む
# model = cv2.dnn_ClassificationModel('mobilenet_v3_large.onnx') # Softmaxなし
model = cv2.dnn_ClassificationModel('mobilenet_v3_large_with_softmax.onnx') # Softmaxあり
```

このとき、`model.setEnableSoftmaxPostProcessing(False)`で`cv2.dnn_ClassificationModel`の後処理を無効にしておきます。（デフォルトでOFFです）

```python
# 後処理のSoftmaxを無効にする
model.setEnableSoftmaxPostProcessing(False)
```

信頼度が[0.0-1.0]の範囲の値になっていることが確認できます。

![result2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34834/5aa453d1-f8e0-3797-7493-c0427fd2e9a6.png)

# まとめ

この記事では書籍「[OpenCVではじめよう ディープラーニングによる画像認識](https://gihyo.jp/book/2022/978-4-297-12775-6)」に掲載するには間に合わなかった、OpenCVのdnnモジュールのクラス分類を利用したときの信頼度の範囲についての問題と解決する方法を紹介しました。手軽に使えるOpenCVのdnnモジュールを是非使ってみてください。

明日の[OpenCV Advent Calendar 2022](https://qiita.com/advent-calendar/2022/opencv)はKazuma_Kikuyaさんの「[【画像認識】AIひろゆきがスプラトゥーン3のキルデス報告してくれるアプリを作った話](https://qiita.com/Kazuma_Kikuya/items/71644455015a30a01571)」です。