TensorFlowのモデルをIntelのMovidius Neural Compute Stickを使ってRaspberryPiで動作させるメモ

概要

Raspberry PiでTensorFlow使って画像認識してしたい!
でもRaspberry PiのCPUでTensorFlow動かしても死ぬほど遅い
そこでIntelのMovidiusをRPIにぶっさすことで,超高速に推論ができるというものです.

これを動かすのにとても苦労したので,メモとして残しておきます.

ちなみに,推論しかできません.学習は別のコンピュータでやりましょう.

手順としては,

  1. ハイスペックなコンピュータでTensorFlowを使って学習
  2. TensorFlowの学習済みモデルを保存して,Movidiusで動くグラフにコンパイル
  3. グラフをRPIに持ってきて,Movidiusで推論

という方法になります. f:id:tokunn:20180922135256p:plain

この記事について

以下,やってる途中に書いたメモのコピペです.
説明するために書いてるわけじゃないのでかなり読みずらいと思います.
質問がありましたらTwitterまで
とーくん (@KTokunn) | Twitter

長ったらしいメモなので真面目に読むことはお勧めしません
ctrl+fでエラーメッセージのキーワードで検索してお読みください.

やったこと
- TensorFlow Low LayerのMnist (deep mnist)のサンプルをMovidiusで実行
- mvNCCompile,mvNCProfile,mvNCCheckが動くようにライブラリを書き換え (なぜか私の環境ではこれらのコンパイラが正しく動作しなかった)
- mvNCCheckに画像を読み込ませるやつはむっちゃごり押し

あとは,やたら出力とかをdetailsタグでまとめたので

まとめられてる部分
って感じで小さくまとめてありますので,左の三角形を押して開いて見てください.

Movidius NCSDK について

NCSDKとは

Movidius NCSDKはMovidius用にモデルを変換・チェックするためのコンパイラと、モデルを利用して推測を行うためのAPIを含むSDKのこと。

 mvNCCompile, mvNCCheck, mvNCProfile  

Movidius NCSDKのバージョン

  • NCSDK1
  • NCSDK2

NCSDK2に含まれるNCAPI v2ではAPIの関数が異なるため互換はない。

今回は情報が多いのでNCSDK1を使っていくことにします.
なんか,SDK1だと16bit floatしか使えないのに対してSDK2だと32bit float使えるらしい(?)

Movidius NCSDKをTensorFlowから使う方法

  1. TensorFlowでプログラムを書く。
  2. モデルを保存する。
  3. TensorFlowからモデルを開いて編集をして、もう一度保存する。
  4. 保存したモデルをMovidius用のgraphにコンパイルする。
  5. graphをMovidiusに転送して実行する。

NCSCKのインストール

詳しくは https://movidius.github.io/ncsdk/install.html

Raspberry Pi 3で行う場合にはそれぞれ4時間程度かかる。

NCSDK1

wget https://ncs-forum-uploads.s3.amazonaws.com/ncsdk/ncsdk-02_05_00_02-full/ncsdk-2.05.00.02.tar.gz

NCSDK2

wget https://ncs-forum-uploads.s3.amazonaws.com/ncsdk/ncsdk-01_12_00_01-full/ncsdk-1.12.00.01.tar.gz

共通

tar xvf ncsdk-*
cd ncsdk-*
make install
make examples

Exampleの実行 (v1, v2)

ncsdk-*/examples/tensorflow/inception_v3にInception-V3のExampleがある。

ファイル構成

Makefile
inception-v3.py
run.py
categories.txt
inputsize.txt

動作

makeでinception-v3.pyとmvNCCompileが呼び出される。 inception-v3.pyは学習済みのInception-V3モデルのダウンロード、ckpt.metaファイルの保存を行う。 モデルはtensorflow.contrib.slim.netsからダウンロードされる。 mvNCCompileはckpt.metaからgraphファイルを作り出す。
make runでrun.pyが呼び出される。 run.pyではgraphファイルをMovius上にロードして一枚の画像の推論を行い、結果を出力する。

動作結果

Number of categories: 1001
Start download to NCS...
*******************************************************************************
inception-v3 on NCS
*******************************************************************************
547 electric guitar 0.9883
403 acoustic guitar 0.00772
715 pick, plectrum, plectron 0.001509
421 banjo 0.000926
820 stage 0.0006595
*******************************************************************************
Finished

V1のAPIでもV2のAPIでも正しく動作し、electric guitarの出力が得られた。

. Movidiusなし
[10.55, 2.15, 2.16, 2.10, 3.90, 2.12, n/a, n/a, n/a, n/a] (10回実施、単位は秒)
RAM使用率 90%程度
CPU使用率 90%程度
predictを実行するとdmesgにUnder-voltage detected!と表示されることがある。表示された場合にはpredictにかかる時間が長くなる。
複数回続けて実行すると電力不足のためかシステムが落ちる。(今回の結果では7回目以降)


2. Movidiusあり
[0.62, 0.59, 0.60, 0.61, 0.61, 0.61, 0.61, 0.61, 0.61, 0.61] (10回実施、単位は秒)
RAM使用率 10%程度
CPU使用率  5%程度

任意のTensorFlowモデルのコンパイル (自作モデルのコンパイル)

学習

自作のTensorFlowモデルをMovidius用にコンパイルするためには、ソースコードの編集を行いMovidius用にする必要がある。
1. 入力のPlaceholderに名前を付ける 2. tf.train.Saver()を使って学習済みネットワークを保存する

これによって、

****.index
****.data-00000-of-00001
****.meta

の3つのファイルが生成される。

