Window10で TensorFlow liteを使ってみる - 前編


1.動機

Elixirと言う関数型言語がある。この言語には、Nervesと言う組込みシステム向けソリューションがあり、Raspberry Piなどのボード・コンピューターを関数型言語で軽快にコントロールすることができる。巷で流行りの関数型言語の中でもひときわ異色を放つ存在だ。

一方、AIテクノロジーである Deep Learningは、パワフルなコンピュータ資源を持つクラウド・サーバーから、より貧弱なエッジ・コンピューターに、その応用フィールドを広げようとしている[*1]。どうやら、本格的なIOTワールドの夜明けが近づきつつあるようだ。

そんな昨今、「Elixir × TensorFlow lite」なるアイデアを試してみない手はないだろう。

この記事(前編/後編)は、そんなアイデアの Feasibility(実現可能性)テストの備忘録である。尤も、ボード・コンピューターではなく、より作業性の良い PC/Windows10上で「Elixir × TensorFlow lite」を試している。悪しからずm(_ _)m

[*1]NVIDIAのJetsonや Googleの Edge TPUの様に、エッジ・コンピューターをパワフルにしようという流れもあるのだが…

2.イントロダクション

この一連の記事の構成は下記の通りである。
- 前編: Windows10向けの TensorFlow liteライブラリを作成する
    残念ながら Windows向けのオフィシャルなビルド手順は公開されていない
- 後編: 簡単なMnistアプリを作成し、「Elixir × TensorFlow lite」を試す
    Elixir/Erlangの他言語拡張機能 Portを用いて TensorFlowインタープリタを利用する

3.MinGW 64bitで TensorFlow liteライブラリをビルド

イントロダクションで触れた通り、今日現在 Googleは TensorFlow liteの Windows10向けビルド手順を公開していないようだ。しかし、幸いなことに Linux向けのビルド・スクリプトは用意されている。そう、Windows下でPOSIX系のツールチェインが利用できる MSys2/MinGW64を用いれば TensorFlow liteをビルド出来そうだ。

試してみると、次の3項目を解決する必要があるが、無事 Windows10向け TensorFlow liteライブラリをビルドすることが出来た。

  • mman.hが見つからない
  • byteswap.hが見つからない
  • benchmark-lib.aのビルドで、コマンドラインが長すぎてエラーする

では、作業を始めよう。
まず最初に、TensorFlowのリポジトリを複製し、添付のスクリプトで依存ファイルをダウンロードする。

$ git clone https://github.com/tensorflow/tensorflow.git tensorflow_src
$ pushd tensorflow_src/tensorflow/lite/tools/make
$ ./download_dependencies.sh
$ popd

次に、下記のリポジトリを複製し、mmanをビルド/インストールする。
https://github.com/witwall/mman-win32

$ git clone https://github.com/witwall/mman-win32
$ cd mman-win32
$ ./configure --libdir=/mingw64/x86_64-w64-mingw32/lib --incdir=/mingw64/x86_64-w64-mingw32/include
$ make
$ make install

参考文献[2]を参考に下記をダウンロードする。
https://github.com/icecoobe/sbc-windows/blob/master/byteswap.h

$ cp byteswap.h /migw64/x86_64-w64-mingw32/include

最後に、Makefile中の$(BENCHMARK_LIB)のルールを、コマンドライン・バッファの長さに収まるように修正する。

$ cd tensorflow_src/tensorflow/lite/tools/make
$ vim Makefile
/^\$(BENCHMARK_LIB)
修正前
$(BENCHMARK_LIB) : $(LIB_PATH) $(BENCHMARK_LIB_OBJS)
    @mkdir -p $(dir $@)
    $(AR) $(ARFLAGS) $(BENCHMARK_LIB) $(LIB_OBJS) $(BENCHMARK_LIB_OBJS)

修正後
$(BENCHMARK_LIB) : $(LIB_PATH) $(BENCHMARK_LIB_OBJS)
    @mkdir -p $(dir $@)
    $(AR) $(ARFLAGS) $(BENCHMARK_LIB) $(LIB_OBJS)
    $(AR) $(ARFLAGS) $(BENCHMARK_LIB) $(BENCHMARK_LIB_OBJS)

以上で準備が整ったので、TensorFlow liteのビルドを行おう。

$./build_lib.sh

ライブラリは、"libtensorflow-lite.a"という名前で、

 tensorflow_src/tensorflow/lite/tools/make/gen/windows_x86_64/lib

に置かれる。また、お隣のディレクトリ

 tensorflow_src/tensorflow/lite/tools/make/gen/windows_x86_64/bin

には、ベンチマーク・ツールや最小構成のTensorFlow liteアプリ "minimal.exe"が出来上がっている。

4.TensorFlow liteライブラリの動作検証

