16 Deep Learning 逆伝播

逆伝播とはなにか?なんのために存在するのか?と疑問に思うのは当然である。
結論から言うと、順伝播のほかに、逆伝播が、勾配の更新に必要だからである。
順伝播だけだと、計算量が異常に増えるため、逆伝播が必要だという認識のもと、
この写経を読んでいただきたい所存である。

16.1 加算ノードの逆伝播

数式z=x+yに対して、逆伝播を考える。z=x+yの微分を解析的に計算すると以下のようになる。

\[ \begin{array}{l} \frac{\partial z}{\partial x} = 1 \\ \frac{\partial z}{\partial y} = 1 \tag{16.1} \end{array} \]

上式(16.1)の通り、dz/dxとdz/dyは、ともに1となる。

計算グラフで表すと下図のようになる。

この例では、上流から伝わった微分をdL/dzとしたが、最終的にLを出力する大きな計算グラフを想定している。

分かりにくいので具体例を示す。10+5=15という計算があるとして、上流から1.3の値が流れてくるとする。これを計算グラフで書くと下図のようになる。

加算ノードの逆伝播は入力信号を次のノードへ出力するだけなので、上図のように1.3をそのまま次の(逆方向の)ノードへ流す。

16.2 乗算ノードの逆伝播

乗算ノードの逆伝播について説明する。ここでは、z=xyという式を考える。この式の解析的な微分は、次式(16.2)で表される。

\[ \begin{array}{l} \frac{\partial z}{\partial x} = y \\ \frac{\partial z}{\partial y} = x \tag{16.2} \end{array} \]

乗算の逆伝播の場合は、上流の値に、順伝播の際の入力信号を”ひっくり返した値”を乗算して下流へ流す。

乗算の逆伝播は、入力信号をひっくり返した値を乗算するので、1.3*5 = 6.5、
1.3*10=13とそれぞれ計算できる。

加算の逆伝播では、上流の値をただ下流に流すだけだったので、順伝播の入力信号の値は必要なかったが、一方乗算の逆伝播では、順伝播のときの入力信号の値が必要になる。このため、乗算ノードの実装時には、順伝播の入力信号を保持する。

 

15 Deep Learning 連鎖律 計算グラフの逆伝播

15.1  計算グラフの逆伝播

上図に示すように、逆伝播の計算手順は信号Eに対して、ノードの局所的な微分dy/dxを乗算し、それを次のノードへ伝達していく。
ここで言う、局所的な微分とは順伝播でのy=f(x)の微分dy/dxを求めることになる。
y=f(x)=x^2 のとき dy/dx = 2xになる。
そして、この局所的な微分を上流から伝達された値(上図ではE)に乗算して、前のノードへ渡していく。

これが、逆伝播で行う計算手順だが、この計算を行うことで、目的とする微分の値を効率よく求めることができる。これは後述する連鎖律の原理から説明できる。

15.2 連鎖律

合成関数についてまず説明する。合成関数とは複数の関数によって、構成される関数のことである。たとえば、

\[z = (x+y)^2 \]

上式は式(15.1)のように、2つの式で構成される。

\[ \begin{array}{l} z = t^2 \\ t = x+y \tag{15.1} \end{array} \]

連鎖律とは合成関数の微分についての性質であり、次のように定義される。

ある関数が合成関数で表される場合、その合成関数の微分は、合成関数を構成するそれぞれの関数の微分の積によって表すことができる。

難しく見えるが、単純に言うと分数の積と同様に計算ができるということである。

dz/dx(xに関するzの微分)はdz/dt(tに関するzの微分)とdt/dx(xに関するtの微分)の積によって表すことができる。

\[ \frac{\delta z}{\delta x} = \frac{\delta z}{\delta t} \frac{\delta t}{\delta x} \tag{15.2} \]

上図のように、分数の積の計算のようにできる。

式(15.2)の微分dz/dxを求めるときは、まず局所的な微分(偏微分)を求める。

\[ \begin{array}{l} \frac{\delta z}{\delta t} = 2t \\ \frac{\delta t}{\delta x} = 1 \tag{15.3} \end{array} \]

よって、最終的に求めたいdz/dxは式(15.3)で求めた微分の積によって計算できる。

\[ \frac{\delta z}{\delta x} = \frac{\delta z}{\delta t} \frac{\delta t}{\delta x} = 2t・1 = 2(x + y) \tag{15.4} \]

15.3  連鎖律と計算グラフ

**2というノードは2乗を表すとする。x+y=tとして順方向に伝播する。さらにt^2 = z と順方向に伝播する。dz/dz は先ほどは省略していたが、連鎖律の原理により、dz/dz=1である。上図で注目すべきは、一番左の逆伝播の結果である。

