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