第 8 章 深層学習の基礎のキソ 76
8.4 誤差逆伝播法
確率的勾配降下法
確率的勾配降下法は先ほどのバッチ学習の欠点を解消したものです。バッチ学習が訓練データ1つ1つの誤差関数(En(w))の 和E(w)の勾配∇E で重みを更新するのに対して、確率的勾配降下法は訓練データから1つのデータを取り出し、そのデータの 誤差関数(En(w))の勾配 ∇En によって以下のように重みを更新します。
w←w−η∇En
確率的勾配降下法はバッチ学習の欠点を解消したものではありますが欠点はあります。まず、ランダムにデータを取り出してい るため最短で最適解にたどり着くわけではありません。ただ、1回あたりの計算量が少ないので結果的に早い場合もあります。次 に、データを1つずつ利用しているので、異常なデータ(いわゆる外れ値)に引きずられやすいです。さらに学習係数の設定に失敗 すると劇的に悪化する場合があります。
ミニバッチ学習
ミニバッチ学習はバッチ学習と確率的勾配降下法の「良いとこどり」をしたような学習アルゴリズムです。確率的勾配降下法で はデータ1つずつ重みを更新していましたが、ここではいくつかのデータの集合(これをミニバッチと呼ぶ)の単位で重みを更新し ていきます。具体的には以下のように重みを更新していきます。
w←w−η 1 N
∑N n
∇En
ここで、 N はミニバッチとして取り出したデータの数を表しています。ミニバッチのサイズは 10~100あたりにするのが一般 的です。
8.4 誤差逆伝播法
前節で誤差関数とそれを最小化する手法を紹介しました。その更新式をそのまま実装しても当然求めたい重みは求まりますが、
何度も何度も誤差関数を微分して重みを更新するという計算を繰り返すのは計算コストがかかります。そこで誤差逆伝播法の出番 です。愚直に微分していては重みの成分1つごとに微分する必要があります。それに対して、誤差逆伝播法では出力層に近い層か ら順に重みの微分を求めて行くのですが、その際に前回の計算結果を利用するので計算コストがあまりかからないというわけです。
抽象的な話になってしまいましたので、具体的な説明をします。ここからは数式ばかり登場します。
連鎖律
微分の連鎖律について記述します。まず、1変数関数f, g について、以下が成立します。
df dx = df
dg dg dx 次に多変数関数f と1変数関数g について、以下が成立します。
∂f
∂x =∑
k
∂f
∂g
∂g
∂x
証明は省きます(筆者は連鎖律の証明を理解していません。精進不足で申し訳ありません)
成分ごとの計算
最終的には行列で計算を行いますが、説明の準備として成分ごとの計算について記述します。
まず、誤差関数をE とした時、 ∂E
∂w(l)ji
について考えます。記号の右肩についている(l)は第l 層の重みであることを表します。
(l) (l)
第8章 深層学習の基礎のキソ 8.4誤差逆伝播法
∂E
∂w(l)ji
= ∂E
∂u(l)j
∂u(l)j
∂w(l)ji
が成り立ちます。次にこの式の右辺の左側部分から考えていきます。
上の図のように、u(l)j はzj(l) を通してu(l+1)k に影響を与えます。したがって、
∂E
∂u(l)j
=∑
k
∂E
∂u(l+1)k
∂u(l+1)k
∂u(l)j
が成り立ちます。ここで、右辺にも ∂E
∂u(k·)
と同じ形が現れていることに着目して、
δj(l)= ∂E
∂u(l)j
と定義します。すると先ほどの式は
δj(l)=∑
k
δ(l+1)k ∂u(l+1)k
∂u(l)j
と書き直すことができます。さらに、u(l+1)k =∑
mw(l+1)km zm(l)=∑
mwkm(l+1)f(u(l)m)なので∂u(l+1)k
∂u(l)j
=w(l+1)kj f′(u(l)j )であるか ら、さらに
δ(l)j =∑
k
δ(l+1)k wkj(l+1)f′(u(l)j )
と書き直せます。次に ∂u(l)j
∂w(l)ji
について考えます。
第8章 深層学習の基礎のキソ 8.4誤差逆伝播法
上の図のように、u(l)j はzi(l−1)w(l)ji (i= 1,2 . . .)の和として表せます。すなわち u(l)j =∑
i
z(li−1)wji(l)
であるので、容易に
∂u(l)j
∂w(l)ji
=z(li−1)
と求まります。ここで初めの式に戻ると
∂E
∂w(l)ji
=δ(l)j zi(l−1)
これが求める微分です。この式を見ると、微分がただの積で表せています。さらに、δ は先ほどの式のように、繰り返し計算す ることができるので計算量コストを抑えることができます。
また、 δを求める際、最後の出力層(これを第 L層とします)における δk(L)が必要になります。ですがこれは
δ(L)k = ∂E
∂u(L)k
と容易に計算することができます。交差エントロピーを誤差関数とし、出力層の活性化関数をSoftmax関数にした場合、出力層 におけるδは
δ(L)k =−∂∑
mtmlogym
∂u(L)k
=−
∂∑
mtm
expu(L)m
∑
iexpu(L)i
∂u(L)k
=. . .=yk−tk
途中計算は省略しました。やるだけなので頑張ってください。
行列による計算
先ほどの成分計算を行列に計算に当てはめます。重み wji (ただしここではバイアスを含めない)(j, i) 要素にもつ行列をW、 ユニットj のバイアスをj 番目の要素にもつベクトルをbと表します。
入力されたデータを X、この入力における第l 層のユニットに対する総入力をU(l) 、この総入力を活性化関数に通した出力を 並べたベクトルをZ(l)とします。このとき、 Z(1)=Xとして、順伝播計算は以下のようになります。
第8章 深層学習の基礎のキソ 8.4誤差逆伝播法
U(l)=W(l)Z(l−1)+b(l) Z(l)=f(l)(U(l))
f(l)(·)は行列の各成分に活性化関数を適応して、元の行列と同じ形の行列を出力するものとします。これらは2節で登場した式 を行列で表したという理解で良いです。
さて、次に逆伝播の計算について考えます。前節の δ(l)j を要素にもつ行列を∆(l) とします。∆(l) の各列は各ミニバッチに対応 しています。逆伝播の計算は以下のようになります。
∆(l)=f(l)′(U(l))⊙(W(l+1)T∆(l+1))
ここで、 ⊙という記号は行列の成分ごとの積を意味します。例を挙げると、 A=aij, B =bij について、A⊙B の(i, j)成分 はaijbij となります。出力層を第L層とすると、
∆(L)=Y−T ただし、 YはXに対する出力Yで、Tは目標となる出力です。
最後に ∆(l)を用いた重みの更新式で勾配を計算します。重みwji(l)についての誤差関数 ∑N
k EnWの微分を第(j, i)成分に持つ 行列をW(l)′ 、バイアスb(l)j についての微分を第j 成分に持つベクトルをb(l)′ とします。するとこれらは次のように計算でき ます。
W(l)′= ∆(l)Z(l−1)T b(l)′ = ∆(l)
この2つを用いると、重みは以下のように更新できます。
W(l)←W(l)−ηW(l)′ b(l)←b(l)−ηW(l)′
python で実装
さて、これまでのことを踏まえて実装していきます。今回はMNISTという手書き数字のデータセットを使います。このデータ セットの読み込み方はここでは説明しませんが、ベンチマークとしてしばしば使われるものなのでインターネットにたくさん情報 があります。MNISTは「手書き数字」なので0 ~9の10クラスの分類になります。この程度の分類問題だと深層にする意味が特 にないので3層のパーセプトロンになります(複数層あるし実質深層ですって)(これは罠で本気の深層はもっと深イ)。中間層の活 性化関数としてReLU関数を、出力層の活性化関数としてSoftmax関数を採用します。
誤差逆伝播法の説明では、一気にデルタを求めるという記述をしていましたが、実装では「活性化関数の順伝播・逆伝播計算を 行うレイヤー」と「行列(出力と重みの計算)の順伝播・逆伝播計算を行うレイヤー」と役割を分けて考えます。つまり、ある層の デルタを求める時は活性化関数の逆伝播の計算を行ってから行列の逆伝播の計算をするということです。こういうように役割を分 けるのには理由があります。活性化関数を変更した時にいちいちコードを書き直すのは手間がかかるので、活性化関数ごとのレイ ヤーを用意して、変更したい時はレイヤーを変更するだけでよくなるからです。さらに、逆伝播の計算途中の状態を取り出すこと も容易にすることが可能になります。ちなみに計算グラフというもので誤差逆伝播法を考えると、このレイヤー分けはとても自然 な流れで登場します。計算グラフについての説明は省きますが、今までのような数式だらけではないので分かりやすいと思います。
いよいよコードです。僕が1から作ったコードを載せることができれば良いのですが、「ゼロから作るDeep Learning」という本 のコードがあまりに素晴らしすぎて超えることなどできないので、そのコードの実装する上で必要な部分だけ載せます。計算グラ フについてはこの本によくまとめられています。レイヤー分けもこの本に書いてあるので読んでみてください。
まずは各関数です。ReLU関数に関しては処理が簡単なので、わざわざ関数としては実装していません。
List 8.9: functions.py
importnumpy as np defsoftmax(x):
if x.ndim == 2:
x=x.T
第8章 深層学習の基礎のキソ 8.4誤差逆伝播法
x=x -np.max(x,axis=0)
y=np.exp(x) / np.sum(np.exp(x), axis=0) returny.T
x= x-np.max(x)
return np.exp(x) /np.sum(np.exp(x)) defcross_entropy(y,t):
if y.ndim== 1:
t= t.reshape(1, t.size) y= y.reshape(1, y.size) if t.size== y.size:
t= t.argmax(axis = 1) batch_size =y.shape[0]
return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
■コラム : Softmax 関数のミニバッチ対応版
Softmax関数に少し追加している部分がありますね。これはミニバッチ学習に対応させたからです。ミニバッチを用いた場
合、データはミニバッチのデータ数が行数、1つのデータが列ベクトルであるような行列です。変更を加える前のものは1つ のデータに対する計算しかできません。変更後は複数のデータに対して同時に計算する機能が追加されているのでミニバッチ 学習用の関数となっています。
次にレイヤーです。Affineというレイヤーが行列の演算を行います。ReluLayer、SoftmaxLossLayerはそれぞれ活性化関数の 順伝播・逆伝播計算を行います。
List 8.10: all_Layers.py
importnumpy as np fromfunctions import* class ReluLayer:
def __init__(self):
self.mask= None def forward(self,x):
self.mask= (x <= 0) out =x.copy() out[self.mask] = 0 return out
def backward(self, dout):
dout[self.mask] = 0 dx =dout
return dx class AffineLayer:
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(self.x, self.W) +self.b return out
def backward(self, dout):
dx =np.dot(dout, self.W.T) self.dW =np.dot(self.x.T, dout) self.db =np.sum(dout,axis= 0) return dx
class SoftmaxLossLayer:
def __init__(self):
self.loss= None self.y =None self.t =None def forward(self,x,t):
self.t =t
self.y =softmax(x)
第8章 深層学習の基礎のキソ 8.4誤差逆伝播法
return self.loss def backward(self, dout= 1):
batch_size=self.t.shape[0]
dx = (self.y- self.t) /batch_size return dx
そして逆伝播・順伝播を制御するコードです。順伝播計算を行った後、逆伝播を行い、重みを更新する量を求めます。地味です
が、OrderedDictが逆伝播するときに役立っています。
List 8.11: network.py
importnumpy as np fromfunctions import* fromall_layersimport *
fromcollections importOrderedDict 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)
self.layers =OrderedDict()
self.layers["Affine1"] =AffineLayer(self.params["W1"], self.params["b1"]) self.layers["Relu1"] = ReluLayer()
self.layers["Affine2"] =AffineLayer(self.params["W2"], self.params["b2"]) self.lastlayer =SoftmaxLossLayer()
def predict(self,x):
for layerin self.layers.values():
x=layer.forward(x) return x
def loss(self,x,t):
y= self.predict(x)
return self.lastlayer.forward(y,t) def gradient(self, x, t):
self.loss(x,t) dout= 1
dout= self.lastlayer.backward(dout) layers =list(self.layers.values()) layers.reverse()
for layerin layers:
dout= layer.backward(dout) grads = {}
grads["W1"] = self.layers["Affine1"].dW grads["b1"] = self.layers["Affine1"].db grads["W2"] = self.layers["Affine2"].dW grads["b2"] = self.layers["Affine2"].db return grads
最後にこれらを利用してニューラルネットに学習させます。ここではミニバッチ学習を行っています。バッチサイズは100です。
List 8.12: train.py
importnumpy as np
frommn.mnist importload_mnist fromnetworks importTwoLayerNet
(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 fori 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.gradient(x_batch,t_batch) for key in ("W1","b1","W2","b2"):
network.params[key] -=learning_rate *grad[key]