ゼロから作るDeep Learning 4章「ニューラルネットワークの学習」

ゼロから作るDeep Learning Pythonで学ぶディープラーニングの理論と実装を進めていく. まとめに流れをまとめたので,まず全体像を把握するにはそっちへ.

f:id:miyamo765:20170426230335j:plain:w200

データから学習する

前回は学習が完了しているものとして,ニューラルネットワークによる手書き文字認識を行った. ニューラルネットワークの特徴はデータから学習できる(重みパラメータの値を自動で決定できる)点にある. その方法の理解と実装が今回の目的.

データ駆動

数字の「5」を認識するためのアルゴリズムを考える. 数字の書き方は人によって様々なクセがあるが,人間にとっては簡単に認識できる. しかし,なぜ手書きで書かれた「5」を「5」と脳が認識できているのか,その理由を明確に述べることはできない(直感でわかる,というようなもの). したがって,その認識ルールをプログラムにするのは難しい作業であることがわかる.

そこで,大量のデータを活用した解決法を考える. 一つ考えられるのは,画像から特徴量を抽出し,その特徴量のパターンを機械学習の識別器(SVNやKNN)で学習する方法である. 特徴量とは入力データから抽出された本質的なデータを指し,通常ベクトルで表される. この方法では,特徴量を人間が考え,識別を行う部分は機械学習によって自動化が行われている.

ここで,特徴量までもを「機械」が学習するのがディープラーニング. 生のデータを入力すると,学習を行い,問題のパターンを発見しようと試みる. つまり,上2つの方法(人による認識,特徴量と機械学習による認識)と比較して決定的に異なるのは,データの入力から結果の出力まで,人のアイデアが介在しないという点である.

訓練データとテストデータ

訓練データ(教師データ):学習を行うデータ.これによってパラメータを設定する. テストデータ:訓練データによって生成されたモデルの性能を評価するデータ.

未知のデータに対して識別を行う汎用的な能力(汎化能力)が得られていなければ,実用に値しない. そのため,テスト用のデータセットを用意し,任意のデータに対してどのような性能かを評価する必要がある. (あるデータセットのみに過度に対応した状態を過学習という.)

損失関数

損失関数(loss function)ニューラルネットワークで用いられる最適なパラメータを探索するための指標. ニューラルネットワークの推論の性能の悪さを表す. 一般には2乗和誤差や交差エントロピー誤差などが用いられる. この値が最も小さくなるような重みパラメータを探すのが,ニューラルネットワークの学習である.

なぜ損失関数を設定するのか?

疑問:「認識精度」そのものを指標にすればよいのではないか?

最適なパラメータを探索する際に,損失関数の値ができるだけ小さくなるようなパラメータを探すが,この作業は損失関数を微分することで行っている(詳細は次節). 認識精度を指標にすると,パラメータの微分がほとんどの場所で0になってしまい,パラメータを探す作業が行えない. 認識精度はパラメータの微小な変化にほとんど反応を示さず,もし反応があっても値が不連続に変化するためである. したがって,損失関数という指標を用いる.

2乗和誤差(mean squared error)

2乗和誤差は次のように表される.

{ \displaystyle
E = \frac{1}{2} \sum_k {(y_k-t_k)}^2}
\tag{4.1} \label{4.1}

ここで, {y_k}ニューラルネットワークの出力, {t_k}は教師データを表し, kはデータの次元数を表す. ニューラルネットワークの出力と正解となる教師データの各要素の差の2乗の総和が2乗和誤差. 出力結果の教師データの誤差が小さい場合,より小さい値を出力する.

Pythonで実装してみる.

関数の定義

def mean_squared_error(y, t):
    return 0.5*np.sum((y-t)**2)

前回の手書き文字認識を例として2乗和誤差の値を2パターン計算. 教師データの正解は「2」(one-hot表現).

