Python DeepLearningに再挑戦 15 誤差逆伝播法 Affine/Softmaxレイヤの実装
概要
Python DeepLearningに再挑戦 15 誤差逆伝播法 Affine/Softmaxレイヤの実装
参考書籍
ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装
- 作者: 斎藤康毅
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/09/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (6件) を見る
Affineレイヤ
ニューラルネットワークの順伝播で行う行列の内積は、幾何学の分野では「ア フィン変換」と呼ばれます。そのため、ここでは、アフィン変換を行う処理を 「Affine レイヤ」という名前で実装していきます。参考書籍より引用
ここでは、重み付き信号の総和の計算レイヤ(行列の内積レイヤ)を実装する。
変数が行列である点が重要。変数の上の数字は、行列の形状。 これまで見てきた計算グラフは、スカラ値がノード間を流れたが、この例では行列がノード間を伝播する。(多分、機械学習、deep learningではこちらがメインになると思われる。)
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)になった点だけらしい。それに伴い全体の形状も変更する。
- 注意すべき点は、バイアスの加算。バイアスは、それぞれのデータに対して加算が行われるため、逆伝播の時には、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レイヤの出力は以下の通り。
affineでスコアが一番高いラベルを推論するだけなら、softmaxはいらないが、各ラベルの確率を求めるときは、softmaxが必要。そのため、ニューラルネットワークの学習時にはsoftmaxで出力する必要あり。
Softmaxレイヤと交差エントロピー誤差を含めて、Softmax-with-Lossというレイヤで実装する。
これはやや複雑で、結論的には以下のようになるらしい。
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
これを計算グラフにすると以下のような図になる。
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が導かれる。
まとめると以下のようなグラフになる!
計算グラフすごい!
結果的に、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
誤差逆伝播法に対応したニューラルネットワークの実装
大まかな手順は、以下の通り。
実装の時のポイント: * 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
ほとんど誤差ないっぽいな!