基于4,200张已标注图像的果蔬分类迁移学习实战指南
发布时间:2026/9/28 16:22:55来源:尧图网络
简介本资源为常见果蔬多类别图像分类数据集面向从事图像分类、分割网络改进及计算机视觉项目实践的学生与开发者可直接作为分类网络输入使用。数据集共标注36个类别涵盖香蕉、苹果、梨、葡萄、橙子、黄瓜、胡萝卜、辣椒、洋葱、土豆等常见果蔬并已划分训练集、测试集与验证集各类别图片分别存放便于直接加载训练与评估。压缩包为7z格式内含约2000个文件以1998张jpg图像为主另附1个py脚本与1个json标注文件整体约364.87MB运行包内show脚本可快速可视化数据分布与样本效果。目前已有119人学习下载适合用于分类模型训练、数据增强实验及网络改进对比帮助读者省去数据采集与清洗环节将精力集中于模型结构与调参优化。1. 果蔬分类数据集怎么选4,200 张已标注图像能撑起什么任务手上有一份约 4,200 张、已经标注好的常见果蔬多类别图像分类数据集第一反应往往不是「赶紧训个模型」而是「这点量够不够、能落地到什么程度」。我见过太多团队拿到几千张图就直接上 ResNet-50 从零训练结果验证集准确率卡在 60% 上下反复横跳最后归因于「数据太少」——其实问题出在没用对迁移学习也没搞清这份数据集的类别分布和场景边界。图像分类这个任务本身不复杂难的是判断一份已标注数据集到底适合做原型验证、课程作业、还是能推到产线做分拣辅助。4,200 张这个量级配合预训练权重做微调覆盖 10 到 30 个常见果蔬类别是完全可行的但如果类别数冲到 50 以上每类平均不到 90 张长尾类别就会明显拖后腿。这篇笔记就围绕这份数据集把类别盘点、划分策略、迁移学习训练、以及部署前的验证方法讲透适合想快速跑通一个多类别图像分类 pipeline 的工程师和做视觉项目落地的同学。下面所有步骤都可以直接抄参数我会给具体数值和调整区间。2. 先摸清数据底细类别分布、图像尺寸与标注格式核对拿到一份「已标注」的数据集最忌讳直接丢进 DataLoader。标注格式可能是按文件夹分好类的 ImageFolder 结构也可能是 CSV 里一列路径一列标签还可能是 COCO 风格的 JSON。不同格式决定了你后面写 Dataset 类的方式先花二十分钟核对能省掉后面几小时的报错排查。2.1 用脚本统计类别数与每类样本量第一步永远是数数。下面这段脚本假设数据是root/类别名/图片的目录结构如果不是改glob那几行即可。import os from collections import Counter from pathlib import Path root Path(./fruits_veggies) # 数据集根目录 exts {.jpg, .jpeg, .png, .bmp, .webp} counter Counter() bad_files [] for cls_dir in sorted(root.iterdir()): if not cls_dir.is_dir(): continue for img in cls_dir.rglob(*): if img.suffix.lower() in exts: counter[cls_dir.name] 1 elif img.is_file(): bad_files.append(str(img)) # 记录非图片文件 total sum(counter.values()) print(f类别数: {len(counter)}, 总图片数: {total}) for cls, n in counter.most_common(): print(f{cls:20s} {n:5d} {n/total*100:5.2f}%) print(异常文件:, bad_files[:10])逻辑说明rglob(*)递归扫描兼容类别目录下还有子目录的情况bad_files用来揪出混进来的.txt、.DS_Store之类。参数上exts集合按你实际数据补充如果数据集里有.tif记得加进去。跑完重点看两个数最大类与最小类的比值以及总类别数。如果最大类是最小类的 5 倍以上后面训练必须做重采样或加权损失否则模型会偏向多数类。2.2 检查图像尺寸与通道一致性果蔬图像常见坑是尺寸和通道不统一有的手机拍的是 4032×3024 的 RGB有的可能是灰度图或带 alpha 通道的 PNG。统一在预处理阶段处理别指望模型自己扛。from PIL import Image import numpy as np sizes, modes Counter(), Counter() sample list(root.rglob(*.jpg))[:200] # 抽样200张足够看分布 for p in sample: with Image.open(p) as im: sizes[im.size] 1 modes[im.mode] 1 print(尺寸分布 top5:, sizes.most_common(5)) print(通道模式:, modes.most_common())如果尺寸分布很散训练时统一Resize(256) CenterCrop(224)或直接Resize((224,224))如果出现L灰度或RGBA模式在 Dataset 里用convert(RGB)强制转三通道。这一步不做后面ToTensor()出来的维度对不上报错信息还特别绕。2.3 标注质量抽检别信「已标注」三个字「已标注」不等于「标对了」。抽 30 到 50 张按类别各抽几张肉眼过一遍。重点看三类问题类别目录名和内容不符比如apple目录里混了梨、同一张图重复出现在多个类别、以及明显模糊或遮挡严重的废图。我一般会写个简单的哈希去重脚本import hashlib from collections import defaultdict hashes defaultdict(list) for p in root.rglob(*): if p.suffix.lower() in exts: h hashlib.md5(p.read_bytes()).hexdigest() hashes[h].append(str(p)) dups {k: v for k, v in hashes.items() if len(v) 1} print(f重复图片组数: {len(dups)}) for k, v in list(dups.items())[:5]: print(v)重复图如果跨了训练集和验证集会导致验证准确率虚高这是最隐蔽的翻车点之一。发现重复就删掉多余的只保留一份。3. 划分训练验证测试集分层抽样与防泄漏4,200 张的规模划分比例我一般用 7:1.5:1.5也就是训练约 2,940 张、验证约 630 张、测试约 630 张。比例不是死的如果某类样本特别少比如只有 40 张可以适当把验证测试压到各 10%保证训练集每类至少 30 张以上。3.1 用分层抽样保证每类比例一致随机划分在小数据集上很容易出现某类全进了训练集、验证集里一张没有的情况。用sklearn的train_test_split带stratify参数最稳。import shutil from sklearn.model_selection import train_test_split all_paths, all_labels [], [] for cls_dir in sorted(root.iterdir()): if cls_dir.is_dir(): for img in cls_dir.rglob(*): if img.suffix.lower() in exts: all_paths.append(str(img)) all_labels.append(cls_dir.name) # 先切出测试集 X_train_val, X_test, y_train_val, y_test train_test_split( all_paths, all_labels, test_size0.15, stratifyall_labels, random_state42) # 再从剩余切验证集 X_train, X_val, y_train, y_val train_test_split( X_train_val, y_train_val, test_size0.176, stratifyy_train_val, random_state42) # 0.176 ≈ 0.15/0.85保证验证集占总量的15% print(len(X_train), len(X_val), len(X_test))参数说明random_state固定住保证每次划分一致方便复现stratify传入标签列表让每类在三个子集里的占比接近原始分布。切完后建议再跑一次类别统计确认没有哪类在验证集里为 0。3.2 按目录结构落盘方便后续直接 ImageFolder 读取划分完把文件实际拷贝到train/val/test三个目录下每个目录里再按类别建子目录。这样后面用torchvision.datasets.ImageFolder一行就能读不用自己写 Dataset。def dump(split_name, paths, labels): for p, lb in zip(paths, labels): dst_dir Path(f./split/{split_name}/{lb}) dst_dir.mkdir(parentsTrue, exist_okTrue) shutil.copy2(p, dst_dir / Path(p).name) dump(train, X_train, y_train) dump(val, X_val, y_val) dump(test, X_test, y_test)用copy2保留元数据别用move原始数据留着当后悔药。落盘后目录结构就是标准的 ImageFolder 格式后面训练脚本直接指向./split/train即可。提示如果磁盘紧张也可以不落盘在 Dataset 里维护一个path-label的字典按索引取但落盘的好处是可以用现成工具快速可视化检查调试期更省心。4. 迁移学习训练从预训练权重到可复现的 baseline4,200 张图从零训练一个 CNN 基本没戏迁移学习是唯一务实的选择。核心思路是拿 ImageNet 上预训练好的骨干网络替换最后的全连接层为你的类别数然后分阶段微调。下面给一套我常用的 PyTorch 训练脚本骨架参数都是经过验证的起点值。4.1 数据增强与 DataLoader 配置果蔬图像的增强要克制颜色抖动别开太大否则青椒和黄瓜的颜色特征会被破坏。水平翻转、小角度旋转、轻微颜色抖动足够。import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), 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, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(./split/train, transformtrain_tf) val_ds datasets.ImageFolder(./split/val, transformval_tf) test_ds datasets.ImageFolder(./split/test, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4) test_loader DataLoader(test_ds, batch_size32, shuffleFalse, num_workers4) print(类别映射:, train_ds.class_to_idx)参数说明RandomResizedCrop的scale(0.7,1.0)表示随机裁原图 70% 到 100% 的区域再缩放比固定 Resize 更能提升泛化Normalize用的是 ImageNet 统计量只要骨干是在 ImageNet 上预训练的就必须保持一致换成自己的均值方差反而会掉点。batch_size32在 8GB 显存上跑 224 输入基本安全显存小就降到 16 并同步调小学习率。4.2 模型构建与分阶段微调策略骨干选 ResNet-18 还是 ResNet-50取决于你的算力和精度要求。4,200 张这个量级ResNet-18 通常就够ResNet-50 提升有限但训练慢一倍。下面用timm或torchvision都行这里用 torchvision 演示。import torch.nn as nn from torchvision import models num_classes len(train_ds.classes) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) # 第一阶段冻结骨干只训分类头 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max10)逻辑说明先冻结骨干只训fc层让随机初始化的分类头快速收敛到一个合理区域避免一开始就把预训练权重带偏。跑 5 个 epoch 后解冻全部参数学习率降到 1e-4 再训 15 到 20 个 epoch。这个两阶段策略在小数据集上比直接全量微调稳定得多血泪经验。4.3 训练循环与关键监控指标训练循环本身不复杂关键是记录对的东西。除了 loss 和 accuracy我强烈建议记录每类的验证准确率否则你只知道总体 85%不知道是哪类在拖后腿。def run_epoch(model, loader, criterion, optimizerNone): is_train optimizer is not None model.train() if is_train else model.eval() total_loss, correct, total 0.0, 0, 0 ctx torch.enable_grad() if is_train else torch.no_grad() with ctx: for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) loss criterion(outputs, labels) if is_train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total for epoch in range(20): if epoch 5: # 解冻全部参数 for param in model.parameters(): param.requires_grad True optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max15) tr_loss, tr_acc run_epoch(model, train_loader, criterion, optimizer) va_loss, va_acc run_epoch(model, val_loader, criterion) scheduler.step() print(fEpoch {epoch:02d} | train {tr_loss:.4f}/{tr_acc:.4f} | val {va_loss:.4f}/{va_acc:.4f})参数说明T_max是余弦退火的周期设成剩余 epoch 数即可weight_decay1e-4对 4,200 张的数据集是合适的正则强度太大欠拟合、太小过拟合。如果验证 loss 连续 5 个 epoch 不降就早停别硬训。5. 避坑与排查果蔬分类训练里最容易翻车的 5 个点这一章全是踩过的坑每条按现象、原因、解决写照着排查能省大量时间。5.1 验证准确率远高于测试准确率现象验证集 92%测试集只有 70%。原因划分时同一张图或同一批次拍摄的相似图同时进了验证和测试数据泄漏。解决回到 2.3 的去重脚本对全量数据做哈希去重如果同一果蔬的连拍图很多考虑按拍摄批次分组划分而不是随机划分。5.2 训练 loss 不降准确率卡在随机水平现象loss 在 2.3 附近震荡假设 10 类随机 loss≈2.3准确率 10% 左右。原因最常见是标签和图像没对齐ImageFolder 按目录名排序生成类别索引如果你手动改过目录名或混入了非图片文件映射就乱了。解决打印train_ds.class_to_idx和几张图的标签肉眼核对另外检查Normalize是否和预训练权重匹配。5.3 某几个类别准确率始终为 0现象总体 80%但cucumber和zucchini两类几乎全错。原因这两类外观高度相似模型区分不开或者这两类样本量太少被多数类压制。解决先看混淆矩阵确认是不是互相混淆是的话考虑合并类别或引入更强的骨干样本少的话用WeightedRandomSampler做重采样给少数类更高采样权重。5.4 显存溢出CUDA out of memory现象训练几个 batch 后报 OOM。原因batch_size太大、图像分辨率太高、或者num_workers开太多导致内存泄漏。解决先把batch_size减半同时把学习率按比例调小num_workers在 Windows 上设 0 或 2Linux 上 4 到 8 即可如果还不行用torch.cuda.empty_cache()在 epoch 之间清理。5.5 推理时单张图预测结果和验证集不一致现象验证集上这张图预测对了单独喂进去却错了。原因推理时的预处理和验证时不一致比如忘了CenterCrop、忘了Normalize、或者用了Resize但没保持长宽比。解决把验证集的 transform 单独抽成一个函数推理时复用同一个函数别手写一套新的。6. 进阶技巧用混淆矩阵和 TTA 把 4,200 张的潜力榨干baseline 跑通之后想再往上提几个点最划算的两件事是分析混淆矩阵和测试时增强TTA。这两个都不需要额外标注数据纯靠现有 4,200 张就能做。先看混淆矩阵。用sklearn的confusion_matrix在测试集上跑一遍把结果画成热力图这里不贴绘图代码重点看数值。你会清楚看到哪些类互相混淆。我做过的一个果蔬数据集里green_apple和pear混淆率高达 30%原因是两者在强光下颜色接近。针对这种情况与其加数据不如在增强里加一点色调偏移让模型学会忽略绝对颜色、关注形状。from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) preds model(imgs).argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namestest_ds.classes, digits3))classification_report会给出每类的 precision、recall、f1比总体准确率有用得多。如果某类 recall 特别低说明漏检严重优先补这类样本或调权重。再说 TTA。思路是推理时对同一张图做多个变换原图、水平翻转、不同裁剪把多次预测的概率平均。通常能提 1 到 3 个点代价是推理时间翻几倍。对 4,200 张这个量级的数据集TTA 是性价比很高的收尾手段。def predict_tta(model, img_tensor): model.eval() probs [] with torch.no_grad(): # 原图 probs.append(torch.softmax(model(img_tensor.unsqueeze(0).to(device)), 1)) # 水平翻转 flipped torch.flip(img_tensor, dims[2]).unsqueeze(0).to(device) probs.append(torch.softmax(model(flipped), 1)) # 中心裁剪放大 _, h, w img_tensor.shape crop img_tensor[:, h//8:h*7//8, w//8:w*7//8] crop torch.nn.functional.interpolate( crop.unsqueeze(0), size(224, 224), modebilinear, align_cornersFalse).to(device) probs.append(torch.softmax(model(crop), 1)) return torch.stack(probs).mean(0).argmax(1).item()参数说明翻转维度dims[2]对应宽度轴别搞错裁剪比例 1/8 到 7/8 是经验值裁太狠会丢主体。TTA 只在最终评估或部署时用训练时不用。最后说个习惯每次改完增强或超参固定用同一个random_state重跑把验证准确率和混淆矩阵存下来对比。我吃过亏有一次调了半天以为提升了结果发现是划分变了导致的虚高。4,200 张的数据集不大但把类别盘点、防泄漏划分、分阶段微调、混淆矩阵分析这几步做扎实跑出一个能用的多类别果蔬分类模型完全没问题。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网