Movidius NCS + Raspberrypi で自作DNNモデルを動かす

# 概要
エッジでAIが流行っている昨今、`ラズパイでdeeplearning`というフレーズに踊らされ、tensorflowもcaffeも触ったことないのに勢いで買ってしまったMovidius NCSで、ようやく自作モデルを動かせたのでまとめておきます。

## Movidius™ Neural Compute Stickとは

- https://developer.movidius.com/
 - DNNの推論をやってくれるUSB型VPU
 - TensorFlow、Caffeのモデルを動かせる
 - **Raspberrypiでも動く**
 - 1万円くらい

## 使い方
ざっくりとこんな感じで動かします。

- PC
 1. tensorflowでモデル作成
 2. SDKでモデルをNCS用にコンパイル
- ラズパイ（+NCS）
 1. APIを叩いて、コンパイル済みモデルを読み込み、推論

## コード
githubに置いておきます。
https://github.com/watanabeyyy/NCS_test

- train.py
  - tensorflowで学習させたスクリプトです。
  - MNISTベースで作ってますが、MS Pゴシックの数字を学習させてます。
- create_pred_model.py
  - 学習済みモデルを、SDKでコンパイルできるように変換するスクリプトです。
  - 学習した重みを読み込み、推論用モデルとして新たに保存します。
- prediction.py
  - NCS用にコンパイルされたモデルを読み込み、推論を行うスクリプトです。

# 導入
環境は

- Ubuntu PC (VirtualBox on Windows)
- Raspberry Pi 3 Model B

## PC

###1. SDKをインストール  
Ubuntu 16.04にしか入らないみたいです。  

```bash
$ git clone http://github.com/Movidius/ncsdk
$ cd ncsdk
$ make install
```

###2. 自作モデルをコンパイル
ここでだいぶ苦戦しました。サンプル動かす以上の情報がなかなか得られず・・・
とりあえず、サンプル通りmvNCCompileコマンドにtensorflowのmetaファイル渡してやって、このエラー

```bash
$ mvNCCompile -s 12 model.meta -in=input -on=softmax
.....
[Error 34] Setup Error: Values for input contain placeholder. Pass an absolute value.
```
tensorflowでinputがplaceholderなのは当たり前だろと思って、別の要因を探していたのですが、改めて難解なサンプルをよくよく見てみると

```python:inception_v1.py
import numpy as np
import tensorflow as tf

from tensorflow.contrib.slim.nets import inception

slim = tf.contrib.slim

def run(name, image_size, num_classes):
  with tf.Graph().as_default():
        image = tf.placeholder("float", [1, image_size, image_size, 3], name="input")
        with slim.arg_scope(inception.inception_v1_arg_scope()):
        logits, _ = inception.inception_v1(image, num_classes, is_training=False, spatial_squeeze=False)
    probabilities = tf.nn.softmax(logits)
    init_fn = slim.assign_from_checkpoint_fn('inception_v1.ckpt', slim.get_model_variables('InceptionV1'))

    with tf.Session() as sess:
        init_fn(sess)
        saver = tf.train.Saver(tf.global_variables())
        saver.save(sess, "output/"+name)

run('inception-v1', 224, 1001)
```

- inputのtensor.shapeでNoneは使わず定数を指定してる（入力のテンソルサイズは固定）
- 学習済みモデルのckptファイルを読み込んで、推論用のsessを新たにsaveしてる  
ていうのが大事そうです。
（確かにhttps://movidius.github.io/ncsdk/TensorFlow.html に、*Save Session with Graph and Checkpoint Information*と記述があります）

ということで改めて
#### 2.1 推論用モデルを作成
推論用のモデルを保存します。

```py:create_pred_model.py
import tensorflow as tf

def predict(x):
    #
    #計算グラフを書く
    #
    y = tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2,name="output")

    return y

def run(name):
  with tf.Graph().as_default():
    input = tf.placeholder("float", [1, 108, 108, 1], name="input")
    output  = predict(input)

    with tf.Session() as sess:
        saver = tf.train.Saver(tf.global_variables())
        last_model = "./model/cnn-model-99"
        saver.restore(sess, last_model)
        saver.save(sess, name)

run('./model/pred_model')
```
入出力にnameを明示しておくのが無難です。
`pred_model.meta`としてモデルが保存されます。

#### 2.2 推論用モデルをコンパイル
`pred_model.meta`に対してコマンドを叩けば、コンパイルが通りました。

```bash
$ mvNCCompile -s 12 pred_model.meta -in=input -on=output
```
NCS用モデルが`graph`として保存されます。

## ラズパイ

### 1. APIをインストール  
Debian 9 Stretchにしか入らないみたいです。

```bash
$ git clone http://github.com/Movidius/ncsdk
$ cd ncsdk
$ make install
```

### 2. 推論
https://github.com/movidius/ncappzoo/tree/master/apps/image-classifier を参考にこんな感じで作成。

```py:prediction.py
import mvnc.mvncapi as mvnc
import numpy as np


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

    with open('./model/graph', 'rb') as f:
        blob = f.read()
    graph = device.AllocateGraph(blob)

    for i in range(4):
        graph.LoadTensor(input[i], 'user object')
        output, userobj = graph.GetResult()
        print(np.argmax(output))

    graph.DeallocateGraph()
    device.CloseDevice()

    return output


if __name__ == "__main__":
    input = np.load("test_data.npy")
    predict(input.astype(np.float16))
```
- LoadTensor()にはfloat16で渡さないとダメっぽいです

### 3. 動作確認
ラズパイにNCSを挿して、スクリプトを実行してみます。
テストデータとして使ったのは下の4枚の画像です。
![Figure_1.png](https://qiita-image-store.s3.amazonaws.com/0/207059/df2cffaf-e0fe-7b06-dda8-665e688ce0ab.png)

```bash
$ python prediction.py
7
1
9
5
```
うまく動いているみたいです。
NCSの性能については追々評価したいと思います。


# まとめ
- raspi+NCS+webcamの格安画像認識システム、ありかも
- tensorflow、意外と楽しい