勾配降下法によるニューラルネットワークの学習の実装

はじめに

勾配降下法によってパラメータを自動学習するニューラルネットワークを実装します.

本記事は,「O'Reilly Japan - ゼロから作るDeep Learning」の第四章の内容を参考にしています.

動作原理

勾配降下法について

前記事の内容を再掲します.

対象とする関数を fとし,関数 fの引数のベクトルを X = (x_{0}, x_{1}, ..., x_{n}) とします.
また, \eta を学習率とします.

まず, fを引数である X = (x_{0}, x_{1}, ..., x_{n}) の各要素で偏微分し,以下のようなベクトルを計算します.
これを,勾配と言います.
 \displaystyle (\frac{\partial f}{\partial x_{0}},  \frac{\partial f}{\partial x_{1}},  ..., 
 \frac{\partial f}{\partial x_{n}})


次に,上で計算した勾配の各要素に学習率を乗じたものを計算します.
 \displaystyle \eta(\frac{\partial f}{\partial x_{0}}),  \eta(\frac{\partial f}{\partial x_{1}}),  ... \eta(\frac{\partial f}{\partial x_{n}})


最後に,全ての  \displaystyle x_{k} (k=0,1,...,n) から,上で計算したものを引き,新たな \displaystyle x_{k}とします.
繰返し回数が \displaystyle t の時の  \displaystyle x_{k} \displaystyle x_{k}^{(t)} と表すとき
  
\displaystyle

