多層パーセプトロンの境界値を可視化する

#はじめに

学習後の境界値を可視化します。
以下のような図です。

![moon_plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/b6bfde7c-6583-2813-0c75-01d2343758c0.png)

入力の次元が2次元の場合は、このように境界を可視化できます。
今回は、TensorFlow の多層パーセプトロンを例に可視化してみたいと思います。

#目次
1.学習用データ
2.モデル（多層パーセプトロン）
3.可視化の方針
4.meshgrid
5.等高線プロット
6.全コード
履歴

#1.学習用データ

今回利用するデータは、scikit-learn の dataset にある三日月型のデータセットを利用します。
2次元のデータですので、平面で図示できます。

```Python
from sklearn import datasets
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
'''
三日月データを生成する
'''
N = 300  # データ数を指定する
X, y = datasets.make_moons(N, noise=0.2)
Y = y.reshape(N, 1)

# トレーニングデータとテストデータに分ける
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, train_size=0.8)

'''
データプロット
'''
plt.figure(figsize=(10, 6))
plt.ylim([-1,1.5])
plt.xlim([-1.5,2.5])
plt.scatter(X[y==1][:,0], X[y==1][:,1], s=10, marker='.')
plt.scatter(X[y==0][:,0], X[y==0][:,1], s=10, marker='^')
plt.show()
```

![moon_plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/8307fea3-980b-9e57-9121-7c8c058de0ee.png)

このデータを使って学習し、境界線を引いてみます。


#2.モデル（多層パーセプトロン）

学習は TensorFlow (1.x) の多層パーセプトロンでやってみます。
モデルは一例ですので、境界値を引くためのモデルは何でもかまいません。
隠れ層を1つにした。3層パーセプトロンのコードは以下です。

```Python
class ThreelayerPerceptron(object):
    '''
    初期化
    '''
    def __init__(self, n_in, n_hidden, n_out):
        self.n_in = n_in
        self.n_hidden = n_hidden
        self.n_out = n_out
        self.weights = []
        self.biases = []

        self._x = None
        self._y = None
        self._t = None,
        self._sess = None
        self._history = {
            'accuracy': [],
            'loss': []
        }

    '''
    重み
    '''
    def weight_variable(self, shape):
        initial = tf.truncated_normal(shape, stddev=0.01)
        return tf.Variable(initial)

    '''
    バイアス
    '''
    def bias_variable(self, shape):
        initial = tf.zeros(shape)
        return tf.Variable(initial)

    '''
    モデル定義（3層パーセプトロン）
    '''
    def inference(self, x):
        
        # 入力層 - 隠れ層
        self.weights.append(self.weight_variable([self.n_in, self.n_hidden]))
        self.biases.append(self.bias_variable([self.n_hidden]))
        h = tf.nn.sigmoid(tf.matmul(x, self.weights[-1]) + self.biases[-1])

        # 隠れ層 - 出力層
        self.weights.append(self.weight_variable([self.n_hidden, self.n_out]))
        self.biases.append(self.bias_variable([self.n_out]))
        y = tf.nn.sigmoid(tf.matmul(h, self.weights[-1]) + self.biases[-1])

        return y

    '''
    損失関数
    '''
    def loss(self, y, t):
        cross_entropy = tf.reduce_mean(-tf.reduce_sum(t * tf.log(y) + (1 - t) * tf.log(1 - y)))
        return cross_entropy

    '''
    最適化アルゴリズム
    '''
    def training(self, loss):
        optimizer = tf.train.GradientDescentOptimizer(0.05)
        train_step = optimizer.minimize(loss)
        return train_step

    '''
    正解率
    '''
    def accuracy(self, y, t):
        correct_prediction = tf.equal(tf.cast(tf.greater(y, 0.5),tf.float32), t)
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        return accuracy
    
    '''
    予測
    '''
    def prediction(self, y):
        return tf.cast(tf.greater(y, 0.5),tf.float32)

    '''
    学習
    '''
    def fit(self, X_train, Y_train,
            nb_epoch=100, batch_size=100, p_keep=0.5,
            verbose=1):
        x = tf.placeholder(tf.float32, shape=[None, self.n_in])
        t = tf.placeholder(tf.float32, shape=[None, self.n_out])

        self._x = x
        self._t = t

        y = self.inference(x)
        loss = self.loss(y, t)
        train_step = self.training(loss)
        accuracy = self.accuracy(y, t)

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

        self._y = y
        self._sess = sess

        N_train = len(X_train)
        n_batches = N_train // batch_size

        for epoch in range(nb_epoch):
            X_, Y_ = shuffle(X_train, Y_train)

            for i in range(n_batches):
                start = i * batch_size
                end = start + batch_size

                sess.run(train_step, feed_dict={
                    x: X_[start:end],
                    t: Y_[start:end]
                })
            loss_ = loss.eval(session=sess, feed_dict={
                x: X_train,
                t: Y_train
            })
            accuracy_ = accuracy.eval(session=sess, feed_dict={
                x: X_train,
                t: Y_train
            })
            self._history['loss'].append(loss_)
            self._history['accuracy'].append(accuracy_)

            if verbose:
                print('epoch:', epoch,
                      ' loss:', loss_,
                      ' accuracy:', accuracy_)

        return self._history

    def evaluate(self, X_test, Y_test):
        accuracy = self.accuracy(self._y, self._t)
        return accuracy.eval(session=self._sess, feed_dict={
            self._x: X_test,
            self._t: Y_test
        })

    def predict(self, X_test):
        prediction = self.prediction(self._y)
        return prediction.eval(session=self._sess, feed_dict={
            self._x: X_test
        })
```

