今日も窓辺でプログラム

外資系企業勤めのエンジニアが勉強した内容をまとめておくブログ

ニューラルネットワークを実装する [Part 2 ロジスティック回帰編]

はじめに

Peter's Notes のニューラルネットワークに関するメモのPart 2の部分を追っていきます。
前回は線形回帰でしたが、今回はロジスティック回帰です。

前回の記事:
www.madopro.net

ロジスティック回帰

今回は2クラス分類問題を考えます。2次元の入力 {\bf x}に対し、 {\bf x}が所属するクラス t \in \{0, 1\}を予測します。
予測にはロジスティック回帰というモデルを使用します。Part 1では入力と出力がともに1次元のモデルを考えましたが、今回は次のように入力 {\bf x}が2次元、出力 yは1次元のモデルを考えます。

f:id:kanohk:20171021182240p:plain
出典: https://peterroelants.github.io/posts/neural_network_implementation_part02/#Logistic-regression-(classification)

このモデルの出力 yは、入力 {\bf x}がクラス t = 1に属している確率を表すこととします。つまり
 \displaystyle y = P(t = 1 | {\bf x}, {\bf w})
と表現できます。

重みの学習に使用する入力の用意

前回同様、人工的に入力データを用意します。下記画像のように、2次元平面の(-1, 0)周辺に散らばっている赤クラス( t = 0)と、(1, 0)周辺に散らばっている青クラス( t = 1)の2クラスを用意します。

f:id:kanohk:20171021183159p:plain

上記のデータを生成するには、次のようなコードを使用します。

# 2クラス分類: 青(t = 1)と赤(t = 0)
num_of_samples_per_class = 20
red_mean = [-1, 0] # 赤クラスは(-1, 0)周辺に分布
blue_mean = [1, 0] # 青クラスは(1, 0)周辺に分布
std_dev = 1.2

# 2クラスのサンプルを生成
x_red = np.random.randn(num_of_samples_per_class, 2) * std_dev + red_mean
x_blue = np.random.randn(num_of_samples_per_class, 2) * std_dev + blue_mean

# 生成したサンプルのマージ
X = np.vstack((x_red, x_blue))

# サンプルのクラス
t = np.vstack((np.zeros((num_of_samples_per_class, 1)), np.ones((num_of_samples_per_class, 1))))

これで、Xには入力の値、tには正解ラベルが格納されている状態になりました。

モデルの出力

モデルの出力 yは、入力 {\bf x}に重み {\bf w}を乗じ、ロジスティック関数 \sigmaを適用してあげることで得られます。
 \displaystyle y = \sigma({\bf x} {\bf w^T})
この時のロジスティック関数 \sigmaは次のように定義します。
 \displaystyle \sigma(z) = \frac{1}{1 + e^{-z}}

これを素直にPythonに落としていくと次のようになります。

# ロジスティック関数の定義
def logistic(z):
    return 1 / (1 + np.exp(-z))

# ロジスティック回帰のモデルを定義
def nn(x, w):
    return logistic(x.dot(w.T))

モデルの出力 yは、入力がクラス t = 1である確率でしたので、 yが0.5以下の時は xが属するクラスは t = 0,  yが0.5より大きいときは xが属するクラスは t = 1と予測することができます。

# モデルの出力をもとにクラスtを予測する関数
def nn_predict(x, w):
    return np.around(nn(x, w))

コスト関数

ロジスティック関数のコスト関数には、クロスエントロピーが使用されることが多いです。
クロスエントロピーを最小化することは、尤度を最大化することと同等のようです。*1

クロスエントロピーは次のように定義されます。
 \displaystyle \xi(t_i, y_i) = -t_i \log(y_i) - (1 - t_i) \log (1 - y_1)

# コスト関数 (クロスエントロピー)
def cost(y, t):
    return -np.sum(np.multiply(t, np.log(y)) + np.multiply((1 - t), np.log(1 - y)))

最初に用意した入力Xと正解ラベルtにを使用して、コスト関数を可視化すると次のようになります。色が濃い部分が値が小さい部分となります。この色が濃い部分に入るような重みを見つけるために最適化を行っていくことになります。
f:id:kanohk:20171021185758p:plain

最急降下法

Part 1同様、最急降下法を使用してコスト関数を最適化していきます。
具体的な数式の導出過程は長くなるので省略してしまいますが、重みの更新は以下のように行います。
 \displaystyle {\bf w} (k + 1) = {\bf w}(k) - \Delta {\bf w} (k + 1)
ただし、 \Delta {\bf w}は次のように表せます。
 \displaystyle {\bf w} = \mu \frac{\partial \xi}{\partial {\bf w}} = \mu {\bf x} (y - t)

これもコードに落とすと次のようになります。

# 勾配の定義
def gradient(w, x, t):
        return (nn(x, w) - t).T * x

# Δw
def delta_w(w_k, x, t, learning_rate):
    return learning_rate * gradient(w_k, x, t)

それでは今までのコードを使って重みの更新をしていきます。

# 初期重みをセット
w = np.asmatrix([-4, -2])

# 学習率
learning_rate = 0.05

# 最急降下法のイテレーションを開始
num_of_iterations = 10
w_iter = [w] # 後の可視化のために、イテレーションごとの重みをここに保存します
for i in range(num_of_iterations):
    dw = delta_w(w, X, t, learning_rate)
    w = w - dw
    w_iter.append(w)

イテレーションが進むにつれて重みがどのように更新されていくかを可視化したものが下図になります。
f:id:kanohk:20171021191326p:plain

初期重み {\bf w}(1) = (-4, -2)から、色が濃い部分に向かって重みが最適化されている様子が確認できます。

学習結果の可視化

以上の学習で得られた重み {\bf w}を使用して、入力 {\bf x}がどちらのクラスに分類されるかを可視化します。

# 入力(x_1, x_2)を[-4, 4]の範囲で可視化するため、[-4, 4]を200分割したメッシュを用意します
num_of_xs = 200
xs1 = np.linspace(-4, 4, num=num_of_xs)
xs2 = np.linspace(-4, 4, num=num_of_xs)
xx, yy = np.meshgrid(xs1, xs2)

# 各点でモデルの出力がどちらのクラスになるかを、
# nn_predict関数を使用して予測します。
classification_plane = np.zeros((num_of_xs, num_of_xs))
for i in range(num_of_xs):
    for j in range(num_of_xs):
        classification_plane[i, j] = nn_predict(np.asmatrix([xx[i, j], yy[i, j]]), w)

cmap = ListedColormap([
        colorConverter.to_rgba('r', alpha=0.30),
        colorConverter.to_rgba('b', alpha=0.30)])

# 予測したクラスと、元の入力をプロットしてあげます
plt.contourf(xx, yy, classification_plane, cmap=cmap)
plt.plot(x_red[:, 0], x_red[:, 1], 'ro', label='target red')
plt.plot(x_blue[:, 0], x_blue[:, 1], 'bo', label='target blue')
plt.grid()
plt.legend(loc=2)
plt.xlabel('$x_1$', fontsize=15)
plt.ylabel('$x_2$', fontsize=15)
plt.title('red vs. blue classification boundary')
plt.show()

上記コードを実行すると次の図が得られます。確かに、初めに生成した入力を大体分類できるような重みが学習できていることが確認できます。
f:id:kanohk:20171021192409p:plain



GitHub

今回使用したJupyter Notebookは下記においてあります。
github.com