>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
>>> # パターン1:「2」の割合が最も高い場合
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> mean_squared_error(np.array(y), np.array(t))
0.097500000000000031
>>> # パターン2:「7」の割合が最も高い場合
>>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
>>> mean_squared_error(np.array(y), np.array(t))
0.59750000000000003

パターン1の方がパターン2よりも2乗和誤差の値が小さくなっており,パターン1の出力結果が教師データにより適合していることを示している.

交差エントロピー誤差(cross entropy error)

交差エントロピー誤差は次のように表される.

{ \displaystyle
E = - \sum_k t_k \log y_k}
\tag{4.2} \label{4.2}

 {t_k}は2乗和誤差と同様教師データであり,one-hot表現であるため,\eqref{4.2}は実質的には正解ラベルが1に対応する出力の自然対数を計算するだけになっている. ここで,自然対数のグラフは次のようになっている.

f:id:miyamo765:20170614145006p:plain:w400

これより,正解ラベルに対応する出力の値が大きいほど,交差エントロピー誤差は0に近づくことがわかる.

Pythonで実装してみる.

def cross_entropy_error(y, t):
     delta = 1e-7    #np.log(0)防止
     return -np.sum(t * np.log(y + delta))
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
>>> # パターン1:「2」の割合が最も高い場合
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> cross_entropy_error(np.array(y), np.array(t))
0.51082545709933802
>>> # パターン2:「7」の割合が最も高い場合
>>> y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
>>> cross_entropy_error(np.array(y), np.array(t))
2.3025840929945458

これまでの議論と結果が一致している.

訓練データに対する損失関数を求め,その値ができるだけ小さくなるようにパラメータの値を調整するのが,訓練データを使って学習するということになる.

ミニバッチ学習

訓練データの数は1つだけでないので,損失関数はすべての訓練データを対象として求め,その総和を指標とる. したがって,バッチ処理に対応した形に損失関数を書き換えたい. 訓練データは膨大なので,データの中から一部をランダムに選び出し(ミニバッチとい言う),そのミニバッチごとに学習を行うのが普通(ミニバッチ学習).

NumPyのnp.random.choice()を用いて訓練データの中からランダムに10枚だけ抜き出す. 詳細はここ

#MNISTデータの読み込み
import sys, os
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = \
    load_mnist(normalize=True, one_hot_label=True)

print(x_train.shape)
print(t_train.shape)

#訓練データからランダムに10枚抜き出す
train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

交差エントロピー誤差をミニバッチ学習に対応するように変更する.

def cross_entropy_error(y ,t)
    if y.dim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    batch_size = y.sahpe[0]
    return -np.sum(t * np.log(y)) / batch_size

数値微分

損失関数のパラメータの微小な変化による変化を求めるために,微分を利用する. ここでは,計算機において微分を実現するために数値微分と呼ばれる方法を用いる.

数値微分:微小な差分によって微分の近似値を求めること.微分を解析的に求めることができない場合に用いる.

微分

微分は「ある瞬間」の変化量を表したものので,次の式で定義される. { \displaystyle
\frac{df(x)}{dx} = \lim_{h \to 0} \frac{f(x+h)-f(x)}{h}}
\tag{4.4} \label{4.4}

コンピュータにおいては無限を扱うことはできないので,微分の近似(数値微分)を行いたい.  hの値は10-4程度を用いれば良い結果が得られることがわかっている. 微分の近似として差分を考える.

  • 前進差分

 f(x+h) Teylor展開する.

{ \displaystyle
f(x+h) = f(x) + hf'(x) + \frac{h^2}{2} + \frac{h^3}{6}f^{(3)}(x) + \cdots
}\tag{4.5} \label{4.5}

 hの2乗以上の項を無視して式変形すると,前進差分式が得られる.

{ \displaystyle
f'(x) \simeq \frac{f(x+h)-f(x)}{h}
}\tag{4.6} \label{4.6}

  • 後退差分

 f(x-h) Teylor展開する.