#3.可視化の方針

境界値の可視化は、格子点ごとに学習結果をプロットすることで行います。
以下の黒い格子点をテストデータとみなし、結果を予測します。

![20200308_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/fadaa9f7-729f-6f9f-ccde-f112728a0a76.png)

仮に、青線が境界値だった場合、
・青線より上の格子点は結果が「0」
・青線より下の格子点は結果が「1」
というような結果が得られます。
この結果を後述する「等高線プロット」で色分けします。

#4.meshgrid

格子点は numpy.meshgrid を使います。
例えば、以下のようにすると、5 × 5 のリストが取得できます。

```Python
x, y  = np.meshgrid(np.arange( 0, 10, 2),
                    np.arange( 1, 6, 1))
x
y
```

```Python
array([[0, 2, 4, 6, 8],
       [0, 2, 4, 6, 8],
       [0, 2, 4, 6, 8],
       [0, 2, 4, 6, 8],
       [0, 2, 4, 6, 8]])
array([[1, 1, 1, 1, 1],
       [2, 2, 2, 2, 2],
       [3, 3, 3, 3, 3],
       [4, 4, 4, 4, 4],
       [5, 5, 5, 5, 5]])
```

これを x軸、y軸に指定することで、下図のような格子点の配列になります。
配列（grids）の 0 から順に、格子点の矢印の順番になります。

```Python
grids = np.array([x.ravel(), y.ravel()]).T
```

![20200308_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/526e8f53-dc6c-3cfd-a137-4a50d498821a.png)

#5.等高線プロット

境界値は matplotlib の contourf で線を引きます。
第1引数にX軸、第2引数にY軸、第3引数に高さを設定します。
高さは、今回のケースでは2値分類の予測結果なので、「0」「1」のどちらかです。

```Python
from matplotlib.colors import ListedColormap
cmap = ListedColormap( ( "mistyrose","lightcyan") )
plt.contourf(x, y, pred, cmap=cmap)
```

格子点の予測を pred とすると、このコードで下図のような境界線を引くことができます。

![moon_plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545343/b6bfde7c-6583-2813-0c75-01d2343758c0.png)


#6.全コード

モデルによる学習を含めた、境界線プロットの全コードは以下です。

