TensorFlowのMLPコードで"Wine"を分類


[前記事](http://qiita.com/TomokIshii/items/92a266b805d7eee02b1d)でTensorFlowのTutorialコードとして，Neural Networkの基本MLP(Multi-layer Perceptron)のコードを紹介した．今回は，そのコードを発展させる形でTensorFlowの諸機能について調べてみた．

コードの変更点は以下の通り．
- 活性化関数を tf.sigmoid() から tf.nn.relu() に変えた．
- オプティマイザーを勾配降下法(Gradient Descent)のものからAdagrad法のものに変更．
- ２層レイヤーから３層レイヤーに変更．
- TensorBoardの機能を使い，学習カーブをプロットしてみた．

（プログラミングの環境は，Python 2.7.10, tensorflow 0.5.0 になります．）

## "Wine"データセットの概略
季節がら(?) "Wine" データセットを用いることにした．UCI Machine Learning Repositoryでも現時点で人気第３位である．データには，13の特徴量(Attribute)が含まれている．（特徴量の詳細については，調べていません．アルコールとかマグネシウムとかいくつかは名前だけで分かりますが．）

> -- Attributes
> 1) Alcohol
> 2) Malic acid
> 3) Ash
> 4) Alcalinity of ash
> 5) Magnesium
> 6) Total phenols
> 7) Flavanoids
> 8) Nonflavanoid phenols
> 9) Proanthocyanins
> 10) Color intensity
> 11) Hue
> 12) OD280/OD315 of diluted wines
> 13) Proline       

これらの特徴量をつかってWineを３つのクラスに分ける多クラス分類問題である．（これも番号が大きい方がいいのか，小さい方がいいのかよくわかっていません．）

> -- Number of Instances
> class 1: 59
> class 2: 71
> class 3: 48

このようなデータセットを扱うために入力層13ユニット，出力層3ユニットのMLPモデルを用意して分類を行った．当初は，お手本通り確率的勾配降下法でmini-batchのデータ供給を行うつもりであったが，データの総数が178しかないので，ループ内計算で一括してデータを与えるBatch Gradient Descentの手法をとることにした．

## コードの詳細説明
初めに活性化関数にReLU (rectified linear unit) を導入しようとして，TensorFlowのドキュメントを調べたところ，

- tf.nn.relu()
- tf.nn.relu6()

の２種類が記載されていた．この relu6() について知識がなかったので，Jupyter Notebookで関数をプロットしてみた．

