Coursera Machine Learningの演習課題をTensorFlowで実装してみる (その2: ex1_multi)

[前回](http://qiita.com/mtakayuki/items/be5216d8df8329462851)に引き続き、Coursera Machine Learningの演習をTensorFlowで実装してみます。

# 演習課題内容

線形回帰(linear regression)により、家の面積と寝室の数から家の価格を予測するプログラムを実装します。前回と同様に線形回帰ですが、フィーチャーの数が複数であることが大きな違いとなります。


# TensorFlowでの実装

## データの読み込み

まずは、以下のように家の面積(平方フィート)、寝室数、価格の値がカンマで区切られた学習データをファイルから読み込みます。

```:data/ex1/ex1data2.txt
2104,3,399900
1600,3,329900
2400,3,369000
1416,2,232000
3000,4,539900
...
```

学習データのサイズが小さいため、一度にすべてのデータをメモリ上に読み込んでいます。

```python
import numpy as np

data = np.genfromtxt('data/ex1/ex1data2.txt', delimiter=',')
input_X = data[:, 0:2].astype(np.float32)
input_y = data[:, 2:3].astype(np.float32)
```

`np.genfromtxt()`を実行すると、サイズが $m \times 3$ の行列(numpyの2次元配列)として`data`に格納されます。numpyのスライス表記でデータを取り出した結果、`input_X`と`input_y`のサイズは、$m \times 2$ と $m \times 1$ にそれぞれなります。

## フィーチャーの正規化

学習データを見ると、家の面積は寝室数の約1000倍となっています。このように各フィーチャーで値が大きく異なっている場合、最急降下法を実行しても収束に時間がかかってしまうため、まずは各フィーチャーを以下のように正規化します。

```math
\frac{X - \mu}{\sigma}
```

* $\mu$: 学習データに対する各フィーチャーの平均
* $\sigma$: 学習データに対する各フィーチャーの標準偏差

このようにすることで、大体のデータが±2の範囲に収まるようになります。

正規化するには、まず学習データを格納するプレースホルダーを定義します。平均と標準偏差を求めるのは学習データに対してのみになるため、、プレースホルダーのシェイプ(配列の各次元の大きさ)は、読み込んだ学習データと同じになるようにします。

```python
train_X = tf.placeholder(dtype=input_X.dtype, shape=input_X.shape, name='train_X')
```

このあと、`tf.nn.moments()`を使って`train_X`に対して平均と分散を求めます。`train_X`のシェイプは`(47, 2)`になりますが、この0番目の軸(47の方)に対して平均と分散を求めるため、`axes=[0]`を指定しています。フィーチャーが2つであるため、求めた結果はサイズが2のベクトルとなります。

```python
mean, var = tf.nn.moments(train_X, axes=[0])
print(train_X.shape, mean.shape, var.shape)
```

```:実行結果
(47, 2) (2,) (2,)
```

`mean`と`var`(の平方根)を直接使って正規化しても動作はするのですが、そうすると学習時だけでなく、学習後に予測する場合でも`input_X`を使って平均と分散を求めることになります。さすがにこれは非効率であるため、`mean`と`var`の平方根を変数`mu`と`sigma`に格納するようにします。

```python
mu = tf.Variable(mean, trainable=False, collections=[], name='mu')
sigma = tf.Variable(tf.sqrt(var), trainable=False, collections=[], name='sigma')
```

これらの変数は学習用の変数ではないため、`trainable=False`を指定しています。また、グローバル変数としてしまうと、他の変数と一緒に初期化されてしまうため、`collections=[]`を指定することで、グローバル変数として定義しないようにしています。

あとは、以下のように`mu`と`sigma`を初期化します。

```python
sess = tf.Session()
sess.run([mu.initializer, sigma.initializer], feed_dict={train_X: input_X})
```

フィーチャーを正規化する前に、プレースホルダーを定義します。`X`については、学習時だけでなく予測でも利用するため、`train_X`とは別に定義しています。

```python
X = tf.placeholder(tf.float32, shape=[None, 2], name='X')
y = tf.placeholder(tf.float32, shape=[None, 1], name='y')
```

なお、[前回](http://qiita.com/mtakayuki/items/be5216d8df8329462851)はプレースホルダーに対して`shape`は指定しませんでしたが、今回は複数のフィーチャーを行列を使って効率的に計算するために指定しています。シェイプの0番目の値は、学習データの場合には$m$(実際には47)ですが、予測にも使うため入力データの数には`None`を指定しています。

実際の正規化は以下のようになります。なお、`X`のシェイプは`(None, 2)`(学習時は`(47,2)`)に対して`mu`と`sigma`のシェイプは`(2,)`と一致していませんが、[ブロードキャスト](https://www.tensorflow.org/versions/master/experimental/xla/broadcasting)によってうまい具合にTensorFlowがシェイプを揃えてくれます。

```python
norm_X = (X - mu) / sigma

print(sess.run(norm_X, feed_dict={X: input_X}))
```

```:実行結果
[[  1.31415352e-01  -2.26093307e-01]
 [ -5.09640813e-01  -2.26093307e-01]
 [  5.07908642e-01  -2.26093307e-01]
 ...
 [ -1.46104956e+00  -1.55439162e+00]
 [ -1.89112723e-01   1.10220504e+00]
 [ -1.01459968e+00  -2.26093307e-01]]
```

## データフローグラフの構築

正規化された`norm_X`が定義できたため、仮説関数とコスト関数を定義します。複数のフィーチャーを表す`weights`を$2 \times 1$の行列(列ベクトル)として定義します。こうすることで、仮設関数を行列の掛け算として記述できます。

```python
weights = tf.Variable(tf.zeros([2, 1]), dtype=tf.float32, name='weights')
bias = tf.Variable(tf.zeros([1]), dtype=tf.float32, name='bias')

with tf.name_scope('hypothesis'):
    h = tf.matmul(norm_X, weights) + bias

with tf.name_scope('cost'):
    square_delta = tf.square(h - y)
    loss = tf.reduce_mean(square_delta) / 2
```

最急降下法を実行するデータフローグラフについては、前回と同様になります。

```python
alpha = 0.1
optimizer = tf.train.GradientDescentOptimizer(alpha)
train = optimizer.minimize(loss)
```

## データフローグラフの実行

実行に関しては、[前回](http://qiita.com/mtakayuki/items/be5216d8df8329462851)と同様になります。

まずは、グローバル変数を初期化します。

```python
sess = tf.Session()

init = tf.global_variables_initializer()
sess.run(init)
```

反復ごとのコストを可視化するためにサマリープロトコルバッファーとライターを準備します。

```python
import os
from datetime import datetime

def subdir(base='logs'):
    return os.path.join(base, datetime.now().strftime('%Y%m%d-%H%M%S'))

tf.summary.scalar('loss', loss)
merged = tf.summary.merge_all()

logdir = 'logs/ex1_multi'
writer = tf.summary.FileWriter(subdir(logdir), sess.graph)
```

実際に学習を実施します。

```python
feed_dict = {X: input_X, y: input_y}
max_iterations = 400
for i in range(max_iterations):
    if i % 10 == 0:
        summary, _ = sess.run([merged, loss], feed_dict=feed_dict)
        writer.add_summary(summary, i)
    sess.run(train, feed_dict=feed_dict)
```

学習後、実際に家の価格を予測してみます。

```python
def predict(x):
    if not hasattr(x, 'reshape'):
        x = np.array(x)
    x = x.reshape(-1, 2)
    return sess.run(h, feed_dict={X: x})

price = predict([1650, 3])
print('Predicted price of a 1650 sq-ft, 3 br house (using gradient descent):',
      '\n $%f\n' % price)
```

```:実行結果
Predicted price of a 1650 sq-ft, 3 br house (using gradient descent): 
 $293081.343750
```

## 学習率の変更

学習率`alpha`については、与えられた0.1をそのまま使っていましたが、実際には様々な値を試しながら適切な値を探していくことになるかと思います。その際、上記のようにハードコーディングしていると値の変更が面倒ですが、TensorFlowを使えば、簡単にコマンドラインオプションで値を指定できるようになります。

そのためには、まずコマンドラインオプションで何を指定できるかを定義します。以下では、学習率`alpha`に加えて、最大反復数`max_iterations`とサマリーの出力先`logdir`を定義しています。

```python
flags = tf.app.flags
flags.DEFINE_integer('max_iterations', 400, 'Max iteration number')
flags.DEFINE_float('alpha', 0.1, 'Learning rate for gradient descent')
flags.DEFINE_string('logdir', 'logs/ex1_multi/', 'Directory for logs')
FLAGS = flags.FLAGS
```

値を参照するには、`FLAGS.alpha`のように記述します。

```python
optimizer = tf.train.GradientDescentOptimizer(FLAGS.alpha)
```

あとは、実際のロジックを`main()`関数内に定義して、その関数を`tf.app.run()`を使って実行することで、スクリプト実行時のコマンドラインオプションで指定できるようになります。

実際のスクリプトは、以下のような構造になるかと思います。

```python:ex1_multi.py
import tensorflow as tf


flags = tf.app.flags
flags.DEFINE_integer('max_iterations', 400, 'Max iteration number')
flags.DEFINE_float('alpha', 0.1, 'Learning rate for gradient descent')
flags.DEFINE_string('logdir', 'logs/ex1_multi/', 'Directory for logs')
FLAGS = flags.FLAGS


def main():
    # データフロー構築や実行といったロジックを記載
    ...


if __name__ == 'main':
    tf.app.run()
```

```:実行結果
$ python ex1_multi.py -h
usage: ex1_multi.py [-h] [--max_iterations MAX_ITERATIONS] [--alpha ALPHA]
                    [--logdir LOGDIR]

optional arguments:
  -h, --help            show this help message and exit
  --max_iterations MAX_ITERATIONS
                        Max iteration number
  --alpha ALPHA         Learning rate for gradient descent
  --logdir LOGDIR       Directory for logs
```

このように記述したスクリプトを使って、実際に`alpha`の値を変更して、`loss`の収束具合をTensorBoardで確認してみます。

```
$ python ex1_multi.py --alpha 0.001
$ python ex1_multi.py --alpha 0.1
$ python ex1_multi.py --alpha 1.5
$ tensorboard --logdir logs/ex1_multi
```

`alpha` 0.1と0.001に対する`loss`の値を比較してみると、0.1(シアン)に比べて0.001(オレンジ)の方が緩やかに収束に向かっていることが分かります。

![small_alpha.png](https://qiita-image-store.s3.amazonaws.com/0/84248/3a30d504-df9f-cf41-c642-43e73a0bd3d3.png)

逆に、`alpha`を1.5の場合、収束せずに発散していました。

![large_alpha.png](https://qiita-image-store.s3.amazonaws.com/0/84248/2f98bf48-b712-66ef-9d62-04ab18080501.png)

このように、TensorFlowは、機械学習する上でいろいろと便利な機能を提供しているようです。便利！

# ソースコード

* [Jupyter notebookによる実行結果](https://github.com/mtakayuki/machine-learning-ex-tensorflow/blob/master/ex1_multi.ipynb)
* [単体スクリプト](https://github.com/mtakayuki/machine-learning-ex-tensorflow/blob/master/ex1_multi.py)