```Python
import numpy as np
import tensorflow as tf
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.utils import shuffle
import matplotlib.pyplot as plt

np.random.seed(0)
tf.set_random_seed(0)

class ThreelayerPerceptron(object):
    '''
    初期化
    '''
    def __init__(self, n_in, n_hidden, n_out):
        self.n_in = n_in
        self.n_hidden = n_hidden
        self.n_out = n_out
        self.weights = []
        self.biases = []

        self._x = None
        self._y = None
        self._t = None,
        self._sess = None
        self._history = {
            'accuracy': [],
            'loss': []
        }

    '''
    重み
    '''
    def weight_variable(self, shape):
        initial = tf.truncated_normal(shape, stddev=0.01)
        return tf.Variable(initial)

    '''
    バイアス
    '''
    def bias_variable(self, shape):
        initial = tf.zeros(shape)
        return tf.Variable(initial)

    '''
    モデル定義（3層パーセプトロン）
    '''
    def inference(self, x):
        
        # 入力層 - 隠れ層
        self.weights.append(self.weight_variable([self.n_in, self.n_hidden]))
        self.biases.append(self.bias_variable([self.n_hidden]))
        h = tf.nn.sigmoid(tf.matmul(x, self.weights[-1]) + self.biases[-1])

        # 隠れ層 - 出力層
        self.weights.append(self.weight_variable([self.n_hidden, self.n_out]))
        self.biases.append(self.bias_variable([self.n_out]))
        y = tf.nn.sigmoid(tf.matmul(h, self.weights[-1]) + self.biases[-1])

        return y

    '''
    損失関数
    '''
    def loss(self, y, t):
        cross_entropy = tf.reduce_mean(-tf.reduce_sum(t * tf.log(y) + (1 - t) * tf.log(1 - y)))
        return cross_entropy

    '''
    最適化アルゴリズム
    '''
    def training(self, loss):
        optimizer = tf.train.GradientDescentOptimizer(0.05)
        train_step = optimizer.minimize(loss)
        return train_step

    '''
    正解率
    '''
    def accuracy(self, y, t):
        correct_prediction = tf.equal(tf.cast(tf.greater(y, 0.5),tf.float32), t)
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        return accuracy
    
    '''
    予測
    '''
    def prediction(self, y):
        return tf.cast(tf.greater(y, 0.5),tf.float32)

    '''
    学習
    '''
    def fit(self, X_train, Y_train,
            nb_epoch=100, batch_size=100, p_keep=0.5,
            verbose=1):
        x = tf.placeholder(tf.float32, shape=[None, self.n_in])
        t = tf.placeholder(tf.float32, shape=[None, self.n_out])

        self._x = x
        self._t = t

        y = self.inference(x)
        loss = self.loss(y, t)
        train_step = self.training(loss)
        accuracy = self.accuracy(y, t)

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

        self._y = y
        self._sess = sess

        N_train = len(X_train)
        n_batches = N_train // batch_size

        for epoch in range(nb_epoch):
            X_, Y_ = shuffle(X_train, Y_train)

            for i in range(n_batches):
                start = i * batch_size
                end = start + batch_size

                sess.run(train_step, feed_dict={
                    x: X_[start:end],
                    t: Y_[start:end]
                })
            loss_ = loss.eval(session=sess, feed_dict={
                x: X_train,
                t: Y_train
            })
            accuracy_ = accuracy.eval(session=sess, feed_dict={
                x: X_train,
                t: Y_train
            })
            self._history['loss'].append(loss_)
            self._history['accuracy'].append(accuracy_)

            if verbose:
                print('epoch:', epoch,
                      ' loss:', loss_,
                      ' accuracy:', accuracy_)

        return self._history

    def evaluate(self, X_test, Y_test):
        accuracy = self.accuracy(self._y, self._t)
        return accuracy.eval(session=self._sess, feed_dict={
            self._x: X_test,
            self._t: Y_test
        })

    def predict(self, X_test):
        prediction = self.prediction(self._y)
        return prediction.eval(session=self._sess, feed_dict={
            self._x: X_test
        })


'''
三日月データを生成する
'''
N = 300  # 全データ数
X, y = datasets.make_moons(N, noise=0.2)
Y = y.reshape(N, 1)

# トレーニングデータとテストデータに分ける
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, train_size=0.8)

'''
データプロット
'''
plt.figure(figsize=(10, 6))
plt.ylim([-1,1.5])
plt.xlim([-1.5,2.5])
plt.scatter(X[y==1][:,0], X[y==1][:,1], s=10, marker='.')
plt.scatter(X[y==0][:,0], X[y==0][:,1], s=10, marker='^')
plt.show()

'''
モデル設定
'''
model = ThreelayerPerceptron(n_in=len(X[0]),
                             n_hidden=3,
                             n_out=len(Y[0]))

'''
モデル学習
'''
history = model.fit(X_train, Y_train,
                    nb_epoch=400,
                    batch_size=20,
                    verbose=1)

'''
予測精度の評価
'''
accuracy = model.evaluate(X_test, Y_test)
print('accuracy: ', accuracy)

'''
グラフを色分けするための格子データを生成する
'''
meshgrids  = np.meshgrid(
               np.arange( -1.5, 2.6, 0.01 ),
               np.arange( -1, 1.6, 0.01 )
             )
xx = meshgrids[0]
yy = meshgrids[1]
# 格子点のリストにする
grids = np.array([xx.ravel(), yy.ravel()]).T

'''
格子データの予測結果を取得する
'''
pred = model.predict(grids)
pred = pred.reshape( xx.shape )

'''
データプロット
'''
from matplotlib.colors import ListedColormap
w_[1,1] * v_[1])
plt.figure(figsize=(10, 6))
plt.ylim([-1,1.5])
plt.xlim([-1.5,2.5])
cmap = ListedColormap( ( "mistyrose","lightcyan") )
plt.contourf(xx, yy, pred, cmap=cmap)
plt.scatter(X[y==1][:,0], X[y==1][:,1], s=10, marker='.')
plt.scatter(X[y==0][:,0], X[y==0][:,1], s=10, marker='^')
plt.show()

```

#履歴
2020/03/08 初版公開
2020/03/09 損失関数の定義修正