{ \displaystyle
f(x+h) = f(x) + hf'(x) + \frac{h^2}{2} - \frac{h^3}{6}f^{(3)}(x) + \cdots
}\tag{4.7} \label{4.7}

 hの2乗以上の項を無視して式変形すると,前進差分式が得られる.

{ \displaystyle
f'(x) \simeq \frac{f(x)-f(x-h)}{h}
}\tag{4.8} \label{4.8}

  • 中心差分

前進差分と後退差分は, hの2乗以上の項を無視しており,誤差が大きい. より誤差を小さくするために,hの2乗項まで含んだ式を考える. 式\eqref{4.5}と\eqref{4.7}の差をとり式変形すると,中心差分式が得られる.

{ \displaystyle
f'(x) \simeq \frac{f(x+h)-f(x-h)}{2h}
}\tag{4.9} \label{4.9}

ここでは中心差分式を採用し,数値微分を実装する.

def numerical_diff(f, x):
    h = 1e-4
    return ((f(x+h)-f(x-h)) / (2*h)

数値微分の例

偏微分

2変数関数に対しては,その関数の変数が1つだけの関数(もう一方の変数を定数とした関数)を定義して,その関数についてnumerical_diff(f, x)微分を求めることで,偏微分を行うことができる.

{ \displaystyle
f(x_0, x_1) = {x_0}^2+{x_1}^2
}偏微分の例

def function_2(x):
    return x[0]**2 + x[1]**2
  • x_0=3,x_1=4のときのx_0に対する偏微分 \frac{\partial f}{\partial x_0}を求めよ.
>>> def function_tmp1(x0):
...     return x0*x0 + 4.0**2.0
>>> numerical_diff(function_tmp1, 3.0)
6.00000000000378
  • x_0=3,x_1=4のときのx_1に対する偏微分 \frac{\partial f}{\partial x_1}を求めよ.
>>> def function_tmp2(x1):
...     return 3.0**2.0 + x1*x1
>>> numerical_diff(function_tmp2, 4.0)
7.999999999999119

両方とも解析的な微分を行った時の解とほぼ一致していることがわかる.

勾配

各変数の偏微分をまとめて計算する.

勾配{ \displaystyle
(\frac{\partial f}{\partial x_0},  \frac{\partial f}{\partial x_1})
}のように,全ての変数の偏微分ベクトルとしてまとめたもの.

Pythonで実装してみる.

def numerical_gradient(f, x):
    h = 1e-4
    grad = np.zeros_like(x) # xと同じ形状の配列を生成

    for idx in range(x.size):
        tmp_val = x[idx]
        #f(x+h)の計算
        x[idx] = tmp_val + h
        fxh1 = f(x)
        #(x-h)の計算
        x[idx] = tmp_val - h
        fxh2 = f(x)

        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val

    return grad

{ \displaystyle
f(x_0, x_1) = {x_0}^2+{x_1}^2
}の勾配を図で表してみる.

f:id:miyamo765:20170614200204p:plain:w400

各点における勾配は,関数f(x_0, x_1)の一番低い場所を指している. また,その点から離れるほど勾配は大きくなっている. 一般に,勾配が指す方向は,各点において関数の値を最も減らす方向である.

勾配法

数値微分がわかったので,これを用いて損失関数の最小値を探すために勾配法と呼ばれる方法を説明する.

勾配法:勾配を利用して損失関数の最小値を探そうという方針.ただし,勾配の指す先が絶対に最小値だとは限らない(極小値や鞍点である可能性があるため).

  • 勾配法の方針

    1. 現在の場所から勾配方向に一定の距離だけ進む

    2. 移動先で勾配を求め,またその方向に進む

    3. 以上を繰り返し,関数の値を徐々に減らす

勾配法を数式で表すと次のようになる.

{ \displaystyle
x_0 = x_0 - \eta \frac{\partial f}{\partial x_0}
}\tag{4.10} \label{4.10}

{ \displaystyle
x_1 = x_1 - \eta \frac{\partial f}{\partial x_1}
}\tag{4.11} \label{4.11}

 \etaは更新の量を表し,ニューラルネットワークの学習においては学習率(learning rate)と呼ばれる. 式\eqref{4.10},\eqref{4.11}は1回の更新式であり,このステップを繰り返すことで徐々に関数の値を減らしていく. 変数が3つ以上になっても同様. 学習率の値は事前に決める必要があるが,大きすぎても小さすぎてもダメである. 学習率の値を変更しながら,正しく学習できているかの確認作業を行うのが一般的.

勾配降下法を実装してみる.

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x

    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x
  • { \displaystyle f(x_0, x_1) = {x_0}^2+{x_1}^2}の最小値を勾配法で求めよ.
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
array([ -6.11110793e-10,   8.14814391e-10])

初期値(-3.0, 4.0)の最終的な結果は,(6.1e-10, 8.1e-10)となり,ほとんど(0, 0)に近い結果となっている.

ニューラルネットワークに対する勾配

ニューラルネットワークにおける勾配を考えていく.

形状が2×3の重み \mathbf{W}だけを持つニューラルネットワークがあるとし,損失関数をLで表すとする. この場合,勾配は{ \displaystyle \frac{\partial L}{\partial \mathbf{W}}}となる.

$$ \mathbf{W} = \begin{pmatrix} w_{11} & w_{21} & w_{31}\\ w_{12} & w_{22} & w_{32} \end{pmatrix} \tag{4.12} \label{4.12} $$

$$ \frac{\partial L}{\partial \mathbf{W}} = \begin{pmatrix} \frac{\partial L}{\partial w_{11}} & \frac{\partial L}{\partial w_{21}} & \frac{\partial L}{\partial w_{31}}\\ \frac{\partial L}{\partial w_{12}} & \frac{\partial L}{\partial w_{22}} & \frac{\partial L}{\partial w_{32}} \end{pmatrix} \tag{4.13} \label{4.13} $$

simpleNetというクラスを実装して実際に勾配を求めてみる.

import sys, os
sys.path.append(os.pardir)  
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3)

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

