続き。ニューラルネットワークのパラメータ更新を少ない計算量で行うためのテクニックに関しての説明になります。数値微分だとどえらい計算量になるのに対して大幅に軽減できます。
コードは書籍記載のgithubのそれを見ると一発です。
5章 誤差逆伝播法
計算グラフ
偏微分を数式で解くより直感的に理解できる手法として書かれています。順伝播とその微分として戻ってくる逆伝播についてわかりやすく解説があります。
文章ではまとめにくい。
連鎖率
合成関数の微分の話です。
ニューロンは積和の関数なので変数Wがいっぱいあるとしてもほかの変数がただの定数になるので偏微分は掛け算される側が戻るだけ。ってシンプルな構成になることでいったん理解しときます。これだけで記事になります。
逆伝播
加算の逆伝播は1、乗算の逆伝播は重みがそのまま伝播する。乗算の場合はy=axをxで微分すればaが戻る。
乗算・加算レイヤの実装
forwardとbackwardの2つの関数を実装してます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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 class AddLayer: def __init__( self ): pass def forward( self , x, y): out = x + y return out def backward( self , dout): dx = dout * 1 dy = dout * 1 return dx, dy |
乗算はforwardの時に覚えてた値を乗じて戻す。
ReLUレイヤの実装
$$ h(x)=\begin{cases}x & x > 0\\0 & x \leq 0\end{cases} $$
$$ \frac{\partial y}{\partial x}=\begin{cases}1 & x > 0\\0 & x \leq 0\end{cases} $$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Relu: 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 |
Sigmoidレイヤの実装
計算がすこぶるややこしいですが、微分はシンプル。コードもシンプル。
$$ h(x)=\frac{1}{1+e^{-x}} $$
$$ \frac{\partial y}{\partial x}=y(1-y) $$
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Sigmoid: def __init__( self ): self .out = None def forward( self , x): out = sigmoid(x) self .out = out return out def backward( self , dout): dx = dout * ( 1.0 - self .out) * self .out return dx |
Affineレイヤの実装
行列の微分に関しての記載。細かい計算過程は省略されていますが、結果だけ記載があります。多分行列を展開して微分してみるとわかるんじゃないかなぁと思います。
先のニューラルネットの順伝播同様にバッチ処理対応も行列を増やすだけでできるようです。この実装はテンソル対応されていないのでGit上のコードとは異なります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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( 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 |
Softmax-with-Lossレイヤの実装
ソフトマックスとクロスエントロピー誤差のコンビネーションで考えると逆伝播が恐ろしく式がシンプルになって、backwardの実装も簡単になっています。詳細の計算グラフは付録についています。追いかけるのは放棄しました。
なんでこんなややこしい式になっているんだろうと思いましたがこんな理由からのようです。でもなんでバッチサイズで割るんだろう?dxもsumでなくベクトルだろうに。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class SoftmaxWithLoss: def __init__( self ): self .loss = None self .y = None # softmaxの出力 self .t = None # 教師データ(one-hot vector) def forward( self , x, t): self .t = t self .y = softmax(x) self .loss = cross_entropy_error( self .y, self .t) return self .loss def backward( self , dout = 1 ): batch_size = self .t.shape[ 0 ] dx = ( self .y - self .t) / batch_size return dx |
誤差逆伝播法の実装
ステップ1 ミニバッチ
訓練データの中からランダムに一部のデータを選ぶ
ステップ2 勾配の算出
各重みパラメータに関する損失関数の勾配を求める
ステップ3 パラメータの更新
重みパラメータを勾配方向に微小量だけ更新する
ステップ4
ステップ1~3を繰り返す
これを理解しやすくするために抜粋したコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | class TwoLayerNet: def __init__( self , input_size, hidden_size, output_size, weight_init_std = 0.01 ): # 重みの初期化 : # レイヤの生成 self .layers = OrderedDict() self .layers[ 'Affine1' ] = Affine( self .params[ 'W1' ], self .params[ 'b1' ]) self .layers[ 'Relu1' ] = Relu() self .layers[ 'Affine2' ] = Affine( self .params[ 'W2' ], self .params[ 'b2' ]) self .lastLayer = SoftmaxWithLoss() def predict( self , x): for layer in self .layers.values(): x = layer.forward(x) return x # x:入力データ, t:教師データ def loss( self , x, t): y = self .predict(x) return self .lastLayer.forward(y, t) def gradient( self , x, t): # forward self .loss(x, t) # backward dout = 1 dout = self .lastLayer.backward(dout) layers = list ( self .layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) # 設定 : return grads |
ポイントはレイヤをOrderedDictにしている点。predictでfor文でforwardを回せるのと、その逆順でbackwardを回せるのが便利です。predictでは最終レイヤを通しておらず、lossでは通している点もポイントですね。
誤差逆伝播法の勾配の確認
数値微分の計算量に比べて大きく節約できる誤差逆伝播法ですが、じゃあ数値微分がいらないのか?というと役に立つ場面があって、それが検算。デバッグに使えるってことですね。
当然演算誤差が出るので完全一致はしません。
数値微分は重みが10個あったら、20回(f(x-h)とf(x+h)で2回)はネットワークを通さないと勾配がわからないところ、誤差逆伝播法では1回で済むということだと思います。多分。計算量が圧倒的に少ない。
誤差逆伝播法を使った学習
上記のモデルを使った学習のコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | # データの読み込み (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) |
コメント