機械学習の基礎まとめ【勾配降下法を利用したニューラルネットワークの学習】

今回は勾配降下法を利用したニューラルネットワークの学習の仕組みをPythonで実装しながら確認します。

 

また、ニューラルネットワークが学習によって推論の精度が上がる過程も同時に確認します。

 

 

この記事を書いている僕はシステムエンジニア6年目

 

普段はJavaでWebアプリを作ったりSQL書いたり・・・、

なので最近流行りのPython、数学、人工知能、デープラーニングができる人には正直憧れています。。。。

 

 

自分も一から勉強してこの辺りできるようになりたい、、画像認識モデルを作ったりして、アプリに組み込みたい!

これが機械学習、深層学習の勉強を始めたきっかけでした。

 

 

体系的に、この分野の基礎から学ぼうとJDLAのG検定の勉強をして合格するところまでいきました。

次のステップとして、

実際にPythonでコードを書きながら機械学習や深層学習の知識を深めているところです。。。

 

 

今回は勾配降下法を利用したニューラルネットワークの学習についてまとめてみました。

 

この記事を読む前に

以下の記事を読んで勾配降下法、ニューラルネットワークの推論については事前に抑えておいてください。

機械学習の基礎まとめ【偏微分と勾配降下法】

 

機械学習の基礎まとめ【ニューラルネットワークの推論(順方向への伝播)】

 

 

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

 

 

まず、ニューラルネットワークの重み\(W\)(\(2 \times 3\)の形状)だけを持つニューラルネットワークについて考えます。

損失関数を\(L\)で表すと、

勾配は\(\frac{ \partial L }{ \partial W }\)と表すことができます。

 

数式で表すと、

$$W = \begin{pmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \end{pmatrix}$$

$$\frac{ \partial L }{ \partial W } = \begin{pmatrix} \frac{ \partial L }{ \partial w_{11} } & \frac{ \partial L }{ \partial w_{12} } & \frac{ \partial L }{ \partial w_{13} } \\ \frac{ \partial L }{ \partial w_{21} } & \frac{ \partial L }{ \partial w_{22} } & \frac{ \partial L }{ \partial w_{23} } \end{pmatrix}$$

 

\(\frac{ \partial L }{ \partial W }\)の各要素は、それぞれの重みの要素に関する偏微分から構成されます。

 

例えば、\(\frac{ \partial L }{ \partial w_{11} }\)は、\(w_{11}\)を少し変化させると損失関数\(L\)がどれだけ変化するかを表します。

また、\(\frac{ \partial L }{ \partial W }\)の形状は\(W\)と同じ形状(\(2 \times 3\))になります。

 

 

シンプルなニューラルネットワークの勾配をPythonで計算すると、

Python
import sys, os
sys.path.append('/content/drive/My Drive/deep-learning-from-scratch')
import numpy as np

# 勾配を計算する
def numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    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


# softmax関数
def softmax(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 cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
        
    # 教師データがone-hot-vectorの場合、正解ラベルのインデックスに変換
    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] + 1e-7)) / batch_size



# シンプルなニューラルネットワーク
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



# ニューラルネットワークを作成する
net = simpleNet()
print("重みパラメータ:\n" + str(net.W) + "\n")

# ニューラルネットワークで推論する
x = np.array([0.6, 0.9])
p = net.predict(x)
print("ニューラルネットワーク推論結果:\n" + str(p) + "\n")
print("最大値のインデックス:\n" + str(np.argmax(p)) + "\n")

# 損失関数を計算する
t = np.array([0, 0, 1])
print("教師データ正解ラベル:\n" + str(t) + "\n")
print("損失関数出力結果:\n" + str(net.loss(x, t)) + "\n")

# 重みの勾配を求める
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)
print("重みパラメータの勾配\n" + str(dW) + "\n")

 

 

Pythonのソースコードでは、ざっくりと以下のような流れで処理を行って勾配を計算しています。

  1. simpleNet()で形状\(2 \times 3\)の重み\(W\)だけを持つニューラルネットワークを作成
  2. ニューラルネットワークで推論を行う
  3. 推論結果から損失関数の値を求める
  4. 損失関数の値から重み\(W\)の勾配を求める

 

重みパラメータの勾配を見ると、

例えば、\(\frac{ \partial L }{ \partial w_{11} }\)は、およそ0.05で、

\(w_{11}\)を少し増やすと損失関数は増えるので、マイナスの方向に値を更新する必要があるということが分かります。

 