さて、前編の締め括りとして、Windows10上で TensorFlow liteが期待通りに動作するかどうかを確かめておく。

おあつらえ向きに、既にTensorFlow liteアプリ"minimal.exe"が手元にあるので、これで動作確認を行おう。あと必要なモノのは、学習済みのモデルだ。TensorFlow liteに与えるモデルは、TensorFlowのモデルを lite用にコンバートして作成するそうだ。Google Colaboratoryに転がっている出来合いのモデルを拾ってきても良いのだが、折角の機会なので自前で用意してみよう。

下記のPythonコードは、おなじみの LeNetによる MNISTの画像認識だ。このコードを実行すると、MNISTの学習データでモデルを訓練し、テストデータでモデルの評価を行い、最後にそのモデル(Keras)を TensorFlow liteのモデルに変換した "mnist.tfl"が得られるようになっている。

※コードの実行にあたって注意点がある。確証はないのだが、モデル・コンバートを行うためには CUDAのインストールが必要なようだ。GPU無しの環境で実行すると、エラーが発生し中断してしまった(T_T)。ここは、大人しく Google Colaboratoryを利用するのが無難だろう。

mnist_tfl.py
import os
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Activation, Flatten, Dropout, Dense
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import TensorBoard
import numpy as np

def lenet(input_shape, num_classes):
    model = Sequential()
    #
    model.add(Conv2D(20, kernel_size=5, padding="same", activation="relu", input_shape=input_shape))
    model.add(MaxPooling2D(pool_size=(2,2)))
    model.add(Conv2D(50, kernel_size=5, padding="same", activation="relu"))
    model.add(MaxPooling2D(pool_size=(2,2)))
    #
    model.add(Flatten())
    model.add(Dense(500, activation="relu"))
    model.add(Dense(num_classes))
    model.add(Activation("softmax"))
    return model

class MNISTDataset():
    def __init__(self):
        self.image_shape = (28, 28, 1)
        self.num_classes = 10

    def get_batch(self):
        (x_train, y_train),(x_test, y_test) = tf.keras.datasets.mnist.load_data()
        x_train, x_test = [self.preprocess(d) for d in [x_train, x_test]]
        y_train, y_test = [self.preprocess(d, label_data=True) for d in [y_train, y_test]]
        return x_train, y_train, x_test, y_test

    def preprocess(self, data, label_data=False):
        if label_data:
            data = tf.keras.utils.to_categorical(data, self.num_classes)
        else:
            data = data.astype("float32")
            data /= 255
            shape = (data.shape[0],) + self.image_shape
            data  = data.reshape(shape)

        return data

class Trainer():
    def __init__(self, model, loss, optimizer):
        self._target = model
        self._target.compile(loss=loss, optimizer=optimizer, metrics=["accuracy"])
        self.verbose = 1
        self.log_dir = os.path.join(".", "logdir")

    def train(self, x_train, y_train, batch_size, epochs, validation_split):
        if os.path.exists(self.log_dir):
            import shutil
            shutil.rmtree(self.log_dir)
        os.mkdir(self.log_dir)

        self._target.fit(
            x_train, y_train,
            batch_size=batch_size, epochs=epochs,
            validation_split=validation_split,
            callbacks=[TensorBoard(log_dir=self.log_dir)],
            verbose=self.verbose
        )


dataset = MNISTDataset()
model   = lenet(dataset.image_shape, dataset.num_classes)

x_train, y_train, x_test, y_test = dataset.get_batch()
trainer = Trainer(model, loss="categorical_crossentropy", optimizer=Adam())
trainer.train(x_train, y_train, batch_size=128, epochs=12, validation_split=0.2)

score = model.evaluate(x_test, y_test, verbose=0)
print("Test loss:", score[0])
print("Test accuracy:", score[1])

# save keras model
model.save('mnist.h5')

# save tensorflow-lite model
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
with open("mnist.tfl", "wb") as f:
    f.write(tflite_model)

それでは、"minimal.exe"に "mnist.tfl"を与えて実行してみよう。
……よくわからないが、それなりに動いていそうかな(^^;)

$./minimal.exe
minimal <tflite model>

$./minimal.exe mnist.tfl
=== Pre-invoke Interpreter State ===
Interpreter has 30 tensors and 8 nodes
Inputs: 0
Outputs: 17

