網(wǎng)絡(luò)的手寫數(shù)字識別:PyTorch完整源碼與訓(xùn)練避坑指南)
簡介基于AlexNet卷積神經(jīng)網(wǎng)絡(luò)的手寫數(shù)字識別Python項(xiàng)目內(nèi)含完整可運(yùn)行源碼與實(shí)驗(yàn)報(bào)告面向畢業(yè)設(shè)計(jì)、期末大作業(yè)和課程設(shè)計(jì)場景。代碼逐行注明關(guān)鍵邏輯從數(shù)據(jù)加載、模型搭建到訓(xùn)練評估一目了然即使新手也能快速上手配套實(shí)驗(yàn)報(bào)告詳細(xì)介紹了數(shù)據(jù)集準(zhǔn)備、AlexNet網(wǎng)絡(luò)結(jié)構(gòu)、訓(xùn)練參數(shù)配置及結(jié)果可視化源自98分高分項(xiàng)目獲得導(dǎo)師認(rèn)可下載后簡單配置環(huán)境即可部署使用。壓縮包共18個(gè)文件以10個(gè)Python腳本為核心覆蓋模型定義、數(shù)據(jù)處理、訓(xùn)練測試等模塊另有4個(gè)預(yù)訓(xùn)練權(quán)重文件gz格式以及項(xiàng)目配置、依賴清單、說明文檔等文本材料整體僅11.07MB非常輕量。目前已有390人學(xué)習(xí)適合作為深度學(xué)習(xí)圖像識別任務(wù)的入門范例也可直接用于課設(shè)答辯或畢設(shè)展示是一份結(jié)構(gòu)完整、注釋規(guī)范的高分模板。1. 基于 AlexNet 卷積神經(jīng)網(wǎng)絡(luò)的手寫數(shù)字識別這份 python 源碼到底能幫你省多少事如果你正在為畢業(yè)設(shè)計(jì)或者期末大作業(yè)找一份能直接跑、能講清楚原理的深度學(xué)習(xí)項(xiàng)目基于 AlexNet 卷積神經(jīng)網(wǎng)絡(luò)實(shí)現(xiàn)手寫數(shù)字識別的這套 python 源碼大概率就是你想要的答案。它不是一個(gè)只有模型的半成品而是把訓(xùn)練、測試、可視化、配置管理都放在同一個(gè)工程里的完整項(xiàng)目代碼里帶注釋結(jié)構(gòu)上分了 models、train、utils、data 幾大塊新手照著 README 也能把訓(xùn)練跑起來。我做過的課程設(shè)計(jì)和畢設(shè)評審里這類項(xiàng)目最大的價(jià)值在于模型結(jié)構(gòu)不堆砌、代碼風(fēng)格可讀、報(bào)告和代碼對應(yīng)得上答辯時(shí)問到哪一層都能接住話。2. 從下載到跑通目錄結(jié)構(gòu)、環(huán)境依賴與首次運(yùn)行2.1 文件清單哪些文件是核心哪些可以直接忽略這套源碼解壓之后是一個(gè) folder-master 目錄里面并不是所有文件都需要你逐行去讀。我一般拿到一個(gè)開源項(xiàng)目首先會把目錄里每個(gè)文件的作用標(biāo)出來避免把時(shí)間浪費(fèi)在 IDE 配置或者無關(guān)文件上。文件/目錄職責(zé)優(yōu)先級main.py程序入口負(fù)責(zé)初始化配置、啟動(dòng)訓(xùn)練或測試流程核心config.py集中管理超參數(shù)、路徑、設(shè)備選擇核心models/AlexNet.pyAlexNet 網(wǎng)絡(luò)定義本項(xiàng)目的主模型核心models/ResNet.pyResNet 版本的對照模型用于對比實(shí)驗(yàn)擴(kuò)展models/BasicModel.py基礎(chǔ) CNN 模型可作為 baseline擴(kuò)展train/dataset.py數(shù)據(jù)加載、預(yù)處理、劃分訓(xùn)練集和驗(yàn)證集核心train/utils/visualize.py繪制 loss 曲線、準(zhǔn)確率曲線、樣本可視化輔助test/測試腳本加載訓(xùn)練好的權(quán)重做評估核心requirements.txtPython 依賴清單部署.idea/vcs.xmlPyCharm 的 IDE 配置文件可忽略.gitignoreGit 忽略規(guī)則可忽略README.md項(xiàng)目說明文檔先讀從經(jīng)驗(yàn)看訓(xùn)練一個(gè) MNIST 級別的手寫數(shù)字識別模型AlexNet.py、dataset.py、config.py這三個(gè)文件決定成敗。.idea是 PyCharm 自動(dòng)生成的換到 VSCode 也不影響運(yùn)行。2.2 環(huán)境要求與 requirements.txt 部署步驟這套代碼是基于 PyTorch 寫的所以在跑之前先把深度學(xué)習(xí)環(huán)境準(zhǔn)備好。新手最容易在這里卡住裝完 Python 之后忘了裝 CUDA 版 PyTorch后面訓(xùn)練時(shí)只能用 CPU一個(gè) epoch 要跑幾分鐘影響調(diào)參節(jié)奏。我的習(xí)慣是先在終端里確認(rèn) Python 版本再裝依賴。python --version # 建議 3.8 及以上版本 pip install -r requirements.txtrequirements.txt里一般包含 torch、torchvision、numpy、matplotlib 這幾個(gè)核心庫。torchvision 負(fù)責(zé)下載和預(yù)處理 MNIST 數(shù)據(jù)集matplotlib 用于 visualize.py 畫圖。如果你用的是 NVIDIA 顯卡建議單獨(dú)安裝 CUDA 版本對應(yīng)的 PyTorch訓(xùn)練速度能快一個(gè)量級如果沒有獨(dú)立顯卡CPU 版本也能跑只是需要把config.py里的設(shè)備改成cpu。2.3 首次運(yùn)行從 config.py 改參數(shù)到啟動(dòng)訓(xùn)練跑通這個(gè)項(xiàng)目不需要改任何模型代碼只需要打開config.py確認(rèn)幾個(gè)關(guān)鍵參數(shù)batch_size、learning_rate、epochs、device。我第一次拿到這套源碼時(shí)直接把 epochs 改成了 10batch_size 保持 64在 CPU 上跑了大約二十分鐘驗(yàn)證集準(zhǔn)確率就到了 98% 以上。python main.py運(yùn)行之后終端會逐行打印當(dāng)前 epoch、loss、accuracy。如果看到 loss 在穩(wěn)步下降說明數(shù)據(jù)和模型已經(jīng)正常交互。如果報(bào)錯(cuò)先看是不是缺依賴再看是不是路徑問題。項(xiàng)目里data目錄帶__init__.py說明數(shù)據(jù)加載是包結(jié)構(gòu)不用手動(dòng)去下載 MNISTdataset.py會在第一次運(yùn)行時(shí)自動(dòng)下載到本地。2.4 IDE 與運(yùn)行方式不是必須用 PyCharm.idea目錄會讓很多人誤以為必須用 PyCharm 打開其實(shí)不是。VSCode 配置好 Python 解釋器之后一樣能跑關(guān)鍵在于main.py的當(dāng)前工作目錄要指向項(xiàng)目根目錄否則相對路徑會找不到models和train包。我一般這樣處理cd folder-master python main.py先在終端里 cd 到項(xiàng)目根目錄再執(zhí)行腳本這樣最不容易出路徑問題。如果你在 IDE 里直接點(diǎn)運(yùn)行按鈕記得檢查 Run Configuration 里的 working directory 是否指向項(xiàng)目根目錄。3. AlexNet 網(wǎng)絡(luò)結(jié)構(gòu)拆解5 個(gè)卷積層如何適配 28×28 的 MNIST3.1 原始 AlexNet 與 MNIST 版的差異原始 AlexNet 是 2012 年 ImageNet 比賽的冠軍模型輸入是 224×224 的 RGB 三通道圖像用 5 個(gè)卷積層加 3 個(gè)全連接層提取特征。但 MNIST 手寫數(shù)字是 28×28 的灰度單通道圖像直接把原始結(jié)構(gòu)搬過來并不合適。最大的問題在于感受野第一層卷積核是 11×11、步長是 4作用在 28×28 圖像上特征圖會迅速縮小信息損失太嚴(yán)重。所以這套源碼里的 AlexNet 是經(jīng)過適配的版本核心改動(dòng)集中在兩點(diǎn)輸入通道從 3 改成 1第一層卷積核從 11×11 改成 5×5步長從 4 改成 1。這樣既保留了 AlexNet 的深層結(jié)構(gòu)特性又能適配小尺寸灰度圖。這也是答辯時(shí)老師最喜歡問的一個(gè)點(diǎn)。3.2 models/AlexNet.py 核心代碼AlexNet.py里定義的網(wǎng)絡(luò)結(jié)構(gòu)并不復(fù)雜關(guān)鍵在于每一層的參數(shù)怎么設(shè)、輸出尺寸怎么算。下面是核心代碼的簡化版本import torch.nn as nn class AlexNet(nn.Module): def __init__(self, num_classes10): super(AlexNet, self).__init__() # 輸入: (batch, 1, 28, 28) 灰度圖 self.features nn.Sequential( # conv1: 1 - 96, 5x5 卷積步長 1padding 2 nn.Conv2d(1, 96, kernel_size5, stride1, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), # 28 - 13 # conv2: 96 - 256, 5x5 卷積 nn.Conv2d(96, 256, kernel_size5, stride1, padding2), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), # 13 - 6 # conv3: 256 - 384, 3x3 卷積 nn.Conv2d(256, 384, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), # conv4: 384 - 384, 3x3 卷積 nn.Conv2d(384, 384, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), # conv5: 384 - 256, 3x3 卷積 nn.Conv2d(384, 256, kernel_size3, stride1, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size3, stride2), # 6 - 2 ) # 最后特征圖尺寸: 256 * 2 * 2 self.classifier nn.Sequential( nn.Dropout(p0.5), nn.Linear(256 * 2 * 2, 1024), nn.ReLU(inplaceTrue), nn.Dropout(p0.5), nn.Linear(1024, 512), nn.ReLU(inplaceTrue), nn.Linear(512, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) # 展平 x self.classifier(x) return x這段代碼里有三個(gè)參數(shù)是手動(dòng)調(diào)過的第一個(gè) MaxPool2d 的 stride2 會把 28×28 降到 13×13第二個(gè)降到 6×6第三個(gè)降到 2×2。全連接層輸入維度是 256×2×2也就是 1024 個(gè)特征值。如果你照搬原始 AlexNet 的 6×6 輸出這里全連接層維度就要改成 256×6×69216顯存和計(jì)算量會大很多。Dropout 設(shè)置在訓(xùn)練時(shí)隨機(jī)丟棄一半神經(jīng)元能有效降低過擬合風(fēng)險(xiǎn)在測試時(shí) PyTorch 會自動(dòng)關(guān)閉 Dropout不需要手動(dòng)處理。3.3 輸入尺寸計(jì)算從卷積到全連接層維度對齊新手最容易算錯(cuò)的就是卷積層輸出尺寸。公式是(輸入尺寸 - kernel_size 2 × padding) / stride 1。以第一層為例(28 - 5 2×2) / 1 1 28padding 2 讓輸出尺寸保持 28 不變。池化層則按(輸入 - kernel_size) / stride 1計(jì)算28×28 經(jīng)過 3×3 池化步長 2 變成(28-3)/2113第二次從 13 變 6第三次從 6 變 2。這個(gè)計(jì)算過程建議自己手推一遍因?yàn)閷?shí)驗(yàn)報(bào)告里需要寫清楚每一層的輸出形狀。答辯時(shí)老師會問「為什么全連接層第一層是 1024 而不是 9216」你只要能說出尺寸推導(dǎo)過程這個(gè)項(xiàng)目就立住了。3.4 ResNet.py 與 BasicModel.py作為對比基線這套源碼還額外給了 ResNet 和 BasicModel 兩個(gè)模型這說明作者在設(shè)計(jì)時(shí)就考慮了對照實(shí)驗(yàn)。畢設(shè)和課程設(shè)計(jì)里對比實(shí)驗(yàn)是很加分的部分用同一個(gè)數(shù)據(jù)集跑三組模型最后對比準(zhǔn)確率和訓(xùn)練收斂速度。BasicModel 是一個(gè)兩層卷積的簡單網(wǎng)絡(luò)作為 baselineResNet 則引入了殘差結(jié)構(gòu)理論上收斂更快。我見過很多學(xué)生只跑一個(gè)模型就交報(bào)告答辯時(shí)被問「為什么不用 ResNet 試試」直接卡住。這套源碼自帶三個(gè)模型你有充分理由在報(bào)告里寫橫向?qū)Ρ葘?dǎo)師會認(rèn)為你對模型選型有思考而不是只會調(diào)庫。4. dataset.py 與 config.py數(shù)據(jù)管線和超參數(shù)下放4.1 數(shù)據(jù)加載與預(yù)處理MNIST 的歸一化是硬要求train/dataset.py負(fù)責(zé)把 MNIST 原始數(shù)據(jù)處理成模型能接受的 Tensor。MNIST 本身是 PIL 圖像格式像素值范圍 0 到 255如果不做歸一化直接喂進(jìn)網(wǎng)絡(luò)第一層卷積的輸入分布會很差訓(xùn)練初期 loss 可能直接不降。這套代碼里用的是transforms.ToTensor()加transforms.Normalize((0.1307,), (0.3081,))這兩個(gè)值是 MNIST 數(shù)據(jù)集的全局均值和標(biāo)準(zhǔn)差屬于官方推薦配置。標(biāo)準(zhǔn)的 PyTorch 數(shù)據(jù)加載寫法是from torchvision import datasets, transforms from torch.utils.data import DataLoader transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) train_loader DataLoader( datasettrain_dataset, batch_size64, shuffleTrue, num_workers2 )num_workers2表示用兩個(gè)子進(jìn)程做數(shù)據(jù)加載能緩解 GPU 訓(xùn)練時(shí)的數(shù)據(jù)瓶頸。如果在 Windows 上跑num_workers大于 0 可能會報(bào)多進(jìn)程相關(guān)的錯(cuò)誤改成 0 就行。4.2 超參數(shù)集中管理config.py 是調(diào)參的后悔藥這個(gè)項(xiàng)目把超參數(shù)全部集中到config.py而不是散落在各個(gè)腳本里。這點(diǎn)我特別認(rèn)可因?yàn)檎{(diào)參的時(shí)候你不需要去翻代碼打開一個(gè)文件改了就能跑。常見參數(shù)包括參數(shù)名推薦值說明batch_size64顯存不夠就降到 32learning_rate0.001Adam 優(yōu)化器常用初始值epochs10 ~ 15MNIST 上超過 15 個(gè) epoch 容易過擬合devicecuda / cpu無顯卡時(shí)強(qiáng)制設(shè)成 cpunum_classes100~9 十個(gè)數(shù)字learning_rate的設(shè)定值直接影響收斂行為。0.001 是 Adam 優(yōu)化器的經(jīng)驗(yàn)值配合交叉熵?fù)p失函數(shù)在 MNIST 上效果穩(wěn)定。如果 loss 震蕩不下降優(yōu)先把學(xué)習(xí)率降到 0.0001 再試這是最有效的后悔藥。4.3 訓(xùn)練測試流程與日志輸出main.py會把訓(xùn)練和測試串起來每個(gè) epoch 結(jié)束之后在測試集上做一次評估打印當(dāng)前準(zhǔn)確率。我建議訓(xùn)練過程中每 500 個(gè) batch 就打印一次 loss這樣能提前發(fā)現(xiàn)訓(xùn)練異常。比如 loss 在某個(gè)數(shù)值附近震蕩不降很可能就是學(xué)習(xí)率偏大或者數(shù)據(jù)沒歸一化。for epoch in range(config.epochs): for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() output model(data) loss criterion(output, target) loss.backward() optimizer.step() if batch_idx % 100 0: print(fEpoch {epoch}, Batch {batch_idx}, Loss {loss.item():.6f})訓(xùn)練完成后權(quán)重會保存成.pth文件test目錄里的腳本會自動(dòng)加載這個(gè)權(quán)重文件做最終評估。這里要記住保存的應(yīng)該是model.state_dict()而不是整個(gè) model 對象后者在跨環(huán)境加載時(shí)容易踩兼容性的坑。5. 訓(xùn)練避坑排查Loss 不降、維度不匹配與設(shè)備失控的五個(gè)常見坑5.1 坑一全連接層維度不匹配報(bào)錯(cuò)現(xiàn)象運(yùn)行python main.py時(shí)拋RuntimeError: size mismatch, m1: [64 x 8192], m2: [1024 x 512]。原因卷積層輸出特征圖被展平后是 8192 維但全連接層第一層期望輸入 1024 維。多半是改了batch_size或者調(diào)整了池化層參數(shù)導(dǎo)致特征圖尺寸變了而全連接層維度沒有同步修改。解決在AlexNet.py的forward函數(shù)里加一行打印print(x.shape)然后根據(jù)實(shí)際的展平維度去改classifier的第一層nn.Linear的輸入尺寸。我一般會在模型里加一段注釋記下特征圖尺寸計(jì)算過程下次改參數(shù)時(shí)直接對照。5.2 坑二Loss 降不下去訓(xùn)練像沒開始一樣現(xiàn)象loss 一直停留在 2.3 左右?guī)缀醪蛔兓愃齐S機(jī)猜測的水平。原因最常見的有三種。第一學(xué)習(xí)率設(shè)置過大loss 在震蕩第二輸入數(shù)據(jù)沒有歸一化像素值太大導(dǎo)致梯度爆炸第三模型初始化權(quán)重有問題。其中數(shù)據(jù)歸一化是最容易被忽略的。解決先檢查transforms.Normalize是否生效再看learning_rate是否在合理區(qū)間。如果用的是 SGD學(xué)習(xí)率通常是 0.01如果用的是 Adam0.001 是起點(diǎn)。兩個(gè)優(yōu)化器的默認(rèn)學(xué)習(xí)率差一個(gè)數(shù)量級混用會踩坑。5.3 坑三CPU 與 GPU 設(shè)備不匹配現(xiàn)象報(bào)錯(cuò)RuntimeError: Expected all tensors to be on the same device模型在 cuda 上數(shù)據(jù)在 cpu 上或者反過來。原因config.py里設(shè)置了device torch.device(cuda)但當(dāng)前機(jī)器沒有可用 GPU或者數(shù)據(jù)沒有調(diào)用.to(device)。解決把設(shè)備選擇改成 device torch.device(cuda if torch.cuda.is_available() else cpu)然后確保每個(gè) batch 的data和target都執(zhí)行了.to(device)。模型也要在訓(xùn)練前model.to(device)。這三處只要有一處漏了就會觸發(fā)這個(gè)報(bào)錯(cuò)。5.4 坑四MNIST 數(shù)據(jù)集下載失敗或卡住現(xiàn)象第一次運(yùn)行腳本時(shí)終端停在Downloading ...長時(shí)間不動(dòng)或者直接超時(shí)報(bào)錯(cuò)。原因torchvision 默認(rèn)從國外服務(wù)器下載 MNIST網(wǎng)絡(luò)不穩(wěn)定時(shí)就容易斷。解決手動(dòng)從鏡像站下載mnist.pkl.gz文件放到data目錄下然后把downloadTrue改成downloadFalse。如果已經(jīng)完全下載失敗刪掉data目錄下的殘留文件再重新跑避免文件損壞。5.5 坑五訓(xùn)練準(zhǔn)確率高測試準(zhǔn)確率卻普通現(xiàn)象訓(xùn)練集最后幾個(gè) epoch 準(zhǔn)確率接近 99%但測試集只有 96% 左右。原因這是典型的過擬合信號模型把訓(xùn)練集的特征背下來了沒有泛化到新樣本。MNIST 數(shù)據(jù)量不算小但 15 個(gè) epoch 以上的訓(xùn)練依然可能過擬合尤其在全連接層參數(shù)比較多時(shí)。解決調(diào)整config.py里的epochs或者增大AlexNet.py中 Dropout 的比例從 0.5 調(diào)到 0.7。如果做過對比實(shí)驗(yàn)也可以用 ResNet 的殘差結(jié)構(gòu)來抑制過擬合。報(bào)告里寫清楚調(diào)參前后對比就行。6. 進(jìn)階驗(yàn)證把 loss 曲線畫出來再對一張真實(shí)手寫圖做預(yù)測6.1 可視化腳本的使用方式別把visualize.py當(dāng)成可有可無的裝飾實(shí)驗(yàn)報(bào)告里最有說服力的就是那張 loss 下降曲線和 accuracy 上升曲線。我一般會在訓(xùn)練完模型之后單獨(dú)跑一次可視化腳本生成兩張圖然后直接貼進(jìn)報(bào)告。visualize.py內(nèi)部會讀取訓(xùn)練過程中記錄的 loss 列表再用 matplotlib 繪制。import matplotlib.pyplot as plt def plot_loss(train_losses, val_losses): plt.figure(figsize(8, 5)) plt.plot(train_losses, labelTrain Loss) plt.plot(val_losses, labelVal Loss) plt.xlabel(Epoch) plt.ylabel(Loss) plt.legend() plt.title(Loss Curve on MNIST) plt.savefig(loss_curve.png, dpi150)如果你的訓(xùn)練過程沒有手動(dòng)記錄 loss 列表最簡單的辦法是在main.py的訓(xùn)練循環(huán)里定義一個(gè)空列表每個(gè) epoch 結(jié)束時(shí)把平均 lossappend進(jìn)去。導(dǎo)出 PNG 之后圖片質(zhì)量夠清晰實(shí)驗(yàn)報(bào)告直接能用。6.2 單張圖片推理流程訓(xùn)練完模型除了在測試集上算準(zhǔn)確率我建議再跑一次單張圖片推理把自己手寫的數(shù)字或測試集里的某張圖單獨(dú)拎出來預(yù)測。這段代碼可以作為實(shí)驗(yàn)報(bào)告最后的驗(yàn)證環(huán)節(jié)from PIL import Image import torchvision.transforms as transforms # 加載模型權(quán)重 model AlexNet(num_classes10) model.load_state_dict(torch.load(checkpoint.pth, map_locationcpu)) model.eval() # 預(yù)處理單張圖片 img Image.open(test_digit.png).convert(L).resize((28, 28)) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) input_tensor transform(img).unsqueeze(0) # 推理 with torch.no_grad(): output model(input_tensor) pred output.argmax(dim1, keepdimTrue) print(fPredicted digit: {pred.item()})這里的model.eval()務(wù)必加上它會把 Dropout 和 BatchNorm 切換到推理模式。如果不加同一個(gè)輸入每次預(yù)測結(jié)果可能都不一樣這是新手最容易忽略的細(xì)節(jié)。我記得曾經(jīng)有個(gè)學(xué)生拿這套項(xiàng)目跑完之后把測試準(zhǔn)確率直接寫在報(bào)告里就交了結(jié)果答辯時(shí)被要求現(xiàn)場用鼠標(biāo)畫一個(gè)數(shù)字讓模型識別當(dāng)場翻車。從那以后我每跑完一個(gè)分類模型都會強(qiáng)制走一遍單張推理流程確認(rèn)不只是一個(gè)測試指標(biāo)是真的能對新樣本做預(yù)測。這套源碼本身已經(jīng)把所有關(guān)鍵環(huán)節(jié)都準(zhǔn)備好了你按照上面的步驟把訓(xùn)練跑完、把可視化做完再順手驗(yàn)證一張真實(shí)圖片整個(gè)項(xiàng)目就能形成一個(gè)完整閉環(huán)希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取