
简介面向图像分类学习者和算法研究人员的小麦种子图像标注数据集包含约2000张已经过预处理的图像可直接作为分类网络输入。数据共4个类别具体类别配置见json文件使用前可先查看以确定网络输出维度。资源已划分训练集与测试集同类别图片存放在对应目录结构清晰便于写脚本或直接套用常见数据加载器。压缩包共2000个文件以1998张jpg图像为主体另含1个Python可视化脚本和1个json配置文件整体大小225.64MB。目前已有221人学习下载配套的show脚本可抽样展示各类别图像方便检查样本分布与标注是否一致。省去数据清洗和格式转换环节可专注于模型结构设计与调参适合图像分类课程设计、毕业设计及算法验证等场景。1. 小麦种子图像分类数据集2,000 张已标注图像的正确用法一个 2,000 张的已标注图像数据集放在今天的视觉任务里不算大但它恰好卡在“能跑通全流程”和“不足以硬训大模型”的中间地带。标题里的“小麦种子图像分类”常见形态是每张图像包含一粒或一组麦粒标签按品种、品质等级饱满、干瘪、破损、霉变或产地划分落到任务上是标准的图像分类。下面这套流程要解决的问题很直接拿到这样一份已标注数据后从目录整理、数据加载、迁移学习训练到验证模型注意力整个分类链路该怎么做。适合正在做农业智能化、作物表型分析或想找一份不烧显卡的真实标签数据来练手图像分类算法的工程师换成其他小样本植物图像同样适用。2. 小麦种子图像分类数据的目录整理与训练集划分拿到标题所述的“已标注小麦种子图像分类数据集”第一件事不是直接上模型。2,000 张图看起来不多但标注一致性和类别分布的坑会在训练后被放大成几十个百分点的精度损失。常见做法是先做数据检查、目录整理和分层切分把文件级问题处理干净再谈训练。2.1 先确认标签对齐方式再决定用分类还是检测标题只写了“已标注”没写标签是整图级还是对象级每粒麦子的框或掩膜这是第一个要确认的细节直接决定后续走图像分类 pipeline 还是目标检测 pipeline。整图单标签是这类小数据集最常见的形式每张图像一个类别图像里可以有多粒种子但类别以主体为准这种数据用 torchvision 的 ImageFolder 就能组织起来。另一种是单粒裁剪每粒麦子被截成小图再打标签本质仍是图像分类但要求裁剪边界准确。如果标签里带 bbox 或掩膜任务就变成“先用目标检测或分割模型把单粒麦子切出来再对每粒做图像分类”这种方案更接近生产中自动考种系统的做法但数据体量对检测模型来说往往不够2,000 张图训练 YOLO 那类模型会非常吃力。我一般拿到数据先跑一个统计脚本遍历所有标签文件列出类别名、样本数和图像分辨率分布再抽查几十张图确认标签边界是否清晰、有没有多标签混叠。这个步骤花不了十分钟却能提前暴露类别数量不均衡和标签噪声两个最麻烦的问题。另外要确认图像里是否带比例尺、批次号水印这类信息会变成模型的泄漏特征后面用 Grad-CAM 验证时很容易暴露。2.2 用脚本把 2,000 张小麦种子图像重排为 ImageFolder 结构无论原始标注是 CSV、JSON 还是按文件夹分类我一般先统一转成 torchvision 的 ImageFolder 结构这样可以直接用datasets.ImageFolder生成 Dataset省掉自己写 Dataset 类的过程。下面是转换脚本# organize_wheat.py import pandas as pd import shutil from pathlib import Path src Path(wheat_v1/images) # 原始图像目录 label_file Path(wheat_v1/labels.csv) # 标注文件 out Path(wheat_split) # 输出目录 df pd.read_csv(label_file) # 至少包含 image_id, label 两列 for row in df.itertuples(): # 按 label 建子目录ImageFolder 把子目录名当作类别名 target_dir out / str(row.label) / row.image_id target_dir.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy(src / row.image_id, target_dir)这段代码的逻辑是读取标注表里的每一条记录把对应图像复制到以类别名命名的子目录下。处理之后datasets.ImageFolder(wheat_split)就能按照子目录名自动生成标签。参数上唯一要留意的是image_id必须和实际文件名完全一致标注文件里常见的前缀不一致、大小写不一致都会在 copy 阶段报错如果原图混有 jpg、png、jpeg 多后缀建议先把image_id统一成含后缀的完整文件名否则shutil.copy找不到源文件。2.3 切分训练/验证/测试集时避免三个偏差2,000 张图不多切分方式直接影响模型评估的可信度。最常见的偏差有三个一是直接 random shuffle如果原始数据按拍摄时间分批次排列同一批次的图像光照、背景几乎一样随机切分会把近似重复的图像同时放进训练集和测试集导致准确率虚高二是类别不均衡时不做分层抽样小类在测试集里可能只有个位数评估指标波动非常大三是在划分前就计算归一化的均值和标准差造成信息泄漏。我的做法是用分层抽样保证每个类别的占比在各集合中一致同时先划分数据、再统计训练集的归一化参数。切分方式适用场景风险纯随机切分数据量大且各批次独立小类样本可能集中在一侧分层随机切分类别不均衡、样本量少需注意连续帧重复按拍摄批次切分评估泛化能力分割比例难控制上表是按数据采集方式选择切分策略的参考。对 2,000 张这样的小数据集我倾向按 7:2:1 切分训练约 1,400 张、验证约 400 张、测试约 200 张。测试集里每个类别至少要有 20 张以上否则准确率的置信区间会宽到十几个百分点没法判断模型真的变好了还是评估噪声在起作用。2.4 尺寸与归一化统一输入避免让网络学到“拍摄规格”小麦种子图像如果来自不同拍摄设备分辨率和长宽比会不一致。直接粗暴 resize 到统一尺寸会把长麦粒压成椭圆所以我的做法是先按短边做中心裁剪再缩放尽量保留长宽比信息。同步做的是统计训练集 RGB 三通道的均值和标准差用数据自己的统计值做标准化而不是盲用 ImageNet 的默认值因为显微拍摄的图像色彩分布和自然图像差别很大。统计脚本写法如下# compute_mean_std.py import torch from torchvision import datasets, transforms as T from torch.utils.data import DataLoader ds datasets.ImageFolder(wheat_split/train, transformT.ToTensor()) loader DataLoader(ds, batch_size64, shuffleFalse, num_workers4) mean torch.zeros(3) std torch.zeros(3) total 0 for images, _ in loader: b, c, h, w images.shape images images.view(b, c, -1) mean images.mean(dim(0, 2)) * b # 按批样本数加权 std images.std(dim(0, 2)) * b total b mean / total std / total print(fmean: {mean.tolist()}, std: {std.tolist()})这段代码做了批级统计的加权平均对 2,000 张图足够逼近全局统计。需要说明的是这里的std是各 batch 标准差的加权近似不是严格的全局标准差但对训练结果影响很小。拿到这两个三元组后填进后面训练脚本的Normalize里即可。3. 用 ResNet18 在小麦种子图像上跑通迁移学习分类数据整理完之后接下来要做的就是把图像分类模型跑起来。2,000 张小麦种子图从零训练深层 CNN 基本必过拟合常见做法是加载 ImageNet 预训练权重做迁移学习把最后分类头替换成自己的类别数。3.1 为什么选择 ResNet18 而不是更大的模型2,000 张小麦种子图像训练一个全新的深度 CNN网络只要一深就会在验证集上崩掉。ImageNet 预训练模型的底层已经学会了边缘、纹理、颜色渐变这些通用特征小麦种子的核心区分信息恰好也是纹理和局部形态底层特征可以直接复用。在 ResNet18、ResNet50 和 EfficientNet 之间我一般先选 ResNet18参数量约 1,100 万普通 GPU 或高性能 CPU 都能跑过拟合风险低torchvision 里直接能加载官方预训练权重。等 ResNet18 在验证集上稳定了再换 ResNet50 或 Swin-T 做对比实验。Transformer 架构的 ViT 在这个样本规模下很难直接和预训练 CNN 竞争除非配很强数据增强和更长训练轮数所以初期以 CNN 为主这类数据集上效率更高。3.2 完整训练脚本加载预训练权重、替换分类头、训练与验证下面是一段能直接跑通的最小训练脚本代码逻辑分六步构建变换、读取数据集、加载预训练模型、冻结骨干、训练分类头、保存模型。# train_wheat.py import torch import torch.nn as nn from torchvision import datasets, transforms as T, models from torch.utils.data import DataLoader # 1. 训练集与验证集采用不同变换策略 train_tf T.Compose([ T.Resize((256, 256)), T.RandomCrop(224), T.RandomRotation(degrees15), T.ColorJitter(brightness0.2, contrast0.2, saturation0.1), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf T.Compose([ T.Resize((224, 224)), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 2. ImageFolder 直接读取前面整理好的目录 ds datasets.ImageFolder(wheat_split/train, transformtrain_tf) val datasets.ImageFolder(wheat_split/val, transformval_tf) train_loader DataLoader(ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val, batch_size32, shuffleFalse, num_workers4) # 3. 加载预训练 ResNet18 并替换最后的全连接层 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_classes len(ds.classes) model.fc nn.Linear(model.fc.in_features, num_classes) # 4. 冻结骨干只训练分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) # 5. 训练与验证 for epoch in range(10): model.train() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(images), labels) loss.backward() optimizer.step() model.eval() correct total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) preds model(images).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fepoch {epoch 1}: val acc {correct / total:.3f}) # 6. 保存模型和类别映射 torch.save({model: model.state_dict(), classes: ds.classes}, wheat_resnet18.pth)关键参数这里需要单独说明。Resize((256, 256))加RandomCrop(224)相当于在放大后的图像上随机取 224×224 区域相当于做尺度扰动比直接Resize((224, 224))增强效果好。batch_size32对应 1,400 张训练图、一个 epoch 约 44 个 batch训练很快显存不够可以降到 16。冻结骨干意味着优化器只更新fc层的参数这时代码里的lr1e-3是安全偏高但不至于发散的水平如果后续解冻骨干学习率必须降到 1e-4 以下。epoch10是起步值实际训练中应观察验证集是否还在上升或波动配合早停来决定是否加轮。这里的Normalize参数用的是 ImageNet 默认值如果按 2.4 的脚本统计了数据自身的均值方差优先替换成统计结果。3.3 全模型微调与只训练分类头的选择只训练fc层在小数据集上收敛快但骨干特征来自 ImageNet和种子图像仍存在分布差异。常见做法是先在冻结状态下把分类头训到稳定再解冻部分骨干层用小学习率微调。对 2,000 张小麦种子数据我一般解冻最后两个残差块也就是layer4和layer3并加载第一阶段的保存权重作为起点。训练策略学习率训练轮数适用阶段只训练分类头1e-35~10首次跑通基线解冻最后两个残差块1e-4 ~ 5e-510~20微调阶段最常用全体解冻1e-5 配合正则20数据量足够、追求上限微调阶段建议配合学习率调度器例如torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience2)验证集准确率停滞两个 epoch 就降一半学习率。只训练分类头时不要用调度器因为 10 轮内通常还没进入平台期。4. 让 2,000 张小麦种子图像发挥最大价值数据增强与损失函数调整样本量少模型能不能泛化增强策略比换网络结构更值得投入。小麦种子没有大幅姿态变化但光照、拍摄角度、载物台背景差异是真实干扰增强的目标就是模拟这些变化。4.1 适合小麦种子图像的增强策略与禁用项我在种子分类项目里常用的增强配置如下表按重要程度排序。核心思路是保持种子本身的颜色和纹理信息不变只扰动拍摄条件因此与颜色强相关的增强要保守使用。增强方式推荐程度参数范围说明随机旋转必选degrees15~30种子在图像中的方向任意随机裁剪/缩放必选Resize 256 RandomCrop 224覆盖拍摄远近变化亮度、对比度扰动推荐0.8~1.2不同设备曝光差异水平/垂直翻转可选p0.5品种分类可用麦粒头尾方向若有关键特征需谨慎颜色直方图随机化不推荐—种子颜色是重要分类依据MixUp / CutMix可尝试alpha0.2提升泛化但置信度会偏低需要更长训练对应的训练变换代码在第 3.2 节的train_tf里已经出现过这里补充一个容易被忽略的顺序问题。T.Compose里的变换按书写顺序执行RandomResizedCrop和RandomRotation的顺序会影响最终的裁剪范围我通常把旋转放在裁剪前面让旋转后的边界裁掉无效区域。另外ColorJitter放在ToTensor()之前它操作的是 PIL 图像对象放在后面会直接报错。4.2 类别不均衡时用加权损失函数小麦种子数据经常不均衡例如霉变异色颗粒占比很低而病害检测恰恰更关心这些少数类。不处理时模型会把几乎所有样本都推到多数类上验证集准确率看起来不差但少数类召回率几乎为 0。处理的方式是给损失函数加类别权重代码实现如下# class_weight.py import numpy as np import torch import torch.nn as nn from sklearn.utils.class_weight import compute_class_weight # 从数据集里读取全部训练标签 y [s[1] for s in ds.samples] classes np.unique(y) weights compute_class_weight(balanced, classesclasses, yy) weights torch.tensor(weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightweights)说明compute_class_weight 计算的是每个类样本数的倒数归一化。 某个类样本越少得到的权重越大让模型在反向传播时对该类的误分类更敏感。替换掉第 3.2 节里的criterion nn.CrossEntropyLoss()其余逻辑不变。加权后训练日志里多数类的准确率通常会小幅下降少数类召回率明显上升这是正常的。要注意ds.samples里存的标签是类别索引不是目录名字符串使用compute_class_weight时要传索引数组不能传目录名称。4.3 训练过程里最容易出现的三个失败模式2,000 张图的训练过程短则几分钟长则半小时失败现象往往在头几个 epoch 就暴露。下面三个是我遇到最多的现象原因排查方向训练准确率很快接近 1.0验证集停滞过拟合增强不够或轮数过多检查是否冻结了骨干验证准确率卡在 80% 左右纹丝不动标签噪声随机抽 100 张训练图人工复查标签验证准确率很高但换一批新图就不行批次效应检查热力图是否落在背景上用下一章的方法过拟合的判断不能只看训练损失降到 0要看验证损失是否在某个 epoch 后开始反弹。第一阶段的“只训练分类头”模式下骨干参数不动模型容量有限过拟合通常发生在第二阶段解冻骨干之后此时把学习率从 1e-4 降到 5e-5并加上更重的随机旋转和裁剪往往比换模型更有效。5. 用 Grad-CAM 检查小麦种子模型的注意力区域验证集准确率达到预期不代表模型真的在“看”麦粒。特别是数据量只有 2,000 张时模型很容易学到载物台的背景纹理、拍摄批次之间的光照差异。判别方式不是再调参数而是用 Grad-CAM 生成热力图检查模型在做决策时关注的区域。Grad-CAM 的核心思想是用类别得分对最后一个卷积层的梯度加权得到与输入同尺寸的注意力图。对小麦种子这样的刚体目标热力图应该集中在麦粒的胚部、腹沟或表面纹理等区分位置上而不是落在图像边缘或背景上。# visualize_cam.py from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget from pytorch_grad_cam.utils.image import show_cam_on_image import cv2 model.eval() # ResNet18 用 layer4 的最后一个卷积块 cam GradCAM(modelmodel, target_layers[model.layer4[-1]]) img, _ val[0] # 取一张验证集图像 img img.to(device).unsqueeze(0) pred_class model(img).argmax(dim1).item() # 生成针对预测类别的热力图 grayscale_cam cam(input_tensorimg, targets[ClassifierOutputTarget(pred_class)]) cam_image show_cam_on_image(denormalize(img), grayscale_cam[0], use_rgbTrue)提示如果项目里还没安装 grad-cam用pip install grad-cam即可代码里的 API 来自这个库的常规用法。批量化验证时可以把热力图按灰度阈值 0.5 做掩膜计算掩膜中落在麦粒区域内的像素占比。这里用简单的颜色阈值把麦粒从背景中分离出来再算热力图能量在该前景区域中的比例。比例超过 0.7说明模型注意力集中在麦粒上如果接近 0.3 甚至更低就要怀疑模型靠背景做分类这时优先检查的是拍摄时背景是否与类别相关而不是更换更强的分类模型。用这个比例作为训练完成后的第一道质检通过后再去看测试集混淆矩阵和错分样本整个 2,000 张小麦种子图像分类数据集的利用才算闭环。本文还有配套的精品资源点击获取