農林漁牧網

您現在的位置是:首頁 > 林業

學界 | Andrej Karpathy:你為什麼應該理解反向傳播

2022-06-17由 機器之心Pro 發表于 林業

梯度怎麼看反向傳播

選自Medium作者: Andrej Karpathy機器之心編譯參與:吳攀、李亞洲、杜夏德

當我們在斯坦福提供 CS231n(深度學習課程)時,我們有意設計程式設計任務來將涉及最底層的反向傳播的明確的計算包括進來。這些學生必須在原始的 numpy 的每一層中實現前向和反向傳播。然而,一些學生在黑板上留下這些抱怨。

「在現實世界中,框架(比如 TensorFlow)會自動計算反向傳播,為什麼我們還一定要寫它們?」

這看起來非常合理,如果課程結束後你永遠不會寫反向傳播,為什麼還要練習呢?難道是因為我們自己的興趣而折磨學生嗎?有一個簡單的回答可能是「在求知慾下值得我們知道反向傳播」,也有回答是「之後你可能想要改進裡面的核心演算法」,但還有一個更有力、更實際的理由,我會在此文章中展現:

反向傳播的問題在於它是一個抽象洩漏(leaky abstraction)。

換言之,你會很容易將學習過程抽象掉——認為自己能非常容易地將任意層堆疊到一起,然後反向傳播就會在你的資料上「神奇發揮功效」。所以,讓我們來看一些非常清楚的例子,這是一種相當直觀的方式。

學界 | Andrej Karpathy:你為什麼應該理解反向傳播

帶有前向傳播(黑色)和反向傳播(紅色)的 Batch Norm 層的計算圖

在 S 型函式上的梯度消失

從這裡開始會很簡單。以前某段時間,在全連線層中使用 S 型函式(或雙曲正切函式 tanh)的非線性(non-linearities)非常流行。當時有一個難點是直到人們想出了反向傳播才意識到的:如果不注意權重初始化或資料預處理,這些非線性會「飽和」並完全停止學習——你的訓練損失將會趨於平坦,難以下降。例如,帶有 S 型非線性的全連線層計算(使用 raw numpy):

如果權重矩陣 W 初始化過大,矩陣相乘的輸出範圍會非常大(例如 -400 到 400 之間的數值),這會使得向量 z 上的所有輸出幾乎是二元的:0 或 1。但如果是這樣,S 型非線性函式的區域性梯度 z*(1-z) 在兩種情況下都會是 0(梯度消失),使得 x 和 W 的梯度都是 0。在鏈式反應下,之後的反向傳播相乘之後得到的都會是 0。

學界 | Andrej Karpathy:你為什麼應該理解反向傳播

另一個關於 S 型的不太明顯的有趣事實是當 z=0。5 時,它的區域性梯度 z*(1-z) 的會達到最小值 0。25。那就意味著這個梯度訊號每次透過一個 S 型函式門時,它的大小總會減少四分之一(或更多)。如果你正在使用基本的隨機梯度下降法(SGD),這會使某個網路中較低層的訓練比較高層慢得多。

注:如果你正在你的網路中使用 S 型或雙曲正切非線性並且你理解反向傳播,你就總會為確保初始化不會使它們完全飽和而感到緊張。更多解釋請參閱這個 CS231 講座影片(CS231n Winter 2016: Lecture 5: Neural Networks Part 2)。

垂死的 ReLU

另一個有趣的非線性是 ReLU,其神經元的閾值範圍是 0 以下。對於一個使用了 ReLU 的全連線層,其前向和反向透過需要在其核心包含:

如果你研究一下這段程式碼,你會發現如果一個神經元在前向透過中被嵌位到 0(即 z=0,它不會「fire」),那麼它的權重將會得到 0 梯度(zero gradient)。這會導致所謂的「死亡的 ReLU(dead ReLU)」問題,即如果一個 ReLU 神經元不幸被初始化為其永遠無法 fire,即如果一個神經元的權重在訓練進入這一範圍期間被使用一次大更新敲除,那麼這個神經元就將永久死亡。這就像是永久的、不可恢復的腦損傷。有時候當你使其整個訓練集前向穿過一個訓練好的網路,你可能會發現你的神經元中很大一部分(比如 40%)在所有時間都是 0。

學界 | Andrej Karpathy:你為什麼應該理解反向傳播

注:如果你瞭解反向傳播並且你的網路有 ReLU,那麼你會常常為死亡的 ReLU 感到焦慮。對於你整個訓練集中的任何樣本,這些神經元都不會開啟,而且會一直保持死亡狀態。神經元在訓練過程中也會死去,通常是由於激進的學習率所造成的。更詳細的解釋請參看 CS231n 教學影片:https://youtu。be/gYpoJMlgyXA?t=20m54s

