LoginSignup
33
31

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-11-11

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

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

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

無題.png
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
image.png
自分の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のモデルの場合、
image.png
入力ノードは「Input」
image.png
出力ノードは「div」でした。
image.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 というのが今回使用したモデルの名前です。このプログラムと同じディレクトリに保存します。

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

以上

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

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

33
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
31