字識別實戰(zhàn):TensorFlow與PyTorch雙框架實現(xiàn)全解析)
手寫數(shù)字識別應(yīng)該是我接觸深度學(xué)習(xí)以來做的第一個完整實驗也是很多教程里的“Hello World”。這次實驗我沒有只用一個框架而是把TensorFlow和PyTorch各寫了一遍從數(shù)據(jù)加載、網(wǎng)絡(luò)搭建、訓(xùn)練調(diào)參到測試評估整個流程完整跑通最后整理成一份實驗報告。代碼和報告一起歸檔之后我又把整個過程的思路重新梳理了一遍這篇文章就相當(dāng)于把這次實驗的全過程展開來講模型怎么搭、參數(shù)怎么設(shè)、訓(xùn)練中遇到哪些問題、實驗報告怎么寫一次性說明白。這個項目非常適合剛?cè)腴T神經(jīng)網(wǎng)絡(luò)的朋友哪怕你之前只寫過一點Python跟著這篇文章的思路也能把一個完整的手寫數(shù)字識別流程跑起來。文章里的代碼都是可以直接復(fù)制運行的環(huán)境和依賴我也會把版本細(xì)節(jié)說清楚。文章后面還會分享我這次實驗中踩過的幾個坑、調(diào)參時的對比數(shù)據(jù)以及實驗報告的寫法參考這些內(nèi)容在常規(guī)教程里一般不會寫。1. 實驗設(shè)計從數(shù)據(jù)到模型整個識別流程怎么串聯(lián)1.1 為什么選擇MNIST作為入門實驗做深度學(xué)習(xí)實驗第一步就是選數(shù)據(jù)集。手寫數(shù)字識別用的是MNIST數(shù)據(jù)集里面有6萬張訓(xùn)練圖片和1萬張測試圖片每張都是28x28的灰度圖內(nèi)容是0到9的手寫數(shù)字。這個數(shù)據(jù)集很“干凈”圖片已經(jīng)做了居中處理背景和前景對比明顯所以即使是簡單模型也能達(dá)到不錯的準(zhǔn)確率。但不要因為“簡單”就小看它。MNIST雖然只是入門級數(shù)據(jù)集但在這個數(shù)據(jù)集上能跑通整套流程你就能把神經(jīng)網(wǎng)絡(luò)怎么工作這件事徹底搞明白輸入怎么進(jìn)入網(wǎng)絡(luò)、權(quán)重怎么更新、損失函數(shù)在優(yōu)化什么、準(zhǔn)確率是怎么計算出來的。這些基本概念一旦通了后續(xù)再接觸CIFAR-10、ImageNet或者各種實際項目思路是完全一樣的。我這次實驗選的不是更花哨的數(shù)據(jù)集就是因為MNIST足夠小、下載快、訓(xùn)練也快。在最開始的時候把精力放在理解原理和流程上比追求復(fù)雜數(shù)據(jù)集更有價值。1.2 兩套框架各寫一遍的價值在哪現(xiàn)在的深度學(xué)習(xí)框架主要就是TensorFlow和PyTorch兩大陣營。很多人在入門時糾結(jié)該學(xué)哪個。我的建議是如果你真的想入這一行兩個都得會一點起碼要能讀懂對方的代碼。原因不復(fù)雜不同公司、不同開源項目用的框架不一樣你出去實習(xí)或接項目很可能遇到什么就得用什么。但更重要的一點是用兩個框架各寫一遍同一個任務(wù)能幫助你區(qū)分“框架的寫法”和“模型的本質(zhì)”。比如在TensorFlow里搭全連接層你可能就是調(diào)用一個Dense()在PyTorch里則要寫Linear()。表面看是API不同但底層都是做同一個矩陣運算輸入向量乘以權(quán)重矩陣加上偏置經(jīng)過激活函數(shù)。你只有兩個都寫過才會真正理解這些層背后的數(shù)學(xué)運算是一致的。這次實驗我在兩個框架里用了同一個網(wǎng)絡(luò)結(jié)構(gòu)、同樣的訓(xùn)練輪數(shù)和差不多的超參數(shù)最終得到的準(zhǔn)確率也很接近。這種對比實驗本身就很有意思能讓你直觀看到“框架”只是工具真正決定模型效果的是數(shù)據(jù)、結(jié)構(gòu)、超參數(shù)這些核心因素。1.3 網(wǎng)絡(luò)結(jié)構(gòu)怎么定先搞清全連接和卷積的區(qū)別這次實驗我并沒有一上來就上卷積神經(jīng)網(wǎng)絡(luò)而是先用全連接網(wǎng)絡(luò)Dense網(wǎng)絡(luò)跑通流程之后又額外做了一個卷積神經(jīng)網(wǎng)絡(luò)版本做對比。很多人喜歡直接跳到CNN跳過了基礎(chǔ)版本我覺得這樣做其實是吃虧的。全連接網(wǎng)絡(luò)的理解成本最低。輸入是一張28x28的圖片你把它“拉平”成784個數(shù)值然后這些數(shù)值作為第一層神經(jīng)元的輸入。每個神經(jīng)元都跟上一層的所有神經(jīng)元連接這就是“全連接”的含義。這種結(jié)構(gòu)看起來直觀缺點是參數(shù)非常多而且它不考慮圖片的空間結(jié)構(gòu)——把一個像素和離它很遠(yuǎn)的像素一視同仁。卷積神經(jīng)網(wǎng)絡(luò)CNN就不同了它利用卷積核在圖片上滑動提取局部特征。一開始的卷積層提取的可能是邊緣、線條這類低級特征后面層會把低級特征組合成高級特征比如數(shù)字的弧線、交叉點。因為卷積核是參數(shù)共享的參數(shù)量反而比全連接少得多而且更適合處理圖片。我在兩種網(wǎng)絡(luò)上做了對比實驗結(jié)果很典型全連接網(wǎng)絡(luò)在測試集上準(zhǔn)確率大約97.5%CNN能達(dá)到99%以上。這個對比不是說全連接沒用而是讓你切身感受到“網(wǎng)絡(luò)結(jié)構(gòu)對任務(wù)效果的巨大影響”。2. 環(huán)境配置與數(shù)據(jù)準(zhǔn)備2.1 依賴安裝與版本建議環(huán)境這塊我踩過不少坑特別是版本不匹配的問題。這里給出一套我實測能跑的版本組合你可以直接參考我用的是Python 3.9環(huán)境。TensorFlow方面用的2.10版本PyTorch用的是1.13版本。如果你電腦有NVIDIA顯卡并且裝好了CUDA那PyTorch版可以啟用GPU加速沒有顯卡也沒關(guān)系這次實驗的數(shù)據(jù)量和模型規(guī)模都不大用CPU訓(xùn)練也完全能跑就是每輪訓(xùn)練可能會多花幾分鐘。安裝方面TensorFlow直接執(zhí)行pip install tensorflow2.10即可。PyTorch因為涉及CUDA版本的選擇建議去它的官網(wǎng)根據(jù)你的環(huán)境生成安裝命令。但要注意如果不需要GPU加速直接pip install torch torchvision也行默認(rèn)裝的CPU版本。建議在虛擬環(huán)境里安裝不要直接干到系統(tǒng)環(huán)境里。我曾經(jīng)圖省事直接在系統(tǒng)環(huán)境裝了一堆包結(jié)果后面其他項目依賴沖突光排查就耗了半天。用virtualenv或者conda管理環(huán)境是每個做深度學(xué)習(xí)的人都應(yīng)該養(yǎng)成的基本習(xí)慣。注意不同版本的框架API會有差異。比如TensorFlow 1.x和2.x的寫法完全不同你在網(wǎng)上查資料時一定要先確認(rèn)版本。如果照著舊教程寫新版本代碼會經(jīng)常遇到報錯。2.2 手寫數(shù)字圖片怎么就變成了矩陣?yán)斫鈹?shù)據(jù)格式是理解整個實驗的第一步。一張MNIST圖片是28x28的灰度圖所謂灰度圖就是每個像素點只有一個數(shù)值表示亮度范圍從0到255。0是純黑255是純白中間是深淺不同的灰色。所以在計算機(jī)眼里一張手寫數(shù)字圖片并不是“圖片”而是一個28x28的二維矩陣?yán)锩媸?80多個數(shù)值。而神經(jīng)網(wǎng)絡(luò)處理輸入時通常需要的是一個向量所以全連接網(wǎng)絡(luò)的第一步就是把28x28的矩陣“拉平”成784個數(shù)值的一維向量。我覺得理解這一點特別重要因為很多人在后面遇到維度不對的報錯時一頭霧水本質(zhì)上就是沒有搞清楚數(shù)據(jù)的形狀。跟數(shù)據(jù)形狀相關(guān)的問題占深度學(xué)習(xí)調(diào)試的很大比例。我這次實驗在展示數(shù)據(jù)時做了一步可視化從訓(xùn)練集里隨機(jī)抽了12張圖片把它們畫出來并標(biāo)注對應(yīng)的標(biāo)簽。你會看到那些手寫數(shù)字有的歪歪扭扭有的甚至人眼都難辨認(rèn)但機(jī)器學(xué)習(xí)算法依然能從中找出規(guī)律這件事本身就挺奇妙的。2.3 數(shù)據(jù)歸一化這一步不能省圖像預(yù)處理中有一件非常重要的事歸一化。簡單說就是把像素值從0到255的范圍縮放到0到1之間。方法很簡單把所有像素值除以255即可。為什么要做這一步因為神經(jīng)網(wǎng)絡(luò)的訓(xùn)練依賴梯度下降而梯度的大小跟輸入數(shù)值的范圍直接相關(guān)。如果輸入數(shù)值都很大而且范圍很寬某些權(quán)重的梯度會過大或過小導(dǎo)致訓(xùn)練不穩(wěn)定收斂也慢。把輸入縮放到0到1這樣的小范圍能讓梯度保持在一個相對合理的尺度訓(xùn)練過程會更順暢。除了歸一化我還對訓(xùn)練數(shù)據(jù)做了打亂操作。這個也很重要如果一批數(shù)據(jù)里全是同一個數(shù)字模型在這批數(shù)據(jù)上算出的梯度就會有偏差長期下來模型泛化能力會變差。打亂數(shù)據(jù)讓每個批次盡可能包含各類數(shù)字訓(xùn)練會更穩(wěn)定。提示我在預(yù)處理階段還額外看了每個數(shù)字類別的數(shù)量分布確認(rèn)了MNIST的數(shù)據(jù)是基本均勻的。如果類別極度不均衡模型的訓(xùn)練策略就需要調(diào)整了好在MNIST沒有這個問題。3. TensorFlow版實現(xiàn)從搭建模型到輸出準(zhǔn)確率3.1 用Sequential搭建一個全連接網(wǎng)絡(luò)TensorFlow里最直觀的建模方式是Keras的Sequential API它就像搭積木一樣一層一層地往上加。我這次設(shè)計的網(wǎng)絡(luò)結(jié)構(gòu)是輸入層784個節(jié)點對應(yīng)28x28的展平圖片第一層全連接128個神經(jīng)元用ReLU激活函數(shù)然后接一個Dropout層丟棄率0.2第二層全連接10個神經(jīng)元用Softmax激活函數(shù)做分類輸出。代碼看起來是這樣的import tensorflow as tf from tensorflow.keras import Sequential from tensorflow.keras.layers import Dense, Dropout, Flatten model Sequential([ Flatten(input_shape(28, 28)), Dense(128, activationrelu), Dropout(0.2), Dense(10, activationsoftmax) ]) model.summary()為什么第一層選128個神經(jīng)元其實沒有絕對的標(biāo)準(zhǔn)。128是一個在效果和計算量之間比較平衡的取值。更少的神經(jīng)元比如64會快一些但可能欠擬合更多的比如256會慢一些但不一定能顯著提升準(zhǔn)確率。你可以把這個128理解為一個“經(jīng)驗起點”后面通過對比實驗再調(diào)整。Dropout層是干什么的它是防止過擬合的。簡單理解就是訓(xùn)練時隨機(jī)“關(guān)掉”一部分神經(jīng)元的連接讓網(wǎng)絡(luò)不能過度依賴某幾個節(jié)點從而逼著更多神經(jīng)元學(xué)會有用的特征。這里設(shè)0.2的意思是每批數(shù)據(jù)訓(xùn)練時有20%的神經(jīng)元連接被隨機(jī)丟棄。10個輸出節(jié)點對應(yīng)0到9這10個數(shù)字類別。Softmax會把10個輸出值轉(zhuǎn)換成概率分布加起來等于1比如模型認(rèn)為這張圖是數(shù)字5的概率是0.85是數(shù)字6的概率是0.1以此類推。最終預(yù)測結(jié)果取概率最大的那個類別。3.2 訓(xùn)練過程里損失函數(shù)和優(yōu)化器怎么選建模之后就要“訓(xùn)練”了。訓(xùn)練的核心是讓模型輸出的概率分布盡量接近真實標(biāo)簽。怎么衡量“接近程度”這就是損失函數(shù)的作用。這次我用的損失函數(shù)是交叉熵CrossEntropyLoss。交叉熵是分類任務(wù)最常用的損失函數(shù)數(shù)值越小說明模型的預(yù)測越接近真實標(biāo)簽。在TensorFlow里因為標(biāo)簽是0到9的整數(shù)而不是獨熱編碼所以用sparse_categorical_crossentropy這個“sparse”表示標(biāo)簽用整數(shù)形式它內(nèi)部會自動處理成獨熱編碼來計算省了一步手動轉(zhuǎn)換很方便。優(yōu)化器選的是Adam。Adam可以理解成一個自適應(yīng)的梯度下降算法它會根據(jù)梯度的情況自動調(diào)整每一步的更新步長學(xué)習(xí)率所以整體上很“省心”。如果是新手用Adam做默認(rèn)選擇基本不會錯。學(xué)習(xí)率我設(shè)置了0.001這是Adam一個很常用的初始值。訓(xùn)練時我設(shè)置驗證集比例是10%。也就是說6萬張訓(xùn)練圖片里5.4萬張用于訓(xùn)練6000張用于驗證。驗證集的作用是在每輪訓(xùn)練后檢查模型在沒見過的數(shù)據(jù)上的表現(xiàn)幫助我們判斷模型是欠擬合、正常、還是過擬合。model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) history model.fit( x_train, y_train, batch_size32, epochs10, validation_split0.1, verbose1 )batch_size32表示每次從訓(xùn)練數(shù)據(jù)里取32張圖片計算一次梯度并更新權(quán)重。為什么不一次把所有數(shù)據(jù)都送進(jìn)去因為6萬張圖片全部算一遍梯度內(nèi)存開銷大而且梯度方向可能不夠平滑。32這個值是經(jīng)驗選擇這也是后面可以調(diào)參的點。3.3 評估階段準(zhǔn)確率之外還要看什么訓(xùn)練結(jié)束后在測試集的1萬張圖片上做了評估得到了大約97.5%的準(zhǔn)確率。也就是說100張手寫數(shù)字圖片里模型大約能正確識別97到98張。test_loss, test_acc model.evaluate(x_test, y_test) print(f測試集準(zhǔn)確率: {test_acc:.4f})但只看準(zhǔn)確率其實不夠我額外生成了分類報告列出了每一個數(shù)字類別的精確率Precision、召回率Recall和F1分?jǐn)?shù)。這一步很有價值因為它能告訴你模型對哪個數(shù)字識別得特別好對哪個數(shù)字容易混淆。比如我這次實驗里模型對數(shù)字“0”的識別非常好但對“4”和“9”這一類字形相似的組合會偶爾搞混。如果某些個別類別的表現(xiàn)拉低了整體準(zhǔn)確率你就有了明確的改進(jìn)方向比如針對混淆的數(shù)字補(bǔ)充更多樣化的訓(xùn)練樣本、調(diào)整網(wǎng)絡(luò)結(jié)構(gòu)、改進(jìn)預(yù)處理方式等。這種帶著業(yè)務(wù)視角去分析模型的方法在實際工作中是非常重要的能力。4. PyTorch版實現(xiàn)手寫訓(xùn)練循環(huán)搞清底層邏輯4.1 數(shù)據(jù)加載Dataset和DataLoader怎么配合PyTorch的數(shù)據(jù)流程跟TensorFlow的高層API差別很大。TensorFlow里model.fit()就把數(shù)據(jù)加載和訓(xùn)練都封裝好了PyTorch則更“手動”每個環(huán)節(jié)都要你自己寫清楚。這個過程能幫你更深入地理解訓(xùn)練到底是怎么發(fā)生的。先把數(shù)據(jù)集讀進(jìn)來。PyTorch自帶MNIST數(shù)據(jù)集可以方便地下載但你通常還需要自己定義怎么對數(shù)據(jù)做變換import torch from torch import nn from torch.utils.data import DataLoader 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) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse)這里的ToTensor()把圖片從0到255的像素值轉(zhuǎn)成0到1的浮點張量同時又多了一個維度變成1x28x281是通道數(shù)因為是灰度圖。Normalize()再做標(biāo)準(zhǔn)化用MNIST數(shù)據(jù)集的像素均值和標(biāo)準(zhǔn)差把數(shù)據(jù)變成標(biāo)準(zhǔn)正態(tài)分布這比簡單的除以255更精細(xì)能進(jìn)一步提升訓(xùn)練穩(wěn)定性。DataLoader就好比一個自動分發(fā)的傳送帶每次告訴你一批數(shù)據(jù)。設(shè)置shuffleTrue意味著每輪訓(xùn)練時數(shù)據(jù)都重新打亂這是防止模型“記住”順序。測試集不用打亂因為測試時順序無所謂。4.2 定義模型類繼承nn.Module要做什么PyTorch里定義模型的標(biāo)準(zhǔn)姿勢是寫一個類繼承nn.Module。這樣做的好處是框架會自動管理模型的參數(shù)、前向傳播過程等。class DigitNet(nn.Module): def __init__(self): super().__init__() self.fc1 nn.Linear(28 * 28, 128) self.dropout nn.Dropout(0.2) self.fc2 nn.Linear(128, 10) def forward(self, x): x x.view(x.size(0), -1) x torch.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) return x model DigitNet()在__init__里定義了網(wǎng)絡(luò)有哪些層在forward里定義了數(shù)據(jù)怎么在這些層之間流動。注意第一層nn.Linear(28*28, 128)輸入是784維但數(shù)據(jù)從DataLoader出來時形狀是[batch_size, 1, 28, 28]所以forward里第一件事就是把[batch_size, 1, 28, 28]展平成[batch_size, 784]用x.view(x.size(0), -1)實現(xiàn)。順便說一下全連接層不經(jīng)過Flatten是沒法直接處理圖片形狀的這也是一個新手經(jīng)常報錯的地方。TensorFlow那邊可以用Flatten()層幫你處理PyTorch這邊則要手動寫這個view操作。兩相對比你會更清楚框架各自幫你做了什么。這里我注意到一個點nn.Linear默認(rèn)會做隨機(jī)初始化通常無需手動指定。但如果你做更復(fù)雜的實驗了解每個層的初始化方式會有幫助因為初始化不當(dāng)可能會導(dǎo)致訓(xùn)練初期的梯度消失或爆炸。4.3 訓(xùn)練循環(huán)里loss.backward()到底發(fā)生了什么PyTorch最讓人印象深刻的地方是自動求導(dǎo)。你只需要把損失傳進(jìn)loss.backward()框架就會自動算出所有參數(shù)的梯度。但具體每一步在做什么我建議每個人都親手寫一遍訓(xùn)練循環(huán)。optimizer torch.optim.Adam(model.parameters(), lr0.001) loss_fn nn.CrossEntropyLoss() def train_one_epoch(model, dataloader, optimizer, loss_fn): model.train() running_loss 0.0 correct 0 total 0 for images, labels in dataloader: optimizer.zero_grad() outputs model(images) loss loss_fn(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return running_loss / len(dataloader), correct / totaloptimizer.zero_grad()的作用是清空上一步留下的梯度。PyTorch的機(jī)制是梯度默認(rèn)會累積如果不手動清零每一步的梯度會疊加導(dǎo)致更新方向混亂訓(xùn)練不收斂。新手最容易犯這個錯。loss.backward()計算梯度。optimizer.step()根據(jù)梯度和優(yōu)化器規(guī)則更新權(quán)重。這三步合在一起才是一個完整的參數(shù)更新過程。在測試時要用torch.no_grad()把梯度計算關(guān)掉因為測試只需要前向傳播不需要反向傳播關(guān)掉它可以省內(nèi)存也加快速度。def evaluate(model, dataloader, loss_fn): model.eval() correct 0 total 0 test_loss 0.0 with torch.no_grad(): for images, labels in dataloader: outputs model(images) loss loss_fn(outputs, labels) test_loss loss.item() _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() return test_loss / len(dataloader), correct / total注意model.train()和model.eval()的切換。這兩個模式會影響Dropout等層的表現(xiàn)訓(xùn)練時Dropout正常隨機(jī)丟棄神經(jīng)元測試時要保證所有神經(jīng)元都用上否則結(jié)果會不穩(wěn)定。忘記切換模式也是一個常見錯誤。用PyTorch跑完同樣的網(wǎng)絡(luò)結(jié)構(gòu)后測試準(zhǔn)確率大約也是97.5%左右。這印證了前面說的兩個框架只是API不同底層數(shù)學(xué)是一樣的。5. 實驗結(jié)果分析與調(diào)參記錄5.1 訓(xùn)練曲線loss降不下去的時候別急著加層我每次訓(xùn)練都會把損失值和準(zhǔn)確率記錄下來畫出曲線圖。這張圖能告訴你很多信息。這次實驗里訓(xùn)練集和驗證集的loss都在下降而且下降速度逐漸變緩這是正常的收斂形態(tài)。訓(xùn)練集和驗證集的曲線大致同步說明沒有嚴(yán)重的過擬合。如果訓(xùn)練loss持續(xù)下降但驗證loss不降反升就是過擬合的典型征兆這時候可以考慮加Dropout、減小網(wǎng)絡(luò)規(guī)模、或用更多數(shù)據(jù)做增強(qiáng)。如果loss一開始降得很慢甚至不降怎么辦我的建議是先別急著改結(jié)構(gòu)查一下學(xué)習(xí)率是否過大或過小才是關(guān)鍵。我這次實驗對比了學(xué)習(xí)率0.01和0.001的情況0.01時loss雖然收斂快但驗證集準(zhǔn)確率波動較大0.001時訓(xùn)練更穩(wěn)最終準(zhǔn)確率反而更高。如果學(xué)習(xí)率再大比如0.1loss可能會出現(xiàn)抖動甚至直接發(fā)散。打開訓(xùn)練曲線就想看“訓(xùn)練跑沒跑起來”這一點比盯著訓(xùn)練集準(zhǔn)確率更重要。訓(xùn)練準(zhǔn)確率高但驗證準(zhǔn)確率低說明泛化差兩個都低可能問題出在數(shù)據(jù)預(yù)處理或?qū)W習(xí)率設(shè)置上。5.2 不同超參數(shù)的對比實驗調(diào)參不能靠猜。我這次實驗做了一組小型對比把關(guān)鍵超參數(shù)和最終準(zhǔn)確率整理成了表格這樣看效果比文字描述直觀得多。網(wǎng)絡(luò)結(jié)構(gòu)優(yōu)化器學(xué)習(xí)率batch_size訓(xùn)練輪數(shù)測試準(zhǔn)確率全連接128Adam0.001321097.5%全連接128SGD0.01321096.9%全連接256Adam0.001321097.8%CNNAdam0.001321099.1%看到這張表結(jié)論就很清晰了從全連接128到256準(zhǔn)確率提升并不明顯但從全連接到CNN準(zhǔn)確率提升非常顯著。這說明在圖像任務(wù)上“結(jié)構(gòu)選對”比“堆參數(shù)”更有效。與其把全連接層從128加到256甚至512不如換一個更適合處理圖像的網(wǎng)絡(luò)結(jié)構(gòu)。批次大小的影響我也測過64的batch_size在訓(xùn)練速度上比32快一些但準(zhǔn)確率會出現(xiàn)輕微下降因為每批次計算的梯度更“粗”了。這個影響在全連接網(wǎng)上還不算大但在一些更復(fù)雜的模型上會更明顯。這些對比實驗本身也是實驗報告里很加分的內(nèi)容。導(dǎo)師和同行看到的不只是“我跑出來了”而是“我理解各個參數(shù)的影響”這完全是兩個層次的體現(xiàn)。5.3 錯誤樣本分析看看模型到底錯在哪光給一個97.5%的數(shù)字還不夠我會把預(yù)測錯的樣本打印出來看看。這個習(xí)慣我非常推薦。我發(fā)現(xiàn)錯誤樣本主要集中在幾類情況有些數(shù)字寫得太潦草連人眼都很難判斷有些數(shù)字因為筆畫太細(xì)或者掃描變形特征不明顯還有一類就是確實長得非常像比如“4”和“9”、“7”和“2”人都有可能看錯。把這些錯例和對應(yīng)的真實標(biāo)簽、預(yù)測標(biāo)簽放在一張圖里你能直觀地看到模型在哪類問題上容易犯糊涂。如果你要繼續(xù)優(yōu)化模型這就是最直接的“改進(jìn)需求說明書”。提高準(zhǔn)確率的思路可以是從數(shù)據(jù)入手做數(shù)據(jù)增強(qiáng)旋轉(zhuǎn)、縮放、添加噪聲讓模型看到更多樣的寫法也可以嘗試更換更大的網(wǎng)絡(luò)架構(gòu)、加入注意力機(jī)制等。6. 實驗報告怎么寫 避坑清單6.1 一份合格的實驗報告該包含什么實驗報告不是把代碼粘貼上去湊頁數(shù)。我寫實驗報告時通常遵循一個結(jié)構(gòu)背景與目標(biāo)、方法、實驗結(jié)果、分析、總結(jié)。背景與目標(biāo)部分清晰說明要解決什么問題識別手寫數(shù)字、為什么這個問題有代表性它是圖像分類的經(jīng)典任務(wù)。方法部分寫清楚網(wǎng)絡(luò)結(jié)構(gòu)、損失函數(shù)、優(yōu)化器、超參數(shù)設(shè)置最好畫出網(wǎng)絡(luò)結(jié)構(gòu)圖配合文字解釋每一層的輸入輸出形狀這部分能讓讀者一眼看懂你的設(shè)計思路。實驗結(jié)果部分放上訓(xùn)練曲線、準(zhǔn)確率表格、錯誤樣本舉例。分析部分對照實驗表格說明哪些因素起了作用哪些沒有。總結(jié)部分寫上這次實驗的收獲和可改進(jìn)方向。寫分析時最忌諱的就是只寫“準(zhǔn)確率是97.5%”而不寫“為什么是97.5%”“有什么辦法能進(jìn)一步提升到99%”。后者才是實驗報告的價值所在。在總結(jié)里我寫了一句自己的真實體會“通過全連接網(wǎng)絡(luò)和卷積網(wǎng)絡(luò)的對比我認(rèn)識到對于圖像任務(wù)選擇合適的網(wǎng)絡(luò)結(jié)構(gòu)比單純增加參數(shù)量更關(guān)鍵?!边@個結(jié)論不是空話而是從對比數(shù)據(jù)里自然得出來的。6.2 我踩過的5個坑和對應(yīng)解決方案做實驗的過程中肯定會出問題我總結(jié)了自己踩過的高頻坑寫在這里。第一個坑是TensorFlow版本不匹配。有一次我照著網(wǎng)上教程用tf.placeholder寫代碼結(jié)果因為我裝的是2.x版本這個API早就沒了直接報錯。解決方法是先確認(rèn)版本教程也必須用對應(yīng)版本的文章?,F(xiàn)在我在查資料時第一眼看的就是文章標(biāo)題或開頭標(biāo)注的版本號。第二個坑是PyTorch訓(xùn)練時忘記調(diào)用optimizer.zero_grad()。導(dǎo)致每輪的梯度不斷累加loss不降反升我還一度以為模型結(jié)構(gòu)寫錯了。自查了一陣才發(fā)現(xiàn)是這么個小問題。這種問題你自己遇到一次就長記性了。第三個坑是CPU訓(xùn)練太慢。我第一次跑實驗時沒裝GPU版PyTorch用CPU訓(xùn)練全連接網(wǎng)絡(luò)還行等到跑一個稍微寬一點的網(wǎng)絡(luò)時每一輪都要好久。后來我發(fā)現(xiàn)一個很聰明的辦法用一個小一點的樣本子集先把整個代碼流程跑通確認(rèn)無誤后再用全量數(shù)據(jù)訓(xùn)練。調(diào)試速度和迭代效率提升明顯。第四個坑是測試時忘了用model.eval()導(dǎo)致Dropout在預(yù)測時仍然生效每次預(yù)測出來的結(jié)果都不一樣。如果你發(fā)現(xiàn)同一個樣本每次預(yù)測結(jié)果都不同十有八九是這個原因。第五個坑是數(shù)據(jù)加載時圖片維度的順序搞混。PyTorch的圖片張量是[通道數(shù), 高度, 寬度]而TensorFlow默認(rèn)是[高度, 寬度, 通道數(shù)]這兩個框架的差異讓人很容易混淆。如果你在寫代碼時發(fā)現(xiàn)維度報錯先檢查數(shù)據(jù)形狀是否符合當(dāng)前框架的約定。6.3 這個項目還能怎么擴(kuò)展跑通MNIST只是一個起點我做完這個實驗后一直在想下一步可以怎么走。最直接的擴(kuò)展是把模型換成真正的CNN網(wǎng)絡(luò)加幾層卷積和池化層你會發(fā)現(xiàn)準(zhǔn)確率明顯提升。進(jìn)一步的思路是嘗試一些現(xiàn)代架構(gòu)里的技巧比如BatchNorm、殘差連接、數(shù)據(jù)增強(qiáng)等觀察它們對性能的影響。再進(jìn)一步你可以把訓(xùn)練好的模型拿來識別自己手寫的數(shù)字通過攝像頭或畫圖工具生成圖片輸入到模型里做預(yù)測。這個玩法看起來簡單但它會逼著你搞定“真實圖像的處理流程”比如背景怎么去除、字母怎么定位、縮放到28x28時保持比例等。這里面門道非常多。如果你對理論更感興趣還可以做一個“參數(shù)可視化”。把訓(xùn)練好的第一層權(quán)重畫出來你可能會看到一些類似筆畫、邊緣的特征提取器。把隱藏層的中間輸出畫出來也能直觀感受到網(wǎng)絡(luò)在逐層提取信息。這些內(nèi)容會讓你的實驗報告和項目展示更有深度。最后再說兩句做完這次雙框架實驗我最大的體會就是手寫數(shù)字識別這個任務(wù)看起來簡單但把整個流程完整跑通、把細(xì)節(jié)搞明白收獲遠(yuǎn)大于“我會用API了”。現(xiàn)在再讓我去寫一個新模型的代碼我不需要去網(wǎng)上搜“該怎么開始”因為訓(xùn)練一個模型的基本流程已經(jīng)刻在腦子里了。這種基本功是任何框架教程都替代不了的。如果你也正在做這個實驗我的建議很直接:別只滿足于把代碼跑通試試改一下網(wǎng)絡(luò)結(jié)構(gòu)、換個優(yōu)化器、看看錯誤樣本這個過程才會讓你真正進(jìn)步。實驗報告也認(rèn)真寫把每個決定背后的原因?qū)懬宄?。等你做完再看這不僅僅是一個作業(yè)而是你深度學(xué)習(xí)入門的第一塊扎實的墊腳石。