2020年2月9日日曜日

Edge TPUをWindowsで動かす(Coral January 2020 Updates)

目的


Edge TPU Python APIをWindowsで動作させる。


動機


2020.1.29にCoral January 2020 Updatesがリリースされた。このリリースでWin, Macがサポートされた。このため、Windowsについて動作することを確認しようと思った。



自分が作成したEdge TPU Python APIを使ったサンプルを動作させるところまで。



Windows環境のセットアップ


環境


試した環境は以下の通り。
  • OS: Windows 10 Home, バージョン 2004, OSビルド 19041.21
  • CPU: AMD Ryzen 7 1700
  • メモリ: 16GB
  • Coral USB Accelerator


セットアップ


Python環境


Python環境はAnaconda 2019.10(Python3.7)をインストールする。

Anaconda Promptを立ち上げ、仮想環境を作成しておく。

> conda create -n coral anaconda


Edge TPU runtimeのインストール


Install the Edge TPU runtime - On Windows を参考に、Edge TPU runtimeをインストールする。ダウンロードしたファイルを解凍後、edgetpu_runtimeフォルダ内にあるinstall.batを実行する。


TensorFlow Lite interpreterのインストール


TensorFlow - Python quickstartを参考に、TensorFlow Lite interpreterをインストールする。

> pip install https://dl.google.com/coral/python/tflite_runtime-2.1.0-cp37-cp37m-win_amd64.whl

これでTensorFlow Lite APIからEdge TPUを使うことが可能。Run a model using the TensorFlow Lite AP のサンプルが動作できる。

[注意]
TensorFlow Lite API の場合、Edge TPUをPCに接続していない場合、
"Failed to load delegate from edgetpu.dll"
のエラーとなる。一見、Edge TPU runtimeのインストールに失敗していそうに思えるので注意。


Edge TPU Python APIのインストール


Coral - Edge TPU Python API for Mac and Windows を参考に、Edge TPU Python API for Windowsをインストールする。

> pip install https://dl.google.com/coral/edgetpu_api/edgetpu-2.13.0-cp37-cp37m-win_amd64.whl

これでインストールは完了。


動作確認


以前、作成したサンプルはRaspberry Pi + PiCamera用であったので、OpenCVベースに変更したものを用意した。とりあえず、現時点はObject detectionのサンプルのみ。

PCに接続できるカメラがないため、動画ファイル(スマホで撮影)入力。

モデルはSSDLite MobileNet EdgeTPU



その他


Edge TPU Compilerのバージョンアップ


Coral January 2020 Updatesでは、Edge TPU Compilerもバージョンアップした(2.0.291256449)。今回のバージョンで少しはまった点について。

SSDLite MobileNet EdgeTPU に含まれるTF-Liteモデル(Full integer quant model)をEdge TPU Compilerでコンパイルしたところ、推論実行時に
"ValueError: Dectection model should have 4 output tensors!This model has 12."
でエラーとなってしまった。

前回のEdge TPU Compiler(2.0.267685300)では問題なく、モデルも変わってはいない。

原因は、もともとのTF-Liteモデルが確かにoutputが12個あったので、Edge TPU Python APIが期待するoutputの個数ではなかったためである。

通常、TF-Liteモデルを生成する際、tocoで4つのoutput_arraysを指定するはずだが、SSDLite MobileNet EdgeTPUのpre-trainedモデルには異なるoutputが指定されていたと思われる(おそらく、もともとはPixel4向けなのでpostprocessingが異なるのかもしれない)。なので、今回のバージョンアップでは正しくコンパイルできるようになった...ということかもしれない。

SSDLite MobileNet EdgeTPUはcheckpointからexport、tocoで変換することで、Edge TPUで動作することは確認できた。

(もとのSSDLite MobileNet EdgeTPUのTF-Liteモデルはどうやって使うのだろうか?)

2020年2月2日日曜日

TensorFlow Lite for microのHello worldサンプルをM5StickVで動かす(その2)

前回


前回のブログでは、TensorFlow Lite for microのサンプル(Hello world)をM5StickV向けにビルド、動作させるところまでを書いた。出力はコンソール出力だけで、M5StickVのLCDには何も表示しない。


目的


今回は、M5StickVのLCDに表示を行うところをメインにする。
こんな感じである。




参考



LCDに表示するまでの道のり


LCDに表示するにはHWの制御がかなり必要である。自前でやるのもよいが、今回はsipeedが提供するMaixPyに含まれるライブラリを使用することにする。

前回のプロジェクトに追加・変更する方針とする。




sipeed / MaixPyのMaixPyのSDK


もともと、Micropython 用の環境であるが、SDKと利用して独自のcプロジェクトをビルドすることができる。


ただ、C++はビルドできないため、SDKのみを利用することにした。

components配下のSDKの部分を利用する。以下のフォルダを利用した。
  • boards
  • drivers
  • kendryte_sdk
  • utils


sipeed/MaixPyのSDKをプロジェクトに追加


プロジェクトに追加する。


sipeed/MaixPyのSDKをC++から呼び出せるように変更


sipeed/MaixPyのSDKはC++から呼び出すことを考慮していない箇所が多数ある。このため、使用する部分のヘッダを変更する。

extern "C" の追加し、呼び出し可能とする。

今回は以下のヘッダを修正。

TensorFlow lite for microのソース変更


以下の処理を追加する。
  • main.ccでLCDを含むHWの初期化
  • output_handler.ccで描画の更新


HWの初期化


src/micro/tensorflow/tensorflow/lite/micro/examples/hello_world/main.ccで行う。
  • HWの初期化を行う。
    m5stick_init()
    (BUTTON B長押しの電源OFFもできるようになったりする)
  • setup_lcd()でLCD初期化を行う。

man.ccはこのような感じになる。



出力値に応じた点の描画


src/micro/tensorflow/tensorflow/lite/micro/examples/hello_world/output_handler.ccで行う。

STM32F746の実装を参考。LCD出力用の関数をsipeed/MaixPyのSDKを使用する。ただ、元の実装では円を描画しているが、sipeed/MaixPyのSDKには用意されていない。今回は矩形描画で代替えした。

