簡(jiǎn)介
在上一篇文章《手寫一個(gè)全連接神經(jīng)網(wǎng)絡(luò)用于MNIST數(shù)據(jù)集》中蔗彤,我們使用少于100行代碼實(shí)現(xiàn)了一個(gè)3層的全連接網(wǎng)絡(luò),并且在MNIST數(shù)據(jù)集上取得了95%以上的準(zhǔn)確率载庭。相信讀過這篇文章的讀者對(duì)全連接網(wǎng)絡(luò)如何使用梯度下降算法來學(xué)習(xí)自身的權(quán)值和偏置的原理已經(jīng)有所了解(如果你沒讀過,建議先看一下再閱讀本文)歇僧。但是上篇文章留下了一個(gè)問題筛峭,就是我們沒有討論如何計(jì)算損失函數(shù)的梯度宛徊,在本文中,我會(huì)詳細(xì)解釋如何計(jì)算這些梯度夜只。
本文會(huì)涉及到較多的數(shù)學(xué)公式垒在,要求讀者了解微積分的基本知識(shí),尤其是鏈?zhǔn)椒▌t扔亥。
反向傳播概覽
先引用一下維基百科中對(duì)于反向傳播的定義:
反向傳播(英語:Backpropagation场躯,縮寫為BP)是“誤差反向傳播”的簡(jiǎn)稱,是一種與最優(yōu)化方法(如梯度下降法)結(jié)合使用的旅挤,用來訓(xùn)練人工神經(jīng)網(wǎng)絡(luò)的常見方法踢关。該方法對(duì)網(wǎng)絡(luò)中所有權(quán)重計(jì)算損失函數(shù)的梯度。這個(gè)梯度會(huì)反饋給最優(yōu)化方法粘茄,用來更新權(quán)值以最小化損失函數(shù)签舞。
反向傳播的核心是對(duì)損失函數(shù)關(guān)于任何權(quán)重(或偏置)的偏導(dǎo)數(shù)(或)的表達(dá)式。這個(gè)表達(dá)式告訴我們?cè)诟淖儥?quán)重和偏置時(shí)柒瓣,損失函數(shù)變化的快慢儒搭。由于是一個(gè)疊加了多種運(yùn)算的多元函數(shù),所以對(duì)網(wǎng)絡(luò)的某一層的某一個(gè)權(quán)重的偏導(dǎo)數(shù)可能會(huì)變得很復(fù)雜芙贫,不過這也讓我們直觀的看到了某一個(gè)權(quán)重的變化究竟會(huì)如何改變網(wǎng)絡(luò)的行為搂鲫。
神經(jīng)網(wǎng)絡(luò)參數(shù)的表示
通過上一篇文章,我們知道神經(jīng)網(wǎng)絡(luò)是由多個(gè)S型神經(jīng)元構(gòu)成的屹培,每個(gè)S型神經(jīng)元有輸入默穴,權(quán)重怔檩,偏置,以及輸出蓄诽。那么當(dāng)它們組合在一起形成復(fù)雜的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)的時(shí)候薛训,參數(shù)會(huì)變得異常的多。這個(gè)時(shí)候我們需要先約定一下如何來表示這些參數(shù)仑氛,以便數(shù)學(xué)描述和基于矩陣的運(yùn)算乙埃。先以一個(gè)簡(jiǎn)單的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)為例:
可以看到上述網(wǎng)絡(luò)一共有3層,第一層是輸入層锯岖,第二層是隱層介袜,第三層是輸出層。接下來我會(huì)在這張圖上標(biāo)注權(quán)重出吹,偏置遇伞,激活值等參數(shù),并且會(huì)引入若干個(gè)符號(hào)來表示這些參數(shù)捶牢。
令表示從層的個(gè)神經(jīng)元到層的個(gè)神經(jīng)元上的權(quán)重鸠珠。這個(gè)表示看起來有點(diǎn)奇怪,也不容易理解秋麸,因?yàn)檫@個(gè)表達(dá)式里面雖然帶有一個(gè)渐排,但是實(shí)際上它表示的是第層到第層的之間的權(quán)重關(guān)系。以上圖的藍(lán)色箭頭所指的線段為例灸蟆,代表的含義是驯耻,第2層的第4個(gè)神經(jīng)元到第3層的第2個(gè)神經(jīng)元之間的權(quán)重。
其實(shí)如果你仔細(xì)看過上一篇文章中的代碼炒考,你會(huì)發(fā)現(xiàn)權(quán)重向量和偏置向量只是第二層和第三層才會(huì)有可缚,而輸入層是沒有這些參數(shù)的。在上圖中斋枢,我們是用帶箭頭的線段來表示的權(quán)重城看,故代表的并不是第層指出去的線段,而指的是第層指向它的線段杏慰,這個(gè)有點(diǎn)違反直覺,但是接下來你會(huì)看到這么做的好處炼鞠。
令表示在第層的第個(gè)神經(jīng)元的偏置缘滥,如上圖表示第2層的第3個(gè)神經(jīng)元上的偏置。
令表示在第層的第個(gè)神經(jīng)元的激活值谒主,如上圖代表第2層的第4個(gè)神經(jīng)元上的激活值朝扼。
有了這些表示,層的第個(gè)神經(jīng)元的激活值就和層的激活值通過方程關(guān)聯(lián)起來了:
其中求和是在層的全部個(gè)神經(jīng)元之間進(jìn)行的霎肯。為了用矩陣的形式重寫這個(gè)表達(dá)式擎颖,我們對(duì)每一層都定義了一個(gè)權(quán)重矩陣榛斯。權(quán)重矩陣的元素是連接到第層的神經(jīng)元的權(quán)重(即指向第層神經(jīng)元的全部箭頭)。同理搂捧,對(duì)每一層定義一個(gè)偏置向量驮俗,向量中的每一個(gè)元素就是。最后定義每一層的激活向量允跑,向量中的每個(gè)元素是王凑。
也許你還不明白這個(gè)怎么計(jì)算的,讓我以上圖的來描述一下它的計(jì)算過程聋丝。我用橙色的線段來代表所有指向第2層第4個(gè)神經(jīng)元的權(quán)重索烹,可以看到一共有3條線段指向了它,其代表的權(quán)重分別是弱睦,百姓,,由于這是第一層况木,故它的激活向量就是輸入向量垒拢,代表的是這個(gè)神經(jīng)元的偏置值,那么完整的計(jì)算過程描述如下:
上面的公式(1)還是太麻煩了焦读,我們注意到(1)式中的參數(shù)子库、、其實(shí)都是向量矗晃,故我們可以將其改寫成矩陣形式仑嗅,如下:
這個(gè)式子看著更加簡(jiǎn)潔,它描述了第層的激活值與第層的激活值之間的關(guān)系张症。我們只需要將本層的權(quán)重矩陣作用在上一層的激活向量上仓技,然后加上本層的偏置向量,最后通過函數(shù)俗他,便得到了本層的激活向量脖捻。
如果將(2)式寫的更詳細(xì)一點(diǎn),其實(shí)我們是先得到了中間量兆衅,然后通過函數(shù)地沮。我們稱為第層神經(jīng)元的帶權(quán)輸入。故為了本文后面描述方便羡亩,我們也會(huì)將(2)式寫成以下形式:
注意是一個(gè)向量摩疑,代表了第層的帶權(quán)輸入。它的每一個(gè)元素是畏铆,其中就是第層的第個(gè)神經(jīng)元的激活函數(shù)的帶權(quán)輸入雷袋。
注意,在本文中辞居,只帶了上標(biāo)而沒有帶下標(biāo)的表達(dá)式楷怒,都是指的向量蛋勺。
損失函數(shù)
還記得我們?cè)谏弦黄恼轮校褂昧司讲顡p失函數(shù)鸠删,定義如下:其中是訓(xùn)練樣本的總數(shù)抱完,求和運(yùn)算遍歷了每個(gè)訓(xùn)練樣本,是每個(gè)樣本對(duì)應(yīng)的標(biāo)簽冶共,代表網(wǎng)絡(luò)的層數(shù)乾蛤,代表的是輸入為時(shí)網(wǎng)絡(luò)輸出的激活向量(在MNIST任務(wù)中,輸出是一個(gè)10維的向量)捅僵。
在上式中家卖,對(duì)于一個(gè)特定的輸入樣本集,和都是固定的庙楚,所以我們可以將看做是的函數(shù)上荡。
本文將會(huì)繼續(xù)使用此損失函數(shù)來描述如何進(jìn)行反向傳播算法的應(yīng)用。
Hadamard乘積
反向傳播算法基于常規(guī)的線性代數(shù)運(yùn)算 —— 諸如向量加法馒闷,向量矩陣乘法等酪捡。但是有一個(gè)運(yùn)算不大常?。特別地纳账,假設(shè)和是兩個(gè)同樣維度的向量逛薇。那么我們使用來表示按元素的乘積。所以的元素就是疏虫。舉個(gè)例子如下:
這種類型的按元素乘法有時(shí)候被稱為Hadamard乘積永罚,具體定義可以參考百度百科。
反向傳播
定義神經(jīng)元誤差
反向傳播其實(shí)是對(duì)權(quán)重和偏置變化影響損失函數(shù)過程的理解卧秘,最終的目的就是計(jì)算偏導(dǎo)數(shù)和呢袱。為了計(jì)算這些值,我們首先引入一個(gè)中間量翅敌,我們稱之為在第層的第個(gè)神經(jīng)元上的誤差羞福。
為了理解誤差是如何定義的,假設(shè)在神經(jīng)網(wǎng)絡(luò)上有一個(gè)調(diào)皮?:
這個(gè)調(diào)皮鬼在第層的第個(gè)神經(jīng)元上蚯涮。當(dāng)輸入進(jìn)來的時(shí)候治专,這個(gè)調(diào)皮鬼對(duì)這個(gè)輸入增加了很小的變化,使得神經(jīng)元輸出由原本的變成了遭顶。這個(gè)變化會(huì)依次向網(wǎng)絡(luò)后面的層進(jìn)行傳播看靠,最終導(dǎo)致整個(gè)損失函數(shù)產(chǎn)生的變化(具體可以參考全微分的定義)。
現(xiàn)在加入這個(gè)調(diào)皮鬼改邪歸正了液肌,并且試著幫你優(yōu)化損失函數(shù),它試著找到可以讓損失函數(shù)更小的鸥滨。假設(shè)有一個(gè)很大的值嗦哆,或正或負(fù)谤祖。那么這個(gè)調(diào)皮鬼可以通過選擇適當(dāng)?shù)?img class="math-inline" src="https://math.jianshu.com/math?formula=%5CDelta%20z%5El_j" alt="\Delta z^l_j" mathimg="1">來降低損失函數(shù)的值。相反老速,如果接近0粥喜,那么無論怎么調(diào)整都不能改善太多損失函數(shù)的值。因此橘券,在調(diào)皮鬼看來额湘,這時(shí)神經(jīng)元已經(jīng)接近最優(yōu)了。所以這里有一種啟發(fā)式的認(rèn)識(shí)旁舰,可以認(rèn)為是神經(jīng)元誤差的度量锋华。
按照上面的描述,我們定義第層的第個(gè)神經(jīng)元上的誤差為:
反向傳播的四個(gè)方程
反向傳播基于4個(gè)基本方程箭窜,這些方程指明了計(jì)算誤差和損失函數(shù)梯度的方法毯焕。先列舉出來:
1. 輸出層誤差的方程
結(jié)合(3)式,我們簡(jiǎn)單證明一下第一個(gè)方程:因?yàn)樯鲜街械那蠛褪窃谳敵鰧拥乃?img class="math-inline" src="https://math.jianshu.com/math?formula=k" alt="k" mathimg="1">個(gè)神經(jīng)元上運(yùn)行的磺樱,由于這里是求損失函數(shù)對(duì)第層的第個(gè)神經(jīng)元的輸出激活值求導(dǎo)纳猫,故當(dāng)都為0竹捉。
上式右邊第一個(gè)項(xiàng)表示損失函數(shù)隨著輸出激活值的變化而變化的速度芜辕。假如不依賴特定的神經(jīng)元,那么就會(huì)比較小块差,這也是我們期望的效果侵续。右邊第二項(xiàng)表達(dá)的是在激活函數(shù)在處的變化速度。
對(duì)于第一項(xiàng)憾儒,它依賴特定的損失函數(shù)的形式询兴,如果我們使用均方差損失函數(shù),那么其實(shí)很容易就可以算出來起趾,如下:
如果我們令代表損失函數(shù)對(duì)激活值向量的偏導(dǎo)數(shù)向量诗舰,那么(6)式可以被寫成矩陣的形式:
(7)式就是反向傳播4個(gè)方程中的第一個(gè)方程。對(duì)于均方差損失函數(shù)训裆,眶根,所以(7)式可以寫成如下形式:
寫成上述向量的形式是為了方便使用numpy之類的庫進(jìn)行矩陣計(jì)算。
2. 使用下一層的誤差來更新當(dāng)前層的誤差
第一個(gè)方程描述了輸出層的誤差边琉,那么如何求得前一層的誤差呢属百?我們還是可以從(5)式出發(fā),對(duì)其運(yùn)用鏈?zhǔn)椒▌t变姨,如下:
又根據(jù)上文族扰,我們知道,,因此可以得到:
上式對(duì)微分的結(jié)果如下:
其中代表第層的權(quán)重矩陣的轉(zhuǎn)置怒竿。這個(gè)公式看起來挺復(fù)雜,但是每一項(xiàng)都有具體的意義扩氢。假如第層的誤差是耕驰,當(dāng)我們使用同一層的轉(zhuǎn)置權(quán)重矩陣去乘以它時(shí),直觀感覺可以認(rèn)為它是沿著網(wǎng)絡(luò)反向地移動(dòng)誤差录豺,這給了我們度量在第層輸出的誤差方法(還記得上文朦肘,我們使用帶箭頭的線段來表明權(quán)重么,這里這么做双饥,相當(dāng)于把第層第個(gè)神經(jīng)元指向第層的所有神經(jīng)元的箭頭線段全部逆向了)媒抠。
接著我們進(jìn)行Hadamard乘積運(yùn)算,這會(huì)讓誤差通過第層的激活函數(shù)反向傳遞回來兢哭,并給出在第層的帶權(quán)輸入誤差领舰。
我估計(jì)你看到這里會(huì)很懵逼,我當(dāng)時(shí)學(xué)習(xí)的時(shí)候也是非常迷惑迟螺,感覺腦子一團(tuán)糟冲秽,不過我將會(huì)以第二層第4個(gè)神經(jīng)元的誤差為例,來展示誤差的反向傳播過程矩父,根據(jù)式(12)锉桑,可以得出以下計(jì)算過程:
其實(shí)這個(gè)也很符合直覺,因?yàn)榈?層第4個(gè)神經(jīng)元連接到了第3層的全部神經(jīng)元窍株,故誤差反向傳播的時(shí)候民轴,應(yīng)該是與其連接的所有神經(jīng)元的誤差之和傳遞給此神經(jīng)元。
有了前兩個(gè)方程之后球订,我們就可以計(jì)算任何層的誤差后裸。首先使用方程(7)計(jì)算當(dāng)前層誤差,然后使用式(13)來計(jì)算得到冒滩,以此類推微驶,直到反向傳播完整個(gè)網(wǎng)絡(luò)。
3. 損失函數(shù)關(guān)于任意偏置的變化率
由上面內(nèi)容可知开睡,因苹,對(duì)求偏導(dǎo)得。則有:
故可知篇恒,誤差和偏導(dǎo)數(shù)完全一致扶檐。
寫成向量的形式如下:
其中和偏置都是針對(duì)同一個(gè)神經(jīng)元。
4. 損失函數(shù)對(duì)于任意一個(gè)權(quán)重的變化率
由上面內(nèi)容可知胁艰,款筑,對(duì)求偏導(dǎo)得智蝠。又:
其中是上一層的激活輸出向量,是當(dāng)前層的誤差奈梳,可以使用下圖來描述:
上圖說明寻咒,當(dāng)上一層的激活值很小的時(shí)候,梯度也會(huì)很小颈嚼,這意味著在梯度下降算法過程中,這個(gè)權(quán)重不會(huì)改變很多饭寺。這樣導(dǎo)致的問題就是來自較低激活值的神經(jīng)元的權(quán)重學(xué)習(xí)會(huì)非常緩慢阻课。
另外觀察一下前兩個(gè)方程,可以看到它們的表達(dá)式中都包含艰匙,即要計(jì)算函數(shù)的導(dǎo)數(shù)限煞。回憶一下上一篇文章中Sigmoid函數(shù)的圖像员凝,可以看到署驻,當(dāng)輸入非常大或者非常小的時(shí)候,函數(shù)變得非常平坦健霹,即導(dǎo)數(shù)趨近于0旺上,這會(huì)導(dǎo)致梯度消失的問題,后面會(huì)專門寫文章討論糖埋。
總結(jié)一下宣吱,如果輸入神經(jīng)元激活值很低,或者神經(jīng)元輸出已經(jīng)飽和了瞳别,那么權(quán)重學(xué)習(xí)的過程會(huì)很慢征候。
反向傳播的算法流程
反向傳播的4個(gè)方程給出了一種計(jì)算損失函數(shù)梯度的方法,下面用算法描述出來:
- 輸入祟敛,為輸入層設(shè)置對(duì)應(yīng)的激活值疤坝。
- 前向傳播: 對(duì)每一層,計(jì)算相應(yīng)的權(quán)重輸入和馆铁。
- 輸出層的誤差: 計(jì)算向量
- 誤差反向傳播:對(duì)于每一層跑揉,計(jì)算
- 輸出: 損失函數(shù)的梯度分別由 和 得出。
很多人在一開始學(xué)習(xí)的時(shí)候叼架,分不清梯度下降和反向傳播之間的關(guān)系畔裕,我最開始也是。不過從上面的流程中應(yīng)該可以了解一點(diǎn)乖订。梯度下降法是一種優(yōu)化算法扮饶,中心思想是沿著目標(biāo)函數(shù)梯度的方向更新參數(shù)值以希望達(dá)到目標(biāo)函數(shù)最小(或最大)乍构。而這個(gè)算法中需要計(jì)算目標(biāo)函數(shù)的梯度甜无,那么反向傳播就是在深度學(xué)習(xí)中計(jì)算梯度的一種方式扛点。
代碼解析
這里只列舉了代碼中反向傳播部分,若需完整代碼岂丘,請(qǐng)?jiān)?a target="_blank">https://github.com/HeartbreakSurvivor/FCN下載陵究。
我對(duì)代碼中關(guān)鍵步驟,都添加了詳細(xì)的注釋奥帘,讀者再對(duì)著文章內(nèi)容铜邮,應(yīng)該能夠看明白。
def update_mini_batch(self, mini_batch, eta):
"""
通過小批量隨機(jī)梯度下降以及反向傳播來更新神經(jīng)網(wǎng)絡(luò)的權(quán)重和偏置向量
:param mini_batch: 隨機(jī)選擇的小批量
:param eta: 學(xué)習(xí)率
"""
nabla_b = [np.zeros(b.shape) for b in self._biases]
nabla_w = [np.zeros(w.shape) for w in self._weights]
for x, y in mini_batch:
# 反向傳播算法寨蹋,運(yùn)用鏈?zhǔn)椒▌t求得對(duì)b和w的偏導(dǎo)
delta_nabla_b, delta_nabla_w = self.backprop(x, y)
# 對(duì)小批量訓(xùn)練數(shù)據(jù)集中的每一個(gè)求得的偏導(dǎo)數(shù)進(jìn)行累加
nabla_b = [nb + dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]
nabla_w = [nw + dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]
# 使用梯度下降得出的規(guī)則來更新權(quán)重和偏置向量
self._weights = [w - (eta / len(mini_batch)) * nw
for w, nw in zip(self._weights, nabla_w)]
self._biases = [b - (eta / len(mini_batch)) * nb
for b, nb in zip(self._biases, nabla_b)]
def backprop(self, x, y):
"""
反向傳播算法松蒜,計(jì)算損失對(duì)w和b的梯度
:param x: 訓(xùn)練數(shù)據(jù)x
:param y: 訓(xùn)練數(shù)據(jù)x對(duì)應(yīng)的標(biāo)簽
:return: Return a tuple ``(nabla_b, nabla_w)`` representing the
gradient for the cost function C_x. ``nabla_b`` and
``nabla_w`` are layer-by-layer lists of numpy arrays, similar
to ``self.biases`` and ``self.weights``.
"""
nabla_b = [np.zeros(b.shape) for b in self._biases]
nabla_w = [np.zeros(w.shape) for w in self._weights]
# 前向傳播,計(jì)算網(wǎng)絡(luò)的輸出
activation = x
# 一層一層存儲(chǔ)全部激活值的列表
activations = [x]
# 一層一層第存儲(chǔ)全部的z向量已旧,即帶權(quán)輸入
zs = []
for b, w in zip(self._biases, self._weights):
# 利用 z = wt*x+b 依次計(jì)算網(wǎng)絡(luò)的輸出
z = np.dot(w, activation) + b
zs.append(z)
# 將每個(gè)神經(jīng)元的輸出z通過激活函數(shù)sigmoid
activation = sigmoid(z)
# 將激活值放入列表中暫存
activations.append(activation)
# 反向傳播過程
# 首先計(jì)算輸出層的誤差delta L
delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])
# 反向存儲(chǔ) 損失函數(shù)C對(duì)b的偏導(dǎo)數(shù)
nabla_b[-1] = delta
# 反向存儲(chǔ) 損失函數(shù)C對(duì)w的偏導(dǎo)數(shù)
nabla_w[-1] = np.dot(delta, activations[-2].transpose())
# 從第二層開始秸苗,依次計(jì)算每一層的神經(jīng)元的偏導(dǎo)數(shù)
for l in range(2, self._num_layers):
z = zs[-l]
sp = sigmoid_prime(z)
# 更新得到前一層的誤差delta
delta = np.dot(self._weights[-l + 1].transpose(), delta) * sp
# 保存損失喊出C對(duì)b的偏導(dǎo)數(shù),它就等于誤差delta
nabla_b[-l] = delta
# 根據(jù)第4個(gè)方程运褪,計(jì)算損失函數(shù)C對(duì)w的偏導(dǎo)數(shù)
nabla_w[-l] = np.dot(delta, activations[-l - 1].transpose())
# 返回每一層神經(jīng)元的對(duì)b和w的偏導(dǎo)數(shù)
return (nabla_b, nabla_w)