print(dW)

simpleNetインスタンス変数として2×3の重みパラメータを持つ. また,予測(ニューラルネットの出力を求める)のためのメソッドpredict(x),損失関数の値を求めるためのメソッドloss(x, t)を持つ.

求められた勾配は次のようになる.

[[ 0.187648    0.02932469 -0.21697269]
 [ 0.28147201  0.04398703 -0.32545904]]               

勾配の絶対値は更新の度合いがどの程度貢献するか,符号は正負どちらの方向に値を更新すべきか(正であれば負,負であればその逆)を表している. 例として,w_{11}は約0.2となっているが,これはw_{11}hだけ増やすと,損失関数の値は0.2hだけ増加するということを意味している. また,w_{23}は約-0.5であるが,これはw_{23}hだけ増やすと,損失関数の値は-0.5hだけ減少するということを意味している. したがって,損失関数を減らすには,w_{11}はマイナス方向へ更新し,w_{23}はプラス方向へ更新するのが良い,ということがわかる. 更新の度合いについては,w_{23}のほうがw_{11}よりも大きく貢献することがわかる.

学習アルゴリズムの実装

これまで述べたようなニューラルネットワークの学習手順をまとめると次のようになる.

  • 前提

ニューラルネットワークの重みとバイアスを訓練データに適応するように調整することを「学習」と呼ぶ. 次の4つの手順で行う.

  1. ミニバッチ

    • 訓練データの中からランダムに一部のデータを選び出す
  2. 勾配の算出

    • ミニバッチの損失関数を減らすために,各重みパラメータの勾配を求める.
  3. パラメータの更新

    • 重みパラメータを勾配方向に微小量だけ更新する.
  4. 繰り返す

    • 1,2,3を繰り返す.

