webエンジニアの日常

RubyやPython, JSなど、IT関連の記事を書いています

初めてのTensorFlow入門~ロジスティック回帰~

こんにちは、エンジニアのさもです。

TensorFlow入門記事の第三回目はロジスティック回帰によるニ値分類器を作ります。

TensorBoardの使い方も勉強したので、

www.uosansatox.biz

最初からTensorBoardのコードを埋め込んでいきます。

スポンサーリンク

目次

モデルの概要

今回使うデータは、0か1のラベルがついた平面上の点で、 それぞれのラベルがついたデータは、異なる2次元正規分布から生成しています。

こんな感じのデータです。

f:id:s-uotani-zetakansu:20170928182822p:plain

やりたいことは、2つのラベルのデータを次の直線で分類することです。

 f(x_{1}, x_{2}) = w_{1}x_{1} + w_{2}x_{2} + w_{0} = 0

これを変形すると、

 x_{2} = ax_{1} + b

 a = -\frac{w_{1}}{w_{2}}, b = -\frac{w_{0}}{w_{2}}

となります。中学校で習う一次関数ですね。

学習するパラメータは W = (w_{1}, w_{2}), bで、入力は X = (x_{1}, x_{2})になります。

ある場所 (m,n)での値 f(m,n)は、直線 f(x_{1},x_{2})からの距離(正確には距離に符号をつけたもの)になります。

ここで、単純にあるデータが1か0かを判定するのではなく、「ある場所にあるデータが1である確率はいくらか」かというのを考えたいと思います。

そのために、直線との距離 [-\infty, \infty]を[0,1]へ移すような関数が必要なのですが、今回は次に示すシグモイド関数を使います。

 \sigma(d) = \frac{1}{1+e^{-d}}

f:id:s-uotani-zetakansu:20170928185359p:plain

 f(x_{1}, x_{2})上の任意の点(m,n)は、 f(m,n) = 0となります。このときのシグモイド関数の値( \sigma(0))は、0.5になります。

シグモイド関数は、値域が[0,1]で、単調増加な関数なので、上手く距離を確率へあてはめられますね。

このシグモイド関数(厳密には標準シグモイド関数)はロジスティック関数の特別な場合なので、シグモイド関数を使った直線回帰はロジスティック回帰とも言われています。

さて、全てのデータがラベルどおりに分類されている確率は、ある点がラベルどおりに分類される確率を全てのデータについて掛け合わせたものなので、次のように計算されます

 p = \prod_{i=1}^{N}\sigma(d_{i})^{t_{i}} * \sigma(1 - d_{i})^{1 - t_{i}}

 d_{i}, t_{i}はそれぞれ、i番目のデータの f(x_{1}, x_{2})の値と、ラベルの値(0 or 1)になります。

このpを最大化すればいいのですが、掛け算は計算効率が悪いので、logを取って足し算にします。また、マイナスの符号をつけて、問題を最小化する問題に帰着させます。これは勾配降下法を使うためです(なのかな?)。

 P = -log(p) = -\sum_{i=1}^{N}t_{i}\sigma(d_{i}) + (1 - t_{i})\sigma(1 - d_{i})

この値 Pを交差エントロピーというらしいです。

モデルの概要は以上です。

実装編

では順に実装していきます。

今回もjupyter notebookを使うことを想定しています。使わない場合は、%matplotlib inlineをコメントアウトするだけだと思います。

ライブラリのインポート

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from numpy.random import multivariate_normal, permutation
import pandas as pd
from pandas import DataFrame
%matplotlib inline

データを操作するために、pandasを入れています。pandasについては詳しくないので解説はしませんが、こちらの書籍が良いらしいです。

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 ―NumPy、pandasを使ったデータ処理

学習データの準備

np.random.seed(20170926)

# ラベルが0のデータを準備する
n0, mu0, variance0 = 200, [10, 11], 18
data0 = multivariate_normal(mu0, np.eye(2)*variance0, n0)
df0 = DataFrame(data0, columns=['x1', 'x2'])
df0['t'] = 0

# ラベルが1のデータを準備する
n1, mu1, variance1 = 120, [18, 20], 20
data1 = multivariate_normal(mu1, np.eye(2)*variance1, n1)
df1 = DataFrame(data1, columns=['x1', 'x2'])
df1['t'] = 1

# ラベルが1のものと0のものとを統合
df = pd.concat([df0, df1], ignore_index=True)
# 実際のデータに近いように、順番をランダムに並び替え
train_set = df.reindex(permutation(df.index)).reset_index(drop=True)

# pandasのデータフレームから入力値とラベルを別々の配列として取り出す
train_x = train_set[['x1', 'x2']].as_matrix()
train_t = train_set['t'].as_matrix().reshape([len(train_set), 1])

multivariate_normal関数は、2次元積分布を生成する関数です。

n0,n1は返り値のデータサイズ(配列の長さ)、mu0,mu1は平均variance0,variance1は分散です。

