習(xí)的大米識(shí)別:PyTorch訓(xùn)練與PyQt界面全流程實(shí)戰(zhàn))
簡(jiǎn)介本資源是一套基于PyTorch框架的CNN深度學(xué)習(xí)大米識(shí)別實(shí)戰(zhàn)項(xiàng)目面向具備Python基礎(chǔ)、希望入門圖像分類的開發(fā)者與在校學(xué)生。項(xiàng)目以大米品種識(shí)別為任務(wù)場(chǎng)景完整覆蓋從數(shù)據(jù)預(yù)處理、模型訓(xùn)練到可視化交互的全流程適合作為課程設(shè)計(jì)或深度學(xué)習(xí)練手案例。壓縮包共906個(gè)文件包含900張jpg圖片構(gòu)成的多類別數(shù)據(jù)集、3個(gè)txt說(shuō)明與日志文件、3個(gè)py腳本整體約11.98MB體積輕量便于本地運(yùn)行。數(shù)據(jù)集已做短邊補(bǔ)灰邊成正方形及旋轉(zhuǎn)角度等增強(qiáng)處理腳本依次完成標(biāo)簽文本生成、模型訓(xùn)練與PyQt界面調(diào)用訓(xùn)練過(guò)程會(huì)保存模型權(quán)重并記錄每個(gè)epoch的驗(yàn)證損失與準(zhǔn)確率。目前已有152人學(xué)習(xí)讀者可借此掌握?qǐng)D像分類數(shù)據(jù)組織、CNN訓(xùn)練調(diào)參與桌面端推理界面的完整實(shí)現(xiàn)思路。1. 從一堆文件名到可訓(xùn)練數(shù)據(jù)集這套大米識(shí)別資源到底能跑出什么如果你手頭正好有一批按類別分文件夾存放的大米圖片文件名里還帶著rotated45、flip這類后綴想快速驗(yàn)證一個(gè) CNN 分類模型能不能把它們區(qū)分開這套「基于 CNN 深度學(xué)習(xí)的大米識(shí)別-含圖片數(shù)據(jù)集」就是沖這個(gè)場(chǎng)景來(lái)的。它把數(shù)據(jù)增強(qiáng)、標(biāo)簽生成、模型訓(xùn)練和 PyQt 可視化界面串成了一條完整鏈路技術(shù)棧是 Python PyTorch適合剛接觸深度學(xué)習(xí)圖像分類、需要跑通一個(gè)完整項(xiàng)目的人也適合想拿它當(dāng)模板改自己數(shù)據(jù)集的從業(yè)者。數(shù)據(jù)集里能看到 Ipsala 等類別圖片已經(jīng)做過(guò)旋轉(zhuǎn)和翻轉(zhuǎn)增強(qiáng)省掉了從零寫增強(qiáng)腳本的功夫。下面按「資源是什么、怎么裝、怎么跑、坑在哪」的順序拆開講。2. 環(huán)境配置與依賴安裝把 PyTorch 和 PyQt 裝進(jìn)同一個(gè)解釋器2.1 為什么這套代碼對(duì)版本敏感這套資源的核心依賴只有兩塊PyTorch 負(fù)責(zé) CNN 訓(xùn)練PyQt 負(fù)責(zé)最后的可視化界面。問題在于PyTorch 的版本和 CUDA 驅(qū)動(dòng)、Python 版本之間是強(qiáng)綁定的而 PyQt 又對(duì) Python 版本有自己的一套要求。我一般會(huì)先確認(rèn)本機(jī) Python 版本再?zèng)Q定裝哪個(gè) PyTorch 輪子。如果你用的是較新的 PythonPyQt5 的某些舊版本可能裝不上這時(shí)候要么降 Python要么換 PyQt6 并改幾行導(dǎo)入代碼。資源里給了requirement.txt但環(huán)境仍然需要自行配置這一步?jīng)]有后悔藥裝錯(cuò)了后面訓(xùn)練和界面都會(huì)報(bào)錯(cuò)。常見做法是建一個(gè)獨(dú)立虛擬環(huán)境避免和系統(tǒng)里已有的包打架。下面這套命令是我在 Windows 和 Linux 上都跑過(guò)的順序先建環(huán)境再裝依賴最后單獨(dú)確認(rèn) PyTorch 能不能調(diào)用 GPU。# 創(chuàng)建虛擬環(huán)境Python 版本建議 3.8 到 3.10 python -m venv rice_env # 激活環(huán)境Windows rice_env\Scripts\activate # 激活環(huán)境Linux / macOS source rice_env/bin/activate # 先裝 PyTorch具體命令按你的 CUDA 版本去官網(wǎng)生成 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 再裝其余依賴requirement.txt 里通常包含 numpy、Pillow、PyQt5 等 pip install -r requirement.txt邏輯說(shuō)明先建虛擬環(huán)境是為了隔離依賴避免 PyQt 和系統(tǒng)里其他 Qt 程序沖突。PyTorch 單獨(dú)裝是因?yàn)樗陌惭b源和普通 PyPI 不一樣混在requirement.txt里容易裝成 CPU 版還不自知。參數(shù)上--index-url后面的cu118對(duì)應(yīng) CUDA 11.8你要按自己顯卡驅(qū)動(dòng)支持的版本換裝完用下面這段驗(yàn)證。import torch print(torch version:, torch.__version__) print(cuda available:, torch.cuda.is_available()) print(device name:, torch.cuda.get_device_name(0) if torch.cuda.is_available() else CPU only)如果cuda available是 False訓(xùn)練會(huì)退回 CPU速度差很多但代碼本身能跑。這時(shí)候別急著懷疑代碼先查驅(qū)動(dòng)和 PyTorch 版本是否匹配。2.2 requirement.txt 里沒寫全的隱式依賴requirement.txt通常只列了直接依賴但實(shí)際跑起來(lái)還會(huì)用到tqdm做進(jìn)度條、matplotlib畫損失曲線、scikit-learn算混淆矩陣這類庫(kù)。我一般會(huì)先按 txt 裝一遍然后直接跑01數(shù)據(jù)集文本生成制作.py報(bào)什么缺什么再補(bǔ)。這樣比一次性裝一大堆用不上的包更省事。另外PyQt 界面在部分 Linux 發(fā)行版上需要額外的系統(tǒng)庫(kù)比如libxcb-xinerama0缺了會(huì)直接閃退報(bào)錯(cuò)信息里會(huì)提到xcb遇到時(shí)用系統(tǒng)包管理器補(bǔ)上即可。提示裝完 PyTorch 后先跑一次torch.cuda.is_available()確認(rèn) GPU 可用再往下走否則后面訓(xùn)練慢到你會(huì)懷疑人生。3. 數(shù)據(jù)增強(qiáng)與標(biāo)簽生成01 腳本到底對(duì)圖片做了什么3.1 短邊補(bǔ)灰邊與旋轉(zhuǎn)增強(qiáng)的實(shí)現(xiàn)邏輯這套資源對(duì)數(shù)據(jù)集的預(yù)處理有兩個(gè)關(guān)鍵動(dòng)作一是把非正方形圖片通過(guò)短邊補(bǔ)灰邊變成正方形二是做旋轉(zhuǎn)角度增強(qiáng)。補(bǔ)灰邊的好處是避免直接拉伸導(dǎo)致大米形狀變形旋轉(zhuǎn)增強(qiáng)則是為了讓模型見過(guò)更多角度下的米粒。資源里的文件名已經(jīng)帶了rotated45和flip后綴說(shuō)明增強(qiáng)后的圖片已經(jīng)落盤01數(shù)據(jù)集文本生成制作.py要做的是讀取這些圖片路徑并生成標(biāo)簽文本。我一般會(huì)先看一眼數(shù)據(jù)集目錄結(jié)構(gòu)確認(rèn)每個(gè)類別一個(gè)文件夾文件夾名就是類別名。下面這段代碼是我按常見做法補(bǔ)的目錄檢查邏輯你可以直接拿去對(duì)照自己的數(shù)據(jù)集。import os data_root dataset # 數(shù)據(jù)集根目錄按實(shí)際路徑改 classes sorted(os.listdir(data_root)) print(類別列表:, classes) for cls in classes: cls_dir os.path.join(data_root, cls) if not os.path.isdir(cls_dir): continue imgs [f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .png, .jpeg))] print(f{cls}: {len(imgs)} 張圖片)邏輯說(shuō)明sorted保證類別順序穩(wěn)定避免每次運(yùn)行標(biāo)簽編號(hào)亂跳。os.path.isdir過(guò)濾掉非文件夾項(xiàng)防止誤讀。圖片后綴判斷覆蓋 jpg、png、jpeg 三種常見格式。參數(shù)上data_root要改成你解壓后的實(shí)際路徑如果數(shù)據(jù)集里還有子文件夾嵌套需要遞歸遍歷這里只做了一層。3.2 生成訓(xùn)練集與驗(yàn)證集 txt 的步驟01數(shù)據(jù)集文本生成制作.py的核心產(chǎn)出是一個(gè) txt 文件每行記錄圖片路徑和對(duì)應(yīng)標(biāo)簽。常見做法是按比例劃分訓(xùn)練集和驗(yàn)證集比如 8:2。下面這段是我常用的劃分邏輯和資源里腳本的思路一致。import os import random data_root dataset output_txt train_val.txt val_ratio 0.2 random.seed(42) # 固定隨機(jī)種子保證每次劃分一致 lines [] classes sorted(os.listdir(data_root)) for idx, cls in enumerate(classes): cls_dir os.path.join(data_root, cls) if not os.path.isdir(cls_dir): continue imgs [f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .png, .jpeg))] random.shuffle(imgs) val_count int(len(imgs) * val_ratio) for i, img in enumerate(imgs): split val if i val_count else train lines.append(f{os.path.join(cls_dir, img)}\t{idx}\t{split}) with open(output_txt, w, encodingutf-8) as f: f.write(\n.join(lines)) print(已寫入, len(lines), 條記錄)邏輯說(shuō)明random.seed(42)是血淚經(jīng)驗(yàn)不固定種子的話每次劃分不同驗(yàn)證集準(zhǔn)確率會(huì)飄沒法復(fù)現(xiàn)。val_count按比例算驗(yàn)證集數(shù)量split字段標(biāo)記 train 或 val訓(xùn)練腳本讀取時(shí)按這個(gè)字段分流。參數(shù)上val_ratio可以按數(shù)據(jù)集大小調(diào)數(shù)據(jù)少的時(shí)候 0.1 到 0.2 都行數(shù)據(jù)多可以到 0.3。標(biāo)簽用類別索引idx訓(xùn)練時(shí)配合CrossEntropyLoss使用。注意如果某個(gè)類別圖片特別少按比例劃分后驗(yàn)證集可能一張都沒有這時(shí)候要么調(diào)低val_ratio要么保證每類至少留一張驗(yàn)證圖。4. 模型訓(xùn)練與日志分析02 腳本里的 CNN 結(jié)構(gòu)和訓(xùn)練參數(shù)4.1 一個(gè)夠用的 CNN 分類網(wǎng)絡(luò)長(zhǎng)什么樣這套資源的訓(xùn)練腳本02深度學(xué)習(xí)模型訓(xùn)練.py里應(yīng)該定義了一個(gè) CNN 網(wǎng)絡(luò)。按大米識(shí)別這個(gè)任務(wù)的特點(diǎn)輸入圖片經(jīng)過(guò)補(bǔ)灰邊后是正方形尺寸通常會(huì)被縮放到 224 或 128。我一般會(huì)用一個(gè)三層卷積加全連接的結(jié)構(gòu)夠用且不容易過(guò)擬合。下面這段是我按常見做法寫的網(wǎng)絡(luò)定義你可以對(duì)照資源里的實(shí)現(xiàn)看差異。import torch import torch.nn as nn class RiceCNN(nn.Module): def __init__(self, num_classes): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 28 * 28, 256), nn.ReLU(), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): return self.classifier(self.features(x))邏輯說(shuō)明三層卷積逐步把通道數(shù)從 3 提到 128每次池化把空間尺寸減半。Dropout(0.5)是防過(guò)擬合的關(guān)鍵大米圖片類間差異小不加 dropout 訓(xùn)練集準(zhǔn)確率會(huì)飆到很高但驗(yàn)證集上不去。參數(shù)上num_classes按你數(shù)據(jù)集實(shí)際類別數(shù)傳128 * 28 * 28這個(gè)維度要按輸入圖片尺寸算如果輸入是 128 而不是 224這里要改成對(duì)應(yīng)的數(shù)。4.2 訓(xùn)練循環(huán)與 log 日志里該看什么訓(xùn)練腳本跑起來(lái)后會(huì)在本地存 log記錄每個(gè) epoch 的驗(yàn)證集損失和準(zhǔn)確率。我一般會(huì)重點(diǎn)看兩個(gè)信號(hào)驗(yàn)證集損失是否在連續(xù)幾個(gè) epoch 后開始上升以及驗(yàn)證集準(zhǔn)確率和訓(xùn)練集準(zhǔn)確率的差距。前者說(shuō)明過(guò)擬合后者說(shuō)明泛化能力。下面這段是訓(xùn)練循環(huán)的核心骨架。import torch from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model RiceCNN(num_classeslen(classes)).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fepoch {epoch}, val acc {correct / total:.4f})邏輯說(shuō)明model.train()和model.eval()切換影響 dropout 和 batchnorm 行為漏寫會(huì)導(dǎo)致驗(yàn)證結(jié)果不準(zhǔn)。optimizer.zero_grad()必須在反向傳播前調(diào)用否則梯度會(huì)累加。參數(shù)上lr1e-3是 Adam 的常用起點(diǎn)如果損失不下降可以降到 1e-4如果下降太慢可以升到 3e-3。epoch數(shù)量按數(shù)據(jù)集大小調(diào)大米識(shí)別這種任務(wù)通常 20 到 50 輪夠用。提示log 里如果驗(yàn)證集準(zhǔn)確率一直在 0.2 到 0.3 徘徊先檢查標(biāo)簽生成時(shí)類別索引和訓(xùn)練時(shí)是否一致這是最常見的翻車點(diǎn)。5. PyQt 界面與推理03 腳本怎么把模型變成可點(diǎn)的按鈕5.1 界面加載模型與圖片的流程03pyqt_ui界面.py的作用是提供一個(gè)可視化窗口點(diǎn)按鈕加載圖片然后顯示識(shí)別結(jié)果。常見做法是在界面初始化時(shí)加載訓(xùn)練好的模型權(quán)重點(diǎn)擊按鈕后用QFileDialog選圖片再用和訓(xùn)練時(shí)一致的預(yù)處理流程把圖片轉(zhuǎn)成張量送進(jìn)模型。下面這段是界面核心邏輯的骨架。import sys import torch from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QFileDialog, QVBoxLayout from PIL import Image from torchvision import transforms class RiceUI(QWidget): def __init__(self, model, classes): super().__init__() self.model model self.classes classes self.transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), ]) self.label QLabel(等待加載圖片) btn QPushButton(選擇圖片) btn.clicked.connect(self.load_image) layout QVBoxLayout() layout.addWidget(btn) layout.addWidget(self.label) self.setLayout(layout) def load_image(self): path, _ QFileDialog.getOpenFileName(self, 選擇圖片, , Images (*.jpg *.png)) if not path: return img Image.open(path).convert(RGB) tensor self.transform(img).unsqueeze(0) with torch.no_grad(): pred self.model(tensor).argmax(dim1).item() self.label.setText(f識(shí)別結(jié)果: {self.classes[pred]})邏輯說(shuō)明transforms.Resize((224, 224))必須和訓(xùn)練時(shí)的輸入尺寸一致不一致會(huì)導(dǎo)致識(shí)別結(jié)果亂跳。unsqueeze(0)是加 batch 維度模型 forward 要求四維輸入。torch.no_grad()關(guān)閉梯度計(jì)算推理時(shí)省顯存。參數(shù)上classes要和訓(xùn)練時(shí)sorted(os.listdir(data_root))的順序完全一致否則標(biāo)簽會(huì)對(duì)錯(cuò)。5.2 界面與訓(xùn)練腳本的標(biāo)簽對(duì)齊問題界面里顯示的類別名來(lái)自classes列表這個(gè)列表的順序必須和訓(xùn)練時(shí)生成標(biāo)簽的順序一致。我一般會(huì)把類別列表存成一個(gè) json 文件訓(xùn)練和界面都讀同一個(gè)文件避免兩邊各寫一份導(dǎo)致對(duì)不上。如果你發(fā)現(xiàn)界面識(shí)別結(jié)果總是固定錯(cuò)到某一類先查這個(gè)順序而不是懷疑模型沒訓(xùn)練好。注意PyQt 界面在部分高分辨率屏幕上會(huì)出現(xiàn)控件錯(cuò)位這是 Qt 的縮放策略問題可以在QApplication創(chuàng)建前設(shè)置QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)緩解。6. 避坑與排查這套資源跑不起來(lái)時(shí)先查這五條6.1 現(xiàn)象01 腳本報(bào) FileNotFoundError原因data_root路徑寫的是相對(duì)路徑但腳本運(yùn)行目錄和你解壓目錄不一致。解決把data_root改成絕對(duì)路徑或者在腳本開頭用os.chdir切到數(shù)據(jù)集所在目錄。我一般直接在代碼里寫絕對(duì)路徑省得來(lái)回調(diào)。6.2 現(xiàn)象訓(xùn)練時(shí) loss 變成 nan原因?qū)W習(xí)率太大或者圖片預(yù)處理時(shí)沒有歸一化導(dǎo)致輸入值范圍過(guò)大。解決把lr從 1e-3 降到 1e-4并在transforms.Compose里加transforms.Normalize(mean[0.5,0.5,0.5], std[0.5,0.5,0.5])。如果已經(jīng)加了歸一化還 nan檢查標(biāo)簽里有沒有超出類別數(shù)的索引。6.3 現(xiàn)象驗(yàn)證集準(zhǔn)確率始終等于類別數(shù)的倒數(shù)原因標(biāo)簽生成時(shí)類別索引和訓(xùn)練時(shí)讀取的索引不一致或者驗(yàn)證集圖片路徑寫錯(cuò)導(dǎo)致讀進(jìn)來(lái)全是同一類。解決打印前 10 條 txt 記錄確認(rèn)路徑和標(biāo)簽對(duì)應(yīng)關(guān)系再打印train_loader里一個(gè) batch 的標(biāo)簽分布。6.4 現(xiàn)象PyQt 界面點(diǎn)按鈕沒反應(yīng)原因QFileDialog返回的路徑為空時(shí)沒有做判斷或者模型加載失敗但異常被吞了。解決在load_image里加if not path: return并在模型加載處用 try-except 打印異常。我一般會(huì)在界面啟動(dòng)時(shí)先打印一行「模型加載成功」確認(rèn)。6.5 現(xiàn)象GPU 顯存不足報(bào) CUDA out of memory原因batch size 太大或者圖片輸入尺寸設(shè)得過(guò)高。解決把 batch size 從 32 降到 8 或 16或者把輸入尺寸從 224 降到 128。如果還不行在訓(xùn)練循環(huán)里加torch.cuda.empty_cache()但根本辦法還是減小 batch。7. 進(jìn)階技巧把驗(yàn)證集準(zhǔn)確率從 0.85 推到 0.95 的幾個(gè)實(shí)操手段這套資源跑通之后你大概率會(huì)看到驗(yàn)證集準(zhǔn)確率在 0.85 到 0.9 之間。想再往上推我一般會(huì)從三個(gè)地方下手。第一是數(shù)據(jù)增強(qiáng)再加強(qiáng)資源里已經(jīng)做了旋轉(zhuǎn)和翻轉(zhuǎn)你可以再加隨機(jī)裁剪和顏色抖動(dòng)讓模型見過(guò)更多變化。第二是換優(yōu)化器策略把固定學(xué)習(xí)率換成CosineAnnealingLR讓學(xué)習(xí)率隨 epoch 下降后期收斂更穩(wěn)。第三是保存最佳模型而不是最后一個(gè) epoch 的模型驗(yàn)證集準(zhǔn)確率最高的時(shí)候存一次避免過(guò)擬合后的權(quán)重被用到界面上。下面這段是加余弦退火和最佳模型保存的改動(dòng)直接嵌到訓(xùn)練循環(huán)里就行。from torch.optim.lr_scheduler import CosineAnnealingLR scheduler CosineAnnealingLR(optimizer, T_max30) best_acc 0.0 for epoch in range(30): # ... 訓(xùn)練和驗(yàn)證代碼同上 ... scheduler.step() if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) print(f已保存最佳模型準(zhǔn)確率 {best_acc:.4f})邏輯說(shuō)明CosineAnnealingLR的T_max設(shè)成總 epoch 數(shù)學(xué)習(xí)率會(huì)按余弦曲線從初始值降到接近 0。best_acc記錄歷史最高驗(yàn)證準(zhǔn)確率只有超過(guò)時(shí)才覆蓋保存。參數(shù)上T_max要和實(shí)際訓(xùn)練的 epoch 數(shù)一致設(shè)小了學(xué)習(xí)率提前降到 0設(shè)大了退火效果不明顯。另外如果你發(fā)現(xiàn)某些類別之間總是互相混淆可以把混淆矩陣打出來(lái)看是哪兩類在打架。常見做法是用sklearn.metrics.confusion_matrix把驗(yàn)證集的預(yù)測(cè)結(jié)果和真實(shí)標(biāo)簽傳進(jìn)去一眼就能看出問題類別。針對(duì)混淆的類別可以單獨(dú)多補(bǔ)一些該角度的圖片或者把輸入尺寸從 128 提到 224讓模型看到更多紋理細(xì)節(jié)。從那以后我每次跑這類圖像分類項(xiàng)目都會(huì)先把類別列表存成 json訓(xùn)練和界面共用一份再固定隨機(jī)種子最后一定保存最佳模型而不是最后一個(gè) epoch。這三步做完復(fù)現(xiàn)性會(huì)好很多也不會(huì)出現(xiàn)界面識(shí)別結(jié)果和訓(xùn)練日志對(duì)不上的情況。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取