ラズパイカメラによる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ファイルを実行する形です。一緒にマニュアルやサンプルプロジェクトもダウンロードされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/d66d945f-3afe-a08b-ad6b-802155fcaa69.png)
UIは上記のような感じです。
真ん中のだるま落としのようにたくさん積みあがってるところで、各層を入れ替えたりプロパティをいじったりしてチューニングができるようです。
今回私はその辺の検討は後回しにして、このツールで出力したモデルをラズパイに実装できるか、という点を中心に検討しました。
なので適当にサンプルモデルをダウンロードして、出力してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/d8d44e32-62ae-a399-89ae-0f5754636f64.png)
こちらのLeNetというネットワークを使ったMNIST手書き文字分類のモデルにしてみました。
（本当は物体検出のモデルにしたかったですが、それはうまくいきませんでした）

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

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/d971f5a1-8b21-f23d-63c2-610d1c275a48.png)
ACTION→EXPORT から出力形式が選択できます。

NNPというのはSONY製の[Neural Network Libraries](https://nnabla.org/ja/download/)というライブラリに準じた形式です。
この形式であれば、Neural Network Librariesの[ファイルフォーマットコンバーター](https://nnabla.readthedocs.io/ja/latest/python/file_format_converter/file_format_converter.html)というツールを使って、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
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/0540fc1b-2170-4bbc-b9a6-664287f053dd.png)
自分のGoogle Driveに変換したい.pbファイルをアップロードした上で、ノートの指示に従って順番にセルを実行していけば、モデルの変換ができます。
少しだけつまったところもあったので、3.) Convert .pb model into TFLite .lite model のところだけ以下に補足コメントを入れます。

```python
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](https://netron.app/)という、モデルファイルを読み込ませることで、モデルの構造を解析してくれる便利なサイトがあります。
これを使って調査できます。
今回私が変換したLeNetのモデルの場合、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/12bfe24a-7006-44b3-dc9b-479ced774f01.png)
入力ノードは「Input」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/fda33fff-6791-a5ee-dd37-ff1cdeda2210.png)
出力ノードは「div」でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/7f2d623b-3b4a-0bf0-3b1b-0c0aba4d20bf.png)
このように記載しました。
入力した情報に間違いがなければ、上記の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 というのが今回使用したモデルの名前です。このプログラムと同じディレクトリに保存します。

```python
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()
```
動作させているところです↓
<blockquote class="twitter-tweet"><a href="https://twitter.com/hashtag/%E3%83%A9%E3%82%BA%E3%83%91%E3%82%A4?src=hash&amp;ref_src=twsrc%5Etfw"> <a href="https://t.co/zAmmDA0qGn">pic.twitter.com/zAmmDA0qGn</a> <a href="https://twitter.com/shi78ge/status/1326186859869859841?ref_src=twsrc%5Etfw">November 10, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

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

netronで、軽量化前と軽量化後のモデルを読み込ませると、下のようにだいぶ構造が変わっています（左：軽量化前、右：軽量化後）
この辺を見比べると、何かわかってきそうです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/d2a6dafa-70e7-5216-7e0d-0c86a90109e8.png)　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569342/c06002d7-bad6-32f5-dbf2-53e830a39d8b.png)

以上

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

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