SONY製ディープラーニングフレームワークであるNNablaを使い、pythonで学習してC++から推論を行うところまでの手順をやってみます。
NNablaの特徴の一つは、コアがピュアC++であることだと思います。学習はGPUの載ったマシンで行い、学習後のネットを組み込み機器などにexportするときにC++にサクッと移行できるのは利点じゃないかと思います。
C++ APIについてはまだドキュメントがありませんが、ver 0.9.4からexamples/cppというところに使用例が追加されました。推論だけならこれを見よう見まねすれば使えそうです。
お題はいつものように、関数近似で学ぶ chainer とディープラーニングとします。exp(x)という関数を三層のMLPで推論するものです。
すべてのコードはこちらに載せました。
Pythonでの学習
まず基本ライブラリをimportします。
import nnabla as nn
import nnabla.functions as F
import nnabla.parametric_functions as PF
import nnabla.solvers as S
グラフ生成はほとんどchainerと同じですね。パラメータを持たない関数はF,パラメータを持つ関数はPFに定義されています。
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に慣れていればまったく違和感のないワークフローです。データ供給用の関数もあるようですが今回は使っていません。
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)
ロスをプロットしました。
ノードのデータにアクセスするときは.dか.dataを使います。グラフ内の任意のノードに対してforward()ができます。これをつかって推論を行います。
x.d= 0.2
y.forward()
print(y.d[0][0])
バッチサイズ100で組まれたネットにスカラーを入れても通るのが一見不思議ですが、どうやらスカラを入れると100個分の同じ値を含むデータに置き換えてくれるようです。なので出力も100個分同じ値が出てきます。
学習済パラメータはsave_parameters()で保存できます。
nn.save_parameters("exp_net.h5")
保存したパラメータを使って推論をするコードはこちらです。load_parameters()で呼び出せます。推論時はバッチサイズは1に変更したほうがよいでしょう。
推論結果です。青線がmathライブラリの出力、赤線が今回のネットの出力で、ほぼ重なっています。
C++での推論
NNPという形式で保存しておくとC++から利用できるようです。この辺の記述は見よう見まねでやりましたが、そのうちAPIドキュメントができることでしょう。
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のままです。
#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のままです。
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で動かすにはこちらに素晴らしいインストラクションがあります。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でインストールされたほうを参照するようにすればよいのでしょう。まあ動いたのでよしとします。