\begin{align}
\left\{
\begin{array}{ll}
x_{0}^{(t+1)} =  x_{0}^{(t)} - \eta(\frac{\partial f}{\partial x_{0}^{(t)}}) \\
x_{1}^{(t+1)} =  x_{1}^{(t)} - \eta(\frac{\partial f}{\partial x_{1}^{(t)}}) \\
... \\
x_{n}^{(t+1)} =  x_{n}^{(t)} - \eta(\frac{\partial f}{\partial x_{n}^{(t)}})
\end{array}
\right.
\end{align}
です.

勾配降下法とニューラルネットワーク

ニューラルネットワークは,精度の指標として,「入力を基に予測した結果」と「実際の答え」がどれだけ離れているかを示す損失関数を持ちます.

また,パラメタとして,特徴量の重要度を操作する「重み」発火のしやすさを操作する「バイアス」の2種類を持ちます.

ここでは勾配降下法を用いて,「重み」と「バイアス」を変化させながら,損失関数の出力を徐々に低くしていくことで精度の向上を図ります.

つまり,損失関数を  L ,重みを  W^{(t)} ,バイアスを b^{(t)},学習率を \etaとするとき,

 \displaystyle W^{(t+1)} = W^{(t)} - \eta(\frac{\partial L}{\partial W^{(t)}})
 \displaystyle b^{(t+1)} = b^{(t)} - \eta(\frac{\partial L}{\partial b^{(t)}})

を繰り返し計算し,「重み」と「バイアス」を徐々に変えることで損失関数の出力を最小化します.

実装

プログラムの概要

今回は,入力ノード数3,出力ノード数3の簡単なニューラルネットワークを対象とします.

f:id:Szarny:20171109220225p:plain:w300

期待する出力は,入力ベクトルの要素の中で最も大きな値を持つ要素の添字を1にしたベクトルです.

例えば,入力データとして  [-1.3, 2.0, 1.1] が与えられたとき,2番目の要素が最も大きいので,出力として [0, 1, 0]と返す状態が理想です.

使用する関数

こちら(GitHub - oreilly-japan/deep-learning-from-scratch: 『ゼロから作る Deep Learning』のリポジトリ) のソースコードをお借りしています.

import numpy as np

def cross_entropy_error(y, t):
    """
    交差エントロピー誤差を計算する関数
    
    y: ニューラルネットワークが予測した値
    t: 教師データ
    """
    
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    if t.size == y.size:
        t = t.argmax(axis=1)
             
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size

def softmax(x):
    """
    ソフトマックス関数
    
    x: ニューラルネットワークの出力
    """
    if x.ndim == 2:
        x = x.T
        x = x - np.max(x, axis=0)
        y = np.exp(x) / np.sum(np.exp(x), axis=0)
        return y.T 

    x = x - np.max(x) # オーバーフロー対策
    return np.exp(x) / np.sum(np.exp(x))

def numerical_gradient(f, x):
    """
    数値微分によって勾配を計算する関数
    
    f: 対象となる関数
    x: 偏微分の対象となる変数
    """
    
    h = 1e-4
    grad = np.zeros_like(x)
    
    it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite'])
    while not it.finished:
        idx = it.multi_index
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 値を元に戻す
        it.iternext()   
        
    return grad

ニューラルネットワークを管理するクラス

class NeuralNetwork:
    def __init__(self, input_size, output_size):
        """
        コンストラクタ
        重みとバイアスの初期化を行う
        
        input_size: 入力層のノード数
        output_size: 出力層のノード数
        """
        
        self.W = np.random.randn(input_size, output_size)
        self.b = np.random.randn(output_size)
        
    def predict(self, x):
        """
        予測を行う関数
        
        x: 入力データ
        """
        return softmax(np.dot(x, self.W) + self.b)
    
    def accuracy(self, x, t):
        """
        識別精度を計算する関数
        
        x: 入力データ
        t: 教師データ
        """
        
        y = self.predict(x)
        
        # 行ごとに,最大の値を持つ要素(予測値/答え)の添字を取得する
        y_ans = np.argmax(y, axis=1)
        t_ans = np.argmax(t, axis=1)
        
        return (np.sum(y_ans == t_ans) / float(x.shape[0]))
    
    def loss(self, x, t):
        """
        損失関数の値を計算する関数
        
        x: 入力データ
        t: 教師データ
        """
        y = self.predict(x)
        return cross_entropy_error(y, t)
    
    def numerical_gradient(self, x, t):
        """
        一時的に重み(W)やバイアス(b)を引数に取り,損失を計算する関数を定義することで,
        重み(W)やバイアス(b)の各要素を対象とした偏微分を行い,勾配を計算する関数
        
        x: 入力データ
        t: 教師データ
        """
        L = lambda X: self.loss(x, t)
        
        gradient = {}
        gradient["W"] = numerical_gradient(L, self.W)
        gradient["b"] = numerical_gradient(L, self.b)
        
        return gradient

ハイパーパラメタとデータセットの準備

labels = 3

# データセットの準備
X_train = np.random.randn(10000, labels)
y_train = np.zeros_like(X_train)
for row in range(X_train.shape[0]):
    y_train[row][np.argmax(X_train[row])] = 1
    
X_test = np.random.randn(100, labels)
y_test = np.zeros_like(X_test)
for row in range(X_test.shape[0]):
    y_test[row][np.argmax(X_test[row])] = 1
    
# ハイパーパラメタの準備
iter_num = 10000
train_size = X_train.shape[0]
batch_size = 100
lr = 0.1

network = NeuralNetwork(input_size=labels, output_size=labels)
loss_record = []
accuracy_record = []

ミニバッチ学習

for t in range(iter_num):
    # ミニバッチの取得
    mask = np.random.choice(train_size, batch_size)
    X_batch = X_train[mask]
    y_batch = y_train[mask]
    
    # 勾配の計算
    gradient = network.numerical_gradient(X_batch, y_batch)
    
    # 勾配降下法
    network.W = network.W - (lr * gradient["W"])
    network.b = network.b - (lr * gradient["b"])
    
    loss = network.loss(X_batch, y_batch)
    loss_record.append(loss)
    
    accuracy = network.accuracy(X_test, y_test)
    accuracy_record.append(accuracy)

結果の可視化

import matplotlib.pyplot as plt
%matplotlib inline

print("W: \n{}\n".format(network.W))
print("b: \n{}\n".format(network.b))

plt.figure(facecolor="w")
plt.title("Loss")
plt.xlabel("Iter Num")
plt.ylabel("Loss Function Output")
plt.plot(np.arange(len(loss_record)), loss_record)
plt.show()

plt.figure(facecolor="w")
plt.title("Accuracy")
plt.xlabel("Iter Num")
plt.ylabel("Accuracy")
plt.plot(np.arange(len(accuracy_record)), accuracy_record)
plt.show()

出力結果

W: 
[[ 7.77319572 -2.33339933 -2.41400815]
 [-2.46684567  7.65562501 -2.44128478]
 [-2.09265106 -1.96798037  8.14140202]]

b: 
[ 0.45265394  0.54509058  0.52702847]

上の出力結果を見ると, W の対角線の要素が正で,その他の要素が負であることが分かります.
これは以下の図のように,ニューラルネットワーク同じ高さの特徴量を重視しているためだと考えられます.

f:id:Szarny:20171109221346p:plain:w300

また,下のグラフを見ると,学習を繰り返すにつれ,損失関数の出力が徐々に下がる一方で,ニューラルネットワークによる出力の精度が徐々に向上しているのが分かります.

f:id:Szarny:20171109215539p:plain:w300
f:id:Szarny:20171109215544p:plain:w300

繰返しになりますが,これは勾配降下法を用いて,損失関数の出力が小さくなるように「重み」と「バイアス」を徐々に変更したことによって精度が向上したためです.

まとめ

勾配降下法を用いたニューラルネットワークの学習についてまとめました.
まだ学習途中なので,徐々に固めていきたいです.
次は,誤差逆伝播法を行う予定です.

参考文献

www.oreilly.co.jp