:手把手跑通LeNet官方Demo)
深度學習入門這條路上很多人卡在同一個地方理論看了一堆反向傳播能推卷積核尺寸會算但真讓自己從頭跑通一個能訓練、能收斂、能出結果的模型手就生了。我當年也是這樣書翻了三遍不如親手把官方 Demo 敲一遍。這篇就圍繞 PyTorch 官方那個最經典的 LeNet 示例展開把從零開始學深度學習的第三站講透——為什么是 LeNet、這個 Demo 到底在做什么、每一行代碼背后的意圖是什么、跑通之后還能怎么改。不管你是剛裝完 PyTorch 的新手還是想回頭把基礎打扎實的老手這篇都能給你一份可以直接照著復現的完整路徑。核心關鍵詞就三個PyTorch、LeNet、官方 Demo全文圍繞它們把環(huán)境、數據、網絡、訓練、評估、調參、排錯一條龍講清楚。1. 為什么入門第一站選 LeNet 這個官方 Demo1.1 LeNet 在深度學習史上的位置決定了它的教學價值LeNet 是上世紀九十年代為手寫數字識別設計的卷積神經網絡結構極簡兩個卷積層、兩個池化層、三個全連接層參數量不到十萬。放到今天看這個規(guī)模連小模型都算不上但它的教學價值恰恰在于小。我見過太多新手一上來就啃 ResNet、Transformer結果被殘差連接、多頭注意力、位置編碼這些概念砸暈連張量形狀怎么變都沒搞明白。LeNet 的好處是每一層的輸入輸出形狀你都能手算出來卷積核 5×5、步長 1、填充 0池化 2×2、步長 2這些數字在紙上畫一畫就清楚了。PyTorch 官方把它作為入門 Demo邏輯很實在它覆蓋了深度學習最核心的幾個環(huán)節(jié)——數據加載、網絡定義、前向傳播、損失計算、反向傳播、參數更新、模型評估一個都不少但每個環(huán)節(jié)都做到了最簡。你跑通這一個 Demo等于把整個訓練流程的骨架摸了一遍后面換更復雜的網絡無非是往這個骨架上掛更復雜的模塊。這就是我常跟人說的一句話LeNet 不是讓你學會識別手寫數字是讓你學會一個模型是怎么被訓練出來的。1.2 官方 Demo 相比網上教程的獨特優(yōu)勢網上 LeNet 的教程一抓一大把為什么還要專門講官方 Demo我踩過的坑告訴我第三方教程最大的問題是省略。作者為了篇幅把數據預處理簡化了、把設備切換省了、把評估邏輯砍了你照著抄能跑但換個數據集就崩報錯都不知道從哪查。官方 Demo 的價值在于它是完整可運行的數據下載、歸一化、批處理、訓練循環(huán)、測試評估、準確率統(tǒng)計每一塊都在而且用的是 PyTorch 官方推薦的寫法。更關鍵的是官方 Demo 的代碼風格是教科書級的規(guī)范。比如它用torchvision.datasets和DataLoader的組合來管理數據用nn.Module的子類來定義網絡用optim模塊管理優(yōu)化器這些寫法是后面所有 PyTorch 項目的通用范式。你把這個 Demo 的每一行都吃透后面看別人的開源代碼就不會有陌生感。我個人的經驗是新手最該做的不是到處找更簡單的教程而是把一個官方 Demo 反復改、反復跑改到你能預測每次修改的結果為止。1.3 這個 Demo 適合誰來學學到什么程度算過關先說適合誰。完全沒碰過 PyTorch 的適合學過一點但沒完整跑過訓練流程的適合跑過別人的代碼但說不清每行在干嘛的也適合。不太適合的是已經能獨立寫訓練腳本、調過超參的老手對他們來說這個 Demo 太淺了。學到什么程度算過關我給自己定的標準是三條第一能不看代碼憑記憶把網絡結構寫出來包括每層的輸入輸出通道數和卷積核尺寸第二能解釋訓練循環(huán)里zero_grad、backward、step這三步為什么是這個順序調換會怎樣第三能自己改一個地方——比如把激活函數從 ReLU 換成 Sigmoid或者把優(yōu)化器從 SGD 換成 Adam——然后預測結果會怎么變再跑一遍驗證。三條都做到了這個 Demo 才算真正吃透而不是跑通了。2. 環(huán)境準備與數據管線的搭建細節(jié)2.1 環(huán)境安裝版本匹配比什么都重要裝 PyTorch 這件事新手最容易犯的錯是隨便裝個最新版。我見過有人裝完發(fā)現torchvision和torch版本不匹配導入就報錯折騰一下午。正確的做法是先確定 Python 版本再按官方給的組合裝。以常見的 Python 3.9 到 3.11 為例torch和torchvision的版本必須對應比如 torch 2.x 系列要配對應版本的 torchvision具體對應關系在官方安裝頁面上有表格裝之前一定對一眼。安裝命令本身不復雜CPU 版本直接 pip 裝就行有獨立顯卡的按官方給的索引地址裝對應計算平臺的版本。我這里不寫具體命令因為版本迭代快寫死了反而誤導你去官方安裝頁面選好系統(tǒng)、包管理器、計算平臺它會直接生成命令復制粘貼最穩(wěn)。裝完驗證很簡單import torch import torchvision print(torch.__version__) print(torchvision.__version__) print(torch.cuda.is_available())最后一行輸出 True 說明能用顯卡加速False 就是純 CPU 跑。LeNet 這個規(guī)模CPU 跑也完全夠MNIST 數據集六萬張訓練圖CPU 上幾分鐘一輪別被必須用顯卡嚇住。提示裝完先跑上面三行驗證別急著寫模型。環(huán)境問題在寫代碼前解決比寫到一半報錯再回頭查要省事得多。2.2 MNIST 數據集為什么它是入門標配MNIST 是七萬張 28×28 的灰度手寫數字圖六萬訓練、一萬測試十個類別0 到 9。它成為入門標配不是偶然圖像小、類別少、干凈、標注準模型很容易就能跑到 98% 以上的準確率新手能快速獲得正反饋。這一點很重要我見過太多人被復雜數據集虐到懷疑人生其實換個簡單的數據集同樣的代碼立刻就能跑出漂亮結果問題不在你在數據。用torchvision.datasets.MNIST加載關鍵參數有三個root指定下載目錄train區(qū)分訓練集和測試集downloadTrue讓它在本地沒有數據時自動下載。這里有個細節(jié)很多人忽略訓練集和測試集要分別實例化trainTrue和trainFalse各來一次別想著一個對象搞定。from torchvision import datasets, transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform)2.3 歸一化參數 0.1307 和 0.3081 是怎么來的上面代碼里那兩個數字不是隨便寫的它們是 MNIST 訓練集的全局均值和標準差。ToTensor()會把像素值從 0 到 255 縮放到 0 到 1Normalize再按(x - mean) / std做標準化。為什么要標準化因為神經網絡對輸入尺度敏感輸入值集中在 0 附近、方差為 1 時梯度下降收斂更穩(wěn)。你可以自己算一遍驗證把所有訓練圖拉平求均值和標準差結果就是這兩個數。我建議新手真去算一次不是為了記住數字是為了理解歸一化參數應該從訓練集統(tǒng)計而不是拍腦袋定這個原則——測試集要用訓練集的參數不能自己再算一遍否則就是數據泄露。2.4 DataLoader批處理與打亂的實操要點數據集對象只是數據在哪真正喂給模型的是DataLoader。它負責三件事按batch_size切批、每個 epoch 打亂順序、多進程預取數據。from torch.utils.data import DataLoader train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers0) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse, num_workers0)batch_size訓練集用 64 是官方 Demo 的選擇小批量能讓梯度帶點噪聲反而有助于跳出局部極小。測試集用 1000 是因為測試不需要反向傳播大批量一次算完更快。shuffleTrue只在訓練集開測試集必須關否則評估結果沒法復現。num_workers在 Windows 上建議設 0設大了容易出多進程相關的報錯Linux 上可以設 2 到 4 加速數據加載。這些都是我實際踩過的坑官方文檔不會專門提醒你 Windows 的這個問題。3. LeNet 網絡結構的逐層拆解與實現3.1 從論文原圖到代碼每一層的形狀變化LeNet 的結構用一句話概括卷積、池化、卷積、池化、展平、三個全連接。但光背這句話沒用你得能算出每一層的張量形狀。輸入是[batch, 1, 28, 28]注意 MNIST 是單通道灰度圖所以通道數是 1這是新手最容易寫錯的地方寫成 3 就會報通道不匹配。第一層卷積Conv2d(1, 6, 5)輸入 1 通道、輸出 6 通道、卷積核 5×5、無填充。28 - 5 1 24所以輸出是[batch, 6, 24, 24]。接著MaxPool2d(2, 2)24 / 2 12輸出[batch, 6, 12, 12]。第二層卷積Conv2d(6, 16, 5)12 - 5 1 8輸出[batch, 16, 8, 8]。再池化8 / 2 4輸出[batch, 16, 4, 4]。展平后是 16 × 4 × 4 256 維接全連接Linear(256, 120)、Linear(120, 84)、Linear(84, 10)最后輸出 10 個類別的分數。這套計算我建議你拿紙筆自己推一遍推完再對照代碼比直接看代碼印象深十倍。很多人卡在為什么展平是 256就是因為沒算這一步。3.2 用 nn.Module 定義網絡的規(guī)范寫法PyTorch 定義網絡的標準姿勢是繼承nn.Module在__init__里聲明層在forward里定義數據流向。這個范式必須刻進肌肉記憶因為后面所有模型都這么寫。import torch.nn as nn import torch.nn.functional as F class LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.conv1 nn.Conv2d(1, 6, 5) self.pool1 nn.MaxPool2d(2, 2) self.conv2 nn.Conv2d(6, 16, 5) self.pool2 nn.MaxPool2d(2, 2) self.fc1 nn.Linear(16 * 4 * 4, 120) self.fc2 nn.Linear(120, 84) self.fc3 nn.Linear(84, 10) def forward(self, x): x self.pool1(F.relu(self.conv1(x))) x self.pool2(F.relu(self.conv2(x))) x x.view(x.size(0), -1) x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) x self.fc3(x) return x幾個細節(jié)值得說。super(LeNet, self).__init__()這行不能省它初始化父類省了會報錯。x.view(x.size(0), -1)里的x.size(0)是 batch 維度-1讓 PyTorch 自動算展平后的維度這樣寫比寫死 256 更穩(wěn)改網絡結構時不容易漏改。最后一層fc3后面不接激活函數因為輸出的是類別分數logits激活交給損失函數CrossEntropyLoss內部處理它自帶 softmax。這一點新手常搞錯在最后加個 softmax 再用交叉熵等于做了兩次結果就偏了。3.3 激活函數為什么選 ReLU 而不是 SigmoidLeNet 原論文用的是 Sigmoid 或 tanh但現代實現基本都換成了 ReLU。原因很實際Sigmoid 在輸入較大或較小時梯度接近 0深層網絡容易出現梯度消失訓練慢甚至訓不動。ReLU 在正區(qū)間梯度恒為 1反向傳播時梯度不容易衰減收斂快得多。我實測過同一個 LeNetSigmoid 版本跑五個 epoch 準確率還在 90% 附近晃ReLU 版本三個 epoch 就上 98% 了。不過 ReLU 也有坑負區(qū)間梯度為 0神經元可能死掉輸入一直是負的話這個神經元就再也不更新了。LeNet 這么淺的網絡基本不用擔心但你要知道這個特性后面搭深網絡時才會理解為什么會有 LeakyReLU、ELU 這些變體。3.4 參數量核算為什么這個網絡輕到可以隨便跑把每層參數量加起來conv1 是 1×6×5×5 6 156conv2 是 6×16×5×5 16 2416fc1 是 256×120 120 30840fc2 是 120×84 84 10164fc3 是 84×10 10 850??傆嫾s 4.4 萬參數。這個量級意味著什么意味著模型文件不到 200KB意味著 CPU 上訓練一輪只要幾十秒意味著你可以在筆記本上反復實驗幾十次而不用等。我特別建議新手在這個階段多改多試因為成本低試錯快正是建立直覺的黃金期。4. 訓練循環(huán)的完整實現與關鍵步驟解析4.1 損失函數與優(yōu)化器的選擇邏輯分類任務用CrossEntropyLoss這是 PyTorch 里把 softmax 和負對數似然打包在一起的損失函數輸入是 logits不需要你手動 softmax。優(yōu)化器官方 Demo 用的是 SGD學習率 0.01動量 0.5。為什么用 SGD 不用 Adam不是 Adam 不好是 SGD 在這個簡單任務上足夠而且能讓你看清學習率這個超參的作用。Adam 自適應調整學習率新手反而感受不到調參的樂趣和痛感。import torch.optim as optim model LeNet() criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.01, momentum0.5)model.parameters()把網絡里所有可訓練參數交給優(yōu)化器管理這一步別漏漏了優(yōu)化器不知道要更新誰。動量 0.5 的作用是讓更新方向帶點慣性減少震蕩加快收斂這個值在 LeNet 上是官方調好的你可以試著改成 0 或 0.9 感受差異。4.2 訓練循環(huán)的三步核心zero_grad、backward、step訓練循環(huán)的骨架就這幾行但每一步的意圖必須清楚def train(model, train_loader, criterion, optimizer, epoch): model.train() for batch_idx, (data, target) in enumerate(train_loader): optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() if batch_idx % 100 0: print(fEpoch {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}] Loss: {loss.item():.4f})optimizer.zero_grad()必須放在最前面因為 PyTorch 的梯度是累加的不清零的話上一批的梯度會疊加到這一批結果全錯。這是新手第一大坑我當年就是漏了這行l(wèi)oss 一直不降查了半天。loss.backward()做反向傳播算出每個參數的梯度。optimizer.step()按梯度更新參數。順序不能亂先清零、再前向、再算損失、再反向、最后更新。model.train()這行也別省它把網絡切到訓練模式影響 Dropout 和 BatchNorm 的行為。LeNet 里雖然沒有這兩層但養(yǎng)成習慣很重要后面加了就會感謝自己。4.3 測試評估為什么必須切 eval 模式并關閉梯度測試階段和訓練階段寫法不同核心是兩點model.eval()和torch.no_grad()。def test(model, test_loader, criterion): model.eval() test_loss 0 correct 0 with torch.no_grad(): for data, target in test_loader: output model(data) test_loss criterion(output, target).item() pred output.argmax(dim1, keepdimTrue) correct pred.eq(target.view_as(pred)).sum().item() test_loss / len(test_loader) accuracy 100. * correct / len(test_loader.dataset) print(fTest Loss: {test_loss:.4f}, Accuracy: {accuracy:.2f}%) return accuracymodel.eval()切評估模式和train()對應。torch.no_grad()關閉梯度計算測試不需要反向傳播關掉能省顯存、提速。output.argmax(dim1)取分數最高的類別作為預測pred.eq(target.view_as(pred))比較預測和真實標簽sum().item()統(tǒng)計正確數。這套寫法是標準范式建議直接背下來。4.4 完整訓練流程串聯(lián)與實測結果把上面拼起來主循環(huán)大概是這樣for epoch in range(1, 6): train(model, train_loader, criterion, optimizer, epoch) test(model, test_loader, criterion)五個 epochCPU 上跑下來大概兩三分鐘。我實測的結果是第一個 epoch 結束準確率約 97%第三個 epoch 到 98.5%第五個 epoch 穩(wěn)定在 98.8% 到 99% 之間。這個曲線很典型——前期漲得快后期趨于平緩。如果你跑出來第一個 epoch 只有 10% 左右等于隨機猜那基本可以確定是zero_grad漏了或者學習率設錯了。如果 loss 變成 nan多半是學習率太大降到 0.001 試試。注意訓練前記得把模型和數據放到同一個設備上。純 CPU 跑不用管用顯卡的話要model.to(device)且每個 batch 的data、target也要.to(device)漏一個就報設備不匹配。5. 常見報錯與調參排錯實錄5.1 新手最常撞的五個報錯及解決思路我把這些年帶人時見過的高頻報錯整理成表對照著查能省不少時間報錯信息關鍵詞常見原因解決方向expected scalar type Byte but found Float輸入沒轉成 float 張量檢查ToTensor()是否在 transform 里mat1 and mat2 shapes cannot be multiplied展平維度算錯重新核算卷積池化后的尺寸Expected input batch_size to match target標簽形狀不對檢查view_as或squeezeCUDA out of memory顯存不夠減小 batch_size 或換 CPUloss 一直是 nan學習率過大或數據異常降學習率、檢查歸一化第一個報錯特別常見本質是數據類型不匹配ToTensor()會把 PIL 圖像轉成 0 到 1 的 float 張量忘了加就會拿到原始整數類型。第二個是形狀問題LeNet 展平是 256你如果改了卷積層沒同步改全連接層就會撞上。這些報錯看著嚇人其實都是形狀或類型對不上養(yǎng)成打印x.shape的習慣問題一目了然。5.2 準確率上不去的排查順序如果模型能跑但準確率卡在某個值上不去按這個順序查先看訓練集準確率如果訓練集都上不去是模型或訓練流程有問題如果訓練集高、測試集低是過擬合如果兩者都低是欠擬合。LeNet 在 MNIST 上正常應該輕松到 98% 以上到不了就逐項排查——學習率是不是太大導致震蕩batch_size 是不是太小導致梯度噪聲過大歸一化是不是漏了epoch 是不是太少。我遇到過一次準確率死活卡在 90%最后發(fā)現是shuffle忘了開數據按類別順序喂進去模型學偏了。這種問題不看代碼根本想不到所以排查時要把數據管線也納入檢查范圍別只盯著模型。5.3 幾個能立刻提升效果的實操技巧第一個技巧是學習率衰減訓練到后期把學習率降下來能讓模型在最優(yōu)解附近更精細地收斂。PyTorch 有l(wèi)r_scheduler幾行就能加上。第二個技巧是數據增強MNIST 上可以加輕微旋轉、平移能提升泛化但對這個簡單任務提升有限屬于錦上添花。第三個技巧是保存和加載模型訓練完用torch.save(model.state_dict(), lenet.pth)存參數下次model.load_state_dict(torch.load(lenet.pth))加載這是實際項目必備技能Demo 里沒寫但你必須會。提示保存模型存state_dict而不是整個模型對象前者只存參數、體積小、跨版本兼容好后者依賴類定義換環(huán)境容易加載失敗。6. 從跑通到改通讓這個 Demo 真正變成你的6.1 三個循序漸進的改造練習跑通只是起點真正學到東西靠改。我給自己安排的第一個練習是把激活函數換成 Sigmoid觀察收斂速度的變化驗證前面說的ReLU 更快。第二個練習是把優(yōu)化器換成 Adam學習率設 0.001看準確率曲線是不是更平滑。第三個練習是改網絡結構比如在 conv1 后加一個 BatchNorm 層或者把兩個卷積核都改成 3×3然后重新核算展平維度。這三個練習做完你對改一處、動全身的理解會深很多。改造時有個原則一次只改一個變量改完跑一遍記錄結果再改下一個。同時改好幾處出了問題根本不知道是哪處引起的。這個習慣我從入門保持到現在做任何實驗都遵守。6.2 把這個 Demo 遷移到自己的數據集上LeNet 跑通 MNIST 之后最有價值的下一步是換成自己的數據。遷移時要改的地方其實不多數據集類換成ImageFolder或自定義Dataset輸入通道數按你的圖改彩色圖是 3全連接層第一層的輸入維度按新的特征圖尺寸重算輸出類別數改成你的類別數。改完先在小樣本上跑通確認形狀都對再上全量數據。我特別想強調通道數這一處很多人拿彩色圖直接喂給Conv2d(1, 6, 5)報錯還找不到原因。記住網絡第一層的輸入通道必須和數據通道一致這是硬約束。6.3 我個人在這個 Demo 上踩過的坑和體會最后說點掏心窩的。我最早跑這個 Demo 時卡在x.view那行整整一個下午因為我不理解-1是什么意思硬寫了個 256結果改了 batch_size 就報錯。后來才明白-1是讓 PyTorch 自己算這種讓框架幫你算的思路貫穿整個 PyTorch 使用。還有一次我把optimizer.zero_grad()寫在了loss.backward()后面loss 一直不降查了兩小時才發(fā)現順序錯了。這些坑現在看很蠢但正是這些蠢坑讓我真正記住了每一步的作用。我的體會是官方 Demo 的價值不在于它多完美而在于它是一個活的標本你可以隨便解剖、隨便改。跑通它只花你半小時但把它改到面目全非、每一處改動你都能預測結果這個過程才真正把知識變成你自己的。后面你再去學更復雜的網絡會發(fā)現無非是在這個骨架上加模塊而骨架你已經爛熟于心了。