ラズパイカメラによるMNIST手書き文字分類 with TensorFlow lite


0.前置き

ラズパイで機械学習を使った何かを作りたいと思いました。
最終的には以下のようなロボットを作ることを目標とします。

・物体検出モデルに家族の顔を学習させて顔検出させる
・「誰々のところに行け!」と言ったら、音声認識して探索を開始し、顔を検出したらその人の近くまで移動する

実現させるためには、いくつかのハードルがあります。
① 家族の顔をアノテーションして自前のデータセットを作る
② ①で作ったデータセットを使って、既存の物体検出モデルを転移学習させる
③ 学習させたモデルをラズパイでも実行できるように軽量化する

①については、まだやったことないですがやればできると思われます。
②が一番ハードルが高い。Pythonを駆使してやるのが一般的ですが、ちょっと難しそうだったので、SONYの「Neural Network Console」というGUIでモデルを構築できるツールが取っつきやすそうに思いました。これを使ってみます。
③についてもいくつかの選択肢があるでしょうが、TensorFlow liteという軽量化モデルが割とメジャーなようなので、それでいきます。

前置きが長くなりましたが、本記事では主に②③の導入、及びラズパイ実装の事前検討の結果を記載します。

1.Neural Network Console

https://dl.sony.com/ja/app/
こちらのサイトからWindows用デスクトップアプリが無料でダウンロードできます。
GPUが使えるクラウド版もあるようです。クラウド版は無料枠を超えると課金が発生するようです。
私はWindows版を使用しました。

ダウンロードしたファイルにはインストーラはなく、いきなりexeファイルを実行する形です。一緒にマニュアルやサンプルプロジェクトもダウンロードされます。


UIは上記のような感じです。
真ん中のだるま落としのようにたくさん積みあがってるところで、各層を入れ替えたりプロパティをいじったりしてチューニングができるようです。
今回私はその辺の検討は後回しにして、このツールで出力したモデルをラズパイに実装できるか、という点を中心に検討しました。
なので適当にサンプルモデルをダウンロードして、出力してみました。


こちらのLeNetというネットワークを使ったMNIST手書き文字分類のモデルにしてみました。
(本当は物体検出のモデルにしたかったですが、それはうまくいきませんでした)

新しくプロジェクトをダウンロードしてきて開くと、最初にデータセットの展開が始まります。
展開が済めば、ボタン一つ押すだけで学習がスタートします。
このプロジェクトの学習だと、GPUなしのCOREi5のPCでも数十分で完了しました。
学習が済めば、モデルの評価(EVALUATION)までツール上でスムーズに実施できます。
EVALUATIONまで完了すれば、いよいよモデルの出力ができます。


ACTION→EXPORT から出力形式が選択できます。

NNPというのはSONY製のNeural Network Librariesというライブラリに準じた形式です。
この形式であれば、Neural Network Librariesのファイルフォーマットコンバーターというツールを使って、NNPから直接.tflite(TensorFlow lite形式)に変換できる、とあるのですが…
試してみましたがエラーが出てうまくいきませんでした(原因不明)
そこで、一旦pb形式で出力した後、別の変換手法を使ってpb→tfliteとする案を検討しました。

2. .pbから.tfliteへの変換

https://cocodrips.hateblo.jp/entry/2020/04/11/184333
こちらのサイトとかにいくつか方法が書かれていますが、どれもうまくいかず…
最終的に、以下のGoogle colabノートにたどり着きました。
https://drive.google.com/file/d/1lDcttsmZC0Y6dXxwe0EVZUsyVoR8HuR-/view?usp=sharing

自分のGoogle Driveに変換したい.pbファイルをアップロードした上で、ノートの指示に従って順番にセルを実行していけば、モデルの変換ができます。
少しだけつまったところもあったので、3.) Convert .pb model into TFLite .lite model のところだけ以下に補足コメントを入れます。

import tensorflow as tf

localpb = 'frozen_inference_graph_frcnn.pb' #変換したいpbファイルの名前
tflite_file = 'frcnn_od.lite' #変換後のtfliteファイルの名前。デフォルトでは.liteになってるが正しくは.tfliteなので要修正

print("{} -> {}".format(localpb, tflite_file))

converter = tf.lite.TFLiteConverter.from_frozen_graph(
    localpb, 
    ["image_tensor"], #変換対象モデルの入力ノードの名前
    ['detection_boxes'] #変換対象モデルの出力ノードの名前
)

tflite_model = converter.convert()

open(tflite_file,'wb').write(tflite_model)

