機械学習の基礎【誤差逆伝播法でニューラルネットワークを実装する】

今回は誤差逆伝播法でニューラルネットワークを1から実装します。

ニューラルネットワークの実装を通じて誤差逆伝播法を理解することを目的としています。

 

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

 

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

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

 

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

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

 

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

次のステップとして、

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

 

今回は、

誤差逆伝播法でニューラルネットワークを実装することで誤差逆伝播法の理解を深めます。

 

前提事項

以下の内容を理解していることを前提とします。

機械学習の基礎【誤差逆伝播法の仕組みを計算グラフで理解する】

 

機械学習の基礎【誤差逆伝播法でニューラルネットワークのいろんなレイヤを実装する】

 

 

誤差逆伝播法の実装

 

 

前回、誤差逆伝播法でRelu、Sigmoid、Affine、Softmaxレイヤを実装しました。

機械学習の基礎【誤差逆伝播法でニューラルネットワークのいろんなレイヤを実装する】

 

これらを組み合わせることで、ニューラルネットワークを構築していきます。

 

 

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

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

 

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

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

 

 

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

 

Python
import sys, os
sys.path.append('/content/drive/My Drive/deep-learning-from-scratch')
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict


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)

        # レイヤの生成
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])

        self.lastLayer = SoftmaxWithLoss()
        
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        
        return x
        
    # x:入力データ, t:教師データ
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 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
        
    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)
        
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads

 

上のTwoLayerNetクラスの特徴をまとめると以下です。

  • paramsという、重み(W1、W2)とバイアス(b1、b2)を持つ。
  • layersという、順番付きディクショナリ変数(Affine1、Relu1、Affine2)を持つ
  • lastlayerというニューラルネットワーク最後のレイヤ(SoftmaxWithLoss)を持つ
  • gradsという、重みに対する勾配(W1、W2)とバイアスに対する勾配(b1、b2)を持つ。
  • predict()関数で推論が行える。
  • loss()関数で損失関数の計算が行える。
  • numerical_gradient()関数で数値微分によって勾配の計算が行える。
  • gradient()関数で誤差逆伝播法によって勾配の計算が行える。

 

実装に関しては、

layersをOrderedDictとして順番を保持してディクショナリに追加しており、

各レイヤのforward()やbackword()を順番に呼ぶことでニューラルネットワークの順伝播や逆伝播によって、

推論や学習に必要な勾配を求めることができます。

 

また、レイヤの層を深く複雑にしたい場合は、

layersに追加する層を追加することで簡単に複雑なニューラルネットワークが実装できます。

これはレイヤのモジュール化(機能を分割して組み合わせられる部品のように実装すること)による実装の大きな利点です。

 

 

学習アルゴリズム

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

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

訓練データ(60,000個)の中からランダムに一部のデータ(100個)を取り出す。

 

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

各重みパラメータに関する勾配を求める。←ここで誤差逆伝播法を利用する

 

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

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

 

ステップ4(繰り返す)

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

 

ざっくりと絵にすると、

 

 

勾配降下法を利用する際は、ステップ2(勾配の算出)で勾配を求めるために数値微分を利用しましたが、

数値微分は簡単に実装できる反面、計算に多くの時間がかかりました。

誤差逆伝播法を用いれば、高速に効率よく勾配を求めることができます。

 

MEMO

数値微分

実装が簡単だが、勾配の算出に時間がかかる。

誤差逆伝播法が正しいかチェックのために数値微分の結果と誤差逆伝播法の結果を比較するのに利用されることがある

 

誤差逆伝播法

実装が複雑だが、勾配を効率よく高速に計算できる。

 

では、最後に誤差逆伝播法を利用した学習処理を実装します。

 

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

import numpy as np
from dataset.mnist import load_mnist
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict


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)

        # レイヤの生成
        self.layers = OrderedDict()
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])

        self.lastLayer = SoftmaxWithLoss()
        
    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        
        return x
        
    # x:入力データ, t:教師データ
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)
    
    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 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
        
    def gradient(self, x, t):
        # forward
        self.loss(x, t)

        # backward
        dout = 1
        dout = self.lastLayer.backward(dout)
        
        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 設定
        grads = {}
        grads['W1'], grads['b1'] = self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'] = self.layers['Affine2'].dW, self.layers['Affine2'].db

        return grads


# データの読み込み
(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 = []

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))

# グラフの描画
markers = {'train': 'o', 'test': 's'}
x1 = np.arange(len(train_loss_list))
x2 = np.arange(len(train_acc_list))


# Figureオブジェクトを作成
fig = plt.figure(figsize=(12, 6))

# 1×2のfigureにオブジェクトを追加
ax = fig.add_subplot(1, 2, 1)
bx = fig.add_subplot(1, 2, 2)


# サブプロットaxのグラフのx軸、y軸にラベルを設定する
ax.set_xlabel("iteration")
ax.set_ylabel("loss")
bx.set_xlabel("epochs")
bx.set_ylabel("accuracy")


# データx, yをグラフにプロットする
ax.plot(x1, train_loss_list)
bx.plot(x2, train_acc_list, label='train acc')
bx.plot(x2, test_acc_list, label='test acc', linestyle='--')

plt.show()

 

実行結果は以下のようになりました。

 

 

実行結果を見ると、勾配降下法で実装した時と同じような結果になっており、

学習を繰り返すことで訓練データに対する認識精度、テストデータに対する認識精度が高くなり、

誤差関数の値が小さくなっていることがわかります。

 

 

まとめ

 

 

今回は誤差逆伝播法を利用したニューラルネットワークの実装を行いました。

以下の点については抑えておきましょう。

 

  • 誤差逆伝播法は勾配の計算を効率よく高速に行える
  • 数値微分と誤差逆伝播法の結果を比較することで誤差逆伝播法の実装が正しいことをチェックできる
  • ニューラルネットワークの構成要素をレイヤとしてモジュール化することで勾配を効率的に求められる

 

 

参考にした資料

 

 

コメントを残す

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

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