【機(jī)器學(xué)習(xí)基礎(chǔ)】使用python實(shí)現(xiàn)BP算法
用pytorch跟tensorflow實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)固然爽。但是想要深入學(xué)習(xí)神經(jīng)網(wǎng)絡(luò),光學(xué)會(huì)調(diào)包是不夠的,還是得親自動(dòng)手去實(shí)現(xiàn)一個(gè)神經(jīng)網(wǎng)絡(luò),才能更好去理解。
一、問題介紹
傳說中線性分類器無(wú)法解決的異或分類問題。我們就拿它來(lái)作為我們神經(jīng)網(wǎng)絡(luò)的迷你訓(xùn)練數(shù)據(jù)。把輸入數(shù)據(jù)拼成一個(gè)矩陣X:
import?numpy?as?np
#訓(xùn)練數(shù)據(jù):經(jīng)典的異或分類問題
train_X?=?np.array([[0,0],[0,1],[1,0],[1,1]])
train_y?=?np.array([0,1,1,0])
我們定義一個(gè)簡(jiǎn)單的2層神經(jīng)網(wǎng)絡(luò):
對(duì)應(yīng)的代碼
linear1?=?LinearLayer(2,3)
relu1?=?Relu()
linear2?=?LinearLayer(3,1)
我們還需要定義一個(gè)損失函數(shù)Loss,用來(lái)衡量我們的輸出結(jié)果與實(shí)際結(jié)果的誤差。這里用的是均方誤差MSE,表達(dá)式如下
二、BP算法 和 計(jì)算圖(Computing Graph)模型
里面的線性層,Relu層也得自己動(dòng)手實(shí)現(xiàn)。
實(shí)現(xiàn)這些,我們首先需要知道計(jì)算圖模型。計(jì)算圖模型是所有神經(jīng)網(wǎng)絡(luò)框架的核心理論基礎(chǔ)。
我們依然還是對(duì)著這個(gè)例子來(lái)講解。
上面的神經(jīng)網(wǎng)絡(luò),用純數(shù)學(xué)公式表達(dá)可以表達(dá)。
計(jì)算圖模型把一個(gè)復(fù)合運(yùn)算拆分成為多個(gè)子運(yùn)算,因此,我們需要引入很多中間變量。
定義:
根據(jù)這些公式,我們就可以用計(jì)算圖模型來(lái)表示我們的神經(jīng)網(wǎng)絡(luò),如下:
這個(gè)計(jì)算圖就是上面那一堆公式的可視化表示。
在圖里面,公式里每個(gè)出現(xiàn)過的變量都被視為一個(gè)節(jié)點(diǎn),變量之間的連線描述了變量之間存在直接的計(jì)算的關(guān)系。計(jì)算圖的表示方法,有什么好處呢?
下面我們基于這個(gè)計(jì)算圖來(lái)用BP算法進(jìn)行模型的訓(xùn)練。
對(duì)模型進(jìn)行訓(xùn)練,就是找到一組模型的參數(shù),使得我們的網(wǎng)絡(luò)模型能夠準(zhǔn)確預(yù)測(cè)我們的訓(xùn)練數(shù)據(jù)。在我們這個(gè)例子里面,需要訓(xùn)練參數(shù)其實(shí)只有線性層的矩陣跟bias項(xiàng):[公式] 。
訓(xùn)練采用的是BP算法,采用梯度下降法來(lái)逐漸迭代去更新參數(shù)。
梯度下降法的原理很簡(jiǎn)單,每次迭代中,用損失函數(shù)關(guān)于參數(shù)的梯度乘以學(xué)習(xí)率,來(lái)更新參數(shù)。
關(guān)于梯度我要多說幾句。梯度表示Y關(guān)于x的變化率,可以理解成x的速度。由于W1是一個(gè)2X3的矩陣,那么loss關(guān)于W1的梯度可以理解W1的每個(gè)元素的瞬時(shí)速度。W1的梯度的形狀,必然是嚴(yán)格跟參數(shù)本身的形狀是一樣的(每個(gè)點(diǎn)都有對(duì)應(yīng)的速度)。也就是說損失函數(shù)關(guān)于W1的梯度也必然是一個(gè)2X3的矩陣(不然更新公式里面無(wú)法做加減)。
下面開始訓(xùn)練過程,
首先給定輸入X,初始化 [公式] (記住不能初始為全0)。
正向傳播(forward pass)
BP算法首先在計(jì)算圖上面進(jìn)行正向傳播(forward pass),即從左到右計(jì)算所有未知量:
嚴(yán)格按照順序計(jì)算,所有的 [公式] 都能先后求出,右邊的y就是網(wǎng)絡(luò)的當(dāng)前預(yù)測(cè)結(jié)果。
反向傳播(backward pass)
既然我們想要用參數(shù)的梯度來(lái)更新參數(shù),那么我們需要求出最后的節(jié)點(diǎn)輸出loss關(guān)于每個(gè)參數(shù)的梯度,求梯度的方法是反向傳播。
由于我們已經(jīng)進(jìn)行過一次正向傳播,因此圖里面所有的節(jié)點(diǎn)的值都變成了已知量。
我們現(xiàn)在要求的是圖里面標(biāo)為紅色的這4個(gè)梯度,它們距離loss有點(diǎn)兒遠(yuǎn)。
但是不急,有了這個(gè)計(jì)算圖,我們可以慢慢從右往左推出這4個(gè)值。
先從最右邊開始,觀察到Loss節(jié)點(diǎn)只有一條邊跟y連著,計(jì)算loss關(guān)于y的導(dǎo)數(shù)(這個(gè)求導(dǎo)只有一個(gè)變量y,怎么求不用我解釋了吧):
我們就求得了損失函數(shù)關(guān)于輸出y的導(dǎo)數(shù),然后繼續(xù)往左邊計(jì)算。
(已經(jīng)求出的梯度我們用橙色來(lái)標(biāo)記)
y是通過O2計(jì)算出來(lái)的,我們可以計(jì)算y關(guān)于O2的梯度:
但是我們想要的是loss關(guān)于O2的梯度,這里應(yīng)用到了鏈?zhǔn)角髮?dǎo)法則:
loss關(guān)于y的梯度在之前已經(jīng)求出來(lái)過了,然后就可以求出loss關(guān)于O2的梯度。
繼續(xù)往左計(jì)算梯度:

