網(wǎng)絡(luò)實(shí)戰(zhàn)指南:從矩陣運(yùn)算到訓(xùn)練排障的完整路徑)
我在自學(xué)AI的路上卡了整整兩周不是因?yàn)榭床欢蕉且驗(yàn)榫W(wǎng)上教程實(shí)在太“干凈”了。打開任何一篇講全連接神經(jīng)網(wǎng)絡(luò)的文章迎面而來的都是標(biāo)準(zhǔn)化結(jié)構(gòu)神經(jīng)元、權(quán)重、激活函數(shù)、反向傳播每個概念都定義清楚但串在一起怎么運(yùn)作、為什么這樣設(shè)計(jì)、訓(xùn)練時為什么loss暴跌后又變NaN完全沒人說。真正干活的人都知道算法原理和工程落地之間隔著一條河而大多數(shù)人被卡住的恰恰是先踩了哪個坑、再補(bǔ)哪塊知識也就是所謂的人工智能學(xué)習(xí)路徑問題。這篇“全連接神經(jīng)網(wǎng)絡(luò)篇”就是奔著解決這個卡點(diǎn)寫的。它能幫你把網(wǎng)絡(luò)最核心的骨架徹底吃透不只是會調(diào)PyTorch或TensorFlow的接口跑通一個模型而是能從矩陣運(yùn)算的角度看穿前向傳播和反向傳播的本質(zhì)知道每個超參數(shù)為什么存在、改它的影響是什么、模型不收斂時該從哪里開刀排查。人工智能大作業(yè)、貓狗識別這類比賽的baseline、入門階段的第一個手寫數(shù)字識別項(xiàng)目都能直接用上這套思路。無論你是剛起步看人工智能導(dǎo)論的學(xué)生還是想轉(zhuǎn)型做算法工程的人這篇文章都適合——它不追求知識點(diǎn)的堆砌只追求一個目標(biāo)讓你學(xué)完之后敢于自己從零搭一個全連接網(wǎng)絡(luò)并且在它出錯時知道去修哪里。1. 為什么全連接神經(jīng)網(wǎng)絡(luò)是AI學(xué)習(xí)路徑里的第一個硬茬1.1 網(wǎng)絡(luò)架構(gòu)千千萬只有全連接是必經(jīng)之路現(xiàn)在市面上最火的模型不管是跑圖片的卷積神經(jīng)網(wǎng)絡(luò)、搞順序數(shù)據(jù)的循環(huán)神經(jīng)網(wǎng)絡(luò)還是說龐大到令人眩暈的Transformer和各類大語言模型底層都離不開“線性層 非線性激活”這個組合而線性層的完整形態(tài)就是全連接結(jié)構(gòu)。所謂全連接就是上一層的每一個神經(jīng)元都和下一層的每一個神經(jīng)元有一條帶權(quán)重的連線網(wǎng)絡(luò)中每一層的所有輸入都會影響該層的所有輸出。這個結(jié)構(gòu)看起來簡單但它是所有神經(jīng)網(wǎng)絡(luò)的基礎(chǔ)。在人工智能基礎(chǔ)教程里常有人把神經(jīng)網(wǎng)絡(luò)比作一個帶旋鈕的黑箱每個旋鈕就是一個權(quán)重訓(xùn)練就是不斷擰旋鈕讓輸出逼近目標(biāo)結(jié)果。至于這個黑箱內(nèi)部真正發(fā)生了什么就得從矩陣運(yùn)算說起。全連接層的本質(zhì)就是一次矩陣乘法加一次偏置加法而神經(jīng)網(wǎng)絡(luò)之所以能擬合任意復(fù)雜函數(shù)正是靠著多層這種變換的堆疊中間再穿插激活函數(shù)引入的非線性。1.2 死記公式?jīng)]意義建立“張量流動”心智模型才是關(guān)鍵我在接觸人工智能應(yīng)用基礎(chǔ)時走過彎路把前向傳播公式記下來了反向傳播公式背下來了可模型跑出來的結(jié)果死活不對。直到后來我才明白學(xué)全連接網(wǎng)絡(luò)最重要的不是背公式而是在腦子里建立一套“張量流動”的心智模型。你要清楚地看到一批樣本以矩陣的形式流進(jìn)網(wǎng)絡(luò)每個樣本在每一層被線性變換、激活最后輸出層給出預(yù)測然后損失函數(shù)算出預(yù)測和真實(shí)標(biāo)簽的差值這個差值再沿著網(wǎng)絡(luò)反向流動逐層計(jì)算每個權(quán)重的梯度優(yōu)化器拿著梯度更新權(quán)重一輪訓(xùn)練結(jié)束。想真正建立這個心智模型我強(qiáng)烈建議你拋開深度學(xué)習(xí)框架用NumPy手寫一個極簡的全連接網(wǎng)絡(luò)去實(shí)現(xiàn)前向傳播、反向傳播里的矩陣運(yùn)算。別怕麻煩這一步的價值遠(yuǎn)高于把PyTorch的nn.Linear用熟練十倍。手寫一遍之后你會發(fā)現(xiàn)所有框架封裝好的接口底下的邏輯全都透明了為什么weight的尺寸是(in_features, out_features)為什么梯度要乘上激活函數(shù)的導(dǎo)數(shù)為什么會有梯度消失這一說。下面我就把網(wǎng)絡(luò)運(yùn)轉(zhuǎn)的原理按最直白的方式拆開。2. 全連接網(wǎng)絡(luò)的運(yùn)轉(zhuǎn)機(jī)制從一次矩陣乘法到完整學(xué)習(xí)閉環(huán)2.1 單個神經(jīng)元的“三合一”處理流程把全連接網(wǎng)絡(luò)拆到最底層就是一個又一個神經(jīng)元。每一個神經(jīng)元做的事其實(shí)非常簡單只有三步把輸入乘上權(quán)重、加上偏置、過激活函數(shù)。拿一個具體的例子來說假設(shè)一個神經(jīng)元有3個輸入x?、x?、x?對應(yīng)的權(quán)重是w?、w?、w?偏置是b那么它先做線性求和z x?w? x?w? x?w? b這就是一次加權(quán)表決然后再把z丟進(jìn)激活函數(shù)σ里得到輸出a σ(z)這個輸出會被送到下一層。有一個生活化的類比這就像一個公司做決策每個提交上來的意見輸入x有它的重要程度權(quán)重w把所有人的意見加權(quán)求和再加上決策者自己的傾向偏置b就得到一個初步得分最后這位決策者可能還有自己的性格比如非得分大于0才拍板這一套“性格”就是激活函數(shù)。這里有個常被初學(xué)者忽略的地方偏置的作用是把激活函數(shù)的輸入向左或向右平移一點(diǎn)點(diǎn)。如果只有權(quán)重沒有偏置那對于任何一個輸入樣本在所有輸入都為0時網(wǎng)絡(luò)輸出必定是0模型的表達(dá)能力會受限很多。有些框架里可以把bias設(shè)為False但絕大多數(shù)情況下你需要保留它。2.2 前向傳播把矩陣運(yùn)算吃透你就不再怕任何網(wǎng)絡(luò)理解了單個神經(jīng)元的操作前向傳播就好說了網(wǎng)絡(luò)一層層地做“輸入乘權(quán)重加偏置再過激活”這件事。為了讓你感受到張量是如何流動的我們直接看一個帶代碼的具體例子。假設(shè)網(wǎng)絡(luò)結(jié)構(gòu)是輸入層3個特征隱藏層4個神經(jīng)元輸出層2個類別。一次處理2個樣本batch_size 2。在NumPy里的前向傳播代碼就是這樣import numpy as np # 輸入2個樣本每個樣本3個特征 X np.array([[0.5, 0.2, 0.1], [0.9, 0.7, 0.3]]) # 第一層輸入3 - 隱藏4 W1 np.random.randn(3, 4) * 0.01 b1 np.zeros((1, 4)) # 第二層隱藏4 - 輸出2 W2 np.random.randn(4, 2) * 0.01 b2 np.zeros((1, 2)) # 前向傳播 Z1 np.dot(X, W1) b1 # (2, 4) A1 np.maximum(0, Z1) # ReLU激活 Z2 np.dot(A1, W2) b2 # (2, 2) # 對輸出層做softmax得到每個類別的預(yù)測概率 exp_scores np.exp(Z2) probs exp_scores / np.sum(exp_scores, axis1, keepdimsTrue) print(第一層線性輸出 Z1:, Z1.shape) # (2, 4) print(激活后 A1:, A1.shape) # (2, 4) print(輸出概率 probs:, probs.shape) # (2, 2)注意幾個細(xì)節(jié)W1的尺寸為什么是(3, 4)而不是(4, 3)因?yàn)檩斎隭的形狀是(2, 3)要讓矩陣乘法的維度匹配就得是(2, 3) (3, 4)。這一點(diǎn)理解了以后看任何模型打印出來的shape報(bào)告都不會再頭疼。權(quán)重初始化用了np.random.randn * 0.01標(biāo)準(zhǔn)正態(tài)分布縮小100倍以避免一開始就把神經(jīng)元的線性輸出推得太遠(yuǎn)。偏置初始化為0是安全的因?yàn)榧词蛊萌珵?網(wǎng)絡(luò)也能通過調(diào)整權(quán)重來學(xué)習(xí)。2.3 反向傳播鏈?zhǔn)椒▌t在神經(jīng)網(wǎng)絡(luò)里的真正含義前向傳播算出一個預(yù)測結(jié)果然后和真實(shí)標(biāo)簽比較得到損失。反向傳播要解決的就是“每一個權(quán)重應(yīng)該往哪個方向調(diào)整”這個問題。這就是人工智能基礎(chǔ)中常提到的梯度下降思想和鏈?zhǔn)椒▌t的應(yīng)用。用一個簡化場景讓你理解鏈?zhǔn)椒▌t假設(shè)損失L是權(quán)重w的復(fù)合函數(shù)路徑是 w → z → predicted → L那么 ?L/?w ?L/?predicted × ?predicted/?z × ?z/?w。從損失出發(fā)一層層往回乘導(dǎo)數(shù)。如果網(wǎng)絡(luò)有10層你就是在做10次連乘這也是為什么激活函數(shù)的選擇會直接影響梯度會不會“越乘越小”從而引發(fā)梯度消失。手動推導(dǎo)反向傳播公式對初學(xué)者來說確實(shí)容易勸退但我的建議是用代碼來“驗(yàn)算”它。在上面那個兩層小網(wǎng)絡(luò)里我繼續(xù)補(bǔ)上反向傳播的核心計(jì)算你看一眼就明白梯度是怎么傳回去的# 假設(shè)真實(shí)標(biāo)簽one-hot形式 y_true np.array([[1, 0], [0, 1]]) # 損失交叉熵 loss -np.sum(y_true * np.log(probs 1e-8)) / 2 # 除以batch_size # 反向傳播的第0步輸出層梯度這個形式來自softmax 交叉熵的求導(dǎo)結(jié)果 dZ2 probs - y_true # 形狀 (2, 2) # 傳回第二層權(quán)重 dW2 np.dot(A1.T, dZ2) / 2 db2 np.sum(dZ2, axis0, keepdimsTrue) / 2 # 傳回第一層激活輸出 dA1 np.dot(dZ2, W2.T) # 通過ReLU的導(dǎo)數(shù)大于0處導(dǎo)數(shù)為1小于0處導(dǎo)數(shù)為0 dZ1 dA1 * (Z1 0) dW1 np.dot(X.T, dZ1) / 2 db1 np.sum(dZ1, axis0, keepdimsTrue) / 2看到了嗎反向傳播里最重要的操作就是“轉(zhuǎn)置、點(diǎn)乘、對batch求平均”而且每一步的梯度計(jì)算都依賴前向傳播中緩存下來的中間結(jié)果比如X、A1、Z1。這就是為什么在PyTorch里你只要打開梯度記錄框架能自動幫你完成整個反向傳播——因?yàn)樗凑涨跋騻鞑サ倪^程記錄了每一次運(yùn)算反向時只需執(zhí)行自動微分。2.4 學(xué)習(xí)閉環(huán)前向傳播、損失計(jì)算、反向傳播、參數(shù)更新的四步舞把上面的代碼串起來一個完整的學(xué)習(xí)閉環(huán)就是四步前向傳播輸入X依次過各層得到預(yù)測概率。計(jì)算損失用交叉熵或均方誤差衡量預(yù)測和真實(shí)標(biāo)簽的差距。反向傳播從損失出發(fā)逐層計(jì)算每個權(quán)重和偏置的梯度。參數(shù)更新weight - learning_rate * gradient讓損失函數(shù)值逐步下降。代碼里對應(yīng)就是learning_rate 0.1 W2 - learning_rate * dW2 b2 - learning_rate * db2 W1 - learning_rate * dW1 b1 - learning_rate * db1這個循環(huán)跑足夠多的輪次epoch損失會逐漸下降準(zhǔn)確率隨之上升。這就是全連接神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)的全部秘密。你可能已經(jīng)發(fā)現(xiàn)我把激活函數(shù)和損失函數(shù)跳過去沒有展開別急它倆單獨(dú)拿出來說特別有講頭因?yàn)檫x對了事半功倍選錯了模型訓(xùn)練就是一場災(zāi)難。3. 激活函數(shù)、損失函數(shù)與權(quán)重初始化讓訓(xùn)練穩(wěn)下來的三個關(guān)鍵旋鈕3.1 激活函數(shù)沒有它疊加再多層也只是個線性模型激活函數(shù)是非線性來源這一點(diǎn)怎么強(qiáng)調(diào)都不為過。如果神經(jīng)網(wǎng)絡(luò)沒有激活函數(shù)那不管疊多少層最終本質(zhì)上都等價于一次線性變換連異或這種簡單問題都學(xué)不會。所以激活函數(shù)的選擇直接決定了網(wǎng)絡(luò)能學(xué)習(xí)哪種類型的模式。常見的激活函數(shù)有這些我把它們放在一張表里方便對比函數(shù)公式輸出范圍優(yōu)點(diǎn)缺點(diǎn)Sigmoidσ(z) 1 / (1 e??)(0, 1)輸出可解釋為概率平滑兩端梯度接近0容易梯度消失Tanhtanh(z) (e? - e??) / (e? e??)(-1, 1)零中心化梯度比Sigmoid好兩端仍然飽和ReLUmax(0, z)[0, ∞)計(jì)算簡單收斂快神經(jīng)元“死亡”即某個神經(jīng)元對所有輸入都輸出0Leaky ReLUmax(αz, z)α常取0.01(-∞, ∞)緩解ReLU死亡問題需要多調(diào)一個參數(shù)α在隱藏層里ReLU是今天的默認(rèn)選擇沒有特別強(qiáng)的理由不要換成Sigmoid。要是不小心用了Sigmoid你會看到訓(xùn)練慢得讓人懷疑人生。輸出層的選擇則完全取決于任務(wù)類型二分類通常用Sigmoid多分類用Softmax回歸問題連激活函數(shù)都不加直接輸出線性值就好。3.2 損失函數(shù)模型優(yōu)化的指揮棒損失函數(shù)定義了一個模型“好”與“壞”的標(biāo)準(zhǔn)。全連接神經(jīng)網(wǎng)絡(luò)最常見的有兩類均方誤差MSE常用于回歸問題公式是 (pred - true)2 的均值。比較簡單直觀但在分類問題里和Sigmoid配合特別差因?yàn)檎`差一大Sigmoid兩端梯度幾乎為0訓(xùn)練會非常慢。交叉熵分類任務(wù)的首選配合Softmax使用幾乎成了標(biāo)準(zhǔn)配置。它衡量的是預(yù)測概率分布和真實(shí)分布之間的距離對分類問題的優(yōu)化方向更直接梯度也更穩(wěn)定。還有一點(diǎn)值得說交叉熵和Softmax搭配時反向傳播的梯度形式特別簡潔就是probs - y_true代碼里那個dZ2 probs - y_true就是這么來的。這個簡潔性是我剛學(xué)的時候完全沒意識到的直到手動推導(dǎo)了一遍才明白為什么框架里都是這個組合。3.3 權(quán)重初始化同樣的網(wǎng)絡(luò)換個初始化方法結(jié)果天差地別權(quán)重初始化是最容易被新手忽略卻又極其影響訓(xùn)練效果的一步。我踩過最深的一次坑把權(quán)重全部初始化為0跑了好幾輪后發(fā)現(xiàn)每個神經(jīng)元的梯度完全一樣整個網(wǎng)絡(luò)退化成了對稱結(jié)構(gòu)。這是因?yàn)槿绻粚铀猩窠?jīng)元的權(quán)重相同那么反向傳播算出來的梯度也相同更新后它們?nèi)匀幌嗤W(wǎng)絡(luò)就沒有“多樣性”可言了。那隨機(jī)初始化就萬事大吉了嗎也不是。如果權(quán)重隨機(jī)得太大比如np.random.randn(3, 4)不乘任何系數(shù)那經(jīng)過多層線性變換和激活之后數(shù)值會迅速膨脹ReLU輸出變得很大梯度也跟著爆掉。如果權(quán)重太小信號傳到深層時又衰減沒了。所以早期實(shí)踐中有人提出用Xavier初始化來匹配輸入輸出維度權(quán)重方差設(shè)為2 / (fan_in fan_out)能有效保持信號在層間傳播時的規(guī)模穩(wěn)定。后來針對ReLU又出現(xiàn)了He初始化方差設(shè)為2 / fan_in。大框架里你看到的kaiming_uniform_、xavier_uniform_就是在做這些事。4. 手寫一個全連接網(wǎng)絡(luò)從零開始識別手寫數(shù)字4.1 為什么勸你先別用PyTorch寫第一行代碼現(xiàn)在網(wǎng)上大量初學(xué)者教程上來就用torch.nn.Sequential幾行代碼就把網(wǎng)絡(luò)定義完了。你跑通了一個LeNet識別MNIST手寫數(shù)字準(zhǔn)確率還挺高但如果問你中間的nn.Linear到底做了什么、反向傳播是怎么算出來的你可能只能說出“框架自動算”這句話。我不否認(rèn)框架對生產(chǎn)效率的巨大提升但對于真正想進(jìn)入人工智能領(lǐng)域的人來說這種“盲跑”會造成一種虛假的掌握感后續(xù)學(xué)卷積神經(jīng)網(wǎng)絡(luò)、Transformer時根基本來就不牢一踩坑就全都塌了。所以我建議你先用NumPy自己寫一個兩層的全連接網(wǎng)絡(luò)來識別手寫數(shù)字。MNIST數(shù)據(jù)集是一個入門級圖像數(shù)據(jù)集每張圖是28×28的灰度圖標(biāo)簽是0到9。用全連接網(wǎng)絡(luò)識別它其實(shí)不算最優(yōu)方案圖像有空間結(jié)構(gòu)卷積網(wǎng)絡(luò)更適合但它數(shù)據(jù)量適中、預(yù)處理簡單、效果容易驗(yàn)證用來理解全連接的原理再合適不過。4.2 用MNIST訓(xùn)練一個兩層全連接網(wǎng)絡(luò)代碼逐段拆解整個項(xiàng)目的核心邏輯非常清晰。我先給出完整的訓(xùn)練腳本框架再逐段拆開解釋關(guān)鍵代碼。import numpy as np from sklearn.datasets import fetch_openml # 1. 加載MNIST數(shù)據(jù)這里用sklearn接口方便一點(diǎn) X, y fetch_openml(mnist_784, version1, return_X_yTrue, as_frameFalse) X X / 255.0 # 像素值歸一化到0~1 # 將標(biāo)簽轉(zhuǎn)為one-hot編碼 num_classes 10 y_onehot np.zeros((y.shape[0], num_classes)) y_onehot[np.arange(y.shape[0]), y.astype(int)] 1 # 劃分訓(xùn)練集和測試集 X_train, X_test X[:60000], X[60000:70000] y_train, y_test y_onehot[:60000], y_onehot[60000:70000] # 2. 定義網(wǎng)絡(luò)結(jié)構(gòu) input_size 784 # 28*28 hidden_size 128 output_size 10 learning_rate 0.5 epochs 200 # 3. 初始化權(quán)重He初始化適合ReLU W1 np.random.randn(input_size, hidden_size) * np.sqrt(2.0 / input_size) b1 np.zeros((1, hidden_size)) W2 np.random.randn(hidden_size, output_size) * np.sqrt(2.0 / hidden_size) b2 np.zeros((1, output_size)) # 4. 訓(xùn)練循環(huán) for epoch in range(epochs): # 前向傳播 Z1 np.dot(X_train, W1) b1 A1 np.maximum(0, Z1) # ReLU Z2 np.dot(A1, W2) b2 # softmax exp_scores np.exp(Z2 - np.max(Z2, axis1, keepdimsTrue)) probs exp_scores / np.sum(exp_scores, axis1, keepdimsTrue) # 交叉熵?fù)p失 loss -np.mean(np.sum(y_train * np.log(probs 1e-8), axis1)) # 反向傳播 dZ2 probs - y_train dW2 np.dot(A1.T, dZ2) / X_train.shape[0] db2 np.sum(dZ2, axis0, keepdimsTrue) / X_train.shape[0] dA1 np.dot(dZ2, W2.T) dZ1 dA1 * (Z1 0) dW1 np.dot(X_train.T, dZ1) / X_train.shape[0] db1 np.sum(dZ1, axis0, keepdimsTrue) / X_train.shape[0] # 更新參數(shù) W1 - learning_rate * dW1 b1 - learning_rate * db1 W2 - learning_rate * dW2 b2 - learning_rate * db2 if epoch % 20 0: print(fEpoch {epoch}, Loss: {loss:.4f}) # 5. 測試準(zhǔn)確率 Z1 np.dot(X_test, W1) b1 A1 np.maximum(0, Z1) Z2 np.dot(A1, W2) b2 preds np.argmax(Z2, axis1) acc np.mean(preds y_test.argmax(axis1)) print(f測試準(zhǔn)確率: {acc * 100:.2f}%)這個腳本最核心的就是前向傳播和反向傳播那幾行矩陣運(yùn)算。數(shù)據(jù)預(yù)處理里有兩個細(xì)節(jié)值得記住像素值從0~255縮放到0~1。這個歸一化太重要了。如果不做784維的輸入數(shù)值很大經(jīng)過權(quán)重相乘后結(jié)果可能過大梯度也會隨之變得不穩(wěn)定。one-hot編碼把標(biāo)簽數(shù)字0~9轉(zhuǎn)成10維向量對應(yīng)位置為1、其他為0。這樣每一行樣本的標(biāo)簽就是可以和softmax輸出直接比較的概率分布。訓(xùn)練中我做了個小的數(shù)值穩(wěn)定處理在Softmax里用Z2 - np.max(Z2, axis1, keepdimsTrue)提前把數(shù)值整體搬移。因?yàn)橹笖?shù)運(yùn)算在輸入很大時容易溢出變成inf減去最大值后指數(shù)函數(shù)的輸入最大是0再大的數(shù)也安全了。跑完這個腳本測試集準(zhǔn)確率大概能到95%以上。這套代碼就是所有分類問題baseline的“祖?zhèn)髂0濉睋Q不同的數(shù)據(jù)集換不同的層數(shù)甚至換成情感分類、房價預(yù)測核心邏輯都是一樣的。4.3 用PyTorch實(shí)現(xiàn)同樣結(jié)構(gòu)的對比框架替你做了什么學(xué)會手寫之后用PyTorch實(shí)現(xiàn)同樣的網(wǎng)絡(luò)就是降維打擊。你可以清楚知道每個API對應(yīng)的底層操作是什么。同樣的兩層網(wǎng)絡(luò)在PyTorch里長這樣import torch import torch.nn as nn import torch.optim as optim class TwoLayerNet(nn.Module): def __init__(self, input_size, hidden_size, num_classes): super().__init__() self.fc1 nn.Linear(input_size, hidden_size) self.relu nn.ReLU() self.fc2 nn.Linear(hidden_size, num_classes) def forward(self, x): x self.fc1(x) x self.relu(x) x self.fc2(x) return x model TwoLayerNet(784, 128, 10) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.5) # 訓(xùn)練時把數(shù)據(jù)轉(zhuǎn)成Tensor # outputs model(x) # loss criterion(outputs, labels) # optimizer.zero_grad() # loss.backward() # optimizer.step()注意這里有個重要的概念差異nn.CrossEntropyLoss()在PyTorch里已經(jīng)內(nèi)置了Softmax操作所以你的網(wǎng)絡(luò)前向傳播不需要手動加Softmax直接輸出原始logits就行。這個細(xì)節(jié)踩坑的人特別多——自作聰明在最后一層加了Softmax結(jié)果損失函數(shù)里又做了一次Softmax損失就是降不下去。當(dāng)你讀過前面NumPy手寫代碼以后看到這里就不會再犯這種錯。5. 訓(xùn)練不收斂、過擬合、Loss為NaN實(shí)戰(zhàn)排障清單5.1 Loss不下降先從數(shù)據(jù)和歸一化查起這是我在帶一些剛?cè)腴T的朋友做人工智能項(xiàng)目時他們最愛問的問題。明明代碼邏輯照著教程寫的Loss就是不動難道是模型壞了我的排查順序一般是這樣的數(shù)據(jù)有沒有歸一化如果輸入特征量級差距過大比如有的特征是0.5有的是5000那梯度更新就會在不同方向上劇烈波動模型很難學(xué)到穩(wěn)定的模式。解決辦法是把所有特征縮放到相近的范圍內(nèi)常用的方式是標(biāo)準(zhǔn)化也就是(x - mean) / std。標(biāo)簽和損失函數(shù)是不是匹配錯誤用MSE算分類問題、把標(biāo)簽寫成普通整數(shù)而不是one-hot、或者交叉熵的輸入是經(jīng)過Softmax的概率值而標(biāo)簽是one-hot向量而不是整數(shù)索引這些都會讓模型“看不懂”學(xué)習(xí)目標(biāo)。學(xué)習(xí)率是不是設(shè)置得離譜學(xué)習(xí)率決定了每次更新權(quán)重的步長。太大Loss可能在下降后突然爆到NaN太小Loss下降就像蝸牛爬。我一般從0.01到0.1之間試驗(yàn)觀察Loss曲線的形態(tài)再調(diào)整方向。只拿一個樣本過擬合測試這里有個技巧很實(shí)用如果你懷疑是代碼邏輯出了bug先拿1個訓(xùn)練樣本訓(xùn)練100輪如果Loss能降到接近0說明前向和反向傳播邏輯通了如果連一個樣本都學(xué)不會問題搜起來范圍就小了大概率是代碼結(jié)構(gòu)的問題。5.2 Loss變成NaN數(shù)值穩(wěn)定性的三大元兇訓(xùn)練到一半Loss變成NaN這個場景凡是用過小型網(wǎng)絡(luò)的人一定都不陌生。常見原因有三個第一個是學(xué)習(xí)率過大。學(xué)習(xí)率太大梯度更新可能跨過了最優(yōu)點(diǎn)反復(fù)震蕩后權(quán)重?cái)?shù)值越來越大最后溢出變成NaN。一個簡單判斷方法把學(xué)習(xí)率降到原來的1/10如果NaN不再出現(xiàn)基本就是這個原因。第二個是指數(shù)運(yùn)算溢出。Softmax里的exp函數(shù)在輸入很大時會產(chǎn)生無窮大。我在前面的代碼里做了Z2 - np.max(Z2)的平移就是為了防這個。如果你自己寫代碼時沒做這個處理換成大數(shù)值的數(shù)據(jù)集時Loss就會頻繁變成NaN。第三個是權(quán)重初始化不當(dāng)。如果初始權(quán)重過大深層網(wǎng)絡(luò)中數(shù)值在層間傳遞時會指數(shù)級膨脹前向傳播就爆掉了。用之前說的Xavier和He初始化能把這個風(fēng)險控制在合理范圍內(nèi)。5.3 過擬合訓(xùn)練集準(zhǔn)確率高、測試集準(zhǔn)確率低的破解方法全連接網(wǎng)絡(luò)因?yàn)閰?shù)量很大在數(shù)據(jù)量不大的場景下特別容易過擬合表現(xiàn)就是訓(xùn)練集上Loss很低、準(zhǔn)確率高得很漂亮一拿到測試集上就原形畢露。想起我當(dāng)年做人工智能大作業(yè)訓(xùn)練集準(zhǔn)確率98%測試集只有88%當(dāng)時整個人是懵的。后來才明白模型不是“學(xué)會”了而是“背下”了訓(xùn)練集。應(yīng)對過擬合的手段有很多按優(yōu)先級排序增加訓(xùn)練數(shù)據(jù)最簡單有效哪怕是做數(shù)據(jù)增強(qiáng)比如圖像平移、旋轉(zhuǎn)、加噪聲也比換一個更復(fù)雜的模型管用。降低模型復(fù)雜度減少隱藏層神經(jīng)元數(shù)量或者減少層數(shù)。全連接網(wǎng)絡(luò)很容易參數(shù)爆炸784維輸入再加256個隱藏神經(jīng)元這一層就有二十多萬個參數(shù)數(shù)據(jù)少了根本沒法泛化。正則化在損失函數(shù)里加上權(quán)重的L2范數(shù)懲罰讓權(quán)重不要太大。有些框架里的weight_decay就是干這個的。直觀理解就是“你可以讓一些特征有影響但別太囂張。”Dropout訓(xùn)練時每次隨機(jī)讓一部分神經(jīng)元失活相當(dāng)于同時訓(xùn)練了很多個共享權(quán)重的子網(wǎng)絡(luò)做預(yù)測時再全部啟用。不過對全連接網(wǎng)絡(luò)來說如果數(shù)據(jù)量不大我建議先試減小模型結(jié)構(gòu)和增加正則化Dropout可以放在后面再上。早停法訓(xùn)練過程中每輪記錄驗(yàn)證集Loss當(dāng)它連續(xù)多個epoch不再下降反而上升時馬上停止訓(xùn)練并恢復(fù)之前最優(yōu)的模型權(quán)重。這個技巧在比賽里特別實(shí)用能省下大量訓(xùn)練調(diào)參的時間。5.4 搭建一個親測有效的訓(xùn)練檢查清單最后把我在多個項(xiàng)目里用下來的排查順序整理成一個清單建議調(diào)試模型時按順序過一遍檢查項(xiàng)優(yōu)先級典型問題數(shù)據(jù)預(yù)處理P0特征未歸一化、標(biāo)簽格式錯誤、數(shù)據(jù)泄漏損失函數(shù)與輸出層匹配P0交叉熵配Softmax重復(fù)、MSE做分類權(quán)重初始化P1全零初始化、隨機(jī)尺度太大學(xué)習(xí)率P1全局學(xué)習(xí)率過大或過小、無學(xué)習(xí)率衰減網(wǎng)絡(luò)結(jié)構(gòu)P2層數(shù)過深梯度消失、隱藏層過寬過擬合訓(xùn)練策略P2無早停、無正則化、batch設(shè)置太小或太大6. 把全連接網(wǎng)絡(luò)做成一個真正有完成度的項(xiàng)目6.1 從“跑通教程”到“完成一個項(xiàng)目”的認(rèn)知升級如果你只是按照教程敲了一遍手寫數(shù)字識別那收獲有限真正讓你成長的是在這個基礎(chǔ)上做一個有完成度的項(xiàng)目。很多同學(xué)做人工智能大作業(yè)時最困惑的不是怎么跑通模型而是跑通之后還能做什么。我的建議是不要只改參數(shù)而是把完整項(xiàng)目鏈路走一遍定義問題、準(zhǔn)備數(shù)據(jù)、實(shí)現(xiàn)模型、訓(xùn)練調(diào)優(yōu)、評估結(jié)果、可視化展示。拿一個經(jīng)典場景舉例子以貓狗識別為原型做一個二分類項(xiàng)目原始數(shù)據(jù)就是一批圖片任務(wù)是判斷圖片里是貓還是狗。全連接網(wǎng)絡(luò)也能做但因?yàn)椴粫脠D像的二維空間結(jié)構(gòu)效果不會很好。但這個流程很適合用來理解“項(xiàng)目化”的思維方式先把圖片縮放成固定尺寸比如64×64把每個像素當(dāng)特征拉直成向量送給全連接網(wǎng)絡(luò)跑一個baseline再對比分析模型錯在哪是光線問題、背景干擾還是類別不平衡。這個過程會逼著你去想數(shù)據(jù)分布在說什么而不只是敲代碼。6.2 找一個“比自己現(xiàn)在水平高一點(diǎn)”的題目練手經(jīng)常有人問我剛學(xué)完神經(jīng)網(wǎng)絡(luò)該找什么樣的項(xiàng)目鞏固太簡單的沒有挑戰(zhàn)性太難的直接勸退。我的經(jīng)驗(yàn)公式是找一個能復(fù)現(xiàn)主流baseline、同時留有優(yōu)化空間的題目。比如用MNIST跑完baseline后你可以給自己的模型加一些難度比如故意在圖像上添加噪聲看看模型魯棒性或者限定只用5000張訓(xùn)練圖片然后逼自己想一些數(shù)據(jù)增強(qiáng)和數(shù)據(jù)清洗的手段把精度提上去。也會有人上來就想去打那種大型比賽我持保留態(tài)度。初學(xué)者最好先選擇基于標(biāo)準(zhǔn)數(shù)據(jù)集的比賽比如經(jīng)典的貓狗識別這類任務(wù)因?yàn)樯鐓^(qū)資料豐富任何一步卡住都能搜到參考。直接上手太大的任務(wù)數(shù)據(jù)預(yù)處理就能把人淹沒核心能力反而沒練到。6.3 模型結(jié)果的呈現(xiàn)訓(xùn)練曲線與常見錯誤分析項(xiàng)目最難的部分往往不是模型本身而是你完成任務(wù)后向別人展示的過程。你大作業(yè)答辯時如果只放一個最后的準(zhǔn)確率數(shù)字那說服力很弱。更有說服力的做法是放三樣?xùn)|西訓(xùn)練過程的Loss曲線和準(zhǔn)確率曲線觀察到了第幾個epoch開始收斂有沒有發(fā)生過擬合的跡象。錯誤案例的可視化分析把預(yù)測錯的樣本打印出來分析到底是因?yàn)閳D像太模糊、還是目標(biāo)太小、還是數(shù)據(jù)集本身標(biāo)注有問題。這一步特別能體現(xiàn)做項(xiàng)目的思考深度。對比實(shí)驗(yàn)表格比如“隱藏層64 vs 128 vs 256”的準(zhǔn)確率對比“有Dropout vs 無Dropout”的對比。不要只把最好的結(jié)果擺出來把探索過程也講出來這才是項(xiàng)目最有價值的部分。7. 全連接網(wǎng)絡(luò)之后的下一步從“能跑”到“能打”的進(jìn)階路徑學(xué)完這篇的內(nèi)容你已經(jīng)掌握了神經(jīng)網(wǎng)絡(luò)的核心骨架。這個能力是可以遷移到幾乎所有后續(xù)AI模型上的。接下來最自然的路徑是順著兩條線延伸一條線是模型結(jié)構(gòu)向復(fù)雜方向延伸從全連接過渡到卷積神經(jīng)網(wǎng)絡(luò)再用卷積網(wǎng)絡(luò)處理圖像數(shù)據(jù)。你會發(fā)現(xiàn)卷積層本質(zhì)上還是“線性加權(quán)求和”的改版只不過權(quán)重不再是每個像素都配一個而是在空間上共享于是參數(shù)大幅減少對圖像也更友好。從全連接過渡到Transformer你會看得更快注意力機(jī)制本質(zhì)上也是加權(quán)求和只不過權(quán)重不是固定的參數(shù)而是根據(jù)輸入動態(tài)計(jì)算出來的。另一條線是工程能力向?qū)崙?zhàn)方向延伸學(xué)一點(diǎn)PyTorch的訓(xùn)練流程封裝、數(shù)據(jù)集加載、模型保存與加載、GPU訓(xùn)練、超參數(shù)搜索工具?;A(chǔ)模型能用只是第一步能寫出一套清晰可復(fù)用的訓(xùn)練代碼并用這些代碼快速迭代出現(xiàn)在不同數(shù)據(jù)集上的最優(yōu)配置這才是企業(yè)里最需要的基礎(chǔ)能力。我在學(xué)習(xí)AI的過程中踩過無數(shù)坑但回過頭來看恰恰是那些因?yàn)椴皇煜さ讓用娣磸?fù)排查的經(jīng)歷幫我建立起了對模型的直覺——看到了Loss曲線就知道該往哪個方向調(diào)試看到報(bào)錯信息就能大致猜到是哪一層的問題。這種直覺不是看出來的是手寫代碼、盯著每一行shape驗(yàn)證、在實(shí)驗(yàn)里試錯試出來的。最后給你一個非常實(shí)際的建議把今天這篇的NumPy代碼親手敲一遍確保每行都能解釋清楚然后跑起來再改一改隱藏層大小、學(xué)習(xí)率、激活函數(shù)觀察Loss和準(zhǔn)確率的變化曲線。等你把這個過程走完一遍全連接網(wǎng)絡(luò)對你來說就不再是一個知識點(diǎn)而是一個真正掌握了的工具。后面再學(xué)任何網(wǎng)絡(luò)結(jié)構(gòu)你都會比別人更快看懂本質(zhì)。