Tensor   0 conv2d_input         kTfLiteFloat32  kTfLiteArenaRw       3136 bytes ( 0.0 MB)  1 28 28 1
Tensor   1 sequential/conv2d/BiasAdd/ReadVariableOp/resource kTfLiteFloat32   kTfLiteMmapRo         80 bytes ( 0.0 MB)  20
Tensor   2 sequential/conv2d_1/BiasAdd/ReadVariableOp/resource kTfLiteFloat32   kTfLiteMmapRo        200 bytes ( 0.0 MB)  50
Tensor   3 sequential/dense/BiasAdd/ReadVariableOp/resource kTfLiteFloat32   kTfLiteMmapRo       2000 bytes ( 0.0 MB)  500
Tensor   4 sequential/dense_1/BiasAdd/ReadVariableOp/resource kTfLiteFloat32   kTfLiteMmapRo         40 bytes ( 0.0 MB)  10
Tensor   5 sequential/flatten/Const kTfLiteInt32   kTfLiteMmapRo          8 bytes ( 0.0 MB)  2
Tensor   6 sequential/dense/MatMul kTfLiteFloat32   kTfLiteMmapRo    4900000 bytes ( 4.7 MB)  500 2450
Tensor   7 sequential/dense_1/MatMul kTfLiteFloat32   kTfLiteMmapRo      20000 bytes ( 0.0 MB)  10 500
Tensor   8 sequential/conv2d/Conv2D kTfLiteFloat32   kTfLiteMmapRo       2000 bytes ( 0.0 MB)  20 5 5 1
Tensor   9 sequential/conv2d_1/Conv2D kTfLiteFloat32   kTfLiteMmapRo     100000 bytes ( 0.1 MB)  50 5 5 20
Tensor  10 sequential/conv2d/Relu;sequential/conv2d/BiasAdd;sequential/conv2d/Conv2D;sequential/conv2d/BiasAdd/ReadVariableOp/resource kTfLiteFloat32  kTfLiteArenaRw      62720 bytes ( 0.1 MB)  1 28 28 20
Tensor  11 sequential/max_pooling2d/MaxPool kTfLiteFloat32  kTfLiteArenaRw      15680 bytes ( 0.0 MB)  1 14 14 20
Tensor  12 sequential/conv2d_1/Relu;sequential/conv2d_1/BiasAdd;sequential/conv2d_1/Conv2D;sequential/conv2d_1/BiasAdd/ReadVariableOp/resource kTfLiteFloat32  kTfLiteArenaRw      39200 bytes ( 0.0 MB)  1 14 14 50
Tensor  13 sequential/max_pooling2d_1/MaxPool kTfLiteFloat32  kTfLiteArenaRw       9800 bytes ( 0.0 MB)  1 7 7 50
Tensor  14 sequential/flatten/Reshape kTfLiteFloat32  kTfLiteArenaRw       9800 bytes ( 0.0 MB)  1 2450
Tensor  15 sequential/dense/Relu;sequential/dense/BiasAdd kTfLiteFloat32  kTfLiteArenaRw       2000 bytes ( 0.0 MB)  1 500
Tensor  16 sequential/dense_1/BiasAdd kTfLiteFloat32  kTfLiteArenaRw         40 bytes ( 0.0 MB)  1 10
Tensor  17 Identity             kTfLiteFloat32  kTfLiteArenaRw         40 bytes ( 0.0 MB)  1 10
Tensor  18 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  19 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  20 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  21 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  22 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  23 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  24 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  25 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  26 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  27 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  28 (null)               kTfLiteFloat32 kTfLiteArenaRwPersistent       2000 bytes ( 0.0 MB)  25 20
Tensor  29 (null)               kTfLiteFloat32 kTfLiteArenaRwPersistent     100000 bytes ( 0.1 MB)  500 50

Node   0 Operator Builtin Code   3 CONV_2D
  Inputs: 0 8 1
  Outputs: 10
  Temporaries: 28
Node   1 Operator Builtin Code  17 MAX_POOL_2D
  Inputs: 10
  Outputs: 11
Node   2 Operator Builtin Code   3 CONV_2D
  Inputs: 11 9 2
  Outputs: 12
  Temporaries: 29
Node   3 Operator Builtin Code  17 MAX_POOL_2D
  Inputs: 12
  Outputs: 13
Node   4 Operator Builtin Code  22 RESHAPE
  Inputs: 13 5
  Outputs: 14
Node   5 Operator Builtin Code   9 FULLY_CONNECTED
  Inputs: 14 6 3
  Outputs: 15
Node   6 Operator Builtin Code   9 FULLY_CONNECTED
  Inputs: 15 7 4
  Outputs: 16
Node   7 Operator Builtin Code  25 SOFTMAX
  Inputs: 16
  Outputs: 17


=== Post-invoke Interpreter State ===
Interpreter has 30 tensors and 8 nodes
Inputs: 0
Outputs: 17