上圖中 a2 關(guān)于它每個(gè)變量的梯度,可以直接根據(jù) a2 與它左邊3個(gè)變量的表達(dá)式來(lái)算出,如下:
這一步我們算出了兩個(gè)需要計(jì)算的梯度,似乎并沒有遇到困難,繼續(xù)往左傳播。
在計(jì)算 a1梯度的時(shí)候,我們遇到了relu激活函數(shù),relu函數(shù)的梯度也很好求:
它的梯度就是在輸入X的基礎(chǔ)上,所有大于0的位置導(dǎo)數(shù)都是1,其他位置導(dǎo)數(shù)都是0,比如:
(這括號(hào)里的看不懂不要緊,當(dāng)N維向量對(duì)M維向量求導(dǎo)應(yīng)用鏈?zhǔn)椒▌t時(shí),通用一點(diǎn)兒的結(jié)果是一個(gè)NM的jacobian矩陣再乘M1向量,但是這里由于1. N=M。2. jacobian矩陣是一個(gè)對(duì)角方陣。所以可以簡(jiǎn)化成兩個(gè)向量相乘)
再往左繼續(xù)傳,我就不寫每個(gè)步驟了。
總之可以一直傳到所有梯度都求出來(lái)為止。接下來(lái)一步就是愉快地進(jìn)行隨機(jī)梯度下降法的更新操作了。
三、模塊化各種Layer
觀察我們的網(wǎng)絡(luò),發(fā)現(xiàn)里面的幾個(gè)模塊之間其實(shí)大部分干的事情都是相似的,無(wú)非就是層數(shù)不一樣。那么我們就可以復(fù)用,我們完全可以把它們抽象成不同的Layer:
于是,我們可以把這些類似模塊看成一個(gè)小黑盒子,我們的模型等價(jià)于下面這個(gè):
于是上面那個(gè)復(fù)雜的網(wǎng)狀結(jié)構(gòu),被我們簡(jiǎn)化成了線性結(jié)構(gòu)。
下面我對(duì)照代碼實(shí)現(xiàn)每個(gè)小黑盒子吧,實(shí)現(xiàn)代碼在這個(gè)文件里面:Layers.py首先介紹線性全連接層,先看代碼吧:
class?LinearLayer:
????def?__init__(self,?input_D,?output_D):
????????self._W?=?np.random.normal(0,?0.1,?(input_D,?output_D))?#初始化不能為全0
????????self._b?=?np.random.normal(0,?0.1,?(1,?output_D))
????????self._grad_W?=?np.zeros((input_D,?output_D))
????????self._grad_b?=?np.zeros((1,?output_D))
????def?forward(self,?X):
????????return?np.matmul(X,?self._W)?+?self._b
????def?backward(self,?X,?grad):?
????????self._grad_W?=?np.matmul(?X.T,?grad)
????????self._grad_b?=?np.matmul(grad.T,?np.ones(X.shape[0]))?
????????return?np.matmul(grad,?self._W.T)
????def?update(self,?learn_rate):
????????self._W?=?self._W?-?self._grad_W?*?learn_rate
????????self._b?=?self._b?-?self._grad_b?*?learn_rate
forward太簡(jiǎn)單了,就不講了,看一下backward。
backward里面其實(shí)要計(jì)算3個(gè)值,W, b的梯度算完以后要存起來(lái),前一層的梯度算完以后直接作為返回值傳出去,推導(dǎo)的公式如下:
注意矩陣求導(dǎo)應(yīng)用鏈?zhǔn)椒▌t的時(shí)候,順序非常重要。要嚴(yán)格按照指定順序來(lái)乘,不然形狀對(duì)不上。具體什么順序,可以自己想辦法慢慢拼湊出來(lái)。
還有一個(gè)update函數(shù),調(diào)用此函數(shù)這一層會(huì)按照梯度下降法來(lái)更新它的W跟b的值,這個(gè)實(shí)現(xiàn)也很簡(jiǎn)單直接看代碼就明白了。
然后實(shí)現(xiàn)Relu層:
class?Relu:
????def?__init__(self):
????????pass
????def?forward(self,?X):
????????return?np.where(X?0,?0,?X)
????def?backward(self,?X,?grad):
????????return?np.where(X?>?0,?X,?0)?*?gr
由于這一層沒有需要保存參數(shù),只需要實(shí)現(xiàn)以下forward跟backward方法就行了,非常簡(jiǎn)單。
接下來(lái)開始實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò)訓(xùn)練。
四、搭建神經(jīng)網(wǎng)絡(luò)
訓(xùn)練部分的代碼在 nn.py 里面,里面的代碼哪里看不懂可以翻回去看之前的解釋,命名都是跟上面說的一樣的。
#訓(xùn)練數(shù)據(jù):經(jīng)典的異或分類問題
train_X?=?np.array([[0,0],[0,1],[1,0],[1,1]])
train_y?=?np.array([0,1,1,0])
#初始化網(wǎng)絡(luò),總共2層,輸入數(shù)據(jù)是2維,第一層3個(gè)節(jié)點(diǎn),第二層1個(gè)節(jié)點(diǎn)作為輸出層,激活函數(shù)使用Relu
linear1?=?LinearLayer(2,3)
relu1?=?Relu()
linear2?=?LinearLayer(3,1)
#訓(xùn)練網(wǎng)絡(luò)
for?i?in?range(10000):
????#前向傳播Forward,獲取網(wǎng)絡(luò)輸出
????o0?=?train_X
????a1?=?linear1.forward(o0)
????o1?=?relu1.forward(a1)
????a2?=?linear2.forward(o1)
????o2?=?a2
????#獲得網(wǎng)絡(luò)當(dāng)前輸出,計(jì)算損失loss
????y?=?o2.reshape(o2.shape[0])
????loss?=?MSELoss(train_y,?y)?#?MSE損失函數(shù)
????#反向傳播,獲取梯度
????grad?=?(y?-?train_y).reshape(result.shape[0],1)
????grad?=?linear2.backward(o1,?grad)
????grad?=?relu1.backward(a1,?grad)
????grad?=?linear1.backward(o0,?grad)
????learn_rate?=?0.01??#學(xué)習(xí)率
????#更新網(wǎng)絡(luò)中線性層的參數(shù)
????linear1.update(learn_rate)
????linear2.update(learn_rate)
????#判斷學(xué)習(xí)是否完成
????if?i?%?200?==?0:
????????print(loss)
????if?loss?0.001:
????????print("訓(xùn)練完成!?第%d次迭代"?%(i))
????????break
我覺得沒啥好講的,就直接對(duì)著我們的計(jì)算圖,一步一步來(lái)。
注意一下中間過程幾個(gè)向量的形狀。列向量跟行向量是不一樣的,一不小心把列向量跟行向量做運(yùn)算,numpy不會(huì)報(bào)錯(cuò),而是會(huì)廣播成一個(gè)矩陣。所以運(yùn)算的之前,記得該轉(zhuǎn)置得轉(zhuǎn)置。
#將訓(xùn)練好的層打包成一個(gè)model
model?=?[linear1,?relu1,?linear2]
#用訓(xùn)練好的模型去預(yù)測(cè)
def?predict(model,?X):
????tmp?=?X
????for?layer?in?model:
????????tmp?=?layer.forward(tmp)
????return?np.where(tmp?>?0.5,?1,?0)
把模型打包然后用上面的predict函數(shù)來(lái)預(yù)測(cè)。也沒啥好說的,就直接往后一直forward就完兒事。
#開始預(yù)測(cè)
print("-----")
X?=?np.array([[0,0],[0,1],[1,0],[1,1]])
result?=?predict(model,?X)
print("預(yù)測(cè)數(shù)據(jù)1")
print(X)
print("預(yù)測(cè)結(jié)果1")
print(result)
預(yù)測(cè)訓(xùn)練完的網(wǎng)絡(luò)就能拿去搞預(yù)測(cè)了,我這里設(shè)置學(xué)習(xí)率為0.01的情況下,在第3315次迭代時(shí)候完成訓(xùn)練。
最后我們成功地預(yù)測(cè)了訓(xùn)練數(shù)據(jù)。
往期精彩回顧
獲取一折本站知識(shí)星球優(yōu)惠券,復(fù)制鏈接直接打開:
https://t.zsxq.com/662nyZF
本站qq群1003271085。
加入微信群請(qǐng)掃碼進(jìn)群(如果是博士或者準(zhǔn)備讀博士請(qǐng)說明):
