更新:從梯度計(jì)算到優(yōu)化器的神經(jīng)網(wǎng)絡(luò)訓(xùn)練核心)
說實(shí)話反向傳播這四個(gè)字我第一次接觸的時(shí)候覺得特別唬人感覺像是某種只有數(shù)學(xué)天才才能駕馭的黑魔法。后來自己動(dòng)手搭了一個(gè)兩層的全連接網(wǎng)絡(luò)把梯度從損失函數(shù)一路推回每一個(gè)參數(shù)才恍然大悟它本質(zhì)上就是微積分里那套鏈?zhǔn)椒▌t只是被神經(jīng)網(wǎng)絡(luò)這個(gè)外殼包裝得嚇人了一點(diǎn)而已。這篇文章想聊的就是兩件事反向傳播怎么把梯度一分不差地傳給每個(gè)參數(shù)參數(shù)更新又是怎么拿著梯度把損失一步步壓下去。無論你是剛?cè)腴T深度學(xué)習(xí)的學(xué)生還是已經(jīng)開始調(diào)模型但被梯度搞暈的工程師這條主線都可以幫你把神經(jīng)網(wǎng)絡(luò)最核心的運(yùn)行機(jī)制徹底理清。全文不會(huì)有太高深的數(shù)學(xué)只要你會(huì)一元函數(shù)求導(dǎo)就能跟得上。1. 反向傳播和參數(shù)更新到底是一對什么組合1.1 用一個(gè)比喻理解“梯度”這件事先別急著看公式我們打一個(gè)比方。假設(shè)你在打籃球投籃命中率不高你想改進(jìn)。影響命中率的因素很多手腕發(fā)力、起跳高度、出手角度、投籃位置。你怎么知道該改哪一項(xiàng)當(dāng)然是一次只改一個(gè)變量反復(fù)試錯(cuò)但這樣效率太低。更聰明的做法是找一臺高速攝像機(jī)把每一次投籃拍下來分析出手角度變化對命中率的“影響程度”。這個(gè)影響程度在數(shù)學(xué)上就是“偏導(dǎo)數(shù)”。如果出手角度是影響命中率的最大因素你就重點(diǎn)調(diào)它如果起跳高度已經(jīng)差不多了你就少動(dòng)它。反向傳播干的就是這件事給定一個(gè)損失函數(shù)它能計(jì)算出損失對網(wǎng)絡(luò)中每一個(gè)參數(shù)的“影響程度”也就是梯度。參數(shù)更新干的是另一件事知道影響程度之后決定每個(gè)參數(shù)該往哪個(gè)方向調(diào)、調(diào)多少。一個(gè)是算賬一個(gè)是行動(dòng)兩者缺一不可。1.2 為什么非得“反向”才算得動(dòng)你可能想問既然是求導(dǎo)數(shù)為什么不能用最原始的辦法對每個(gè)參數(shù)單獨(dú)求導(dǎo)理論上可以。把某個(gè)參數(shù) w 改變一個(gè)極小量 ε重新跑一次前向傳播看看損失變了多少就能算出偏導(dǎo)數(shù)的近似值。但問題是一個(gè)一千萬參數(shù)的模型做一次這樣的操作就要跑一千萬次前向傳播訓(xùn)練一次要迭代幾十萬步這計(jì)算量放到任何機(jī)器上都撐不住。反向傳播的聰明之處在于“復(fù)用中間結(jié)果”。前向傳播時(shí)網(wǎng)絡(luò)會(huì)生成一系列中間值線性層的輸出、激活函數(shù)的輸出。反向傳播從損失函數(shù)出發(fā)沿著前向傳播的路徑倒著走一遍每經(jīng)過一個(gè)中間結(jié)果就把已經(jīng)算好的上游梯度“接力”給下游。這樣整個(gè)網(wǎng)絡(luò)只需要一次正向計(jì)算、一次反向計(jì)算就能拿到所有參數(shù)的梯度。整體復(fù)雜度大概是 O(前向計(jì)算量)而不是 O(參數(shù)個(gè)數(shù) × 前向計(jì)算量)。這個(gè)思路本質(zhì)上就是動(dòng)態(tài)規(guī)劃。你計(jì)算每一層的梯度時(shí)不需要重新算一遍前序的所有東西因?yàn)槟切┝康奶荻纫呀?jīng)被上一步算過了你只是不斷做局部求導(dǎo)并相乘而已。1.3 參數(shù)更新拿著梯度該走多遠(yuǎn)梯度只是一個(gè)方向告訴你往哪個(gè)方向調(diào)損失會(huì)下降。真正決定下降快慢和穩(wěn)定性的是“走多遠(yuǎn)”也就是學(xué)習(xí)率。舉個(gè)例子你在山上想下山梯度告訴你北邊是最陡的下坡方向。但這一腳邁多大邁大了可能直接沖到對面山坡邁小了要走幾千步才到山腳。參數(shù)更新本質(zhì)上就是回答這個(gè)問題沿著梯度方向邁出多大的一步。通常的更新公式極其簡單θ_new θ_old ? 學(xué)習(xí)率 × 梯度如果你只用單個(gè)樣本的梯度來更新因?yàn)闃颖驹肼曁蟾路较驎?huì)左右亂跳。所以實(shí)踐中幾乎都是用 mini-batch 的梯度來更新取一小批樣本各自算出梯度取平均再更新。平均之后的方向更接近整體下降方向噪聲小得多。反向傳播負(fù)責(zé)算梯度更新規(guī)則負(fù)責(zé)決定怎么動(dòng)。這兩個(gè)環(huán)節(jié)一起構(gòu)成了神經(jīng)網(wǎng)絡(luò)訓(xùn)練的“心臟”。理解了這一點(diǎn)后面的所有細(xì)節(jié)都是在圍繞它們做優(yōu)化。2. 拆開反向傳播每一層梯度的流向2.1 線性層的三個(gè)梯度公式神經(jīng)網(wǎng)絡(luò)里最基礎(chǔ)的層是線性層就是 y Wx b。輸入 x 是一個(gè)向量W 是權(quán)重矩陣b 是偏置向量。前向傳播很簡單矩陣乘法加上加法。反向傳播的關(guān)鍵問題是已知損失對 y 的梯度 dL/dy怎么得到損失對 W、x、b 的梯度答案來自鏈?zhǔn)椒▌t。先看最簡單的偏置 b。y Wx b所以 y 對 b 的導(dǎo)數(shù)是一個(gè)單位矩陣損失對 b 的梯度就等于損失對 y 的梯度本身。如果訓(xùn)練時(shí)用了 batch 大小為 N 的輸入那么多個(gè)樣本的梯度需要求和也就是把 dL/dy 按 batch 維度和特征維度做規(guī)約。再看權(quán)重 W。W 的維度是 (輸出維度, 輸入維度)它和 x 之間是線性關(guān)系。對 W 中某一個(gè)元素 W[i, j] 來說它只影響 y[i] 而不會(huì)影響 y 的其他維度所以 dL/dW[i, j] dL/dy[i] × x[j]。寫成矩陣形式梯度就是 dL/dy 和 x 的外積一個(gè)“上游梯度向量”轉(zhuǎn)置后乘以“輸入向量”。最后看對輸入 x 的梯度。這一步看起來是“反向”的因?yàn)榍跋驎r(shí)是 W 作用在 x 上反向時(shí)是 W 的轉(zhuǎn)置作用在上游梯度上dL/dx W^T × dL/dy。為什么要轉(zhuǎn)置因?yàn)榍跋騻鞑r(shí)x 的每一個(gè)分量會(huì)同時(shí)影響 y 的所有輸出所以反向傳播時(shí)x 的梯度需要把 y 相同維度上的梯度按權(quán)重加權(quán)求和。轉(zhuǎn)置運(yùn)算做的正是這件事。這三個(gè)公式是所有全連接網(wǎng)絡(luò)反向傳播的基石。理解了它們CNN 里的全連接層、RNN 里的輸出層本質(zhì)都是一回事。2.2 激活函數(shù)的局部梯度激活函數(shù)是神經(jīng)網(wǎng)絡(luò)里“非線性”的來源。如果沒有激活函數(shù)多層線性變換疊加起來還是線性變換網(wǎng)絡(luò)再深也白搭。激活函數(shù)的前向計(jì)算很簡單反向傳播同樣只多一步把上游梯度乘上激活函數(shù)在當(dāng)前位置的導(dǎo)數(shù)。以最常用的 ReLU 為例它前向是 max(0, x)。當(dāng) x 大于 0 時(shí)導(dǎo)數(shù)是 1梯度直接透過當(dāng) x 小于等于 0 時(shí)導(dǎo)數(shù)是 0梯度被截?cái)嗖辉偻髠鳌_@個(gè)“截?cái)唷钡母弊饔檬侨绻粋€(gè)神經(jīng)元的輸入長期小于 0它的梯度永遠(yuǎn)為 0參數(shù)永遠(yuǎn)得不到更新這就是大家常說的“神經(jīng)元死亡”。Sigmoid 的導(dǎo)數(shù)則有一個(gè)非常漂亮的公式σ(x) σ(x) × (1 ? σ(x))。意思是前向傳播時(shí)算出的激活值 σ(x)在反向傳播時(shí)可以直接用來計(jì)算局部梯度不需要重新算一次指數(shù)函數(shù)。但它的缺陷也很明顯當(dāng)激活值接近 0 或 1 時(shí)導(dǎo)數(shù)趨近于 0梯度會(huì)在多層傳播中被乘成近似 0 的數(shù)導(dǎo)致梯度消失。這也是為什么現(xiàn)在深層網(wǎng)絡(luò)里 ReLU 及其變體更流行的原因之一。寫代碼時(shí)特別容易犯一個(gè)錯(cuò)誤把激活函數(shù)的導(dǎo)數(shù)和損失函數(shù)對輸出層的梯度搞混。損失函數(shù)對輸出 y 的梯度是 dL/dy它是一個(gè)由損失定義決定的量激活函數(shù)的局部梯度是 dy/dz它只和激活函數(shù)本身有關(guān)。反向傳播時(shí)每經(jīng)過一個(gè)運(yùn)算節(jié)點(diǎn)就要把上游梯度乘以該節(jié)點(diǎn)的局部雅可比兩個(gè)量雖然最后會(huì)相乘但各自的來源和含義完全不同。2.3 損失函數(shù)層的梯度損失函數(shù)是反向傳播的起點(diǎn)所有梯度的“源頭”。不同損失函數(shù)的梯度形式差異非常大這里只說兩個(gè)最常見的。均方誤差MSE通常配合回歸問題使用表達(dá)式是 L 0.5 × (y_pred ? y_true)2。如果前面沒有接 sigmoid 這類激活函數(shù)那么它對輸出的梯度就是 y_pred ? y_true形式非常簡潔誤差越大梯度越強(qiáng)。嚴(yán)格說一下如果 y_pred 前面接的是 sigmoid那么 MSE 對 sigmoid 輸入的梯度就不是這個(gè)簡潔形式而是會(huì)多乘一個(gè) sigmoid 導(dǎo)數(shù)這會(huì)造成“輸出接近 0 或 1 時(shí)梯度很小”的問題訓(xùn)練會(huì)變得非常緩慢。交叉熵則更像分類問題的標(biāo)準(zhǔn)配置。對于二分類用 sigmoid 加二元交叉熵組合起來之后損失對輸出的梯度也可以簡化成 y_pred ? y_true 這種簡潔形式這才是“配對了”的損失和激活函數(shù)。如果是多分類softmax 加交叉熵也有同樣的簡化效果實(shí)現(xiàn)時(shí)把這兩者合并成一個(gè)“softmaxloss”的節(jié)點(diǎn)來計(jì)算梯度不僅能提高計(jì)算效率還能避免中間項(xiàng)指數(shù)運(yùn)算導(dǎo)致的數(shù)值溢出。所以我的習(xí)慣是除非你有特別的理由否則不要把損失函數(shù)和激活函數(shù)當(dāng)作兩個(gè)獨(dú)立節(jié)點(diǎn)去手工推梯度而是把常用的組合當(dāng)成一個(gè)整體來理解。很多深度學(xué)習(xí)框架內(nèi)部也是這么做的。2.4 深層網(wǎng)絡(luò)中的梯度流到多層網(wǎng)絡(luò)里反向傳播就是把這些局部規(guī)則串起來。假設(shè)網(wǎng)絡(luò)是輸入→線性層→激活→線性層→激活→損失梯度從損失出發(fā)逐層倒推dL/dy_pred → dL/dz_output → dL/dW2更新權(quán)重同時(shí) dL/da1 → dL/dz_hidden → dL/dW1。每一層的梯度計(jì)算都依賴上一層已經(jīng)傳下來的 dL/dz。只要前一層算完了后一層的梯度就能接著往下傳不需要回看更原始的輸入。這里有個(gè)很關(guān)鍵的感受梯度在網(wǎng)絡(luò)里是一路“連乘”過去的。每經(jīng)過一個(gè)權(quán)重矩陣和激活函數(shù)梯度就乘以一次某個(gè)矩陣的范數(shù)或激活導(dǎo)數(shù)的標(biāo)量。如果大部分因子都大于 1梯度會(huì)指數(shù)增長變成梯度爆炸如果大部分因子都小于 1梯度會(huì)指數(shù)衰減變成梯度消失。所以你看到的所有初始化方法、激活函數(shù)選型、BN批歸一化、殘差連接本質(zhì)上都是在試圖控制這條梯度連乘鏈的“量級”不要失控。3. 參數(shù)更新的幾種主流策略3.1 樸素 SGD直接沿著梯度走最樸素的更新方式就是 SGD每個(gè)參數(shù)都按同一個(gè)學(xué)習(xí)率沿著梯度的反方向走一步。公式前面已經(jīng)寫過θ θ ? η × grad。訓(xùn)練時(shí)把樣本分成小 batch每個(gè) batch 算一次梯度更新一次。用著用著你會(huì)發(fā)現(xiàn)它的幾個(gè)問題。第一個(gè)是學(xué)習(xí)率的選擇很講究設(shè)小了訓(xùn)練半天不收斂設(shè)大了損失會(huì)在最優(yōu)值附近來回震蕩甚至直接發(fā)散。第二個(gè)是它對梯度的各個(gè)維度一視同仁但實(shí)際場景里參數(shù)的量級和使用頻率差異很大有些參數(shù)應(yīng)該走大步有些應(yīng)該走小步統(tǒng)一用一個(gè)學(xué)習(xí)率不夠靈活。不過樸素 SGD 有一個(gè)好處是簡單、可解釋而且在一些精心調(diào)整過的任務(wù)上加上動(dòng)量和學(xué)習(xí)率衰減效果往往能逼近甚至超過更花哨的優(yōu)化器。它仍然是理解后面所有優(yōu)化器的基準(zhǔn)。3.2 動(dòng)量帶著慣性更新動(dòng)量Momentum的思想特別直覺既然單次梯度噪聲大那我就記住歷史梯度的“平均方向”讓每次更新既參考當(dāng)前梯度又參考過去一段時(shí)間的方向。更新公式一般寫成v μ × v ? η × gradθ θ v這里的 μ 通常取 0.9 左右意思是上一次的速度保留 90%再疊加當(dāng)前這次梯度的貢獻(xiàn)。你可以把它想象成小球從山上滾下來它不會(huì)每一步都被局部小坑帶偏因?yàn)閼T性會(huì)讓它沖過那些狹窄的局部極小值最終更可能落到更低的坑里。實(shí)際使用中動(dòng)量能明顯加快收斂速度尤其是在損失 landscape 比較狹長的碗狀區(qū)域。方向重復(fù)的成分會(huì)被動(dòng)量積累放大方向來回震蕩的成分會(huì)被逐步抵消。這也是為什么它比樸素 SGD 在深層網(wǎng)絡(luò)里有用得多。3.3 自適應(yīng)方法每個(gè)參數(shù)一個(gè)學(xué)習(xí)率模型里不同參數(shù)的梯度量級可能差好幾個(gè)數(shù)量級。有的參數(shù)訓(xùn)練樣本中出現(xiàn)頻率高梯度大學(xué)習(xí)率應(yīng)該小一點(diǎn)有的參數(shù)出現(xiàn)頻率低梯度小學(xué)習(xí)率應(yīng)該大一點(diǎn)。于是就有了自適應(yīng)學(xué)習(xí)率方法。AdaGrad 會(huì)為每個(gè)參數(shù)累計(jì)歷史梯度的平方和然后用當(dāng)前梯度除以這個(gè)累計(jì)值的平方根作為實(shí)際更新步長。梯度大的參數(shù)被自動(dòng)壓低步長梯度小的參數(shù)被自動(dòng)放大步長。但它的缺點(diǎn)是累計(jì)值只增不減訓(xùn)練時(shí)間長了學(xué)習(xí)率會(huì)變得無窮小直接停擺。RMSProp 針對這個(gè)問題做了改進(jìn)用指數(shù)滑動(dòng)平均代替了簡單累加只記住近期的梯度平方信息。這樣學(xué)習(xí)率不會(huì)一直衰減到 0能保持合理范圍。本質(zhì)上它是在估算“每個(gè)維度近期梯度的典型大小”再用這個(gè)典型大小去歸一化當(dāng)前梯度。3.4 Adam動(dòng)量和自適應(yīng)學(xué)習(xí)的合體Adam 在實(shí)踐中幾乎成了默認(rèn)選項(xiàng)因?yàn)樗瑫r(shí)融合了兩代優(yōu)化器的優(yōu)點(diǎn)它既用一階矩估算梯度方向的歷史趨勢相當(dāng)于動(dòng)量又用二階矩估算梯度幅度的歷史趨勢相當(dāng)于自適應(yīng)學(xué)習(xí)率。為了防止訓(xùn)練前期歷史信息太少導(dǎo)致的偏差它還會(huì)做一個(gè)偏差修正讓前幾步的估計(jì)更準(zhǔn)確。常用的默認(rèn)超參數(shù)是一階矩衰減系數(shù) β1 0.9二階矩衰減系數(shù) β2 0.999防除零的小項(xiàng) ε 1e-8。這幾個(gè)值在絕大多數(shù)問題上都能直接工作不需要你花太多時(shí)間去調(diào)。不過 Adam 也有被人吐槽的地方它對學(xué)習(xí)率依然敏感而且由于自適應(yīng)機(jī)制的存在某些情況下權(quán)重更新步長會(huì)出現(xiàn)不自然的放大效應(yīng)導(dǎo)致泛化效果不如精心調(diào)過的 SGD 動(dòng)量。所以實(shí)踐中大家會(huì)說“Adam 幫你快速找到一個(gè)好區(qū)域SGD 幫你精調(diào)”。3.5 優(yōu)化器到底怎么選選優(yōu)化器我認(rèn)為分場景看更現(xiàn)實(shí)。表格列出常見對比優(yōu)化器核心思路優(yōu)點(diǎn)典型問題適用場景SGD統(tǒng)一學(xué)習(xí)率直走簡單、可解釋、泛化好學(xué)習(xí)率難調(diào)、收斂慢已經(jīng)調(diào)好學(xué)習(xí)率的小中規(guī)模任務(wù)SGDMomentum累積歷史梯度減少震蕩、加速收斂仍需手動(dòng)調(diào)學(xué)習(xí)率常規(guī)監(jiān)督學(xué)習(xí)可精調(diào)RMSProp自適應(yīng)縮放每參數(shù)對尺度差異不敏感二階矩估計(jì)需要衰減參數(shù)非平穩(wěn)目標(biāo)、RNN 類任務(wù)Adam動(dòng)量自適應(yīng)默認(rèn)好用、收斂快可能泛化不如精調(diào) SGD探索期、大規(guī)模模型、圖像分類等我的個(gè)人傾向是拿到一個(gè)新任務(wù)先無腦用 Adam 開跑把網(wǎng)絡(luò)結(jié)構(gòu)、數(shù)據(jù)預(yù)處理、損失函數(shù)這些更本質(zhì)的問題調(diào)對。等到模型結(jié)構(gòu)基本穩(wěn)定、要刷最后幾個(gè)點(diǎn)的精度時(shí)再切到 SGD 動(dòng)量配合學(xué)習(xí)率衰減用網(wǎng)格搜索挑一個(gè)合適的學(xué)習(xí)率。這個(gè)流程我在幾個(gè)項(xiàng)目里試過通常比一開始就死磕 SGD 學(xué)習(xí)率要快得多。4. 手算一遍從零做一次反向傳播和參數(shù)更新4.1 網(wǎng)絡(luò)設(shè)定與數(shù)值初始化看再多的公式都不如手算一遍來得透徹。我設(shè)計(jì)了一個(gè)極小但完整的網(wǎng)絡(luò)輸入 x 只有 1 個(gè)維度中間層只有 1 個(gè)神經(jīng)元輸出層也只有 1 個(gè)神經(jīng)元。結(jié)構(gòu)是這樣的輸入 x → 線性層w1, b1→ Sigmoid 激活 → 線性層w2, b2→ 輸出 y_pred為了手算方便我不給最終輸出接激活函數(shù)。損失函數(shù)用 MSE并且寫成 L 0.5 × (y_pred ? y_true)2這個(gè) 0.5 是為了求導(dǎo)后消掉平方指數(shù)讓梯度表達(dá)式更干凈。初始化數(shù)值如下輸入 x 1目標(biāo) y_true 0.5w1 0.2b1 0.1w2 0.3b2 0.0學(xué)習(xí)率 η 0.54.2 前向傳播計(jì)算按照網(wǎng)絡(luò)結(jié)構(gòu)一步步算下去。第一個(gè)線性層z1 w1 × x b1 0.2 × 1 0.1 0.3經(jīng)過 Sigmoid 激活a1 sigmoid(0.3) 1 / (1 e^(-0.3))e^(-0.3) ≈ 0.7408所以 1 0.7408 1.7408a1 ≈ 0.5744。第二個(gè)線性層b2 為 0所以y_pred a1 × w2 b2 0.5744 × 0.3 0 ≈ 0.1723損失函數(shù)L 0.5 × (y_pred ? y_true)2 0.5 × (0.1723 ? 0.5)2 0.5 × (?0.3277)2 0.5 × 0.1074 ≈ 0.0537前向傳播結(jié)束我們得到了預(yù)測值 0.1723它距離目標(biāo) 0.5 還挺遠(yuǎn)損失大約 0.054。4.3 反向傳播手工推導(dǎo)現(xiàn)在從損失開始一步一步往回推。我先寫結(jié)論反向傳播的所有中間量都依賴于“損失對當(dāng)前節(jié)點(diǎn)輸出的梯度”我按層切開計(jì)算。先從最末端開始。損失對 y_pred 的梯度dL/dy_pred y_pred ? y_true 0.1723 ? 0.5 ?0.3277這個(gè)值是整個(gè)反向傳播的源頭它是負(fù)的意思是增加 y_pred 能降低損失。接著更新輸出層的參數(shù)。因?yàn)?y_pred w2 × a1 b2所以dL/dw2 dL/dy_pred × a1 ?0.3277 × 0.5744 ≈ ?0.1882dL/db2 dL/dy_pred ?0.3277然后繼續(xù)往回傳計(jì)算損失對 a1 的梯度。由于 y_pred 對 a1 的導(dǎo)數(shù)是 w2所以dL/da1 dL/dy_pred × w2 ?0.3277 × 0.3 ≈ ?0.0983接下來經(jīng)過 Sigmoid 激活。Sigmoid 在 z1 0.3 處的局部導(dǎo)數(shù)是σ(0.3) a1 × (1 ? a1) 0.5744 × (1 ? 0.5744) 0.5744 × 0.4256 ≈ 0.2445所以損失對 z1 的梯度dL/dz1 dL/da1 × σ(0.3) ?0.0983 × 0.2445 ≈ ?0.0240最后回到第一個(gè)線性層。 z1 w1 × x b1于是dL/dw1 dL/dz1 × x ?0.0240 × 1 ?0.0240dL/db1 dL/dz1 ?0.0240到此四個(gè)參數(shù)的梯度都拿到了dw1 ≈ ?0.0240db1 ≈ ?0.0240dw2 ≈ ?0.1882db2 ≈ ?0.3277。4.4 參數(shù)更新效果驗(yàn)證按前面設(shè)定的學(xué)習(xí)率 η 0.5用最樸素的 SGD 更新參數(shù)w1 0.2 ? 0.5 × (?0.0240) 0.2 0.0120 0.2120b1 0.1 ? 0.5 × (?0.0240) 0.1 0.0120 0.1120w2 0.3 ? 0.5 × (?0.1882) 0.3 0.0941 0.3941b2 0.0 ? 0.5 × (?0.3277) 0.0 0.1639 0.1639注意到所有更新都在“增大”參數(shù)因?yàn)閾p失對它們的梯度都是負(fù)的增大參數(shù)意味著減小損失。再用新參數(shù)做一次前向傳播驗(yàn)證損失是否真下降了。第一個(gè)線性層z1 0.2120 × 1 0.1120 0.3240a1 sigmoid(0.3240) ≈ 0.5803第二個(gè)線性層y_pred 0.5803 × 0.3941 0.1639 ≈ 0.2287 0.1639 0.3926損失L 0.5 × (0.3926 ? 0.5)2 0.5 × (?0.1074)2 ≈ 0.0058看損失從 0.0537 降到了 0.0058一步就降了一個(gè)量級。這就是反向傳播加參數(shù)更新配合起來的效果。你完全可以照著這個(gè)流程換成 ReLU、換成更大的網(wǎng)絡(luò)用同樣的鏈?zhǔn)椒▌t層層往回推每步都是簡單的乘法。我也把整個(gè)計(jì)算過程整理成一張表方便你對數(shù)節(jié)點(diǎn)前向值反向參數(shù)數(shù)值z10.3dL/dz1?0.0240a10.5744dL/da1?0.0983y_pred0.1723dL/dy_pred?0.3277w10.2dL/dw1?0.0240b10.1dL/db1?0.0240w20.3dL/dw2?0.1882b20.0dL/db2?0.32775. 調(diào)參實(shí)戰(zhàn)中的常見問題與排查5.1 梯度消失和梯度爆炸梯度消失的特征很好辨認(rèn)網(wǎng)絡(luò)前面的層參數(shù)幾乎不更新?lián)p失降了一段之后就不再變化或者訓(xùn)練曲線在某個(gè)平臺期停滯特別久。梯度爆炸的特征更明顯訓(xùn)練過程中可能出現(xiàn)極端大的參數(shù)值loss 直接變成 NaN權(quán)重更新一步后整個(gè)網(wǎng)絡(luò)數(shù)值結(jié)構(gòu)都被沖垮。原因剛才說過就是梯度連乘鏈的乘積要么趨近于 0要么趨近于無窮。排查的時(shí)候先從架構(gòu)入手激活函數(shù)用 Sigmoid 的深層網(wǎng)絡(luò)幾乎都會(huì)遇到梯度消失換成 ReLU 或類 ReLU 激活是第一選擇殘差連接可以給梯度一條“高速公路”讓信號不經(jīng)過任何激活就直接向后傳批歸一化通過對每層輸出做標(biāo)準(zhǔn)化把激活值拉回一個(gè)有效區(qū)間也能明顯緩解梯度消失的問題。梯度爆炸的話最常用的急救方法是梯度裁剪算完梯度之后如果梯度的全局范數(shù)超過某個(gè)閾值就把它等比例縮小到閾值以內(nèi)。我在實(shí)際項(xiàng)目中就把閾值設(shè)為 1.0 或 5.0效果非常直接訓(xùn)練穩(wěn)定性立刻提升。注意裁剪不等于根治根本原因往往在初始化、數(shù)據(jù)尺度或者網(wǎng)絡(luò)結(jié)構(gòu)上。5.2 學(xué)習(xí)率導(dǎo)致的振蕩與不收斂訓(xùn)練時(shí)如果發(fā)現(xiàn) loss 曲線像心電圖一樣劇烈震蕩第一反應(yīng)永遠(yuǎn)是先調(diào)小學(xué)習(xí)率。學(xué)習(xí)率過大時(shí)每一次更新都會(huì)跨過最優(yōu)點(diǎn)的“狹窄區(qū)域”在兩邊來回彈。我遇到過最經(jīng)典的現(xiàn)象是用學(xué)習(xí)率 0.01 時(shí) loss 完全不降改成 0.001 后開始穩(wěn)定下降這基本就是學(xué)習(xí)率差了數(shù)量級的典型例子。反過來學(xué)習(xí)率太小也有問題loss 確實(shí)在降但每步只降一點(diǎn)點(diǎn)訓(xùn)練幾萬步還沒到目標(biāo)精度。這時(shí)候可以用學(xué)習(xí)率預(yù)熱策略前幾百步用一個(gè)很小的學(xué)習(xí)率慢慢熱身之后再切到正常學(xué)習(xí)率最后再按 epoch 做余弦退火或分段衰減。這種三段式學(xué)習(xí)率調(diào)度在很多項(xiàng)目里都是穩(wěn)定可靠的選擇。怎么判斷當(dāng)前學(xué)習(xí)率方向?qū)Σ粚ξ业膽腥朔▌t是先嘗試一小組數(shù)量級比如 0.1、0.01、0.001分別跑幾千步畫在同一個(gè)坐標(biāo)系里看曲線趨勢。哪個(gè)方向是平滑單調(diào)下降的就在哪個(gè)數(shù)量級附近微調(diào)。這個(gè)做法雖然簡單但每次都能大大縮小搜索范圍。5.3 梯度檢查確認(rèn)梯度沒算錯(cuò)反向傳播是最容易出現(xiàn)“差一個(gè)負(fù)號”“少乘一個(gè)權(quán)重”“搞混了轉(zhuǎn)置”的地方。寫網(wǎng)絡(luò)代碼時(shí)尤其是手寫反向傳播必須做梯度檢查。原理是用數(shù)值微分來近似真實(shí)梯度。取一個(gè)很小的 h比如 1e-5對某個(gè)參數(shù) w 計(jì)算前后向各一次差值數(shù)值梯度 ≈ (L(w h) ? L(w ? h)) / (2h)這就是中心差分它比單邊差分 (L(w h) ? L(w)) / h 更準(zhǔn)確誤差階數(shù)更高所以我一直都用中心差分做檢查。然后把算出來的“解析梯度”和“數(shù)值梯度”做相對比較公式是 |解析梯度 ? 數(shù)值梯度| / (|解析梯度| |數(shù)值梯度|)。相對誤差在 1e-6 到 1e-4 之內(nèi)基本可以認(rèn)為反向傳播寫對了。做梯度檢查時(shí)有三個(gè)注意點(diǎn)第一檢查時(shí)一定要關(guān)閉 dropout 這類隨機(jī)操作和任何會(huì)改變計(jì)算圖的東西否則兩次前向傳播之間的損失函數(shù)都不同數(shù)值梯度根本不準(zhǔn)第二h 不能取得太小太小會(huì)因?yàn)楦↑c(diǎn)數(shù)精度損失讓數(shù)值微分反而變差通常取 1e-5 到 1e-6 之間比較好第三如果模型太大不要所有參數(shù)都檢查隨機(jī)抽一小批參數(shù)比如每類層抽幾組 W 和 b 檢查即可因?yàn)橥粋€(gè)層的代碼邏輯往往是共享的。5.4 訓(xùn)練中的異常值與隱蔽錯(cuò)誤訓(xùn)練到一半 loss 變成 NaN 或者出現(xiàn)無窮大大概率的幾類原因數(shù)據(jù)里有 NaN 值、數(shù)值運(yùn)算中除以了 0、log 函數(shù)的輸入為 0、或者梯度爆炸導(dǎo)致權(quán)重變?yōu)闊o窮。排查時(shí)先檢查輸入數(shù)據(jù)把任何 NaN 值都處理掉再檢查損失函數(shù)看里邊有沒有 log、除法、sqrt 這類容易出問題的運(yùn)算必要時(shí)在分母上加一個(gè)極小值 ε。還有一個(gè)很容易被忽略的問題數(shù)值穩(wěn)定性。比如 softmax 的實(shí)現(xiàn)如果先算 exp(z) 再歸一化當(dāng) z 很大時(shí) exp(z) 會(huì)直接溢出為無窮大。正確的做法是先減去最大值再算 exp這樣最大值變成 0其他項(xiàng)都不會(huì)溢出。類似這種實(shí)現(xiàn)細(xì)節(jié)在小規(guī)模測試時(shí)數(shù)據(jù)范圍小看不出來一旦數(shù)據(jù)量增大就會(huì)突然爆炸。還有一些更隱蔽的問題和代碼邏輯有關(guān)。比如權(quán)重初始化時(shí)把標(biāo)準(zhǔn)差設(shè)得過大第一層輸出就落進(jìn) Sigmoid 的飽和區(qū)梯度瞬間被壓成 0比如 batch size 太小且數(shù)據(jù)分批次不均導(dǎo)致梯度方向長期偏斜再比如更新參數(shù)時(shí)是在原權(quán)重上原地賦值還是拷貝后賦值深拷貝和淺拷貝用錯(cuò)可能會(huì)讓上一次的更新被意外疊加。遇到 loss 反常時(shí)我的習(xí)慣是先把程序和訓(xùn)練流程中的“確定性”找回來固定隨機(jī)種子然后一個(gè)一個(gè)因素去隔離而不是一股腦改十個(gè)超參數(shù)。6. 養(yǎng)成好習(xí)慣讓我少走彎路的幾條經(jīng)驗(yàn)6.1 訓(xùn)練前先做“小數(shù)據(jù)過擬合”測試接手任何一個(gè)新模型我會(huì)先做一次過擬合測試只拿幾十個(gè)樣本訓(xùn)練足夠多的 epoch看模型能不能把損失降到非常低。如果連幾十個(gè)樣本都過擬合不了那說明代碼邏輯、梯度計(jì)算、數(shù)據(jù)管道里大概率有 bug這時(shí)候先不要?jiǎng)觾?yōu)化器也不要調(diào)學(xué)習(xí)率先把代碼修對。這個(gè)習(xí)慣幫我排除了大量低級錯(cuò)誤比自己對照公式一行行摳要省時(shí)間得多。過擬合測試通過之后再切換到正常數(shù)據(jù)量、正常正則化和數(shù)據(jù)增強(qiáng)。這時(shí)候出現(xiàn)的現(xiàn)象才值得用優(yōu)化器、學(xué)習(xí)率這些手段去調(diào)。6.2 每次只改一個(gè)變量把實(shí)驗(yàn)記錄當(dāng)作資產(chǎn)訓(xùn)練深度學(xué)習(xí)模型非常容易陷入“同時(shí)改了三個(gè)東西”的混亂狀態(tài)。調(diào)學(xué)習(xí)率的同時(shí)改了 batch size又換了數(shù)據(jù)增強(qiáng)方式最后損失確實(shí)下降了但你根本不知道功勞屬于誰。我踩過幾次這種坑之后給自己定了一條紀(jì)律任何一個(gè)實(shí)驗(yàn)周期所有變量保持不變只動(dòng)其中一個(gè)。想調(diào)學(xué)習(xí)率就只調(diào)學(xué)習(xí)率想改 batch size就先凍結(jié)學(xué)習(xí)率。每一步改動(dòng)都記錄下來包括 IDE 里隨手改的數(shù)值也要記因?yàn)椤斑@次跑得好”和“上次跑得好”大家通常隔了一個(gè)星期才對比遺忘是常態(tài)。我會(huì)給每次實(shí)驗(yàn)記錄這樣的基本信息隨機(jī)種子、學(xué)習(xí)率、優(yōu)化器、batch size、網(wǎng)絡(luò)層數(shù)、數(shù)據(jù)預(yù)處理方式、loss 曲線截圖、跑出來的最終量化指標(biāo)。排查問題時(shí)翻歷史記錄比重新跑一次實(shí)驗(yàn)高效得多。深度學(xué)習(xí)項(xiàng)目本質(zhì)上是在大量實(shí)驗(yàn)里找規(guī)律沒有記錄的實(shí)驗(yàn)等于沒做。反向傳播和參數(shù)更新這條線是理解所有后續(xù)高級調(diào)優(yōu)的底座。把第 4 節(jié)的例子親手算一遍把常見問題和排查方法心里有數(shù)再去接觸分布式訓(xùn)練、混合精度、大規(guī)模并行這些進(jìn)階話題時(shí)你會(huì)發(fā)現(xiàn)它們本質(zhì)上都是在解決同一個(gè)問題如何讓梯度更準(zhǔn)確、更高效地流動(dòng)并讓參數(shù)沿著更好的方向更新。