使用した関数は以下。
  • lcd_get_width ... LCDの幅を取得
  • lcd_get_height ... LCDの高さを取得
  • lcd_clear ... LCDのクリア
  • lcd_fill_rectangle ... 矩形を描画

output_handler.ccはこのような感じになる。


最後に


前回、今回とM5StickVでTensorFlow Lite for microのサンプルをビルドするやり方を記載した。M5StickVには6軸センサやカメラがあるので、他のサンプルも行けると思うのでチャレンジしてみたい。



2020年1月19日日曜日

TensorFlow Lite for microのHello worldサンプルをM5StickVで動かす(その1)

更新


LCDを表示するぶぶんを「その2」に記載した。


なお、ここで説明した時のソースは以下のcommitとなる。



目的


TensorFlow Lite for microのサンプル(Hello world)をM5StickV向けにビルド、動作させる。今回は、サンプルをそのまま動作させて、シリアルで結果が得られるところまでを確認する。


動機


前回前々回のブログでTensorFlow Lite for microをRISC-Vプロセッサ向けにビルド・Qemuで動作させることを試した。やっぱり、実際のモノ(マイコンとか、実機)で動かしてみたいと思った。そういえば、M5StickVのCPUはRISC-V 64 bitなので、もしかしたらうまくいくかもしれないと思い、試してみた。


ビルドまでの道のり


ここでは、前々回のブログで用いたリポジトリ(本家からのfork)を使う。このため、最新では異なることがあるので注意。

ビルドできるまでのいろいろなことを記載する。


コンパイラ


M5StickV向けのコンパイラは、以下で公開しているものを使った。
バージョンは、「v8.2.0-20190409」である。


Kendryte K210 standalone SDK


non OS向けのSDKが公開されている。
リンカスクリプトやBSP、ドライバのSDKが付属し、cmakeを利用してビルドできる。このリポジトリのsrc配下にターゲットとなるソースを配置し、CMakeLists.txtを編集することで、目的のバイナリがビルドできる。


kflash_gui


M5StickVのバイナリ書き込みツール。
(メインで使っている)Fedora 30ではうまく起動できなかったため、Windows版を利用する。


TensorFlow Lite for microの構成


ビルドに必要なソース・ヘッダファイルを確認した。ここでは、「hello world」サンプルをもとにする。

Makefile


大本のMakefileは以下。ビルド時にターゲットCPUとビルドするアプリケーション(プロジェクト)を指定する。
前回指定したRISC-V 32bit、hello worldのサンプルの場合、この辺りも参照する。
riscv32_mcu/Makefile.inc は、#33972 のRISC-Vターゲットでビルドが失敗する問題を解消するためである。これは、今時点(2020.1.19)で取り込まれていないため注意。


3rd party ライブラリ


TensorFlow以外のライブラリは以下を使用している。
上記以外では、ターゲット固有のSDKやtoolchainであるため、ここでは省略。なお、gemmlowp、flatbuffersともヘッダーファイルだけを使用する。


ファイル構成


