Szarny.io

There should be one-- and preferably only one --obvious way to do it.

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

はじめに

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

本記事は,「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

Python3で多変数関数の最急降下法を実装

はじめに

ニューラルネットワークについて勉強中です.
その確認ついでに,Python3で多変数関数の最急降下法を実装していきます.

最急降下法

最急降下法とは

関数の傾きを調べることで,その関数の最小値を探索する手法です.
具体的には,傾きが最も急な方向に向かって降下させていき,変数の値を順次変化させることで,関数の出力を最小値に近づけていきます.

再急降下法のアルゴリズム

具体的には,以下の通りです.

対象とする関数を 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}
です.

最急降下法の実装

使用するモジュールのインポート

今回は,numpyを使用します.

import numpy as np

勾配を計算する関数の実装

def calc_gradient(f, X):
    """
    calc_gradient
    偏微分を行う関数
    関数fを変数xの各要素で偏微分した結果をベクトルにした勾配を返す
    
    @params
    f: 対象となる関数
    X: 関数fの引数のベクトル(numpy.array)
    
    @return
    gradient: 勾配(numpy.array)
    """
    
    h = 1e-4
    gradient = np.zeros_like(X)
    
    # 各変数についての偏微分を計算する
    for i in range(X.size):
        store_X = X
        
        # f(x+h)
        X[i] += h
        f_x_plus_h = f(X)

        X = store_X
        
        # f(x-h)
        X[i] -= h
        f_x_minus_h = f(X)
        
        X = store_X
        
        # 偏微分
        gradient[i] = (f_x_plus_h - f_x_minus_h) / (2 * h)
        
    return gradient

最急降下法を行う関数

def gradient_descent(f, X, learning_rate, max_iter):
    """
    gradient_descent
    最急降下法を行う関数
    
    @params
    f: 対象となる関数
    X: 関数fの引数のベクトル(numpy.array)
    learning_rate: 学習率
    max_iter: 繰り返し回数
    
    @return
    X: 関数の出力を最小にする(であろう)引数(numpy.array)
    """
    
    for i in range(max_iter):
        X -= (learning_rate * calc_gradient(f, X))
        print("[{:3d}] X = {}, f(X) = {:.7f}".format(i, X, f(X)))
        
    return X

実験

適切な学習率を設定した場合

繰返しを重ねるごとに,f(X)の値が0に近づいていきます

f = lambda X: X[0]**2 + X[1]**2
X = np.array([3.0, 4.0])
gradient_descent(f, X, learning_rate=0.1, max_iter=100)
[  0] X = [ 2.699995  3.599995], f(X) = 20.2499370
[  1] X = [ 2.4299905  3.2399905], f(X) = 16.4023923
[  2] X = [ 2.18698645  2.91598645], f(X) = 13.2858867
[  3] X = [ 1.9682828  2.6243828], f(X) = 10.7615223
[  4] X = [ 1.77144952  2.36193952], f(X) = 8.7167917
[  5] X = [ 1.59429957  2.12574057], f(X) = 7.0605641
[  6] X = [ 1.43486461  1.91316151], f(X) = 5.7190234
[  7] X = [ 1.29137315  1.72184036], f(X) = 4.6323789
...
[ 50] X = [ 0.01386542  0.01850382], f(X) = 0.0005346
...
[ 96] X = [  5.93079900e-05   9.57433795e-05], f(X) = 0.0000000
[ 97] X = [  4.83771910e-05   8.11690415e-05], f(X) = 0.0000000
[ 98] X = [  3.85394719e-05   6.80521374e-05], f(X) = 0.0000000
[ 99] X = [  2.96855247e-05   5.62469236e-05], f(X) = 0.0000000

array([  2.96855247e-05,   5.62469236e-05])
学習率が低すぎた場合

降下する量が少なすぎて,Xの値がほとんど変わりません

f = lambda X: X[0]**2 + X[1]**2
X = np.array([3.0, 4.0])
gradient_descent(f, X, learning_rate=0.001, max_iter=100)
[  0] X = [ 2.99699995  3.99599995], f(X) = 24.9500243
[  1] X = [ 2.9940029  3.9920039], f(X) = 24.9001485
[  2] X = [ 2.99100885  3.98801185], f(X) = 24.8503724
[  3] X = [ 2.98801779  3.98402378], f(X) = 24.8006958
[  4] X = [ 2.98502972  3.98003971], f(X) = 24.7511185
[  5] X = [ 2.98204464  3.97605962], f(X) = 24.7016403
[  6] X = [ 2.97906255  3.97208351], f(X) = 24.6522611
[  7] X = [ 2.97608343  3.96811138], f(X) = 24.6029805
...
[ 50] X = [ 2.85076078  3.8010152 ], f(X) = 22.5745536
...
[ 96] X = [ 2.72253126  3.63004322], f(X) = 20.5893902
[ 97] X = [ 2.71980868  3.62641313], f(X) = 20.5482314
[ 98] X = [ 2.71708882  3.62278666], f(X) = 20.5071549
[ 99] X = [ 2.71437168  3.61916383], f(X) = 20.4661604