interpreter = tf.lite.Interpreter(model_content=tflite_model)
interpreter.allocate_tensors()

入力ノード/出力ノードの名前をどうやって調べるか?
Neural Network Console の画面上でわかればいいんですが、ちょっとわかりませんでした。
netronという、モデルファイルを読み込ませることで、モデルの構造を解析してくれる便利なサイトがあります。
これを使って調査できます。
今回私が変換したLeNetのモデルの場合、

入力ノードは「Input」

出力ノードは「div」でした。

このように記載しました。
入力した情報に間違いがなければ、上記の3)のセルを実行することで、.tfliteへのモデル変換が実施されます。

3.ラズパイへの実装

ラズパイに実装するためには、TensorFlow liteと、OpenCVのインストールが必要です。
https://qiita.com/karaage0703/items/38f314d01a67ded949c2
こちらの記事の手順通りにやれば、スムーズに環境構築できると思います。

今回はお試し実装として、「ラズパイに繋いだWEBカメラで撮影した手書き文字画像をLeNetモデルに入力し、予測したラベルを出力する」というプログラムを実装しました。

構成
・ラズパイ3B(SDカード32GB、OS:Raspberry Pi Imagerで書いた最新のやつ)
・WEBカメラ

プログラムを以下に掲載します。
lenet_mnist.tflite というのが今回使用したモデルの名前です。このプログラムと同じディレクトリに保存します。

import numpy as np
from tflite_runtime.interpreter import Interpreter
import time
import cv2

camera = cv2.VideoCapture(0) # カメラCh.(ここでは0)を指定

# TFLiteモデルの読み込み
interpreter = Interpreter(model_path="lenet_mnist.tflite")
# メモリ確保
interpreter.allocate_tensors()

# 学習モデルの入力層・出力層のプロパティをGet.
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

def detect(image):
    frame = cv2.resize(image, (28,28)) #28*28ピクセルに画像縮小
    frame = frame / 255 #0.0~1.0に正規化

    #モデルに入力するために次元を合わせる
    #この操作により(1,1,28,28)の配列になる
    frame = np.expand_dims(frame, 0)
    frame = np.expand_dims(frame, 0)

    #float32形式にキャストする
    frame = frame.astype(np.float32)

    # indexにテンソルデータのポインタをセット
    interpreter.set_tensor(input_details[0]['index'], frame)
    start_time = time.time()
    interpreter.invoke() #推論実行
    stop_time = time.time()
    print("time: ", stop_time - start_time) #推論時間の表示

    # get results
    label = interpreter.get_tensor(output_details[0]['index'])
    return label


if __name__ == '__main__':

    while True:
        ret, frame = camera.read()  #フレームを取得
        frame = frame[ : , 80:560 ]  #正方形にトリミング。640*480解像度なので、横方向を80~560(480)トリミング
        frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) #グレースケール化
        ret, frame_bin = cv2.threshold(frame_gray, 50, 255, cv2.THRESH_BINARY_INV)
        #↑白黒反転二値化。MNISTデータセットが白黒反転のため、取得画像も反転させる必要あり。50は二値化の閾値。255は最大値。

        cv2.imshow('camera', frame)  # フレームを画面に表示。ここでは反転前の生画像を表示している。

        num = detect(frame_bin)  #推論結果の保存
        print("label: " + str(np.argmax(num)) + " accuracy: " + str(np.amax(num)))  #推論結果の表示

        # キー操作があればwhileループを抜ける
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break

    # 撮影用オブジェクトとウィンドウの解放
    camera.release()
    cv2.destroyAllWindows()

動作させているところです↓

無事にtfliteモデルを動作させることができました。
ちょっと分類精度がイマイチなんですが、原因調査はできていません。
ただ、ラズパイ3Bでも数msec間隔で推論を回せているので、だいぶモデルは軽量化されているようです。その分精度が悪化しているのかもしれません。
TensorFlow liteがどのようにモデルを軽量化しているのかは(私はまだ)よくわかってません。

netronで、軽量化前と軽量化後のモデルを読み込ませると、下のようにだいぶ構造が変わっています(左:軽量化前、右:軽量化後)
この辺を見比べると、何かわかってきそうです。
 

以上

最後のPythonコード作成にあたっては、以下の記事を参考にさせてもらいました。
https://qiita.com/yohachi/items/434f0da356161e82c242
https://qiita.com/iwatake2222/items/d63aa67e5c700fcea70a

また、モデルの軽量化については、以下の記事に大変詳しく記載されています。
https://qiita.com/PINTO/items/008c54536fca690e0572