2019/07/29

Inference Engineを学んで感情分類

どうも、ディープなクラゲです。
OpenVINO™ でゼロから学ぶディープラーニング推論」シリーズの7回目記事です。
このシリーズは、ディープラーニング概要、OpenVINO™ツールキット、Neural Compute Stick、RaspberryPiの使い方、Pythonプログラミングをゼロから徹底的に学び、成果としてディープラーニング推論アプリケーションが理解して作れるようになることを目指します。

第7回目はOpenVINO™のInference Engineを使ってディープラーニング推論を学びます。具体的には顔画像から感情分類を行います。

【 目次 】


Inference Engine

Inference Engineは推論エンジンのことで、プログラミングで呼び出して使います。
4つのステップで簡単に使うことができます

C++とPythonの両方が用意されていますが、このシリーズではPythonで使います。
Inference Engine Python APIの詳細はこちらにありますが、クラゲ視点で一つずつ解説してゆきます!

モジュール読み込み

Inference Engineの中のIENetworkIEPluginというクラスを使います。
Python基礎で習ったimportの3つ目の形式を使います。

# 現状だとエラー 
from openvino.inference_engine import IENetwork, IEPlugin

しかし、RaspberryPiの場合、現状ではImportError: No module named 'inference_engine'などとエラーが出てしまいます。
これはパスが通っていないことが原因なので、path.appendを使って強制的にパスを追加することにします。なお、この関数を使うにはsysモジュールをインポートする必要があります。

# 対策盛り込み 
import sys
sys.path.append('/opt/intel/openvino/python/python3.5/armv7l')
from openvino.inference_engine import IENetwork, IEPlugin

これでIENetworkIEPluginが使えるようになりました

ターゲットデバイスの指定

ターゲットデバイス(プロセッサ)には以下のものがあります

今回はNCS(Neural Compute Stick)を使いますのでMYRIADを引数に渡します。以下のコード例ではキーワード引数を用いて指定しています。
※NCSを使わないPCのみの環境の方はCPUなどに変更する必要があります

plugin = IEPlugin(device="MYRIAD")

IEPluginクラスのインスタンス生成しpluginという名前にしています

モデルの読み込み

概要で説明したmodelとweightsの読み込みです
modelファイル名がxxx.xmlでweightsファイル名がxxx.binだった場合、以下のコードで、読み込むことができます。

net = IENetwork(model='xxx.xml', weights='xxx.bin')
exec_net = plugin.load(network=net)

1行目はIENetworkクラスのインスタンス生成しnetという名前にしてます
2行目はIEPluginのメソッドloadを呼び出しています。引数に'net'を渡しています。
結果として exec_net という推論実行可能なネットワーク(ExecutableNetworkクラス)を作成しています。

推論実行

推論実行は、ExecutableNetworkのメソッド infer を呼び出すだけで行われ、戻り値に結果が入ります。
引数には具体的な入力データを渡します。具体例は後ほど。

out = exec_net.infer(inputs)

以上が、ディープラーニング推論で使うAPIです。数行程度で簡単に書けることが理解できたかと思います。

ディープラーニングで感情推論

推論エンジンの使い方が分かったところで、具体的な学習済みモデルと入力データを使って推論させ、出力結果を出したいと思います。今回は「感情推論」を行います。

学習済みモデルの取得

インテルの学習済みモデルを使用します。
modelとweightをこちらのサイトからダウンロードしてください。workspaceの中にFP16フォルダを作成して、そこに移動してください。

先程のリンクのParent Directoryを見ると以下の3つのフォルダに分かれていると思います

FPとはFloating Pointの略、INTはIntegerの略で、数値はバイト数を表しています。
これらは好きに選べる訳ではなく、デバイス(プロセッサ)により決まっています。
NCSを使った環境の場合はFP16を使います。逆にPCのみ環境の場合は場合はFP32を使ってください。
PCのみ環境の場合はINT8を使うことでパフォーマンスを向上させることも可能ですが、推奨はFP32になっています。
※サポート対応表はこちら

