Azure Custom Visionで学習した画像分類モデルを「AE2100」で推論してみよう

# 要約
* 本記事では、マイクロソフト社のクラウドサービスであるAzureのCustom Visionを使用して、画像分類モデルを作成します。
* その画像分類モデルをAIエッジコンピューター「AE2100」へデプロイして、推論を実行するまでを説明します。

# はじめに
Custom Visionとは、GUI操作だけで簡単にAI学習モデルの作成ができるマイクロソフト社のSaaS型のクラウドサービスで、製造業の検品作業や倉庫の類似部品の識別(管理)などで利用されています。
Custom Visionで学習モデルを作成すると、専用のURLが割り当てられます。Azure上で推論させる場合は、このURLに対し推論させたい画像データをREST APIで送信すると、推論結果を取得することができます。
しかし、Azureは、従量課金制のため、Azure上で推論(API呼び出し)される度にコストがかかりまいます。また、クラウドでのAIの処理は、クラウドに画像データを送信する必要があるため、リアルタイムな推論ができないデメリットもあります。

本記事では、Custom Visionで作成した学習モデルをAE2100へデプロイし、推論実行する方法を解説します。これにより、無料かつリアルタイムで推論することができます。

# 環境
構成イメージは以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/062d411d-e11c-82e2-96fe-00ee80386567.png)

