與梯度推導實戰(zhàn))
1. 從Softmax分類器說起這個作業(yè)到底在練什么如果你正在啃深度學習入門的第一塊硬骨頭大概率繞不開圖像分類這條主線。Assignment1里從KNN到SVM再到Softmax本質(zhì)上是在用四種不同的視角回答同一個問題怎么讓機器把一張圖分到正確的類別里。Softmax這個環(huán)節(jié)特別關(guān)鍵因為它是從傳統(tǒng)線性分類器過渡到神經(jīng)網(wǎng)絡(luò)損失函數(shù)的橋梁——你現(xiàn)在手寫的這個Softmax損失后面在卷積網(wǎng)絡(luò)里會以幾乎一模一樣的形態(tài)出現(xiàn)只是輸入從原始像素變成了特征圖。這個作業(yè)的核心任務(wù)其實就三件事實現(xiàn)一個數(shù)值穩(wěn)定的Softmax損失函數(shù)、推導并實現(xiàn)它的解析梯度、用向量化方式完成參數(shù)更新。聽起來簡單但真正動手寫的時候新手最容易在三個地方翻車指數(shù)運算的數(shù)值溢出、梯度推導的鏈式法則、以及循環(huán)改向量化時的維度對齊。我見過太多人損失函數(shù)寫對了但梯度算錯結(jié)果訓練半天準確率卡在10%不動排查一晚上才發(fā)現(xiàn)是某個轉(zhuǎn)置寫反了。這篇文章我會把整個Softmax作業(yè)拆成可復現(xiàn)的步驟從數(shù)學推導到代碼實現(xiàn)從樸素循環(huán)到全向量化每一步都講清楚為什么這么做。適合正在做這個作業(yè)但卡住的同學也適合想重新梳理線性分類器底層邏輯的從業(yè)者。你不需要有很強的數(shù)學背景但需要會一點Python和NumPy的基本操作。2. 核心原理拆解Softmax到底在算什么2.1 從得分到概率為什么需要Softmax線性分類器的基本形式是 ( f(x, W) Wx b )輸入一張圖片拉平成向量 ( x )乘以權(quán)重矩陣 ( W )得到每個類別的得分score。這個得分是實數(shù)可正可負大小關(guān)系代表了模型對類別的偏好但它不是概率——你沒法說“這張圖有3.2分是貓”。Softmax的作用就是把這個得分向量變成概率分布。公式很簡潔[ P(yk|x) \frac{e^{s_k}}{\sum_j e^{s_j}} ]其中 ( s_k ) 是第k類的得分。指數(shù)運算保證了所有輸出為正除以總和保證了加起來等于1。這樣我們就得到了一個合法的概率分布可以套用交叉熵損失來衡量預測和真實標簽的差距。交叉熵損失的形式是 ( L_i -\log P(yy_i|x_i) )也就是取正確類別概率的負對數(shù)。直覺上很好理解正確類別的概率越接近1負對數(shù)越接近0損失越小概率越接近0負對數(shù)越大損失越大。這個損失函數(shù)是凸的對于線性模型而言而且梯度形式非常優(yōu)雅這也是它成為分類任務(wù)標配的原因。2.2 數(shù)值穩(wěn)定那個讓新手翻車的指數(shù)溢出直接按公式實現(xiàn)Softmax第一版代碼大概率會炸。原因在于指數(shù)函數(shù)增長極快得分稍微大一點比如100( e^{100} ) 就溢出了NumPy會返回inf然后inf除以inf得到nan整個損失變成nan訓練直接崩掉。解決辦法是利用Softmax的一個不變性給所有得分同時減去一個常數(shù)結(jié)果不變。數(shù)學上[ \frac{e^{s_k}}{\sum_j e^{s_j}} \frac{e^{s_k - C}}{\sum_j e^{s_j - C}} ]證明很簡單分子分母同乘 ( e^{-C} ) 即可。實踐中我們?nèi)?( C \max(s) )這樣最大的指數(shù)變成 ( e^0 1 )其余都是小于1的正數(shù)完全避免了溢出。這個技巧叫“數(shù)值穩(wěn)定化”是所有深度學習框架內(nèi)部都在做的事你手寫一遍就理解為什么PyTorch的CrossEntropyLoss不需要你手動做這個操作了。注意減去最大值之后分母至少包含一項 ( e^0 1 )所以分母永遠不會是0不會出現(xiàn)除零錯誤。這個細節(jié)在推導梯度時也要保持一致。2.3 梯度推導鏈式法則的完整展開Softmax的梯度推導是Assignment1里數(shù)學味最濃的部分。設(shè)得分 ( s Wx )損失 ( L_i -\log(\frac{e^{s_{y_i}}}{\sum_j e^{s_j}}) -s_{y_i} \log\sum_j e^{s_j} )。對得分 ( s_k ) 求偏導分兩種情況當 ( k y_i )正確類別時 [ \frac{\partial L_i}{\partial s_k} -1 \frac{e^{s_k}}{\sum_j e^{s_j}} P_k - 1 ]當 ( k eq y_i )錯誤類別時 [ \frac{\partial L_i}{\partial s_k} \frac{e^{s_k}}{\sum_j e^{s_j}} P_k ]合并起來就是 ( \frac{\partial L_i}{\partial s_k} P_k - \mathbb{1}(k y_i) )。這個形式非常簡潔預測概率減去真實標簽的one-hot編碼。再對 ( W ) 求導根據(jù)鏈式法則 ( \frac{\partial L_i}{\partial W} \frac{\partial L_i}{\partial s} \cdot \frac{\partial s}{\partial W} (P - \mathbb{1}) \cdot x^T )。這就是解析梯度的最終形式也是我們代碼里要實現(xiàn)的。3. 代碼實現(xiàn)從樸素循環(huán)到全向量化3.1 損失函數(shù)的樸素實現(xiàn)先寫一個最直白的版本用循環(huán)逐樣本計算方便理解每一步在做什么import numpy as np def softmax_loss_naive(W, X, y, reg): loss 0.0 dW np.zeros_like(W) num_train X.shape[0] num_classes W.shape[1] for i in range(num_train): scores X[i].dot(W) # (C,) # 數(shù)值穩(wěn)定化 scores - np.max(scores) exp_scores np.exp(scores) probs exp_scores / np.sum(exp_scores) # 交叉熵損失 loss -np.log(probs[y[i]]) # 梯度計算 for j in range(num_classes): dW[:, j] (probs[j] - (j y[i])) * X[i] loss / num_train dW / num_train loss reg * np.sum(W * W) dW 2 * reg * W return loss, dW這段代碼邏輯清晰但有兩個問題一是雙重循環(huán)在CIFAR-10的50000張訓練集上跑一次要幾十秒二是內(nèi)層循環(huán)可以合并。不過作為理解梯度的第一步這個版本值得親手敲一遍。實操心得寫樸素版本時建議先用一個極小的數(shù)據(jù)集比如5張圖、3個類別手動驗證。打印出scores、probs、loss的中間值拿計算器核對一兩個數(shù)字確認無誤后再上完整數(shù)據(jù)集。我當初就是靠這個笨辦法發(fā)現(xiàn)梯度里少乘了一個X[i]。3.2 向量化損失函數(shù)一次算完所有樣本向量化的核心思想是把循環(huán)維度變成矩陣運算。對于損失部分def softmax_loss_vectorized(W, X, y, reg): num_train X.shape[0] scores X.dot(W) # (N, C) scores - np.max(scores, axis1, keepdimsTrue) # 數(shù)值穩(wěn)定 exp_scores np.exp(scores) probs exp_scores / np.sum(exp_scores, axis1, keepdimsTrue) # (N, C) # 損失取正確類別的概率取負對數(shù)求平均 correct_logprobs -np.log(probs[np.arange(num_train), y]) loss np.sum(correct_logprobs) / num_train loss reg * np.sum(W * W) # 梯度 dscores probs.copy() # (N, C) dscores[np.arange(num_train), y] - 1 dW X.T.dot(dscores) / num_train dW 2 * reg * W return loss, dW關(guān)鍵點在于probs[np.arange(num_train), y]這個高級索引它一次性取出每個樣本正確類別的概率避免了循環(huán)。梯度部分dscores先復制概率矩陣然后在正確類別位置減1得到的就是 ( P - \mathbb{1} )再左乘 ( X^T ) 完成對所有樣本的累加。注意keepdimsTrue這個參數(shù)很容易漏。如果不加np.max(scores, axis1)返回的是形狀(N,)的數(shù)組廣播時會出現(xiàn)維度不匹配或者錯誤的對齊。加上之后保持(N, 1)才能正確地和(N, C)的scores做減法。3.3 梯度檢查別跳過這一步作業(yè)里提供了grad_check_sparse函數(shù)用數(shù)值梯度驗證解析梯度。數(shù)值梯度的原理是中心差分[ \frac{\partial L}{\partial W_{ij}} \approx \frac{L(W_{ij} h) - L(W_{ij} - h)}{2h} ]取 ( h 10^{-5} ) 左右相對誤差應該在 ( 10^{-8} ) 量級。如果誤差大于 ( 10^{-4} )說明解析梯度有bug。我踩過的坑是梯度檢查通過不代表代碼完全正確因為數(shù)值梯度只采樣了部分維度。有一次我的梯度在正確類別上少減了1但采樣點恰好沒覆蓋到檢查通過了訓練時準確率卻上不去。所以梯度檢查通過后還要看訓練曲線是否正常收斂。4. 訓練與調(diào)參讓模型真正跑起來4.1 超參數(shù)搜索學習率和正則化強度作業(yè)要求用驗證集調(diào)學習率和正則化強度。我的經(jīng)驗是先用對數(shù)尺度粗搜再在最優(yōu)附近細搜。比如學習率試[1e-7, 5e-7, 1e-6, 5e-6, 1e-5]正則化強度試[1e-5, 1e-4, 1e-3, 1e-2]組合起來跑一輪看驗證集準確率。CIFAR-10上Softmax分類器大概能到35%-38%的準確率。如果低于30%大概率是學習率太大導致震蕩或者正則化太強導致欠擬合。如果訓練準確率遠高于驗證準確率說明過擬合了該加大正則化。學習率正則化強度訓練準確率驗證準確率1e-71e-428%27%1e-61e-436%35%1e-51e-438%36%1e-61e-233%32%這張表是我當時跑出來的典型結(jié)果可以看到學習率在1e-6到1e-5之間比較合適正則化1e-4是個不錯的起點。4.2 權(quán)重可視化看看模型學到了什么訓練完之后把權(quán)重矩陣 ( W ) 的每一列還原成32x32x3的圖像可以直觀看到每個類別的模板。你會發(fā)現(xiàn)Softmax學到的模板有點像類別的平均圖像——比如貓的模板隱約有貓臉的輪廓車的模板有車身的形狀。這是因為線性分類器本質(zhì)上是在找每個類別的“原型”通過比較輸入和原型的相似度來分類。這個可視化不僅是作業(yè)要求也是排查問題的好工具。如果某個類別的模板看起來像噪聲說明這個類別的樣本太少或者學習率有問題。如果所有模板長得差不多說明模型沒學到東西可能權(quán)重初始化太小或者學習率太低。4.3 與SVM的對比為什么Softmax更常用Assignment1里同時實現(xiàn)了SVM和Softmax兩者都是線性分類器但損失函數(shù)不同。SVM用的是合頁損失hinge loss只關(guān)心正確類別的得分是否比錯誤類別高出至少一個邊界值Softmax用的是交叉熵關(guān)心的是整個概率分布。實際效果上兩者在CIFAR-10上的準確率差不多但Softmax有兩個優(yōu)勢一是輸出是概率可以給出置信度二是損失函數(shù)光滑可導優(yōu)化起來更穩(wěn)定。這也是為什么后續(xù)的神經(jīng)網(wǎng)絡(luò)幾乎都用Softmax加交叉熵的組合。實操心得如果你在Softmax上調(diào)參調(diào)不動了可以回頭看看SVM的實現(xiàn)對比兩者的梯度形式。SVM的梯度只在邊界內(nèi)非零Softmax的梯度處處非零這個差異在訓練初期表現(xiàn)得特別明顯——Softmax收斂更平滑但每輪計算量稍大。5. 常見報錯與排查實錄5.1 損失變成nan指數(shù)溢出的典型癥狀這是最常見的報錯沒有之一。癥狀是第一次迭代loss就是nan或者訓練幾輪后突然變nan。原因幾乎總是忘了做數(shù)值穩(wěn)定化。檢查你的代碼里有沒有scores - np.max(scores, axis1, keepdimsTrue)這一行。還有一種隱蔽的情況學習率太大權(quán)重更新后得分變得極大即使做了穩(wěn)定化exp之后仍然可能溢出。這時候要降低學習率或者在權(quán)重更新后加一個裁剪。5.2 準確率卡在10%梯度方向反了CIFAR-10有10個類別隨機猜測的準確率就是10%。如果訓練半天準確率一直在10%附近說明模型根本沒學到東西。排查順序檢查梯度是否取反了。dscores應該是probs - 1在正確類別上如果你寫成了1 - probs梯度方向就反了參數(shù)會往錯誤方向更新。檢查損失函數(shù)是否正確。打印幾個樣本的loss正確類別的概率應該隨著訓練增大。檢查數(shù)據(jù)預處理。如果忘了減均值或歸一化訓練會非常慢甚至不收斂。5.3 向量化后維度不匹配從循環(huán)改向量化時最常見的錯誤是維度對不上。比如X.T.dot(dscores)要求X.T的列數(shù)等于dscores的行數(shù)。X是(N, D)X.T是(D, N)dscores是(N, C)乘出來是(D, C)正好和W的形狀一致。如果報錯說維度不匹配先打印每個變量的shape對照上面的維度關(guān)系逐個檢查。我習慣在向量化代碼里加一行assert來驗證形狀比如assert dW.shape W.shape這樣出錯時能立刻定位。報錯信息可能原因解決方法loss為nan指數(shù)溢出加數(shù)值穩(wěn)定化準確率10%梯度方向反檢查dscores符號維度不匹配轉(zhuǎn)置或廣播錯誤打印shape逐行核對梯度檢查失敗鏈式法則漏項重新推導并對照公式訓練震蕩學習率過大降低學習率一個量級5.4 梯度檢查的相對誤差偏大如果梯度檢查的相對誤差在1e-4到1e-2之間說明梯度基本正確但有數(shù)值精度問題。常見原因是數(shù)值穩(wěn)定化時減去的最大值在解析梯度里沒有對應處理。實際上減去常數(shù)不影響梯度因為常數(shù)項求導為0但如果你在計算概率時用了不同的穩(wěn)定化方式可能導致微小差異。另一個原因是正則化項的梯度。reg * np.sum(W * W)的梯度是2 * reg * W別忘了乘2。我見過有人寫成reg * W梯度檢查誤差不大但訓練效果差一截。6. 從Softmax到神經(jīng)網(wǎng)絡(luò)這個作業(yè)的后續(xù)價值Softmax作業(yè)看起來只是Assignment1的一個小環(huán)節(jié)但它埋下了后面所有內(nèi)容的種子。你在這里手寫的損失函數(shù)和梯度在Assignment2的多層神經(jīng)網(wǎng)絡(luò)里會以模塊化的形式復用你在這里理解的數(shù)值穩(wěn)定化在實現(xiàn)Batch Normalization和Layer Normalization時會再次遇到你在這里踩過的向量化坑在寫卷積層的前向傳播時會以更復雜的形式重現(xiàn)。我個人建議是做完Softmax之后不要急著往下走花半小時把梯度推導在紙上重新寫一遍不看代碼純手推。推到 ( P - \mathbb{1} ) 這一步的時候你會對“預測減真實”這個形式有肌肉記憶后面看到任何分類損失都能快速反應出它的梯度長什么樣。最后分享一個小技巧如果你想讓Softmax分類器的準確率再往上提一兩個點可以試試在訓練前對數(shù)據(jù)做PCA白化或者用動量更新代替樸素的梯度下降。這些在作業(yè)里沒有要求但能幫你更直觀地感受不同優(yōu)化策略對線性模型的影響。