機械学習

ニューラルネットワークのバックプロパゲーションを実装してみた

以下の記事で、バックプロパゲーション(最急降下法を用いて、ニューラルネットワークの誤差を効率的に逆伝播させる手法)により、重み\(\boldsymbol w\)の最適解を求めることを考え、誤差関数\(E=(x_3 – y)^2\)を重みの各変数について偏微分した式を計算してきた。

ニューラルネットワークの誤差関数の微分を計算してみた 以下の記事で、ニューラルネットワークのフォワードプロパゲーションによる出力値\(y\)を算出してきたが、その際、重み\(\bolds...

今回は、上記記事の偏微分を利用して、重みwの最適解を求めるバックプロパゲーションを実装してみたので、そのサンプルプログラムを共有する。

なお、バックプロパゲーションについては、以下のサイトを参照のこと。
https://reinforz.co.jp/bizmedia/5656/

前提条件

以下の記事の実装が完了していること。

ニューラルネットワークのフォワードプロパゲーションを実装してみた ニューラルネットワークは、人間の脳の神経細胞(ニューロン)の仕組みをプログラム上で模したモデルで、機械学習の1手法であるディープラー...

今回実装するニューラルネットワークの全体構成は、以下のようになる。
ニューラルネットワーク

また、重み\(\boldsymbol w\)の最適解は、下図における誤差関数\(E=(x_3 – y)^2\)が最小となる箇所で、これは誤差関数を(桃枠の)各変数について偏微分した結果が\(0\)になる値となる。
ニューラルネットワークの誤差関数

このうち、単一ニューロンを実装した内容は以下の通りで、前提条件の記事に、バックプロパゲーションを行うメソッド(back)と、重みwを返却するメソッド(get_w)を追加している。

import numpy as np

# 単一ニューロン
class OrigNeuron:
    
    # クラス変数
    eta = 0.1  # 学習率η
    
    # 変数の初期化
    def __init__(self):
        # 入力データ(変数x)
        self.x = np.array([])
        # 入力データ(重みw)
        self.w = np.array([])
        # 出力データ(シグモイド関数の変換前)
        self.u = 0
        # 出力データ
        self.y = 0
    
    # 入力データ(変数x、重みw:いずれもNumpy配列)の設定
    def set_input_data(self, x, w):
        if self.__input_check(x, 2) and self.__input_check(w, 3):
            self.x = x
            self.w = w  
        else:
            print("OrigNeuron set_input_data : 引数の指定方法が誤っています")
    
    # フォワードプロパゲーションで出力変数を設定 
    def forward(self):
        if self.__input_check(self.x, 2) and self.__input_check(self.w, 3):
            self.u = self.x[0] * self.w[0] + self.x[1] * self.w[1] + self.w[2]
            self.y = self.__sigmoid(self.u)
    
    # バックプロパゲーションで入力データ(重みw)を変更
    def back(self, dw):
        if self.__input_check(dw, 3):
            # 引数の偏微分を利用して、最急降下法により、重みwの値を更新する
            self.w = self.w - OrigNeuron.eta * dw
    
    # 出力データyを返却
    def get_y(self):
        return self.y
    
    # 重みwを返却
    def get_w(self):
        return self.w
    
    # 入力データの型・長さをチェック
    def __input_check(self, data, size):
        if isinstance(data, np.ndarray) and len(data) == size: 
            if np.issubdtype(data.dtype, float) or np.issubdtype(data.dtype, int):
                return True
            return False
        return False
    
    # シグモイド関数による変換
    def __sigmoid(self, data):
        return 1.0 / (1.0 + np.exp(-data))

なお、上記クラスの「back」メソッドでは、以下の更新式を繰り返している。

最急降下法
出所:最急降下法の仕組みをイラストでわかりやすく解説

また、ニューラルネットワークを実装した内容は以下の通りで、前提条件の記事に、バックプロパゲーションを行うメソッド(back)と、フォワードプロパゲーションとバックプロパゲーションを繰り返すメソッド(repeat_forward_back)を追加している。

import numpy as np

# ニューラルネットワーク
class OrigNeuralNetwork:
    
    # クラス変数
    repeat_num = 10000  # 最急降下法の繰り返し回数
    
    # 変数の初期化
    # 変数x、重みw、重みの微分dw:いずれもNumpy配列 の設定
    # ニューロンの生成と初期値の代入
    def __init__(self, x):
        if self.__input_check(x, 3):
            self.x = x[:-1]    # 入力値x_0,x_1
            self.x_3 = x[-1:]  # 入力値x_3(正解値)
            self.w = np.array([[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]])
            self.dw = np.array([[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 1.0]])
            # ニューロンの生成と初期値の代入
            self.on11 = OrigNeuron()
            self.on11.set_input_data(self.x, self.w[0])
            self.on12 = OrigNeuron()
            self.on12.set_input_data(self.x, self.w[1])
            self.on21 = OrigNeuron()
            self.on21.set_input_data(np.array([self.on11.get_y(), self.on12.get_y()])
                                     , self.w[2])
        else:
            self.x = np.array([])
            print("OrigNeuralNetwork set_input_data : 引数の指定方法が誤っています")
    
    # フォワードプロパゲーションとバックプロパゲーションを繰り返す
    def repeat_forward_back(self):
        for num in range(OrigNeuralNetwork.repeat_num):
            self.forward()
            self.back()

    # フォワードプロパゲーションで出力変数を設定
    def forward(self):
        if self.__input_check(self.x, 2):
            self.on11.forward()
            self.on12.forward()
            self.on21.forward()
    
    # バックプロパゲーションで重み、出力変数を変更
    def back(self):
        if self.__input_check(self.x, 2):
            # 後続の偏微分を計算する際に必要なシグモイド関数の微分を定義
            self.d_on21 = self.on21.get_y() * (1 - self.on21.get_y())
            self.d_on11 = self.on11.get_y() * (1 - self.on11.get_y())
            self.d_on12 = self.on12.get_y() * (1 - self.on12.get_y())
            
            # 後続の偏微分を計算する際の共通部分を定義
            self.d_com = 2 * (self.on21.get_y() - self.x_3[0]) * self.d_on21
            
            # w^1_10, w^1_11, w^1_12についての偏微分を計算し、重みw[0]を更新
            self.dw[0][0] = self.d_com * self.w[2][0] * self.d_on11 * self.x[0]
            self.dw[0][1] = self.d_com * self.w[2][0] * self.d_on11 * self.x[1]
            self.dw[0][2] = self.d_com * self.w[2][0] * self.d_on11
            self.on11.back(self.dw[0])
            self.w[0] = self.on11.get_w()
            
            # w^1_20, w^1_21, w^1_22についての偏微分を計算し、重みw[1]を更新
            self.dw[1][0] = self.d_com * self.w[2][1] * self.d_on12 * self.x[0]
            self.dw[1][1] = self.d_com * self.w[2][1] * self.d_on12 * self.x[1]
            self.dw[1][2] = self.d_com * self.w[2][1] * self.d_on12
            self.on12.back(self.dw[1])
            self.w[1] = self.on12.get_w()
            
            # w^2_10, w^2_11, w^2_12についての偏微分を計算し、重みw[2]を更新
            self.dw[2][0] = self.d_com * self.on11.get_y()
            self.dw[2][1] = self.d_com * self.on12.get_y()
            self.dw[2][2] = self.d_com
            self.on21.back(self.dw[2])
            self.w[2] = self.on21.get_w()
    
    # 出力データを返却
    def get_y(self):
        if self.__input_check(self.x, 2):
            return self.on21.get_y()
        else:
            return None
        
    # 入力データの型・長さをチェック
    def __input_check(self, data, size):
        if isinstance(data, np.ndarray) and len(data) == size: 
            if np.issubdtype(data.dtype, float) or np.issubdtype(data.dtype, int):
                return True
            return False
        return False

なお、backメソッドで利用している重みwの各変数の偏微分については、以下の記事を参照のこと。

ニューラルネットワークの誤差関数の微分を計算してみた 以下の記事で、ニューラルネットワークのフォワードプロパゲーションによる出力値\(y\)を算出してきたが、その際、重み\(\bolds...



さらに、先ほどのニューラルネットワークを呼び出した結果は以下の通りで、出力結果\(y\)は、入力値\(x_0=x_1=0\)または\(x_0=x_1=1\)の場合に\(0\)に近く、そうでない場合は\(1\)に近いことが確認できる。

import numpy as np

# 作成した入力データのフォワード&バックプロパゲーションを実行
input_data = np.array([[0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 0]])
for data in input_data:
    onn = OrigNeuralNetwork(data)
    print("*** 入力データ ***")
    print(data)
    print("*** 出力結果 ***")
    onn.repeat_forward_back()
    print(onn.get_y())
    print()
ニューラルネットワークの呼び出し

また、先ほどのニューラルネットワークを呼び出した際の\(dw\)の最終結果は以下の通りで、それぞれ\(0\)に近づいていることが確認できる。

import numpy as np

# 作成した入力データのフォワード&バックプロパゲーションを実行
input_data = np.array([[0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 0]])
for data in input_data:
    onn = OrigNeuralNetwork(data)
    print("*** 入力データ ***")
    print(data)
    onn.repeat_forward_back()
    print("*** 出力結果 ***")
    print(onn.get_y())
    
    # 小数点以下10桁まで+指数表記しない形式に設定後、dwの最終結果を出力
    np.set_printoptions(precision=10, suppress=True)
    print("*** dwの最終結果 ***")
    print(onn.dw)
    print()
ニューラルネットワーク呼出後のdw

要点まとめ

  • ニューラルネットワークのフォワードプロパゲーションとバックプロパゲーションを繰り返すことで、重み\(\boldsymbol w\)の最適解を求め、正解に近い出力値を求めることができる。