np.eye(2)*variance0で分散共分散行列を生成しています。variance0が10のときは、


\begin{pmatrix}
10&0\\\
0&10
\end{pmatrix}

となるので、 x_{1}軸、 x_{2}軸へ同じ広がり方で、全く相関のないデータということになります。

np.random.seed(20170926)でseedを指定しておけば、いつでも同じ乱数が生成されるので、何度も実験をする場合は便利ですね。

変数の準備

with tf.name_scope('X'):
    x = tf.placeholder(tf.float32, [None, 2]) # 入力データ
with tf.name_scope('W'):
    w = tf.Variable(tf.zeros([2,1]))          # 学習パラメータ(直線の係数)
with tf.name_scope('B'):
    w0 = tf.Variable(tf.zeros([1]))           # 学習パラメータ
with tf.name_scope('f'):
    f = tf.matmul(x, w) + w0                  # 直線とデータとの距離
with tf.name_scope('P'):
    p = tf.sigmoid(f)                         # 距離を確率に変換
with tf.name_scope('T'):
    t = tf.placeholder(tf.float32, [None, 1]) # 正解ラベルデータ
sess = tf.Session()

損失関数、学習方法、正解率の定義

with tf.name_scope('loss'):
    loss = -tf.reduce_sum(t*tf.log(p) + (1-t)*tf.log(1-p))
with tf.name_scope('train'):
    train_step = tf.train.AdamOptimizer().minimize(loss)
with tf.name_scope('accuracy'):
    correct_prediction = tf.equal(tf.sign(p-0.5), tf.sign(t-0.5))
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

lossはモデルの概要で紹介した交差エントロピーになっています。

正解率のところは補足が必要そうですね。

correct_prediction = tf.equal(tf.sign(p-0.5), tf.sign(t-0.5))

の部分です。

まずtf.signで引数が正の数か負の数かを返します。

tf.sign(p-0.5)は予測したラベルが0の場合(pは0.5以下)は負になります。逆に1と予測した場合は、tf.sign(p-0.5)が正を返します。

次に、tf.sign(t-0.5)の部分で、ラベルの値(0 or 1)を正か負か(-0.5 or 0.5 -> - or +)に変換しています。

もし予測が正しければ、tf.equalがtrueになり、異なっていればfalseということになります。

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

の部分ですが、tf.castで引数の配列(TrueとFalseが要素)を要素が1と0の配列に変換しています。その平均をとるので、要するに、

 \frac{正解数}{データの数}

を計算していることになり、すなわち正解率になっています。

なかなかテクニカルな書き方ですね。私は初めてみたとき、なぜこれで正解率が計算できるのか謎でした。

TensorBoardで追跡する変数を定義

with tf.name_scope('summary'):
    tf.summary.scalar('accuracy', accuracy)
    tf.summary.scalar('loss', loss)
    merged = tf.summary.merge_all()
    writer = tf.summary.FileWriter('./logs', sess.graph)

今回は正解率と損失関数の値をグラフに出してみたいと思います。

もし./logsにグラフのデータがある場合は削除しておいてください。

学習

sess.run(tf.global_variables_initializer())

i = 0
for _ in range(20000):
    i += 1
    __, summary = sess.run([train_step, merged], feed_dict={x: train_x, t: train_t})
    writer.add_summary(summary, _)
    if i % 2000 == 0:
        loss_val,acc_val = sess.run([loss, accuracy], feed_dict={x: train_x, t: train_t})
        print('step: %d, loss: %f, accuracy: %f' % (i, loss_val, acc_val))

学習を行う部分です。

実行すると、2000ステップごとに損失関数の値と正解率が表示されます。

tensorboard --logdir=./logsとコンソールに入力して、http://localhost:6006へアクセスすると、TensorBoardで正解率のグラフや、計算グラフの形を見ることが出来ると思います。

学習結果の表示

では学習した結果、どんな直線ができたのか、グラフに表示してみましょう

plt.scatter(df0['x1'], df0['x2'], marker="o")
plt.scatter(df1['x1'], df1['x2'], marker="x")
w_val = sess.run(w)
w0_val = sess.run(w0)
linex = np.linspace(0,35,100)
liney = -(w_val[0][0]/w_val[1][0]) * linex - w0_val[0]/w_val[1][0]
plt.plot(linex, liney)

実行すると以下のようなグラフが表示されると思います。

f:id:s-uotani-zetakansu:20170929131637p:plain

最後に

最後までお読みいただきありがとうございました。

記事には書いていないですが、オプティマイザを変えてみたり、データ数を変えてみたり、いろいろ実験できる部分があって楽しいですね。

今回の正解率は90%前後でしたが、直線で完全に分類することは不可能なので、90%が限界かなと思います。

mnistあたりになってくると、自分であれこれ試行錯誤できそうなので楽しみです。

ゆっくりですが、これからもTensorFlowの勉強したことを記事に書いていきたいと思います。

応援よろしくお願いします。