**Fig. "tf.nn.relu()" & "tf.nn.relu6()"**
![tensorflow_relu_relu6.PNG](https://qiita-image-store.s3.amazonaws.com/0/74152/c5fb769c-903b-620b-8d93-2e9be2d79aab.png)

上図の通り，普通のReLUではｘが＋方向に増加するのに従いｙも増加しているが，ReLU６では，y=6.0 で飽和するような関数となっている．（飽和ポイントのディフォルト値がy=1.0 はなくy=6.0である理由がよくわかりません．．．）今回は，よく使われている関数 (tf.nn.relu())を用いることとした．

これを含めて隠れ層(Hidden Layer)のコードを次のようにした．weightの初期化は，前回同様，tf.random_normal() を使って正規分布のランダム値を発生させている．

```py
# Hidden Layer
class HiddenLayer(object):
    def __init__(self, input, n_in, n_out):
        self.input = input
    
        w_h = tf.Variable(tf.random_normal([n_in,n_out],mean=0.0,stddev=0.05))
        b_h = tf.Variable(tf.zeros([n_out]))
     
        self.w = w_h
        self.b = b_h
        self.params = [self.w, self.b]
    
    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.nn.relu(linarg)  # switch sigmoid() to relu()
        
        return self.output

```

また出力層については，"MNIST"と同様 "Wine"分類問題も「多クラス分類」のケースなので，活性化関数にはSoftmax関数を用いている．（わずか３クラスですが，「多クラス」になります．）

```py
# Read-out Layer
class ReadOutLayer(object):
    def __init__(self, input, n_in, n_out):
        self.input = input
        
        w_o = tf.Variable(tf.random_normal([n_in,n_out],mean=0.0,stddev=0.05))
        b_o = tf.Variable(tf.zeros([n_out]))
       
        self.w = w_o
        self.b = b_o
        self.params = [self.w, self.b]
    
    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.nn.softmax(linarg)  

        return self.output
```

これらのネットワーク層をモデル化した２つのクラスを用いて，MLPの構成をプログラムで定義した．（コードは後ろに掲載します．）

また，変更したオプティマイザー**Adagrad**であるが，単に関数名（オプティマイザー名）をGradientDescentOptimizerからAdagradOptimizerに変更するだけで済んでしまった．

```py
# Train
    optimizer = tf.train.AdagradOptimizer(0.01)
    train_op = optimizer.minimize(loss)
    
    init = tf.initialize_all_variables()

    with tf.Session() as sess:
        sess.run(init)

      . . . 

```

ただ１点，注意を要するのが，オプティマイザーの定義と変数初期化のタイミングである．上のコードには明示的にならないが，AdagradOptimizer() はプログラムで指定されると，内部で用いるパラメータ用の変数(tf.Variable)を生成するとのことである．従って，TensorFlowの変数初期化プロセス(tf.initialize_all_variables())はその後に行わなければならない．仮に，順番を入れ換えて，

```py
# Train (ERROR)
    init = tf.initialize_all_variables()
    
    optimizer = tf.train.AdagradOptimizer(0.01)
    train_op = optimizer.minimize(loss)
    
```

とやってしまうとエラーとなるので要注意である．（当初，ここでデバッグに手間取りました．stackoverflow.com で上記解決策を得ました．）

## TensorBoardで計算プロセス表示

何事も初めてだと動かすまでにいろいろ手間取るが，下がうまく動作したコードになる．（TensorBoard関連箇所に "T.B."のコメントを入れています．）

```py
（前略）
    loss, accuracy = mk_NN_model()
    logloss = tf.scalar_summary('loss_w_L2', loss)    # T.B.
    logaccu = tf.scalar_summary('accuracy', accuracy) # T.B.
    summary_op = tf.merge_summary([logloss, logaccu]) # T.B.

    # Train
    optimizer = tf.train.AdagradOptimizer(0.01)
    train_op = optimizer.minimize(loss)
    
    init = tf.initialize_all_variables()

    with tf.Session() as sess:
        sess.run(init)
        summary_buff = tf.train.SummaryWriter('/tmp/tf_logs',
                                            graph_def=sess.graph_def) # T.B.
        print('Training...')
        for i in range(10001):
            train_op.run({x: train_x, y_: train_y})
            if i % 100 == 0:                 # for T.B. logging
                summary_str = sess.run(summary_op, {x: train_x, y_: train_y})  
                summary_buff.add_summary(summary_str, i)
            if i % 1000 == 0:                # echo status on screen
                train_accuracy = accuracy.eval({x: train_x, y_: train_y})
                print(' step, accurary = %6d: %8.3f' % (i, train_accuracy))
（後略）

```

ログ記録ためのの手続きとしては次の通り．
1. tf.scalar_summary() で記録したい変数を指定する．
2. tf.merge_summary() で変数をまとめる．それを使って操作(summary_op)オブジェクトとする．
(セッションを起動後...）
3. tf.train.SummaryWritere()で，ログを入れるディレクトリを指定，サマリ用のバッファを確保する．
4. sess.run(summary_op, {feed_dict}) に操作オブジェクト(summary_op)を渡してサマリ文字列を生成．
5. 生成した文字列を add_summary() で記録．

デバッグに時間をとられたのは，上の４番めのプロセス，sess.run() で文字列を作るところである．当初，訓練データのfeed_dictを与えていなかったが，動作しなかった．

```py
（誤）
    if i % 100 == 0:                 # for T.B. logging
        summary_str = sess.run(summary_op)   # <--- {feed_dict} が与えられていない
        summary_buff.add_summary(summary_str, i)
（正）
    if i % 100 == 0:                 # for T.B. logging
        summary_str = sess.run(summary_op, {x: train_x, y_: train_y})  
        summary_buff.add_summary(summary_str, i)
```

このプロセスの詳細については理解できていないが，TensorBoard用に記録を取る場合には，上記のようにsess.run()に訓練データ(feed_dict)を指定しなければいけないというルールは覚えておく必要がある．

以上，プログラムを実行した結果を以下に示す．

```text
Training...
 step, accurary =      0:    0.258
 step, accurary =   1000:    0.944
 step, accurary =   2000:    0.984
 step, accurary =   3000:    0.984
 step, accurary =   4000:    0.984
 step, accurary =   5000:    0.992
 step, accurary =   6000:    0.992
 step, accurary =   7000:    0.992
 step, accurary =   8000:    0.992
 step, accurary =   9000:    0.992
 step, accurary =  10000:    0.992
accuracy =  0.944444
```

**TensorBorad Plot (Loss with L2 term), (Accuracy)**
![tensorboard_loss_w_L2.png](https://qiita-image-store.s3.amazonaws.com/0/74152/ef1685b7-05c6-6d9a-e7df-effb155e6cf9.png)
![tensorboard_accuracy.png](https://qiita-image-store.s3.amazonaws.com/0/74152/47a9643d-7f93-3947-51a5-79e8a080319e.png)

loss_w_L2 は，L2正則化項を加えたコスト関数，accuracyは精度を示す．

約10,000回の反復計算を実施しているが，実際には4,000-5,000回ぐらいで十分な精度が得られているように見える．ランダムシャッフル後に訓練データとテストデータに分割する処理上，結果はばらつくが，約94 ~ 95% の分類精度となった．

## 全体コード

以上をまとめて，コードの全体を記載する．

"input_data.py"

```py
import numpy as np
import pandas as pd


def load_data(filename):
    # Shuffle data by row
    def data_shuffle(xm, ym, siz):
        idv = np.arange(siz)
        idv0 = np.array(idv)    # copy numbers
        np.random.shuffle(idv)
        
        xm[idv0] = xm[idv] 
        ym[idv0] = ym[idv]
        x_new = np.zeros_like(xm)
        y_new = np.zeros_like(ym)
        x_new[idv0,:] = xm[idv,:]
        y_new[idv0] = ym[idv]
        
        return x_new, y_new
    
    # Split dataset to train data and test data        
    def split_data(xm, ym, tr_ratio, te_ratio):
        tot = len(ym)
        itr = int(tot * tr_ratio / (tr_ratio + te_ratio))
        
        return xm[:itr,:], ym[:itr,:], xm[itr:,:], ym[itr:,:]
    
    mydf = pd.read_csv(filename, header=None)
    mydf.dropna(inplace=True)
    
    xdata = (mydf.iloc[:, 1:]).values   # Wine features
    iydata = (mydf.iloc[:, 0]).values   # Wine class (1, 2, 3)
    m = len(iydata)
    data_shuffle(xdata, iydata, m)

    ydata = np.zeros((m, 3))
    for i in range(m):
        ic = int(iydata[i] - 1)
        ydata[i, ic] = 1.0
    
    xtr, ytr, xte, yte = split_data(xdata, ydata, 0.7, 0.3)
    
    return xtr, ytr, xte, yte
    
```

"wine_classifier.py"

```py
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import tensorflow as tf

# Import data
from input_data import load_data

# Hidden Layer
class HiddenLayer(object):
    def __init__(self, input, n_in, n_out):
        self.input = input
    
        w_h = tf.Variable(tf.random_normal([n_in,n_out],mean=0.0,stddev=0.05))
        b_h = tf.Variable(tf.zeros([n_out]))
     
        self.w = w_h
        self.b = b_h
        self.params = [self.w, self.b]
    
    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.nn.relu(linarg)  # switch sigmoid() to relu()
        
        return self.output

# Read-out Layer
class ReadOutLayer(object):
    def __init__(self, input, n_in, n_out):
        self.input = input
        
        w_o = tf.Variable(tf.random_normal([n_in,n_out],mean=0.0,stddev=0.05))
        b_o = tf.Variable(tf.zeros([n_out]))
       
        self.w = w_o
        self.b = b_o
        self.params = [self.w, self.b]
    
    def output(self):
        linarg = tf.matmul(self.input, self.w) + self.b
        self.output = tf.nn.softmax(linarg)  

        return self.output

# Create the model
def mk_NN_model():
    # Define network structure
    h_layer1 = HiddenLayer(input=x, n_in=13, n_out=20)
    h_layer2 = HiddenLayer(input=h_layer1.output(), n_in=20, n_out=20)
    o_layer = ReadOutLayer(input=h_layer2.output(), n_in=20, n_out=3)

    # Cost Function basic term
    out = o_layer.output()
    cross_entropy = -tf.reduce_sum(y_*tf.log(out), name='xentropy')

    # Regularization terms (weight decay)
    L2_sqr = (tf.nn.l2_loss(h_layer1.w)
              + tf.nn.l2_loss(h_layer2.w)    
              + tf.nn.l2_loss(o_layer.w))
    lambda_2 = 0.01

    # the loss and accuracy
    loss = cross_entropy + lambda_2 * L2_sqr
    correct_prediction = tf.equal(tf.argmax(out,1), tf.argmax(y_,1))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
    
    return loss, accuracy


if __name__ == '__main__':
    train_x, train_y, test_x, test_y = load_data('./wine.data')

    # Variables
    x = tf.placeholder("float", [None, 13])
    y_ = tf.placeholder("float", [None, 3])
  
    loss, accuracy = mk_NN_model()
    logloss = tf.scalar_summary('loss_w_L2', loss)    # T.B.
    logaccu = tf.scalar_summary('accuracy', accuracy) # T.B.
    summary_op = tf.merge_summary([logloss, logaccu]) # T.B.

    # Train
    optimizer = tf.train.AdagradOptimizer(0.01)
    train_op = optimizer.minimize(loss)
    
    init = tf.initialize_all_variables()

    with tf.Session() as sess:
        sess.run(init)
        summary_buff = tf.train.SummaryWriter('/tmp/tf_logs',
                                            graph_def=sess.graph_def) # T.B.
        print('Training...')
        for i in range(10001):
            train_op.run({x: train_x, y_: train_y})
            if i % 100 == 0:                 # for T.B. logging
                summary_str = sess.run(summary_op, {x: train_x, y_: train_y})  
                summary_buff.add_summary(summary_str, i)
            if i % 1000 == 0:                # echo status on screen
                train_accuracy = accuracy.eval({x: train_x, y_: train_y})
                print(' step, accurary = %6d: %8.3f' % (i, train_accuracy))

        # Test trained model
        print('accuracy = ', accuracy.eval({x: test_x, y_: test_y}))

```

まだTensorFlowフレームワークの一部機能しか使っていないが，Neural Network モデルを実装する上で，キーとなるところは，かなりカバーできたのではないかと考えている．

## 参考文献 (web site)
- TensorFlow ドキュメント http://www.tensorflow.org/
- 深層学習（講談社 機械学習プロフェッショナルシリーズ）
http://www.kspub.co.jp/book/detail/1529021.html
- 落ちこぼれないためのTensorFlow Tutorialコード - Qiita
[http://qiita.com/TomokIshii/items/92a266b805d7eee02b1d](http://qiita.com/TomokIshii/items/92a266b805d7eee02b1d)
- UCI Machine Learning Repository: Wine Data Set
https://archive.ics.uci.edu/ml/datasets/Wine
- Tensorflow: Using Adam optimizer - stackoverflow
[http://stackoverflow.com/questions/33788989/tensorflow-using-adam-optimizer](http://stackoverflow.com/questions/33788989/tensorflow-using-adam-optimizer)