コンパイル可能なファイルに変換

次に、生成された学習済みモデルをもう一度開いてMovidiusでコンパイルが可能なモデルを出力する。 修正点は次の通り 1. 出力の活性化関数に名前を付ける 2. 入力以外のPlaceholderをすべて削除する 3. dropoutを削除する 4. 学習用データの読み込みを削除する 5. loss, training, accuracyなど学習用のコードを削除する

こられの変更をしたコードで、保存したモデルをrestoreで開き、またsaveする。
これによって、

****_inference.index
****_inference.data-00000-of-00001
****_inference.meta

の3つのファイルが生成される。

コンパイル

mvNCCompileコマンドを使って保存した****.metaファイルをgraphファイルに変換する。

mvNCCompile ****_inference.meta -s 12 -in input -on output -o ****_inference.graph

この時、inputoutputにはそれぞれ指定したinput nodeとoutput nodeの名前を入れる。

実行結果

TensorflowのExampleにあるdeep_mnist.pyを試した。

tokunn@tokunn-VirtualBox 11:31:44 [~/Documents/MovidiusTensorflow/mnist] $ mvNCCompile mnist_inference.meta -s 12 -in input -on output -o mnist_inference.graph
mvNCCompile v02.00, Copyright @ Movidius Ltd 2016

/usr/local/lib/python3.5/dist-packages/tensorflow/python/framework/ops.py:766: DeprecationWarning: builtin type EagerTensor has no __module__ attribute
  EagerTensor = c_api.TFE_Py_InitEagerTensor(_EagerTensorBase)
/usr/local/lib/python3.5/dist-packages/tensorflow/python/util/tf_inspect.py:45: DeprecationWarning: inspect.getargspec() is deprecated, use inspect.signature() instead
  if d.decorator_argspec is not None), _inspect.getargspec(target))
Traceback (most recent call last):
  File "/usr/local/bin/mvNCCompile", line 118, in <module>
    create_graph(args.network, args.inputnode, args.outputnode, args.outfile, args.nshaves, args.inputsize, args.weights)
  File "/usr/local/bin/mvNCCompile", line 104, in create_graph
    net = parse_tensor(args, myriad_config)
  File "/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py", line 293, in parse_tensor
    if have_first_input(strip_tensor_id(node.outputs[0].name)):
IndexError: list index out of range

ファイル変換までは問題なく動くが、コンパイル時にmvNCCompile内でIndexErrorを起こして止まってしまう。 配列の要素を確認せずにアクセスしていることが原因。

ライブラリの書き換え

/usr/local/bin/ncsdk/Controllers/TensorFlowParser.pyを編集して、回避する。 やってよいのかどうかは不明。

<                 if have_first_input(strip_tensor_id(node.outputs[0].name)):
---
>                 # ******* EDIT ******
>                 print(len(node.outputs))
>                 if len(node.outputs) and have_first_input(strip_tensor_id(node.outputs[0].name)):

これによってmvNCCompileは通って、graphファイルも生成されるようになった。

tokunn@tokunn-VirtualBox 11:36:34 [~/Documents/MovidiusTensorflow/mnist] $ mvNCCompile mnist_inference.meta -s 12 -in input -on output -o mnist_inference.graph     
mvNCCompile v02.00, Copyright @ Movidius Ltd 2016

/usr/local/lib/python3.5/dist-packages/tensorflow/python/framework/ops.py:766: DeprecationWarning: builtin type EagerTensor has no __module__ attribute
  EagerTensor = c_api.TFE_Py_InitEagerTensor(_EagerTensorBase)
/usr/local/lib/python3.5/dist-packages/tensorflow/python/util/tf_inspect.py:45: DeprecationWarning: inspect.getargspec() is deprecated, use inspect.signature() instead
  if d.decorator_argspec is not None), _inspect.getargspec(target))
1
1
1
1
0
1
/usr/local/bin/ncsdk/Controllers/FileIO.py:52: UserWarning: You are using a large type. Consider reducing your data sizes for best performance
  "Consider reducing your data sizes for best performance\033[0m")

Movidiusで動作確認 (v1)

実際にどうさせてみる。 以下、特筆なしの場合、APIはv1を使用。

pi@raspberrypi:~/week2/workspace $ python3 prediction_byMovidius4mvncV1.py
Start prediting ...
2 2 8 2 2 2 2 2 2 2 2 2 8 2 2 2 2 2 2 0 2 2 2 2 2 2 8 8 2 2 0 2 2 2 2 2 2 2 2 2 2 0 2 2 2 2 2 2 2 2 2 0 8 2 2 2 2 2 2 2 0 2 2 2 2 2 2 8 8 0 2 0 2 2 2 2 2 8 2 0 2 2 2 2 2 2 2 2 0 0 2 2 8 2 2 2 2 2 2 0 8 2 2 2 2 2 2 2 2 2 2 2 2 0 2 2 8 2 2 2 2 2 0 2 2 2 2 8 2 2 2 2 2 2 2 2 2 2 2 0 2 2 2 2 8 2 2 2 0 2 2 2 8 8 2 2 2 2 2 2 2 2 2 2 2 2 8 2 2 2 8 2 2 2 2 8 2 2 2 2 2 2 2 8 2 0 2 2 2 0 2 2 8 2 2 2 0 2 0 8 2 2 2 2 2 2 2 8 2 2 2 8 2 8 2 2 2 2 2 2 8 2 2 2 8 2 2 8 8 2 2 2 0 2 2 2 0 0 2 2 2 2 2 8 2 0 2 2 8 2 2 8 2 2 2 2 2 0 2 2 8 2 2 2 2 2 2 0 8 0 2 2 0 2 0 8 0 2 2 2 2 2 2 0 8 2 2 0 2 2 2 2 2 0 2 2 2 2 2 2 2 0 2 0 2 2 2 8 2 2 2 2 2 2 2 0 2 2 0 2 2 8 2 2 8 2 2 0 2 0 2 0 2 2 2 0 2 0 2 2 8 2 2 2 0 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 8 2 2 2 8 2 2 2 2 0 2 2 2 8 2 2 0 2 2 2 2 2 0 2 2 2 2 2 2 0 8 2 2 2 8 2 0 2 2 2 2 2 2 2 0 2 2 2 2 2 2 2 2 2 2 8 2 2 2 2 8 2 2 2 2 2 2 0 0 2 2 2 2 2 0 8 2 2 2 2 2 2 2 2 2 2 2 8 2 2 2 2 8 2 8 2 2 0 2 2 2 2 2 2 2 0 2 2 2 2 0 2 2 2 2 8 2 8 2 2 2 2 2 2 2 2 8 2 2 2 2 8 2 2 2 2 2 2 2
Time : 4.403196096420288 (500 images)

