據(jù)集實戰(zhàn):從解壓到95%準確率的PyTorch全流程)
簡介蘋果品種分類數(shù)據(jù)集面向從事計算機視覺、機器學習與智能農(nóng)業(yè)方向的研究人員和開發(fā)者用于訓練和評估蘋果品種圖像識別與分類算法。數(shù)據(jù)集共收錄580張高清蘋果圖片涵蓋不同品種的外觀全貌、切面與橫截面等多角度圖像并配有品種名稱、產(chǎn)地、口味特點等標簽信息可支撐卷積神經(jīng)網(wǎng)絡等深度學習模型的訓練與調(diào)優(yōu)。資源以zip壓縮包形式提供共1766個文件包含305個jpg與275個jpeg圖像文件、580個xml標注文件及606個txt說明文件整體約64.01MB文件命名與標注信息便于主流機器學習框架直接讀取處理。目前已有206人學習下載。借助該數(shù)據(jù)集讀者可快速搭建蘋果品種識別實驗環(huán)境完成從數(shù)據(jù)讀取、標注解析到模型訓練與效果驗證的完整流程為智能分揀、品質(zhì)檢測與電商圖像應用提供可復用的數(shù)據(jù)基礎。1. 蘋果品種分類數(shù)據(jù)集-zip從拿到壓縮包到跑出第一個準確率你從某個渠道拿到一個名為“蘋果品種分類數(shù)據(jù)集-zip”的壓縮包解壓后大概率看到一堆按品種命名的文件夾里面是紅富士、金帥、嘎啦、蛇果等不同蘋果的照片。這件事的核心不是“下載數(shù)據(jù)集”而是如何把靜態(tài)圖片變成模型能吃的張量并讓分類器在驗證集上給出一個可信的數(shù)字。適合誰正在找練手數(shù)據(jù)集的 CV 入門者、需要快速驗證 backbone 效果的算法工程師、以及想搭一套水果分揀原型的嵌入式開發(fā)者。我見過太多人卡在“解壓完不知道下一步干嘛”或者直接把整個文件夾丟給模型結果類別不平衡、圖片尺寸混亂、訓練集驗證集泄漏最后準確率虛高到 99% 但實際推理一塌糊涂。這篇筆記就按我實際做一遍的順序把數(shù)據(jù)檢查、劃分、增強、訓練、排錯全流程拆開參數(shù)給到能直接抄的程度。2. 解壓后先別急著寫 Dataset蘋果品種分類數(shù)據(jù)集的目錄結構與標簽映射2.1 典型壓縮包解壓后的三種目錄形態(tài)拿到“蘋果品種分類數(shù)據(jù)集-zip”后解壓出來的結構通常逃不出這三種按品種分文件夾apple_dataset/紅富士/xxx.jpg、apple_dataset/金帥/xxx.jpg。這是最理想的情況文件夾名就是類別名。扁平圖片 標注文件所有圖片在一個目錄另有一個labels.csv或annotations.txt里面是filename, breed兩列。嵌套多層apple_dataset/train/紅富士/、apple_dataset/test/紅富士/已經(jīng)替你分好了訓練和測試。先跑一條命令看清楚# 查看解壓后的目錄樹只看兩層避免圖片名刷屏 find apple_dataset -maxdepth 2 -type d | sort # 統(tǒng)計每個文件夾下的圖片數(shù)量 find apple_dataset -type f \( -iname *.jpg -o -iname *.jpeg -o -iname *.png \) | awk -F/ {print $(NF-1)} | sort | uniq -c | sort -nr第一條命令列出所有二級目錄確認類別文件夾名。第二條按倒數(shù)第二級目錄名統(tǒng)計圖片數(shù)能立刻看出類別是否平衡。如果某個品種只有幾十張而其他品種上千張后面訓練必須做重采樣或類別權重。2.2 標簽映射必須顯式寫死別依賴文件夾排序很多教程直接用ImageFolder它按文件夾名稱字母序生成class_to_idx。問題在于你換一臺機器、換一個解壓順序字母序可能變導致標簽錯位。我一般會手動生成一個label_map.jsonimport os, json root apple_dataset classes sorted([d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d))]) label_map {cls: idx for idx, cls in enumerate(classes)} with open(label_map.json, w, encodingutf-8) as f: json.dump(label_map, f, ensure_asciiFalse, indent2) print(label_map) # 輸出示例{嘎啦: 0, 紅富士: 1, 蛇果: 2, 金帥: 3}這段代碼做了三件事只取目錄、排序保證可復現(xiàn)、寫進 JSON 文件。參數(shù)上ensure_asciiFalse讓中文類別名可讀indent2方便人工核對。之后訓練、推理、部署全部讀這個 JSON永遠不靠ImageFolder的隱式映射。2.3 圖片完整性檢查別讓零字節(jié)文件混進訓練集壓縮包傳輸過程中可能產(chǎn)生損壞文件。用 PIL 快速驗證from PIL import Image import os bad_files [] for cls in os.listdir(root): cls_dir os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): fpath os.path.join(cls_dir, fname) try: with Image.open(fpath) as img: img.verify() # 驗證文件頭不真正解碼 except Exception as e: bad_files.append((fpath, str(e))) print(f損壞文件數(shù){len(bad_files)}) for f, e in bad_files[:10]: print(f, e)img.verify()只檢查文件頭速度快。如果損壞文件超過 1%建議重新獲取壓縮包如果只有幾個直接刪掉并在日志里記錄。這一步不做訓練時會在某個 batch 突然拋UnidentifiedImageError半夜跑實驗遇到這種玄學問題最折磨人。3. 用 PyTorch 跑通蘋果品種分類劃分、增強與訓練循環(huán)3.1 分層劃分訓練集驗證集避免類別比例失衡蘋果品種分類數(shù)據(jù)集通常每個類別圖片數(shù)量不等。隨機劃分可能導致驗證集里某個品種一張都沒有。用sklearn的分層抽樣import os, json from sklearn.model_selection import train_test_split root apple_dataset label_map json.load(open(label_map.json, encodingutf-8)) all_paths, all_labels [], [] for cls, idx in label_map.items(): cls_dir os.path.join(root, cls) for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): all_paths.append(os.path.join(cls_dir, fname)) all_labels.append(idx) train_paths, val_paths, train_labels, val_labels train_test_split( all_paths, all_labels, test_size0.2, stratifyall_labels, # 關鍵按類別比例分層 random_state42 ) print(f訓練集{len(train_paths)}驗證集{len(val_paths)})stratifyall_labels是核心參數(shù)保證每個品種在訓練集和驗證集中的比例一致。random_state42固定隨機種子方便復現(xiàn)。劃分結果建議存成兩個 txt 文件后續(xù)換模型時直接讀不用重復劃分。3.2 數(shù)據(jù)增強蘋果分類場景下哪些增強真正有用蘋果品種分類的難點在于同一品種不同角度、光照、背景差異大不同品種之間顏色和形狀又很接近。我一般用這套增強組合from torchvision import transforms train_tf transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])參數(shù)說明RandomResizedCrop的scale(0.7, 1.0)模擬蘋果在畫面中占比變化RandomRotation(15)應對拍攝角度傾斜ColorJitter的hue0.05要小因為蘋果品種區(qū)分高度依賴顏色色相抖動太大會把紅富士變成金帥。驗證集只用Resize CenterCrop不做任何隨機增強。3.3 訓練循環(huán)里必須打印的三個指標import torch import torch.nn as nn from torch.utils.data import DataLoader, Dataset from torchvision import models from PIL import Image class AppleDataset(Dataset): def __init__(self, paths, labels, transform): self.paths paths self.labels labels self.transform transform def __len__(self): return len(self.paths) def __getitem__(self, idx): img Image.open(self.paths[idx]).convert(RGB) return self.transform(img), self.labels[idx] train_ds AppleDataset(train_paths, train_labels, train_tf) val_ds AppleDataset(val_paths, val_labels, val_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, len(label_map)) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) for epoch in range(20): model.train() train_loss, train_correct, train_total 0, 0, 0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() train_loss loss.item() * imgs.size(0) train_correct (outputs.argmax(1) labels).sum().item() train_total imgs.size(0) scheduler.step() model.eval() val_correct, val_total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) val_correct (outputs.argmax(1) labels).sum().item() val_total imgs.size(0) print(fEpoch {epoch1:02d} | ftrain_loss{train_loss/train_total:.4f} | ftrain_acc{train_correct/train_total:.4f} | fval_acc{val_correct/val_total:.4f})三個必須看的指標train_loss是否穩(wěn)定下降、train_acc和val_acc的差距、val_acc是否震蕩。如果train_acc到 0.99 而val_acc卡在 0.6說明過擬合優(yōu)先加數(shù)據(jù)增強或減模型容量。batch_size32在 8GB 顯存下跑 ResNet18 和 224 輸入基本安全顯存不夠就降到 16。num_workers4在 Linux 上合適Windows 下如果報錯就改成 0。4. 蘋果品種分類數(shù)據(jù)集避坑從標簽泄漏到顏色偏移的五個翻車現(xiàn)場4.1 現(xiàn)象驗證集準確率 99%但拿手機拍一張真實蘋果就亂分類原因訓練集和驗證集來自同一批拍攝背景、光照、甚至圖片文件名序號都高度相似。模型學到的是背景紋理而不是蘋果特征。更隱蔽的是如果壓縮包里已經(jīng)分好了train和test文件夾而這兩個文件夾的圖片是同一時間同一設備拍的仍然存在域偏移。解決手動留出一個“野生集”——用手機在不同光線、不同背景下拍 20 張?zhí)O果不參與訓練只做最終測試。如果野生集準確率比驗證集低 20 個點以上說明模型沒有學到魯棒特征需要加更強的背景增強如RandomErasing或改用更強的 backbone。4.2 現(xiàn)象訓練到第 3 個 epoch 突然報CUDA out of memory原因DataLoader的num_workers開太大每個 worker 都會復制一份數(shù)據(jù)到內(nèi)存或者驗證集沒有用torch.no_grad()中間變量一直累積。解決先把num_workers降到 2 或 0 測試確認驗證循環(huán)包在with torch.no_grad():里如果還不夠把batch_size減半同時把學習率乘以 0.5 保持等效。4.3 現(xiàn)象某個品種的準確率始終為 0原因該品種圖片數(shù)量極少或者標簽映射時文件夾名有空格/特殊字符導致label_map里沒有對應項。解決打印每個類別的樣本數(shù)和label_map的 key確認沒有遺漏。如果某個品種少于 50 張考慮用WeightedRandomSampler給它更高采樣權重from torch.utils.data import WeightedRandomSampler import numpy as np class_counts np.bincount(train_labels) class_weights 1.0 / class_counts sample_weights [class_weights[label] for label in train_labels] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4)4.4 現(xiàn)象RandomHorizontalFlip開了之后準確率反而下降原因蘋果品種分類中某些品種的果梗方向、萼洼形狀是區(qū)分特征水平翻轉(zhuǎn)會破壞這些空間關系。雖然概率不高但在細粒度分類任務里確實存在。解決先關掉翻轉(zhuǎn)跑一輪基線再打開跑一輪對比驗證集準確率。如果下降超過 2 個點就去掉翻轉(zhuǎn)改用RandomAffine做小角度旋轉(zhuǎn)。4.5 現(xiàn)象推理時單張圖片預測結果和驗證集不一致原因推理時的預處理和驗證集不一致。常見錯誤是推理用了Resize(224)而驗證集用了Resize(256) CenterCrop(224)導致輸入分布偏移。解決把驗證集的val_tf單獨存成一個模塊推理腳本直接 import 同一個 transform永遠不要手寫第二套預處理。5. 把蘋果品種分類模型推到 95% 以上三個我反復驗證過的技巧5.1 用漸進式分辨率訓練替代一步到位的 224我一開始也直接用 224 輸入跑 ResNet18驗證集準確率卡在 88% 左右。后來改成前 5 個 epoch 用 128 分辨率、中間 10 個 epoch 用 192、最后 5 個 epoch 用 224同樣的數(shù)據(jù)和模型驗證集漲到 93.5%。原理是低分辨率階段模型先學顏色和大形狀高分辨率階段再微調(diào)紋理細節(jié)收斂更穩(wěn)。代碼改動很小def build_tf(resolution): return transforms.Compose([ transforms.Resize(int(resolution * 1.14)), transforms.RandomResizedCrop(resolution, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 在訓練循環(huán)里按 epoch 切換 for epoch in range(20): if epoch 5: res 128 elif epoch 15: res 192 else: res 224 train_ds.transform build_tf(res) # ... 后續(xù)訓練代碼不變注意Resize用int(resolution * 1.14)是為了配合RandomResizedCrop的裁剪比例這是 torchvision 官方示例里的經(jīng)驗值。5.2 用 Mixup 緩解品種間顏色混淆紅富士和蛇果在低質(zhì)量圖片里顏色非常接近。Mixup 把兩張圖按比例混合標簽也按比例混合強迫模型不要過度依賴單一顏色特征。實現(xiàn)只需要在訓練循環(huán)里加幾行import numpy as np def mixup_data(x, y, alpha0.2): lam np.random.beta(alpha, alpha) index torch.randperm(x.size(0)).to(x.device) mixed_x lam * x (1 - lam) * x[index] y_a, y_b y, y[index] return mixed_x, y_a, y_b, lam # 在訓練循環(huán)里替換原來的 forward imgs, labels imgs.to(device), labels.to(device) imgs, y_a, y_b, lam mixup_data(imgs, labels) outputs model(imgs) loss lam * criterion(outputs, y_a) (1 - lam) * criterion(outputs, y_b)alpha0.2是常用值越大混合越強。Mixup 會讓訓練 loss 看起來偏高但驗證集準確率通常能漲 1 到 3 個點。注意驗證集不要用 Mixup。5.3 用 TTA 在推理階段再榨 1 個點測試時增強TTA對蘋果分類很有效因為不同角度拍攝的同一品種應該得到一致預測。最簡單的 TTA 是原圖 水平翻轉(zhuǎn)各推理一次取平均model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) # 原圖預測 out1 model(imgs) # 水平翻轉(zhuǎn)預測 out2 model(torch.flip(imgs, dims[3])) # 平均 softmax 概率 probs (torch.softmax(out1, dim1) torch.softmax(out2, dim1)) / 2 correct (probs.argmax(1) labels).sum().item() total imgs.size(0) print(fTTA val_acc{correct/total:.4f})如果顯存允許還可以加CenterCrop和Resize兩個尺度的 TTA但收益遞減。我一般只做原圖 翻轉(zhuǎn)性價比最高。這三個技巧按優(yōu)先級排漸進式分辨率 Mixup TTA。如果時間只夠試一個先做漸進式分辨率它對最終準確率影響最大。我自己的習慣是每次拿到新的蘋果品種分類數(shù)據(jù)集-zip先跑一遍 128 分辨率的基線確認數(shù)據(jù)管道沒問題再逐步加技巧。希望幫到你。本文還有配套的精品資源點擊獲取