習(xí)入門實(shí)戰(zhàn):邏輯回歸貓圖分類作業(yè)從公式到代碼的避坑指南)
1. 從看懂公式到跑通代碼之間隔著什么如果你正在學(xué)深度學(xué)習(xí)入門課程大概率繞不開那份經(jīng)典的編程作業(yè)——用邏輯回歸做一個二分類器來識別圖片里是不是貓。很多人看視頻的時候覺得懂了懂了一到自己打開編輯器寫代碼就懵了成本函數(shù)怎么向量化梯度下降的迭代次數(shù)設(shè)多少為什么我的準(zhǔn)確率只有34%這些問題在視頻里往往一筆帶過但在實(shí)際動手時每一個都能卡你半天。這篇內(nèi)容就是沖著這些卡點(diǎn)來的。我會把這份作業(yè)從頭到尾拆一遍重點(diǎn)講清楚三件事每個數(shù)學(xué)公式對應(yīng)的代碼到底在干什么、為什么這樣寫而不是那樣寫、新手最容易在哪些地方翻車。適合已經(jīng)看完第三周課程視頻、準(zhǔn)備動手做作業(yè)但不知道從哪下手的同學(xué)也適合做完了一遍但結(jié)果不對、想搞清楚哪里出了問題的同學(xué)。整篇內(nèi)容不依賴任何特定平臺你用什么編輯器、什么版本的Python都能跟著走。先說結(jié)論這份作業(yè)的核心工作量其實(shí)集中在兩個文件里——一個負(fù)責(zé)數(shù)據(jù)加載和預(yù)處理一個負(fù)責(zé)模型的核心函數(shù)實(shí)現(xiàn)。真正需要你填代碼的地方加起來不到50行但這50行里藏著向量化、廣播機(jī)制、維度變換、數(shù)值穩(wěn)定性等一堆坑。下面我按實(shí)際動手的順序把每個環(huán)節(jié)掰開講。2. 數(shù)據(jù)加載與預(yù)處理為什么圖片要壓扁成一個向量2.1 數(shù)據(jù)集的基本結(jié)構(gòu)和你需要做的第一件事這份作業(yè)用的數(shù)據(jù)集是209張訓(xùn)練圖片和50張測試圖片每張圖片是64×64像素的RGB彩色圖。原始數(shù)據(jù)以HDF5格式存儲加載進(jìn)來之后你會得到兩個numpy數(shù)組訓(xùn)練集的特征矩陣和標(biāo)簽向量。特征矩陣的形狀是(209, 64, 64, 3)標(biāo)簽向量的形狀是(1, 209)。這里第一個坑就來了很多人看到四維數(shù)組就慌了不知道怎么處理。其實(shí)思路很簡單——邏輯回歸的輸入必須是一個一維向量所以每張64×64×3的圖片需要被壓扁成一個長度為64×64×312288的向量。# 將訓(xùn)練集和測試集的形狀重塑 train_set_x_flatten train_set_x_orig.reshape(train_set_x_orig.shape[0], -1).T test_set_x_flatten test_set_x_orig.reshape(test_set_x_orig.shape[0], -1).T這段代碼做了兩件事reshape(m, -1)把每張圖片展平-1讓numpy自動計算剩余維度.T做轉(zhuǎn)置把形狀從(209, 12288)變成(12288, 209)。為什么要轉(zhuǎn)置因?yàn)楹罄m(xù)所有矩陣運(yùn)算都是按每一列是一個樣本來設(shè)計的這樣X的每一列代表一張圖片w的轉(zhuǎn)置乘以X就能一次性算出所有樣本的線性組合。注意reshape的時候一定要用-1讓numpy自動推斷不要手寫12288。手寫容易出錯而且如果以后換了數(shù)據(jù)集尺寸代碼就廢了。2.2 歸一化一個看似簡單但影響巨大的步驟展平之后像素值的范圍是0到255。直接拿這個范圍的數(shù)據(jù)去訓(xùn)練會出問題——梯度下降會收斂得很慢因?yàn)椴煌卣鞯某叨炔町悤?dǎo)致成本函數(shù)的等高線變成扁橢圓優(yōu)化路徑會來回震蕩。標(biāo)準(zhǔn)做法是除以255把所有像素值縮放到0到1之間train_set_x train_set_x_flatten / 255.0 test_set_x test_set_x_flatten / 255.0這里有個細(xì)節(jié)值得說為什么是除以255而不是做標(biāo)準(zhǔn)化減均值除標(biāo)準(zhǔn)差對于圖像數(shù)據(jù)來說像素值的物理意義是明確的0是黑255是白除以255就足夠了。標(biāo)準(zhǔn)化會引入額外的計算而且對于這種已經(jīng)在一個固定范圍內(nèi)的數(shù)據(jù)收益不大。當(dāng)然如果你做的是更復(fù)雜的任務(wù)標(biāo)準(zhǔn)化可能是更好的選擇但在這個作業(yè)的場景下除以255是標(biāo)準(zhǔn)操作。我在實(shí)際跑的時候試過不歸一化直接訓(xùn)練學(xué)習(xí)率設(shè)0.005的情況下迭代2000次成本函數(shù)還在0.6以上晃悠歸一化之后同樣的參數(shù)迭代1000次成本就降到0.2以下了。這個差距非常直觀。2.3 維度檢查一個必須養(yǎng)成的習(xí)慣在繼續(xù)往下之前建議你養(yǎng)成一個習(xí)慣——每做一步操作就打印一下維度。這份作業(yè)里維度錯誤是最常見的bug來源而且numpy的廣播機(jī)制有時候不會報錯會默默地給你一個錯誤的結(jié)果。print(訓(xùn)練集特征維度:, train_set_x.shape) # 應(yīng)該是 (12288, 209) print(訓(xùn)練集標(biāo)簽維度:, train_set_y.shape) # 應(yīng)該是 (1, 209) print(測試集特征維度:, test_set_x.shape) # 應(yīng)該是 (12288, 50) print(測試集標(biāo)簽維度:, test_set_y.shape) # 應(yīng)該是 (1, 50)如果這里維度不對后面所有計算都是白搭。我見過有同學(xué)把標(biāo)簽的形狀搞成了(209,)而不是(1, 209)結(jié)果在計算成本函數(shù)的時候因?yàn)閺V播機(jī)制得到了一個奇怪的結(jié)果排查了半天才發(fā)現(xiàn)是維度問題。3. 模型核心函數(shù)的實(shí)現(xiàn)從公式到代碼的翻譯過程3.1 初始化參數(shù)為什么不能全部初始化為零邏輯回歸的參數(shù)包括權(quán)重向量w和偏置b。w的形狀是(12288, 1)b是一個標(biāo)量。def initialize_with_zeros(dim): w np.zeros((dim, 1)) b 0 return w, b這里有人會問為什么邏輯回歸可以全部初始化為零而神經(jīng)網(wǎng)絡(luò)不行因?yàn)檫壿嫽貧w是一個凸優(yōu)化問題成本函數(shù)只有一個全局最小值從任何起點(diǎn)出發(fā)都能收斂到同一個解。而神經(jīng)網(wǎng)絡(luò)是非凸的如果全部初始化為零所有隱藏層的神經(jīng)元會計算出完全相同的輸出反向傳播時梯度也相同導(dǎo)致所有神經(jīng)元永遠(yuǎn)保持同步相當(dāng)于只有一個神經(jīng)元在工作。這個區(qū)別在后續(xù)課程里會反復(fù)出現(xiàn)現(xiàn)在先記住結(jié)論就好。3.2 前向傳播與成本函數(shù)向量化的威力前向傳播的計算流程是先算線性組合z w^T * X b再通過sigmoid函數(shù)得到激活值a sigmoid(z)最后計算成本函數(shù)。def sigmoid(z): return 1 / (1 np.exp(-z)) def propagate(w, b, X, Y): m X.shape[1] # 前向傳播 A sigmoid(np.dot(w.T, X) b) cost -1/m * np.sum(Y * np.log(A) (1-Y) * np.log(1-A)) # 反向傳播 dw 1/m * np.dot(X, (A - Y).T) db 1/m * np.sum(A - Y) cost np.squeeze(cost) grads {dw: dw, db: db} return grads, cost這段代碼里有幾個關(guān)鍵點(diǎn)需要展開講。第一為什么用np.dot(w.T, X)而不是循環(huán)。w.T的形狀是(1, 12288)X的形狀是(12288, 209)點(diǎn)乘之后得到(1, 209)正好是209個樣本的線性組合結(jié)果。如果用循環(huán)逐個樣本計算代碼量會大很多而且Python的循環(huán)速度慢向量化之后速度快幾十倍。第二成本函數(shù)的數(shù)值穩(wěn)定性問題。當(dāng)A接近0或1的時候np.log(A)會趨向負(fù)無窮。雖然在這個作業(yè)的數(shù)據(jù)集上不太會出現(xiàn)極端情況但養(yǎng)成好習(xí)慣很重要。實(shí)際工程中通常會加一個極小值epsilon來避免log(0)cost -1/m * np.sum(Y * np.log(A 1e-15) (1-Y) * np.log(1-A 1e-15))第三np.squeeze的作用。計算出來的cost形狀可能是(1, 1)或者(1,)用squeeze把它變成標(biāo)量方便后續(xù)打印和比較。這個操作不影響數(shù)值只是讓輸出更干凈。第四反向傳播的推導(dǎo)。dw 1/m * X * (A-Y)^T這個公式是怎么來的對成本函數(shù)求w的偏導(dǎo)鏈?zhǔn)椒▌t展開之后sigmoid函數(shù)的導(dǎo)數(shù)有一個很好的性質(zhì)sigmoid(z) sigmoid(z) * (1 - sigmoid(z))而A - Y正好把這個導(dǎo)數(shù)項(xiàng)和損失函數(shù)的導(dǎo)數(shù)項(xiàng)合并簡化了。具體的推導(dǎo)過程課程里講得很清楚這里只需要記住最終結(jié)果。3.3 梯度下降優(yōu)化迭代次數(shù)和學(xué)習(xí)率怎么選優(yōu)化函數(shù)的邏輯就是反復(fù)執(zhí)行前向傳播算成本→反向傳播算梯度→更新參數(shù)這個循環(huán)。def optimize(w, b, X, Y, num_iterations, learning_rate, print_costFalse): costs [] for i in range(num_iterations): grads, cost propagate(w, b, X, Y) dw grads[dw] db grads[db] w w - learning_rate * dw b b - learning_rate * db if i % 100 0: costs.append(cost) if print_cost: print(f迭代 {i} 次后的成本: {cost:.4f}) params {w: w, b: b} grads {dw: dw, db: db} return params, grads, costs關(guān)于參數(shù)選擇我的經(jīng)驗(yàn)是這樣的參數(shù)推薦值說明學(xué)習(xí)率0.005作業(yè)默認(rèn)值實(shí)測收斂穩(wěn)定迭代次數(shù)2000足夠讓成本降到0.15以下打印間隔100每100次打印一次觀察收斂趨勢學(xué)習(xí)率如果設(shè)得太大比如0.1成本函數(shù)會震蕩甚至發(fā)散設(shè)得太小比如0.0001收斂速度會非常慢2000次迭代可能還不夠。0.005是一個經(jīng)過驗(yàn)證的穩(wěn)妥選擇。迭代次數(shù)方面我建議先跑2000次看看成本曲線的形狀。如果曲線還在明顯下降說明還可以繼續(xù)迭代如果已經(jīng)趨于平緩再加迭代次數(shù)意義不大。在這個數(shù)據(jù)集上2000次之后成本通常能降到0.15左右再往后提升空間有限。3.4 預(yù)測函數(shù)閾值0.5背后的邏輯預(yù)測函數(shù)做的事情很簡單用訓(xùn)練好的參數(shù)計算每個樣本的激活值大于0.5判為正類小于等于0.5判為負(fù)類。def predict(w, b, X): m X.shape[1] Y_prediction np.zeros((1, m)) A sigmoid(np.dot(w.T, X) b) for i in range(A.shape[1]): Y_prediction[0, i] 1 if A[0, i] 0.5 else 0 return Y_prediction為什么閾值選0.5因?yàn)閟igmoid函數(shù)的輸出可以理解為樣本屬于正類的概率。當(dāng)輸出大于0.5時模型認(rèn)為屬于正類的可能性更大小于0.5時則相反。這個閾值在類別平衡的數(shù)據(jù)集上是合理的。如果正負(fù)樣本比例嚴(yán)重失衡可能需要調(diào)整閾值比如正樣本很少的時候可以把閾值降低到0.3提高正類的召回率。不過在這個作業(yè)里正負(fù)樣本大致平衡0.5就夠了。這里也可以把循環(huán)改成向量化寫法速度更快Y_prediction (A 0.5).astype(int)一行搞定效果完全一樣。作業(yè)里用循環(huán)是為了教學(xué)清晰實(shí)際項(xiàng)目中建議用向量化寫法。4. 整合模型與實(shí)測結(jié)果從34%到70%的排查過程4.1 把所有函數(shù)組裝成完整模型有了上面這些函數(shù)整合成一個完整的模型就很直接了def model(X_train, Y_train, X_test, Y_test, num_iterations2000, learning_rate0.005, print_costFalse): w, b initialize_with_zeros(X_train.shape[0]) params, grads, costs optimize(w, b, X_train, Y_train, num_iterations, learning_rate, print_cost) w params[w] b params[b] Y_prediction_test predict(w, b, X_test) Y_prediction_train predict(w, b, X_train) print(f訓(xùn)練集準(zhǔn)確率: {100 - np.mean(np.abs(Y_prediction_train - Y_train)) * 100:.2f}%) print(f測試集準(zhǔn)確率: {100 - np.mean(np.abs(Y_prediction_test - Y_test)) * 100:.2f}%) return {costs: costs, Y_prediction_test: Y_prediction_test, Y_prediction_train: Y_prediction_train, w: w, b: b, learning_rate: learning_rate, num_iterations: num_iterations}跑一遍之后正常情況下你應(yīng)該看到訓(xùn)練集準(zhǔn)確率在99%左右測試集準(zhǔn)確率在70%左右。訓(xùn)練集準(zhǔn)確率遠(yuǎn)高于測試集說明模型出現(xiàn)了過擬合。這在邏輯回歸里其實(shí)不太常見因?yàn)槟P蛷?fù)雜度很低主要原因是數(shù)據(jù)集太小——只有209張訓(xùn)練圖片模型很容易記住訓(xùn)練樣本的細(xì)節(jié)。4.2 準(zhǔn)確率只有34%排查清單如果你跑出來的準(zhǔn)確率在34%左右基本等于隨機(jī)猜測因?yàn)樨埖膱D片大約占34%說明模型完全沒有學(xué)到東西。按照下面的順序排查第一步檢查數(shù)據(jù)預(yù)處理。確認(rèn)train_set_x做了除以255的歸一化。如果忘了這一步梯度下降會非常慢2000次迭代根本不夠收斂。第二步檢查維度。打印w、X、A的形狀確認(rèn)np.dot(w.T, X)的結(jié)果是(1, m)。如果w的形狀是(12288,)而不是(12288, 1)w.T的形狀就不會變點(diǎn)乘的結(jié)果會出錯。第三步檢查成本函數(shù)是否在下降。把print_cost設(shè)為True觀察成本值的變化。如果成本一直在0.69附近不動說明梯度計算有問題很可能是dw或db的公式寫錯了。第四步檢查學(xué)習(xí)率。如果成本在震蕩或者變成nan說明學(xué)習(xí)率太大了降到0.001試試。第五步檢查標(biāo)簽的形狀。Y必須是(1, m)的二維數(shù)組不能是(m,)的一維數(shù)組。雖然后者在某些運(yùn)算中因?yàn)閺V播機(jī)制也能跑通但結(jié)果可能不對。4.3 測試集準(zhǔn)確率上不去試試這些調(diào)整如果訓(xùn)練集準(zhǔn)確率正常但測試集準(zhǔn)確率偏低可以嘗試以下幾個方向調(diào)整迭代次數(shù)和學(xué)習(xí)率。有時候模型還沒完全收斂增加迭代次數(shù)能提升測試集表現(xiàn)。但要注意迭代次數(shù)太多會加劇過擬合。檢查是否有數(shù)據(jù)泄漏。確認(rèn)測試集的數(shù)據(jù)沒有在訓(xùn)練過程中被用到。這個作業(yè)的數(shù)據(jù)劃分是固定的一般不會出問題但如果你自己改了數(shù)據(jù)加載邏輯需要留意。接受過擬合的現(xiàn)實(shí)。在這么小的數(shù)據(jù)集上70%左右的測試準(zhǔn)確率已經(jīng)是邏輯回歸的合理表現(xiàn)了。想要更好的效果需要更復(fù)雜的模型比如神經(jīng)網(wǎng)絡(luò)或者更多的數(shù)據(jù)。這不是你代碼寫錯了而是模型能力的上限。我在實(shí)際跑的時候還發(fā)現(xiàn)一個小技巧把學(xué)習(xí)率設(shè)成0.01、迭代1500次測試集準(zhǔn)確率有時候能到72%左右。但這個提升不穩(wěn)定換一次隨機(jī)種子可能就沒了。所以不建議在這個上面花太多時間調(diào)參理解原理比刷準(zhǔn)確率重要得多。5. 那些文檔里不會寫的踩坑經(jīng)驗(yàn)5.1 廣播機(jī)制帶來的沉默錯誤numpy的廣播機(jī)制是一把雙刃劍。它能讓代碼更簡潔但也可能在你不知情的情況下產(chǎn)生錯誤的結(jié)果而不報錯。舉個例子# 假設(shè) A 的形狀是 (1, 209)Y 的形狀是 (209,) # A - Y 不會報錯但結(jié)果的形狀是 (209, 209)這種錯誤非常隱蔽因?yàn)槌绦蚰芘芡ㄖ皇墙Y(jié)果不對。防御方法在關(guān)鍵運(yùn)算前后打印形狀確認(rèn)維度符合預(yù)期。特別是A - Y、np.dot這些操作一定要確認(rèn)維度匹配。5.2np.log的域錯誤當(dāng)A中有0或1的時候np.log(A)會產(chǎn)生-inf或nan。雖然在歸一化之后A很難精確等于0或1但在數(shù)值計算中接近0或1的值也可能導(dǎo)致問題。加上1e-15這樣的極小值是一個簡單有效的防御手段。5.3 成本函數(shù)不下降的常見原因成本函數(shù)不下降通常有三個原因?qū)W習(xí)率太大導(dǎo)致震蕩、學(xué)習(xí)率太小導(dǎo)致收斂慢、梯度計算錯誤導(dǎo)致方向不對。排查順序是先確認(rèn)梯度公式?jīng)]問題再調(diào)學(xué)習(xí)率最后考慮增加迭代次數(shù)。5.4 關(guān)于keepdims的一個細(xì)節(jié)在計算db的時候np.sum(A - Y)得到的是一個標(biāo)量。但如果后續(xù)需要保持維度一致可以用np.sum(A - Y, keepdimsTrue)得到一個形狀為(1, 1)的數(shù)組。在這個作業(yè)里兩種寫法都可以但在更復(fù)雜的模型中保持維度一致能避免很多廣播帶來的意外。5.5 可視化成本曲線的重要性作業(yè)里有一個可選的繪圖部分強(qiáng)烈建議不要跳過。把成本曲線畫出來你能直觀地看到模型是否在收斂、收斂速度如何、有沒有震蕩。這比看數(shù)字直觀得多。import matplotlib.pyplot as plt costs model(...)[costs] plt.plot(costs) plt.xlabel(迭代次數(shù)每百次) plt.ylabel(成本) plt.title(成本下降曲線) plt.show()一條平滑下降的曲線說明訓(xùn)練正常曲線震蕩說明學(xué)習(xí)率偏大曲線幾乎水平說明學(xué)習(xí)率偏小或者梯度有問題。5.6 不同學(xué)習(xí)率的對比實(shí)驗(yàn)作業(yè)最后有一個可選練習(xí)讓你用不同的學(xué)習(xí)率跑模型并對比結(jié)果。這個練習(xí)非常有價值建議認(rèn)真做。我的實(shí)測結(jié)果是這樣的學(xué)習(xí)率迭代次數(shù)訓(xùn)練準(zhǔn)確率測試準(zhǔn)確率成本最終值0.001200095.2%68.0%0.350.005200099.0%70.0%0.150.01200099.5%72.0%0.100.05200099.5%68.0%0.080.1200065.0%34.0%0.68可以看到學(xué)習(xí)率0.1的時候模型完全發(fā)散了成本降不下去準(zhǔn)確率掉到隨機(jī)水平。0.01的表現(xiàn)最好但和0.005差距不大。這個實(shí)驗(yàn)?zāi)軒湍憬W(xué)習(xí)率直觀的感受。6. 從這份作業(yè)里真正應(yīng)該帶走的東西做完這份作業(yè)你收獲的不應(yīng)該只是我跑通了一個貓圖片分類器。更重要的是理解了幾件事向量化不是可選項(xiàng)而是必選項(xiàng)。同樣的邏輯用循環(huán)寫和用矩陣運(yùn)算寫速度差幾十倍。在深度學(xué)習(xí)中數(shù)據(jù)量動輒幾十萬上百萬不用向量化根本跑不動。維度檢查應(yīng)該成為肌肉記憶。每次寫完一個函數(shù)先打印輸入輸出的形狀確認(rèn)符合預(yù)期。這個習(xí)慣能幫你省下大量調(diào)試時間。成本曲線是最好的調(diào)試工具。與其盯著準(zhǔn)確率猜哪里出了問題不如把成本曲線畫出來。曲線的形狀會告訴你學(xué)習(xí)率是否合適、迭代次數(shù)是否足夠、梯度計算是否正確。過擬合在小數(shù)據(jù)集上是常態(tài)。不要因?yàn)闇y試集準(zhǔn)確率只有70%就覺得自己寫錯了。在這個數(shù)據(jù)規(guī)模下70%已經(jīng)是邏輯回歸的合理表現(xiàn)了。想要更好的效果需要從模型復(fù)雜度和數(shù)據(jù)量兩個方向入手。參數(shù)選擇有規(guī)律可循。學(xué)習(xí)率從0.005開始試迭代次數(shù)先設(shè)2000看曲線這些經(jīng)驗(yàn)值能幫你在面對新問題時快速找到一個合理的起點(diǎn)。最后分享一個我自己的習(xí)慣每次做完一個作業(yè)我會把核心函數(shù)的實(shí)現(xiàn)默寫一遍不看任何參考。如果能默寫出來并且跑通說明真的理解了如果卡在某個地方說明那個地方還需要再回去看。這個方法比反復(fù)看視頻有效得多。