正しくない結果が出力された。
1~9までの9枚で確認する。

pi@raspberrypi:~/week2/workspace $ vim prediction_byMovidius4mvncV1.py
pi@raspberrypi:~/week2/workspace $ python3 prediction_byMovidius4mvncV1.py
../../JPEGImages/pickup/00628.jpg
../../JPEGImages/pickup/00042.jpg
../../JPEGImages/pickup/00025.jpg
../../JPEGImages/pickup/00652.jpg
../../JPEGImages/pickup/00013.jpg
../../JPEGImages/pickup/00520.jpg
../../JPEGImages/pickup/00663.jpg
../../JPEGImages/pickup/00683.jpg
../../JPEGImages/pickup/00433.jpg
Start prediting ...
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]

Time : 0.11402153968811035 (9 images)

なぜか4と7が100%"8"だと認識されて、それ以外は100%"2"だと認識される。 逆順に入れてみる。

pi@raspberrypi:~/week2/workspace $ vim prediction_byMovidius4mvncV1.py
pi@raspberrypi:~/week2/workspace $ python3 prediction_byMovidius4mvncV1.py
../../JPEGImages/pickup/00433.jpg
../../JPEGImages/pickup/00683.jpg
../../JPEGImages/pickup/00663.jpg
../../JPEGImages/pickup/00520.jpg
../../JPEGImages/pickup/00013.jpg
../../JPEGImages/pickup/00652.jpg
../../JPEGImages/pickup/00025.jpg
../../JPEGImages/pickup/00042.jpg
../../JPEGImages/pickup/00628.jpg
Start prediting ...
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]

Time : 0.11403870582580566 (9 images)

やはり4と7が100%"8"だと認識されて、それ以外は100%"2"。

もう一度最初から

学習

deepmnist.pyをjupyter-notebookで実行。

saving graph to: /tmp/tmpc_vjn3sg
step 0, training accuracy 0.08
step 10, training accuracy 0.4
step 20, training accuracy 0.46
(略)
step 470, training accuracy 0.96
step 480, training accuracy 0.92
step 490, training accuracy 0.96
test accuracy 0.939

Test Accuracyは0.939で正しく学習できている。
出力ファイルが正しく保存されていることを確認。

tokunn@tokunn-VirtualBox 13:49:04 [~/Documents/MovidiusTensorflow/mnist0911] $ ls -1
MNIST_data/
checkpoint
conv4movidius.ipynb
deepmnist.ipynb
mnist_model.data-00000-of-00001
mnist_model.index
mnist_model.meta
output/

さらに、Movidius用に保存しなおし。

tokunn@tokunn-VirtualBox 13:51:43 [~/Documents/MovidiusTensorflow/mnist0911] $ ls -1
MNIST_data/
checkpoint
conv4movidius.ipynb
deepmnist.ipynb
mnist_inference.data-00000-of-00001
mnist_inference.index
mnist_inference.meta
mnist_model.data-00000-of-00001
mnist_model.index
mnist_model.meta
output/

コンパイルを実行。

mvNCCompile mnist_inference.meta -s 12 -in input -on output -o mnist_inference.graph

RPIでMovidiusでpredictしてみる。

pi@raspberrypi:~/week2/mnist0911 $ python3 prediction_byMovidius4mvncV1.py 
../../JPEGImages/pickup/00433.jpg
../../JPEGImages/pickup/00683.jpg
../../JPEGImages/pickup/00663.jpg
../../JPEGImages/pickup/00520.jpg
../../JPEGImages/pickup/00013.jpg
../../JPEGImages/pickup/00652.jpg
../../JPEGImages/pickup/00025.jpg
../../JPEGImages/pickup/00042.jpg
../../JPEGImages/pickup/00628.jpg
Start prediting ...
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]

Time : 0.1024773120880127 (10 images)

やはり変わらず。

RPI3じゃなくて実マシンでやってみる

Rspberry Piが悪いかもしれないから、実際のLinuxマシン(Ubuntu 16.04 - nanase)でやってみる。