RNN 中的梯度爆炸

Vanilla RNN 是反向傳播的非直觀效應的另一個好例子。我從 CS231n 複製了一張幻燈片,上面有一份簡化的 RNN,其沒有采用任何的輸入 x,只在隱態(相當於輸入 x 一直是 0)上計算迴圈(recurrence):

學界 | Andrej Karpathy:你為什麼應該理解反向傳播

這個 RNN 透過 T 時間步展開。當你盯著反向傳遞在做什麼時,你會看到在所有隱藏狀態下會及時反向傳播的梯度訊號總是被同一矩陣(迴圈矩陣 Whh)相乘,中間穿插著非線性反向傳播。

當你拿到某個數字 a 並開始將它與另一個數字 b(例如,a*b*b*b*b。。。)相乘時會發生什麼?|b|<1 時,這個序列會趨近於 0;當|b|>1 時,這個序列會「爆炸」成無限。在某個 RNN 的反向傳遞中會發生同樣的事情,除非 b 是一個矩陣,而不僅僅是一個數,所以我們必須推理出它的最大特徵值。

注:如果你理解了反向傳播並且你正在使用 RNN,你就總是會為一定要做梯度裁剪而感到焦慮,要不然你就去使用 LSTM。詳細解釋可以看看這個影片:CS231n Winter 2016: Lecture 10: Recurrent Neural Networks, Image Captioning, LSTM(https://www。youtube。com/watch?v=yCC09vCHzF8)

自己的發現:DQN Clipping

讓我們再看一個,也是它啟發我寫出了這篇文章。昨天,我正在瀏覽 Deep Q Learning 在 TensorFlow 上的實現(看看別人是怎麼處理等同於 numpy 的 Q [:, a] 的計算,其中的 a 是個整數向量——發現 TensorFlow 並不支援該繁瑣的操作。)總之,我搜索了一下「dqn tensorflow」,點開第一條搜尋連線,發現了它的核心程式碼,下面是引用:

學界 | Andrej Karpathy:你為什麼應該理解反向傳播

如果你熟悉 DQN,你可以看到有一個 target_q_t,也就是 [reward * \gamma \argmax_a Q(s』,a)],然後有一個 q_acted,也就是採用的該 action 的 Q(s,a)。作者在這裡將上面這兩個簡化到了變數間隔(variable delta),然後他們想要把 295 行的程式碼中的 L2 loss 用 tf。reduce_mean(tf。square()) 替換掉從而進行縮減,到這裡還好。

但問題出在第 291 行。作者想要保持對異常值(outlier)的穩健性,所以如果間隔(delta)過大,它們就使用 tf。clip_by_value 修剪它。意圖很好,而且從前向傳播的角度來看也是合理的,但如果你考慮一下就會發現它引入了一個大漏洞。

這個 clip_by_value 函式在 min_delta 到 max_delta 的範圍之外還有一個 0 的區域性梯度,所以無論何時,該 delta 都在 min/max_delta 之上,該梯度會在反向傳播過程中變成確定的 0。當作者為了額外增加穩健性而很可能嘗試修剪梯度時,他們修剪了原始的 Q delta。在那個案例中,正確的做法是在 tf。square 的位置使用 Huber loss:

在 TensorFlow 中,這有點難辦;因為如果梯度超過了一個閾值,我們想做的只是修剪該梯度,但是因為我們不能直接干預梯度,所以我們必須透過定義 Huber loss 這種迂迴的方式來做這件事。在 Torch 中,這會簡單得多。

我在 DQN repo 中提交了一個問題(https://github。com/devsisters/DQN-tensorflow/issues/16),這已經被及時修復了。

總結

反向傳播是一個 leaky abstraction;它是一個有著非瑣細後果的信用分配機制。如果你試著忽視引擎下面的工作原理,因為「TensorFlow 會自動讓我的網路學習」,那麼,你就無法應對它會帶來的危險,而且在搭建和除錯神經網路時,會低效很多。

值得慶幸的是,如果呈現方式合適,反向傳播不難理解。我對此深有體會,因為在我看來,95% 的反向傳播材料都錯誤地呈現了反向傳播,通篇都是機械化的數學。我不會這麼做,在 CS231n 的課堂講解裡,反向傳播將會以更加直觀的方式展開。如果你願意花時間完成 CS231n 的課後作業,作為獎勵,手動編寫反向傳播會鞏固你的理解。

目前只有這麼多。我希望你對反向傳播向前(going forward)更加充滿疑問了,而且,仔細思考向後傳遞是在做什麼。我也留意到,在這篇 post 裡給 CS231n 打了好幾次廣告(不是故意的!),對此表示歉意:)