Djangoroidの奮闘記

python,django,angularJS1~三十路過ぎたプログラマーの奮闘記

Python DeepLearningに再挑戦 15 誤差逆伝播法 Affine/Softmaxレイヤの実装

概要

Python DeepLearningに再挑戦 15 誤差逆伝播法 Affine/Softmaxレイヤの実装

参考書籍

Affineレイヤ

ニューラルネットワークの順伝播で行う行列の内積は、幾何学の分野では「ア フィン変換」と呼ばれます。そのため、ここでは、アフィン変換を行う処理を 「Affine レイヤ」という名前で実装していきます。参考書籍より引用

ここでは、重み付き信号の総和の計算レイヤ(行列の内積レイヤ)を実装する。

f:id:riikku:20161224094538p:plain

変数が行列である点が重要。変数の上の数字は、行列の形状。 これまで見てきた計算グラフは、スカラ値がノード間を流れたが、この例では行列がノード間を伝播する。(多分、機械学習、deep learningではこちらがメインになると思われる。)

f:id:riikku:20161224100042p:plain

WTのTは転置を表すらしく、Wの(i,j)の要素を(j,i)に変換するらしい。

例えば、以下のような感じらしい。

W =
([w11   w21   w31],
 [w12   w22   w32],)

WT = 
([w11, w12],
 [w21, w22],
 [w31, w32])

X と ∂L/∂X、Wと∂L/∂W は同じ形状。行列の計算では、形状がかなり重要になってくる。 逆伝播の計算でも、形状を合わせた形で、実装を組み立てる必要がある。

バッチ版Affineレイヤ

上記のAffineレイヤは、Xが1つのデータだったが、これをバッチ版に変更する。 違いは、Xの形が(N,2)になった点だけらしい。それに伴い全体の形状も変更する。

f:id:riikku:20161224101839p:plain

  • 注意すべき点は、バイアスの加算。バイアスは、それぞれのデータに対して加算が行われるため、逆伝播の時には、axisを0番目として、総和を求める。(これはちょっとよくわかってないので、手法だけを覚えておく)
#入力がテンソル(4次元のデータ)の場合も考慮した実装
class Affine:
    def __init__(self, w,b):# インスタンス変数の初期化
        self.W = W # 重みは固定
        self.b = b # バイアスも固定
        self.x = None
        self.dW = None
        self.db = None
        
    def forward(self, x):
        self.x = x #入力を引数で渡す。
        out = np.dot(x, self.W) + self.b # バイアス+重みx入力の行列を渡す
        
        return out

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)#重みの形状の転置を行なって、それをdoutでdotする。
        self.dW = np.dot(self.x.T, dout)# 入力の形状の転置を行なって、それをdoutでdotする。
        self.db = np.sum(dout, axis=0)#バイアスはaxis=0で微分する。
        
        return dx

うーん、これはむずいw

Softmax-with-Lossレイヤ

出力層のソフトマックス関数の実装。ソフトマックス関数は、入力された値を正規化して出力する。手書き文字認識の場合は、Softmaxレイヤの出力は以下の通り。

f:id:riikku:20161224112825p:plain

affineでスコアが一番高いラベルを推論するだけなら、softmaxはいらないが、各ラベルの確率を求めるときは、softmaxが必要。そのため、ニューラルネットワークの学習時にはsoftmaxで出力する必要あり。

Softmaxレイヤと交差エントロピー誤差を含めて、Softmax-with-Lossというレイヤで実装する。

f:id:riikku:20161225082320p:plain

これはやや複雑で、結論的には以下のようになるらしい。

f:id:riikku:20161225083119p:plain

Softmax-with-Loss レイヤの逆伝播の結果は、(y1-t1, y2-t2, y3-t3) となる。

Softmax-with-Lossの順伝播

Softmax の数式

yk = exp(ak) / nΣ(i=1) exp(ai)

Cross Entropy Error の数式

L = - Σ(k) tk log yk

これを計算グラフにすると以下のような図になる。

f:id:riikku:20161225085011p:plain

Softmax レイヤのポイント: * 分母に当たる exp(ai)の和をSとしている。 * 最終的な出力はy1,y2,y3としてある。

Softmax-with-Lossの逆伝播