tokunn@nanase 7:11:40 [~/Documents/week2/0911] $ python3 prediction_byMovidius4mvncV1.py
ERROR 1: libgrass_vector.7.4.0.so: cannot open shared object file: No such file or directory
ERROR 1: libgrass_vector.7.4.0.so: cannot open shared object file: No such file or directory
ERROR 1: libgrass_dgl.7.4.0.so: cannot open shared object file: No such file or directory
ERROR 1: libgrass_dgl.7.4.0.so: cannot open shared object file: No such file or directory
Image path : ./JPEGImages/*.jpg
Start prediting ...
[ 0.  0.  0.  0.  0.  1.  0.  0.  0.  0.]
[ 1.  0.  0.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  1.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  1.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  1.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  1.  0.  0.  0.  0.  0.  0.  0.]
[ 0.  0.  1.  0.  0.  0.  0.  0.  0.  0.]
[  3.35454941e-04   0.00000000e+00   0.00000000e+00   0.00000000e+00
   1.00000000e+00   0.00000000e+00   0.00000000e+00   0.00000000e+00
   0.00000000e+00   0.00000000e+00]
[ 0.  0.  0.  0.  0.  1.  0.  0.  0.  0.]
[ 0.  0.  1.  0.  0.  0.  0.  0.  0.  0.]

Time : 0.0796973705291748 (10 images)

やっぱり何かおかしい。
Rspberry Piだから起きている訳ではない。 おそらく問題はmvNCCompileで生成されたgraphファイルなのでは?

Tensorflowで保存したファイルが正しいかを確認する。

jupyter-notebook上で、mnist_modelからrestoreしてpredictさせてみる。

INFO:tensorflow:Restoring parameters from ./output/mnist_model
test accuracy 0.9477

問題なくpredictできている。

mvNCCompileに入れているファイルが正しいかどうかを確認する

これが正しく動いていれば、問題はmvNCCompile。 もしくは突っ込んだファイルがダメか。
jupyter-notebook上で、mnist_inferenceからrestoreしてpredictさせてみる。

INFO:tensorflow:Restoring parameters from ./output/mnist_inference
test accuracy 0.9477

こちらも問題なくpredictできている。 ---> 問題はmvNCCompile周辺

mvNCCheckでモデルの確認をしてみる

mvNCCheckを使って、モデルの確認を行う。

tokunn@nanase 8:20:05 [~/Documents/week2/0911/output] $ mvNCCheck mnist_inference.meta -s 12 -in input -on output -of mnist_inference.graph
mvNCCheck v02.00, Copyright @ Movidius Ltd 2016

USB: Transferring Data...
USB: Myriad Execution Finished
USB: Myriad Connection Closing.
USB: Myriad Connection Closed.
Result:  (1, 1, 10)
1) 8 0.69141
2) 2 0.10852
3) 4 0.075806
4) 3 0.047607
5) 5 0.028824
Expected:  (1, 10)
1) 8 0.685481
2) 2 0.111384
3) 4 0.078136
4) 3 0.0483064
5) 5 0.0283811
------------------------------------------------------------
 Obtained values 
------------------------------------------------------------
 Obtained Min Pixel Accuracy: 0.8644439280033112% (max allowed=2%), Pass
 Obtained Average Pixel Accuracy: 0.19093991722911596% (max allowed=1%), Pass
 Obtained Percentage of wrong values: 0.0% (max allowed=0%), Pass
 Obtained Pixel-wise L2 error: 0.325355674229958% (max allowed=1%), Pass
 Obtained Global Sum Difference: 0.013088561594486237
------------------------------------------------------------

Result(Movidiusの出力)とExpected(TensorFlowの出力)がほぼ同じであることから、正しく動作していると考えられる。
ということは、問題なのは推論に使っているNCAPIのほう?

mvNCCheckを使って認識してみる

mvNCCheckに-iオプションを付けることで自分で画像ファイルを指定して入力することができる。

tokunn@nanase 8:58:09 [~/Documents/week2/0911/output] $ mvNCCheck mnist_inference.meta -s 12 -in input -on output -of mnist_inference.graph -i ../JPEGImages/pickup/00013.jpg
mvNCCheck v02.00, Copyright @ Movidius Ltd 2016

Traceback (most recent call last):
  File "/usr/local/bin/mvNCCheck", line 152, in <module>
    quit_code = check_net(args.network, args.image, args.inputnode, args.outputnode, args.nshaves, args.inputsize, args.weights, args)
  File "/usr/local/bin/mvNCCheck", line 130, in check_net
    net = parse_tensor(args, myriad_config, file_gen=True)
  File "/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py", line 266, in parse_tensor
    int(shape[3]),
IndexError: list index out of range

ライブラリ内の/usr/local/bin/ncsdk/Controllers/TensorFlowParser.pyでまたIndexError: list index out of rangeが発生。
いい加減TensorFlowParserはリストにアクセスする前に要素数をチェックしていただきたい。
とりあえず204行目のdebug=Trueを有効化して、shapeを表示つするように追記。

204c204
<     # debug = True
---
>     debug = True
263a264
>             print("Input image shape", shape)

これを実行して、

tokunn@nanase 9:25:28 [~/Documents/week2/0911/output] $ mvNCCheck mnist_inference.meta -s 12 -in input -on output -of mnist_inference.graph -i ../JPEGImages/pickup/00013.jpg
mvNCCheck v02.00, Copyright @ Movidius Ltd 2016

Input image shape [1, 784]
Traceback (most recent call last):
  File "/usr/local/bin/mvNCCheck", line 152, in <module>
    quit_code = check_net(args.network, args.image, args.inputnode, args.outputnode, args.nshaves, args.inputsize, args.weights, args)
  File "/usr/local/bin/mvNCCheck", line 130, in check_net
    net = parse_tensor(args, myriad_config, file_gen=True)
  File "/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py", line 267, in parse_tensor
    int(shape[3]),
IndexError: list index out of range

の結果を得る。
Input shapeは[1, 784]であることがわかる。
-i [画像]のオプションなしで実行すると、

tokunn@nanase 9:26:30 [~/Documents/week2/0911/output] $ mvNCCheck mnist_inference.meta -s 12 -in input -on output -of mnist_inference.graph                                 
mvNCCheck v02.00, Copyright @ Movidius Ltd 2016

Input image shape [1, 784]
        0 Const Const
           OUT: Const:0
/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py line no.290
        1 VariableV2 Variable
           OUT: Variable:0
(略)
BiasAdd
        75 Softmax output
           IN: fc2/add:0
           OUT: output:0
Softmax
(略)
Result:  (1, 1, 10)
1) 8 0.32178
2) 4 0.22034
3) 2 0.14233
4) 0 0.10773
5) 3 0.1059
(略)

のように正しく実行される。
Input shapeは[1, 784]で、エラーが起きたときと同じである。
あれ、なんでこれCNNなのに1次元([1,784]の2次元)で入力してるんだこれ? [1,28,28]じゃないのかな
-> ちゃんと入力してからreshapeして28x28にしてた。
こちらとしては2次元([1, 784])で入力したいが、TensorFlowParserは最低でも4次元はないとお気に召さないらしい。
でも、内部で乱数生成しているときには何にも起きないのなんで?
とりあえず、問題のparse_img()の定義を探してみる。

tokunn@nanase 9:55:12 [/opt/movidius/NCSDK/ncsdk-x86_64/tk/Models] $ find / -type f 2>/dev/null | grep .py | xargs grep parse_img 2>/dev/null
/usr/local/bin/ncsdk/Controllers/MiscIO.py:def parse_img(path, new_size, raw_scale=1, mean=None, channel_swap=None):
/opt/movidius/NCSDK/ncsdk-armv7l/tk/Controllers/MiscIO.py:def parse_img(path, new_size, raw_scale=1, mean=None, channel_swap=None):
/opt/movidius/NCSDK/ncsdk-x86_64/tk/Controllers/MiscIO.py:def parse_img(path, new_size, raw_scale=1, mean=None, channel_swap=None):

/usr/local/bin/ncsdk/Controllers/MiscIO.pyにあるらしい。

# 定義
227 def parse_img(path, new_size, raw_scale=1, mean=None, channel_swap=None):
228     """
229     Parse an image with the Python Imaging Libary and convert to 4D numpy array
234     """

確かにあった。
ちなみに呼び出しは、

/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py

# 呼び出し
266:  input_data = parse_img(image,
267-                         [int(shape[0]),
268-                          int(shape[3]),
269-                          int(shape[1]),
270-                          int(shape[2])],
271-                         raw_scale=arguments.raw_scale,
272-                         mean=arguments.mean,
273-                         channel_swap=arguments.channel_swap)

となっている。
呼び出しの変数をどうしたらいいのか知りたいので、parse_img()を追いかける。 /usr/local/bin/ncsdk/Controllers/MiscIO.py

248     if path.split(".")[-1].lower() in ["png", "jpeg", "jpg", "bmp", "gif"]:
250         greyscale = True if new_size[2] == 1 else False

どうやらnew_size[2](配列の3番目)はカラーチャンネルを確かめているらしい。
というわけでint(shape[1])は今回は1となる。
第一引数のpath(呼び出し側ではimage)は画像ファイルへのパスを表している。

279     if (len(data.shape) == 2):
280         # Add axis for greyscale images (size 1)
281         data = data[:, :, np.newaxis]
282 
283     data = skimage.transform.resize(data, new_size[2:])
284     data = np.transpose(data, (2, 0, 1))
285     data = np.reshape(data, (1, data.shape[0], data.shape[1], data.shape[2]))
286 
287     data *= raw_scale

まず、grayscaleの場合にはshapeが(x, y)のみなので、1次元追加することで3次元としている。
これでshapeは(x, y, カラーチャンネル数)となる。

次にresizeの指定にnew_size[2]new_size[3]を使っている。
呼び出し側で、int(shape[1])int(shape[2])となるわけだが、int(shape[1])はグレースケールか否かを表しているのでは?
newsizeが使われているのはこの2か所のみ。
どういうこったい

次に、(x, y, カラーチャンネル数)を(カラーチャンネル数, x, y)になるように軸を入れ替えする。

ちなみに、正しく動作しているランダム値の時のinput_data(TensorFlowParser内)は、

259  input_data = np.random.uniform(0, 1, shape)

となっているから、最終的にはinput_dataのshapeは(1,784)のもとの形となるべき。

これらの情報から引数の予想を立てる。
まず、new_image以外に入るべき値はそのままでよい。
次に、new_imageに入れるべきshapeについてであるが、通常は画像は(枚数, x, y, カラーチャンネル数)のようになっているであろうと想像がつく。
これを呼び出し側では[枚数, カラーチャンネル数, x, y]の形にして入力している。
よって、入力は[1, 1, shape[0], shape[1]]となる。

/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py

268,271c268,271
<                                    [int(shape[0]),
<                                     int(shape[3]),
<                                     int(shape[1]),
<                                     int(shape[2])],
---
>                                    [int(1),
>                                     int(1),
>                                     int(shape[0]),
>                                     int(shape[1])],

ただし、このままでは

250         greyscale = True if new_size[2] == 1 else False

の時にxの値を見てgreyscaleを判断してしまう。
なので、new_size[2]ではなく、new_size[1]書き換える。誤植?

/usr/local/bin/ncsdk/Controllers/MiscIO.py

250c250
<         greyscale = True if new_size[2] == 1 else False
---
>         greyscale = True if new_size[1] == 1 else False

このパッチを当てて実行すると、

tokunn@nanase 11:16:22 [~/Documents/week2/0911/output] $ mvNCCheck mnist_inference.meta -s 12 -in input -on output -of mnist_inference.graph -i ../JPEGImages/pickup/00013.jpg
mvNCCheck v02.00, Copyright @ Movidius Ltd 2016

Input image shape [1, 784]
image path ../JPEGImages/pickup/00013.jpg
/usr/local/lib/python3.6/dist-packages/skimage/transform/_warps.py:84: UserWarning: The default mode, 'constant', will be changed to 'reflect' in skimage 0.15.
  warn("The default mode, 'constant', will be changed to 'reflect' in "
Traceback (most recent call last):
  File "/usr/local/bin/mvNCCheck", line 152, in <module>
    quit_code = check_net(args.network, args.image, args.inputnode, args.outputnode, args.nshaves, args.inputsize, args.weights, args)
  File "/usr/local/bin/mvNCCheck", line 130, in check_net
    net = parse_tensor(args, myriad_config, file_gen=True)
  File "/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py", line 274, in parse_tensor
    channel_swap=arguments.channel_swap)
  File "/usr/local/bin/ncsdk/Controllers/MiscIO.py", line 290, in parse_img
    data[0] = data[0][np.argsort(channel_swap), :, :]
IndexError: index 2 is out of bounds for axis 0 with size 1

と新しいエラーが出る。
なので、とりあえずパッチを当てる。

/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py

272,274c272,274
<                                    raw_scale=arguments.raw_scale,
<                                    mean=arguments.mean,
<                                    channel_swap=arguments.channel_swap)
---
>                                    raw_scale=1,
>                                    mean=None,
>                                    channel_swap=None)

これで実行すると、

tokunn@nanase 11:41:03 [~/Documents/week2/0911/output] $ mvNCCheck mnist_inference.meta -s 12 -in input -on output -of mnist_inference.graph -i ../JPEGImages/pickup/00013.jpg
mvNCCheck v02.00, Copyright @ Movidius Ltd 2016

input_data shape (1, 1, 1, 784)
Traceback (most recent call last):
  File "/usr/local/bin/mvNCCheck", line 152, in <module>
    quit_code = check_net(args.network, args.image, args.inputnode, args.outputnode, args.nshaves, args.inputsize, args.weights, args)
  File "/usr/local/bin/mvNCCheck", line 130, in check_net
    net = parse_tensor(args, myriad_config, file_gen=True)
  File "/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py", line 281, in parse_tensor
    res = outputTensor.eval(feed_dict={inputnode + ':0' : input_data})
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/ops.py", line 570, in eval
    return _eval_using_default_session(self, feed_dict, self.graph, session)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/ops.py", line 4455, in _eval_using_default_session
    return session.run(tensors, feed_dict)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/client/session.py", line 889, in run
    run_metadata_ptr)
  File "/usr/local/lib/python3.6/dist-packages/tensorflow/python/client/session.py", line 1096, in _run
    % (np_val.shape, subfeed_t.name, str(subfeed_t.get_shape())))
ValueError: Cannot feed value of shape (1, 1, 784, 1) for Tensor 'input:0', which has shape '(1, 784)'

とエラーが出る。
終わりがない。
これやる意味あるのか?
別にmvNCCheckで画像を与えなくてもよくない?
PythonでのAPI追いかけるべきでは?
mvNCCheckのランダムでgraphは正しいと思ったけど、そもそも出力されたグラフがおかしかったら、tensorflowでやったやつもおかしな結果を出力するのでは

DAY2

やっぱり4次元にしたところで入力は2次元なので入力できない。
さらに、雑なパッチを当てる。 これでほかの画像は入力できないし、ほかのネットワークにも対応できない。

/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py

204c204
<     debug = True
---
>     # debug = True
277a278
>             input_data = input_data.transpose([0, 1, 3, 2])[0][0]
279a281
>                 #print(input_data)
297d298
<                 print("/usr/local/bin/ncsdk/Controllers/TensorFlowParser.py line no.290")

実行してみる。

tokunn@nanase 23:49:44 [~/Documents/week2/0912/output] $ for i in $(ls ../JPEGImages/pickup/); do echo $i; mvNCCheck mnist_inference.meta -s 12 -in input -on output -of mnist_inference.graph -i ../JPEGImages/pickup/$i 2>/dev/null; done
1.jpg

Result:  (1, 1, 10)
1) 2 0.28613
Expected:  (1, 10)
1) 2 0.281931
------------------------------------------------------------
2.jpg

Result:  (1, 1, 10)
1) 2 0.81348
Expected:  (1, 10)
1) 2 0.81391
------------------------------------------------------------
3.jpg

Result:  (1, 1, 10)
1) 2 0.87158
Expected:  (1, 10)
1) 2 0.870968
------------------------------------------------------------
4.jpg

Result:  (1, 1, 10)
1) 3 0.39771
Expected:  (1, 10)
1) 3 0.40986
------------------------------------------------------------
5.jpg

Result:  (1, 1, 10)
1) 2 0.70898
Expected:  (1, 10)
1) 2 0.712083
------------------------------------------------------------
6.jpg

Result:  (1, 1, 10)
1) 5 0.77686
Expected:  (1, 10)
1) 5 0.773194
------------------------------------------------------------
7.jpg

Result:  (1, 1, 10)
1) 5 0.49268
Expected:  (1, 10)
1) 5 0.499106
------------------------------------------------------------
8.jpg

Result:  (1, 1, 10)
1) 2 0.95605
Expected:  (1, 10)
1) 2 0.955554
------------------------------------------------------------
9.jpg

Result:  (1, 1, 10)
1) 2 0.50293
Expected:  (1, 10)
1) 2 0.494139
------------------------------------------------------------

やっぱりほとんどの数字が2だと出力される。
ただし、mvNCCheckからの場合には確率は100%ではない。

PythonAPIから呼び出しても、mvNCCheckから呼び出しても同じ結果が返ってくる。
ということは変換したgraphファイルがおかしいということがわかる。

mvNCCompileに渡したファイルか、mvNCCompileのどちらかが間違えている。

deep_mnistのサンプルをgithub上のコードでやってみる

deep_mnistのサンプルがgithub上にあった。 https://github.com/ashwinvijayakumar/ncappzoo/tree/mnist/tensorflow/mnist

動く。
mvNCCompileのパッチを元に戻しても大丈夫。
自分で用意したgraphとJPEGにしても動く。
あれ
これは、私がAPIを正しく使えていないのでは? (もちろんmvNCCheck -i のほうはIndexError)
ただ、Intelのサンプルは確実に間違えていた。

原因

  1. 黒地に白字で学習させてたのに、推論の時に白地に黒字の画像でやっていた
  2. 画像を読み込んだ後に255で割るのを忘れていた
  3. mvNCCheckに画像を読み込ませる機能は動かない

ずっと、おかしな出力だと思っていたものは正しい出力であった。

結論 (動作した方法・ソースコード)

3つの問題を解決することができた。
1. TensorFlowのモデルをmvNCCompileでコンパイルできない
-> TensorFlowParser.pyの書き換え
2. mvNCCheckに-iオプションで画像を読み込ませることができるらしいが、動かない
-> TensorFlowParser.pyとMiscIO.pyをモデルに合わせて書き換え(汎用性なし)
3. 推論の結果がおかしい
-> 入力がおかしい

mvNCCompileの書き換え

/usr/local/bin/ncsdk/Controllers/TensorFlowParser.pyの290行目付近を編集して、配列の要素を確認してからアクセスるようにする。

<                 if have_first_input(strip_tensor_id(node.outputs[0].name)):
---
>                 if len(node.outputs) and have_first_input(strip_tensor_id(node.outputs[0].name)):

これで、mvNCCompileは通るようになる。

mvNCCheckの書き換え

/usr/local/bin/ncsdk/Controllers/MiscIO.pyの250行目付近を編集。

250c250
<         greyscale = True if new_size[2] == 1 else False
---
>         greyscale = True if new_size[1] == 1 else False

/usr/local/bin/ncsdk/Controllers/TensorFlowParser.pyの270行目付近を編集。

265,271c265,271
<                                    [int(shape[0]),
<                                     int(shape[3]),
<                                     int(shape[1]),
<                                     int(shape[2])],
<                                    raw_scale=arguments.raw_scale,
<                                    mean=arguments.mean,
<                                    channel_swap=arguments.channel_swap)
---
>                                    [int(1),
>                                     int(1),
>                                     int(shape[0]),
>                                     int(shape[1])],
>                                    raw_scale=1,
>                                    mean=None,
>                                    channel_swap=None)
272a273
>             input_data = input_data.transpose([0, 1, 3, 2])[0][0]

これで

mvNCCheck mnist_inference.meta -s 12 -in input -on output -of mnist_inference.graph -i ../github_deep_mnist/ncappzoo/data/digit_images/one.png  -S 255

で動き出す。ただし結果は?。入力する画像を加工すべき?

メインプログラム(学習用)

from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
import tempfile
def deepnn(x):
    with tf.name_scope('reshape'):
        x_image = tf.reshape(x, [-1, 28, 28, 1]) # -1 = number of x
        
    with tf.name_scope('conv1'):
        W_conv1 = weight_variable([5, 5, 1, 32])
        b_conv1 = bias_variable([32])
        h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
        
    with tf.name_scope('pool1'):
        h_pool1 = max_pool_2x2(h_conv1)
        
    with tf.name_scope('conv2'):
        W_conv2 = weight_variable([5, 5, 32, 64])
        b_conv2= bias_variable([64])
        h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
        
    with tf.name_scope('pool1'):
        h_pool2 = max_pool_2x2(h_conv2)
        
    with tf.name_scope('fc1'):
        W_fc1 = weight_variable([7 * 7 * 64, 1024])
        b_fc1 = bias_variable([1024])
        
        h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
        h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
        
    with tf.name_scope('dropout'):
        keep_prob = tf.placeholder(tf.float32)
        h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)
        
    with tf.name_scope('fc2'):
        W_fc2 = weight_variable([1024, 10])
        b_fc2 = bias_variable([10])
        
        y_conv = tf.matmul(h_fc1_drop, W_fc2) + b_fc2
    
    return y_conv, keep_prob
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')
def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')
def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1) # random
    return tf.Variable(initial)
def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape) # all 0.1
    return tf.Variable(initial)
def main():
    mnist = input_data.read_data_sets('MNIST_data', one_hot=True)
    
    x = tf.placeholder(tf.float32, [None, 784], name='input')
    y_ = tf.placeholder(tf.float32, [None, 10])
    
    y_conv, keep_prob = deepnn(x)
    
    with tf.name_scope('loss'):
        cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y_conv)
    cross_entropy = tf.reduce_mean(cross_entropy)
    
    with tf.name_scope('adam_optimizer'):
        train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)
    
    with tf.name_scope('accuracy'):
        correct_prediction = tf.equal(tf.argmax(y_conv, 1), tf.argmax(y_, 1))
        correct_prediction = tf.cast(correct_prediction, tf.float32)
    accuracy = tf.reduce_mean(correct_prediction)
    
    graph_location = tempfile.mkdtemp()
    print('saving graph to: %s' % graph_location)
    train_writer = tf.summary.FileWriter(graph_location)
    train_writer.add_graph(tf.get_default_graph())
    
    saver = tf.train.Saver()
    
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        for i in range(500):
            batch = mnist.train.next_batch(50)
            if i % 10 == 0:
                train_accuracy = accuracy.eval(feed_dict={x: batch[0], y_: batch[1], keep_prob: 1.0})
                print('step %d, training accuracy %g' % (i, train_accuracy))
            train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5})
        
        print('test accuracy %g' % accuracy.eval(feed_dict={x: mnist.test.images, y_: mnist.test.labels, keep_prob: 1.0}))
        
        graph_location = "."
        save_path = saver.save(sess, graph_location + "/mnist_model")
main()

モデル変換用

from tensorflow.examples.tutorials.mnist import input_data
import tensorflow as tf
import tempfile
def deepnn(x):
    with tf.name_scope('reshape'):
        x_image = tf.reshape(x, [-1, 28, 28, 1]) # -1 = number of x
        
    with tf.name_scope('conv1'):
        W_conv1 = weight_variable([5, 5, 1, 32])
        b_conv1 = bias_variable([32])
        h_conv1 = tf.nn.relu(conv2d(x_image, W_conv1) + b_conv1)
        
    with tf.name_scope('pool1'):
        h_pool1 = max_pool_2x2(h_conv1)
        
    with tf.name_scope('conv2'):
        W_conv2 = weight_variable([5, 5, 32, 64])
        b_conv2= bias_variable([64])
        h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
        
    with tf.name_scope('pool1'):
        h_pool2 = max_pool_2x2(h_conv2)
        
    with tf.name_scope('fc1'):
        W_fc1 = weight_variable([7 * 7 * 64, 1024])
        b_fc1 = bias_variable([1024])
        
        h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])
        h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)
        
    with tf.name_scope('fc2'):
        W_fc2 = weight_variable([1024, 10])
        b_fc2 = bias_variable([10])
        
        y_conv = tf.matmul(h_fc1, W_fc2) + b_fc2
    
    return y_conv
def conv2d(x, W):
    return tf.nn.conv2d(x, W, strides=[1,1,1,1], padding='SAME')
def max_pool_2x2(x):
    return tf.nn.max_pool(x, ksize=[1,2,2,1], strides=[1,2,2,1], padding='SAME')
def weight_variable(shape):
    initial = tf.truncated_normal(shape, stddev=0.1) # random
    return tf.Variable(initial)
def bias_variable(shape):
    initial = tf.constant(0.1, shape=shape) # all 0.1
    return tf.Variable(initial)
def main():
    x = tf.placeholder(tf.float32, [None, 784], name='input')

    y_conv = deepnn(x)
    output = tf.nn.softmax(y_conv, name='output')
    
    saver = tf.train.Saver()
    
    with tf.Session() as sess:
        sess.run(tf.global_variables_initializer())
        sess.run(tf.local_variables_initializer())
        
        saver.restore(sess, '.' + '/output/mnist_model')
        saver.save(sess, '.' + '/output/mnist_inference')
main()

コンパイル

mvNCCompile mnist_inference.meta -s 12 -in input -on output -o mnist_inference.graph

もしinputとoutputのノードの名前がinputとoutpuであれば、-inと-onいらないかもしれない。

Movidiusでの推論

import mvnc.mvncapi as mvnc
import numpy as np
from PIL import Image
import cv2
import time, sys, os

import glob

IMAGE_DIR_NAME = './JPEGImages/pickup/'
#IMAGE_DIR_NAME = 'github_deep_mnist/ncappzoo/data/digit_images'

def predict(input):
    print("Start prediting ...")
    devices = mvnc.EnumerateDevices()
    device = mvnc.Device(devices[0])
    device.OpenDevice()

    # Load graph file data
    with open('./output/mnist_inference.graph', 'rb') as f:
        graph_file_buffer = f.read()

    # Initialize a Graph object
    graph = device.AllocateGraph(graph_file_buffer)

    start = time.time()
    for i in range(len(input)):
        # Write the tensor to the input_fifo and queue an inference
        graph.LoadTensor(input[i], None)
        output, userobj = graph.GetResult()
        print(np.argmax(output), end=' ')
    stop = time.time()
    print('')
    print("Time : {0} ({1} images)".format(stop-start, len(input)))

    graph.DeallocateGraph()
    device.CloseDevice()

    return output

if __name__ == '__main__':
    print("Image path : {0}".format(os.path.join(IMAGE_DIR_NAME, '*.jpg')))
    jpg_list = glob.glob(os.path.join(IMAGE_DIR_NAME, '*.jpg'))
    if not len(jpg_list):
        print("No image file")
        sys.exit()
    jpg_list.reverse()
    print([i.split('/')[-1] for i in jpg_list])
    img_list = []
    for n in jpg_list:
        image = cv2.imread(n)
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        image = cv2.bitwise_not(image)
        iamge = cv2.resize(image, (28, 28))
        img_list.append(image)
    img_list = np.asarray(img_list)[:10] * (1.0/255.0)
    print("imgshape ", img_list.shape)
    predict(img_list.astype(np.float16))