16 Deep Learning 逆伝播 2

16.3  リンゴの例

最初に、リンゴなどを買う例の計算グラフを紹介した。リンゴのみに着目すると、
下図のようになる。

この場合の変数は、リンゴの値段、リンゴの個数、消費税である。これら3つの変数それぞれが、最終的な支払金額にどのように影響するかということである。左端の3つの灰色の数字が最終的な支払金額220にどのように影響するか、を表す。「支払金額のリンゴの値段に関する偏微分」、「支払金額のリンゴの個数に関する偏微分」、「支払金額のリンゴの消費税に関する偏微分」を求めることに相当する。これを計算グラフの逆伝播を使って解くと上図のようになる。すなわち、「支払金額のリンゴの値段に関する偏微分」は2.2、「支払金額のリンゴの個数に関する偏微分」は110、「支払金額のリンゴの消費税に関する偏微分」は200である。消費税のスケールとリンゴの値段と個数のスケールは違うので、消費税の影響を表す値が大きくなっている。(消費税の1は100%、リンゴの個数、値段は1個、1円)

前述した通り、乗算ノードの逆伝播では入力信号と”ひっくり返った”積が下流へ流れる。加算ノードはそのまま下流へ流れる。暇な方は下図の空欄を埋めてほしい。

16.4  単純なレイヤの実装

この上のリンゴの買い物の例をPythonで実装する。ここでは、計算グラフの乗算ノードを「乗算レイヤ(MulLayer)」、加算ノードを「加算レイヤ(AddLayer)」という名前で実装する。

16.4.1  乗算レイヤの実装

レイヤはforward()とbackward()という共通のメソッド(関数あるいはインターフェース)を持つように実装する。forward()は順伝播(順方向の伝播)、backward()は逆伝播ん(逆方向の伝播)に対応する。乗算レイヤは下記のように実装できる。

class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out

    def backward(self, dout):
        dx = dout * self.y
        dy = dout * self.x

        return dx, dy

__init__(self)は他言語のコンストラクタに対応し、インスタンス変数の初期化を行う。コンストラクタやインスタンスとは何かとはプログラミングに興味があれば調べてほしい。
forward()では、x、yの2つの引数を受け取り、それらを乗算して出力する。一方、backward()では上流から伝わってきた微分、あるいは信号(dout: d out)に対して、順伝播の”ひっくり返した値”を乗算して下流に流す。
このクラスを使って、下図を実装する。

# coding: utf-8


class MulLayer:
    def __init__(self):
        self.x = None
        self.y = None

    def forward(self, x, y):
        self.x = x
        self.y = y                
        out = x * y

        return out

    def backward(self, dout):
        dx = dout * self.y
        dy = dout * self.x
        
        return dx, dy

apple = 100
apple_num = 2
tax = 1.1

mul_apple_layer = MulLayer() # 1st layer
mul_tax_layer = MulLayer() # 2nd layer

# forward
apple_price = mul_apple_layer.forward(apple, apple_num)
price = mul_tax_layer.forward(apple_price, tax)

# backward
dprice = 1
dapple_price, dtax = mul_tax_layer.backward(dprice)
dapple, dapple_num = mul_apple_layer.backward(dapple_price)

print("price:", int(price))
print("dApple:", dapple)
print("dApple_num:", int(dapple_num))
print("dTax:", dtax)

16_simplelayer.pyとして上記のコードを保存する。その後、コマンドプロンプトで実行すると下記の出力が得られる。

$python 16_simplelayer.py

上図の灰色の数字とdで始まる変数の値が一致していることに注目されたい。

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

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