Keras→NNVMを試す



NNVMがKerasの学習済みモデルのインポートに対応したとのことなので、試してみました。
実験用のコードは短いので全て記事中に記載していますが、[tkat0/keras-nnvm-example](https://github.com/tkat0/keras-nnvm-example)にも公開しています。

開発者のTianqi Chen氏のツイート。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">thanks to yuwei hu, NNVM compiler now deploys keras models directly <a href="https://t.co/aTS96roOYN">https://t.co/aTS96roOYN</a> to the hardware backends</p>&mdash; Tianqi Chen (@tqchenml) <a href="https://twitter.com/tqchenml/status/944404594356113408?ref_src=twsrc%5Etfw">2017年12月23日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

- nnvm.frontend.from_mxnet
- nnvm.frontend.from_onnx
- nnvm.frontend.from_coreml
- nnvm.frontend.from_keras ★ 今回これが追加

では早速動かしてみます！ 環境構築は最後に記載しています。

## NNVMのKeras向けチュートリアル

NNVM公式のチュートリアルに、Kerasからのモデルのインポートに関する記事が追加されました。Kerasの学習済みモデル（ImageNetで学習済みのResnet-50）で画像認識をするデモです。なるほど簡単そう。

http://nnvm.tvmlang.org/tutorials/from_keras.html#sphx-glr-tutorials-from-keras-py

## Kerasの学習済モデルをNNVMで動かしてみよう

KerasはImageNetの画像認識タスクを学習したモデルが簡単に使えます。以下の図は公式ドキュメントからの転載ですが、これらのモデルに対応しています（最新のmasterブランチだと、さらにNASNetも追加されていました）

https://keras.io/applications/

![image.png](https://qiita-image-store.s3.amazonaws.com/0/98406/e850b470-8f0b-e716-6862-2c2735313cf3.png)

MobileNetを動かしたかったのですが、今の[`nnvm.frontend.from_keras`はMobileNetが含むReLU6を未サポート](https://github.com/dmlc/nnvm/blob/65578d6a11ee3db538bb63f44a9e795c1623fe11/python/nnvm/frontend/keras.py#L45)でしたので、Resnet-50を動かしてみます。

モデル定義から実行までの全てのコードは以下になります。短い。

```py3
from time import time
from PIL import Image
import nnvm
import tvm
from tvm.contrib import graph_runtime
import keras
import numpy as np

from keras.applications.resnet50 import ResNet50, preprocess_input

USE_GPU = True # True: GPU, False: CPU

# Kerasの学習済みモデルのロード
model = ResNet50(include_top=True, weights='imagenet', input_tensor=None, input_shape=None)

# 入力画像の前処理
img = Image.open('cat.png').resize((224, 224))
data = np.array(img)[np.newaxis, :].astype('float32')
data = preprocess_input(data).transpose([0, 3, 1, 2]) # NHWC -> NCHW
print('data', data.shape)

# KerasのモデルをNNVMで読み込む
sym, params = nnvm.frontend.from_keras(model)

# モデルのコンパイル
if USE_GPU:
    target = 'opencl'
else:
    target = 'llvm'
shape_dict = {'data': data.shape}
with nnvm.compiler.build_config(opt_level=3):
    graph, lib, params = nnvm.compiler.build(sym, target, shape_dict, params=params)

ctx = tvm.context(target, 0)
m = graph_runtime.create(graph, lib, ctx)

# 入力データのセット
m.set_input('data', tvm.nd.array(data.astype('float32')))
m.set_input(**params)
m.run() # 推論実行

# 計算結果を取得し、推論結果を確認
out_shape = (1000,)
tvm_out = m.get_output(0, tvm.nd.empty(out_shape, 'float32')).asnumpy()
top1_tvm = np.argmax(tvm_out)

with open('imagenet1000_clsid_to_human.txt') as f:
    synset = eval(f.read())
print('NNVM top-1 id: {}, class name: {}'.format(top1_tvm, synset[top1_tvm]))

# Kerasでも同じ入力に対して推論し、結果を出力
keras_out = model.predict(data.transpose([0, 2, 3, 1]))
top1_keras = np.argmax(keras_out)
print('Keras top-1 id: {}, class name: {}'.format(top1_keras, synset[top1_keras]))

# Benchmark of NNVM
num_iter = 100
ftimer = m.module.time_evaluator("run", ctx, num_iter)
prof_res = ftimer()
print(prof_res)

# Benchmark of Keras
x = data.transpose([0, 2, 3, 1])
start = time()
for i in range(0, num_iter):
    model.predict(x)
end = time()
print((end-start)/num_iter)
```

事前にテスト用の猫画像と、ImageNetのラベルをダウンロードして、

```bash
$ wget https://github.com/dmlc/mxnet.js/blob/master/data/cat.png?raw=true -O cat.png
$ wget https://gist.githubusercontent.com/yrevar/942d3a0ac09ec9e5eb3a/raw/c2c91c8e767d04621020c30ed31192724b863041/imagenet1000_clsid_to_human.txt -O imagenet1000_clsid_to_human.txt
```

スクリプトを実行すると、自動で学習済みモデルのダウンロードが始まり…

```bash
$ python keras-nnvm-example.py
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.2/resnet50_weights_tf_dim_ordering_tf_kernels.h5
  1155072/102853048 [..............................] - ETA: 6:10
```

計算が完了し、結果が標準出力されました。Kerasの結果とも一致していますね。

```bash
NNVM top-1 id: 282, class name: tiger cat
Keras top-1 id: 282, class name: tiger cat
```

100回forwardした平均時間の計測結果は以下のようになりました。
NNVM(GPU) > Keras(CPU) > NNVM(CPU)
最適化次第でもっと高速化できるのだろうか。

| フレームワーク 	| ターゲット 	| 実行時間[sec] 	|
|:--------------:	|:----------:	|:-------------:	|
|      Keras     	|     CPU    	|            0.38 	|
|    NNVM/TVM    	|     CPU    	|          2.09 	|
|    NNVM/TVM    	|     GPU    	|          0.16 	|


## まとめ

NNVMがKerasからのインポートに対応したとのことで、早速試してみました。

先日は Chainer → ONNX → NNVM （そしてAndroidで動かす） という[記事を書いた](https://qiita.com/tkat0/items/28d1cc3b5c2831d86663)のですが、レイヤーによってはONNX → NNVMの制限があったりして難航しました。フレームワーク間のモデル変換はめんどいですね…
今回試したKerasは直接NNVMへ変換できるのでハマりどころが少なくて良さそうです。Kerasで学習してNNVMで各種デバイスデプロイするのは簡単で良いなあ。

## その他

ハマりどころや環境構築など。

### nnvm.frontend.from_kerasで`NameError: name 'unicode' is not defined`

いきなりモデルのインポートに失敗して泣きそうになった。

どうやら[nnvm/python/nnvm/frontend/keras.py#L20](https://github.com/dmlc/nnvm/blob/65578d6a11ee3db538bb63f44a9e795c1623fe11/python/nnvm/frontend/keras.py#L20)でstrかunicodeか見ているようで…`unicode`型はPython3では削除されてるので参照エラーとなったわけです。NNVMはPython2推奨ってこと？えー…

```py
def _convert_activation(insym, keras_layer, _):
    if isinstance(keras_layer, (str, unicode)):
        act_type = keras_layer
    else:
```

今日は祝日だし何もかもがめんどくさかったので、以下のように暫定対応したら動きました。

```py
unicode=str # work around

def _convert_activation(insym, keras_layer, _):
    if isinstance(keras_layer, (str, unicode)):
        act_type = keras_layer
    else:
```

### 環境構築

最後に、私の環境でのインストール手順を記載します。

- MacBook Pro (13-inch, 2017)
    - macOS Sierra 10.12.6
    - Python 3.6.2 (anaconda3-5.0.0)
        - NNVM/TVM 0aa9541
        - TensorFlow 1.4.0
        - Keras 2.1.2

#### NNVM/TVM

公式のインストール手順は、NNVMは[ここ](http://nnvm.tvmlang.org/how_to/install.html
)。TVMは[ここ](http://docs.tvmlang.org/how_to/install.html
)。


NNVMのリビジョンは、[0aa9541](https://github.com/dmlc/nnvm/tree/0aa95413c3394452d2abb30a100639e193516552)を使いました。

```bash
$ git clone --recursive https://github.com/dmlc/nnvm.git
$ pushd nnvm/tvm
$ vim make/config.mk # USE_OPENCL = 1, LLVM_CONFIG = llvm-configをコメントアウト
$ make -j8
$ cd python; python setup.py build; cd ../
$ cd topi/python; python setup.py build; cd ../
$ popd
$ pushd nnvm
$ make -j8
$ cd python; python setup.py build; cd ../
```

私はデバッグ目的でnnvm/tvmを修正したいので、`python setup.py install`はせず`build`のみしています。

nnvm/tvmのPythonモジュールのパスを通すために、以下をbashrcに追記して`source ~/.bashrc`

```bash
NNVM_ROOT=/path/to/nnvm
export PYTHONPATH=$NNVM_ROOT/python:${PYTHONPATH}
export PYTHONPATH=$NNVM_ROOT/tvm/python:${PYTHONPATH}
export PYTHONPATH=$NNVM_ROOT/tvm/topi/python:${PYTHONPATH}
```

インストールできたことを確認

```bash
$ python
>>> import nnvm
>>> nnvm.__version__
'0.8.0'
>>> import tvm
>>> tvm.__version__
'0.1.0'
>>> nnvm.__file__
'/path/to/nnvm/python/nnvm/__init__.py'
>>> tvm.__file__
'/path/to/nnvm/tvm/python/tvm/__init__.py'
```

#### Tensorflow

公式のインストール手順は[ここ](https://www.tensorflow.org/install/install_mac)。

```bash
$ pip install tensorflow
$ python
>>> import tensorflow
>>> tensorflow.__version__
'1.4.0'
```

私の環境では、インストール後にpythonで`import tensorflow`したときに`KeyError: "Couldn't find field google.protobuf.EnumDescriptorProto.EnumReservedRange.start"`というエラーが出たため、StackOverFlowの[この記事](https://stackoverflow.com/questions/47373706/enumdescriptorproto-error-while-importing-tensorflow)を参考に、protobufをアップグレードした。

#### Keras

公式のインストール手順は[ここ](https://keras.io/ja/#_2)

```bash
$ pip insall keras
$ python
>>> import keras
>>> keras.__version__
'2.1.2'
```