Cross Entropy Errorの逆伝播のポイント:

  • Lの部分の逆伝播は、1。(∂L/∂L = 1のため)
  • x ノードの逆伝搬では、入力値をひっくり返した値を上流からの微分に乗算して下流に流す。
    • ノードでは、上流から伝わる微分をそのまま流す。
  • log ノードの逆伝播は、`y = logx, ∂y/∂x = 1/x' となる。
  • 最終的に、(-t1/y1, -t2/y2, -t3/y3)になる。

Softmaxレイヤの逆伝播のポイント: * x ノードでは、順伝播の値をひっくり返して乗算する。

y1 = exp(a1)/S、また、exp(a1)が順伝播の対の値、上流から流れてきた微分が - t1/y1のため、以下の通り置き換えられる。
- t1/y1 * exp(a1) = -t1 * (S/exp(a1)) * exp(a1) = -t1S
  • 今回の/ ノードのように、順伝搬の時に複数に枝分かれした場合は、それらの逆伝播の値が加算される。(-t1S, -t2S, -t3S) が加算され、その加算された値に対して、「/」の逆伝播を行う。その結果は、1/S(t1+t2+t3)になり、t1~t3は、one-hotベクトルのため、どれか正解の1つが1、それ以外は0になっているため、t1 + t2 + t3=1と置き換えられる。そのため、ここは、1/S となる。 ただ、そもそも / ノードの逆伝播がよくわかっていないので、もう一度復習が必要。

  • 「+」ノードはそのまま流すだけ。

  • 「x」ノードはひっくり返して乗算。 結論的に、-t1 / exp(a1) となる。

y1 = exp(a1)/Sのため、
1/S * (-t1/y1) = 1/S * -t1/ (exp(a1)/S) =  1/S * (-t1 * S / exp(a1)) = -t1 / exp(a1)  
  • 「exp」ノードは、以下の関係式が成り立つ。y = exp(x), ∂y/∂x = exp(x)変わらないのか〜。そのため、枝別れした2つの入力の和にexp(a1)かけた値が逆伝播の値になる。
exp(a1) * ( 1/S - t1/exp(a1) ) = exp(a1)/S - t1 
y1 = exp(a1)/S であることから、y1-t1が導かれる。

まとめると以下のようなグラフになる!

f:id:riikku:20161225093533p:plain

計算グラフすごい!

結果的に、Softmaxの逆伝播は、y1-t1,y2-t2, y3-t3という綺麗な値になる。 ニューラルネットワークの逆伝播では、この差分である誤差が前レイヤに伝わっていく。 これは偶然ではなく、そうなるように交差エントロピー誤差が設定されているらしい。

「ソフトマックス関数」の損失関数として「交差エントロピー誤差」を用いると、 逆伝播が (y1 − t1, y2 − t2, y3 − t3) という“キレイ”な結果になりました。 実は、そのような“キレイ”な結果は偶然ではなく、そうなるように交差エント ロピー誤差という関数が設計されたのです。また、回帰問題では出力層に「恒 等関数」を用い、損失関数として「2 乗和誤差」を用いますが(「3.5 出力層の 設計」参照)、これも同様の理由によります。つまり、「恒等関数」の損失関数 として「2 乗和誤差」を用いると、逆伝播が (y1 − t1, y2 − t2, y3 − t3) とい う“キレイ”な結果になるのです。>>参考書籍より引用

SoftmaxWithLossのレイヤの実装は以下の通り。

import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
from common.functions import softmax, cross_entropy_error

class SoftmaxWithLoss:
    def __init__(self):#インスタンス変数の初期化
        self.loss = None # 損失
        self.y = None # softmaxの出力
        self.t = None # 教師データ
        
    def forward(self, x, t):
        self.t = t # 教師データの代入
        self.y = softmax(x) # softmax関数の出力
        self.loss = cross_entropy_error(self.y, self.t) # 交差エントロピー誤差の出力 引数には、softmax関数の出力
        
        return self.loss #交差エントロピー誤差のインスタンス変数を返す。
    
    def backward(self, dout=1):
        batch_size = self.t.shape[0] #教師データの数を取得する
        dx = (self.y - self.t) / batch_size #教師データの数で割り算する。
        
        return dx

誤差逆伝播法に対応したニューラルネットワークの実装

大まかな手順は、以下の通り。

f:id:riikku:20161225133724p:plain

実装の時のポイント: * 2層のニューラルネットワークをTwoLayerNetとして実装する。 * ニューラルネットワークのレイヤをOrderedDictで保持する。順伝播、逆伝播を実装しやすい。 * 数値微分は、誤差伝播法の正しさを確認するために使う。数値微分は、実装が簡単なため。 * 数値微分と誤差伝播法の結果を確認する作業を勾配確認という。

import sys, os
sys.path.append(os.pardir)
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) # W1入力層->第1層の重み
        self.params['b1'] = np.zeros(hidden_size)# 入力層->第1層のバイアス
        self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)# W2第1層->出力層の重み
        self.params['b2'] = np.zeros(output_size)# 第1層->出力層のバイアス
        
        # レイヤの生成
        self.layers = OrderedDict()#OrderedDict = 項目が追加された順序を記憶する辞書のサブクラス
        self.layers['Affine1'] = Affine(self.params['W1'], self.params['b1'])# Affine1 入力層->第1層 重みバイアスで出力
        self.layers['Relu1'] = Relu() # Relu関数で、0以上のものだけを出力する。a->zのところ
        self.layers['Affine2'] = Affine(self.params['W2'], self.params['b2'])# Affine2 重みバイアスで出力
        
        self.lastLayer = SoftmaxWithLoss() # SoftmaxWithLoss()クラスのインスタンス
    
    #
    def predict(self, x):
        for layer in self.layers.values():# OrderedDictクラスインスタンスのvalues()メソッドでリストを表示
            x = layer.forward(x)#Affin1.forward(x), Relu1.forward(x), Affine2.forward(x) でAffin2までを行う。
            
        return x # 最終的にfor構文が終わった時には、Affine2.forward(x)の結果になっているはず。
    
    # x:入力データ、t:教師データ
    def loss(self, x, t):# 損失関数
        y = self.predict(x)#predictが出力した値をyに代入
        return self.lastLayer.forward(y, t) # yを元に、損失関数を出力
    
    # 認識精度を割り出す。
    def accuracy(self, x, t):
        y = self.predict(x) #predict関数の結果
        y = np.argmax(y, axis=1) # argmaxのラベルの数字を抜き出して配列にする。
        if t.ndim != 1 : t = np.argmax(t, axis=1) # 教師データのargmaxのラベルの数字を抜き出して配列にする。
            
        accuracy = np.sum(y == t) / float(x.shape[0])  # y と tが一致している確率を算出する。
        return accuracy
    
    # x:入力データ、t:教師データ #これは数値微分かな。
    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)# def loss_W(W): return 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'])
    
    def gradient(self, x, t):
        # forward
        self.loss(x, t) #損失関数のメソッド は、return self.lastLayer.forward(y, t)つまり、loss関数を計算して代入するだけなはず。
        
        # backward
        dout = 1 #損失関数->出力層への勾配 ∂L/∂Lで1
        dout = self.lastLayer.backward(dout) # 出力層の逆伝播を求める。
        
        layers = list(self.layers.values())#layersをリストにする。
        layers.reverse()# 逆の順番にする。
        for layer in layers:
            dout = layer.backward(dout) #各レイヤーの逆伝搬の値を算出する。
            
        grads ={}
        grads['W1'] = self.layers['Affine1'].dW # 各レイヤ関数に、self.dW , self.dbが組み込まれているので、backwardを実行すると各変数に代入される。
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db
        
        return grads

勾配確認の実装は以下の通り。

import sys, os
sys.path.append(os.pardir)  # 親ディレクトリのファイルをインポートするための設定
import numpy as np
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)

x_batch = x_train[:3]
t_batch = t_train[:3]

grad_numerical = network.numerical_gradient(x_batch, t_batch)
grad_backprop = network.gradient(x_batch, t_batch)

for key in grad_numerical.keys():
    diff = np.average( np.abs(grad_backprop[key] - grad_numerical[key]) )
    print(key + ":" + str(diff))

# 結果

b2:1.19682040667e-10
b1:9.3332133756e-13
W1:2.50826652379e-13
W2:9.6885424412e-13

ほとんど誤差ないっぽいな!