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