專欄中《零神經(jīng)網(wǎng)絡(luò)實戰(zhàn)》系列持續(xù)更新介紹神經(jīng)元怎么工作,最后使用python從0到1不調(diào)用任何依賴神經(jīng)網(wǎng)絡(luò)框架(不使用tensorflow等框架)來實現(xiàn)神經(jīng)網(wǎng)絡(luò),梯度下降、反向傳播、卷積神經(jīng)網(wǎng)絡(luò)CNN、循環(huán)神經(jīng)網(wǎng)絡(luò)RNN。從0基礎(chǔ)角度進行神經(jīng)網(wǎng)絡(luò)實戰(zhàn)。
上一篇:零基礎(chǔ)神經(jīng)網(wǎng)絡(luò)實戰(zhàn)(1):單個神經(jīng)元+隨機梯度下降學習邏輯與規(guī)則
作者:司南牧
實例介紹反向傳播,為何說深度學習離不開反向傳播?
之前介紹了單個神經(jīng)元如何利用隨機梯度下降自己調(diào)節(jié)權(quán)重。深度學習指的是數(shù)以百千層的層數(shù)很深的神經(jīng)網(wǎng)絡(luò),每層又有數(shù)以百千個神經(jīng)元。那么深度學習是否也可以使用這種形式的梯度下降來進行調(diào)節(jié)權(quán)重呢?答:很難。為什么?主要原因是太深了。為何“深”用梯度下降解會有問題呢?主要是因為鏈式法則導致計算損失函數(shù)對前面層權(quán)重的導數(shù)時,損失函數(shù)對后面層權(quán)重的導數(shù)總是被重復(fù)計算,反向傳播就是將那些計算的值保存減少重復(fù)計算。不明白?那這篇文章就看對了。接下來將解釋這個重復(fù)計算過程。反向傳播就是梯度下降中的求導環(huán)節(jié),它從后往前計算導數(shù)重復(fù)利用計算過的導數(shù)而已。。梯度下降不懂或者神經(jīng)網(wǎng)絡(luò)不懂請先閱讀這個文章單個神經(jīng)元+隨機梯度下降學習邏輯與規(guī)則。
文章結(jié)構(gòu):
- 為何要使用反向傳播?
- 反向傳播優(yōu)化神經(jīng)網(wǎng)絡(luò)權(quán)重參數(shù)實踐。
為何要使用反向傳播?
我們用一個最簡單的三層神經(jīng)網(wǎng)絡(luò)(無激活函數(shù))來進行解釋為何需要使用反向傳播,所使用的三層神經(jīng)網(wǎng)絡(luò)如下所示:
這里我們有三個要優(yōu)化的參數(shù):w1,w2,w3。
我們先用傳統(tǒng)的梯度下降方法來看看哪些地方重復(fù)計算了,而反向傳播就是將這些重復(fù)計算的地方計算結(jié)果保存,并且向前面層傳播這個值以減少運算復(fù)雜度。 梯度下降需要先得求損失函數(shù)對w1,w2,w3的導數(shù),然后根據(jù)導數(shù)來不斷迭代更新w1,w2,w3的值。
第1層:求損失函數(shù)L(w1,w2,w3)對w1的導數(shù)(更規(guī)范的講是偏導數(shù))。
我們先看看損失函數(shù)L(w1,w2,w3)和w1之間的關(guān)系是什么。
$$L(w1,w2,w3)=1/2 (e-/hat e )^2$$
e=w3 * z
z=w2 * y
y=w1 * x
所以這是個復(fù)合函數(shù)的求導。根據(jù)高中學習到的復(fù)合函數(shù)求導法則(也是大家可能經(jīng)常聽到的鏈式法則),復(fù)合函數(shù)求導等于各級復(fù)合函數(shù)導數(shù)的乘積,也就是這樣
第2層:求損失函數(shù)L(w1,w2,w3)對w2的導數(shù)。
我們再看看損失函數(shù)L(w1,w2,w3)和w2之間的關(guān)系是什么。
$$L(w1,w2,w3)=1/2 (e-/hat e )^2$$
e=w3 * z
z=w2 * y
根據(jù)復(fù)合函數(shù)求導可得:
第3層:求損失函數(shù)L(w1,w2,w3)對w3的導數(shù)。
我們再看看損失函數(shù)L(w1,w2,w3)和w3之間的關(guān)系是什么。
$$L(w1,w2,w3)=1/2 (e-/hat e )^2$$
e=w3 * z
根據(jù)復(fù)合函數(shù)求導可得:
我們將這三層的損失函數(shù)對相應(yīng)層權(quán)重導數(shù)列在一起看看哪兒重復(fù)計算了:
我們再看看這個圖:
所搭建的最簡單的三層神經(jīng)網(wǎng)絡(luò)
你會發(fā)現(xiàn),如果將損失函數(shù)也看做是一層的話。即我們認為e->L(w1,w2,w3)這也算一層。
找規(guī)律:
反向傳播過程理解
前面我們提到了可以從后面往前面計算,將公共部分的導數(shù)往前傳。這只是解決了求導問題,那怎么進行參數(shù)更新呢?答:參數(shù)更新跟梯度下降完全一樣,都是這個公式。反向傳播就是梯度下降中的求導環(huán)節(jié),它重復(fù)利用計算過的導數(shù)而已。
我們看看反向傳播版的使用梯度下降進行參數(shù)更新是怎樣的。
損失函數(shù)L(w1,w2,w3)是這樣:$$L(w1,w2,w3)=1/2 (e-/hat e )^2$$
其他的幾層函數(shù)如下所示:
第3層:e=w3 * z
第2層:z=w2 * y
第1層:y=w1 * x
在這篇文章“單個神經(jīng)元+隨機梯度下降學習邏輯與規(guī)則”介紹了,權(quán)重更新是一個不斷猜(迭代更新)的過程。下一次權(quán)重值 = 本次權(quán)重值 - 學習率* 損失函數(shù)對該權(quán)重的導數(shù)。定義學習率為α. 接下來我們只需要知道怎么利用后面層數(shù)傳遞過來的值來求“損失函數(shù)對當前層權(quán)重wi的導數(shù)”即可。
則各層網(wǎng)絡(luò)更新權(quán)重的步驟為如下所示:
1.更新第3層權(quán)重參數(shù)w3.
2.更新第2層權(quán)重參數(shù)w2
3.更新第1層權(quán)重參數(shù)w1.
所以,將上面3步用偽代碼寫可以寫成下面這樣。
終于可以告別可怕的公式了,越難的東西你堅持了,你的不可替代性就越強。加油你不是一個人在奮斗。
反向傳播實踐
我們將上面的偽代碼轉(zhuǎn)成Python代碼。我們希望這個神經(jīng)網(wǎng)絡(luò)能自己學習到的功能是輸入x輸出的e=-x.
我們提供訓練集數(shù)據(jù)(我們只有兩條數(shù)據(jù)):
重復(fù)訓練次數(shù)epoch = 160。
好開工實現(xiàn)它。
# -*- coding: UTF-8 -*-
"""
@author 知乎:@Ai醬
"""
class NeuralNetwork:
def __init__(self):
self.LEARNING_RATE = 0.05 # 設(shè)置學習率
# 初始化網(wǎng)絡(luò)各層權(quán)重(權(quán)重的初試值也會影響神經(jīng)網(wǎng)絡(luò)是否收斂)
# 博主試了下權(quán)重初始值都為0.2333是不行的
self.w3 = -0.52133
self.w2 = -0.233
self.w1 = 0.2333
self.data = [1, -1] # 輸入數(shù)據(jù)
self.label= [-1, 1]
def train(self):
epoch = 160
for _ in range(epoch):
# 逐個樣本進行訓練模型
for i in range(len(self.data)):
x = self.data[i]
e_real = self.label[i]
self.y = self.w1 * x #計算第1層輸出
self.z = self.w2 * self.y # 計算第2層輸出
self.e = self.w3 * self.z # 計算第3層輸出
# 開始反向傳播優(yōu)化權(quán)重
self.result3 = self.e - e_real
self.w3 = self.w3 - self.LEARNING_RATE * self.result3 * self.z
self.result2 = self.result3 * self.w3
self.w2 = self.w2 - self.LEARNING_RATE * self.result2 * self.y
self.w1 = self.w1 - self.LEARNING_RATE * self.result2 * self.w2 * x
def predict(self,x):
self.y = self.w1 * x #計算第1層輸出
self.z = self.w2 * self.y # 計算第2層輸出
self.e = self.w3 * self.z # 計算第3層輸出
return 1 if self.e>0 else -1
nn = NeuralNetwork()
nn.train()
print(1,',',nn.predict(1))
print(-1,',',nn.predict(-1))
'''
輸出:
1 , -1
-1 , 1
'''
如何檢驗反向傳播是否寫對?
手動推導,人工判斷
前面提到了反向傳播本質(zhì)是梯度下降,那么關(guān)鍵在于導數(shù)必須對。我們現(xiàn)在網(wǎng)絡(luò)比較小可以直接手動計算導數(shù)比對代碼中對各權(quán)重導數(shù)是否求對。
比如上面代碼中三個參數(shù)的導數(shù)將代碼中的result*展開表示就是:
dw3 = (self.e - e_real) * self.z
= (self.e - e_real) * self.w2 * self.y
= (self.e - e_real) * self.w2 * self.w1 * x
= (self. w3 * self.w2 * self.w1 * x - e_real) * self.w2 * self.w1 * x
dw2 = (self.e - e_real) * self.w3* self.y
= (self.e - e_real) * self.w3* self.w1 * x
= (self. w3 * self.w2 * self.w1 * x - e_real) * self.w3* self.w1 * x
dw3 = self.result3 * self.z
= (self.e - e_real) * self.w2 * self.w1 * x
= (self. w3 * self.w2 * self.w1 * x - e_real)* self.w2 * self.w1 * x
而損失函數(shù)展開可以表示為: $$L(w1,w2,w3) = 1/2 (w3 * w2 * w1 * x - /hat e)^2$$
對各權(quán)重參數(shù)求導為:
$${dL}/ {dw3} = (w3 * w2 * w1 * x - /hat e) * w2 * w1$$
$${dL}/ {dw2} = (w3 * w2 * w1 * x - /hat e) * w3 * w1$$
$${dL}/ {dw1} = (w3 * w2 * w1 * x - /hat e) * w3 * w2$$
可以發(fā)現(xiàn)我們代碼展開,與我們實際的公式求導是一致的證明我們代碼是正確的。
但是,一旦層數(shù)很深,那么我們就不能這么做了
我們需要用代碼自動判斷是否反向傳播寫對了。
代碼自動判斷反向傳播的導函數(shù)是否正確
這個和手工判斷方法類似。反向傳播是否正確,關(guān)鍵在于dL/dwi是否計算正確。根據(jù)高中學過的導數(shù)的定義,對于位于點θ的導數(shù)有:
$$f'(/theta) = /lim_{/epsilon/to 0} {f(/theta + /epsilon) + f(/theta - /epsilon) } /{2/epsilon}$$
所以我們可以看反向傳播求的導函數(shù)值和用導數(shù)定義求的導函數(shù)值是否接近。
即我們需要讓代碼判斷這個式子是否成立:$${dL}/ {dwi} /approx {L(wi+ 10^{-4}) - L(wi- 10^{-4})} /{2* 10^{-4}}$$
左邊的dL/dwi是反向傳播求得,右邊是導數(shù)定義求的導數(shù)值。這兩個導數(shù)應(yīng)當大致相同。
實踐:程序自動檢驗導函數(shù)是否正確:
新增了一個梯度檢驗函數(shù)check_gradient(),如下所示:
# -*- coding: UTF-8 -*-
"""
@author 知乎:@Ai醬
"""
class NeuralNetwork:
def __init__(self):
self.LEARNING_RATE = 0.05 # 設(shè)置學習率
# 初始化網(wǎng)絡(luò)各層權(quán)重(權(quán)重的初試值也會影響神經(jīng)網(wǎng)絡(luò)是否收斂)
# 博主試了下權(quán)重初始值都為0.2333是不行的
self.w3 = -0.52133
self.w2 = -0.233
self.w1 = 0.2333
self.data = [1, -1] # 輸入數(shù)據(jù)
self.label= [-1, 1]
def L(self,w1,w2,w3,x,e_real):
'''
損失函數(shù) return 1/2 * (e - e_real)^2
'''
return 0.5*(w1*w2*w3*x - e_real)**2
def train(self):
epoch = 160
for _ in range(epoch):
# 逐個樣本進行訓練模型
for i in range(len(self.data)):
x = self.data[i]
e_real = self.label[i]
self.y = self.w1 * x #計算第1層輸出
self.z = self.w2 * self.y # 計算第2層輸出
self.e = self.w3 * self.z # 計算第3層輸出
# 開始反向傳播優(yōu)化權(quán)重
self.result3 = self.e - e_real
self.w3 = self.w3 - self.LEARNING_RATE * self.result3 * self.z
self.result2 = self.result3 * self.w3
self.w2 = self.w2 - self.LEARNING_RATE * self.result2 * self.y
self.w1 = self.w1 - self.LEARNING_RATE * self.result2 * self.w2 * x
self.check_gradient(x,e_real)
def check_gradient(self,x,e_real):
# 反向傳播所求得的損失函數(shù)對各權(quán)重的導數(shù)
dw3 = self.result3 * self.z
dw2 = self.result2 * self.y
dw1 = self.result2 * self.w2 * x
# 使用定義求損失函數(shù)對各權(quán)重的導數(shù)
epsilon = 10**-4 # epsilon為10的4次方
# 求損失函數(shù)在w3處的左極限和右極限
lim_dw3_right = self.L(self.w1, self.w2, self.w3+epsilon, x, e_real)
lim_dw3_left = self.L(self.w1, self.w2, self.w3-epsilon, x, e_real)
# 利用左右極限求導
lim_dw3 = (lim_dw3_right - lim_dw3_left)/(2*epsilon)
lim_dw2_right = self.L(self.w1, self.w2+epsilon, self.w3, x, e_real)
lim_dw2_left = self.L(self.w1, self.w2-epsilon, self.w3, x, e_real)
lim_dw2 = (lim_dw2_right - lim_dw2_left)/(2*epsilon)
lim_dw1_right = self.L(self.w1+epsilon, self.w2, self.w3, x, e_real)
lim_dw1_left = self.L(self.w1-epsilon, self.w2, self.w3, x, e_real)
lim_dw1 = (lim_dw1_right - lim_dw1_left)/(2*epsilon)
# 比對反向傳播求的導數(shù)和用定義求的導數(shù)是否接近
print("dl/dw3反向傳播求得:%f,定義求得%f"%(dw3,lim_dw3))
print("dl/dw2反向傳播求得:%f,定義求得%f"%(dw2,lim_dw2))
print("dl/dw1反向傳播求得:%f,定義求得%f"%(dw1,lim_dw1))
def predict(self,x):
self.y = self.w1 * x #計算第1層輸出
self.z = self.w2 * self.y # 計算第2層輸出
self.e = self.w3 * self.z # 計算第3層輸出
return 1 if self.e>0 else -1
nn = NeuralNetwork()
nn.train()
print(1,',',nn.predict(1))
print(-1,',',nn.predict(-1))
'''
輸出:
dl/dw1反向傳播求得:-0.026729,定義求得-0.026727
dl/dw3反向傳播求得:0.003970,定義求得0.004164
dl/dw2反向傳播求得:-0.032617,定義求得-0.033257
dl/dw1反向傳播求得:-0.027502,定義求得-0.027499
dl/dw3反向傳播求得:0.004164,定義求得0.004367
dl/dw2反向傳播求得:-0.033272,定義求得-0.033932
dl/dw1反向傳播求得:-0.028291,定義求得-0.028288
dl/dw3反向傳播求得:0.004367,定義求得0.004579
dl/dw2反向傳播求得:-0.033947,定義求得-0.034625
dl/dw1反向傳播求得:-0.029097,定義求得-0.029094
... ...
1 , -1
-1 , 1
'''
可以發(fā)現(xiàn)反向傳播求得損失函數(shù)對各參數(shù)求得的導數(shù)和我們用高中學的定義法求導數(shù),兩者基本一致,證明我們反向傳播求得的導數(shù)沒有問題。
附上上面的易讀版代碼的github代碼下載地址:從本質(zhì)如何理解機器學習?
框架化反向傳播
每個程序員都有一個寫框架的夢想,不如我們將前面的代碼寫個類似TensorFlow這種框架的反向傳播簡單框架吧。附上框架版的代碼github下載地址:https://github.com/varyshare/newbie_neural_network_practice/blob/master/backpropagation/backpropagation_framework.py
# -*- coding: utf-8 -*-
"""
框架化反向傳播編程
@author: 知乎@Ai醬
"""
import random
class Layer(object):
'''
本文中,一層只有一個神經(jīng)元,一個神經(jīng)元只有一個輸入一個輸出
'''
def __init__(self,layer_index):
'''
layer_index: 第幾層
'''
self.layer_index = layer_index
# 初始化權(quán)重[0,1] - 0.5 = [-0.5,0.5]保證初始化有正有負
self.w = random.random() - 0.5
# 當前層的輸出
self.output = 0
def forward(self,input_var):
'''
前向傳播:對輸入進行運算,并將結(jié)果保存
input_var: 當前層的輸入
'''
self.input = input_var
self.output = self.w * self.input
def backward(self, public_value):
'''
反向傳播:計算上層也會使用的導數(shù)值并保存
假設(shè)當前層的計算規(guī)則是這樣output = f(input),
而 input == 前一層的輸出,
因此,根據(jù)鏈式法則損失函數(shù)對上一層權(quán)重的導數(shù) = 后面層傳過來的公共導數(shù)* f'(input) * 前一層的導數(shù)
也就是說,后面層傳過來的公共導數(shù)值* f'(input) 是需要往前傳的公用的導數(shù)值。
由于本層中對輸入做的運算為:output = f(input) = w*input
所以, f'(input) = w.
public_value: 后面?zhèn)鬟^來的公共導數(shù)值
'''
# 當前層要傳給前面層的公共導數(shù)值 = 后面?zhèn)鬟^來的公共導數(shù)值 * f'(input)
self.public_value = public_value * self.w
# 損失函數(shù)對當前層參數(shù)w的導數(shù) = 后面?zhèn)鬟^來的公共導數(shù)值 * f'(input) * doutput/dw
self.w_grad = self.public_value * self.input
def upate(self, learning_rate):
'''
利用梯度下降更新參數(shù)w
參數(shù)迭代更新規(guī)則(梯度下降): w = w - 學習率*損失函數(shù)對w的導數(shù)
learning_rate: 學習率
'''
self.w = self.w - learning_rate * self.w_grad
def display(self):
print('layer',self.layer_index,'w:',self.w)
class Network(object):
def __init__(self,layers_num):
'''
構(gòu)造網(wǎng)絡(luò)
layers_num: 網(wǎng)絡(luò)層數(shù)
'''
self.layers = []
# 向網(wǎng)絡(luò)添加層
for i in range(layers_num):
self.layers.append(Layer(i+1))#層編號從1開始
def predict(self, sample):
'''
sample: 樣本輸入
return 最后一層的輸出
'''
output = sample
for layer in self.layers:
layer.forward(output)
output = layer.output
return 1 if output>0 else -1
def calc_gradient(self, label):
'''
從后往前計算損失函數(shù)對各層的導數(shù)
'''
# 計算最后一層的導數(shù)
last_layer = self.layers[-1]
# 由于損失函數(shù)=0.5*(last_layer.output - label)^2
# 由于backward中的public_value = 輸出對輸入的導數(shù)
# 對于損失函數(shù)其輸入是last_layer.output,損失函數(shù)對該輸入的導數(shù)=last_layer.output - label
# 所以 最后一層的backward的public_value = last_layer.output - label
last_layer.backward(last_layer.output - label)
public_value = last_layer.public_value
for layer in self.layers:
layer.backward(public_value) # 計算損失函數(shù)對該層參數(shù)的導數(shù)
public_value= layer.public_value
def update_weights(self, learning_rate):
'''
更新各層權(quán)重
'''
for layer in self.layers:
layer.upate(learning_rate)
def train_one_sample(self, label, sample, learning_rate):
self.predict(sample) # 前向傳播,使得各層的輸入都有值
self.calc_gradient(label) # 計算各層導數(shù)
self.update_weights(learning_rate) # 更新各層參數(shù)
def train(self, labels, data_set, learning_rate, epoch):
'''
訓練神經(jīng)網(wǎng)絡(luò)
labels: 樣本標簽
data_set: 輸入樣本們
learning_rate: 學習率
epoch: 同樣的樣本反復(fù)訓練的次數(shù)
'''
for _ in range(epoch):# 同樣數(shù)據(jù)反復(fù)訓練epoch次保證權(quán)重收斂
for i in range(len(labels)):#逐樣本更新權(quán)重
self.train_one_sample(labels[i], data_set[i], learning_rate)
nn = Network(3)
data_set = [1,-1]
labels = [-1,1]
learning_rate = 0.05
epoch = 160
nn.train(labels,data_set,learning_rate,epoch)
print(nn.predict(1)) # 輸出 -1
print(nn.predict(-1)) # 輸出 1
歡迎關(guān)注我的知乎專欄適合初學者的機器學習神經(jīng)網(wǎng)絡(luò)理論到實踐。
上一篇為零基礎(chǔ)神經(jīng)網(wǎng)絡(luò)實戰(zhàn)(1):單個神經(jīng)元+隨機梯度下降學習邏輯與規(guī)則
下一篇為適合初學者的神經(jīng)網(wǎng)絡(luò)理論到實踐(3):打破概念束縛:強化學習是個啥?
審核編輯:符乾江
-
神經(jīng)網(wǎng)絡(luò)
+關(guān)注
關(guān)注
42文章
4777瀏覽量
100954 -
人工智能
+關(guān)注
關(guān)注
1792文章
47514瀏覽量
239229
發(fā)布評論請先 登錄
相關(guān)推薦
評論