C++ヘッダだけでDeep Learning、tiny-dnnの紹介

[DeepLearning Advent Calendar](http://qiita.com/advent-calendar/2016/deeplearning),6日目の記事です。

Deep Learningフレームワークも世の中に随分と充実してきた昨今、いかがお過ごしでしょうか。今日はC++プログラマが簡単に導入できるDeep Learningフレームワーク、tiny-dnnを紹介します。

まとめ
===
- C++でDeep Learningやるなら、[tiny-dnn](http://github.com/tiny-dnn/tiny-dnn)が便利
- Header-only＆外部依存なしで簡単導入
- Caffeからのインポートやシリアライズなどの各種機能にも対応
- いろんなデバイス上で動かしたい人、既成のModel＋αを素早くアプリケーション化したい人、C++でDeep Learningを理解したい人に最適

背景
====
Deep Learningフレームワークといえば、

- Chainer
- TensorFlow
- Caffe

あたりが有名ですね。他にも老舗のTheanoにTorch、Tensorflowをラッパーした高レベルライブラリのKeras/TFlearn/TF-Slim、それからBaiduのPaddleやMSのCNTK、AWSが採用したMxNet、Intelが買収したNervanaのneonなどがあり…**多い！！**

Theanoしか選択肢が無かったような時代と違って、昨今はプログラミング言語を使い分けるように、Deep Learningフレームワームも用途に応じて使い分けることができるようになってきました。

ところが、ほとんどのフレームワークはPython APIをメインに想定されており、C++erにとっては意外と選択肢が限られています。CaffeとTensorFlowはC++APIにアクセスすることは可能ですが、CaffeはC++APIのドキュメント自体無いし、[TensorFlowはグラフ構築はPythonでやる前提](https://www.tensorflow.org/versions/r0.11/api_docs/cc/index.html)のため、学習済みのモデルをそのまま動かすだけなら兎も角、それ以上のことをやるなら結局Pythonが必要です。そこでC++erのためのフレームワーク、tiny-dnnの出番です。

tiny-dnnについて
=====

http://github.com/tiny-dnn/tiny-dnn

C++11で書かれたDeep Learningフレームワークです。私が2012年頃に勉強用に書いたコードを出発点として成長し、特定の大学・企業に属さずにC++erコミュニティで運営・開発されています。ありがたいことに2016/12/05現在、GitHub上で2164 starを集めており、C/C++ APIをメインとするDeep Learningフレームワークとしては最大勢力の1つとなっています。
コアメンバの多くは画像認識が専門なので、どちらかといえばCV系に強いフレームワークです。OpenCVのGSoC(Googleのオープンソース奨励プロジェクト)のうち2件でtiny-dnnがDeep Learningバックエンドとして採用されたこともあり、OpenCVにも今後取り込まれるかもしれません[^1]。


主な特徴は

- 簡単導入
- 抽象度の高いAPI

の2つ。以下解説していきます。

### 簡単導入
tiny-dnnはC++erにお馴染み、header-onlyで書かれています。さらに外部依存がない、スタンドアロンのフレームワークです。つまりGitHubからダウンロードして適当な場所に展開したら[^2]、おもむろに

```cpp
#include <tiny_dnn/tiny_dnn.h>
```

これで使えるようになります。インストールという概念自体ありません。圧倒的簡単ですね。Travis CIとAppVeyorを使ってOSはLinux/OSX/Windows、コンパイラはgcc/clang/msvc(2013/2015)でCIビルドが通る状態を維持しています。MinGW、Android、iOSで動かしているユーザーもいるようですし、スレッドが無い環境や例外を使わないビルド条件でも最小の労力で対応できるように作られており[^3]、Portabilityをかなり重視した作りになっているので、導入で転ぶことはかなり少ないはずです。

### 抽象度の高いAPI
導入が簡単でもシンタックスがゴリゴリだったり、覚えないといけない独自の概念が盛り込まれていると辛い。tiny-dnnはどうでしょうか？

まず3層のMLP(Multi Layer Perceptron)：

```cpp
network<sequential> net;

// 100-300-10
net << fully_connected_layer<relu>(100, 300)
    << fully_connected_layer<sigmoid>(300, 10);
```

まずネットワークを宣言して、1層ずつ```operator <<```でレイヤを積んでいきます。このへんはKerasあたりに近い感じ。レイヤ名は```fully_connected_layer```のようなfull nameと、```layers::fc```や```layers::dense```のような短いtypedefが[レイヤ毎に用意されている](https://github.com/tiny-dnn/tiny-dnn/blob/master/tiny_dnn/tiny_dnn.h#L86_L140)ので、好みのものを使いましょう。テンプレート引数で活性化関数を指定していますが、活性化関数を挟まない場合は、代わりに```dense<identity>```などと指定すればOK[^4]。もし活性化関数がすべての層で共通なら、

```cpp
auto net = make_mlp<relu>({100, 300, 10});
```

なんと1行でOK。

もはやお馴染み、CNN(Convolutional Neural Networks)でMNISTを学習するコードはこんな感じ：

```cpp
network<sequential> net;

net << conv<tan_h>(32, 32, 5, 1, 6)  // in:32x32x1, 5x5conv, 6fmaps
    << ave_pool<tan_h>(28, 28, 6, 2) // in:28x28x6, 2x2pooling
    << fc<tan_h>(14 * 14 * 6, 120)   // in:14x14x6, out:120
    << fc<identity>(120, 10);        // in:120,     out:10

// load MNIST dataset (insert 2x2 padding, value range=[-1.0,1.0])
std::vector<label_t> train_labels;
std::vector<vec_t> train_images;

parse_mnist_labels("train-labels-idx1-ubyte", &train_labels);
parse_mnist_images("train-images-idx3-ubyte", &train_images, -1.0, 1.0, 2, 2);

// declare optimization algorithm
adagrad optimizer;

// train (50-epoch, 30-minibatch)
net.train<mse>(optimizer, train_images, train_labels, 30, 50);

// save
net.save("net");
```

詳しい説明は[公式サンプル](https://github.com/tiny-dnn/tiny-dnn/tree/master/examples/mnist)と[APIドキュメント](http://tiny-dnn.readthedocs.io/en/latest/how_tos/How-Tos.html)に譲るとして、シンプルかつ素朴に書けることが分かるでしょうか？tiny-dnnのユーザーは他のフレームワークと併用するだろう、という想定が根底にあり、使い分け時のスイッチングコストを下げるためにtiny-dnn固有の用語(TheanoでいうscanやTensorFlowでいうsessionなど)は極力排除する方針をとっています。

ネットワークへの入力と期待結果として、```vec_t```と```label_t```という2つの独自型が登場していますが、これも何のことはない、それぞれ```std::vector<float>```と```size_t```のtypedefなので、手元のデータをfloat配列に変換して、クラス毎に適当な整数を割り当てればtiny-dnnで分類学習ができます(もちろん回帰にも対応)。画像を学習させたい場合、画像をMNIST形式(idx format)に変換する簡単なユーティリティも書いたので、これを使えば明示的に変換コードを書かなくてもOKです。

https://github.com/nyanp/mnisten

Deep Learningフレームワークは、提供するAPIの抽象度で分けると

- 低レベル(演算子レベル)のAPIを持つもの (Theano, TensorFlowなど)
- 高レベル(レイヤレベル)のAPIを持つもの (Keras, Caffe, tiny-dnnなど)

に分類でき、前者は柔軟性、後者は簡潔さに利点があります。たとえばニューラルネットでいう全結合層は、TensorFlowだと

```python
W = tf.Variable(tf.zeros([784, 10]))
b = tf.Variable(tf.zeros([10]))
y = tf.nn.softmax(tf.matmul(x, W) + b)
```

こんな感じなのに対して、Kerasでは

```python
y = Dense(10, input_shape=(784,))(x)
```

となります。tiny-dnnもレイヤ単位のAPIを提供しているのが一つの特徴[^5]で、C++erが手軽に使えることを目指しています。

### いつ使うべきか？
C++アプリケーションの中でDeep Learningを使う場合を主に想定しています。Portabilityが高いため、組み込み系の環境で使う場合には特によい選択肢になりそうです。一方、簡潔に書けるぶんモデル定義の柔軟性には劣るので、最新の論文を最速で実装したい、という場合にはChainerなどを使うのが吉です。

外部依存が無いので、OpenCVがそうしたように、他ライブラリのバックエンドとして採用することも容易です。ヘッダしか無いので、git submoduleでそのまま取り込めばOK。

C++かつ高レベルAPIのものが貴重なので、C++コードを介してDeep Learningを理解したい人にもおすすめできます。以下には、フレームワークの内部実装を使った簡単な日本語解説も書いています(コードがちょっと古いので、内部実装との対応を追う場合は[v0.1.1](https://github.com/tiny-dnn/tiny-dnn/tree/v0.1.1)あたりのリリースを参照ください)。

[https://github.com/tiny-dnn/tiny-dnn/wiki/実装ノート](https://github.com/tiny-dnn/tiny-dnn/wiki/%E5%AE%9F%E8%A3%85%E3%83%8E%E3%83%BC%E3%83%88)



### そのほかの機能と、今後の展開
個別の解説は行いませんが、以下の機能にも対応しています。

- Caffeからの学習済モデルのインポート
- JSON/Binary形式での学習結果の保存/読み込み (重みだけ/モデルだけ/両方の指定可)
- Jpeg/Bitmap/PNGの読み込みとリサイズ、正規化
- AVXを使った高速実行
- 一部の重みを固定したFine Tuning
- バッチ単位/epoch単位でのcallback
- dot言語でのグラフ可視化
- 畳み込み層の重み可視化
- seedの設定

Caffeからのインポートのみ、追加でProtocol Bufferが必要ですが、他はすべて外部依存なしで使えます[^6]。要望・BugFixはユーザー用の[Gitter room](https://gitter.im/tiny-dnn/users)か、GitHub Issueにお願いします :)

また、現時点ではGPUには非対応ですが、嬉しいことに数名のContributorによってOpenCL/CUDAへの対応が進められています。現時点でのVersionはv1.0.0alpha3ですが、v1.0.0正式リリースの時点でGPU対応が取り込まれる予定です。TensorFlowからのimporterもロードマップに入っています。

### Contribution募集中
tiny-dnnでは、どんな些細なPull Requestでも歓迎しています！最近だと、

- Contribution Guideのtypoを修正([#430](https://github.com/tiny-dnn/tiny-dnn/pull/430))
- コンパイラ警告の抑制([#431](https://github.com/tiny-dnn/tiny-dnn/pull/441))
- TensorFlowがWindows対応したので、他フレームワークとの比較表を更新([#435](https://github.com/tiny-dnn/tiny-dnn/pull/435))

などがありました。Deep Learningフレームワークとしては実装規模も小さくてとっつきやすいですし、Original Authorも日本人(私)なので、困ったら@nyanpに質問を投げてもらえれば答えられます。OSSへのコントリビューションに関心があるけど難しそう…という方も、取っ掛かりとして利用していただければ、今日からあなたもDeep Learning FrameworkのContributor。正直注目度に対して人手が圧倒的に不足している状況にあるので、Issue Listの[Contributions Welcome](https://github.com/tiny-dnn/tiny-dnn/issues?q=is%3Aissue+is%3Aopen+label%3A%22contributions+welcome%22)タグが付いている辺りに貢献していただけると泣いて喜びます…！

[^1]: 1) GSoCの成果物はOpenCV本体ではなくopencv_contribという別プロジェクトにいったん取り込まれ、そこから本体にマージされるものが選ばれる…という仕組みのため、GSoCの成果が全てOpenCVに入るとは限らない　2) 既存のcv::dnnモジュールとの共存(or置換)についてコミュニティ内で結論が出ていない、の2点から、現時点では入ることが決定したわけではない
[^2]: 自分のアプリケーション配下にまるごとコピペするもよし、PATHを通すもよし
[^3]: たとえば```#define CNN_SINGLE_THREAD```をincludeの前に置くと、single threadとして動作します
[^4]: 他のフレームワークと同様、活性化関数を独立したレイヤとして扱うようなAPIも追加予定です
[^5]: TensorFlowはじめ多くのフレームワークが、C++で実装されたコアにPython APIをかぶせる形態をとっているため、必然的に他のフレームワークを使うC++ Userは低レベルAPIを触ることになる
[^6]: シリアライズ用にCereal,画像処理にstb_imageといった3rd-partyのheader-onlyフレームワークを内部に含んで実現しています