dz/dz dz/dt dt/dx = dz/dx となり、「xに関するzの微分」に対応する。

上図に式(15.3)を代入すると下図のようになる。

dz/dx = 2t = 2(x+y) となる。

14 Deep Learning 計算グラフ

「馬鹿よ貴方は」のネタで、算数の問題がある。
例えば、以下のようである。

問 太郎君はスーパーでリンゴを2個、みかんを3個買いました。リンゴは1個100円、みかんは1個150円です。消費税が10%かかるものとして、支払う金額を求めなさい。

この問は「金額の数値」を問うているが、何故問うているかは不明である。
たとえば、教室で算数の時間で先生が問いを発しているならわかる。生徒に計算能力をつけるため、国の施策に従い、給料をえるために発している問いである。

しかし、「馬鹿よ貴方は」のファラオさんが、いきなりこの問いを発すると途端に訳が分からなくなる。
太郎君は何歳なのか? スーパーでフルーツだけ買って帰ることがあるのか? 金額を求めなさいと言われているのは、おそらくレジの人であろう。
レジの人はバーコードを打つだけだろうし、手計算や暗算などはしないだろう。ではレジのコンピュータに向かってレジの人が求めなさいといっているのか?
音声認識システムのレジをわざわざ作る必要はまったくないので、このレジの人は独語癖があると考えられる。しかもなぜレジの人は、太郎君だと名前を知っているのか?
そもそも日本円で消費税が10%とは未来の話をしているのか?などと、掘り下げるといくらでもできるが、問い自体に意味はない。
これが人生に意味がないというハイデガーやニーチェのニヒリズムや仏教の空と同一であるとするのはいささか強引に思われるかもしれないが、ほとんど同義だと私は考える。

誤差伝播法で使用する計算グラフで問いを解くと下図のようになる。

分かりやすい問題を難しくすることは、このように容易であるが、問題を簡潔にするのは難しい。

今回は簡単なグラフの説明なので、余談が多めであった。

13 Deep Learning 学習アルゴリズムの実装

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

ステップ1 ミニバッチ

訓練データの中からランダムに一部のデータを選びだす。その選ばれたデータをミニバッチと言い、ここでは、このミニバッチの損失関数の値を減らすことを目的とする。

ステップ2 勾配の算出

ミニバッチの損失関数を減らすために、各重みパラメータの勾配をもとめる。勾配は、損失関数の値を最も減らす方向を示す。

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

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

繰り返す

ステップ1,2,3を繰り返す。

ここで使用するデータはミニバッチとして無作為に選ばれたデータを使用していることから、確率的勾配降下法(stochastic gradient descent)と呼ばれる。
省略してSGDと呼ばれることもある。

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

2層(隠れ層が1層)のネットワークを一つのクラスとして実装する。

実装は以下。

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
        
    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

13.2 ミニバッチ学習の実装

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 = []

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'}
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()

下図のグラフが出力される。

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

今更ながら、外部ファイルの関数を使用する方法を見つけたので記しておく。

他のファイルの中で定義された関数は、import文で読み込んでから使います。たとえば、「myhelper.py」というPythonコードの中で定義されたmyfunc1という関数を使いたい場合は

from myhelper import myfunc1 

myfunc1(x)

と書くことで使うことができます。構文は「from [ファイル名] import [関数名]」です。

他のコードの中で定義された関数をまとめてimportしたい場合は

from [ファイル名] import *

とすることで一括でimportできます。ただ、このように何をimportするのかを明示しない場合には名前が衝突してしまう可能性もあるので注意が必要です。

ファイル全体をimportする場合には、fromは使わず、importの対象に直接ファイル名を指定する次のような使い方も可能です。

import myhelper

myhelper.myfunc1(x)

引用元:LIFE WITH PYTHON Python関数の使い方

ニューラルネットワークの学習においても、勾配を求める必要がある。ここで言う勾配は、重みパラメータに関する損失関数の勾配である。たとえば、形状が 2 × 3 (2行3列) の重みWだけを持つニューラルネットワークがあり、損失関数をLで表す場合を考えると、この場合、勾配はδL/δW と表すことができる。実際に数式で表すと次のようになる。

\begin{equation}\begin{array}{c} W = \left( \begin{array}{ccc} w_{11} & w_{21} & w_{31} \\ w_{12} & w_{22} & w_{32} \end{array} \right) \\
\frac{\partial L}{\partial W} = \left( \begin{array}{ccc} \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{array} \right) \tag{12.2} \end{array}
\end{equation}

δL/δWの各要素は、それぞれの要素に関する偏微分から構成される。
まず、重みのWを初期化し、入力xと重みWの内積predict()の出力から損失関数の値を求める。

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

実装すると以下のようになり、重みの勾配dWは次のように出力される。