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