简介这是面向图像分类学习与实验的小麦种子图像分类数据集包含约2000张已标注图片共4个类别具体类别定义可查看配套的json配置文件。所有数据已完成预处理可直接作为分类网络的输入同时划分了训练集、测试集同一类别的图片存放于对应目录省去手动整理与切分的环节。压缩包内附带show.py可视化脚本运行后即可浏览不同类别的样本帮助使用者快速了解数据分布、标注格式与图像质量对计算机视觉初学者比较友好也适用于小麦种子识别、农作物图像分类等方向的算法验证或课程设计。包体信息方面全包共2000个文件以jpg图片为主体共1998张另有1个Python脚本和1个json类别配置文件整体大小约225.64MB目录结构简洁清晰。目前已有223人学习下载数据规模适中既能覆盖常见的四类小麦种子又不会因数据量过大而增加训练负担能够有效支撑分类网络快速迭代与效果对比。1. 小麦种子图像分类2000张已标注图像直接喂进分类网络做图像分类的人都有个共识挑模型花一小时处理数据花三天。农业类图像分类尤甚——拍种子、抠背景、标类别一套流程下来人都麻了。这份小麦种子图像分类数据集约2000张已标注4个类别具体类别名在json文件里训练集测试集都按类别目录分好图片经过预处理可以直接作为分类网络输入还带一个show脚本做可视化复检。适合拿来当baseline实验、验证分类网络改进思路或者给农业视觉项目做第一版模型。和网上随手打包的杂图不同这份数据的目录规范程度高省掉大部分清洗工作。2. 数据集里有什么文件名、json标注与train/test目录三层信息拿到压缩包先别急着训。我的习惯是花二十分钟把数据翻一遍搞清楚三件事哪几类、每类多少张、图片什么尺寸。前两个问题靠文件名和json回答第三个问题跑一下脚本就能确认。这些动作会直接影响后面训练参数怎么定比直接python train.py靠谱得多。2.1 文件名就是品种索引从压缩包挑几个文件名就能看出规律aca315_0219.jpg、aca315_0222.jpg、buckguapo_0283.jpg、buckmeteoro_0206.jpg。下划线前是品种代号下划线后是同一品种内的样本编号。aca315、buckguapo、buckmeteoro是三个能从命名直接读出来的品种第四个品种的代号在这批文件名里暂时没出现等会儿从json文件里确认。命名规律统一说明这批图是同一次采集、同一条流水线处理的光照和拍摄角度相对一致。这一点对分类任务很重要模型不会把光照条件当成品种特征训练过程会稳很多。文件名只能辅助判断最终以json文件为准别凭印象猜。2.2 json文件类别映射和唯一可信标注来源整个数据集里json文件是类别信息的唯一权威来源。类别数、类别名、类别与目录的对应关系都在里面。用Python把它读出来看一眼几行代码的事import json json_path classes.json # 实际文件名以压缩包内的json文件为准 with open(json_path, r, encodingutf-8) as f: class_info json.load(f) print(类别数量:, len(class_info)) print(class_info)json.load把文件内容反序列化成Python对象len(class_info)看类别数量是否为4。打印出来的内容可能是列表也可能是字典如果是列表下标就是类别索引如果是字典通常key是类别名value是对应的目录名或统计信息。这里埋着一个最容易翻车的细节PyTorch的ImageFolder按目录名排序自动生成标签索引如果json里的类别顺序和目录排序不一致训练出来的编号就对不上。所以读json时把class_info的类别顺序和train/下面的目录顺序对着看一遍这个动作花不了30秒能避免后期所有标签错位问题。2.3 train/test目录结构分类网络最友好的组织方式这个数据集的结构是训练集、测试集各自独立同类图片放在同一个子目录里大概长这样# 示例目录结构实际以压缩包为准 train/ ├── aca315/ │ ├── aca315_0001.jpg │ └── ... ├── buckguapo/ │ └── ... └── buckmeteoro/ └── ... test/ ├── aca315/ ├── buckguapo/ └── buckmeteoro/这种组织方式是分类网络最喜欢的格式不需要手写CSV不需要额外的标注文件ImageFolder直接读目录结构就能把路径映射成标签。对比一下从YOLOv8检测任务转过来的人可能习惯txt标注那一套但分类数据集要简单得多——目录就是标签。正文说数据经过预处理可以直接作为分类网络输入。这句话的意思是已经做了背景抠除、主体居中这类基础处理但不代表不需要Resize和归一化。我拿到数据后第一件事永远是全量扫描一遍图片尺寸和通道from PIL import Image import glob import os for path in sorted(glob.glob(os.path.join(train, *, *.jpg)))[:30]: with Image.open(path) as im: print(path, im.size, im.mode)这个脚本打印前30张图的路径、宽高、颜色模式。重点看两项所有图尺寸是否一致、mode是否全是RGB。如果出现尺寸不一致后面统一Resize到224×224如果混进灰度图要在transform里加.convert(RGB)。约2000张图摊到4类每类大约500张这个规模决定了后面必须走迁移学习而不是从零训练。3. 先跑通show脚本四类检查项把数据问题拦在训练之前show脚本存在的意义是让“机器觉得没问题、人眼觉得有问题”的图现形。农业图像分类翻车大多不是模型不行是数据里有脏东西类别目录混了、背景不统一、偶尔夹一两张模糊图。运行脚本把图片拼成grid一眼扫过去问题就出来了。3.1 show脚本怎么运行数据集自带的show脚本通常是一个可视化入口我一般这样调用python show.py --data_dir ./ --num_samples 6 --save_dir ./preview如果压缩包里脚本名不是show.py也没关系找带show、preview、visualize字样的py文件。脚本做的事情大同小异从train目录或test目录的每个类别里抽几张图拼成一张网格图输出到preview目录每张图上方标类别名。--num_samples控制每个类别抽几张6张足够看出问题。脚本入口名和参数名以压缩包内实际内容为准但调用逻辑基本一致。如果脚本不带参数直接运行能出图那更省事。跑完把预览图打开逐行看一遍下面这四类问题都要过一眼。3.2 可视化要盯住的四类检查项检查项具体怎么看发现问题后怎么处理类别是否均衡ls train/*/ | wc -l或脚本统计少数类做重采样或加权损失背景是否统一grid图逐行看加RandomResizedCrop和色彩抖动坏图与模糊图文件打不开或视觉发虚删除该样本或整批重下尺寸与通道是否一致打印im.size和im.mode统一Resize灰度图转RGB类别不均衡在小样本数据里最常见。4类平均分配当然最好但现实采集时某些品种的种子特别少这类就容易被其他类“吃掉”。训练前把每类数量打印出来贴屏幕上后面调损失函数才有依据。背景统一这条最玄学。种子的分类特征是表皮纹理如果某一类是在偏黄的背景下拍的模型很可能学会“背景色等于类别”这个捷径训练时acc很好看换个环境推理立刻掉。遇到这种情况加RandomResizedCrop、ColorJitter这类增强能让模型被迫把注意力放回种子本身。坏图这关是show脚本存在的意义。解压过程偶发文件损坏、异常光照拍出全黑图模型碰到直接NaN。尺寸和通道方面预处理过的数据集通常没问题但我见过把灰度图和RGB图混在同一个目录里的ImageFolder不会报错但模型输入分布会被污染。3.3 show脚本运行失败的三种常见原因预览脚本本身也可能跑不起来。常见的有三种环境缺依赖、路径传错、个别图片损坏导致读取返回空。环境问题通常是缺PIL或torchvisionpip install pillow torchvision就能解决路径问题注意--data_dir要指向数据集根目录不是train子目录图片损坏会在读取时报OSError或返回None脚本里加个try跳过就行。如果脚本实在跑不通自己写一个也就二三十行读目录、取前几张图、用PIL拼图保存不依赖数据集自带的脚本也能完成检查。这一步的目的不是“完成一个可视化任务”而是确保后面训练时不会因为坏图浪费一个晚上。4. 用ResNet18做四分类迁移学习的选型逻辑与训练参数模板数据检查完了接下来是模型选型和训练。这个数据集的规模决定了直接用大模型从零训练不是明智选择ResNet18加迁移学习是性价比最高的起点。4.1 为什么先选ResNet18而不是ViT2000张图、4个类别每类平均约500张这是典型的小样本分类任务核心矛盾是参数量和数据量的匹配。ResNet18参数量大约11.7MResNet50约25.6MViT-Base约86M。数据只有2000张时参数量越大过拟合风险越高。模型参数量2000张上的表现预期单卡训练速度ResNet18约11.7M稳定通常能到90%以上准确率30分钟级别ResNet50约25.6M上限略高但更容易过拟合40~60分钟ViT从零训练约86M极容易过拟合慢且不稳定Transformer不是不能做图像分类而是它在数据量上很“挑食”。2000张直接从头训ViT通常二十个epoch内就在验证集上失去泛化能力。真想对比Transformer的效果要用预训练的DeiT或CaiT做迁移而不是从头训。ResNet18的ImageNet预训练权重在torchvision里直接下载是这类任务最常用的起点。提示调参时先把baseline跑通再谈换模型。ResNet18这一段跑不顺利换更大的模型大概率更不顺利。4.2 一个可以跑通的PyTorch训练脚本下面这个脚本是我在这种小样本分类数据集上常用的模板直接复制改路径就能跑import torch import torch.nn as nn from torchvision import transforms, datasets, models from torch.utils.data import DataLoader # 训练集增强轻微翻转、旋转、色彩抖动 train_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 验证集不增强保持原始分布 test_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(train, transformtrain_transforms) test_ds datasets.ImageFolder(test, transformtest_transforms) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) test_loader DataLoader(test_ds, batch_size32, shuffleFalse, num_workers4) # 加载ImageNet预训练权重新版torchvision用weights参数 model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) # 旧版torchvision可用model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, 4) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max40) for epoch in range(40): model.train() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() model.eval() correct total 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) print(fepoch {epoch 1} val acc: {correct / total:.4f}) torch.save(model.state_dict(), wheat_resnet18.pth)几个关键参数说清楚。Resize((224, 224))是把输入统一到224×224这是ResNet18的默认输入尺寸Normalize的mean和std是ImageNet的统计值因为用的是ImageNet预训练权重这两个值必须保持一致否则预训练特征会被破坏。model.fc从1000改成4是迁移学习必改的一行in_features是上一层输出的特征维度直接用属性拿就行。优化器用AdamW学习率1e-4。预训练权重已经收敛到特征空间里的一个较好位置学习率太大会把权重冲乱1e-4是安全起点如果你用小学习率跑完一轮发现收敛太慢再提到3e-4不迟。CosineAnnealingLR配合epochs40让学习率从1e-4余弦退火到接近0比固定学习率在最后阶段更能精调。保存的state_dict后面推理时要用别丢。4.3 别只看全局准确率按类看precision、recall和混淆矩阵全局准确率会掩盖问题。某个类样本多、预测得好整体acc被拉高但实际有两类一直互相错分。所以训练完我习惯补一个分类报告from sklearn.metrics import confusion_matrix, classification_report y_true, y_pred [], [] model.eval() with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) y_true.extend(labels.cpu().numpy()) y_pred.extend(preds.cpu().numpy()) print(classification_report(y_true, y_pred, target_namestest_ds.classes)) cm confusion_matrix(y_true, y_pred) print(cm)classification_report会输出每个类别的precision、recall、f1-score。如果某个类的recall明显低于其他类说明它的图经常被预测成别的类混淆矩阵能直接看出是哪两类在互相打架。这两行代码的价值在于它告诉你下一步该调损失函数、加数据还是换模型而不是瞎试参数。5. 避坑记录小麦种子小样本分类的四个典型翻车现场约2000张图的数据量训练起来坑不少。下面四条是我在类似小样本图像分类数据集上反复遇到过的每一条都按现象、原因、解决的顺序说清楚。5.1 强化增强也救不了的过拟合现象第一轮训练跑到第20个epochtrain acc到了98%val acc卡在76%上下不来val loss一路往上走。训练集表现越好验证集越差典型的过拟合信号。原因2000张图太少数据增强太弱模型把训练集每张图的纹理细节和背景污点都背下来了。种子图像背景简单模型尤其容易走捷径记住背景而不是种子本身。解决加强数据增强同时加早停。把训练分支的transform换成更强的版本from torchvision.transforms import RandAugment train_transforms transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomRotation(15), transforms.RandAugment(num_ops2, magnitude9), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomResizedCrop让模型每次看到同一张图的不同裁剪区域RandAugment自动组合颜色和几何变换比手工调ColorJitter省事。早停的做法是监控val loss连续5个epoch不下降就停止并且只保存val loss最低时的权重best_loss float(inf) for epoch in range(epochs): # ...训练代码... if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), wheat_best.pth)早停是过拟合场景下的后悔药。训练日志里val loss一旦掉头向上后面多少个epoch都是浪费。5.2 两类互相混淆加权损失与重采样二选一现象classification_report里某两个类的precision和recall都不到70%混淆矩阵显示大量相互错分。整体准确率有85%但这两类是短板。原因两方面叠加。一是这两个品种本身的外观特征确实接近模型区分吃力二是两类样本量不均衡模型在训练时天然偏向多数类少数类学不充分。解决给少数类更高的损失权重这是最直接的手段import glob import torch # 按类别样本数计算权重样本越少权重越高 counts [len(glob.glob(ftrain/{cls}/*.jpg)) for cls in train_ds.classes] weights torch.tensor([1.0 / c for c in counts], dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightweights)CrossEntropyLoss的weight参数会对少数类样本的loss乘以一个放大系数相当于把少数类样本“复制”了一部分但不增加实际计算量。如果加权后效果还不够再试WeightedRandomSampler做重采样让每个batch里少数类出现的概率更高。两条路先走加权损失代码改动最小。5.3 预训练权重死活加载不进模型现象运行训练脚本时终端报错提示size mismatch for fc.weight具体是copying a param with shape torch.Size([1000, 512])之类。原因torchvision默认的ResNet18最后一层fc输出是1000维ImageNet类别数而你已经把model.fc改成了4维。直接load_state_dict时fc.weight和fc.bias维度对不上加载失败。解决加载时把fc层相关的key过滤掉让fc保持随机初始化让它在训练中自己学。常见的做法是这样state torch.load(resnet18_imagenet.pth, map_locationcpu) state {k: v for k, v in state.items() if not k.startswith(fc.)} model.load_state_dict(state, strictFalse)strictFalse是兜底设置真正起作用的是过滤掉fc.前缀的键。fc层不加载预训练权重没关系它本来就只需要从零学种子类别而前面的卷积层特征才是迁移学习的核心价值。我一般先改fc再过滤这样加载时模型的前面所有层都和预训练权重对齐。5.4 训练到一半loss变成nan现象训练进行到第11个epochloss突然变成nan之后每个batch都是nan整个训练废了。原因最常见的是学习率设置过大在迁移学习任务里尤为明显。也有人碰到数据集里存在异常图——全黑、全白或像素值溢出导致前向传播计算出非数值。解决把学习率从1e-3降到1e-4或3e-4重启训练如果已经是nan把lr降到原来的十分之一再试。同时跑一遍全量扫描脚本把尺寸异常、通道异常的图筛掉。这种问题看起来低级但在小样本数据集中出现频率不低尤其是来自不同拍摄条件的数据混合时。6. 让2000张数据出更稳的结果五折验证、盲测与集成投票单次train/test划分在小样本数据集上运气成分太大。测试集如果恰好包含某类最难样本全局准确率能掉好几个点。想让结果可信通常我会再用三种方法加固。6.1 用StratifiedKFold替代单次划分五折交叉验证能抹平数据划分的偶然性。重点是用分层抽样保证每一折里4个类别的比例和整体一致避免某一折全是同一个品种from sklearn.model_selection import StratifiedKFold paths glob.glob(train/*/*.jpg) labels [p.split(/)[-2] for p in paths] skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for fold, (tr_idx, va_idx) in enumerate(skf.split(paths, labels)): # 用tr_idx和va_idx切分数据集构造PyTorch的Subset pass每折训练一次5次结果取平均比单次划分的结果可靠得多。6.2 预留十张盲测图交叉验证只能说明模型在“这批数据”上的表现。我习惯从原始文件里手工挑10张图不透任何训练和验证过程单独放在一边训练完全结束才跑一次推理model.eval() with torch.no_grad(): for path in blind_paths: im Image.open(path).convert(RGB) x test_transforms(im).unsqueeze(0).to(device) pred model(x).argmax(dim1).item() print(path, test_ds.classes[pred])盲测图最好覆盖不同光照和拍摄角度逼真模拟真实使用场景。这一步能发现数据预处理阶段隐藏的风格差异。6.3 多模型集成投票如果单模型结果在盲测上不稳定可以训练2到3个不同初始化或不同增强策略的模型推理时投票preds [] for m in models: preds.append(m(x).argmax(dim1).item()) final max(set(preds), keypreds.count)小样本数据集上集成投票比单独加大模型更有效代价是训练时间翻倍但换来的是稳定性。这份数据第一次跑完我的val acc只有82%后来发现原因很简单——第2章建议做的全量扫描我没做某个类别目录里混进了几张其他品种的图。从那以后我拿到任何小样本图像分类数据集都强制走一遍同样的流程先翻json确认类别再全量扫描图片形状和通道最后才定baseline和增强方案。这套流程听起来慢实际最多花半小时省下的调试时间远超过当年踩坑浪费的。数据集的下载入口就在项目页压缩包里的json和show脚本都别删后续验证批次和复现都要用。希望帮到你。本文还有配套的精品资源点击获取