NNablaのC++APIを試す


SONY製ディープラーニングフレームワークである[NNabla](https://nnabla.org/ja/)を使い、pythonで学習してC++から推論を行うところまでの手順をやってみます。

NNablaの特徴の一つは、コアがピュアC++であることだと思います。学習はGPUの載ったマシンで行い、学習後のネットを組み込み機器などにexportするときにC++にサクッと移行できるのは利点じゃないかと思います。

C++ APIについてはまだドキュメントがありませんが、ver 0.9.4からexamples/cppというところに使用例が追加されました。推論だけならこれを見よう見まねすれば使えそうです。

お題はいつものように、[関数近似で学ぶ chainer とディープラーニング](http://qiita.com/ashitani/items/1dc0a54da218ec224ad8)とします。exp(x)という関数を三層のMLPで推論するものです。

すべてのコードは[こちら](https://github.com/ashitani/NNabla_exp)に載せました。

# Pythonでの学習

まず基本ライブラリをimportします。

```py
import nnabla as nn
import nnabla.functions as F
import nnabla.parametric_functions as PF
import nnabla.solvers as S
```

グラフ生成はほとんどchainerと同じですね。パラメータを持たない関数はF,パラメータを持つ関数はPFに定義されています。

```py
x = nn.Variable((batch_size,1))
h1 = F.elu(PF.affine(x, 16,name="affine1"))
h2 = F.elu(PF.affine(h1, 32,name="affine2"))
y = F.elu(PF.affine(h2, 1,name="affine3"))
```
PFについては、scopeを定義してもよいし、name引数を指定してもよいですが、指定しないと名前空間が被ってエラーになりますので注意しましょう。leaky_reluがなかったので今回はeluで代用しました。

次にロス関数を定義します。とくに迷うことはないです。

```
t = nn.Variable((batch_size,1))
loss = F.mean(F.squared_error(y, t))
```

solverの定義です。何も考えずにAdamで。

```
solver = S.Adam()
solver.set_parameters(nn.get_parameters())
```

学習ループを回します。forward(), zero_grad(), backward(), update()という流れで、chainerに慣れていればまったく違和感のないワークフローです。データ供給用の関数もあるようですが今回は使っていません。

```py
losses=[]
for i in range(10000):
    dat=get_batch(batch_size)
    x.d=dat[0].reshape((batch_size,1))
    t.d=dat[1].reshape((batch_size,1))
    loss.forward()
    solver.zero_grad()
    loss.backward()
    solver.update()
    losses.append(loss.d.copy())
    if i % 1000 == 0:
        print(i, loss.d)
```

ロスをプロットしました。

![train.png](https://qiita-image-store.s3.amazonaws.com/0/30340/f80d5b3d-ddcb-d9a3-caa6-a83343b3df2f.png "train.png")



ノードのデータにアクセスするときは.dか.dataを使います。グラフ内の任意のノードに対してforward()ができます。これをつかって推論を行います。

```py
x.d= 0.2
y.forward()
print(y.d[0][0])
```

バッチサイズ100で組まれたネットにスカラーを入れても通るのが一見不思議ですが、どうやらスカラを入れると100個分の同じ値を含むデータに置き換えてくれるようです。なので出力も100個分同じ値が出てきます。


学習済パラメータはsave_parameters()で保存できます。

```py
nn.save_parameters("exp_net.h5")
```

保存したパラメータを使って推論をするコードは[こちら](https://github.com/ashitani/NNabla_exp/blob/master/replay.py)です。load_parameters()で呼び出せます。推論時はバッチサイズは１に変更したほうがよいでしょう。


推論結果です。青線がmathライブラリの出力、赤線が今回のネットの出力で、ほぼ重なっています。

![graph.png](https://qiita-image-store.s3.amazonaws.com/0/30340/2cf28954-864f-8acc-ce0d-74993ef114ab.png "graph.png")


# C++での推論

NNPという形式で保存しておくとC++から利用できるようです。この辺の記述は見よう見まねでやりましたが、そのうちAPIドキュメントができることでしょう。

```py
import nnabla.utils.save
                                      
runtime_contents = {
        'networks': [
            {'name': 'runtime',
             'batch_size': 1,
             'outputs': {'y': y},
             'names': {'x': x}}],
        'executors': [
            {'name': 'runtime',
             'network': 'runtime',
             'data': ['x'],
             'output': ['y']}]}
nnabla.utils.save.save('exp_net.nnp', runtime_contents)
```

これを下記のようなコードで使います。ほとんどexampleのままです。

```C++
#include <nbla_utils/nnp.hpp>

#include <iostream>
#include <string>
#include <cmath>

int main(int argc, char *argv[]) {
  nbla::CgVariablePtr y;
  float in_x;
  const float *y_data;

  // Load NNP files and prepare net
  nbla::Context ctx{"cpu", "CpuCachedArray", "0", "default"};
  nbla::utils::nnp::Nnp nnp(ctx);
  nnp.add("exp_net.nnp");
  auto executor = nnp.get_executor("runtime");
  executor->set_batch_size(1);
  nbla::CgVariablePtr x = executor->get_data_variables().at(0).variable;
  float *data = x->variable()->cast_data_and_get_pointer<float>(ctx);

  for(int i=1;i<10;i++){
    // set input data
    in_x = 0.1*i;
    *data = in_x;

    // execute
    executor->execute();
    y = executor->get_output_variables().at(0).variable;
    y_data= y->variable()->get_data_pointer<float>(ctx);

    // print output
    std::cout << "exp(" << in_x <<"):" << "predict: " << y_data[0] << ", actual: " << std::exp(in_x) <<std::endl;
  }

  return 0;
}

```


ビルドに使ったMakefileは下記。exampleのままです。

```Makefile
all: exp_net.cpp
    $(CXX) -std=c++11 -O -o exp_net exp_net.cpp -lnnabla -lnnabla_utils

clean:
    rm -f exp_net
```


実行結果です。

```
exp(0.1):predict: 1.10528, actual: 1.10517
exp(0.2):predict: 1.22363, actual: 1.2214
exp(0.3):predict: 1.34919, actual: 1.34986
exp(0.4):predict: 1.4878, actual: 1.49182
exp(0.5):predict: 1.64416, actual: 1.64872
exp(0.6):predict: 1.81886, actual: 1.82212
exp(0.7):predict: 2.01415, actual: 2.01375
exp(0.8):predict: 2.2279, actual: 2.22554
exp(0.9):predict: 2.45814, actual: 2.4596
```

推論そのものの時間比較は10000sampleぶんのループで

|Python|C++    |
|-----:|------:|
|566msec| 360msec|

でした。今回ぐらいのネットだと小さすぎてあまり比較には信憑性がないと思います。しかし、なにより起動直後のオーバヘッドが段違いで、同じ10000sampleを計算するだけのプログラムの実行時間全体を比較すると下記です。h5とNNPのサイズが結構違ったりしますし、python側はモデル構築からやり直していたりと、いろいろと不公平な比較だとは思いますので参考程度ですが、ずいぶん違います。

|          |Python|C++    |
|---------:|-----:|------:|
|	real	|7.186s| 0.397s|
|	user	|1.355s| 0.385s|
|	sys		|0.286s| 0.007s|



# 感想

基本はexampleのマネをしただけですが、pythonで学習したモデルがC++からサクッと使えるのはなかなか便利ではないかと思います。速度比較は今回の比較ではアテになりませんが、まあ遅くなる理由はないでしょう。大きめのネットで比較してみたいですね。

NNablaのOSXでのビルドはまだサポート外ということでいろいろと罠がありますが、そのうち解消されていくことでしょう。現時点でのインストール手順などは次章以降に書いておきます。

なお、Dockerで動かすには[こちら](http://tkat0.hateblo.jp/entry/2017/07/01/002918)に素晴らしいインストラクションがあります。jupyter notebookのファイル置き場をホスト側にするというアイデアが素晴らしいと思います。

# python ライブラリのインストール(OSX)

ビルド手順はインストラクションどおり。

```
git clone https://github.com/sony/nnabla
cd nnabla
sudo pip install -U -r python/setup_requirements.txt
sudo pip install -U -r python/requirements.txt

mkdir build
cd build
cmake ../
make
cd dist
sudo pip install -U nnabla-0.9.4.post8+g1aa7502-cp27-cp27mu-macosx_10_11_x86_64.whl
```

しかしimport nnablaで下記エラー。

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/***/.pyenv/versions/2.7.11/Python.framework/Versions/2.7/lib/python2.7/site-packages/nnabla/__init__.py", line 16, in <module>
    import _init  # Must be imported first
ImportError: dlopen(/Users/***/.pyenv/versions/2.7.11/Python.framework/Versions/2.7/lib/python2.7/site-packages/nnabla/_init.so, 2): Library not loaded: @rpath/libnnabla.dylib
  Referenced from: /Users/***/.pyenv/versions/2.7.11/Python.framework/Versions/2.7/lib/python2.7/site-packages/nnabla/_init.so
  Reason: image not found
```

```
 Library not loaded: @rpath/libnnabla.dylib
```

とのことなので、libnnabla.dylibの存在するパスをDYLD_LIBRARY_PATHに追加します。

```
export DYLD_LIBRARY_PATH='~/.pyenv/versions/2.7.11/Python.framework/Versions/2.7/lib/python2.7/site-packages/nnabla/':$DYLD_LIBRARY_PATH
```

これでimport nnablaは通りました。

ただし、examples/vision/mnist/classification.py"を試してみようとすると

```
Symbol not found: __gfortran_stop_numeric_f08
```

とのことで実行できませんでした。MNISTのデータを準備するところのようで、深くは追っていません。とりあえずこの状態でも本記事の例は動かせました。

# C++ライブラリのインストール(OSX)

libarchiveはhomebrewで入れます。

```
brew install libarchive
brew link --force libarchive

cmake .. -DBUILD_CPP_UTILS=ON -DBUILD_PYTHON_API=OFF
make
```

下記エラーが出ました。

```
Undefined symbols for architecture x86_64:
  "_archive_read_free", referenced from:
      nbla::utils::nnp::Nnp::add(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&) in nnp.cpp.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[2]: *** [lib/libnnabla_utils.dylib] Error 1
make[1]: *** [src/nbla_utils/CMakeFiles/nnabla_utils.dir/all] Error 2
make: *** [all] Error 2
```

どうやらlibarchiveのバージョン不整合のようです。追っていくと、エラーで止まっているのは下記

```
nnabla/build_cpp/src/nbla_utils/CMakeFiles/nnabla_utils.dir/link.txt
```

の最後で、/usr/lib/libarchive.dylib が指定されているところです。brewでインストールした/usr/local/lib/libarchive.dylibに書き換えればビルドが通りました。/usr/libのほうはOSX由来なのか自分で過去にいれたものか定かでないです。。

本来はcmake時にbrewでインストールされたほうを参照するようにすればよいのでしょう。まあ動いたのでよしとします。