hello worldのビルドに必要なソース・ヘッダファイルの構成は以下。microがどのようなファイルを必要としているのか、見渡せるようにする。本体と比べれば、ファイル数は少ない。

    tensorflow
    ├ tensorflow
    |  ├ core
    |  |  └ public
    |  |      └ version.h
    |  └ lite
    |      ├ c
    |      |  ├ builtin_op_data.h
    |      |  ├ common.c
    |      |  └ common.h
    |      ├ core
    |      |  └ api
    |      |      ├ error_reporter.cc
    |      |      ├ error_reporter.h
    |      |      ├ flatbuffer_conversions.cc
    |      |      ├ flatbuffer_conversions.h
    |      |      ├ op_resolver.cc
    |      |      ├ op_resolver.h
    |      |      ├ tensor_utils.cc
    |      |      └ tensor_utils.h
    |      ├ kernels
    |      |  ├ internal
    |      |  |  ├ optimized
    |      |  |  |  └ neon_check.h
    |      |  |  ├ reference
    |      |  |  |  ├ integer_ops
    |      |  |  |  |  ├ add.h
    |      |  |  |  |  ├ conv.h
    |      |  |  |  |  ├ depthwise_conv.h
    |      |  |  |  |  ├ fully_connected.h
    |      |  |  |  |  ├ mul.h
    |      |  |  |  |  ├ pooling.h
    |      |  |  |  |  └ softmax.h
    |      |  |  |  ├ add.h
    |      |  |  |  ├ arg_min_max.h
    |      |  |  |  ├ binary_function.h
    |      |  |  |  ├ ceil.h
    |      |  |  |  ├ comparisons.h
    |      |  |  |  ├ concatenation.h
    |      |  |  |  ├ conv.h
    |      |  |  |  ├ depthwiseconv_float.h
    |      |  |  |  ├ depthwiseconv_uint8.h
    |      |  |  |  ├ dequantize.h
    |      |  |  |  ├ floor.h
    |      |  |  |  ├ fully_connected.h
    |      |  |  |  ├ logistic.h
    |      |  |  |  ├ maximum_minimum.h
    |      |  |  |  ├ mul.h
    |      |  |  |  ├ neg.h
    |      |  |  |  ├ pad.h
    |      |  |  |  ├ pooling.h
    |      |  |  |  ├ prelu.h
    |      |  |  |  ├ process_broadcast_shapes.h
    |      |  |  |  ├ quantize.h
    |      |  |  |  ├ round.h
    |      |  |  |  ├ softmax.h
    |      |  |  |  └ strided_slice.h
    |      |  |  ├ common.h
    |      |  |  ├ compatibility.h
    |      |  |  ├ quantization_util.cc
    |      |  |  ├ quantization_util.h
    |      |  |  ├ round.h
    |      |  |  ├ scoped_profiling_label_wrapper.h
    |      |  |  ├ strided_slice_logic.h
    |      |  |  ├ tensor.h
    |      |  |  ├ tensor_ctypes.h
    |      |  |  └ types.h
    |      |  ├ kernel_util.cc
    |      |  ├ kernel_util.h
    |      |  ├ op_macros.h
    |      |  └ padding.h
    |      ├ micro
    |      |  ├ examples
    |      |  |  └ hello_world
    |      |  |      ├ constants.cc
    |      |  |      ├ constants.h
    |      |  |      ├ main.cc
    |      |  |      ├ main_functions.cc
    |      |  |      ├ main_functions.h
    |      |  |      ├ output_handler.cc
    |      |  |      ├ output_handler.h
    |      |  |      ├ sine_model_data.cc
    |      |  |      └ sine_model_data.h
    |      |  ├ kernels
    |      |  |  ├ activations.cc
    |      |  |  ├ activation_utils.h
    |      |  |  ├ add.cc
    |      |  |  ├ all_ops_resolver.cc
    |      |  |  ├ all_ops_resolver.h
    |      |  |  ├ arg_min_max.cc
    |      |  |  ├ ceil.cc
    |      |  |  ├ comparisons.cc
    |      |  |  ├ concatenation.cc
    |      |  |  ├ conv.cc
    |      |  |  ├ depthwise_conv.cc
    |      |  |  ├ dequantize.cc
    |      |  |  ├ elementwise.cc
    |      |  |  ├ floor.cc
    |      |  |  ├ fully_connected.cc
    |      |  |  ├ logical.cc
    |      |  |  ├ logistic.cc
    |      |  |  ├ maximum_minimum.cc
    |      |  |  ├ micro_ops.h
    |      |  |  ├ micro_utils.h
    |      |  |  ├ mul.cc
    |      |  |  ├ neg.cc
    |      |  |  ├ pack.cc
    |      |  |  ├ pad.cc
    |      |  |  ├ pooling.cc
    |      |  |  ├ prelu.cc
    |      |  |  ├ quantize.cc
    |      |  |  ├ reshape.cc
    |      |  |  ├ round.cc
    |      |  |  ├ softmax.cc
    |      |  |  ├ split.cc
    |      |  |  ├ strided_slice.cc
    |      |  |  ├ svdf.cc
    |      |  |  └ unpack.cc
    |      |  ├ memory_planner
    |      |  |  ├ greedy_memory_planner.cc
    |      |  |  ├ greedy_memory_planner.h
    |      |  |  ├ linear_memory_planner.cc
    |      |  |  ├ linear_memory_planner.h
    |      |  |  └ memory_planner.h
    |      |  ├ riscv64_mcu
    |      |  |  ├ debug_log.cc
    |      |  |  └ README.md
    |      |  ├ tools
    |      |  |  └ make
    |      |  |      └ downloads
    |      |  |          ├ flatbuffers
    |      |  |          |  ├ include
    |      |  |          |  |  └ flatbuffers
    |      |  |          |  |      ├ base.h
    |      |  |          |  |      ├ flatbuffers.h
    |      |  |          |  |      └ stl_emulation.h
    |      |  |          |  └ LICENSE.txt
    |      |  |          └ gemmlowp
    |      |  |              ├ fixedpoint
    |      |  |              |  ├ fixedpoint.h
    |      |  |              |  └ fixedpoint_sse.h
    |      |  |              ├ internal
    |      |  |              |  └ detect_platform.h
    |      |  |              └ LICENSE
    |      |  ├ compatibility.h
    |      |  ├ debug_log.h
    |      |  ├ debug_log_numbers.cc
    |      |  ├ debug_log_numbers.h
    |      |  ├ memory_helpers.cc
    |      |  ├ memory_helpers.h
    |      |  ├ micro_allocator.cc
    |      |  ├ micro_allocator.h
    |      |  ├ micro_error_reporter.cc
    |      |  ├ micro_error_reporter.h
    |      |  ├ micro_interpreter.cc
    |      |  ├ micro_interpreter.h
    |      |  ├ micro_mutable_op_resolver.cc
    |      |  ├ micro_mutable_op_resolver.h
    |      |  ├ micro_optional_debug_tools.cc
    |      |  ├ micro_optional_debug_tools.h
    |      |  ├ micro_utils.cc
    |      |  ├ micro_utils.h
    |      |  ├ simple_memory_allocator.cc
    |      |  ├ simple_memory_allocator.h
    |      |  ├ test_helpers.cc
    |      |  └ test_helpers.h
    |      ├ schema
    |      |  └ schema_generated.h
    |      ├ string_type.h
    |      ├ string_util.h
    |      ├ type_to_tflitetype.h
    |      └ version.h
    ├ AUTHORS
    └ LICENSE


デバッグログの出力


HWやtoolchain固有の部分であり、DebugLog関数を実装することになる。RISC-V向けには、RISC-V 32bitをそのまま流用することで、シリアルにログが出力される。


ビルドプロジェクト


今回は、TensorFlow Lite for microのMakefileを使用するのではなく、Kendryte K210 standalone SDKのcmakeを使ってのビルド方法にした(そのほうが楽そうだった)。

ビルド用のプロジェクトは以下で公開している。



TensorFlow Lite for microのhello worldで使用するソース・ヘッダファイルのみをsrc配下に格納する。
※tensorflowすべてのファイルを格納してしまうと、SDK内でヘッダファイルの競合が発生するため、必要なファイルのみとした。