ミニバッチとして無作為に選ばれたデータを使用していることから,確率的勾配降下法(stochastic gradient descent:SGD)と呼ばれる.

2層ニューラルネットワークのクラス

2層のニューラルネットワークをクラスTwoLayerNetとして実装する.

import sys, os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient


class TwoLayerNet:

    def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
        # 重みの初期化
        self.params = {}
        self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

    def predict(self, x):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
    
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        return y
        
    # x:入力データ, t:教師データ
    def loss(self, x, t):
        y = self.predict(x)
        
        return cross_entropy_error(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        t = np.argmax(t, axis=1)
        
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy
        
    # x:入力データ, t:教師データ
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)
        
        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
        
        return grads

paramsニューラルネットワークのパラメータを保持するディクショナリ変数,gradは勾配を保持するディクショナリ変数. メソッド__init__は初期化,predictは認識(推論),lossは損失関数の値の導出,accuracyは認識精度の導出,numerical_gradientは重みパラメータに対する勾配の導出をそれぞれ行う.

ミニバッチ学習の実装とテストデータでの評価

TwoLayerNetクラスで,MNISTデータセットを用いてミニバッチ学習を行う. ミニバッチのサイズを100とし,その100個を対象として勾配を求め,確率的勾配降下法によりパラメータを更新する. ここでの更新回数は10000回としている.

import sys, os
sys.path.append(os.pardir)
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet

# データの読み込み
(x_train, t_train), (x_test, t_test) = \
    load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

iters_num = 10000  # 繰り返しの回数を適宜設定する
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []
# 1エポックあたりの繰り返し回数
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 勾配の計算
    grad = network.gradient(x_batch, t_batch)

    # パラメータの更新
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    # 1エポックごとに認識精度を計算
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

# グラフの描画
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()

テストデータの評価では,学習過程において,定期的に入力データと正解データそれぞれの訓練データとテストデータを対象に認識精度を記録する(メソッドaccuracyを用いる). numerical_gradient内でnumerical_gradientが呼び出されてるのは再帰になるんかな? common.functions内のnumerical_gradientだと関数とパラメータを引数で渡してってのはわかるが,再帰だと第一引数が入力データとなっているので納得できない…

出力結果は次のようになる.

f:id:miyamo765:20170618112319p:plain:w400

学習が進むにつれて,訓練データとテストデータを使って評価した認識精度は両方とも向上していることがわかる.

以上の実装は全て数値微分を用いて行ったが,その計算には時間がかかるため,より高速に求めることのできる誤差逆伝播法を用いるのが一般的である.

まとめ

機械学習では,学習を行うための訓練データと,学習したモデルの汎化能力を確かめるためのテストデータをそれぞれ用いる. ニューラルネットワークの学習には,損失関数という指標を用いる. 損失関数はニューラルネットワークの推論の性能の悪さを表し,この値が最も小さくなるような重みパラメータを探すのが,ニューラルネットワークの学習の目標である. 損失関数の値が最も小さくなる重みパラメータを探すには,損失関数の重みパラメータによる勾配をもとめ,勾配方向に値を徐々に更新していくというやり方をとる. 損失関数を求める際は,訓練データの中からランダムにデータを取り出し(ミニバッチ),複数に分けて行うのが普通. `h 考え方は理解した感あるけど実装に関してはきちんと理解できてきてない感(コードの読み込みが足りてない感)があるな〜という感(感を多様するオタク).

参考文献

第69回 微分・積分の数学 数値微分 [前編] :はじめMath! Javaでコンピュータ数学

はてなブログでtexの数式がうまく表示されない場合の対処法 - INPUTしたらOUTPUT!

7 数値微分,数値積分

ニューラルネットワークの数理

ニューラルネットワークについて学んでみた。(その3) - いものやま。