简介一套面向计算机视觉与深度学习初学者的已标注小麦种子图像分类数据集涵盖4个类别共约2000张图片。数据已完成预处理可直接作为分类网络输入免去清洗与标注环节训练集与测试集按类别目录划分并附json文件记录类别索引与划分信息。压缩包内共2000个文件以jpg图片为主1998张另含1个类别配置文件与1个可视化脚本整体大小225.64MB结构清晰易用。运行内置show脚本可快速查看各类别样例便于核对数据分布。目前已有223人学习下载既可用于小麦种子质量筛选、农作物品种识别等场景也可作为分类网络改进实验的基准数据集帮助读者跳过数据准备专注模型设计与调参。1. 从2000张已标注小麦种子图开始这是一份能直接喂进分类网络的数据集做图像分类的同行应该都有这种经历花在数据整理上的时间经常比调模型还多尤其是种皮纹理这类细粒度分类拍出来的图反光、背景脏、类别不均还没到训练就已经消耗了大半精力。这份小麦种子图像分类数据集约2000张、4个类别已经把训练集和测试集按类别分开存放附了json文件保存类别映射还带一个show脚本可以直接可视化。对我这种经常要快速搭baseline的从业者来说最省事的地方在于它已经完成标注和预处理取过来就能作为分类网络输入对刚入门图像分类的新手它又是一份能对照学习的标准数据组织样本目录怎么划、标签怎么对、怎么验证标注和图片一致都有实际落地的答案。2. 数据组织与预处理边界目录划分、JSON 映射和反光白底图2.1 目录与标签体系为什么训练集测试集要按类别分开存放拿到资源后先看目录结构这一点比跑代码更重要。从文件名能看出一些线索比如aca315_0219.jpg、buckguapo_0283.jpg、buckmeteoro_0206.jpg前缀aca315、buckguapo、buckmeteoro大概率对应品种或类别名后面的数字串是图片编号。但注意这只是「大概率」最终以json文件为准不要直接用文件名前缀当标签。数据集划分逻辑是训练集、测试集各放各的内部再按类别分开目录存放。这意味着同一类图片会集中在一个文件夹里目录名就是类别名。实际使用时的目录结构通常是下面这个样子dataset/ ├── train/ │ ├── aca315/ │ ├── buckguapo/ │ ├── buckmeteoro/ │ └── ... ├── test/ │ ├── aca315/ │ ├── buckguapo/ │ ├── buckmeteoro/ │ └── ... └── classes.json这种按类别分目录的结构是和torchvision.datasets.ImageFolder天然兼容的。ImageFolder会自动把每个子目录名映射成一个类别id目录顺序决定id编号。但这里有个容易被忽视的问题ImageFolder的类别顺序是按字典序排的而json里的类别映射可能不是这个顺序两套编号如果对不上训练出来的模型在推理时就会张冠李戴。我一般会先读json把class_name - class_id的映射打印出来再和ImageFolder.class_to_idx对照一遍。json文件的格式常见的是两种。一种是直接把类别名映射到数字id{ aca315: 0, buckguapo: 1, buckmeteoro: 2, buckotero: 3 }另一种是嵌套结构把每个类别对应的文件名列表也存进去。不管哪种核心要拿到的是「类别名到数字id」的映射。这份资源的摘要里明确写了分类个数是4具体类别名是什么、id怎么排看json最准。拿到json之后第一件事不是写训练代码而是写个断言确保json里的类别名和目录名完全一致防止后面加载时静默错位。2.2 预处理做到哪一步尺寸、通道和「能直接训练」的真实含义「数据经过预处理可以直接作为分类网络输入使用」这句话要拆开看。它通常意味着图片已经做过基础清洗比如裁掉了大块无关背景、统一了文件格式、可能已经缩放到某个固定尺寸。但具体尺寸是多少、通道顺序是什么、是否已经是RGB需要自己验证一次不能想当然。我用一段代码打开一张图做检查这是拿到任何数据集后的固定动作from PIL import Image import os img_path dataset/train/aca315/aca315_0219.jpg img Image.open(img_path) print(size:, img.size) # 例如 (224, 224) print(mode:, img.mode) # RGB / L / RGBA print(format:, img.format) # JPEG / PNG这段代码的输出能回答三个问题图片是否统一尺寸、是不是RGB三通道、是什么编码格式。如果发现尺寸不统一后面Resize就要做得更小心如果出现RGBA需要转成RGB再喂给网络如果是灰度图要决定是复制成三通道还是改网络第一层输入。从文件名和这类种子图像数据的常见拍摄方式看这批图大概率是白底或浅色背景下的单粒种子特写光照相对均匀但也存在高光区域——种皮本身有蜡质光泽打光稍微硬一点就会出现局部过曝。这类高光会冲掉种皮纹理而纹理恰恰是细粒度分类的关键特征。预处理能解决背景杂乱的问题但解决不了反光后者要靠数据增强或者成像端的补光来缓解这一点后面避坑章节会具体讲。另一个值得确认的点是图片的数值范围。如果预处理已经做了归一化那图片读出来就是浮点如果只是普通的JPEG存储读出来是0到255的整数。这决定了你的ToTensor()之后的数据分布。稳妥的做法是先统计一下训练集的均值和标准差而不是直接套ImageNet的统计值——当然2000张规模的迁移学习场景下套用ImageNet的mean/std也不会出大问题但如果要做精细化训练还是以实际统计为准。3. PyTorch 分类全流程Dataset、数据增强和训练参数怎么搭3.1 自定义 Dataset从 JSON 读类别映射而不是猜文件名目录结构清晰的前提下最省事的做法是直接上ImageFolder但为了保证类别映射和json一致我会写一个自定义Dataset把json作为标签的唯一真源。这样做的好处是以后换数据集、加新类别、做类别过滤都只改json不动代码逻辑。import json import os from PIL import Image from torch.utils.data import Dataset class WheatSeedDataset(Dataset): def __init__(self, data_dir, class_map_path, transformNone): self.data_dir data_dir self.transform transform with open(class_map_path, r, encodingutf-8) as f: self.class_map json.load(f) # 例如 {aca315: 0, buckguapo: 1, ...} self.samples [] for cls_name, cls_id in self.class_map.items(): cls_dir os.path.join(data_dir, cls_name) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): self.samples.append((os.path.join(cls_dir, fname), cls_id)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label这段代码的逻辑分三层__init__负责读json、遍历类别目录、把「图片路径标签」拼成样本列表__len__返回样本总数供DataLoader分批__getitem__按索引读图、转RGB、做变换。几个参数值得注意transform传入的是torchvision.transforms的组合如果设为None就返回原始PIL图像.convert(RGB)会把灰度图或RGBA图统一转成三通道避免后面网络输入通道数不匹配continue跳过不存在的目录防止类目缺失时报错。这里我要强调一点不要用文件名前缀当标签。虽然这份数据的前缀看起来和类别名高度相关但标注过程中难免有文件名和内容不一致的情况json是整理过的权威版本。如果你直接用ImageFolder它按目录名生成标签确实也能跑但一旦目录名和json有出入你都不知道错在哪。自定义Dataset的好处是让json成为唯一真源排查问题时有据可查。3.2 数据增强取舍亮度扰动要做旋转要克制数据增强这一节最容易走极端。看到2000张的规模很多人上来就套一堆强增广结果模型训练不稳定loss曲线像心电图。种子图像的领域特性决定了增强策略不能乱来种子的形状、种皮纹理、颜色分布是分类的核心线索而旋转对这类细粒度任务的影响比想象中大——旋转90度可能让长粒品种看起来像短粒品种破坏形态判别信息。水平翻转相对安全因为左右镜像不改变种子的真实形态。我在这类细粒度图像分类任务上常用的增强组合是尺寸统一、水平翻转、亮度对比度扰动、轻微模糊旋转和裁剪尽量克制或者干脆不加。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.15, contrast0.1, saturation0), transforms.GaussianBlur(kernel_size3, sigma(0.1, 0.5)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])ColorJitter里saturation0是我有意为之小麦种子的颜色差异是分类依据之一饱和度扰动太强会让颜色特征失真。亮度扰动0.15是应对拍摄时打光不均的问题给模型一点亮度变化的余量这也是上一章说的「反光问题要靠增强缓解」的具体落地。GaussianBlur模拟轻微失焦能提升模型对成像质量的鲁棒性但sigma不要开太大否则纹理特征会被抹掉。测试集和推理时用val_transform只做尺寸统一和归一化不做任何随机增强。这一步如果做错比如把随机增强用在评测上那指标波动会非常大同一张图每次测出来的准确率都不一样。3.3 训练超参与早停2000 张规模下先微调再全量2000张的训练集规模对深度学习来说偏小从头训练一个ResNet大概率过拟合。常见做法是加载ImageNet预训练权重冻结前面几个stage只微调深层特征和分类头。等深层稳定了再解冻全部参数用更小的学习率做全量微调。这套两阶段策略在细粒度分类里几乎是标配。import torch import torch.nn as nn import torchvision.models as models from torch.utils.data import DataLoader model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 4) # 阶段一只训练 layer4 和 fc trainable [] for name, param in model.named_parameters(): if name.startswith(layer4) or name.startswith(fc): param.requires_grad True trainable.append(param) else: param.requires_grad False optimizer torch.optim.Adam(trainable, lr1e-4) criterion nn.CrossEntropyLoss() # 阶段二解冻全部降低学习率 for param in model.parameters(): param.requires_grad True optimizer torch.optim.Adam(model.parameters(), lr5e-5)requires_grad的设定决定了哪些参数参与梯度更新。阶段一layer4和fc可训练前面层保持ImageNet的预训练特征不动这符合「先微调高维语义特征」的直觉阶段二全量微调学习率降到5e-5防止把预训练权重冲得太狠。分类头的输出维度改成了4对应json里的类别个数这里如果写错成别的数字CrossEntropyLoss计算时维度不匹配会直接报错。batch size 我一般取16或32。2000张的数据量batch size 32 一个epoch只有60多个step训练很快。学习率从1e-4起步配合ReduceLROnPlateau按验证loss降低学习率patience设3。早停的patience设5到8轮验证集准确率连续多轮不涨就停。这个组合在细粒度分类上翻车概率最低。训练轮数不用死磕通常30到50轮内早停就会触发。4. show 脚本与单张推理可视化核对标注再用训练好的权重预测4.1 show 脚本怎么用先确认标注再谈训练资源里带的show脚本很多人当它是个可有可无的装饰实际上它是整个数据集质量验证的第一道关口。这个脚本的作用是可视化数据集把每一类抽样出来的图片拼成一张大图让你肉眼确认标注和图片内容是否对应。这一步看起来原始但对细粒度分类数据集来说标注错误往往就藏在一两张「看起来不太对」的图里。运行方式通常是命令行直接调用python show.py --data_dir dataset/train --class_map classes.json如果脚本没有参数接口也可能会直接读取当前目录下的结构并弹出一张拼接图。具体参数名以资源内的README或脚本头注释为准但逻辑是通用的遍历每个类别目录每个类别抽3到5张图按网格拼接标注上类别名。做这件事的目的有两个一是确认aca315、buckguapo、buckmeteoro这些目录名和实际种皮纹理特征对得上二是确认json里的映射没有串位。我自己的习惯是在训练前、训练后各跑一次。训练前跑确认数据没问题训练后跑把类别名和模型预测结果摆在一起做主观对照。很多模型「acc看着还行但实际不能用」的问题就是这一步发现的。4.2 单张推理脚本从图片路径到类别概率的完整链路训练完成后最简单也最刚需的一个脚本是单张图片推理。它把测试集评估之外的最常用场景独立出来给定一张没见过的种子图输出它属于每个类别的概率。判断题主的工作流是「先归档、后分类」还是「单张即拍即测」影响到脚本要不要做批量目录处理但单张推理是基础版本。import torch import torch.nn.functional as F from PIL import Image from torchvision import transforms def predict_one(img_path, model, class_names, devicecpu): model.eval() transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(img_path).convert(RGB) x transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits model(x) probs F.softmax(logits, dim1)[0] top1_idx torch.argmax(probs).item() top1_prob probs[top1_idx].item() return class_names[top1_idx], top1_prob, probs.tolist() # 使用示例 classes [aca315, buckguapo, buckmeteoro, buckotero] name, prob, all_probs predict_one(test.jpg, model, classes) print(f预测类别: {name}, 置信度: {prob:.4f}) print(f各类别概率: {dict(zip(classes, all_probs))})几个细节说明unsqueeze(0)把单张图扩成batch维度因为网络输入要求四维张量(B, C, H, W)torch.no_grad()关闭梯度计算推理时省显存也提速F.softmax(logits, dim1)把logits转成概率分布dim1对应类别维。输出里class_names[top1_idx]的索引顺序必须和训练时的类别id对齐如果你的json里aca315对应 id 0那classes列表的第0个元素就得是aca315顺序错了预测结果就全是错的。5. 避坑与常见问题小麦种子分类最容易翻车的 5 个地方5.1 类别不平衡导致的高acc低recall现象训练loss正常下降测试集整体准确率也不错但画混淆矩阵发现某个类别的recall特别低大量样本被错分到另一个纹理相近的类别。原因约2000张数据平均到4个类别理论上每类500张但实际采集过程中不同品种的样本量容易失衡。某些类别先天难拍或者采集时数量偏少模型在样本多的类别上过拟合样本少的类别学不到足够的判别特征。解决先统计每类数量确认是否失衡。如果差距超过2倍用WeightedRandomSampler对训练集做加权采样让每个epoch里类别出现的频率趋于均衡或者给CrossEntropyLoss传入类别权重给样本少的类别更大的梯度权重。两个方案选一个就行不要同时上否则模型会过度补偿。5.2 高光区域把种皮纹理抹掉现象训练时loss降得很快但测试集上凡是带高光斑块的图预测概率普遍偏低甚至把aca315误判成buckmeteoro。原因种皮表面有蜡质硬光照射下的高光区域会损失纹理细节而纹理恰恰是区分这几个品种的核心特征。如果训练集里高光图比例低模型没学会对这种干扰鲁棒测试时高光图就成了分布外样本。解决数据增强里加亮度扰动和适量高斯模糊模拟不同光照强度下的成像差异。更彻底的做法是对训练集做直方图均衡或CLAHE降低高光区域的局部对比度。如果成像条件可控拍摄时用柔光箱或漫射板避免直射硬光这是从源头解决问题。5.3 json 类别映射和目录名不一致现象训练正常结束推理时发现有些图的预测结果完全对不上比如明明是buckguapo的种皮纹理模型却高置信度输出buckmeteoro。原因json里的类别名顺序和训练目录的扫描顺序不一致。ImageFolder按字典序生成class_to_idx而json里的id可能是按采集先后排的两个映射对不上模型学到的标签本身就是错的。解决加载数据后先断言json的类别集合 目录名集合然后确认json[id]和class_to_idx[目录名]指向同一个数字。最简单粗暴的方式是打印出来逐条核对虽然原始但有效。再懒一点直接让我上面写的自定义Dataset作为唯一数据入口它从json生成样本列表天然保证标签和json一致。5.4 测试集和训练集来自同一拍摄批次现象测试集acc高达98%但拿到外部拍摄的小麦种子图一测准确率直线掉到70%多。原因训练集和测试集如果来自同一次拍摄、同一个背景、同一台设备模型学到的是拍摄环境特征而非种皮本身特征——背景边缘、光照角度、镜头畸变都可能被当成分类依据。这在细粒度分类数据集里非常常见。解决训练前先确认测试集图片的光照、背景和拍摄角度是否与训练集有差异。如果没有差异至少用留一拍摄批次的交叉验证来评估真实泛化能力。如果这份资源的测试集也存在同批次问题那就自己划分一个「跨条件验证集」比如按文件名编号末尾抽一部分模拟分布偏移。5.5 只用 accuracy 评估细粒度分类现象整体准确率看着不错但对生产环境没有丝毫指导意义——不知道哪两个类别最容易混淆不知道模型是保守型还是激进型。原因accuracy 在类别相对均衡时是个合理的总体指标但它不告诉你错在哪里。小麦种子分类的代价并不对称——把抗病品种认成普通品种的代价远高于普通品种之间认错的代价只看acc看不出这种风险。解决至少画出混淆矩阵看哪两个类别的混淆最严重针对性地补数据或调整增强策略。更进一步在测试集上统计「top-2命中率」——如果真实类别在模型预测的前两个概率里那说明特征学习没问题只是相似类别的边界没划清。这个指标对细粒度分类的模型诊断价值极高。6. 进阶验证导出置信度报告把黑匣子变成可追溯的表格模型跑通不是终点能不能上线才是。上线之前我会做一步已经很固定的事情把测试集的所有预测结果连同置信度导成一张CSV按置信度排序逐张检查底部那批低置信度样本。这个过程相当于给模型做一次「摸底体检」能直接回答三个问题模型在哪些图上最没把握、这些图有什么共同特征、哪些错误是标注噪声导致的。import csv import torch import torch.nn.functional as F from tqdm import tqdm def export_confidence_report(model, dataloader, class_names, output_pathreport.csv): model.eval() rows [] with torch.no_grad(): for images, labels in tqdm(dataloader): logits model(images) probs F.softmax(logits, dim1) top2_prob, top2_idx torch.topk(probs, k2, dim1) for i in range(images.size(0)): rows.append([ dataloader.dataset.samples[i][0], # 图片路径 class_names[labels[i]], # 真实类别 class_names[top2_idx[i][0].item()], # 预测类别 round(top2_prob[i][0].item(), 4), # top1置信度 round(top2_prob[i][1].item(), 4) # top2置信度 ]) with open(output_path, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([img_path, true_class, pred_class, top1_prob, top2_prob]) writer.writerows(rows)这段代码把DataLoader输出的每个batch做一次前向topk(k2)取出概率最高的两个类别连同真实类别、图片路径一起写入CSV。注意dataloader.dataset.samples[i][0]这句它假设你的Dataset里有samples这个属性也就是我在第3章自定义Dataset里定义的样本列表。如果你的数据集类没有这个属性就先遍历一遍把路径收集起来再对齐。导出之后我会按top1_prob升序排列拿出置信度最低的50张图用show脚本拼成网格人工逐个核对。这个过程常发现三类问题一是标注本身错了模型其实是「错」的正确答案二是反光、阴影、失焦这类成像问题需要增强来兜底三是两个类别的边界样本本来就模糊需要重新定义分类标准或者加一个「不确定」类别。做完这一步这份数据集和模型的能力边界就完全摸清了。从那以后我每次拿到任何已标注数据集都会强制走一遍show脚本核对、训练、导出置信度报告、人工复审这个闭环哪怕只花半小时也会在下一次遇到「模型acc虚高但线下用不了」的玄学问题时帮我省下两天的排查时间。希望这次的数据集拆解和代码片段能帮你在小麦种子分类这条路上少走几个弯路。本文还有配套的精品资源点击获取
