這是一篇包含較少數學推導的NN入門文章python
上篇文章中簡單介紹瞭如何手撕一個NN,但其中仍有能夠改進的地方,將在這篇文章中進行完善。網絡
以前的NN計算梯度是利用數值微分法,雖容易實現,可是計算速度慢,這裏介紹的偏差反向傳播法
可以高效計算權重參數的梯度的方法。app
這裏將經過計算圖
的方法來說解反向傳播dom
問題一:函數
小明在超市買了2個100元一個的蘋果,消費稅是10%,請計算支付金額學習
問題二:優化
小明在超市買了2個蘋果、3個橘子。其中,蘋果每一個100元, 橘子每一個150元。消費稅是10%,請計算支付金額。spa
從上面兩問計算圖的表示中能夠很容易理解其計算原理,從左到右的計算稱爲正向傳播
。同時,咱們也能夠利用這種方法進行反向傳播
。設計
再來思考一個問題:3d
問題1中,咱們計算了購買2個蘋果時加上消費稅最終須要支付的金額。這裏,假設咱們想知道蘋果價格的上漲會在多大程度上影響最終的支付金額,即求「支付金額關於蘋果的價格的導數」。
如上圖所示,反向傳播使用與正方向相反的箭頭(粗線)表示。反向傳播傳遞「局部導數」,將導數的值寫在箭頭的下方。在這個例子中,反向傳播從右向左傳遞導數的值爲(1→1.1→2.2)。從這個結果中可知,「支付金額 關於蘋果的價格的導數」的值是2.2。這意味着,若是蘋果的價格上漲1元, 最終的支付金額會增長2.2元(嚴格地講,若是蘋果的價格增長某個微小值, 則最終的支付金額將增長那個微小值的2.2倍)。
可見,利用計算圖能夠經過正向傳播與反向傳播高效地計算各個變量的導數值。
上面提到的反向傳播其實是基於鏈式法則進行的,這裏將介紹下其原理。
假設存在 \(y=f(x)\) 的計算,這個計算的反向傳播以下圖所示
如圖所示,反向傳播的計算順序是,將信號E乘以節點的局部導數 ,而後將結果傳遞給下一個節點。經過這樣的計算,能夠高效地求出導數的 值,這是反向傳播的要點。
然而,爲何鏈式傳播在這裏會有效呢?
學太高數話,就知道什麼是鏈式傳播
若是某個函數由複合函數表示,則該複合函數的導數能夠用構成複合函數的各個函數的導數的乘積表示。
以 \(z=(x+y)^2\) 爲例,能夠當作由下面兩個式子構成
\[ z=t^2 \\ t=x+y \tag{1} \]
那麼
\[ \frac{\partial z}{\partial x} = \frac{\partial z}{\partial t} \frac{\partial t}{\partial x}\tag{2} \]
如今來使用鏈式法則
\[ \frac{\partial z}{\partial t} = 2t\\ \frac{\partial t}{\partial x} = 1\tag{3} \]
因此
\[ \frac{\partial z}{\partial x} = 2t \cdot 1=2(x+y)\tag{4} \]
將上式按照計算圖表示以下
這樣,咱們的反向傳播就算介紹完了,下面將介紹簡單實現。
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 # 翻轉x和y dy = dout * self.x return dx, dy
__ init __()中會初始化實例變量x和y,它們用於保存正向傳播時的輸入值。 forward()接收x和y兩個參數,將它們相乘後輸出。backward()將從上游傳來的導數(dout)乘以正向傳播的翻轉值,而後傳給下游。
至於爲何要翻轉x和y可能會有點迷惑,看下面這張圖再結合上面的例子就能理解了
使用
apple = 100 apple_num = 2 tax = 1.1 # layer mul_apple_layer = MulLayer() mul_tax_layer = MulLayer() # forward apple_price = mul_apple_layer.forward(apple, apple_num) price = mul_tax_layer.forward(apple_price, tax) print(price) # 220 # backward dprice = 1 dapple_price, dtax = mul_tax_layer.backward(dprice) dapple, dapple_num = mul_apple_layer.backward(dapple_price) print(dapple, dapple_num, dtax) # 2.2 110 200
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
加法層不須要特地進行初始化,因此__ init __()中什麼也不運行。加法層的forward()接收x和y兩個參數,將它們相加後輸出。backward()將上游傳來的導數(dout)原封不動地傳遞給下游。
前面介紹瞭如何利用計算圖來計算導數,下面將介紹如何利用計算圖來設計NN的其它層。
\[ y=\begin{cases} x,\quad x > 0\\ 0,\quad x<=0 \end{cases}\tag{5} \]
對上式求導
\[ \frac{\partial y}{\partial x}=\begin{cases} 1,\quad x > 0\\ 0,\quad x<=0 \end{cases}\tag{6} \]
能夠看出,若是正向傳播時的輸入x大於0,則反向傳播會將上游的值原封不動地傳給下游。反過來,若是正向傳播時的x小於等於0,則反向傳播中傳給下游的信號將停在此處。計算圖表示以下
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
\[ y=\frac{1}{1+exp(-x)}\tag{7} \]
其正向傳播的計算圖能夠表示以下
那麼,依次進行求導
節點「/」
\[ y=\frac{1}{x} \\ \frac{\partial y}{\partial x} = -\frac{1}{x^2}=-y^2\tag{8} \]
節點「+」
原封不動傳給下一節點
節點「exp」
\[ \frac{\partial y}{\partial x} = exp(x)\tag{9} \]
節點「x」
上一節點的傳來的導數乘上-1
最後計算圖表示以下
化簡一下就是
這裏還能將公式進一步化簡
\[ \begin{aligned} \frac{\partial L}{\partial y}y^2\exp(-x) &= \frac{\partial L}{\partial y}\frac{1}{(1+\exp{-x})^2}\exp(-x)\\ &=\frac{\partial L}{\partial y}\frac{1}{1+\exp{-x}}\frac{\exp{-x}}{1+\exp{-1}}\\ &= \frac{\partial L}{\partial y}y(1-y) \end{aligned} \tag{10} \]
此時計算圖爲
class Sigmoid: def __init__(self): self.out = None def forward(self, x): out = 1 / (1 + np.exp(-x)) self.out = out return out def backward(self, dout): dx = dout * (1.0 - self.out) * self.out return dx
神經網絡的正向傳播中進行的矩陣的乘積運算在幾何學領域被稱爲「仿射變換」 A。所以,這裏將進行仿射變換的處理實現爲「Affine層」。
對於矩陣運算的反向傳播實現,涉及到了對矩陣求導,以前看到一篇很好的文章,裏面介紹瞭如何進行計算。這裏將直接給出結論,對下圖
\[ \frac{\partial L}{\partial X}=\frac{\partial L}{\partial Y} \cdot W^T \\ \frac{\partial L}{\partial W}=X^T \cdot \frac{\partial L}{\partial X} \tag{11} \]
反向傳播的計算圖
上面只是以單個數據爲對象,若是是N個數據一塊兒進行正向傳播呢?先來看看計算圖表示
代碼以下
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 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=1) return dx
Softmax-with-loss層由Softmax層與Cross Entropy Error層組合而成,其結構以下所示
公式表示
Softmax
\[ y_k=\frac{\exp{(a_k)}}{\sum^{n}_{i=1}\exp{(a_i)}}\tag{12} \]
Cross Entropy Error
\[ L=-\sum_{k}t_klogy_k\tag{13} \]
Cross Entropy Error 反向傳播
\[ y=logy\\ \frac{\partial y}{\partial x} = \frac{1}{x} \tag{14} \]
綜上,可求得Cross Entropy Error層的反向傳播的結果爲 \((-\frac{t_1}{y_1},-\frac{t_2}{y_2},-\frac{t_3}{y_3})\) 。
Softmax層反向傳播
\[ \left. \begin{gathered} y_i=\frac{\exp{(a_i)}}{S} \\ -\frac{t_i}{y_i}\exp{(a_i)} \end{gathered} \right\} \implies -t_i\frac{S}{\exp{(a_i)}}\exp{(a_i)}=-t_iS \tag{15} \]
"/"節點反向傳播爲\(-\frac{1}{S^2}\)
因此,Softmax層中間最上面的結果爲\(\frac{1}{S}(t_1+t_2+t_3)\),因爲\(t_1, t_2, t_3\)爲one-hot表示,因此僅有一個的值爲1,因此此處導數爲\(\frac{1}{S}\)。
「/」節點後的''+"節點,原封不動傳遞上游的值,此時反向傳播計算圖以下所示
接着是中間的橫向"x"節點,將值翻轉後相乘
\[ -\frac{t_i}{y_i}\frac{1}{S}=-\frac{t_i}{\exp{(a_i)}}\tag{16} \]
而後就是"exp"節點
\[ y=\exp{(x)}\\ \frac{\partial y}{\partial x}=\exp{(x)} \tag{17} \]
根據上式,兩個分支輸入和乘以\(\exp(a_i)\)後的值就是所求的反向傳播值。
\[ \left. \begin{gathered} y_i=\frac{\exp{(a_i)}}{S} \\ (\frac{1}{S}-\frac{t_i}{\exp{(a_i)}})\exp{(a_i)} \end{gathered} \right\} \implies y_i-t_i \tag{18} \]
到此爲止,Softmax-with-Loss層的反向傳播就算好了,下面來看看代碼
Class SoftWithLoss: 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 # 請注意反向傳播時,需除以批的大小(batch_size) return dx
上面介紹了各層如何利用反向傳播進行實現,這裏將介紹利用反向傳播構建NN。
步驟一(mini-batch)
從訓練數據中隨機選擇一部分數據
步驟二(計算梯度)
計算損失函數關於各個權重參數的梯度
步驟三(更新參數)
將權重參數沿梯度方向進行微小的更新
步驟四(重複)
重複步驟1、2、三
反向傳播將出如今步驟二中。
先來個簡單的兩層NN
import sys, os sys.path.append(os.pardir) import numpy as np from collections import OrderDict 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 = OrderdDict() 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(x, y) def accuracy(self, x, t): y = self.predict(x) y = np.argmax(y, axis=1) if t.ndim != 1 : t = np.argmax(t, axis=1) accuracy = np.sum(y==t) / float(x.shape[0]) return accuracy # x:輸入數據,t:監督數據 微分法梯度計算 def numerical_gradient(self, x, t): loss_W = lambda W: 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']) return grads # x:輸入數據,t:監督數據 計算圖法梯度計算 def gradient(self, x, t): # forward self.loss(x, t) # backward dout = 1 dout = self.lastLayer.backward(dout) layers = list(self.layers.values()).reverse() for layer in 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
這裏插入一點內容,咱們以前使用的數值微分的優勢是實現簡單,所以,通常狀況下不太容易出錯。而偏差反向傳播法的實現很複雜,容易出錯。因此,常常會比較數值微分的結果和偏差反向傳播法的結果,以確認偏差反向傳播法的實現是否正確。確認數值微分求出的梯度結果和偏差反向傳播法求出的結果是否一致(嚴格地講,是很是相近)的操做稱爲梯度確認(gradient check)。
代碼實現以下
# 讀入數據 (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))
使用上面的網絡進行學習
import sys, os sys.path.append(os.pardir) import numpy as np # 讀入數據 (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.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)
注:數據加載與微分求導的代碼在上篇中已給出
這樣咱們就完成了一個利用偏差反向傳播實現的簡單的兩層NN,固然,代碼能夠更加通常化,生成多層的全鏈接神經網絡,可能將在後面的文章中給出其實現。
這篇中介紹了基於反向傳播法,對上篇中實現的兩層神經網絡進行了更進一步的優化。在NN的參數更新方面,還有待優化,其方法有許多,如SGD、Momentum、AdaGrad、Adam等方法;另外還有對於權重的初始值的設置,也有蠻多的研究;以及如何抑制過擬合等,這些都得去了解,並思考其中原理。
本文首發於個人知乎