NN入門,手把手教你用Numpy手撕NN(二)


這是一篇包含較少數學推導的NN入門文章

上篇文章中簡單介紹了如何手撕一個NN,但其中仍有可以改進的地方,將在這篇文章中進行完善。

誤差反向傳播

之前的NN計算梯度是利用數值微分法,雖容易實現,但是計算速度慢,這里介紹的誤差反向傳播法能夠高效計算權重參數的梯度的方法。

這里將通過計算圖的方法來講解反向傳播

計算圖

問題一:

​ 小明在超市買了2個100元一個的蘋果,消費稅是10%,請計算支付金額

問題二:

​ 小明在超市買了2個蘋果、3個橘子。其中,蘋果每個100元, 橘子每個150元。消費稅是10%,請計算支付金額。

從上面兩問計算圖的表示中可以很容易理解其計算原理,從左到右的計算稱為正向傳播。同時,我們也可以利用這種方法進行反向傳播

再來思考一個問題:

​ 問題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各層實現

前面介紹了如何利用計算圖來計算導數,下面將介紹如何利用計算圖來設計NN的其它層。

激活函數層

ReLU層

\[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

Sigmoid層

\[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

Affine/Softmax層

Affine

神經網絡的正向傳播中進行的矩陣的乘積運算在幾何學領域被稱為“仿射變換” 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-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 反向傳播

  • 初始值為1
  • "x"節點的反向傳播將正向傳播時的輸入值翻轉,乘以上游傳來的導數后,再傳給下游
  • "+"節點將上游傳來的導數原封不動傳給下游
  • "log"節點的反向傳播如下式

\[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實現

上面介紹了各層如何利用反向傳播進行實現,這里將介紹利用反向傳播構建NN。

神經網絡學習步驟

  1. 步驟一(mini-batch)

    從訓練數據中隨機選擇一部分數據

  2. 步驟二(計算梯度)

    計算損失函數關於各個權重參數的梯度

  3. 步驟三(更新參數)

    將權重參數沿梯度方向進行微小的更新

  4. 步驟四(重復)

    重復步驟一、二、三

反向傳播將出現在步驟二中。

先來個簡單的兩層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等方法;另外還有對於權重的初始值的設置,也有蠻多的研究;以及如何抑制過擬合等,這些都得去了解,並思考其中原理。

本文首發於我的知乎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM