DNN（ディープラーニング）ライブラリ : chainerとTensorFlowの比較 (1)

#はじめに
近年，機械学習の分野でディープラーニング(ディープニューラルネットワーク:DNN)に注目が集まっています．それに伴って様々なDNNライブラリが公開されていますが，それぞれどんな違いがあるのでしょうか（どれを使えば良いのだろう）．本稿ではDNNのネットワーク構造を，各ライブラリでどのように記述するのか比較してみたいと思います．
本稿で想定するのは，DNNについてはある程度知っているが，ライブラリについては知らないという方への説明です．各ライブラリのチュートリアルを読んで実際にコードを書いたことがある場合は，それ以上の内容はないです．

#ライブラリ
多数のライブラリが公開されていますが，本稿ではchainerとTensorFlowの2つを扱います．将来的には他のライブラリについても追加できればと思っていますが．
以下ではまず各ライブラリの簡単な基本情報だけまとめてみます．速度等の比較は [Pythonで書けるDeepLearningのライブラリを比較してみた][] が分かりやすかったです，ありがとうございます．このサイトで十分じゃね?って気もしますが，本稿では具体的にソースコードを並べて同じネットワーク構造の記述を比較してみたいと思います．

TensorFlow
------------------------------
* 開発元: google
* 公式チュートリアル: https://www.tensorflow.org/versions/master/tutorials/index.html
* githubスター数: 19830