array([ 2.71437168,  3.61916383])
学習率が高すぎた場合

降下する量が多すぎて,Xの値が発散してしまっています

f = lambda X: X[0]**2 + X[1]**2
X = np.array([3.0, 4.0])
gradient_descent(f, X, learning_rate=5.0, max_iter=100)
[  0] X = [-12.00025 -16.00025], f(X) = 400.0140001
[  1] X = [ 48.00075  64.00075], f(X) = 6400.1680012
[  2] X = [-192.00325001 -256.00325001], f(X) = 102402.9120322
[  3] X = [  768.01275031  1024.0127495 ], f(X) = 1638445.6957638
[  4] X = [-3072.05125096 -4096.05124272], f(X) = 26215134.6715417
[  5] X = [ 12288.20484725  16384.20472615], f(X) = 419442142.8762379
[  6] X = [-49152.81880054 -65536.81989439], f(X) = 6711074357.9075851
[  7] X = [ 196611.24598828  262147.29646914], f(X) = 107377187095.1434631
...
[ 50] X = [  1.23730419e+13   1.24100600e+13], f(X) = 307101753022579397827231744.0000000
...
[ 96] X = [  1.23730419e+13   1.24100600e+13], f(X) = 307101753022579397827231744.0000000
[ 97] X = [  1.23730419e+13   1.24100600e+13], f(X) = 307101753022579397827231744.0000000
[ 98] X = [  1.23730419e+13   1.24100600e+13], f(X) = 307101753022579397827231744.0000000
[ 99] X = [  1.23730419e+13   1.24100600e+13], f(X) = 307101753022579397827231744.0000000

array([  1.23730419e+13,   1.24100600e+13])

まとめ

最急降下法アルゴリズムとその実装について紹介しました.

このアルゴリズムは,ニューラルネットワークのパラメタの最適化に利用されています.
具体的には,ニューラルネットワークによる出力を理想状態に近づけるために,実際の出力と理想の出力の誤差を算出する誤差関数の値を最小化するために利用されています.

「人工知能 ― 機械といかに向き合うか」 を読んだ

本のリンク

www.diamond.co.jp

感想

人工知能にまつわる話(主に経済面)についての論文8本が書籍化されている本で,単刀直入にとても面白いです.論文が元ということもあり,興味深い考察や将来の展望が多分に含まれています.

以下の内容について知りたい方には,非常におすすめです.

特に興味深かった章について,以下で簡単に書きます.

オーグメンテーション:人工知能と共存する方法

人工知能と雇用・労働の関係について書かれてある章です.

最近,人工知能が仕事を奪う」といったことが各メディアで広く伝えられています.

この章は,そういった内容から一歩踏み込んで,どのような職業が代替可能なのか,人工知能が労働に入り込んできた時どのように対処すれば良いのか,どのような心構えでいればよいのかを体系的に示してあります.

特に,人工知能は「労働」や「雇用」といったパイを人間と取り合う存在ではなく,人間とコラボレーションしながら私たちの「労働」をオーグメンテーション(拡張)するものだという指摘は興味深いと感じました.

人工知能はビジネスをどう変えるか

人工知能をビジネスにどう生かすのか,について書かれてある章です.

人工知能は,現状どのような問題を解決することが可能/不可能なのかそれによって事業の流れやマネジメントはどのように変革していくのかについて詳しく知ることができます.

特に,

経営資源は,「ヒト・モノ・カネ」から「ヒト・データ・キカイ」へ

P64 より抜粋

という一文は,人工知能ビッグデータといった新興の情報技術が,ビジネスに対していかに影響を与えているかを如実に示していると感じました.

ディープラーニングで日本のモノづくりは復権する

人工知能の将来とディープラーニングの発展が及ぼす影響について書かれた章です.

人工知能の歴史と展望が分かりやすくまとめられていて,人工知能はどのような歴史を経てきたのか」や「X年後にはどのようなことが可能になり,それがどう応用されるのか」といったことを詳しく知ることができます.

そして,現在の流行の主観となる技術であるディープラーニングによって,日本の産業やモノづくりにどのような影響を与えるのかを示してくれます.

あなたの上司がロボットに代わったら

ロボティクスの章です.

人工知能がロボットに搭載され,それらが単なるツールから共同作業を行うチームメイトに変革するようになったことで,私たちの心理にどのような影響を及ぼすのかを知ることができます.

特に,擬人化によって人工知能に対する信頼度が増したりアルゴリズムよりも主観を信用しやすい人間の心理について言及してある部分は,非常に面白いです.

どのような手法をとれば,人工知能やロボットに対する人間の心理的な抵抗感を取り除くことができるのかを詳しく記述してあり,おすすめの章です.