入力データ

人の感情を推論するので、入力データは顔画像です。
しかし、任意の画像をそのまま推論エンジンに入力するのではなく、学習済みモデルが要求する入力フォーマットに合わせる必要があります。ただし、これらは手動ではなくプログラミングを用いて簡単に合わせることができます。
このモデルについてこちらのページに詳細が書いてあります。

ページの下の方を見ると、入力フォーマットが書いてあります。

Inputs
name: "input" , shape: [1x3x64x64] - An input image in [1xCxHxW] format. Expected color order is BGR.

入力データの名前に 'input' とありますが、Python APIで使う場合は 'data' が正しいようです。
型は [1x3x64x64]と書いてあります。 フォーマットは[1xCxHxW]で、カラーの順番は BGR という記載があります。
OpenCVのimreadを使って画像を読み込んだ場合は、カラーの順番はBGR(青, 緑, 赤)になるため、color order についてはそのままでOKです。
一方で画像のサイズについては64x64である必要があります。また、Numpyで習った通り、画像の次元は3次元でHCWフォーマットです。つまり、「画像サイズ」と「HCWの順番」及び「次元数」についてはフォーマット変更が必要であるということが分かります。
具体的なコードは以下の通りです

# 画像サイズを64x64にする 
img = cv2.resize(img, (6464))
 
# HWCからCHWに変更 
img = img.transpose((201))
 
# 大きさ1の次元を追加し4次元にする。省略OK 
img = np.expand_dims(img, axis=0)

サイズ変更はOpenCVで習った項目、HWC>CHW変更と次元追加はNumPyで習った項目です。ちなみに、最後の次元追加は省略したとしても、推論エンジン側で自動的に解釈するため問題なさそうです。

では画像データを準備しましょう。最低64x64ピクセル以上のサイズがあった方が良いです。
人の顔画像であれば何でもOKです。クラゲは以下のサイトからトリミングして保存しました。出来る限り顔のみが写るようにトリミングしてください
https://www.pakutaso.com/20190610177post-21595.html

ファイル名はface.jpgに変更し、workspaceフォルダに移動しました

出力データ

先程のモデル説明ページをみると出力フォーマットについても記載があります。

Outputs
name: "prob", shape: [1, 5, 1, 1] - Softmax output across five emotions ('neutral', 'happy', 'sad', 'surprise', 'anger').

出力データの名前が 'prob' となっていますが、実際は違うようです。実際の名前の取得方法を後ほど説明します。
型は[1, 5, 1, 1]で、Softmaxという形式で5つの感情を出力するということも読み取れます。Softmaxの形式は割合です。つまり各感情に対応した5つの小数値が配列として出力されるということです。
大きさが1の次元は取り除いた方が見やすいので、NumPyのときに習ったsqueezeを使って次元削減します。

# 次元の削減 
out = np.squeeze(out)

全体プログラム

これで関連ファイルの準備、入力・出力のフォーマットも分かりましたので、全体プログラムを作ってゆきたいと思います。
emotion1.pyというファイルを新規作成しましたが、任意のファイル名でOKです。同じディレクトリにFP16フォルダとface.jpgがあることを確認してください。以下の画像は画像ファイルface.jpgをダブルクリックしただけです。

これまでの組合せで一連のプログラムが出来ます

import cv2
import numpy as np
 
# モジュール読み込み 
import sys
sys.path.append('/opt/intel/openvino/python/python3.5/armv7l')
from openvino.inference_engine import IENetwork, IEPlugin
 
# ターゲットデバイスの指定 
plugin = IEPlugin(device="MYRIAD")
 
# モデルの読み込み 
net = IENetwork(model='FP16/emotions-recognition-retail-0003.xml', weights='FP16/emotions-recognition-retail-0003.bin')
exec_net = plugin.load(network=net)
 
# 入力画像読み込み 
img = cv2.imread('face.jpg')
 