googleが内部で実際に使っているライブラリをオープン化したものです．googleってだけで惹かれますね(?

chainer
------------------------------
* 開発元: Preferred Networks（PFN）
* 公式チュートリアル: http://docs.chainer.org/en/stable/tutorial/index.html
* githubスター数: 1240

PENは日本のスタートアップでNTTが出資しているようです．日本語のドキュメントに期待できる(?


#ライブラリの比較
チュートリアルを参考にまとめただけですが...
DNN初心者ですので用語に間違い等あるかもしれません．

ネットワーク構造・データセット
------------------------------
本稿では3層MLPのネットワーク構造を用います．本当はCNNをやりたかったのですがそれは次回ということで．隠れ層のユニット数は100とします．
``入力層 - 隠れ層 - 出力層``

データセットとしてはMNISTの手書き数字を用います．すでにDNNにとっては簡単な問題となっているようですが，今回は主に記述方法の比較がしたいので．入力層は784次元(28 x 28)，出力は10次元とします．

データの取得にはchainerのサンプルで使用されている``data.py``を使用しました．TensorFlowでは``input_data.py``というサンプルがよく使われていますが，勉強のためと思い``data.py``から読み込んだデータを使いました．
(``data.py``ではラベルを数字で表し``{1,4,9,2...}``のようなベクトルで表現します．一方の``input_data.py``では``{0,0,...,1,0}``というようなOne-Hotベクトルの集合で表現します．はじめこの違いに気付かず次元がおかしいというエラーが出ました．以下ではdense_to_one_hot(x)というメソッドでその変換操作を表します．

```py:trainingData
import data
import numpy as np

mnist = data.load_mnist_data()
x_all = mnist['data'].astype(np.float32) / 255
y_all = mnist['target'].astype(np.int32)

#only tensorFlow
y_all = dense_to_one_hot(y_all)

x_train, x_test = np.split(x_all, [60000])
y_train, y_test = np.split(y_all, [60000])

```
``x_train.shape => (60000, 784)``
``y_train.shape => (60000, 1) or (60000, 10) ``

となっています．


環境構築
------------------------------
今回はubuntu14.04(CPU-only)を用いました．そのため両ライブラリ共に ``pip`` で簡単に導入できました．windowsでは...今回は触れません．

ネットワークの記述
------------------------------
### chainer

```py:chainer(classDefine)
import chainer
import chainer.functions as F
import chainer.links as L

class MLP(chainer.Chain):
    def __init__(self):
        super(MLP, self).__init__(
                                  l1=L.Linear(784, 100),
                                  l2=L.Linear(100, 100),
                                  l3=L.Linear(100, 10),
                                  )

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        y = self.l3(h2)
        return y



class Classifier(Chain):
    def __init__(self, predictor):
        super(Classifier, self).__init__(predictor=predictor)

    def __call__(self, x, t):
        y = self.predictor(x)
        self.loss = F.softmax_cross_entropy(y, t)
        self.accuracy = F.accuracy(y, t)
        return self.loss

```
まずレイヤーの構造を定義するクラスと出力の誤差を定義するクラスを作ります．
``MLP``クラスでは各層に``Linear``(重み$W$とバイアス$b$によって表現される全結合)を，活性化関数として``ReLU(...)``を用いていることが分かります．この部分を変更することで``dropout(...)``を追加したり，畳み込み層を追加することが出来そうです．

また，誤差の算出には``softmax_cross_entropy(...)``，つまりソフトマックス関数の出力に対して交差エントロピーを計算したもの，を用いています．ちなみに定義したClassifierクラスは，同様のクラスがchainer.links.Classifierとして実装されています．そのままの実装でよければそちらを使えます．

```py:chainer(model)
        model = Classifier(MLP()) # same as ``L.Classifier(MLP())``
        optimizer = optimizers.SGD()
        optimizer.setup(model)
```

次に定義したクラスのインスタンスを生成します．ここで最適化の手法を指定しており，``SGD()``(確率的最急降下法)を指定しています．

### tensorFlow

```py:tensorFlow
import tensorFlow as tf

        # input
            x = tf.placeholder(tf.float32, [None, 784])
        # label
            y_ = tf.placeholder(tf.float32, [None, 10])


        # FC1
            W1 = tf.Variable(tf.random_normal([784, 100], mean=0.0, stddev=0.05))
            b1 = tf.Variable(tf.zeros([100]))
            # layer output
            h1 = tf.nn.relu(tf.matmul(x, W1) + b1)

        # FC2
            W2 = tf.Variable(tf.random_normal([100, 100], mean=0.0, stddev=0.05))
            b2 = tf.Variable(tf.zeros([100]))
            # layer output
            h2 = tf.nn.relu(tf.matmul(h1, W2) + b2)

        # FC3
            W3 = tf.Variable(tf.random_normal([100, 10], mean=0.0, stddev=0.05))
            b3 = tf.Variable(tf.zeros([10]))
            # output
            y = tf.nn.softmax(tf.matmul(h2, W3) + b3)

        # training
            cross_entropy = -tf.reduce_sum(y_ * tf.log(y))
            train_step = tf.train.GradientDescentOptimizer(0.01).minimize(cross_entropy)
```
tensorFlowでは各層を重み$W$とバイアス$b$を用いた行列の計算式$y = Wx + b$で表します($x$:入力，$y$:出力)．
入力$x$は今回``(データ数,784)``という行列になります．データ数をコードでは``None``と指定することで可変としています．
1層目の重み``W1``は``(784,100)``，バイアス``b1``は``(100,)``と定義しており，1層目の出力として``h1``の計算式を定義します．ここで活性化関数として``relu(...)``を用いています．また，出力層では``softmax(...)``を計算していることがわかります．
``y_``は正解ラベルを格納する変数であり，誤差``cross_entropy``の定義に使用します．誤差として交差エントロピーを用いており，``GradientDescentOptimizer``を用いて``minimize``するように指定しています．

chainerに比べると各層の記述が面倒な気がしますが，ネットワーク構造に手を加えようと思った時に記述が式そのままなのでやりやすい?と思いました．(コードが煩雑なのはちゃんとクラスを定義してやらないからか



学習
------------------------------
``batch_x, batch_y``をそれぞれ``x_train, y_train``のミニバッチとします．以下はひとつのミニバッチに対する処理です．

### chainer
```py:chainerTraining
optimizer.update(model, batch_x, batch_y)
```
optimizer.updateの引数に渡してやります．


### tensorFlow
```py:tensorFlowTraining
#sess = tf.Session()
sess.run(train_step, feed_dict={x:batch_x, y_:batch_y})
```
tensorFlowではデータをDictionaryの形で，上で定義した``placeholder``とセットにしてデータを渡してやります．


速度・精度の比較
------------------------------
今回はCPUのみのひ弱なデスクトップPCで動かしますので，epochも数十回程度でコードが動くことを確認しています．広く使われてるライブラリなので精度や速度といった面では互角なのでは，と思っています．今後機会があれば検証してみます．



#おわりに

今後の課題としては以下に取り組んでみたいと思います．

* CNNやRNNなどの複雑なネットワーク構造
* GPU
* 学習済モデルの配布状況

上2つ辺りでライブラリの違いが出てきそうです．複数GPUへの対応など試してみたい(そのためのマシンが...
また，caffeなどは学習済モデルを用いて分類をするだけの目的で使用することも多い(多くはない?)気がしますが，そうした目的に使えるかどうかも気になります．


#参考
* のんびりしているエンジニアの日記 - 『[Pythonで書けるDeepLearningのライブラリを比較してみた][]』 
   


[Pythonで書けるDeepLearningのライブラリを比較してみた]: http://nonbiri-tereka.hatenablog.com/entry/2015/12/16/003512