CMakeLists.txtには、必要なヘッダのパス、ソースファイルの一覧を記載する。また、前々回のブログでも記載した、std::roundの参照エラー(#35302)の回避のために、以下の定義を追加しておく。あと、Kendryte K210 standalone SDKが便利なマクロを用意してくれるので助かる。

M5StickV向けのビルドで発生するビルドエラーの回避


1か所、ビルドエラーが発生するためにソースを修正する。
std::minの第二引数「(1ll << 31) - 1.0」がfloatであるといわれて、第一引数「beta * input_scale * (1 << (31 - input_integer_bits))」のdoubleと型が不一致だと怒られてしまう。第二引数「(1ll << 31) - 1.0」はdoubleではないのか?(しかも、前々回のブログではエラーとならなかった...)のでもしかしたら、toolchainによるものかも?

とりあえず、doubleにキャストすることで回避する。


ビルド


kflash_guiが(自分の環境だと)Windowsでしか動かないのでWindows 10でビルドする。

Windows版のCMakeをインストールする。

Kendryte RISC-V GNU Compiler Toolchainをダウンロードして適当なディレクトリで展開する。

PowerShellで操作。
> git clone https://github.com/NobuoTsukamoto/m5stickv-tensorflow-lite-micro.git
> $env:Path="\kendryte-toolchain\bin;C:\Program Files\CMake\bin;" + $env:Path
> cd m5stickv-tensorflow-lite-micro\src\micro
> mkdir build
> cd build
> cmake -G "MinGW Makefiles" ../../../..
> make

ビルド後、buildディレクトリに「micro」と「micro.bin」のファイルができる。
「micro.bin」ファイルをkflash_guiでM5StickVに焼けばOK。


結果


Teratermで接続すると、シリアルにログが出力される。



次回


つぎは、M5StickVのLCDに結果表示するために行ったことを記載する。


2020年1月4日土曜日

TensorFlow lite for micro で RISC-Vターゲットのサンプルをqemuで動かす

目的


前回のブログ「TensorFlow lite for micro で RISC-Vターゲットのサンプルをビルドする」で生成した、Hello worldのバイナリを実行する。
ただ、実機("Hifive1" SiFive FE310 development board)は持っていないため、qemuで動作させる。


qemuでの実行


qemuの準備


RISC-Vターゲットのqemuを準備する。Fedora 31にはパッケージ(qemu-system-riscv 4.1.1)が存在するが、今回はver.4.2をソースビルドする。

ソースビルド


依存関係のインストール。追加で必要になったのは以下(その他はTensorFlowなどのビルドで入っているかも?)。

$ sudo dnf install glib2-devel pixman-devel

ソースファイルをダウンロード。

$ wget https://download.qemu.org/qemu-4.2.0.tar.xz
$ tar xf qemu-4.2.0.tar.xz
$ cd qemu-4.2.0/

configure, makeを行う。ターゲットはriscv32-softmmuのみでよいはず。後のことも考えて、その他のRISC-Vターゲットもビルドする。

$ ./configure --target-list=riscv64-softmmu,riscv32-softmmu,riscv64-linux-user,riscv32-linux-user
$ make -j$(nproc)
$ sudo make install


バイナリのqemuでの実行


前回のブログで作成したTensorFlow lite for microのRISC-Vターゲットのバイナリ(Hello worldのサンプル)を実行する。
オプションの指定は以下。
  • machineオプション: sifive_e
  • kernelオプション: 作成したELFファイル
    tensorflow/tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world

実行。

$ qemu-system-riscv32 -nographic -machine sifive_e -kernel tensorflow/tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_worl

コンソールにログが出力される。Hello worldのサンプルは正弦波を出力するモデルである。x_value, y_valueにそれなりの値が永遠と出力される。




2020年1月3日金曜日

TensorFlow lite for micro で RISC-Vターゲットのサンプルをビルドする

目的

  • TensorFlow Lite for microをやってみる。
  • はじめのサンプル"Hello world"をビルドして、ビルド方法やビルドしているものをなんとなく見てみる。
  • ターゲットはRISC-V、最終的にはRISC-Vプロセッサで動かしたい。


動機


去年よりTensorFlow Lit for microをやってみたいと思ってた。また、RISC-Vにも興味がある。


ビルドする前に


2019.1.3時点で、TensorFlowのmaster(commit 68cbb78)では、RISC-V("Hifive1" SiFive FE310 development board)向けのビルド手順がREADMEから削除されている。いつの時点からなのか、理由は不明。このあたりまでは、手順があった。experimentalが外れたときに削除されたのかも?

また、現時点ではRISC-Vのターゲットではビルドができない。PRはあるがまだ取り込まれていない。
#35302はレビュー待ちの状態なので、そのうち取り込まれると思われる。
#33972はコンフリクトが発生(microがexperimentalでなくなったため)しているため、いつ取り込まれるかは不明。。。

この2つはRISC-Vターゲットの固有のものなので、少し内容をおさえておく。

#35302


RISC-Vターゲットでのみビルドエラーが発生する。C++11にはstd::roundがあるが、RISC-V toolchainには何故か存在せず、::roundが使える(おそらく、toolcainのバグ?)。このため、tflite::TfLiteRound を使うことにする。tflite::TfLiteRoundはstd::roundが定義されていない場合は、::roundを使用するようになっているだけである。

std::roundが使えず、::roundが使える理由はおそらく、このtweetのリンク先と同じだろう(RISC-V toolchainのことではないが)。



#33972


もとのissueはこちら。2つの問題がある。
  • __wrap__funsのundefined referenceが発生する。
    lite/micro/tools/make/targets/mcu_riscv_makefile.incにある変数が上書きされてしまうためである。このため、example/xxxx配下に変数が上書きされないよう、makfile.incを作ることで回避。
  • __dso_handleが定義されていない。
    RISC-V toolcainには__dso_handleが定義されていない。lite/experimental/micro/arduino/abi.cc に定義してビルドすることで回避している(fno-use-cxa-atexitオプションで回避できるのだろうか?)。


ビルド


ようやく本題。

手順は古いREADMEを参照しながら。#35302、#33972については自分のリポジトリに取り込んだ。このため、この手順は公式のものでなく、今後変更される可能性は大。


まずは、githubからリポジトリをclone。ブランチは"micro_riscv"。

$ git clone -b micro_riscv https://github.com/NobuoTsukamoto/tensorflow.git

ビルド用のDocker imageをビルドし、コンテナを立ち上げる。

$ sudo docker build -t riscv_build -f ./tensorflow/tensorflow/lite/micro/testing/Dockerfile.riscv ./tensorflow/tensorflow/lite/micro/testing/
$ sudo docker run -it -v ./tensorflow/micro/tensorflow:/workspace riscv_build:latest bash

コンテナ内でhello worldのサンプルのビルドを行う。RISC-V toolchainはビルド時にダウンロードする(ダウンロードするtoolchainはここを参照)。toolchainへのパスを通しておく。

# export PATH=${PATH}:/workspace/tensorflow/lite/micro/tools/make/downloads/riscv_toolchain/bin/
# cd /workspace
# make -f tensorflow/lite/micro/tools/make/Makefile TARGET=riscv32_mcu hello_world_bin

ビルド時に気になるメッセージがあるが、バイナリが生成されているので気にしない(たぶん、問題ない?)。

/workspace/tensorflow/lite/micro/tools/make/downloads/riscv_toolchain/bin/../lib/gcc/riscv64-unknown-elf/8.1.0/../../../../riscv64-unknown-elf/bin/ld: tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world: section `.gcc_except_table._ZN9__gnu_cxx27__verbose_terminate_handlerEv' can't be allocated in segment 1
LOAD: .data .gcc_except_table._ZN10__cxxabiv111__terminateEPFvvE .gcc_except_table.__gxx_personality_v0 .gcc_except_table.__cxa_call_unexpected .gcc_except_table._ZN9__gnu_cxx27__verbose_terminate_handlerEv

ビルドが終了するとtensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin 配下にELFと.binファイルが生成される。readelfコマンドで見るとRISC-V 32bit向けであることがわかる。

# ls -alh tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin
drwxr-xr-x. 2 root root 4.0K Jan  3 08:56 .
drwxr-xr-x. 5 root root 4.0K Jan  3 06:14 ..
-rwxr-xr-x. 1 root root 4.0M Jan  3 06:14 hello_world
-rwxr-xr-x. 1 root root 1.5G Jan  3 06:14 hello_world.bin
# readelf -h tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           RISC-V
  Version:                           0x1
  Entry point address:               0x20400000
  Start of program headers:          52 (bytes into file)
  Start of section headers:          4175496 (bytes into file)
  Flags:                             0x1, RVC, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         3
  Size of section headers:           40 (bytes)
  Number of section headers:         37
  Section header string table index: 36


Renodeによるテスト


Renodeのエミュレータによるバイナリのテストを行う。まず、テスト用バイナリの生成を行う。hello_world_test が生成される。

# make -f tensorflow/lite/micro/tools/make/Makefile TARGET=riscv32_mcu hello_world_test

スクリプトはmicro_speech用なのでhello_worldに書き換える。
/workspace/tensorflow/lite/micro/testing/sifive_fe310.resc ファイルを修正する。10行目のパスの実行ファイルを"micro_speech_test"から"hello_world_test"に変更する。
$bin?=@/workspace/tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world_test

Renodeでバイナリを実行する。~~~ALL TESTS PASSED~~~が出力されればOK。

# renode -P 5000 --disable-xwt -e 's@/workspace/tensorflow/lite/micro/testing/sifive_fe310.resc'
08:56:34.2849 [INFO] Loaded monitor commands from: /opt/renode/./scripts/monitor.py
08:56:34.3117 [INFO] Monitor available in telnet mode on port 5000
08:56:34.6272 [INFO] Including script: /workspace/tensorflow/lite/micro/testing/sifive_fe310.resc
08:56:34.6453 [INFO] System bus created.
08:56:35.1569 [DEBUG] Segment size automatically calculated to value 64KiB (3)
08:56:35.3093 [DEBUG] Segment size automatically calculated to value 16MiB
08:56:35.3691 [WARNING] Previous log file detected and renamed to: /tmp/renode_riscv_log.txt.1
08:56:35.5157 [DEBUG] sysbus: Loading ELF /workspace/tensorflow/lite/micro/tools/make/gen/riscv32_mcu_riscv32_mcu/bin/hello_world_test.
08:56:35.5244 [INFO] sysbus: Loading segment of 135576 bytes length at 0x20400000.
08:56:35.5396 [DEBUG] sysbus: Segment loaded.
08:56:35.5397 [INFO] sysbus: Loading segment of 2152 bytes length at 0x20421198.
08:56:35.5399 [DEBUG] sysbus: Segment loaded.
08:56:35.5784 [INFO] cpu: Setting PC value to 0x20400000.
08:56:35.7980 [INFO] SiFive-FE310: Machine started.
08:56:35.8604 [WARNING] sysbus: [cpu: 0x20400AA8] ReadDoubleWord from non existing peripheral at 0x10000070.
08:56:35.8615 [WARNING] sysbus: [cpu: 0x20400AB6] WriteDoubleWord to non existing peripheral at 0x10000070, value 0x0.
08:56:35.8616 [WARNING] sysbus: [cpu: 0x20400AC2] (tag: 'PRCI_HFROSCCFG') WriteDoubleWord to non existing peripheral at 0x10008000, value 0x40100004.
08:56:35.8618 [WARNING] sysbus: [cpu: 0x20400AC8] (tag: 'PRCI_HFROSCCFG') ReadDoubleWord from non existing peripheral at 0x10008000, returning 0xFFFFFFFF.
08:56:35.8620 [WARNING] sysbus: [cpu: 0x20400ACE] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8622 [WARNING] sysbus: [cpu: 0x20400AD6] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0xFFFEFFFF.
08:56:35.8624 [WARNING] sysbus: [cpu: 0x20400AD8] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8625 [WARNING] sysbus: [cpu: 0x20400AE8] (tag: 'PRCI_HFROSCCFG') WriteDoubleWord to non existing peripheral at 0x10008000, value 0x40100004.
08:56:35.8627 [WARNING] sysbus: [cpu: 0x20400AEE] (tag: 'PRCI_HFROSCCFG') ReadDoubleWord from non existing peripheral at 0x10008000, returning 0xFFFFFFFF.
08:56:35.8629 [WARNING] sysbus: [cpu: 0x20400AF4] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8631 [WARNING] sysbus: [cpu: 0x20400AFC] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0xFFFEFFFF.
08:56:35.8632 [WARNING] sysbus: [cpu: 0x20400B04] WriteDoubleWord to non existing peripheral at 0x10014000, value 0x8.
08:56:35.8634 [WARNING] sysbus: [cpu: 0x20400B0E] WriteDoubleWord to non existing peripheral at 0x1000800C, value 0x100.
08:56:35.8651 [WARNING] sysbus: [cpu: 0x20400B18] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0x405F1.
08:56:35.8652 [WARNING] sysbus: [cpu: 0x20400B1A] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8653 [WARNING] sysbus: [cpu: 0x20400B24] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0xFFFBFFFF.
08:56:35.8989 [WARNING] sysbus: [cpu: 0x20400B42] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8990 [WARNING] sysbus: [cpu: 0x20400B4C] (tag: 'PRCI_PLLCFG') ReadDoubleWord from non existing peripheral at 0x10008008, returning 0xFFFFFFFF.
08:56:35.8991 [WARNING] sysbus: [cpu: 0x20400B52] (tag: 'PRCI_PLLCFG') WriteDoubleWord to non existing peripheral at 0x10008008, value 0xFFFFFFFF.
08:56:35.8994 [WARNING] gpioInputs: Unhandled read from offset 0x3C.
08:56:35.8998 [WARNING] gpioInputs: Unhandled write to offset 0x3C, value 0x0.
08:56:35.8998 [WARNING] gpioInputs: Unhandled read from offset 0x38.
08:56:35.8999 [WARNING] gpioInputs: Unhandled write to offset 0x38, value 0x30000.
08:56:36.0392 [DEBUG] uart0: [+0.67s host +0.4ms virt 0.4ms virt from start] core freq at 170833 Hz
08:56:36.0416 [DEBUG] uart0: [+2.47ms host +0s virt 0.4ms virt from start]   Testing LoadModelAndPerformInference
08:56:36.0771 [DEBUG] uart0: [+35.5ms host +0.7ms virt 1.1ms virt from start]   1/1 tests passed
08:56:36.0776 [DEBUG] uart0: [+0.52ms host +0s virt 1.1ms virt from start]   ~~~ALL TESTS PASSED~~~
08:56:36.0777 [DEBUG] uart0: [+83?s host +0s virt 1.1ms virt from start]   
08:56:36.0798 [DEBUG] uart0: [+2.14ms host +0s virt 1.1ms virt from start]   
08:56:36.0808 [DEBUG] uart0: [+0.99ms host +0s virt 1.1ms virt from start]   Progam has exited with code:0x00000000
09:02:45.0257 [INFO] SiFive-FE310: Machine paused.
09:02:45.0330 [DEBUG] SiFive-FE310: Disposing sysbus.cpu.
09:02:45.0352 [DEBUG] SiFive-FE310: Disposing sysbus.
09:02:45.0358 [DEBUG] SiFive-FE310: Disposing sysbus.maskRom.
09:02:45.0360 [DEBUG] SiFive-FE310: Disposing sysbus.otp.
09:02:45.0361 [DEBUG] SiFive-FE310: Disposing sysbus.dtim.
09:02:45.0362 [DEBUG] SiFive-FE310: Disposing sysbus.qspi0Xip.
09:02:45.0371 [INFO] SiFive-FE310: Disposed.


その他


今回、ビルドする際の情報を記載する。

RISC-V toolchainについて


デフォルトで使用しているtoolchainはSiFiveのtoolchain(最新でない)である。別途、riscv-gnu-toolchainでビルドしたものも使用できるはず。なお、Newlib cross-compilerの32bit向けが必要。


ボード向けのBSPなど


これもダウンロードスクリプトでダウンロードしている。Makefileでもこのあたりこのあたり(各サンプルのMakefile)でヘッダや必要なモジュールを加えているので参考になりそう。


Renodeについて


まだ、あまりわかっていない。オープンソースの"virtual development tool "。RISC-Vだけでなくて、その他アーキテクチャもサポートする仮想開発ツール?もう少し調べてみよう。

今回、dockerのコンテナ内でビルドしたのはRenodeを使用するためである。Renodeを使わなければ、dockerは不要。


最後に


TensorFlow Lite for microのRISC-V向けのhello worldのバイナリを作成した。次はこのバイナリを動作させてみたい。

2019年12月29日日曜日

2019年の振り返り

今年はいろいろとやった気がするので、振り返ってみようと思う。やっぱり、Edge TPUとJetson Nanoを中心にとても楽しい一年だったと思う。


エッジ向けAIデバイス


今年はエッジ向けAIデバイスがブームになったと思う。個人では3つ購入して遊んでみた。


個人でこのような素晴らしいデバイスを購入できるようになったのはとても良いと思う。また、AIデバイスによってOn-DeviceでDeep Learningができるようになることは、とても大きい可能性があると思う。


Edge TPU


3月に発売を知って、真っ先に買ってみた。
画像認識、物体検出だけでなくて、セグメンテーションモデルやポーズ推定も動作するのは素晴らしい。まだまだ、使い方はたくさんあると思うので、来年も継続してやってみたい。

こんなことをやってみた。

ブログ(いくつか)


コード

また、TensorFlow Liteについても、ちょっとだけわかった気がする(あくまで気がする)。ラズパイなどのARM CPUでそれなりに動作できるのはとても素晴らしいと思う。モバイルも含めていろいろとできることが増えるなぁと感じる。
TensorFlow Lite for MicrocontrollersやAndoridも気になる分野なので来年は絶対、チャレンジする。

インフルエンサーのかたがつぶやくとものすごい広まることも知った(数日で1,000ビュー超えるなんて初めて...)。


Jetson Nano


最初は我慢していたけど、物欲に負けて7月に購入。
TF-TRT(TensorFlow integration with TensorRT)を中心にやってみた。また、もっと推論は早くなるはず、と指摘いただいて試してみた結果、原因が判明したこともよかった。

ブログ
コード

CUDAはいろいろと使い方がたくさんある。まだ、TensorRTも試せていないので、来年は手を出したいと思う。


M5STICKV


RISC-V CPUとKPUプロセッサで\3,000ちょっとはとても安い!これも物欲に負けて購入。まだ十分使えていないので、来年はやってみる!これを使ってやってみたいことはたくさんある。


MediaPipe


気になっていて、ようやく試せた。MLのアプリケーション作るのは面倒!だけど、フレームワークにして楽にしちゃおう!という感じだと思うけど、これからこういったアプローチは増えてくるのだろうか?来年もウォッチしようと思う。

ブログ
コード



コンペ


今年の前半は、いくつか画像のコンペをチャレンジしてみた。まあ、メダルは取れたけど、やっぱり金じゃないのはくやしい。上位を目指したい。時間が取れなくてチャレンジだけで終わってしまったのもいくつかる。来年はどうしよう?もう一回チャレンジしてみようかな?


釣り


まぁ、今年も行ける回数は少なかったよね(しょうがない)。しかも数年ぶりにボウズもあった(涙)来年も、数回は行きたいね。







ラーメン


一応、長野はラーメン激戦区。お店は14回訪問。月1回ぐらいのペース?今度はブログにラーメンを書いてみようかな。旨い!しかないけど。。。

久々の阿吽。塩。健康診断の後の一杯は最高

土鍋や竹さん。いつもおいしい!

県外でのラーメンは1回だけ。東京ラーメンストリートの斑鳩


最後に


書かなかったけど、Yoctoもっと詳しくなりたいし、カメラもうまくなりたい。jax colab tpuもある、ラズパイ4も使い倒したい... スノーボードも復活したいし...
やりたいことはたくさんある。

ただ、今年は楽しい一年だったと思う。
一番は、Twitterでいろいろとアドバイスやいいねをいただくフォロワーさんが増えたことがうれしかった(ほんと、ありがとうございます)。いろいろとリプライをもらったり、ここに記載できないものがたくさんありすぎて、感謝。やっぱり、色々もらえるとモチベーションもちがうんだなぁとわかった。

来年も、面白いことをやってきたいと思う。

2019年12月27日金曜日

MediaPipeでEdge TPU


目的


ここ最近、MediaPipeのサンプルをEdge TPUで動作させてみて、オリジナルから変えてみたこと、ハマったこと、感じたことをまとめてみる。


動機


前のブログでも書いたとおり、MediaPipeは気になっていた。

そこにGoogle Coral DevBoard向けのサンプルがでてきた。
DevBoardを持っておらず購入しようか迷ったが、USB Acceleratorでもできるのは?と思いやって見た結果、Jetson Nano + Edge TPU、Raspberry Pi + Edge TPUで動かすことができた。





メリットとデメリット


思いつくままにメリットとデメリットを記載したい。ただし、所感であり誤りも含んでいると思うので注意。

論文、ドキュメントは以下を参照。


メリット


モジュールの再利用性と非同期処理のサポートが特徴と感じる。

これはMediaPipeが目的でもあるMLアプリケーションのプロトタイプモデルをすばやく構築するために再利用性を高めることと、MLアプリケーションには必要になる非同期処理(重い推論と他の処理を分離)を意識していることからだと思われる。

  • モジュールの再利用
  • 非同期処理のサポート
  • フロー制御のサポート

モジュールの再利用がしやすい


Calculatorとしてコンポーネントを分割することで、再利用しやすい作りとなっている。このコンポーネントをCalculatorと呼んでいる。特に推論を行う前処理、後処理は同じものを再利用したい。AnnotationOverlayCalculator(bonding boxの描画)やPacketResamplerCalculator(フレームの間引き)などがそれに当たる。あと、CPU⇔GPU間のメモリ割当もサポートしているため、AndroidのGPU delegateも簡単に構築できる。


非同期処理のサポート


一般に、推論部分は一番重い(処理時間がかかる)。

このため、GPU(もしくはAccelerator、CPUなど)で推論している間に他の処理をCPUで行うことで、トータルの処理時間を短縮させたい。一般的には、カメラ入力、推論、描画は非同期に行って描画を高速にしたい(見せたい)ということもある。しかし、一般的に非同期処理を実装するのは難易度が高い。

MediaPipeにはtimestampと呼ばれる概念で同期を取ることができる。timestampはデータ(packetと呼ばれる)が生成されるたびにインクリメントされていく。Calculatorは分散で処理していて、データ(packetと呼ばれる)に付属するtimestampによって、データの到達順が管理され、Calculator間のデータの同期が取られる。


フロー制御のサポート


推論など重い処理を担当するCalculatorは、データのボトルネックとなってしまう。適切な制御を行わないとオーバーフローやデッドロックの原因になってしまう。このため、MediaPipeは2つのフロー制御を持っている。
  • The back-pressure mechanism
    ストリームでバッファリングされたPacketが制限に達すると、上位ノードの実行を抑制する。
  • 専用ノードの挿入
    PacketResamplerCalculatorを使ったノードの場合は、Packetを間引きする(たとえば、フレームレートを1/10に間引くなど)。
    FlowLimiterCalculatorを使ったノードの場合は、処理が終わるまでの間のPacketを受け捨てる。

デメリット


まだ、ドキュメントやツール類が追いついていないところにデメリットを感じてしまう。このあたりは今後改善されるかもしれない。
  • Bazelによるビルド
  • どんなCalculatorがあるのか?
  • デバッグツールがない?
  • 実装がC++

Bazelによるビルド


Bazelを多少は理解していないといけないところは辛い。また、graphとBUILDの対応は手動で行う必要がある(graphからビルドするCalculatorのパス・ファイル名を割り出さないといけない)。もう少しスマートな方法がないのだろうか?


どんなCalculatorがあるのか?


これは、ドキュメントが整備されていないのが原因。今は、ソースから何があるのかを拾い出さないといけない。ドキュメントが整備されれば解消されるとは思う。


デバッグツールがない?


Calculatorが非同期で動作するということは、デバッグが大変である。論文を見る限りツールがありそうだがまだすべてが公開されていないようである。ブレークポイントを設定してのデバッグは大変かもしれない...


実装がC++


現状はアプリケーション、CalculatorともC++での実装が必要。C++は敷居が高いと思われる。少なくともアプリケーションはPythonで書きたいのでは?


Edge TPU向けのサンプルを作成したポイント


ここでは、Coral Dev Board Setup (experimental)からEdge TPU USB Acceleratorを使ったサンプルを作成したのポイントを残す。
作成したサンプルは2つ。

Jetson Nano + Edge TPU


こちらは、ほとんどCoral Dev Board Setup (experimental)から変更はない。
Jetson Nano側にEdge TPUのRuntimeをインストールするぐらいである。OpenCVもJetpackにインストール済みのライブラリを使用すればよかった。

Raspberry Pi + Edge TPU


Raspbianは32bit OSであるため、クロスコンパイル時の指定を"--cpu=armv7a"にするぐらいである。ほぼ変更なく動作できるのは、MediaPipeの強みの一つだと思う。


Object Detection and Tracking


AndroidのデモをRaspberry Pi + Edge TPUで動かそうと思ったが、ハマってしまった... 最初の検出は動作するのだが、2回目以降の検出がされない... 原因は、Raspberry Pi(もとはCoarl)のメイン処理とPacketResamplerCalculatorにあった。

もともと、Object Detection and TrackingのGraphは、入力ストリーム(カメラ or ビデオ)を間引いて推論を行っている(トラッキングや表示は間引かない)。これは、時間のかかる・リソース消費が大きい推論を全フレームで行わず、トラッキングで補完することで、リアルタイム・リソース消費を軽くしている。このフレームの間引きをPacketResamplerCalculatorが担当し、デフォルトでは0.5フレームにしている(フロー制御の一つ)。

PacketResamplerCalculatorはデータのtimestampをチェックしてtimestampが指定したフレームレートの時間に達したら、後続のCalculatorに流すようになっている。このため、timestampは経過時間(実時間)を期待している。

timestampはデータの生成の際(メイン処理)に付与され、増加している。Raspberry Pi(もとはCoarl)のメイン処理では、timestampが+1しかされていなかった。このため、timestampの時間の流れが非常にゆっくりになってしまったのが原因だった...(timestampはμs単位なので、非常に長ーく待っていれば、2回目の推論がくる...)

  while (grab_frames) {
    // Capture opencv camera or video frame.
    cv::Mat camera_frame_raw;
    capture >> camera_frame_raw;
    if (camera_frame_raw.empty()) break;  // End of video.
    cv::Mat camera_frame;
    cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB);
    if (!load_video) {
      cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1);
    }

    // Wrap Mat into an ImageFrame.
    auto input_frame = absl::make_unique(
        mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows,
        mediapipe::ImageFrame::kDefaultAlignmentBoundary);
    cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get());
    camera_frame.copyTo(input_frame_mat);

    // Send image packet into the graph.
    MP_RETURN_IF_ERROR(graph.AddPacketToInputStream(
        kInputStream, mediapipe::Adopt(input_frame.release())
                          .At(mediapipe::Timestamp(frame_timestamp++))));
    // ★↑ここでframe_timestampが+1されていることが原因...

    // Get the graph result packet, or stop if that fails.
    mediapipe::Packet packet;
    if (!poller.Next(&packet)) break;
    auto& output_frame = packet.Get();

    // Convert back to opencv for display or saving.
    cv::Mat output_frame_mat = mediapipe::formats::MatView(&output_frame);
    cv::cvtColor(output_frame_mat, output_frame_mat, cv::COLOR_RGB2BGR);
    if (save_video) {
      writer.write(output_frame_mat);
    } else {
      cv::imshow(kWindowName, output_frame_mat);
      // Press any key to exit.
      const int pressed_key = cv::waitKey(5);
      if (pressed_key >= 0 && pressed_key != 255) grab_frames = false;
    }
  }


このため、実時間(前回データ生成時との時間の差)を与えることで、問題が解消できたのである。修正のコードは下記。githubにもアップしている。std::chronoで前回からの時間差をとっている(ただ、この修正だと、ビデオファイル入力ではまずい... ビデオファイル入力の場合は、フレームレートから算出しないといけない)。

  std::chrono::system_clock::time_point start;
  start = std::chrono::system_clock::now();
  while (grab_frames) {
    // Capture opencv camera or video frame.
    cv::Mat camera_frame_raw;
    capture >> camera_frame_raw;
    if (camera_frame_raw.empty()) break;  // End of video.
    cv::Mat camera_frame;
    cv::cvtColor(camera_frame_raw, camera_frame, cv::COLOR_BGR2RGB);
    if (!load_video) {
      cv::flip(camera_frame, camera_frame, /*flipcode=HORIZONTAL*/ 1);
    }

    // Wrap Mat into an ImageFrame.
    auto input_frame = absl::make_unique(
        mediapipe::ImageFormat::SRGB, camera_frame.cols, camera_frame.rows,
        mediapipe::ImageFrame::kDefaultAlignmentBoundary);
    cv::Mat input_frame_mat = mediapipe::formats::MatView(input_frame.get());
    camera_frame.copyTo(input_frame_mat);

    // Get timestamp.
    // ★ここで時間差(μs)を取得する。
    auto end = std::chrono::system_clock::now();
    frame_timestamp = std::chrono::duration_cast(end - start).count();
    
    // Send image packet into the graph.
    MP_RETURN_IF_ERROR(graph.AddPacketToInputStream(
        kInputStream, mediapipe::Adopt(input_frame.release())
                          .At(mediapipe::Timestamp(frame_timestamp))));

    // Get the graph result packet, or stop if that fails.
    mediapipe::Packet packet;
    if (!poller.Next(&packet)) break;
    auto& output_frame = packet.Get();

    // Convert back to opencv for display or saving.
    cv::Mat output_frame_mat = mediapipe::formats::MatView(&output_frame);
    cv::cvtColor(output_frame_mat, output_frame_mat, cv::COLOR_RGB2BGR);
    if (save_video) {
      writer.write(output_frame_mat);
    } else {
      cv::imshow(kWindowName, output_frame_mat);
      // Press any key to exit.
      const int pressed_key = cv::waitKey(5);
      if (pressed_key >= 0 && pressed_key != 255) grab_frames = false;
    }
  }


最後に


MediaPipeを使うとMLアプリケーションの開発がとても簡単になるように思える。
ただ、Calculator(コンポーネント)の実装やデバッグまで考えると難易度が高いようにも思える。
もう少し使われるようになるのか?2020年もウォッチしてみたいと思う。