網(wǎng)絡(luò)前向傳播:手寫數(shù)字識別實現(xiàn)與踩坑全記錄)
我花了兩天時間啃完這門經(jīng)典課程第三周的課后練習(xí)把多元分類和神經(jīng)網(wǎng)絡(luò)前向傳播從頭到尾實現(xiàn)了一遍。整個過程踩了不少坑也把很多原本知其然不知其所以然的細(xì)節(jié)徹底搞明白了。這篇就把整個實現(xiàn)思路、關(guān)鍵代碼、踩坑記錄完整寫出來希望能幫你少走彎路。這個練習(xí)的核心是兩件事第一用正則化邏輯回歸訓(xùn)練多個二分類器組合成能識別手寫數(shù)字的多元分類模型第二體驗神經(jīng)網(wǎng)絡(luò)的前向傳播流程——不需要訓(xùn)練權(quán)重直接用課程提供的參數(shù)跑通一次預(yù)測。無論你是剛學(xué)完邏輯回歸、準(zhǔn)備進入神經(jīng)網(wǎng)絡(luò)階段還是卡在某個實現(xiàn)細(xì)節(jié)里出不來這篇文章都值得你看下去。1. 為什么這道題值得認(rèn)真做從二分類到多分類的關(guān)鍵跨越邏輯回歸在數(shù)學(xué)上是個二元分類模型可現(xiàn)實中大部分分類問題都不止兩個類別。手寫數(shù)字識別就是個典型例子——你要區(qū)分0到9一共10個類別。怎么把二元分類器擴展成多元分類器這是機器學(xué)習(xí)里第一個真正需要設(shè)計模式思維的地方。課程給出的方案是一對一One-vs-All策略訓(xùn)練10個獨立的二元分類器每個分類器只回答一個特定問題——這張圖是數(shù)字3嗎、這張圖是數(shù)字7嗎以此類推。預(yù)測時把圖片輸入全部10個分類器拿置信度最高的那個答案作為最終結(jié)果。這套思路后來在神經(jīng)網(wǎng)絡(luò)里也以完全不同的形式出現(xiàn)了神經(jīng)網(wǎng)絡(luò)不再訓(xùn)練10個獨立的模型而是用一層多個輸出神經(jīng)元并行完成這件事每個輸出神經(jīng)元對應(yīng)一個類別最后用softmax或直接取最大值決定結(jié)果。理解了ex3的One-vs-All再看神經(jīng)網(wǎng)絡(luò)最后一層的設(shè)計你會發(fā)現(xiàn)邏輯完全相通——只是實現(xiàn)效率高得多。我在做這道題之前總覺得多元分類就是把二元分類多套幾層做完之后才意識到問題沒這么簡單。真正關(guān)鍵的是每個分類器的正負(fù)樣本怎么定義、輸出概率怎么合并、預(yù)測置信度怎么比較這些細(xì)節(jié)才是工程落地的重點。而且練習(xí)的負(fù)載數(shù)據(jù)本身就很有挑戰(zhàn)——手寫數(shù)字潦草、筆畫粗細(xì)不均、甚至還有傾斜比教科書上那些干凈分布的數(shù)據(jù)點棘手得多。用這個數(shù)據(jù)集訓(xùn)練一個能上90%準(zhǔn)確率的模型你會對特征質(zhì)量比模型復(fù)雜度更重要這句話有切身體會。這道題在整門課的練習(xí)序列里也是個分水嶺前三題是在理解基礎(chǔ)模型到ex3開始你要自己組合多個子模型協(xié)同工作同時第一次接觸神經(jīng)網(wǎng)絡(luò)的概念。我建議不要直接抄網(wǎng)上的答案而是按下面的思路自己把每一步走通。2. 數(shù)據(jù)和接口細(xì)節(jié)加載、可視化和特征向量的維度理解不論用什么框架實現(xiàn)第一步都是把數(shù)據(jù)徹底看懂。ex3提供了兩個數(shù)據(jù)文件一個包含5000個訓(xùn)練樣本的輸入矩陣X以及對應(yīng)的標(biāo)簽向量y另一個是給神經(jīng)網(wǎng)絡(luò)部分用的預(yù)訓(xùn)練權(quán)重。先別急著跑模型花10分鐘把數(shù)據(jù)本身的形態(tài)、范圍、含義搞清楚后面能省大量排查時間。2.1 數(shù)據(jù)矩陣的真實含義X是一個5000行、400列的矩陣每一行是20像素乘以20像素的灰度圖像被展平后得到的一維向量。所以每張圖最初是個20x20的矩陣按行或者按列串起來就得到400個數(shù)值。這些數(shù)值是像素灰度理論范圍應(yīng)該是0到255但課程已經(jīng)把數(shù)據(jù)歸一化到0到1之間了。讀數(shù)據(jù)時有個特別容易忽視的坑y變量里存的標(biāo)簽并不是0-9而是1-10。其中數(shù)字0對應(yīng)的標(biāo)簽是10數(shù)字1-9對應(yīng)的標(biāo)簽是1-9。這個1代表1、10代表0的映射關(guān)系在訓(xùn)練單獨分類器和計算準(zhǔn)確率時很容易出錯。我第一次就是忘了轉(zhuǎn)換映射導(dǎo)致最后準(zhǔn)確率看起來怎么都不對排查了半天才發(fā)現(xiàn)是標(biāo)簽偏移了。加載完數(shù)據(jù)后強烈建議做一個可視化步驟用reshape將某一行數(shù)據(jù)還原成20x20的矩陣用圖像方式把前100個樣本顯示出來對著y標(biāo)簽驗證自己讀入的數(shù)據(jù)是否正確。不要跳過這一步否則可能在后續(xù)所有步驟里帶著錯誤數(shù)據(jù)白跑。2.2 邏輯回歸部分的輸入輸出約定邏輯回歸部分的核心函數(shù)大致是損失函數(shù)costFunction接收參數(shù)向量theta、特征矩陣X、標(biāo)簽向量y和正則化系數(shù)lambda返回?fù)p失值J和梯度grad。這里的theta維度必須和特征數(shù)匹配——特征是400維加上偏置項變成401維所以theta是401維的向量。X的第一列全部設(shè)為1作為偏置項。優(yōu)化階段使用fmincg內(nèi)置的高級優(yōu)化器時需要把theta、X、y、lambda作為參數(shù)傳遞進去優(yōu)化器迭代更新theta讓損失函數(shù)最小化。每次調(diào)用只訓(xùn)練一個分類器對應(yīng)一個數(shù)字類別。訓(xùn)練10次后得到一個10行401列的theta矩陣每一行對應(yīng)一個數(shù)字分類器的參數(shù)。這部分細(xì)節(jié)下面展開細(xì)說。3. 正則化邏輯回歸損失函數(shù)、梯度公式和向量化實現(xiàn)的坑正規(guī)化邏輯回歸是這次的主菜。表面上只是把標(biāo)準(zhǔn)邏輯回歸加一個正則項但實現(xiàn)起來梯度、損失、向量化三個環(huán)節(jié)都有各自的細(xì)節(jié)坑。我一個個拆開說。3.1 損失函數(shù)和梯度的標(biāo)準(zhǔn)形式單個樣本的邏輯回歸損失是交叉熵加上正則項后的整體目標(biāo)函數(shù)長這樣損失項取每個樣本的預(yù)測概率和真實標(biāo)簽的交叉熵求和取平均。梯度項在原有梯度基礎(chǔ)上theta的所有分量除了偏置項theta0額外加一個lambda / m乘以該分量的值。注意偏置項不參與正則化。這是很多教程容易含糊帶過的地方——正則化懲罰的是特征權(quán)重不是偏置。如果連偏置項一塊懲罰了模型的平移自由度會受損結(jié)果會偏。實現(xiàn)時有一個高效向量化寫法避免逐樣本for循環(huán)用sigmoid函數(shù)計算假設(shè)值h一個矩陣乘法就能算出全部樣本的預(yù)測概率。損失計算則可以直接用矩陣運算和log運算一次算出全部樣本的交叉熵之和。梯度中非正則化的部分可以表示成X^T乘以(h-y)這個式子看起來抽象本質(zhì)上就是在累計每個特征維度上的誤差。熟悉矩陣求導(dǎo)的人會理解這個操作的來源不熟悉的可以先當(dāng)作公式記下來后面做數(shù)值梯度驗證時自然會確信它是對的。3.2 用數(shù)值梯度驗證你寫的梯度對不對寫梯度函數(shù)的時候有個非常有價值的驗證方法數(shù)值梯度檢驗?;驹硎潜┝魄髮?dǎo)——在某個點theta上對第j個分量加一個極小量epsilon計算損失變化量除以2倍epsilon就得到該方向上的數(shù)值導(dǎo)數(shù)。拿數(shù)值導(dǎo)數(shù)和你的解析梯度對比如果兩者非常接近說明解析梯度寫對了。這個方法在正式訓(xùn)練前花十分鐘做一次可以避免后面訓(xùn)練出的模型莫名其妙不收斂卻找不到原因。我這次寫代碼時梯度里忘記加正則項數(shù)值檢驗立刻爆出巨大差異一查就發(fā)現(xiàn)了問題。如果跳過這步直接跑優(yōu)化損失值可能也會下降但訓(xùn)練出的分類器準(zhǔn)確率會很不理想問題隱藏得很深。數(shù)值梯度檢驗的注意點是epsilon不能選太大也不能選太小過大近似誤差明顯過小浮點精度出問題。一般取1e-5附近比較穩(wěn)。檢驗時用個小樣本子集比如100個樣本就夠了全量跑會很慢。3.3 為什么偏置項在正則化里被特殊對待剛開始我直接把所有theta都加了正則項答案也不報錯但分類器在測試集上的表現(xiàn)會比正確處理差一截。你要知道為什么偏置項被區(qū)別對待偏置項控制的是決策邊界整體平移它不直接對應(yīng)某個具體特征的權(quán)重。把所有特征權(quán)重同等懲罰時如果偏置也被無差別懲罰優(yōu)化器就必須在降低損失和縮小偏置之間做無意義的妥協(xié)這會扭曲整個決策邊界的位置。類比來說正則化像是在對模型的幅度收稅但偏置項只是模型的零點位置不應(yīng)該交這個稅。這個細(xì)節(jié)在面試?yán)镆步?jīng)常被問到值得記牢。4. fmincg和fminunc怎么選優(yōu)化器背后的工程考量課程練習(xí)特別要求用fmincg而不是自己寫梯度下降。fmincg和fminunc是兩種不同的高級優(yōu)化器前者基于共軛梯度法后者基于擬牛頓法BFGS。兩者都不需要你手動設(shè)定學(xué)習(xí)率內(nèi)部會自適應(yīng)推進步長這對新手非常友好。這里先說清楚一個理念問題為什么課程不讓你自己實現(xiàn)梯度下降來優(yōu)化因為在工業(yè)界實踐中優(yōu)化器本身是一門學(xué)問。手寫梯度下降時學(xué)習(xí)率、動量、收斂判據(jù)都要自己調(diào)對新手來說這些都是額外負(fù)擔(dān)。高級優(yōu)化器把這些細(xì)節(jié)都封裝好了讓你可以專注于模型本身。這也是編程思維和機器學(xué)習(xí)思維的一個差異——前者喜歡掌控一切后者知道什么時候該把成熟工具利用起來。fmincg和fminunc在小型問題上的收斂速度差異不是重點重點是接口約定fmincg要求傳入一個函數(shù)句柄該函數(shù)接受theta參數(shù)返回?fù)p失和梯度同時要傳入一個初始theta向量和選項結(jié)構(gòu)體。有個細(xì)節(jié)坑是直接寫fmincg(costFunction, theta, options)這種形式會報錯或行為怪異因為costFunction除了theta還依賴X、y、lambda這些額外參數(shù)。正確做法是構(gòu)造一個匿名函數(shù)把這些額外參數(shù)捕獲進來每次只用theta作為自變量。如果讀過相關(guān)文檔會看到官方推薦的做法正是我上面描述的這個模式。從選項里可以設(shè)定最大迭代次數(shù)和是否輸出過程信息。我第一次跑用默認(rèn)值結(jié)果訓(xùn)練過程完全黑盒只能在結(jié)束后看準(zhǔn)確率。后來顯式打開輸出選項每一輪迭代的損失值變化一目了然對判斷是否收斂非常有幫助。5. 從單個分類器到完整預(yù)測系統(tǒng)訓(xùn)練、組裝和評估的完整流程有了能優(yōu)化單個邏輯回歸分類器的工具接下來要把10個分類器全部訓(xùn)練出來并組裝成統(tǒng)一的預(yù)測系統(tǒng)。5.1 訓(xùn)練循環(huán)和all_theta矩陣訓(xùn)練層面用一個for循環(huán)遍歷1到10這10個類。每個循環(huán)里做三件事第一把y重映射成當(dāng)前類別的二值標(biāo)簽——是當(dāng)前數(shù)字則標(biāo)為1否則標(biāo)為0第二調(diào)用fmincg優(yōu)化當(dāng)前分類器的theta第三把訓(xùn)練好的theta存到一個矩陣的對應(yīng)行。這個模塊如果封裝成函數(shù)會清爽很多。函數(shù)返回一個叫all_theta的矩陣維度是10乘以401每一行是某個數(shù)字分類器的完整參數(shù)向量。注意數(shù)字1排在矩陣第1行數(shù)字0排在第10行因為y標(biāo)簽里0對應(yīng)10。這個順序在預(yù)測階段要記住否則輸出結(jié)果會錯亂。初始化theta時我用的全零向量。對邏輯回歸來說全零初始化是可行的——因為損失函數(shù)是凸函數(shù)不管從哪開始優(yōu)化都能收斂到全局最優(yōu)這和神經(jīng)網(wǎng)絡(luò)必須隨機初始化有本質(zhì)區(qū)別。這里可以與后面對比著理解為什么神經(jīng)網(wǎng)絡(luò)不能用全零初始化因為對稱性問題。這個細(xì)節(jié)到神經(jīng)網(wǎng)絡(luò)訓(xùn)練時你就會深有體會。5.2 預(yù)測函數(shù)概率矩陣的行列含義預(yù)測時把整個X矩陣輸入到所有分類器中得到一個5000行10列的概率矩陣。第i行第j列表示第i個樣本被第j個分類器判定為正類的概率。實際預(yù)測就是逐行找最大值所在列的索引。某學(xué)習(xí)者寫過一個簡短的預(yù)測函數(shù)靠一個矩陣乘法和一次sigmoid調(diào)用就拿到了全部概率再用max函數(shù)順著列方向找出每行最大值及索引。索引從1開始且對應(yīng)數(shù)字1到10最后要把10映射回0。我在預(yù)測階段犯過一個很低級的錯誤沒有把全零特征矩陣?yán)锏钠昧刑幚砗?。?xùn)練時X第一列全是1預(yù)測時如果忘了加這一列矩陣維度直接對不上。這個錯誤雖然會立即報維度錯誤、容易發(fā)現(xiàn)但一旦數(shù)據(jù)剛好巧合匹配就會出隱藏問題所以養(yǎng)成訓(xùn)練和預(yù)測走同一條預(yù)處理流程的習(xí)慣值得刻意培養(yǎng)。5.3 準(zhǔn)確率評估和每個類別的獨立表現(xiàn)訓(xùn)練完成后在訓(xùn)練集上算準(zhǔn)確率大概在95%左右不同實現(xiàn)略有浮動。這個數(shù)字看著不錯但建議進一步看看每個類別各自的準(zhǔn)確率不要只看平均。每個數(shù)字獨立來看表現(xiàn)是有差異的有些數(shù)字容易混比如4和9、3和8這些形近數(shù)字單個類別準(zhǔn)確率可能掉到90%以下。這提醒我們準(zhǔn)確率均值高不代表每個類別都健康。在真實項目里如果某個類別是高風(fēng)險類別比如醫(yī)療診斷里的某種罕見病單獨看該類的召回率比看整體準(zhǔn)確率重要得多。模型評估不能只看匯總指標(biāo)這一點到后面學(xué)習(xí)精確率、召回率時會反復(fù)出現(xiàn)。6. 神經(jīng)網(wǎng)絡(luò)初體驗不訓(xùn)練權(quán)重只跑一次前向傳播ex3的后半部分不是訓(xùn)練神經(jīng)網(wǎng)絡(luò)而是直接給你一組訓(xùn)練好的權(quán)重讓你實現(xiàn)前向傳播跑通預(yù)測。很多人覺得這半部分太簡單但它是理解神經(jīng)網(wǎng)絡(luò)正向工作原理的關(guān)鍵一步。6.1 三層結(jié)構(gòu)的數(shù)據(jù)流動過程這里的神經(jīng)網(wǎng)絡(luò)是三層結(jié)構(gòu)輸入層400個神經(jīng)元對應(yīng)400像素特征、隱藏層25個神經(jīng)元、輸出層10個神經(jīng)元對應(yīng)10個數(shù)字類別。課程給好的權(quán)重Theta1維度是25乘以401Theta2維度是10乘以26。25后面為什么跟401因為輸入層400個特征加一個偏置單元隱藏層25個神經(jīng)元同理加偏置后變成26個輸入。前向傳播的邏輯很直白輸入數(shù)據(jù)從輸入層進入加權(quán)求和后用sigmoid激活得到隱藏層的輸出隱藏層的輸出作為下一層的輸入再次加權(quán)求和、再用sigmoid激活得到最終輸出。所謂神經(jīng)元被激活與否就是sigmoid函數(shù)輸出的值是否顯著接近1。第一次看到想到原來我前面手工訓(xùn)練出的10個獨立分類器這里用一組權(quán)重并行完成了——輸出層10個神經(jīng)元本質(zhì)上就起了多分類器的作用。區(qū)別在于這里不是獨立優(yōu)化每個輸出而是共享隱藏層特征表達權(quán)重在訓(xùn)練時是一起調(diào)整的。這種共享特征表達正是神經(jīng)網(wǎng)絡(luò)高效的地方。我建議實現(xiàn)時逐步打印每一層的維度變化確認(rèn)每一步矩陣相乘是合法的。很多新手在這里被維度搞暈先手算一下第一層輸入是5000乘以401乘Theta1轉(zhuǎn)置25乘以401得到5000乘以25的隱藏層活躍值加上偏置列得5000乘以26再乘Theta2轉(zhuǎn)置10乘以26得到5000乘以10的輸出層值。中間任何一步維度不匹配都會立刻暴露出來。6.2 從邏輯回歸到神經(jīng)網(wǎng)絡(luò)同一個思想的不同載體前向傳播實現(xiàn)完之后我最大的收獲是突然理解了邏輯回歸和神經(jīng)網(wǎng)絡(luò)之間的關(guān)系。神經(jīng)網(wǎng)絡(luò)的一層本質(zhì)上是多個邏輯回歸單元的組合但層與層之間通過非線性激活函數(shù)疊加表達能力大大增強了。注意一個關(guān)鍵細(xì)節(jié)在隱藏層計算出a1之后、輸入到輸出層之前必須再插入一列1作為偏置。這個偏置單元在神經(jīng)網(wǎng)絡(luò)里同樣不參與特征表達只是給該層的加權(quán)和提供了一個可調(diào)節(jié)的偏移。如果漏掉這一步矩陣維度對不上訓(xùn)練好的Theta2也沒法用預(yù)測結(jié)果就亂了。神經(jīng)網(wǎng)絡(luò)部分用課程給的權(quán)重跑出來準(zhǔn)確率在97%以上高于前面邏輯回歸的95%。為什么更高因為神經(jīng)網(wǎng)絡(luò)的特征表達是數(shù)據(jù)驅(qū)動學(xué)到的而邏輯回歸用的是原始像素直接組合。像素級別的特征表達能力有限神經(jīng)網(wǎng)絡(luò)學(xué)到的是更高層的抽象模式。7. 整個練習(xí)做完后的幾個認(rèn)知升級7.1 關(guān)于調(diào)包和手寫的重新認(rèn)識做這道題之前我總覺得自己手寫一切實現(xiàn)才算真正理解。做完后發(fā)現(xiàn)在機器學(xué)習(xí)工程里合理選用成熟優(yōu)化器不是偷懶是正確分工。fmincg背后的共軛梯度實現(xiàn)細(xì)節(jié)絕大多數(shù)場景不需要你重寫你需要理解的是它的接口約定、收斂含義、以及什么時候換用別的優(yōu)化器。這和深度學(xué)習(xí)里用現(xiàn)成框架的基本道理一脈相承——工程價值在于組件搭配和問題建模不是重復(fù)造輪子。7.2 有哪些可以繼續(xù)深挖的擴展方向如果你做完這個練習(xí)還有余力我建議你按這幾個方向繼續(xù)探索一是自己實現(xiàn)簡單的反向傳播訓(xùn)練神經(jīng)網(wǎng)絡(luò)把給好權(quán)重跑前向升級成自己學(xué)到權(quán)重。你會發(fā)現(xiàn)難點從計算過程轉(zhuǎn)移到維度管理和梯度推導(dǎo)。二是把數(shù)據(jù)稍作增強旋轉(zhuǎn)、平移、加噪聲觀察準(zhǔn)確率怎么變化。這能讓你提前體驗真實項目中數(shù)據(jù)質(zhì)量對模型表現(xiàn)的主導(dǎo)作用。三是試著把邏輯回歸部分換成支持向量機、決策樹等不同模型對比它們在相同數(shù)據(jù)上的表現(xiàn)差異。這種模型對比實驗是機器學(xué)習(xí)從業(yè)者的日?;竟Α?.3 一個實用建議從一開始就寫好調(diào)試輔助工具我在這次練習(xí)中體會很深的一點是調(diào)試工具前置能省下大量時間。數(shù)值梯度檢驗和維度打印這兩個小工具在這次練習(xí)里幫我快速定位了至少四個問題。養(yǎng)成寫模型之前先寫驗證工具的習(xí)慣后面做復(fù)雜項目時收益巨大。數(shù)值檢驗思路在神經(jīng)網(wǎng)絡(luò)的反向傳播實現(xiàn)里同樣適用屆時光靠看代碼找錯會非常痛苦梯度檢驗幾乎是救命稻草。回到開頭說的那句話這門課練習(xí)的真正價值不在分?jǐn)?shù)而在親手把每個公式變成能跑的代碼并且理解每一步為什么這么做。希望這篇筆記能幫你在ex3上少走一些彎路——尤其是那個標(biāo)簽映射、那個偏置列、那個匿名函數(shù)傳參的坑。如果過程中卡住了對照著我上面說的排查思路一步步查大概率能自己找到問題所在。