用意するものは、以下の３つです。
①カメラによる撮影または、[kaggle等](https://www.kaggle.com/datasets?search=inspection)のオープンデータをダウンロードして、学習させる画像を用意してください。
本記事では、製造業における検品作業を想定して、[ゴルフボールの画像](https://github.com/UnknownUser1025/DataSet)を使用したいと思います。

②Custom Visionで学習モデルを作成するために、[Azureのアカウント](https://azure.microsoft.com/ja-jp/free/search/?&ef_id=CjwKCAiAxJSPBhAoEiwAeO_fP141mqh4CKvw2XthgMzDmU8gEQDxW5FqDpT2rBHBT9AhPnqceRXgwxoCalQQAvD_BwE:G:s&OCID=AID2200208_SEM_CjwKCAiAxJSPBhAoEiwAeO_fP141mqh4CKvw2XthgMzDmU8gEQDxW5FqDpT2rBHBT9AhPnqceRXgwxoCalQQAvD_BwE:G:s&gclid=CjwKCAiAxJSPBhAoEiwAeO_fP141mqh4CKvw2XthgMzDmU8gEQDxW5FqDpT2rBHBT9AhPnqceRXgwxoCalQQAvD_BwE)を用意してください。

③[AE2100のマニュアル(SDK取扱説明書 DeepLearning編)](https://www.oki.com/jp/AIedge/support/)に従って、AE2100の環境と開発環境ご用意してください。
なお、AE2100および開発環境は以下のものを前提としています。
【開発環境】
OS：Ubuntu20.04 LTS　　　OpenVINO：2021.4.1 LTS

【AE2100】
本体ファーム：V3.6.0（HDDL Daemon：OpenVINO2021.4.1 LTS）
標準コンテナ：OpenVINO有（Ubuntu20.04版）2021年10月版

# プロジェクトの作成(Custom Vision)
さっそく、[Custom Visionのwebページ](https://www.customvision.ai/)に移動し、[サインイン]の後、[NEW PROJECT]を選択して、新しいプロジェクトを作成していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/bf41a102-67f6-d107-b18b-c113114716e8.png)
Custom Vision サービスを使用するには、[Azure で Custom Vision Training リソースと Prediction リソースを作成](https://portal.azure.com/?microsoft_azure_marketplace_ItemHideKey=microsoft_azure_cognitiveservices_customvision#create/Microsoft.CognitiveServicesCustomVision)する必要がありますので、Create new project＞Resourceの[Create new]をクリックして、リソースを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/97bd21aa-5ce1-8f4f-9cb3-162408159a69.png)

この時、「Name」は、Azure上で一意な名前にしなければならない点と、「pricing Tier」の選択によって、[使用できる機能と料金](https://azure.microsoft.com/ja-jp/pricing/details/cognitive-services/custom-vision-service/)が変化する点を注意してください。今回は、あくまでお試しなので、無料で利用できる「F0」を選択しています。

リソースの作成が完了したら、プロジェクトの詳細を設定します。ここではAIの種類を選択します。
今回、ゴルフボールの正しい並び(OK)と誤った並び(NG)の判定をおこなうために、「Project Type」は”Classification”を選択します。
また、AE2100へデプロイ可能なAIモデルを出力するためには、[「Domains」](https://docs.microsoft.com/en-us/azure/cognitive-services/custom-vision-service/select-domain)は”General(compact)”もしくは”General(compact)[S1]”を選択する必要があります。今回は、精度の高い”General(compact)[S1]”を選択します。
残りの選択項目は、デフォルトのままで構いません。

プロジェクトの詳細を設定後[Create project]をクリックすると、以下のような画面に遷移します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/44c85daa-fe6c-08ee-7b7b-892b15c49c25.png)


# 学習モデルの作成(Custom Vision)
続いて、[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/cognitive-services/custom-vision-service/getting-started-build-a-classifier)の通りに、データセット(学習させたい画像)をアップロードしていきます。
公式ドキュメントでは、タグごとに最低30枚の画像を学習することが推奨になりますが、ここではOKとNGの画像をそれぞれ10枚ずつ学習します。
なお、正しい並び(OK)は、左から黄、白、橙、白の順で列毎に同じ色のボールが並んでおり、それ以外の並びの画像は誤った並び(NG)としております。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/05d3e389-3297-1377-0b33-5bb41b5026a5.png)

[正しい並び(OK)と誤った並び(NG)の画像(「1_1_OK_images_TRAIN」、「1_2_NG_images_TRAIN」フォルダの画像)](https://github.com/UnknownUser1025/DataSet)に対してタグ付けし、[Train]をクリックして学習を開始します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/b26754e1-8d8f-96ed-b97b-acc7d867da10.png)
3分程度で学習が完了します。その後、[テスト画像(「2_1_OK_images_TEST_to_TRAIN」、「2_2_NG_images_TEST_to_TRAIN」フォルダの画像)](https://github.com/UnknownUser1025/DataSet)のアップロードを行い、[Quick Test]でモデルの評価を実施します。
「OK」を正しく「OK」と予測した画像が9/10枚、
「NG」を正しく「NG」と予測した画像が2/10枚でした。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/1b0d004f-1b18-5db2-87b0-2f1cf826f9ed.png)

「NG」の正解率が低いので、「Quick Test」で使用したテスト画像に正しいタグを付けて再度学習します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/7f1187d7-b8d9-50cf-669f-5ae4ba08620f.png)

2回目は、「OK」を正しく「OK」と予測した画像が10/10枚、「NG」を正しく「NG」と予測した画像が7/10枚でしたので、[NGのテスト画像(「3_2_NG_images_TEST_to_TRAIN」フォルダの画像)](https://github.com/UnknownUser1025/DataSet)10枚のみを再度学習させました。
そうすると、「OK」、「NG」の両方のテスト画像を10/10枚正しく予測させることができました!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/05f7c89d-4e24-0ea7-1a18-45d5d627e2f4.png)



[学習モデルの精度を上げるには](https://docs.microsoft.com/ja-jp/azure/cognitive-services/custom-vision-service/getting-started-improving-your-classifier)、いろいろな方法があるようですので、自作の学習モデルを作成する際は、試行錯誤してみてください。

# 学習モデルのエクスポート(Custom Vision)
それでは、先ほど作成した学習モデルをエクスポートしていきます。
「performance」>「Export」>「OpenVino」>「Export」の順にクリックし、AE2100で動作させるのに最適なOpenVINO形式で学習モデルをエクスポートします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/b120b1a0-558e-328f-797b-b4788fe37c67.png)

ダウンロードして開いたzipファイルが、以下のようになっていれば問題ありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/6620a6f9-95bd-7d88-3d8b-4b4bf13b27f0.png)




# AE2100へのデプロイ
エクスポートした学習モデルをAE2100へデプロイします。
学習モデルのzipファイル、テスト画像、[サンプルコード(predict.py)](https://github.com/Azure-Samples/customvision-export-samples/blob/main/samples/python/openvino/classification/predict.py)をzipに圧縮して、[SCP](https://winscp.net/eng/docs/lang:jp)でAE2100へ転送します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/c68f599b-b45e-2ef1-d255-5ddfaaf15307.png)

なお、サンプルコード(predict.py)は、OpenVINO形式の学習モデルを読み込み、入力画像に対してラベル毎の推論結果(確率)を表示するプログラムになっています。
<details><summary>サンプルコード(predict.py)の確認は、ここをクリック</summary><div>

```python
import argparse
import pathlib
import numpy as np
import PIL.Image
from openvino.inference_engine import IECore


class Model:
    def __init__(self, xml_filepath, bin_filepath):
        ie = IECore()
        net = ie.read_network(str(xml_filepath), str(bin_filepath))
        assert len(net.input_info) == 1

        self.exec_net = ie.load_network(network=net, device_name='CPU')
        self.input_name = list(net.input_info.keys())[0]
        self.input_shape = net.input_info[self.input_name].input_data.shape[2:]
        self.output_names = list(net.outputs.keys())

    def predict(self, image_filepath):
        # The model requires RGB[0-1] NCHW input.
        image = PIL.Image.open(image_filepath).resize(self.input_shape)
        input_array = np.array(image)[np.newaxis, :, :, :]
        input_array = input_array.transpose((0, 3, 1, 2))  # => (N, C, H, W)
        input_array = input_array / 255  # => Pixel values should be in range [0, 1]

        return self.exec_net.infer(inputs={self.input_name: input_array})


def print_outputs(outputs):
    outputs = list(outputs.values())[0]
    for index, score in enumerate(outputs[0]):
        print(f"Label: {index}, score: {score:.5f}")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('xml_filepath', type=pathlib.Path)
    parser.add_argument('bin_filepath', type=pathlib.Path)
    parser.add_argument('image_filepath', type=pathlib.Path)

    args = parser.parse_args()



    model = Model(args.xml_filepath, args.bin_filepath)
    outputs = model.predict(args.image_filepath)
    print_outputs(outputs)


if __name__ == '__main__':
    main()
```
</div></details>

OpenVINOを利用したプログラムの実装方法につきましては、[別の記事](https://qiita.com/TWAT/items/0cbf6286e921f7c5664e)で詳しく記載されておりますので、ご参照ください。



続いて、teratermでAE2100へログインし、標準コンテナ上に「CV_Classification」ディレクトリを作成します。

```
root@ae2100:~# docker start ubuntu-openvino
root@ae2100:~# docker exec -it ubuntu-openvino /bin/bash
root@3a69a45317dd:/opt/intel/openvino_2021.4.689# cd
root@3a69a45317dd:~# mkdir CV_Classification
root@3a69a45317dd:~# exit
exit
```

ホストからコンテナへzipファイルをコピーします。

```
root@ae2100:~# docker cp Send_TO_AE2100.zip ubuntu-openvino:/root/CV_Classification
```

そして、コンテナ内でunzipを実行(解凍)します。
(unzipコマンドがない場合は、インターネットに接続した状態でインストールしてください。)

```
root@ae2100:~# docker start ubuntu-openvino
root@ae2100:~# docker exec -it ubuntu-openvino /bin/bash
root@3a69a45317dd:/opt/intel/openvino_2021.4.689# cd /root/CV_Classification/
root@3a69a45317dd:~/CV_Classification# apt install unzip
root@3a69a45317dd:~/CV_Classification# unzip Send_TO_AE2100.zip
root@3a69a45317dd:~/CV_Classification# cd Send_TO_AE2100
root@3a69a45317dd:~/CV_Classification/Send_TO_AE2100# unzip ExportFile_OpenVino.zip
```

#結果の出力
最後に推論を実行していきましょう。

```
root@3a69a45317dd:~/CV_Classification/Send_TO_AE2100# python3 predict.py model.xml model.bin OK_image.jpg
Label: 0, score: 0.16326
Label: 1, score: 0.83674

root@3a69a45317dd:~/CV_Classification/Send_TO_AE2100# python3 predict.py model.xml model.bin NG_image1.jpg
Label: 0, score: 0.94221
Label: 1, score: 0.05779

root@3a69a45317dd:~/CV_Classification/Send_TO_AE2100# python3 predict.py model.xml model.bin NG_image2.jpg
Label: 0, score: 0.97225
Label: 1, score: 0.02775
```

Label: 0が誤った並び(NG)、Label: 1が正しい並び(OK)である各確率をscoreで表示しています。
よって、「OK_image.jpg」は、16%の確率でNG、83%の確率でOKと推論しています。
「NG_image1.jpg」については94%でNG、6%でOK、「NG_image2.jpg」については97%でNG、3%でOKということで、正しく推論できていることがわかります。

続いて、[viコマンド](https://docs.oracle.com/cd/E19683-01/816-3946/editorvi-tbl-83/index.html)で「predict.py」の14行目「self.exec_net = ie.load_network(network=net, device_name='CPU')」を「self.exec_net = ie.load_network(network=net, device_name='HDDL')」に変更して、VPUを指定して実行できるか確認してみましょう。

```
root@3a69a45317dd:~/CV_Classification/Send_TO_AE2100# vi predict.py
```
<details><summary>サンプルコード(predict.py)の確認は、ここをクリック</summary><div>

```python
import argparse
import pathlib
import numpy as np
import PIL.Image
from openvino.inference_engine import IECore


class Model:
    def __init__(self, xml_filepath, bin_filepath):
        ie = IECore()
        net = ie.read_network(str(xml_filepath), str(bin_filepath))
        assert len(net.input_info) == 1

        self.exec_net = ie.load_network(network=net, device_name='HDDL')
        self.input_name = list(net.input_info.keys())[0]
        self.input_shape = net.input_info[self.input_name].input_data.shape[2:]
        self.output_names = list(net.outputs.keys())

    def predict(self, image_filepath):
        # The model requires RGB[0-1] NCHW input.
        image = PIL.Image.open(image_filepath).resize(self.input_shape)
        input_array = np.array(image)[np.newaxis, :, :, :]
        input_array = input_array.transpose((0, 3, 1, 2))  # => (N, C, H, W)
        input_array = input_array / 255  # => Pixel values should be in range [0, 1]

        return self.exec_net.infer(inputs={self.input_name: input_array})


def print_outputs(outputs):
    outputs = list(outputs.values())[0]
    for index, score in enumerate(outputs[0]):
        print(f"Label: {index}, score: {score:.5f}")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('xml_filepath', type=pathlib.Path)
    parser.add_argument('bin_filepath', type=pathlib.Path)
    parser.add_argument('image_filepath', type=pathlib.Path)

    args = parser.parse_args()



    model = Model(args.xml_filepath, args.bin_filepath)
    outputs = model.predict(args.image_filepath)
    print_outputs(outputs)


if __name__ == '__main__':
    main()
```
</div></details>


```
root@3a69a45317dd:~/CV_Classification/Send_TO_AE2100# python3 predict.py model.xml model.bin OK_image.jpg
Label: 0, score: 0.25000
Label: 1, score: 0.75000

root@3a69a45317dd:~/CV_Classification/Send_TO_AE2100# python3 predict.py model.xml model.bin NG_image1.jpg
Label: 0, score: 0.93506
Label: 1, score: 0.06494

root@3a69a45317dd:~/CV_Classification/Send_TO_AE2100# python3 predict.py model.xml model.bin NG_image2.jpg
Label: 0, score: 0.95703
Label: 1, score: 0.04312
```

こちらも問題ないみたいですね！
ターミナル上で出力が少し遅かったですが、GPUやHDDLで実行する際は、モデルのロード時間がかかりますので、その影響かと思います。
また、HDDLのパフォーマンスを最大限に活かすためには、非同期処理のプログラムを作成する必要があります。
別途、[参考の記事](https://qiita.com/TWAT/items/4b1c8289ccf0e2dd38d0)がありますので、挑戦してみてください。

#参考
念のため、OpenVINOのベンチマークを行ってみます。
(※AE2100上でベンチマークを行うには、OpenVINOのフルパッケージをインストールする必要がありますので、リソースにご注意ください。)

CPUのベンチマーク結果

```
root@3a69a45317dd:/opt/intel/openvino_2021.4.689/deployment_tools/tools/benchmar
k_tool# python3 benchmark_app.py -d CPU -i /root/CV_Classification/Send_TO_AE210
0/OK_image.jpg -m  /root/CV_Classification/Send_TO_AE2100/model.xml
~中略~
Count:      572 iterations
Duration:   60481.09 ms
Latency:    414.16 ms
Throughput: 9.46 FPS
```

GPUのベンチマーク結果

```
root@3a69a45317dd:/opt/intel/openvino_2021.4.689/deployment_tools/tools/benchmar
k_tool# python3 benchmark_app.py -d GPU -i /root/CV_Classification/Send_TO_AE210
0/OK_image.jpg -m  /root/CV_Classification/Send_TO_AE2100/model.xml
~中略~
Count:      1236 iterations
Duration:   60328.87 ms
Latency:    195.01 ms
Throughput: 20.49 FPS
```

VPUのベンチマーク結果

```
root@3a69a45317dd:/opt/intel/openvino_2021.4.689/deployment_tools/tools/benchmar
k_tool# python3 benchmark_app.py -d HDDL -i /root/CV_Classification/Send_TO_AE2100/OK_image.jpg -m  /root/CV_Classification/Send_TO_AE2100/model.xml
~中略~
Count:      2432 iterations
Duration:   60212.76 ms
Latency:    197.85 ms
Throughput: 40.39 FPS

```

ベンチマークの結果を表にすると、以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1620360/68e696ef-cae6-f8ca-c7c4-11789afc88f5.png)
CPUに対してVPUのスループットが4倍以上の数値であることをご確認いただけるかと思います。

# まとめ
Custom VisionとAE2100を使用することで、簡単にAIの学習モデルを作成し、実運用の環境でリアルタイムな推論ができます。
Custom Visionは、GPUサーバーなどのAIの学習環境を構築が不要で、無料枠も充実しているため、気軽に利用を開始することができます。
皆様の現場でどの程度使えそうか(精度)は、やってみないと分からないところもありますので、ぜひ色々な使い方で試してみてください。