news 2026/9/28 16:22:53

基于4,200张已标注图像的果蔬分类迁移学习实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于4,200张已标注图像的果蔬分类迁移学习实战指南

简介:本资源为常见果蔬多类别图像分类数据集,面向从事图像分类、分割网络改进及计算机视觉项目实践的学生与开发者,可直接作为分类网络输入使用。数据集共标注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_size=0.15, stratify=all_labels, random_state=42) # 再从剩余切验证集 X_train, X_val, y_train, y_val = train_test_split( X_train_val, y_train_val, test_size=0.176, stratify=y_train_val, random_state=42) # 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(parents=True, exist_ok=True) 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. 迁移学习训练:从预训练权重到可复现的 baseline

4,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(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.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", transform=train_tf) val_ds = datasets.ImageFolder("./split/val", transform=val_tf) test_ds = datasets.ImageFolder("./split/test", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) test_loader = DataLoader(test_ds, batch_size=32, shuffle=False, num_workers=4) print("类别映射:", train_ds.class_to_idx)

参数说明:RandomResizedCrop的scale=(0.7,1.0)表示随机裁原图 70% 到 100% 的区域再缩放,比固定 Resize 更能提升泛化;Normalize用的是 ImageNet 统计量,只要骨干是在 ImageNet 上预训练的就必须保持一致,换成自己的均值方差反而会掉点。batch_size=32在 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(weights=models.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()), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)

逻辑说明:先冻结骨干只训fc层,让随机初始化的分类头快速收敛到一个合理区域,避免一开始就把预训练权重带偏。跑 5 个 epoch 后解冻全部参数,学习率降到 1e-4 再训 15 到 20 个 epoch。这个两阶段策略在小数据集上比直接全量微调稳定得多,血泪经验。

4.3 训练循环与关键监控指标

训练循环本身不复杂,关键是记录对的东西。除了 loss 和 accuracy,我强烈建议记录每类的验证准确率,否则你只知道总体 85%,不知道是哪类在拖后腿。

def run_epoch(model, loader, criterion, optimizer=None): 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(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=15) tr_loss, tr_acc = run_epoch(model, train_loader, criterion, optimizer) va_loss, va_acc = run_epoch(model, val_loader, criterion) scheduler.step() print(f"Epoch {epoch:02d} | train {tr_loss:.4f}/{tr_acc:.4f} | val {va_loss:.4f}/{va_acc:.4f}")

参数说明:T_max是余弦退火的周期,设成剩余 epoch 数即可;weight_decay=1e-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 或 2,Linux 上 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_names=test_ds.classes, digits=3))

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), mode="bilinear", align_corners=False).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 张的数据集不大,但把类别盘点、防泄漏划分、分阶段微调、混淆矩阵分析这几步做扎实,跑出一个能用的多类别果蔬分类模型完全没问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 16:22:14

微信小程序+Java后端马拉松报名系统:高并发抢名额与毕业设计实战

简介:这是一套面向高校计算机相关专业学生的毕业设计/课程设计完整项目,采用微信小程序前端搭配Java后端与MySQL数据库,实现马拉松赛事报名与活动商城一体化业务。系统区分管理员与普通用户两类角色:管理员可管理个人中心、用户、…

作者头像 李华
网站建设 2026/9/28 16:21:31

基于YOLOv5的步态识别多目标跨镜头跟踪系统实战

简介:这份资源是面向人工智能、计算机视觉方向本科生与研究者的毕业设计完整源码包,围绕「基于步态识别的多目标跨镜头跟踪算法研究」展开,核心采用YOLOv5-DeepSORT框架完成目标检测与多目标跟踪,并融合GaitSet步态识别算法实现跨…

作者头像 李华
网站建设 2026/9/28 16:21:17

Superpowers:让AI编码代理从“会写代码”到“会干活”的技能包

我做了三年多的 AI 辅助编程,工具换了一茬又一茬,从 Copilot 到 Cursor 再到 Codex CLI,说实话都挺好用,但总有一种“差口气”的感觉——代理能写代码、能跑测试,可一旦涉及“先想清楚再动手”的环节,它就容…

作者头像 李华
网站建设 2026/9/28 16:20:58

Jetson Nano网线直连笔记本共享网络配置指南

1. 为什么Jetson Nano的联网问题值得单独拿出来说Jetson Nano这块板子,玩过的人都知道,算力在入门级边缘AI设备里算是相当能打的,但它的无线联网体验实在让人头疼。我手上这块4GB版本,板载没有Wi-Fi模块,只有一个千兆以…

作者头像 李华
网站建设 2026/9/28 16:20:35

Agent-Native架构设计实战:从原理到落地

1. 从“AI增强”到“AI原生”的架构转向1.1 agent-native到底是什么给老系统塞一个聊天窗口、把一个LLM API接到客服页面上、在报表工具里加个“智能生成”按钮——这些事情我过去两年干过不少,也见过身边团队干了不少。它们有几个共同的特点:AI只是附件…

作者头像 李华
网站建设 2026/9/28 16:19:58

基于Python+CNN的在线课堂考勤系统:人脸识别与注意力检测实战

简介:这份资源是一套基于Python的在线课堂考勤系统完整项目源码,面向具备一定Python基础、希望将CNN与计算机视觉落地到教育场景的开发者与学习者。项目以卷积神经网络为核心,结合OpenCV与深度学习框架完成人脸特征提取与识别,并配…

作者头像 李华