桑叶品种图像分类实战:基于ResNet50与ViT的迁移学习与验证

发布时间:2026/10/25 12:38:24
桑叶品种图像分类实战:基于ResNet50与ViT的迁移学习与验证 简介面向桑叶品种识别与图像分类任务这份已标注数据集包含约5000张图片覆盖红王、白王、Chiang Mai 60、Buriram 60等10个常见品种。训练集与测试集已提前划分同品种图片集中存放便于直接训练与评估随包还提供Python可视化脚本可快速查看各类别样本分布json文件则记录了完整类别映射与划分信息。整个7z压缩包共2000个文件核心为1998张JPG图片另附1个Py脚本与1个json配置文件压缩后约730MB适合用作物联网/智慧农业背景下的图像分类教学、算法对比或毕业论文实验。该资源目前已吸引76人学习下载配套技术专栏还给出了图像分类网络改进思路与计算机视觉完整项目参考能帮助初学者快速走通数据加载、模型训练与评估全流程。1. 桑叶品种图像分类一份5000张已标注数据能做什么桑树品种识别在蚕桑产区、代用茶加工和农技推广里是一道反复出现的重复劳动靠肉眼把采回来的叶片按品种分拣不同品种的叶形、叶色、叶脉和锯齿差异确实存在但看久了眼睛容易花新人上手还要培训一段时间。这个场景非常适合用图像分类算法来替代人工初筛。“桑叶品种数据集图像分类数据集”这类已标注数据正好把最费人工的两个环节采集、打标直接省掉约5000张图对单品种多分类是一个够用的起点。对象是两类人一是想做农业视觉落地的算法工程师二是拿公开流程练手的入门者。落地主线很清楚按规范划分数据、用预训练模型做迁移学习、用混淆矩阵和Grad-CAM验证结果。下文按这条线展开每一步的参数和坑都列出来。2. 拿到数据集先别训练目录规划、标注检查与数据划分很多人拿到一份已标注数据集第一反应是直接开训。但图像分类任务里数据准备流程决定了模型可靠性的上限它比模型选型更值得花时间。这一章处理三件事判断数据规模够不够用、把数据按规范拆成三份、把标注质量检查一遍。2.1 5000张已标注数据意味着什么先估算“够不够用”数据够不够用取决于类别数和类间差异。具体有多少个品种以你拿到的实际标注为准假设有5个品种每类大约1000张这是常规迁移学习配置假设有15个品种每类300多张就已经进入小样本训练区间。桑叶品种之间的差异非常细叶形、叶色、叶脉密度、锯齿深浅都可能成为判据和森林图像分类里的树种识别类似属于典型的细粒度特征。每类只有几百张时只能靠预训练权重、数据增强和正则化来补。已标注是这份数据最大的红利。图像分类落地最贵的成本不是训练而是“采集清洗打标”。标注阶段省掉之后精力可以集中在目录检查、拆分策略和训练参数上。但要注意标注做过不代表标注全对后面第2.3节会给一套检查脚本千万别跳过。2.2 设计目录与划分策略train/val/test怎么分常见做法是改成ImageFolder风格的目录结构torchvision的datasets.ImageFolder可以直接读取省掉自定义Dataset。如果你的原始数据是“一个CSV标注文件加一堆图片”第一步要先转成目录结构否则后面每次加载都要写额外映射逻辑。sangye_splits/ ├── train/ │ ├── cv1/ │ ├── cv2/ │ └── ... ├── val/ │ ├── cv1/ │ ├── cv2/ │ └── ... └── test/ ├── cv1/ ├── cv2/ └── ...三个目录的作用要提前定好目录用途是否参与训练train模型训练权重更新是val每轮结束后评估用于保存历史最佳模型否但间接影响模型选择test全部调参完成后的最终评估只跑一次否下面这段脚本把原始目录拆成70 / 15 / 15的比例。我这里用的是“复制”而不是“移动”原始素材保留一份后面发现划分有问题还有后悔药。import os import random import shutil from collections import defaultdict random.seed(42) # 假设原始目录结构: raw/品种名/图像文件 src_root raw dst_root sangye_splits ratio (0.7, 0.15, 0.15) for cls_name in os.listdir(src_root): cls_dir os.path.join(src_root, cls_name) if not os.path.isdir(cls_dir): continue files [f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(files) n_total len(files) n_train int(n_total * ratio[0]) n_val int(n_total * ratio[1]) splits { train: files[:n_train], val: files[n_train:n_train n_val], test: files[n_train n_val:], } for split_name, split_files in splits.items(): for fname in split_files: src os.path.join(cls_dir, fname) dst os.path.join(dst_root, split_name, cls_name, fname) os.makedirs(os.path.dirname(dst), exist_okTrue) shutil.copyfile(src, dst)逻辑说明外层循环遍历每个品种目录内层把图像文件名随机打乱后切三段。random.seed(42)固定随机源保证之后重跑脚本得到相同划分。复制而非移动是为了保留原始素材便于后续追查问题。注意如果原始文件名包含拍摄批次、植株编号等信息或者你手头有“同一棵树多张连拍”的现场记录不能直接shuffle。要按批次分组后再留出保证同一来源的图像只进train、val或test其中一个。这是后面避坑章里的高频翻车点。2.3 标注质量检查读图、统计类别、找损坏样本已标注不等于没问题。常见标注质量问题有三类类别目录里混入其他物种、图片损坏、类名拼写不统一。下面这段脚本做两项基础检查统计每个类别的图片数量并找出打不开的损坏文件。from PIL import Image import os counts {} broken [] for cls_name in os.listdir(src_root): cls_dir os.path.join(src_root, cls_name) if not os.path.isdir(cls_dir): continue counts[cls_name] 0 for fname in os.listdir(cls_dir): full_path os.path.join(cls_dir, fname) try: with Image.open(full_path) as im: im.verify() counts[cls_name] 1 except Exception: broken.append(full_path) print(类别统计:, counts) print(疑似损坏:, broken)逻辑说明im.verify()会读取文件结构但不完整解码所有像素遇到截断或格式伪装的文件会抛异常正好用来筛选损坏样本。类别统计结果出来之后数量明显偏少的类要单独标记后面训练时给更高权重或更多增强。损坏文件不要直接删除复制到一个broken/目录里检查清楚是什么原因再处理避免误删。除此之外还要检查“标对了没有”。细粒度数据里最容易混的是两个外观很接近的品种我一般会从每类随机抽50张拼成一张九宫格肉眼过一遍。如果发现只有一两个类别大量互混说明原始标注在采集时就错了靠训练是救不回来的只能重新清洗或剔除。2.4 类别不均衡的应对先统计再决定加权还是采样统计结果出来后通常会看到类别数量不均衡。两个常见处理思路给损失函数加权或者用采样器让每个batch尽量均衡。max_count max(counts.values()) class_weights {cls: max_count / cnt for cls, cnt in counts.items()} class_names list(counts.keys()) weight_vector torch.tensor([class_weights[c] for c in class_names]) criterion torch.nn.CrossEntropyLoss(weightweight_vector)逻辑说明反频权重的含义是“样本越少的类单样本梯度贡献越大”适合中等程度不均衡比如3倍以内的差距。如果少数类只有几十张加权容易让模型反复看同一批图导致对少数类过拟合此时优先考虑数据增强或更强的预训练特征。重采样的话在DataLoader里挂WeightedRandomSampler让每个epoch抽样时多抽少数类效果和损失加权接近但batch内的数据多样性更好。3. 用预训练ResNet50在本地跑通桑叶分类训练代码与参数调法数据准备好之后训练阶段就是很多人搜的“用ResNet50小样本图像分类的整个过程详细步骤”的主干。最容易出问题的不是模型定义而是预处理、学习率和增强策略。用ImageNet预训练的ResNet50做迁移学习是这个数据量级下最稳的起点。3.1 为什么迁移学习是首选预训练权重、模型容量与数据量匹配5000张已标注数据按5个品种算每类约1000张从头训练CNN理论上可行但桑叶的纹理特征很细随机初始化很难学到叶脉、锯齿这种高频局部结构。ImageNet预训练权重提供了大量纹理基元在叶片这类自然物体上迁移效果通常不错。ResNet50的定位是“容量适中”比ResNet18表达能力强比ResNet152更容易过拟合显存占用和训练时长都比较合理。EfficientNet系列也可作为备选适合愿意多调增强参数的读者。一句话选型结论先跑ResNet50做baseline再换ViT验证上限这也是第4章要做的事。3.2 从零实现一个最小训练流程数据加载、数据增强、微调import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import transforms, datasets from torchvision.models import resnet50, ResNet50_Weights device cuda if torch.cuda.is_available() else cpu train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.3), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform transforms.Compose([ transforms.Resize(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(sangye_splits/train, transformtrain_transform) val_ds datasets.ImageFolder(sangye_splits/val, transformval_transform) class_names train_ds.classes train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) model resnet50(weightsResNet50_Weights.IMAGENET1K_V2) model.fc nn.Linear(model.fc.in_features, len(class_names)) 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) best_acc 0.0 for epoch in range(40): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) logits model(imgs) loss criterion(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() model.eval() correct 0 total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) val_acc correct / total if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_sangye_resnet50.pth) print(fepoch {epoch}: val_acc {val_acc:.4f} *)逻辑说明RandomResizedCrop的scale(0.6, 1.0)表示随机裁剪面积占原图面积的60%到100%对桑叶这种非正方形物体来说比固定resize更能保留叶片实体如果图像背景占比高可以把下限降到0.5。val_transform先Resize(256)再CenterCrop(224)保持评估时的平移不变性不做随机增强。model.fc替换成品种数输出这是迁移学习的标准动作因为ImageNet自带的1000类分类头在桑叶数据集上没有意义。优化器用AdamW配合weight_decay1e-4比SGD少操心Momentum调参当前数据量下微调很顺手。学习率1e-4是全量微调的常见起点比从头训练低一个数量级。CosineAnnealingLR的T_max40与总epoch数一致让学习率从1e-4平滑退火到接近0。如果显存不够把batch_size降到16学习率同步降到5e-5。提示scheduler.step()在每轮末尾调用。如果换用ReduceLROnPlateau这类需要“看指标”的调度器要在验证集跑完后带着val_acc调用两种写法的位置完全不同。3.3 关键参数说明batch size、学习率、epoch怎么定参数推荐起点说明调整信号batch size325000张数据下足够支撑BN统计显存OOM就减半学习率1e-4微调阶段比从头训练低一档验证loss震荡就降到5e-5epoch30~50对应一个完整余弦周期验证指标连续不涨就早停weight_decay1e-4抑制过拟合训练/验证差距大就升到1e-3增强强度scale(0.6,1.0)农业图像背景杂乱需要更强增广过拟合信号明显就再加强如果手里只有一张消费级显卡batch size被迫调到8要注意BN统计会变抖。两个办法一是开启梯度累积每4个step更新一次参数等效于batch_size32二是先换ResNet18把流程跑通再回来调大模型。另外在脚本开头固定随机种子def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True这个不写不报错但不写的结果是“每次跑出来的指标都不一样”在对比实验阶段会浪费时间。3.4 训练曲线怎么看损失下降但验证不动时怎么办训练loss一路掉到0.1、验证准确率却停在72%不再动这是过拟合的典型画面。先看训练损失和验证损失的差差距越大过拟合越重。处理顺序建议先加强增强再加正则最后才考虑换模型。增强看第4章的mixup和随机擦除正则就是调大weight_decay换模型是把ResNet50换成ResNet18验证是不是容量过剩。反过来如果训练loss和验证loss都高说明欠拟合此时加增强反而更坏。优先提高学习率或训练轮数。每次实验把train_loss、val_loss、val_acc按epoch存成CSV画出两条曲线曲线比任何trick都直观。我自己会把训练和验证损失之差当作训练是否健康的硬指标绝不只看准确率。4. 从CNN换到ViT小样本图像分类的选型对比与数据增强最新的图像分类模型榜单上ViT相关结构的出镜率已经超过CNN但它不一定适合所有数据规模。这一章重点讲清楚ViT在5000张级别数据上处于什么位置分类头到底要怎么调整以及哪些增强手段真正有用。4.1 ViT在5000张级别数据上的真实定位数据效率与特征容量ViT从头训练时数据效率低因为它没有CNN那种内置的局部性和平移等变性空间结构都得靠数据去学。用预训练ViT做迁移学习情况会改观但微调时仍然比CNN更容易过拟合。对照关系大致如下模型参数量近似在小规模数据上的表现倾向适合场景ResNet5025M数据效率高不容易过拟合优先跑出的baselineEfficientNet-S约5~8M需要精细调增强收益有限轻量化部署ViT-B/1686M特征容量大但吃数据靠预训练兜底想验证模型上限时做对照结论是在5000张已标注数据上不要一上来就押注ViT。先跑CNN基线拿到一组可靠指标再用ViT做对照实验。如果ViT有强预训练权重比如在更大量级数据上预训练过的版本它有可能超过ResNet50但这个“有可能”必须在自家验证集上确认不能拿公开榜单的结论硬套。4.2 用torchvision加载预训练ViT并处理分类头迁移时分类头用调整吗from torchvision.models import vit_b_16, ViT_B_16_Weights vit vit_b_16(weightsViT_B_16_Weights.IMAGENET1K_V1) vit.heads.head torch.nn.Linear(vit.heads.head.in_features, len(class_names))逻辑说明torchvision的ViT把分类头部放在heads.head字段替换成桑叶品种数即可。in_features通常等于768这是ViT-B/16的token embedding维度。直接回答高频问题用ViT评估时分类头肯定要调整。预训练权重自带的是ImageNet的1000类输出不改会直接报维度不匹配。更隐蔽的是第二个问题新分类头是随机初始化的。如果一上来就全量微调随机头的梯度会连带污染预训练主体。常见做法是先冻结主体只训新分类头几个epoch再解冻后半段做全量微调。这个“先线性探测、再微调”的顺序在ViT上比ResNet50更关键。ViT输入默认是224×224。torchvision也支持更高分辨率版本但会明显增加显存和训练时间。在第3章CNN基线跑完之前不建议直接上高分辨率当真正常用的细纹理区分不开时再考虑ViT加更大输入尺寸的组合。4.3 数据增强加码mixup、CutMix 与随机擦除的落地写法小样本图像分类能起死回生的地方数据增强排在第一位。mixup把两张图按比例线性插值标签也跟着混合能有效阻止模型死记单张图。下面是完整的mixup实现可以直接挂进第3章的训练循环。import numpy as np import torch def mixup(x, y, alpha0.2): if alpha 0: lam np.random.beta(alpha, alpha) else: lam 1.0 batch_size x.size(0) idx torch.randperm(batch_size).to(x.device) x_mixed lam * x (1 - lam) * x[idx] y_mixed lam * y (1 - lam) * y[idx] return x_mixed, y_mixed # 在训练循环里使用: # for imgs, labels in train_loader: # imgs, labels mixup(imgs, labels, alpha0.2) # logits model(imgs)逻辑说明lam服从Beta(α, α)α越小混合程度越轻α0.2是比较保守的起点。y_mixed不再是整数标签而是两个类别之间的软标签CrossEntropyLoss能够直接处理这种分布不影响反向传播。注意只能对训练集做mixup验证集和测试集必须保持原始数据。CutMix则把一块矩形区域替换成另一张图的切片标签按面积比例混合对“叶片重叠、部分遮挡”的场景更贴近但实现稍复杂。优先级建议先给train_transform加RandomErasing(p0.25, scale(0.02, 0.1))跑一组对比再上mixup最后试CutMix每次只改一项避免多个因素混在一起看不出谁起作用。4.4 如果数据只有几十张1-shot/5-shot的处理思路后续如果要扩新的桑叶品种新类别可能只给几张参考图这时普通微调很难起作用已经进入小样本图像分类1-shot/5-shot的范畴。常见做法有两个方向一是用预训练模型倒数第二层输出的特征向量作为图像表示把参考图特征存成原型新样本通过余弦相似度或最近邻来分类二是对原型做简单线性分类头。这两类方法都不需要大量训练但桑叶品种间差异过细容易混淆特征提取器的质量比分类方法更重要。如果整个数据集只有几十张另一个思路是用自监督预训练模型或强监督预训练模型抽特征然后冻结特征做线性探测这比端到端微调更稳。5000张已标注数据足够走端到端微调但理解这条链路能帮你判断“到底该补数据还是该换方法”。5. 桑叶品种分类的5个踩坑记录从过拟合到标注错位下面这些坑是农业细分类任务里的共性问题换到桑叶品种数据集上大概率也能遇到。每条按“现象 → 原因 → 解决”的顺序列出来训练出问题时直接对照排查。5.1 训练损失一路下降验证准确率原地踏步现象训练loss降到0.1以下验证准确率停在70%上下不动模型看起来“学得很好”但验证集就是不给面子。原因数据增强太弱模型把训练集的背景细节背下来了。桑叶数据集如果拍摄背景单一模型很容易去记桌面木纹、标签笔迹这类背景线索而不是叶片本身。解决把RandomResizedCrop的scale下限降到0.5加入RandomErasing(p0.25)同时把weight_decay从1e-4提到1e-3。如果还压不住换ResNet18跑一组对比低成本判断到底是容量问题还是增强不够。5.2 验证集95%换环境立刻崩现象验证集准确率能到95%换到另一个场地、另一批拍摄条件下直接掉到60%以下。原因典型的“兄弟图”泄漏。同一棵植株的连拍图、同一批拍摄的连续帧被随机shuffle到了train和val两个集合模型在验证集上见过太多相似图指标虚高一到独立数据就露馅。解决划分数据前先按拍摄批次、植株编号或目录名分组保证同一来源的图片只进入train/val/test中的一个集合。代码层面可以先把样本按组聚合再用sklearn.model_selection.GroupShuffleSplit按组切分比逐文件随机shuffle可靠得多。5.3 少数品种几乎不识别现象混淆矩阵里多数类acc能做到90%小众类recall接近0所有叶片都被预测成大类。原因类别不均衡普通CrossEntropyLoss被高频类别主导。小类别在总损失里占比太低梯度更新方向基本只看多数类。解决先跑一遍类别统计最大类与最小类差距超过3倍就考虑给CrossEntropyLoss的weight参数传反频权重或改用WeightedRandomSampler。如果少数类本身只有几十张先靠增强和预训练特征撑不要一上来就猛加权否则模型会对这几张图过拟合。5.4 直接缩到224把纹理细节压没了现象所有图片直接用Resize(224, 224)塞进模型准确率怎么调都上不去换更强的模型也没用。原因桑叶品种差异集中在叶缘锯齿、叶脉密度这类高频纹理强行把整张图等比压成224×224这些细节被插值算法抹掉了。解决改用Resize(256)再CenterCrop(224)保留中心区域的更多细节或者用RandomResizedCrop并把scale下限设在0.6以上。显存允许时把输入尺寸提到320再做对比细粒度任务里分辨率提升带来的收益经常比换模型更明显。5.5 模型学的是背景而不是叶片现象准确率看着不错把Grad-CAM热区画出来一看就翻车高亮全在纸面、桌沿和花盆边缘叶片区域反而是冷的。原因数据集采集环境太统一模型学会了“背景捷径”。只要换背景或摆放姿态模型立刻失效。解决先剔除背景占比过高、叶柄或标签入画的样本增强阶段增加随机平移、旋转和亮度扰动打破背景一致性。这类问题靠acc发现不了必须结合第6章的可视化验证手段来揪。6. 用混淆矩阵与Grad-CAM验证模型落地前的最后一关这一章不写训练只做验证。桑叶品种分类模型交到农技站或上线之前至少跑两件事混淆矩阵和Grad-CAM。前者回答“哪两个品种最容易互相认错”后者回答“模型到底在看叶子的哪个部分”。混淆矩阵的生成很简单拿第3章训练好的模型对val集做一次推理from sklearn.metrics import confusion_matrix, classification_report import torch all_preds, all_labels [], [] model.eval() with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) preds model(imgs).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_namesclass_names)) print(confusion_matrix(all_labels, all_preds))一眼扫对角线之外的最大混淆对。桑叶品种里最常见的是“叶缘锯齿深的A品种被错认成B品种”说明全局RGB特征不够接下来可以加局部裁剪图作为第二路输入或者提高输入分辨率。分类报告里的macro avg比整体acc更能反映小类别表现。Grad-CAM用最简实现就够看热区不需要额外库。选ResNet50的最后一个卷积层钩住它的输出和梯度import torch.nn.functional as F grads, feats [], [] model.layer4[-1].register_forward_hook( lambda m, i, o: feats.append(o)) model.layer4[-1].register_full_backward_hook( lambda m, gi, go: grads.append(go[0])) model.eval() img val_transform(example_image).to(device) out model(img.unsqueeze(0)) target out.argmax(dim1) model.zero_grad() out[0, target].backward() weights grads[0].mean(dim(2, 3), keepdimTrue) cam (weights * feats[0]).sum(dim1, keepdimTrue).relu() cam F.interpolate(cam, size(224, 224), modebilinear, align_cornersFalse) cam cam.squeeze().detach().cpu().numpy()逻辑说明最后一个卷积层的输出代表模型学到的空间特征梯度全局平均得到每个通道的重要性加权求和后经过ReLU去掉负响应再上采样到输入分辨率。这就是Grad-CAM的核心思路。看热区位置落在叶脉和锯齿上是好信号落在桌面、标签、花盆上就是第5.5节的背景捷径。如果背景热区明显说明数据采集条件太干净需要补一批不同背景的样本。我的习惯是每次训练结束后先跑这两个图再决定下一步是调参、补数据还是直接换模型。这套验证流程成本极低但能把“凭感觉相信acc”变成“知道模型哪里强哪里弱”。第一次做时觉得麻烦多做两次就会发现它省下的试错时间远多于这点付出希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询