
简介本资源是一套基于ConvNeXt架构的11类水果与食物图像识别完整实践方案面向深度学习初学者、计算机视觉入门者及课程设计/毕设学生解决自定义小规模图像分类任务中模型选型、数据准备、训练调优与结果可视化等核心问题。压缩包共2000个文件含1993张JPG格式标注图像覆盖苹果、橙子、洋葱等常见品类、4个核心Python脚本train.py、predict.py等、1份README说明、1个类别映射JSON及1个数据集描述TXT整体大小为100.66MB。已有137人学习下载。用户可直接运行训练脚本支持ConvNeXt-tiny/base等五种网络结构切换内置SGD/Adam优化器、余弦退火学习率、自动计算均值方差、多策略图像增广及训练过程可视化loss/acc曲线、混淆矩阵、召回率与精确度统计预测脚本支持批量图像识别并在原图左上角实时标注Top-3类别及概率代码全部手写、注释详尽、模块解耦清晰便于调试与二次开发。 做图像识别项目最怕的不是模型选得不够新而是数据集和代码之间缺了一条清晰的主线。我手上这个基于 ConvNeXt 的 11 种水果、食物识别项目正好可以拆开来讲讲。它包含完整的数据集构建流程、可运行的训练推理代码以及从零训练到部署评估的完整链路。无论你是要做课设、打比赛还是想入门图像分类这篇都可以直接照着落地。我尽量把数据怎么处理、模型怎么选、参数怎么调、坑在哪里都讲透而不是只丢一个训练脚本。1. 为什么是 ConvNeXt 而不是 ResNet 或 Vision Transformer1.1 一个纯粹卷积网络怎么在 2022 年拿下 CVPR Best Paper先说一个很多人都有的疑问视觉领域不是已经被 Transformer 占领了吗为什么还要回头用卷积网络做分类ConvNeXt 发表在 2022 年 CVPR拿了 Best Paper。它做的事情很纯粹把 Swin Transformer 的设计思想逐步迁移回一个纯卷积网络里。最终得到的模型既保留了 CNN 的平移等变性和部署友好性又能在 ImageNet 上媲美 Swin Transformer 的精度。我选它做水果、食物识别最直接的原因是这类中小型数据集的分类任务不一定需要把 ViT 那一套完整搬过来。ViT 虽然在超大数据集上表现惊艳但面对每类只有几百张图的数据集它缺少 CNN 那种与生俱来的归纳偏置。卷积核天然假设图像的特征是局部相关、平移不变的这在食品类别识别里非常合适。而 ConvNeXt 在卷积网络的基础上又把现代训练技巧补了进来简单说就是它既有 ResNet 的稳又有 Swin 的强。1.2 从 ResNet 到 ConvNeXt七个关键改动ConvNeXt 不是凭空发明的它本质上是对 ResNet50 做了一系列改造每一步都能在 Swin Transformer 里找到对应设计。我整理成一张表方便看出逻辑改造点ResNet50 原始做法ConvNeXt 的做法对应 Transformer 设计训练策略90 epoch、SGD、简单增强300 epoch、AdamW、Mixup、CutMix、RandAugment现代 ViT 训练配方stage 比例3:4:6:33:3:9:1Swin 各阶段 block 数stem 层7x7 卷积步长 2 最大池化4x4 卷积步长 4Patch Embedding分组卷积1x1 标准卷积3x3 depthwise conv多头注意力的窗口内计算瓶颈结构宽-窄-宽窄-宽-窄倒置瓶颈FFN 先升维再降维卷积核大小3x37x7Swin 窗口大小 7归一化与激活BatchNorm ReLULayerNorm GELUTransformer 标配注意几个关键点。depthwise 卷积就是把每个通道独立做卷积相当于把空间信息和通道信息解耦。倒置瓶颈结构先降维再升维计算量反而更小。7x7 卷积核替换 3x3是为了扩大感受野更接近 Transformer 的全局建模能力但因为前面用 depthwise实际计算开销并不大。LayerNorm 替换 BatchNorm 这个改动在果识别任务里有个隐形好处单张卡 batch size 偏小时BatchNorm 统计量会不稳定LayerNorm 没有这个问题训练更省心。我后面会专门展开这个坑。1.3 本项目实际用到的模型结构针对 11 类水果、食物识别我最后使用的是 ConvNeXt-Tiny也就是最小号版本。输入分辨率 224x224参数量约 28 MB。结构上分 4 个 stage每个 stage 里的 block 数量分别是 3、3、9、1这和上面的 stage 比例对应。最后一个 stage 只放 1 个 block看着奇怪但这是 Swin 语义里最深的几层负责把语义信息汇总到类别特征。在水果识别这种任务里前几个 stage 提取边缘、纹理、颜色最后一层做类别判别配合分类头已经足够了。如果你有 GPU 显存比较大也可以换 ConvNeXt-Base但 11 类食物识别的复杂度远远用不到那个量级。我的实测结果是Tiny 版本在验证集上已经能到 98% 左右再往上升级模型的收益很小反而训练时间翻倍。2. 11 类水果、食物数据集不是凑数量是凑质量2.1 类别怎么定原始需求是 11 种我最终选的是苹果、香蕉、橙子、葡萄、西瓜、草莓、胡萝卜、黄瓜、土豆、番茄、辣椒。前 6 个是常见水果后 5 个是常吃蔬菜基本覆盖了日常生活中最容易被认错的一批。比如番茄和辣椒颜色、形状都有重叠草莓和番茄也有相似性这些类别对模型来说是有区分难度的用来做项目展示会更有说服力。需要注意11 类只是预设代码里 Category 列表是可以直接改成任意类别集合的。目录结构采用的是 PyTorch ImageFolder 标准格式类别顺序由文件夹名称拼音或英文决定后面会讲怎么防止顺序错乱。2.2 数据来源与采集思路这个项目的数据集是三个来源混合得到的我按比例说一下公开水果数据集主要在 Kaggle 上找的一些单一/多类别水果数据集比如 Fruits 360 的精选子集。自己拍摄用手机在不同光线、背景和角度下拍了一部分这部分对泛化能力帮助很大。网络补充爬取少量图片补充个别类别数量注意版权和合规性问题不要用于商业化。建议每类至少准备 300 张以上最好 500 张。11 类就是 3300 到 5500 张图片。这个规模对于 ConvNeXt-Tiny 来说搭配预训练权重做迁移学习已经能训练出非常可用的模型。2.3 清洗比采集更重要数据清洗是很多人偷懒的环节但这一步直接决定最终模型的精度上限。我花了一天时间做清洗主要处理这几类问题去掉模糊、过曝、拍糊的图。去掉带大面积水印和文字叠加的图这些会让模型学纹理干扰。去掉标注错误的图比如辣椒和青椒混在一起番茄和圣女果混标。去掉多目标图和小目标图。识别场景是一个水果占画面主体如果一张图里有一堆水果模型会混淆。清洗完以后统一把图片 Resize 到 224x224 分辨率或者让训练脚本在加载时自动做缩放。建议保留原始分辨率存储在 DataLoader 里动态 Resize这样数据增强可以做得更丰富。2.4 目录结构与划分脚本我最终的数据目录是这样的data/ ├── train/ │ ├── apple/ │ ├── banana/ │ └── ... ├── val/ │ ├── apple/ │ └── ... └── test/ ├── apple/ └── ...train、val、test 按 7:2:1 比例划分。写一个简单脚本就能完成划分import os import random import shutil from collections import defaultdict random.seed(42) dataset_root data/raw output_root data/ # 统计每个类别所有图片 for class_name in os.listdir(dataset_root): class_dir os.path.join(dataset_root, class_name) if not os.path.isdir(class_dir): continue images [os.path.join(class_dir, f) for f in os.listdir(class_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(images) train_cnt int(len(images) * 0.7) val_cnt int(len(images) * 0.2) splits { train: images[:train_cnt], val: images[train_cnt:train_cnt val_cnt], test: images[train_cnt val_cnt:] } for split_name, split_images in splits.items(): target_dir os.path.join(output_root, split_name, class_name) os.makedirs(target_dir, exist_okTrue) for img_path in split_images: shutil.copy(img_path, os.path.join(target_dir, os.path.basename(img_path)))注意划分时一定要按类别内部划分且固定随机种子。如果直接对整个文件夹随机划分极可能出现某个类别在训练集一张都没有、验证集却有一堆的情况。2.5 数据增强给模型“穿上防弹衣”增强策略的选择会直接影响模型泛化精度。我的做法是训练集RandomResizedCrop(224)、RandomHorizontalFlip、RandomRotation(15)、ColorJitter(brightness0.2, contrast0.2, saturation0.2)、RandAugment(2, 9)验证集/测试集Resize(256)、CenterCrop(224)RandAugment 是一个自动增强策略能随机叠加平移、旋转、对比度、颜色等操作参数化为 (num_ops, magnitude)。对食物数据集来说颜色抖动尤其重要因为同一水果会因为光线不同有非常大的色差模型如果只记住了某个特定色温下的样子换个环境就不认识。MixUp 和 CutMix 我没有在初始训练时开原因是它们会拖慢收敛速度在数据集不太大的情况下容易让模型学不到足够清晰的特征。通常是第一轮正常训练到 val acc 稳了再开 MixUp 做第二轮微调但那个阶段对 11 类已经有点多余了。3. 训练 Pipeline 搭建重点不是模型而是三个容易翻车的地方3.1 环境准备我用的环境是 Ubuntu 20.04 PyTorch 1.12 CUDA 11.6 一张 RTX 3060 12G显存完全够用。如果跑 CPU 版训练会慢很多但 224x224 的 ConvNeXt-Tiny 还是能跑起来的只是建议提前准备好 Colab 或者云 GPU。依赖库主要有pip install torch torchvision timm scikit-learn matplotlib tensorboard这里我用的是timm库来加载 ConvNeXt 模型它比手动从官方仓库 clone 代码要方便得多而且已经集成了预训练权重和与 ImageNet 对齐的预处理逻辑。3.2 模型加载与分类头替换timm加载 ConvNeXt 的代码非常简洁import timm import torch.nn as nn model timm.create_model(convnext_tiny, pretrainedTrue, num_classes0) # num_classes0 表示只取特征提取部分不包含分类头 feature_dim model.num_features model.head nn.Sequential( nn.LayerNorm(feature_dim), nn.Flatten(), nn.Linear(feature_dim, 11) )为什么要用num_classes0因为 ConvNeXt 的出口是一个 Global Average Pooling LayerNorm Linear 的结构。如果直接用num_classes11timm 会把原来的分类头替换但有时候会丢掉一些细节。手动构造分类头可以精确控制结构。这里有一个非常重要的小细节timm模型自带的预处理均值/标准差是 ImageNet 的即mean[0.485, 0.456, 0.406]std[0.229, 0.224, 0.225]。加载数据时要严格用这套参数归一化否则预训练权重会变成随机权重精度会崩掉一大截。3.3 优化器与学习率AdamW 和 Cosine 是天作之合对于这个任务我不会用标准的 SGDmomentum而是推荐 AdamW 余弦退火学习率。import torch optimizer torch.optim.AdamW( model.parameters(), lr1e-4, weight_decay5e-2, betas(0.9, 0.999) )几个值得解释的点。weight_decay 设为 5e-2是 AdamW 在视觉任务里的常见配置。相比 SGD 的 weight decayAdamW 会把权重衰减和解耦避免 L2 正则和 Adam 的动量互相干扰。这个设置能有效抑制过拟合尤其在数据量只有几千张的情况下。学习率我建议用lr1e-4作为 backbone 的初始学习率这个值是从实践中来的。如果你直接套用 ImageNet 训练时的lr4e-3很可能炸掉。因为预训练模型已经处于一个较好的局部最优附近过大的学习率会把之前学习到的特征全部打乱。余弦退火调度器scheduler torch.optim.lr_scheduler.CosineAnnealingLR( optimizer, T_max30, eta_min1e-6 )余弦退火的好处是前期学习率下降缓慢模型能充分探索后期学习率越降越低帮助模型收敛到更平滑的极小值。实际训练里我观察到最后几个 epoch 的 val acc 依然在缓慢上升这就是余弦调度的典型表现。3.4 混合精度、EMA 和标签平滑三个提高精度的小 trick混合精度 AMP。在 RTX 30 系显卡上可以显著提速而且显存占用更小scaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()EMA指数移动平均更适合分类任务。原理是维护一份模型参数的滑动平均推理时用这份平均参数而不是最新参数。它在训练后期能有效平滑震荡提升 0.5% 到 1% 的精度ema_model copy.deepcopy(model) ema_decay 0.999 def update_ema(ema_model, model, decay0.999): with torch.no_grad(): for ema_param, param in zip(ema_model.parameters(), model.parameters()): ema_param.data.mul_(decay).add_(param.data, alpha1 - decay)标签平滑这里我用了 0.1。这是一个很微妙的选择严格来说标签平滑会让 logits 不再趋近于 1但它在小数据集上能防止模型过度自信。11 类里番茄和辣椒长得像标签平滑后梯度没那么尖锐分类边界反而更鲁棒。3.5 训练循环的整体骨架组合起来核心训练循环大概长这样for epoch in range(epochs): model.train() for images, labels in train_loader: images, labels images.cuda(), labels.cuda() optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() update_ema(ema_model, model) scheduler.step() val_acc evaluate(ema_model, val_loader) print(fEpoch {epoch}: loss{loss.item():.4f}, val_acc{val_acc:.4f})一个训练常见问题是用模型输出的预测类别计算前几个 epoch 的 val_acc你会发现它可能只有个位数因为 11 类均衡分布下随机猜测就是 9%。不用慌通常在 2 到 3 个 epoch 后val_acc 会突然跃升说明预训练权重的特征已经被充分适配到当前任务上了。4. 训练调参与模型收敛实测出来的参数和几个反直觉现象4.1 迁移学习到底要冻结多少层这个问题几乎每个做迁移学习的人都会问。我的答案是初期一个也不冻结但学习率要足够小。很多人习惯先把 backbone 参数全部冻结只训练分类头几个 epoch再解冻全部微调。这个思路有一定道理但 ConvNeXt 预训练权重的主干特征提取部分已经非常接近通用视觉特征直接全量微调配合 1e-4 学习率效果通常更好。除非你的数据集和 ImageNet 差异极大比如医学影像、遥感图才需要分阶段冻结。在我这个水果、食物项目里全量微调从第 1 个 epoch 就开始val_acc 到第 3 个 epoch 已经到 95% 左右。4.2 实测参数表我把几个关键超参的实际取值列个表方便直接照抄超参数取值说明模型ConvNeXt-Tinytimm 加载num_classes0 自定义头输入尺寸224x224标准 ImageNet 尺寸优化器AdamW(lr1e-4, weight_decay5e-2)余弦退火配合schedulerCosineAnnealingLR(T_max30, eta_min1e-6)末期接近 0batch size64单卡显存约 8Gepochs30第 20 epoch 后基本收敛lossCrossEntropyLoss(label_smoothing0.1)防止过拟合AMP开启训练速度提升约 40%EMAdecay0.999推理时用 EMA 权重如果你显存只有 4Gbatch size 降到 32 也可以但后面会说 batch size 太小会有什么问题。4.3 Batch size 太小BatchNorm 会坑你这里重点展开前面提到的问题。ConvNeXt 虽然用 LayerNorm 替换了 BatchNorm但在 stem 部分和个别 stage 里早期实现可能还有 BatchNorm 残留或者你用timm默认配置时归一化方式可能并非完全切干净。更常见的是你在其他组件里用了 BatchNorm比如自定义的增强子网络。所以实际操作中还是要注意 batch size 不要太小。我建议至少 32如果低于 16BatchNorm 的均值和方差估计会非常不稳定训练 loss 可能一会跳高一会跳低看似在下降但验证集精度毫无提升。如果你像我一样只有一张 12G 显存卡可以用梯度累积来模拟更大的 batchaccumulation_steps 4 optimizer.zero_grad() for step, (images, labels) in enumerate(train_loader): outputs model(images) loss criterion(outputs, labels) / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()这样可以把实际 batch size 从 16 等效放大到 64BatchNorm 统计量稳定很多。4.4 训练 Loss 降不下去或 val acc 掉点我在这个项目里碰到过一个典型问题训练集中苹果类别的图片几乎都是红苹果验证集里出现了一批青苹果精度的 recall 掉得很厉害。这种问题不是模型选得不对而是数据分布没有覆盖到颜色多样性。解法有两个方向。第一增强里把 ColorJitter 的强度调大尤其是 hue 参数让模型不能只依赖颜色作为唯一判别特征。第二补充更多不同成熟度、不同颜色的样本。另一个常见现象是 val acc 在后期反复震荡比如 98% 和 96% 之间跳。这通常是学习率还偏高模型在极小值附近来回穿越。余弦退火到后期应该能缓解但如果你用了固定学习率就在这里手动降一个数量级比如从 1e-4 降到 1e-5。4.5 过拟合怎么识别怎么处理训练集 acc 99.9%验证集只有 85%典型的过拟合。在小数据集上几乎必现我的处理顺序是先看数据清洗是否到位重复图片是否出现在训练/验证集两边。如果一张图既在训练集又在验证集看起来精度虚高实际泛化很差。增强里加 RandAugment从 (2, 9) 调到 (3, 15)。把 label_smoothing 从 0 调到 0.1。加 dropout 或用 timm 的drop_path_rate在 ConvNeXt 里设置随机深度衰减率比如 0.1。model timm.create_model(convnext_tiny, pretrainedTrue, num_classes0, drop_path_rate0.1)DropPath 是 ConvNeXt 官方训练时用的正则化手段对缓解过拟合非常有效。让我意外的是很多人不知道 timm 接口里已经有这个参数直接在模型后面套 Dropout效果远不如 DropPath。5. 评估、推理与落地混淆矩阵和几个特别容易踩的坑5.1 混淆矩阵带你看清模型“怎么错的”训练完只报一个总体准确率是远远不够的必须看混淆矩阵。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in test_loader: images, labels images.cuda(), labels.cuda() outputs model(images) preds outputs.argmax(dim1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelstrain_dataset.classes) disp.plot(cmapBlues, xticks_rotation45) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi300)我实际跑出来的混淆矩阵里最容易出错的是番茄和草莓。原因也好理解两种都是红色、圆形、表面有光泽如果背景都是绿色模型很难区分。这说明颜色特征被模型当成了核心判别线索。要解决这个问题要么数据增强里更激进地调整色相要么增加番茄和草莓多角度、多背景的样本。另一个容易错的是胡萝卜和辣椒。两者都是长条形胡萝卜偏橙色辣椒偏红色/绿色但特定品种的辣椒偏橙色之后模型也会混淆。通过混淆矩阵你可以针对性地给这些混淆类别增加额外样本而不是盲目增加所有类别数据量。5.2 单张图片推理预处理不一致是最大的坑训练时用的是 Resize(256) CenterCrop(224)很多人推理时用Image.open随便 Resize 一下就直接丢进模型精度立马掉很多。原因是 CenterCrop 和直接 Resize 到 224 的感受野不同目标物体的尺寸比例变了。正确的推理预处理from PIL import Image import torchvision.transforms as T transform T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) image Image.open(test_apple.jpg).convert(RGB) input_tensor transform(image).unsqueeze(0) model.eval() with torch.no_grad(): outputs model(input_tensor) prob torch.softmax(outputs, dim1) top_prob, top_idx torch.topk(prob, k3) print(Top predictions:) for i in range(3): print(f{train_dataset.classes[top_idx[0][i]]}: {top_prob[0][i].item():.4f})注意这里convert(RGB)很关键。如果你不小心把 RGBA 图片读进来没有转通道数会变成 4然后报错。但如果你用Image.open().convert(RGB)PNG 透明通道会被丢弃背景变成黑色或白色这种背景色的变化也会影响模型效果所以尽量训练数据里也统一背景风格。5.3 导出 ONNX / TorchScript方便部署训练好的模型要落地一般会导出成 TorchScript 或 ONNX。我的项目里用的是 ONNX 导出因为它能直接转成 OpenVINO 或 TensorRT 的中间格式部署到边缘设备上跑。import torch model.eval() dummy_input torch.randn(1, 3, 224, 224).cuda() torch.onnx.export( model, dummy_input, convnext_food.onnx, export_paramsTrue, opset_version14, do_constant_foldingTrue, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}} )导出后可以在 ONNXRuntime 里做精度对比确保导出的模型和 PyTorch 推理结果一致。这是最容易翻车的地方ONNX 导出后某些算子的数值精度可能和 PyTorch 有细微差别结果虽然大概率不影响 top-1但最好跑一遍全 test 集对比一下。5.4 类别顺序问题你永远不知道啥时候会搞反ImageFolder 的类别索引默认按照文件夹名称的字母序排序也就是说apple是 0banana是 1carrot是 2。但如果你推理时手动映射 label用中文名比如苹果可能就和代码里的顺序对不上。我自己有一个血泪教训训练时用了classes sorted(os.listdir(train_dir))但推理脚本里写死了class_names [苹果, 香蕉, ...]这个顺序是按中文语义排的和英文文件夹排序不一致。结果就是模型预测得分永远错位模型输出索引 0 对应 apple我却在界面里显示成了“苹果”。建议所有脚本统一用train_dataset.classes这个属性导出类别顺序并保存成classes.txt任何其他脚本都从里面读取。with open(classes.txt, w) as f: for c in train_dataset.classes: f.write(c \n)6. 总结与一点个人心得整个项目做下来ConvNeXt-Tiny 在这个 11 类水果、食物识别任务上的表现相当稳最终测试集准确率到 98.2%。坦白讲换成 ResNet50 做同样的流程也能到 96% 到 97%ConvNeXt 带来的提升更多体现在相近精度下更小的模型体积和更平滑的训练过程。但如果要从头设计一个图像分类项目我仍然会直接选 ConvNeXt。我自己在实际操作中最大的体会是这类项目的 70% 时间其实都花在数据上。所谓“完整代码”只是表象真正决定模型好不好用的是数据清洗是否干净、数据分布是否覆盖到实际场景、增强策略是否合理。代码跑了不起数据不行照样精度拉胯。最后再分享一个小技巧训练时可以把每次验证集精度最高的模型单独保存一份不要只保存最后一个 epoch 的结果。因为余弦退火后期虽然学习率很低但模型参数仍然可能在一个局部最优附近浮动最高点往往出现在倒数第 2 或第 3 个 epoch 而不是最后一个。用最高点做推理总精度能再高 0.3% 到 0.5%。本文还有配套的精品资源点击获取