Tensor   0 conv2d_input         kTfLiteFloat32  kTfLiteArenaRw       3136 bytes ( 0.0 MB)  1 28 28 1
Tensor   1 sequential/conv2d/BiasAdd/ReadVariableOp/resource kTfLiteFloat32   kTfLiteMmapRo         80 bytes ( 0.0 MB)  20
Tensor   2 sequential/conv2d_1/BiasAdd/ReadVariableOp/resource kTfLiteFloat32   kTfLiteMmapRo        200 bytes ( 0.0 MB)  50
Tensor   3 sequential/dense/BiasAdd/ReadVariableOp/resource kTfLiteFloat32   kTfLiteMmapRo       2000 bytes ( 0.0 MB)  500
Tensor   4 sequential/dense_1/BiasAdd/ReadVariableOp/resource kTfLiteFloat32   kTfLiteMmapRo         40 bytes ( 0.0 MB)  10
Tensor   5 sequential/flatten/Const kTfLiteInt32   kTfLiteMmapRo          8 bytes ( 0.0 MB)  2
Tensor   6 sequential/dense/MatMul kTfLiteFloat32   kTfLiteMmapRo    4900000 bytes ( 4.7 MB)  500 2450
Tensor   7 sequential/dense_1/MatMul kTfLiteFloat32   kTfLiteMmapRo      20000 bytes ( 0.0 MB)  10 500
Tensor   8 sequential/conv2d/Conv2D kTfLiteFloat32   kTfLiteMmapRo       2000 bytes ( 0.0 MB)  20 5 5 1
Tensor   9 sequential/conv2d_1/Conv2D kTfLiteFloat32   kTfLiteMmapRo     100000 bytes ( 0.1 MB)  50 5 5 20
Tensor  10 sequential/conv2d/Relu;sequential/conv2d/BiasAdd;sequential/conv2d/Conv2D;sequential/conv2d/BiasAdd/ReadVariableOp/resource kTfLiteFloat32  kTfLiteArenaRw      62720 bytes ( 0.1 MB)  1 28 28 20
Tensor  11 sequential/max_pooling2d/MaxPool kTfLiteFloat32  kTfLiteArenaRw      15680 bytes ( 0.0 MB)  1 14 14 20
Tensor  12 sequential/conv2d_1/Relu;sequential/conv2d_1/BiasAdd;sequential/conv2d_1/Conv2D;sequential/conv2d_1/BiasAdd/ReadVariableOp/resource kTfLiteFloat32  kTfLiteArenaRw      39200 bytes ( 0.0 MB)  1 14 14 50
Tensor  13 sequential/max_pooling2d_1/MaxPool kTfLiteFloat32  kTfLiteArenaRw       9800 bytes ( 0.0 MB)  1 7 7 50
Tensor  14 sequential/flatten/Reshape kTfLiteFloat32  kTfLiteArenaRw       9800 bytes ( 0.0 MB)  1 2450
Tensor  15 sequential/dense/Relu;sequential/dense/BiasAdd kTfLiteFloat32  kTfLiteArenaRw       2000 bytes ( 0.0 MB)  1 500
Tensor  16 sequential/dense_1/BiasAdd kTfLiteFloat32  kTfLiteArenaRw         40 bytes ( 0.0 MB)  1 10
Tensor  17 Identity             kTfLiteFloat32  kTfLiteArenaRw         40 bytes ( 0.0 MB)  1 10
Tensor  18 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  19 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  20 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  21 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  22 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  23 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  24 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  25 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  26 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  27 (null)               kTfLiteNoType  kTfLiteMemNone          0 bytes ( 0.0 MB)  (null)
Tensor  28 (null)               kTfLiteFloat32 kTfLiteArenaRwPersistent       2000 bytes ( 0.0 MB)  25 20
Tensor  29 (null)               kTfLiteFloat32 kTfLiteArenaRwPersistent     100000 bytes ( 0.1 MB)  500 50

Node   0 Operator Builtin Code   3 CONV_2D
  Inputs: 0 8 1
  Outputs: 10
  Temporaries: 28
Node   1 Operator Builtin Code  17 MAX_POOL_2D
  Inputs: 10
  Outputs: 11
Node   2 Operator Builtin Code   3 CONV_2D
  Inputs: 11 9 2
  Outputs: 12
  Temporaries: 29
Node   3 Operator Builtin Code  17 MAX_POOL_2D
  Inputs: 12
  Outputs: 13
Node   4 Operator Builtin Code  22 RESHAPE
  Inputs: 13 5
  Outputs: 14
Node   5 Operator Builtin Code   9 FULLY_CONNECTED
  Inputs: 14 6 3
  Outputs: 15
Node   6 Operator Builtin Code   9 FULLY_CONNECTED
  Inputs: 15 7 4
  Outputs: 16
Node   7 Operator Builtin Code  25 SOFTMAX
  Inputs: 16
  Outputs: 17

5.次回予告

次回は、関数型言語 Elixirから TensorFlow liteを呼び出す仕組みを作り、今回用意した LeNet MNISTのモデルで簡単な手書き数字認識アプリを作ってみる。

参考文献

[1] TensorFlow Lite ガイド
[2] Windows向けtensorflowliteのビルド(for gcc on MinGW)