图像分类实战:基于ConvNeXt与迁移学习的11类果蔬识别

图像分类实战:基于ConvNeXt与迁移学习的11类果蔬识别 简介图像分类是计算机视觉领域的核心任务其目标是对图像内容进行语义识别在工业质检、智能零售、农产品分选等场景中应用广泛。传统方法依赖手工特征而基于卷积神经网络CNN的深度学习模型能够自动提取层次化特征成为当前主流方案。在实际工程中直接从头训练CNN往往需要海量数据和算力因此迁移学习成为关键策略利用在ImageNet上预训练的模型权重在特定任务上进行微调可以显著降低数据需求并加速收敛。ConvNeXt作为新一代纯卷积网络吸收了Transformer的设计理念在保持训练稳定性的同时具备更强的特征表达能力适合作为中小型数据集的分类骨干。本文以11种常见果蔬的图像识别为例完整展示了基于PyTorch的数据集构建、数据增强、模型加载、分类头替换、混合精度训练与调优流程并分享了解决过拟合、类别不均衡等实际问题的经验。 先交代一下背景。这个项目是我前段时间在做的一个人工智能图像分类任务基于 ConvNeXt 网络对 11 种常见水果和食物做图像识别。说白了就是喂一张图片给程序程序告诉你它属于哪一类比如苹果、香蕉、面包、鸡蛋、洋葱、土豆等等。项目里包含完整的数据集构建流程、训练代码、推理脚本和部署建议我从数据准备到模型微调一路跑下来积累了不少实战经验这里一次性整理出来。如果你是刚入门图像识别的同学或者正在准备自己的分类项目但被网络选型、数据组织、训练调参这些琐事卡住这篇文章应该能帮你省不少时间。我会把为什么选 ConvNeXt、数据集怎么整理、完整代码的关键部分怎么写、训练时踩过的坑怎么排全部拆开讲清楚。内容不是教科书式的理论堆砌而是我实际操作时的完整记录。1. 为什么选 ConvNeXt 这件事值得好好掰扯1.1 需求倒推网络结构做这类小规模分类任务第一件事不是找代码而是想清楚网络怎么选。市面上的分类模型太多了ResNet、EfficientNet、Vision Transformer、Swin Transformer、ConvNeXt 各有拥趸。我在这个项目开始时面临的选择是11 类水果和食物每类大概几百到一千张图数据量不算大但也不是玩具级。先排除了 ViT 和 Swin 这类 Transformer 模型。不是说它们不好而是中小型数据集上纯 Transformer 的训练非常容易过拟合学习率、数据增强、正则化每一项都要仔细调折腾成本很高。它们能在 ImageNet 上大放异彩很大程度依赖 1400 万张图像的预训练换到几万张图的场景优势完全发挥不出来。ResNet 当然能跑但作为 2015 年的结构它在参数效率、感受野设计、训练技巧上已经明显落后了。EfficientNet 精度不错但它的训练对超参数相当敏感NAS 搜出来的结构一旦改动就容易翻车。最后我把目光锁定在 ConvNeXt 上——它是 Meta 在 2022 年提出的纯 CNN 结构吸收了大量 Transformer 的设计思路却保留了卷积网络的训练稳定性和迁移友好性配合 ImageNet 预训练权重做微调在小数据集上表现非常稳。1.2 ConvNeXt 到底改了什么ConvNeXt 的出发点其实很朴素既然 Swin Transformer 能在视觉任务上打赢 CNN那能不能把 Swin 的先进设计理念搬回纯卷积网络里让 CNN 也享受同样的红利它做到了而且做的很巧妙。改动点可以概括成几个关键操作。首先是 stem 部分把 ResNet 里经典的 7×7 卷积接最大池化的组合换成了 4×4、步长为 4 的卷积这一步被称为 patchify灵感来自 ViT 的 patch embedding好处是下采样在一步内完成且保留了更清晰的局部信息。其次是网络各阶段的 block 数量比例。ResNet 各层通常按 3:4:6:3 分配ConvNeXt 则按照 Swin 的分配方式调整为 3:3:9:3也就是把计算量更多倾斜到更深的阶段因为深层特征对分类的贡献更大。再往下是整个 block 的结构重设计用 7×7 的深度可分离卷积替代传统卷积把通道数先扩展 4 倍再压缩回去形成倒瓶颈结构用 LayerNorm 替代 BatchNorm激活函数从 ReLU 换成 GELU最后加上 LayerScale对每个通道的输出做可学习缩放初始值设的非常小让网络在训练初期更稳定。这些改动单独拿出来每一个都不是什么震撼发明但组合在一起的效果非常显著。我在实际训练中最直观的感受是同样的数据、同样的超参数ConvNeXt 的收敛稳定性和最终精度都明显优于 ResNet 和 EfficientNet而且不用做太多额外调参。2. 数据集怎么建比模型选型更磨人2.1 数据和类别确定这个项目的任务对象是 11 类水果和食物。我实际选用的是下列类别类别说明苹果红苹果为主少量青苹果香蕉整根香蕉与切段香蕉橙子完整果与切瓣果葡萄整串与单粒葡萄若干草莓带蒂与去蒂草莓面包吐司、法棍、欧包形态鸡蛋白壳与褐壳鸡蛋胡萝卜整根与切段胡萝卜番茄红番茄与圣女果洋葱紫皮与黄皮洋葱土豆完整的土豆与切块选择这 11 类是因为它们形态差异适中既有容易混淆的胡萝卜和土豆又有颜色差异明显的草莓和面包能较好地检验模型的真实区分能力。你在做自己的项目时完全可以根据需求替换类别比如换成食品包装袋品牌、不同品种的植物叶片等等。数据来源方面建议优先从开源数据集平台获取比如 Kaggle 上的 Fruits 360、Food-101 子集也可以自己拍摄补充。我最终混合使用了 Fruits 360 的部分子集和自己拍摄的补充照片。自己拍的时候注意一点尽量覆盖不同光线、不同背景、不同拍摄角度。模型能不能在真实场景下扛得住很大程度取决于训练数据的多样性而不是单纯的数量。2.2 目录结构和标签划分方案数据集的目录组织直接决定后续代码怎么写。我强烈建议按 PyTorch 的 ImageFolder 格式组织这是最省事的方案打通训练代码完全不需要自己写标签映射。各个目录结构如下dataset/ ├── train/ │ ├── apple/ │ │ ├── apple_001.jpg │ │ ├── apple_002.jpg │ │ └── ... │ ├── banana/ │ ├── bread/ │ ├── carrot/ │ ├── egg/ │ ├── grape/ │ ├── onion/ │ ├── orange/ │ ├── potato/ │ ├── strawberry/ │ └── tomato/ ├── val/ │ └── (同 train 结构) └── test/ └── (同 train 结构)划分比例我采用的是 8:1:1也就是训练集 80%验证集 10%测试集 10%。验证集用于训练过程中的模型选择和早停判断测试集则模拟“从未见过的新照片”做最终评估。这个比例对中小数据集来说是稳妥的选择不建议测试集占比太低否则最终评估结果的可信度会打折扣。我在实际操作中发现了一个容易被忽略的问题不同类别的样本数量要尽量均衡。如果苹果有 1500 张洋葱只有 300 张模型会不自觉偏向苹果类因为哪怕不认识的东西猜苹果也有比较大的概率蒙对。遇到这种情况可以采用两种手段一是对少数类做复制增强把图像做随机旋转、裁剪、水平翻转后重新加入数据集二是在训练时使用加权采样器让样本数量少的类别获得更高的被采样概率。2.3 图像预处理和数据增强图像统一处理是整个 pipeline 里很容易被忽视的环节。我的做法是把所有训练图片统一 Resize 到 256×256然后随机裁剪到 224×224验证集和测试集则直接 Resize 到 224×224。这里有个关键点必须说明输入尺寸选择 224是因为预训练模型都是在 ImageNet 的 224×224 上训练的保持一致的输入尺寸能让迁移学习的效果最大化。如果直接用自己的随机尺寸去跑预训练模型模型第一层卷积的感受野还是那个感受野但输入图像的语义尺度完全错位了最终的微调效果会明显打折。我在一开始测试过 384 分辨率效果确实略有提升但显存占用直接翻倍训练时间也拉长了不少。对于 11 类这种简单分类224 完全够用。训练阶段我还用了比较强的数据增强策略代码用的是 torchvision.transforms.v2from torchvision import transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.1), 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), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize 的均值和标准差用的是 ImageNet 统计值不能乱改。这是配合预训练权重的核心操作改了相当于人为改变输入分布预训练的效果直接归零。RandomResizedCrop 的 scale 参数我调到了 0.6 到 1.0比默认的 0.08 到 1.0 范围小一些。因为水果和食物往往是整图出现过度裁剪容易裁掉关键部位导致模型学到残缺特征。3. 核心代码实现从模型加载到训练流程3.1 模型加载和分类头替换torchvision 在 0.13 版本开始就内置了 ConvNeXt 的官方实现包括 tiny、small、base、large 等不同规格。这意味着基本不需要自己从头写结构直接加载预训练权重替换分类头即可。我的实验选择的是 tiny 版本理由是11 类分类任务的复杂度不高tiny 的参数量约 28M已经远大于 ResNet50 的 25M在中小数据集上有足够的拟合能力base 版本虽然性能更强但训练时间和显存占用会明显上升。如果你想要更高精度后续可以直接把 tiny 换成 base代码几乎不用改。下面是模型加载代码import torch import torch.nn as nn from torchvision.models import convnext_tiny, ConvNeXt_Tiny_Weights def build_model(num_classes11, freeze_backboneFalse): weights ConvNeXt_Tiny_Weights.IMAGENET1K_V1 model convnext_tiny(weightsweights) # 替换分类头原分类头是 768 - 1000改成 768 - num_classes in_features model.classifier[2].in_features model.classifier[2] nn.Linear(in_features, num_classes) # 选项冻结主干网络只训练分类头 if freeze_backbone: for param in model.features.parameters(): param.requires_grad False return model冻结主干网络的策略我有话要说在数据量极小每类几十张的情况下冻结主干只训练分类头能有效防止过拟合但在数据量中等每类几百张以上时全量微调的效果通常更好。我在项目初期试过冻结策略验证集准确率大概在 90% 左右放开全量微调后准确率直接提升了 3 到 4 个百分点。所以我最终的方案是全量微调但初始学习率设得比较保守。3.2 训练脚本的关键配置训练配置上优化器我选的是 AdamW 而不是传统的 SGD。ConvNeXt 论文和社区实践都表明这个结构配合 AdamW 的收敛速度明显更快而且对学习率的敏感度更低。学习率初始值设为 1e-4这个值对于迁移学习微调来说非常合适——比从零训练的 1e-3 要低一个数量级避免在预训练权重的基础上步子迈得太大导致灾难性遗忘。Loss 函数用的是 CrossEntropyLoss并打开了 label smoothing设置为 0.1。Label smoothing 的作用是防止模型对训练集过于自信把预测概率推向 0 或 1 的极端值提升泛化能力。实际效果是验证集准确率稳定提升了约 0.5 个百分点代价几乎为零。学习率调度上我采用了 cosine annealing 配合前 5 个 epoch 的线性 warmup。Warmup 阶段的必要性在于虽然加载了预训练权重但新替换的分类头是随机初始化的这部分参数的梯度在初期会非常大直接跑大学习率容易把网络搞乱。先让网络在小学习率下跑几个 epoch 热身再进入正常的余弦衰减整体收敛会平稳很多。混合精度训练我强烈推荐开启。PyTorch 2.x 的做法如下from torch.cuda.amp import GradScaler, autocast scaler GradScaler() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()开启混合精度后训练速度能提升 40% 到 50%显存占用减少一半左右。对于小数据集训练这不仅能节约时间还允许你开更大的 batch size。Batch size 我最终定为 64在 224×224 分辨率下单张 RTX 3060 或 2070 就能轻松跑起来。3.3 完整训练循环和关键超参数速查训练循环本身并不复杂但有几个细节值得注意。每个 epoch 结束后我会同时计算训练集和验证集的 loss 和准确率把验证集准确率最高的模型权重保存下来。注意保存的是 state_dict 而不是整个模型这样后续加载时更灵活也不容易因为 PyTorch 版本升级而踩坑。best_val_acc 0.0 for epoch in range(epochs): model.train() train_loss 0.0 train_correct 0 train_total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() train_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) train_correct (preds labels).sum().item() train_total labels.size(0) # 验证 model.eval() val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) val_correct (preds labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth)我常用的一组超参数整理成速查表方便你直接套用参数数值说明输入尺寸224×224与预训练一致Batch Size64显存不够时调 32优化器AdamWlr1e-4weight_decay5e-2学习率调度Cosine Warmupwarmup 5 epochsLossCrossEntropyLosslabel smoothing0.1Epochs40配合早停策略混合精度开启显著加速4. 训练过程中遇到的那些坑4.1 报错和解决方案速查第一个坑是加载预训练权重时 shape 不匹配。很多人第一次跑会看到类似size mismatch for classifier.2.weight的报错这是因为 torchvision 的预训练模型是 1000 类而你的任务是 11 类。解决方案就是在加载模型后手动替换最后一层我在上面代码里已经处理了。这类问题其实不需要恐慌它本质上是正常的结构替换。第二个坑是 CUDA 显存不足报错通常是CUDA out of memory。常见解决办法是按顺序尝试把 batch size 从 64 降到 32 或 16开启混合精度如果还是不够把输入尺寸从 224 降到 192 或 176但精度会有轻微下降。我实测下来单张 8GB 显存的显卡用 224 分辨率训练 ConvNeXt-Tinybatch size 16 到 32 是可以稳定运行的。第三个坑是训练集 loss 下降但验证集 loss 不降反升这说明过拟合了。解决办法优先级是这样排列的先加大数据增强强度尤其是 RandomResizedCrop 和 ColorJitter再降低学习率并加长 warmup然后考虑冻结部分底层卷积层最后才是增加 L2 正则化或 dropout。注意数据增强永远是对抗过拟合的第一选择因为它在增加数据多样性的同时不削弱模型表达能力。第四个坑是类别不均衡导致的准确率虚高。我遇到的情况是数据量最大的类别占总数 25%所以哪怕模型把所有图片都猜那一类整体准确率也有 25%。但显然这不是我们想要的。检查方法很简单看混淆矩阵的主对角线之外是不是有大量预测偏向某一类。解决手段就是前面提到的加权采样或者对少数类做更强的针对性增强。4.2 验证集性能评估光看一个准确率数字远远不够。我在最终评估时还会做三件事计算每一类的 precision、recall、F1 分数绘制混淆矩阵并可视化特征空间。混淆矩阵尤其能揭示问题。我在第一版训练完的混淆矩阵里发现胡萝卜和土豆存在明显的互混——两者颜色、形状都比较接近薯类表面有凸起纹理胡萝卜有尖锥形状但单纯从某个角度拍摄的照片确实容易混淆。定位到具体问题后我在训练数据里增加了切块形态的胡萝卜和土豆照片并且特意调整了亮度增强的强度最终这两类之间的混淆度明显下降。t-SNE 特征可视化是一个很好用的诊断工具。把模型的倒数第二层特征提取出来降维到二维平面上画散点图如果各个类别的点云能明显分开说明特征提取阶段已经学到了不错的语义如果某些类别的点云大面积重叠那就是网络没能区分这些类别需要回到数据和增强策略上去找原因。4.3 推理脚本的编写训练完成后最终目标是把模型用在真实图片上。我写了一个简单的单图推理函数核心代码如下from PIL import Image import torchvision.transforms.functional as TF def predict_image(image_path, model, class_names, device, image_size224): image Image.open(image_path).convert(RGB) image TF.resize(image, (image_size, image_size)) image TF.to_tensor(image) image TF.normalize(image, mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) image image.unsqueeze(0).to(device) model.eval() with torch.no_grad(): outputs model(image) probs torch.softmax(outputs, dim1) top1_prob, top1_idx torch.max(probs, 1) return class_names[top1_idx.item()], top1_prob.item()注意这里推理时不能用训练时的随机裁剪而是直接 Resize 到 224×224。训练和推理的数据预处理不一致是很多新手会犯的错会导致精度大幅下降。我之前有次发现准确率从验证集的 95% 掉到推理时的 85%排查了很久才发现在推理流程里多了一步随机翻转。5. 调优经验与后续扩展思路5.1 把准确率从 90% 拉到 95% 的实际操作第一版训练完成后我的验证集准确率大概在 91% 到 92% 之间作为一个快速实现的原型已经能用了但我不太满意。后来我在两周里做了一系列调整把准确率稳定提到 95% 以上。第一步是调整数据增强策略。把 RandomResizedCrop 的 scale 范围从 0.08~1.0 改为 0.6~1.0增加了整图保留的概率加强 ColorJitter 的饱和度扰动因为不同成熟度的水果颜色差异很大模型需要学会忽略颜色细节而关注更稳定的形状和纹理特征。第二步是重新审视学习率。原先的固定 1e-4 在全量微调阶段还可以但到训练后期会出现验证集 loss 震荡。我改成 cosine 衰减后loss 曲线明显平滑了验证集准确率提升了 1 个百分点左右。第三步是关键引入 MixUp 和 CutMix。这两个混合增强算法并不是只能在大型数据集上使用在中小型数据集上效果其实更好。实现上可以用 torchvision.transforms.v2 提供的 MixUp 接口也可以手动实现。我用 CutMix 后模型对遮挡和裁剪的鲁棒性明显增强验证集准确率从 93% 跳到 95%。最后是类别平衡的处理。我给样本量较少的类别做了过采样也就是在 data loader 里通过 WeightedRandomSampler 实现。这个操作让洋葱和胡萝卜这两类原本准确率偏低的数据分别提升了约 6 个百分点。5.2 模型部署和任务迁移方向做完分类模型后自然想到的是怎么把它用起来。如果要部署到移动端或者树莓派上跑常见做法是把它导出成 ONNX 格式再转化成 TensorRT 或 CoreML推理时延可以在毫秒级。ONNX 导出在 PyTorch 里的操作非常标准dummy_input torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, convnext_tiny.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, opset_version17 )这个模型也能很自然地迁移到检测或分割任务。因为 ConvNeXt 是通用的视觉特征提取网络你可以把它作为 YOLOv8 的 backbone或者换到 Mask R-CNN 上做实例分割。在数据量足够的情况下这样做往往比从头训练 ResNet 或 CSPDarknet 有更好的特征表示能力。我在另一个项目里把 ConvNeXt-Tiny 换成 YOLOv8 的 backbone 后目标检测的 mAP 提升了约 2 个百分点代价只是推理速度略微下降。一点个人体会整个项目跑下来我最深的感受是图像分类这种任务真正决定模型上限的是数据质量和数据组织方式而不是模型结构本身。ConvNeXt 当然是一个很强的主干网络但它只是把你能达到的精度上限抬高了一点如果数据集本身就存在大量噪声、类别不平衡、背景单一化的问题再强的网络也救不回来。所以如果你正准备做类似的项目我建议把至少一半的时间花在数据整理上而不是急着跑代码。另外训练过程中一定养成记录每次实验的习惯哪怕只是简单笔记本记一条“第 5 次实验改了增强的 scale 参数验证集涨了 0.8%”。这些看似不起眼的记录在后期调参时能给你省下大量重复实验的时间。希望这套思路和代码能帮你少走弯路。本文还有配套的精品资源点击获取