# 入力データフォーマットへ変換 
img = cv2.resize(img, (6464))   # サイズ変更 
img = img.transpose((201))    # HWC > CHW 
img = np.expand_dims(img, axis=0) # 次元合せ 
 
# 推論実行 
out = exec_net.infer(inputs={'data': img})
 
# 出力 
print(out)

推論実行のinferメソッドの引数はinputs={'data': img}となっていますが、Python基礎で習ったキーワード引数と辞書の組合せです。入力データのキーワードは'data'で、値としてimgを入れています
実行結果は以下のように出力されたと思います。ただし各数値は入力画像により結果は異なります。

{'prob_emotion': array([[[[ 0.85107422]],
 
        [[ 0.09069824]],
 
        [[ 0.03393555]],
 
        [[ 0.01065063]],
 
        [[ 0.01377869]]]], dtype=float32)}

これを見ると出力データの名前が'prob_emotion'であることが分かると思います。
必要なデータのみを取得するには、以下のコードを最後の出力前に追記します

# 出力から必要なデータのみ取り出し 
out = out['prob_emotion']
out = np.squeeze(out) #不要な次元の削減 

これで実行すると以下が出力されます

[ 0.85107422  0.09069824  0.03393555  0.01065063  0.01377869]

それぞれの値は、'neutral', 'happy', 'sad', 'surprise', 'anger'の割合を示しています。今回入力した画像だと 'neutral' が群を抜いて最も高いことが分かります。
ちなみに、割合なので当然ですが、これらの数値を合計するとほぼ1.0になります

感情分類アプリ

最後にアプリっぽくするために、元画像を表示してそこに分類結果を文字列描画したいと思います

import cv2
import numpy as np
 
# モジュール読み込み 
import sys
sys.path.append('/opt/intel/openvino/python/python3.5/armv7l')
from openvino.inference_engine import IENetwork, IEPlugin
 
# ターゲットデバイスの指定 
plugin = IEPlugin(device="MYRIAD")
 
# モデルの読み込み 
net = IENetwork(model='FP16/emotions-recognition-retail-0003.xml', weights='FP16/emotions-recognition-retail-0003.bin')
exec_net = plugin.load(network=net)
 
# 入力画像読み込み 
img_face = cv2.imread('face.jpg')
 
# 入力データフォーマットへ変換 
img = cv2.resize(img_face, (6464))   # サイズ変更 
img = img.transpose((201))    # HWC > CHW 
img = np.expand_dims(img, axis=0) # 次元合せ 
 
# 推論実行 
out = exec_net.infer(inputs={'data': img})
 
# 出力から必要なデータのみ取り出し 
out = out['prob_emotion']
out = np.squeeze(out) #不要な次元の削減 
 
# 出力 
print(out)
 
# 出力値が最大のインデックスを得る 
index_max = np.argmax(out)
 
# 各感情の文字列をリスト化 
list_emotion = ['neutral', 'happy', 'sad', 'surprise', 'anger']
 
# 文字列描画 
cv2.putText(img_face, list_emotion[index_max], (1030), cv2.FONT_HERSHEY_SIMPLEX, 1, (255255255), 2)
 
# 画像表示 
cv2.imshow('image', img_face)
 
# キーが押されたら終了 
cv2.waitKey(0)
cv2.destroyAllWindows()

先程のプログラムだと元画像もフォーマット変換後の画像も同じimgという変数になっているので、元画像の部分だけをimg_faceに変更しています。(17行目と20行目)
それ以外は、先ほどのプログラムへの追記です。NumPyで習ったargmaxを使い、最も出力値が高いインデックスを得ます。各感情の文字列をリスト化し、先ほどのインデックスを用いてputTextで文字列描画を行っています。画像表示に関しては「OpenCVを学ぶ」で習った項目を使っています。

実行結果

別の画像も色々と入力して試してみてください!


現状だと、手動で顔の領域をトリミングしなければなりません。これだとリアルタイムカメラ入力に対応できないため、次回はディープラーニングで自動的に顔領域の検出を行いたいと思います。

以上、「Inference Engineを学んで感情分類」でした。