また、\(\frac{ \partial L }{ \partial w_{22} }\)を見ると、

およそ0.50で、この重みの変化は、\(\frac{ \partial L }{ \partial w_{11} }\)よりも損失関数の増減に大きな影響を与えることが分かります。

 

このように、ニューラルネットワークでは損失関数の重みに関する勾配を求めることで、

各要素の重みをどのように更新すれば損失関数が減るか??

各要素の重みの変化が損失関数の変化にどれくらい影響するのか??

を知ることができるのです。

 

 

勾配降下法を利用したニューラルネットワークの学習

 

 

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

まず、今回は2層ニューラルネットワークで学習(重みとバイアスを最適値に更新)を行います。

 

今回扱うニューラルネットワークは、

手書き数字(画像データ)を識別でき、入力層(784)、隠れ層(100)、出力層(10)とします。

 

 

2層ニューラルネットワークのプログラムは以下のようになります。

 

Python
import sys, os
sys.path.append('/content/drive/My Drive/deep-learning-from-scratch')
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist


class TwoLayerNet:

    # 重みの初期化を行う
    # input_size:入力層のニューロンの数, hidden_size:隠れ層のニューロンの数, output_size:出力層のニューロンの数
    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

 

上のTwoLayerNetクラスの特徴としては以下があります。

  • paramsという、重み(W1、W2)とバイアス(b1、b2)を持つ。
  • gradsという、重みに対する勾配(W1、W2)とバイアスに対する勾配(b1、b2)を持つ。
  • predict()関数で推論が行える。
  • loss()関数で損失関数の計算が行える。
  • numerical_gradient()関数で勾配の計算が行える。

 

 

学習アルゴリズム

ニューラルネットワークの学習手順は以下です。

ステップ1(ミニバッチ)

訓練データ(60,000個)の中からランダムに一部のデータ(100個)を取り出す。その選ばれたデータをミニバッチと言い、そのミニバッチの損失関数の値を減らすことを目的とする。

 

ステップ2(損失関数の計算)

ニューラルネットワークの出力と訓練データの正解ラベルから損失関数を計算する。

 

ステップ3(勾配の算出)

ミニバッチの損失関数を減らすために、各重みパラメータの勾配を求める。

 

ステップ4(パラメータの更新)

重みパラメータを勾配方向に微小量だけ更新する。

 

ステップ5(繰り返す)

ステップ1〜ステップ4を10,000回繰り返す。

 

ざっくりと流れを絵にすると、

 

 

この方法はミニバッチとして無作為に選ばれたデータを使用していることから、

確率的勾配降下法(SGD:stochastic gradient descent)と呼ばれます。

 

プログラムは以下です。

Python
import sys, os
sys.path.append('/content/drive/My Drive/deep-learning-from-scratch/')
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist


# データの読み込み
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
# 2層ニューラルネットワークを生成(入力層:784、隠れ層:50、出力層:10)
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 = []

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.numerical_gradient(x_batch, t_batch)
    # 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)
    
    # エポック毎にニューラルネットワークの認識精度を計算する
    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))

 

 

MEMO
エポック(epoch)とは単位を表し、

1エポックは学習において訓練データを全て使い切った時の回数に対応します。

今回だと60,000個の訓練データをミニバッチ100個ずつ使うので600回繰り返すと1エポックになります。

 

ちなみに、

学習過程における損失関数の値の変化と、

ニューラルネットワークの訓練データとテストデータに対する精度をグラフにすると以下になりました。

 

 

左は誤差関数の変化を表しており、学習の過程で値が小さくなっていることがわかります。

右は重なっていますが、、

エポック毎のテストデータと訓練データに対するニューラルネットワークの精度を表しており、

学習の過程で値が大きくなっていることがわかります。

 

まとめ

 

 

今回はニューラルネットワークの学習の仕組みについて確認しました。

確率的勾配降下法を使ってニューラルネットワークの認識精度を高めることができました。

ニューラルネットワークの学習の流れをおさらいすると、

 

  1. 訓練データの中からランダムに一部のデータを取り出す。(ミニバッチ)
  2. ニューラルネットワークの出力と訓練データの正解ラベルから損失関数を計算する。
  3. ミニバッチの損失関数を減らすために、各重みパラメータの勾配を求める。
  4. 重みパラメータを勾配方向に微小量だけ更新する。
  5. 1〜4を繰り返す

 

次は、勾配降下法よりも高速に学習できる誤差逆伝播法について見ていきたいと思います。

 

 

参考にした資料

 

 

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください