ディープラニングの学習の肝である誤差逆伝播法またはバックプロパゲーション（ Backpropagation）（以後、誤差逆伝播法）について、ちょっとした実験をしてみます。
目次
- 誤差逆伝播法は「微分」だと知った
- 普通の算術微分でも学習できるのか確かめる
- 確認用ミニニューラルネットワークプログラムの補足説明
- 実行してみたら普通の微分でも学習はできるとわかった
- ついでに微分についてPythonプログラムで復習する
- おわりです
##### 誤差逆伝播法は「微分」だと知った
誤差逆伝播法はディープラーニングで、学習時のウエイトやバイアスを更新する勾配の値を求めるのに使います。
すっごい難しく複雑な技術のような印象をうけるのですが、実はそんなことはなくて、単なる微分なんだけど、普通にやったら遅くて使い物にならないから、微分を高速に計算できる方法として工夫されたものが「誤差逆伝播法」らしい・・というのを、こちらのサイトで知りました。
説明をよーく見ると、誤差逆伝播法の説明の前に微分の「連鎖律」を説明されていて。
連鎖律とは、複数の関数によって構成される関数の微分は、それぞれの関数の微分の積によって表すことができるといった性質のことです。
後ろの方には
各重みパラメータに関する損失関数の勾配を、連鎖律を利用して求める
とか
出力から入力側に向かって順に学習パラメータを更新していくことから、誤差逆伝播法と呼ばれ、各パラメータの偏微分の計算を効率化します。
などと書かれています。
整理すると、学習時に各重みパラメータを更新に使う勾配を求める方法は「微分」なんですけど、普通に微分計算（算術微分）すると、計算効率が悪く恐ろしく遅い。
なので、効率的かつ高速に微分計算ができるよう工夫した方法が「誤差逆伝播法」なんだということのようです。
目から鱗・・とは、まさにこのことでした。
自分にとっては。
##### 普通の算術微分でも学習できるのか確かめる
誤差逆伝播法は、微分を高速に計算できるように工夫した方法である。
これが正しいかどうか試すには、誤差逆伝播法を使わず、普通の算術微分で学習できるかをやってみればよいと考えました。
tensorflowなどのフレームワークを使わず、numpyだけでニューラルネットワークモデルを作って、算術微分だけを使って学習させてみます。
Pythonのソース全文です。
import numpy as np # サンプルデータの生成クラス class Data: def __init__(self, size): self.rasio = 10 self.size = size def data_create(self, num, padding): datasets = [] labels = [] for i in range(1, num + 1): rt = i % 2 if (rt == 0): datasets.append(np.random.randn(self.size)) else: datasets.append([((i + padding) % 100) / self.rasio for k in range(self.size)]) labels.append(rt) return np.array(datasets), np.array(labels) # 誤差逆伝播法を使わない「なんちゃってニューラルネットワーク」サンプルクラス class SampleNet: 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 = self.params['W1'] W2 = self.params['W2'] b1 = self.params['b1'] b2 = self.params['b2'] # Affine層 a1 = np.dot(x, W1) + b1 # Sigmoid層 z1 = 1 / (1 + np.exp(a1)) # Affine層 a2 = np.dot(z1, W2) + b2 # Softmax層 a2t = a2.T a2t = a2t - np.max(a2t, axis=0) yy = np.exp(a2t) / np.sum(np.exp(a2t), axis=0) y = yy.T return y # x:入力データ, t:教師データでロス率を求める def loss(self, x, t): y = self.predict(x) batch_size = y.shape[0] # クロスエントロピーです。 return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size # 正解率を求める def accuracy(self, x, t): y = self.predict(x) y = np.argmax(y, axis=1) accuracy = np.sum(y == t) / float(x.shape[0]) return accuracy # x:入力データ, t:教師データ def gradient(self, x, t): def loss_W(W): return self.loss(x, t) grads = {} grads['W1'] = self.__bibun_np__(loss_W, self.params['W1']) grads['b1'] = self.__bibun_np__(loss_W, self.params['b1']) grads['W2'] = self.__bibun_np__(loss_W, self.params['W2']) grads['b2'] = self.__bibun_np__(loss_W, self.params['b2']) return grads # 行列を微分する関数 def __bibun_np__(self, f, x): h = 1e-4 # 0.0001 grad = np.zeros_like(x) it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite']) while not it.finished: idx = it.multi_index tmp_val = x[idx] x[idx] = float(tmp_val) + h fxh1 = f(x) # f(x+h) x[idx] = float(tmp_val) - h fxh2 = f(x) # f(x-h) grad[idx] = (fxh1 - fxh2) / (2 * h) x[idx] = tmp_val it.iternext() return grad iters_num = 300 # 繰り返しの回数を適宜設定する test_size = 200 size = 256 # データを生成する。奇数を1000で割った数字G=1、偶数を1000で割ったG=0 としてカテゴライズ。 d = Data(size) x_train, t_train = d.data_create(iters_num, 0) x_test, t_test = d.data_create(test_size, size + 1) # ネットワークを構築する network = SampleNet(input_size=size, hidden_size=64, output_size=2) 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(int(iter_per_epoch * 5)): 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] # 更新したパラメータで推論を行いロス率を求める loss = network.loss(x_batch, t_batch) train_loss_list.append(loss) # 更新したパラメータでの推論結果を評価する 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( "loss:" + str(loss) + "/train acc:" + str(train_acc) + "/test acc:" + str(test_acc))
ちょっと長いですが、ご容赦を。
##### 確認用ミニニューラルネットワークプログラムの補足説明
ニューラルネットワークの各レイヤー（層）は、シンプルに計算式をそのままPyhonの式に展開して書いてます。
- Affine層（全結合層）： np.dot(x, W1) + b1
- Sigmoid層（活性化関数）：1 / (1 + np.exp(a1))
- SoftMax層：np.exp(a2t) / np.sum(np.exp(a2t), axis=0)
- 損失関数（クロスエントロピー）：-np.sum(np.log(y[np.arange(batch_size), t])) / batch_size
勾配は、誤差逆伝播を使わず、素の微分（__bibun_np__関数）を使って求めてます。
関数名に__がついているのは、クラス内部でのみ使う命名規則に従ってるだけです。
悲しいかな。
このやり方（素の微分を使う）だと、ものすごく遅いです。
MNISTなどのありものの画像データを使うと、正直、いつ終わるかわかりません。
なので。
今回は、無理くり、小さなデータセットを生成して使ってます。
ラベルは「0」と「1」の２つにしました。
片方は
もう一方は
みたいな感じで、同じ数字ばかりで構成されたデータと、ランダムな数字で構成されたデータという、あからさまに分類しやすい（笑）データにしてみました。
##### 実行してみたら普通の微分でも学習はできるとわかった
実行してみます。
毎回、データが変わるので結果は変化します。
以下は、ある回のログです。
最初、0.5（まったくでたらめ）の状態から、0.96（96%正解）までいってます。
学習はできていると思って良さげです。
まあ。
こんなものは、学習用というか頭の整理用以外の何の役にも立ちません。
だけど。
誤差逆伝播法を使わなくても、まがりなりにも学習ができました。
ということは、やっぱ根本のところは「微分」なんだな・・と納得もできました。
##### ついでに微分についてPythonプログラムで復習する
念のため、微分についても補足しておきます。
微分とは「変数の微小な変化に対応する、関数の変化の割合の極限（＝微分係数）を求めること。」などと説明されますが。
正直。
自分も学校で習った時にはちんぷんかんぷんでした。
だけど、プログラムで書くと多少はわかりやすくなるのが面白いところです。
ここはPythonの疑似プログラムで書いてみます。
こんな感じですね。
ちょっとだけ変化させてその差をとって計算するので、hに極めて小さな数（1e-4＝1.0 × 0.0001）の値を足し引きして、関数(f）で値をもとめて、差をとることで極限をもとめる・・、というわけです。
で・・。
$$y = x^3$$
をPythonで書くと
みたいになるので、この式の微分を求める式は
yy = bibun(function1, x)
になります。
ためしに、x=10でやってみると、1000（10の３乗）で、微分の結果が300（３×10の2乗・・微分の公式）になりますから・・、あってます。
微分は、よくグラフなんかで傾きとか勾配とか呼ばれます。
同じように、ディープラーニングの学習でも「どこくらい増減させるか」を決めるのに、微分の計算値を使っているということらしいです。
実際には。
上記みたいに、ひとつだけの値を求めるということはあまりありません。
INPUTの「ｘ」は行列データになり、微分のプログラムもこんな感じになります。
行列を扱いやすいように「numpy」を使ってます。
ループをすっきり書こうとして「np.nditer」なんかを使っているのと、行列データをひとつずつ取り出して微分計算をしているので複雑に見えますけど、基本やっていることは上記のひとつの値の場合と一緒です。
ためしに、インプットを行列でやってみると。
結果は
みたいに行列ででてきます。
##### おわりです
さてさて。
誤差逆伝播法が、実は微分計算を効率的・高速にやっているのに近い・・というのを知ったとき、自分的にはすごく興奮しました。
それを少しでも共有できれば・・と記事を書いてみました。
今回はこんなところで。
ではでは。