尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

快餐图像分类实战:用ConvNeXt迁移学习与PyTorch微调

发布时间:2026/9/10 12:10:56

资讯中心
01
ARTICLE

快餐图像分类实战:用ConvNeXt迁移学习与PyTorch微调

快餐图像分类实战:用ConvNeXt迁移学习与PyTorch微调
简介面向图像分类与迁移学习场景这份PyTorch实现资源提供了ConvNeXt网络的完整图像识别源码覆盖tiny、small、base、large、xlarge五种规格可供不同算力与精度需求者选用。包内共2000个文件以快餐图像分类数据集为主1994张jpg附带4个Python训练/评估脚本、1个说明txt及readme文件整体压缩后约483MB。代码实现了数据预处理、随机翻转与裁剪等数据增强、网络构建、预训练权重载入、训练与评估全流程并通过混淆矩阵、召回率、精确度、特异度及loss/acc等指标监控模型表现有效防止过拟合。资源还包含可直接运行的图像数据集支持从数据准备到模型验证的闭环实验readme文件提供了详细实现参考便于快速上手。目前已有207人学习既适用于复现ConvNeXt在快餐食品分类上的迁移学习实验也可作为自定义图像识别任务的工程基线。1. 用ConvNeXt给快餐图像分类迁移学习为什么是正解快餐图像分类数据集有一个很磨人的特点类别少且外观高度相似汉堡和肉卷、炸鸡和鸡米花、薯条和薯角往往只在纹理细节上有区分日常拍摄还会叠加偏色、遮挡和餐盘反光。如果用随机初始化的卷积网络从头训练几百张到几千张的数据量很难收敛出足够鲁棒的语义特征训练曲线也容易出现震荡。这个场景的正解是在ImageNet上预训练的骨干网络上做迁移学习保留它已经学到的通用视觉结构再用快餐数据集把最后几层和部分骨干做微调。ConvNeXt是这里值得优先选的骨干模型它在保持纯卷积框架的同时吸收了Transformer的设计思路预训练权重对纹理密集的小型数据集迁移效果比同量级ResNet更好训练成本和显存占用又低于同精度ViT。本文按“架构理解 → 数据管线 → PyTorch实现 → 训练参数 → 验证与导出”的路径把快餐图像分类完整落地。2. 从ResNet到ConvNeXt先理解它改了什么再决定要不要换2.1 一张表看懂ConvNeXt的七项关键设计ConvNeXt的核心思路是把Vision Transformer的成功经验逐条搬回纯卷积网络但保留卷积操作在平移等变性和计算效率上的天然优势。它并不是从零设计的新结构而是对ResNet-50做逐项现代化改造得到的基线模型。理解这点很重要因为迁移学习里我们用到的是完整预训练结构而不是拆开单点套用。改动位置ResNet-50原做法ConvNeXt做法对微型数据集迁移的影响Stem下采样块7x7卷积stride24x4卷积stride4更早压缩空间分辨率减少后续计算量下采样模块主分支stride2的3x3卷积2x2卷积stride2后接LayerNorm下采样位置规整减少信息混叠卷积方式标准3x3卷积3x3深度卷积加1x1逐点卷积参数更少空间与通道信息解耦瓶颈结构常规1x1-3x3-1x1反向瓶颈1x1-3x3-1x1深层特征表达能力更强卷积核大小3x37x7感受野更大利于区分相邻食物区域归一化层BatchNormLayerNorm摆脱batch size依赖微调更稳定激活函数ReLUGELU梯度更平滑微调阶段更鲁棒需要强调这七项是一条完整的设计链不是可以随意单取的技巧。例如只把ReLU换成GELU却保留BatchNorm在迁移学习微调时反而可能因为数值分布变化导致掉点。我自己做迁移学习时一般直接复用完整预训练结构不做架构上的删改只替换最后的分类头。2.2 为什么在迁移学习场景下ConvNeXt比ResNet更有性价比在快餐图像这种下游数据集上选骨干模型核心看三个指标预训练特征的分层质量、特征提取时的显存开销和推理延迟、微调时对超参数的敏感程度。ConvNeXt在这三点上都有明显优势。第一7x7大核深度卷积在模型早期就能接触到更大范围的上下文信息快餐图像的典型构图是“食物主体加餐盘背景”大核卷积比3x3更容易把主体和背景解耦。第二反向瓶颈结构把通道数先放大再压回来信息瓶颈集中在通道维上迁移时只微调最后几个Block就能恢复到不错的基线效果。第三因为保留了卷积的归纳偏置它收敛速度远快于同精度的ViT不需要照搬ViT那套三百轮的长训练设置一般预训练权重迁移过来十几轮微调就能看到效果。2.3 ConvNeXt与EfficientNetV2-S怎么选这里顺便把和ConvNeXt常被一起对比的EfficientNetV2-S也讲清楚因为很多人在快餐分类这类任务上会纠结这两个模型。EfficientNetV2-S的优势是推理速度更快它把3x3卷积和SE注意力融合成Fused-MBConv块结构更紧凑更适合移动端部署。但它的Squeeze-and-Excitation模块引入了全局池化后的通道注意力分支在小数据集微调时这个注意力分支对学习率更敏感学习率稍高就容易震荡。在ImageNet精度上ConvNeXt-T比EfficientNetV2-S更高迁移到纹理密集的数据集时更容易保住形状和边缘信息。快餐分类里大量区分点就集中在细节纹理上比如汉堡面包上的芝麻、炸鸡表面的面糊裂纹这类特征确实更适合用ConvNeXt来保留。结论是如果部署环境对延迟极其敏感选EfficientNetV2-S如果目标是把分类准确率做到更高且训练和推理都在GPU上进行选ConvNeXt更稳妥。3. 快餐数据集准备与PyTorch数据管线搭建3.1 数据集目录结构怎么摆放最省事这里给出一个能让torchvision.datasets.ImageFolder直接读取的目录结构省去手写CSV标注文件的麻烦。food_dataset/ ├── train/ │ ├── burger/ │ │ ├── 001.jpg │ │ └── 002.jpg │ ├── fries/ │ ├── fried_chicken/ │ └── pizza/ ├── val/ │ ├── burger/ │ └── ... └── test/ └── ...每个类别一个文件夹子目录名就是标签名。ImageFolder会自动把文件夹名映射成从0开始的整数索引。动手之前先做一次类别数量统计如果发现某些类图片数量差异过大以最少类别为基准对多数类做下采样不要让模型因为类别不平衡而偏向高频类。3.2 训练集和验证集的transform必须分开配置训练阶段和验证阶段的图像预处理必须使用不同策略这一点常被忽略导致验证指标虚高或训练不收敛。给出一份可复用的配置from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), 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]) ])参数说明RandomResizedCrop的scale(0.6, 1.0)是我针对快餐图像调整过的值默认的(0.08, 1.0)裁剪比例范围太大容易把食物主体大部分裁掉快餐主体通常占据画面较大比例所以把缩放下限提高到0.6。ColorJitter三个通道的扰动幅度控制在0.2以内避免炸鸡这类食物偏色太严重否则模型会学会用颜色而不是纹理做判断。Normalize用ImageNet的均值和标准差迁移学习训练时不要改这组参数因为预训练权重是在这套输入分布下学出来的换成自建数据集的mean/std反而会破坏分布一致性。3.3 用DataLoader边读边解码的完整写法数据加载是快餐图像训练里非常容易成为瓶颈的环节。尤其是jpg格式图片数量多如果只开默认的num_workers0GPU会在每个step等待CPU完成解码和增强训练吞吐量掉得很明显。这里给一份直接能用的DataLoader配置from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_ds ImageFolder(data/food_dataset/train, transformtrain_transform) val_ds ImageFolder(data/food_dataset/val, transformval_transform) train_loader DataLoader( train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue, drop_lastTrue, ) val_loader DataLoader( val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue, drop_lastFalse, ) print(类别映射:, train_ds.class_to_idx)参数说明batch_size32针对单卡训练ConvNeXt-T在224x224输入下每个样本约占2GB显存除以batch size的量32对大多数16GB显存的显卡是舒适区。num_workers4意味着有4个进程并行做图像解码和数据增强不再占用主进程时间。pin_memoryTrue把数据固定在页锁定内存中减少从CPU传到GPU的拷贝延迟。drop_lastTrue只用于训练集丢弃最后一个不完整的batch避免BatchNorm统计量在最后一个小batch上产生波动验证集要设置drop_lastFalse保证所有验证样本都被评估。打印class_to_idx可以确认文件夹名到数字标签的映射是否正确。在Windows环境下num_workers设得过高会触发多次spawn导致内存溢出建议降到2或者把整体训练代码放进if __name__ __main__:保护块中再执行。4. 用PyTorch加载ConvNeXt预训练权重并替换分类头4.1 最小加载代码搭建模型、替换分类头PyTorch的torchvision里已经内置了ConvNeXt的预训练权重不需要手动逐层搭建网络。加载时要分两步走先拿到完整模型和一个可用的官方权重再替换掉最后的分类层。这里给出标准写法import torch import torch.nn as nn from torchvision import models net models.convnext_tiny(weightsmodels.ConvNeXt_Tiny_Weights.IMAGENET1K_V1) num_features net.classifier[2].in_features net.classifier[2] nn.Linear(num_features, num_classes)说明weightsmodels.ConvNeXt_Tiny_Weights.IMAGENET1K_V1会从torchvision官方下载地址自动拉取在ImageNet-1K上训练好的权重并按照模型结构完成参数加载。官方ConvNeXt实现里classifier是一个Sequential容器最后一项才是全连接层因此需要替换的是net.classifier[2]。in_features必须从原线性层读出不能写死因为不同规格的ConvNeXt最后的通道数不同Tiny是768Small是768Base是1024。num_classes按你的快餐数据集设定比如有6个类就填6。4.2 冻结骨干网络分阶段微调策略迁移学习里最容易犯的错误是一上来就全量微调。快餐数据量通常只有几百到几千张全量微调会让随机初始化的分类头在前几个epoch里产生很大的梯度噪声反向传播到骨干层后反而破坏了预训练权重学到的有效特征。更稳的做法是两阶段微调先冻结骨干只训练分类头等分类头收敛到稳定状态后再解冻骨干做小学习率微调。下面给出冻结与解冻的标准代码# 阶段一冻结全部骨干只训练分类头 for name, param in net.named_parameters(): param.requires_grad False for param in net.classifier.parameters(): param.requires_grad True # 阶段二解冻全部参数 for name, param in net.named_parameters(): param.requires_grad True说明阶段一通常训练5轮左右把分类头从随机状态拉到一个合理区间。阶段二再解冻所有参数但骨干的学习率要调得比分类头低一个数量级这个在第5章会展开讲。需要留意的是ConvNeXt骨干里的LayerNorm参数在解冻后也会更新它们数量小但负责特征分布的重标定让模型适应快餐图像与ImageNet分布的差异保留更新是有益的。4.3 显存不够时的梯度检查点方案ConvNeXt的7x7深度卷积会在计算图上保存较大的中间激活。如果显存实在紧张又不想缩batch_size可以用梯度检查点技术用时间换显存。下面是把所有CNBlock前向改为checkpoint版本的代码from torch.utils.checkpoint import checkpoint from torchvision.models.convnext import CNBlock def ckpt_forward(self, x): return checkpoint(self.forward, x, use_reentrantFalse) for module in net.modules(): if isinstance(module, CNBlock): module.forward ckpt_forward.__get__(module, CNBlock)说明checkpoint在前向传播时不保存中间激活反向传播时重新计算一遍显存占用可以下降一个量级代价是训练时间增加约20%到30%。use_reentrantFalse是PyTorch 2.0之后推荐的写法老版本需要改成True。这个方案适合显存紧但又想验证基线结果的场景正式训练大数据量时还是直接缩小batch_size更划算因为重计算带来的额外耗时在大规模数据上会被放大。5. 训练循环与关键参数设置让迁移学习稳定收敛5.1 优化器、学习率与调度器的搭配训练ConvNeXt微调模型优化器优先选AdamW这和原版ConvNeXt训练策略一致。学习率设置上有一个很实用的原则骨干层和分类头要分开设置骨干层用2e-5分类头用2e-4分类头比骨干高一个数量级。原因在于骨干层在ImageNet上已经学到通用特征只需要极小步长在局部做调整分类头是从随机初始化开始的需要更大的步长快速收敛。weight_decay也要分组建AdamW的weight_decay和L2正则效果不等价在分类头上设置0.01骨干上设置0.05因为预训练特征不想被过度惩罚。下面是一份直接可用的参分组配置import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR param_groups [ {params: net.classifier.parameters(), lr: 2e-4, weight_decay: 0.01}, {params: [p for n, p in net.named_parameters() if not n.startswith(classifier)], lr: 2e-5, weight_decay: 0.05}, ] optimizer optim.AdamW(param_groups, betas(0.9, 0.999), eps1e-8) scheduler CosineAnnealingLR(optimizer, T_max10, eta_min1e-6)说明CosineAnnealingLR把学习率按余弦曲线从初始值逐渐降到eta_min前几轮学得快后面缓慢收敛比起分段下降的step scheduler省去了手动调衰减节点的麻烦。T_max10对应阶段二的10轮训练。阶段二解冻参数后要重新构建一次优化器并挂上新的scheduler不要沿用阶段一的优化器状态否则学习率已经被余弦曲线压低过一轮后段会几乎不动。还有一个细节是把LayerNorm的gamma和beta参数排除在weight_decay之外但因为它们数量很少当训练轮数不长时不排除也影响不大。5.2 训练循环骨架混合精度和梯度累积ConvNeXt包含大量卷积矩阵乘法非常适合用混合精度训练加速。PyTorch 2.0以后的推荐写法是torch.amp.autocast配合GradScaler。下面是一个完整的单epoch训练函数def train_one_epoch(net, loader, optimizer, criterion, device, scalerNone, grad_accum1): net.train() total_loss, total_correct, total_num 0.0, 0, 0 optimizer.zero_grad() for i, (images, labels) in enumerate(loader): images, labels images.to(device), labels.to(device) with torch.amp.autocast(cuda, dtypetorch.float16): logits net(images) loss criterion(logits, labels) / grad_accum scaler.scale(loss).backward() if (i 1) % grad_accum 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad() total_loss loss.item() * grad_accum * images.size(0) total_correct (logits.argmax(dim1) labels).sum().item() total_num images.size(0) return total_loss / total_num, total_correct / total_num参数说明scaler.scale(loss).backward()先把loss放大再反传防止float16下小梯度被直接置零scaler.step(optimizer)内部会检测本轮梯度是否溢出溢出时自动跳过参数更新。grad_accum是梯度累积步数当显存只允许batch_size16但你想达到32的有效batch大小时设置grad_accum2每两个batch更新一次参数。注意loss要用grad_accum归一化否则累积梯度的量级会随累积步数线性放大导致学习率需要重新调整。5.3 损失函数与类别不平衡处理快餐图像分类常用交叉熵损失。如果数据集类别分布不均比如汉堡类有500张、沙拉类只有80张可以在CrossEntropyLoss里传入每个类别的权重让少数类的梯度贡献更大counts torch.tensor([200, 50, 80, 120, 40, 100], dtypetorch.float) weights 1.0 / counts weights weights / weights.sum() * len(counts) criterion nn.CrossEntropyLoss(weightweights.to(device))说明weights取类别样本数的倒数然后做归一化确保loss整体量级不会被拉大。这种做法等价于对少数类做了过采样但优点是改动集中在损失函数上不需要改动DataLoader的采样逻辑。需要注意的是过高的少数类权重会牺牲多数类精确率训练结束后要看每个类别的F1分数不能只看总体准确率。如果数据量本身就小建议优先尝试不加权重的交叉熵把类别不平衡放到验证阶段用F1评估后再决定是否需要加权。6. 用验证集检查混淆、导出模型、写单图预测函数6.1 用验证集输出每个类别的精确率和召回率训练结束后只观察总准确率是远远不够的要逐类检查。快餐分类里最常见的失败模式是“薯条和鸡块混淆严重、汉堡和肉卷互相误判”这时总准确率可能还有80%但某个类别的召回率已经跌到50%了。下面这个函数直接输出每个类别的精确率和召回率def evaluate(net, loader, device, num_classes6): net.eval() confusion torch.zeros(num_classes, num_classes, dtypetorch.long) with torch.inference_mode(): for images, labels in loader: images, labels images.to(device), labels.to(device) logits net(images) preds logits.argmax(dim1) for t, p in zip(labels.cpu(), preds.cpu()): confusion[t, p] 1 for i in range(num_classes): tp confusion[i, i].item() total_gt confusion[i].sum().item() total_pred confusion[:, i].sum().item() recall tp / total_gt if total_gt else 0 precision tp / total_pred if total_pred else 0 print(f类别{i}: 精确率{precision:.3f} 召回率{recall:.3f}) return confusion confusion evaluate(net, val_loader, device)说明混淆矩阵的下标是[真实标签, 预测标签]对角线是预测正确的数量。通过total_gt和total_pred分别计算召回率和精确率。两者都低的类别说明模型普遍漏检精确率低但召回率高说明模型把大量别的类别误判成了这一类需要对应检查数据增强里是否缺少这类样本。6.2 把权重、类别映射和transform一起导出部署阶段最容易出的问题是只保存了state_dict却在加载时忘了类别映射和对应的预处理参数。把下面三个东西打包保存才算完整的成果交付torch.save({ state_dict: net.state_dict(), class_to_idx: train_ds.class_to_idx, transform: val_transform, }, convnext_food.pth) # 推理加载 ckpt torch.load(convnext_food.pth, map_locationcpu) model models.convnext_tiny(weightsNone) model.classifier[2] nn.Linear(768, len(ckpt[class_to_idx])) model.load_state_dict(ckpt[state_dict]) model.eval()说明map_locationcpu保证没有GPU的机器也能正常加载。保存时把val_transform对象也序列化进去部署阶段对单张图片推理时就不用再手动写一套预处理增强参数。常见的报错是load_state_dict时出现size mismatch原因通常是保存时的num_classes和加载重建模型时的len(class_to_idx)不一致检查这两处即可修复。这里线性层输入维度768对应ConvNeXt-T的最终特征维度如果你用的是Base规格需要改成1024。6.3 单张图片预测与低置信度拒识实际部署到点餐识别或后厨质检场景时预测函数还要处理真实世界里的模糊图片和无关背景图。这里给出一个完整流程from PIL import Image def predict_single(model, img_path, device, ckpt): img Image.open(img_path).convert(RGB) x ckpt[transform](img).unsqueeze(0).to(device) with torch.inference_mode(): out model(x) prob torch.softmax(out, dim1).squeeze() class_name {v: k for k, v in ckpt[class_to_idx].items()} idx int(out.argmax(dim1).item()) if prob[idx].item() 0.5: print(f预测: {class_name[idx]} 置信度: {prob[idx].item():.3f}) else: print(f无法确定最高置信度类别: {class_name[idx]}置信度仅为 {prob[idx].item():.3f})说明.convert(RGB)用于处理png的alpha通道或灰度图统一转成三通道避免通道数不一致报错。softmax得到每个类别的概率当最高概率低于0.5时建议标注为“不确定”不要硬给一个低置信度的预测结果。在真实业务里这类拒识逻辑能避免把店员误拍的餐盘背景强行归类成某种食物比强行输出一个类别实用得多。同时注意transform在验证阶段没有Resize以外的随机增强这里直接复用保存的val_transform就是正确的做法不要临时套用训练阶段的transform否则会引入随机裁剪导致预测不稳定。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。