首页 / 资讯中心 / 文章详情

卷积神经网络与迁移学习:食物图像识别实战与数据增强避坑指南

卷积神经网络与迁移学习:食物图像识别实战与数据增强避坑指南 ★ FEATURED ARTICLE
简介基于卷积神经网络的食物图像识别项目资源包面向深度学习初学者与CV开发者解决食物图片分类任务从建模到部署的完整流程问题。压缩包共32个文件总大小17.28MB主体为12个Python脚本分别承担数据预处理、模型构建、训练与测试等功能另含tfrecords样本数据、模型权重、项目配置及说明文档结构清晰便于按步骤运行。已有6701人学习。资源内容覆盖CNN卷积层、池化层、全连接层及激活函数的代码实现并包含Dish_CNN工程完整复现读者可基于自带数据训练识别意大利面、披萨、寿司等类别也可加载预训练权重快速验证效果。适合具备Python基础、希望结合TensorFlow实践图像识别的读者亦可用于课程设计或竞赛入门。1. 食物识别比“认猫”难在哪难的不是卷积是数据分布先给一个反直觉的结论在图像识别任务里认出一只猫比认出一道菜容易得多。猫有胡须、耳型、体态这些跨环境稳定的结构线索一道菜在一张照片里留下的只是盘中食材的颜色、纹理和大致切法。同一道菜换个滤镜、换个摆盘、换个光源像素分布的变化可能比不同菜之间的差异还大反过来宫保鸡丁和鱼香肉丝这种“看起来差不多”的类别又会在高层语义上互相拉扯。卷积神经网络要解决的就是从“像素太像”和“类别太近”这两重压力下找出真正可判别的视觉证据。很多人拿到一份食物图像识别项目第一反应是“网络不够深、数据不够多”实际跑起来才发现数据划分、类别不均衡、预处理一致性这些环节才是决定最终精度的主要因素。这也是这篇文章要拆开讲的东西CNN 在这类任务里到底怎么提特征迁移学习为什么有效以及哪些坑会让人反复翻车。理解这些机制比盲目再堆几层卷积都值。下面从最底层的特征提取说起。2. 卷积特征提取浅层看颜色、深层看食材感受野一层层放大2.1 卷积核在学什么从边缘、纹理到“食材部件”卷积的运算本质不复杂一个可学习的滤波器在输入特征图上滑动每个位置做一次内积得到一个新的响应值。参数共享让同一组滤波器能出现在图像的任何位置这就是“不管菜放在盘子左边还是右边同样的纹理都能被激发”的原因。但真正值得追问的是这些滤波器到底在响应什么在一个用食物数据训练好的网络里浅层卷积核学到的往往是颜色块和基础边缘。食物识别里最有用的浅层线索是“酱汁的色相”和“食材颗粒的纹理”红亮的油光、青椒的深绿、蒜末的浅黄颗粒这些视觉基元不需要语义就能区分开。中层卷积核开始把这些基元组合成部件级概念“肉块”“花生米”“辣椒段”“葱花”这是在浅层边缘之上叠加出来的组合模式。到了深层网络把部件组合成整道菜的判别结构比如“鸡丁和花生同时出现且被深色酱汁包裹”指向宫保鸡丁“肉丝配木耳和笋丝”指向鱼香肉丝。这里有个很多人会忽略的点没有任何一行代码告诉网络“什么是肉块”“什么是花生”这些概念完全由梯度反传自发组织出来。把某层 feature map 可视化或者用激活最大化(activation maximization)去放大某个滤波器最“喜欢”的输入模式你会看到清晰的分层底层是条纹和色块中层是食材碎片顶层是接近完整食物的组合。这就是表示学习的核心——特征不是人工设计的是数据分布逼出来的。这也解释了为什么食物数据集的质量直接决定特征质量网络结构反而不是瓶颈。2.2 感受野与池化平移不变性为什么对一盘菜这么重要感受野是理解 CNN 深度的第一把钥匙。它定义的是某一层输出特征图上的一个点对应输入图像上多大的区域。感受野按层递推常见公式是 rₖ rₖ₋₁ (kernel_size − 1) × stride 的累积效果。举个例子如果只用 3×3 卷积堆三层的网络最后一层的感受野只有 7×7也就是说一个输出单元只能“看”到输入图像里 7×7 像素的信息。对于食物识别这个范围只能看到“一小片红色”或“几粒米”根本看不到“肉丁配花生”这种跨区域的组合证据。池化在这里的角色常被低估。MaxPool2d 用 2×2 窗口、步长 2 做下采样特征图分辨率减半但每个输出单元对应到输入上的范围立即扩大一倍。更重要的是最大池化带来对局部平移的鲁棒性某个食材轻微偏移几个像素只要还在池化窗口内最大值基本不变。放到食物场景里就是“菜被摆歪了”“手机斜拍了”“盘子没放正”都不该影响最后的判断。这是 CNN 比全连接网络更适合图像的根本原因之一。在真实项目里ResNet、MobileNet 这类结构的参数量差异很大但骨干设计都是同一个套路前几层分辨率高、通道少保住边缘纹理后几层分辨率低、通道多扩大感受野、抽象出整体结构。判断一个网络适不适合食物识别可以先看它的感受野是否覆盖到图像中“成菜”的尺度而不用盲目追求大模型。2.3 跑通前向一个能打印每层 shape 的微型 CNN为了把上面的机制落到可验证的代码里下面这个微型 CNN 足够说明问题。它没有用任何花哨结构但前向过程完整演示了“通道数翻倍、分辨率减半”这个标准节奏。import torch import torch.nn as nn class MiniFoodCNN(nn.Module): def __init__(self, num_classes20): super().__init__() # 浅层抓颜色块、边缘分辨率高信息细 self.block1 nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 112x112 ) # 中层把边缘组合成食材部件 self.block2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 56x56 ) # 高层抽象食材组合关系感受野覆盖大半张图 self.block3 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2) # 28x28 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d(1), # 不管输入多大压成 1x1 nn.Flatten(), nn.Linear(128, num_classes) ) def forward(self, x): x self.block1(x); print(after block1:, x.shape) x self.block2(x); print(after block2:, x.shape) x self.block3(x); print(after block3:, x.shape) x self.classifier(x) return x model MiniFoodCNN(num_classes20) dummy torch.randn(1, 3, 224, 224) # 模拟一张 RGB 食物图 print(model(dummy).shape)运行这个脚本输出依次是[1, 32, 112, 112]、[1, 64, 56, 56]、[1, 128, 28, 28]最后分类器输出[1, 20]。每一组 ConvBNReLUPool 做的事情可以拆开看Conv 负责在保持分辨率的前提下提取局部模式BN 把激活分布拉回零均值附近避免梯度震荡MaxPool 丢掉一半空间精度、扩大感受野。224×224 这个输入尺寸不是随便定的。常见预训练模型都在这个分辨率下设计移动端 FP32 推理一块 224×224 的图计算量还能接受再往上提到 512×512食材纹理更清楚但训练时间和显存占用几乎翻到四倍。AdaptiveAvgPool2d 是个容易被忽略的设计它让网络接受任意输入尺寸最后统一压成 1×1 向量再接全连接层这样分类头的参数就不会因为输入尺寸变化而失效。真项目里的 ResNet 就是把上面这三个 block 重复加深加宽机制上完全一致。想验证感受野递推可以在每个 block 后面打印一下当前层能回看的输入范围你会看到最后一个 block 的输出单元实际覆盖了整张盘子。3. 数据分布是主战场细粒度差异和类别不均衡怎么吃掉精度3.1 细粒度识别为什么模型会把两道类似的菜认成同一个通用图像分类处理的是“猫 vs 狗 vs 汽车”这种类别间距很大的问题一个全局轮廓特征就够用了。食物识别是典型的细粒度识别类别之间共享大量视觉元素。宫保鸡丁和鱼香肉丝都有鸡丁、花生、葱段区别可能只在酱汁的颜色深浅和配菜的切法红烧肉和东坡肉同样是红色方块状肉块判别证据是肥瘦比例和汤汁光泽度。这意味着模型的深层输出向量在两类上几乎重合决策边界只能靠很小区域的局部证据撑开。细粒度任务里最容易出现的失败模式是模型学到“捷径”。如果训练集里辣味菜都装在红色盘子里模型很可能用“红色盘子”而不是“辣椒和花椒”做判断如果米饭照片总是配木质桌面背景纹理就成了隐性分类器。这类模型在测试集上可能精度不低但换个场景立刻崩掉。验证模型到底在看什么常用梯度加权类激活映射把决策区域叠加回原图如果高亮区域集中在菜本体之外比如盘子边缘、筷子、桌布基本可以确认模型学歪了。这也是为什么食物识别任务里“深”不一定对。更深的网络能提供更大的感受野和更细的局部抽象但在数据量不够大时深网络更容易把捷径也记住。常见做法是让网络在中等深度下配合注意力机制或更大的感受野而不是无脑加到几十层。理解了细粒度这个本质再看数据增强、迁移学习思路就清晰了一切手段都是为了让模型把注意力压到真正的食材证据上。3.2 类别不均衡用加权损失和 focal loss 修正梯度方向真实食物数据集几乎都是长尾分布米饭、面条这类主食样本可能有几千张某些地方菜只有几十张。很多人只看整体准确率比如 88%但把混淆矩阵拉出来才发现模型对头部类别几乎全对尾部类别几乎全错。机制层面看softmax 交叉熵的梯度是所有类别样本共同累加的头部类别样本多在每一步更新里占据主导尾部类别得到的有效梯度被稀释学不到稳定的判别特征。修正方向有两类。第一类是样本加权给每个类别一个权重常见取 w_c 1 / sqrt(freq_c)。取倒数的平方根而不是直接倒数是为了防止极端类别被给的权重过大、导致训练震荡freq_c 是该类别样本数占总样本数的比例。第二类是 focal loss它在标准交叉熵上乘一个调制因子 (1 − p_t)^γ其中 p_t 是模型对真实类别的预测概率。样本分得越对p_t 越接近 1调制因子越接近 0损失贡献被压低那些分不清的尾部样本p_t 小梯度就能保留下来。γ 取 2 在细粒度任务里通常比较稳。配合这两类修正评估口径也要换。整体 accuracy 会被头部类别淹没宏平均 F1、宏平均召回率对尾部类别更敏感。一个项目如果只报告 accuracy 不报告宏平均多半是有不均衡问题没处理干净。训练时监控宏平均 F1 做早停比只看 loss 曲线可靠得多。3.3 数据增强CutMix 如何逼模型看“局部证据”数据增强在食物识别里不只是防过拟合它直接改变模型学到什么。几何增强随机旋转、翻转很好理解菜不会被摆得绝对端正模型需要容忍这些变化。颜色增强要小心食物颜色是核线索HSV 做轻微扰动可以模拟不同餐厅的灯光差异但扰动太大会破坏“牛肉是红的、菜叶是绿的”这种真实语义模型反而学到错误的颜色映射。mixup 和 CutMix 这两类区域混合增强机制上更有意思。mixup 在两张图之间做像素线性插值标签也按比例线性混合强迫网络学习线性决策边界而不是记忆个别样本。CutMix 则是把 A 图里的一块矩形区域剪贴到 B 图上标签按面积比例混合。对食物识别CutMix 有一个天然优势一桌菜、一碗饭配一道菜现实里经常一张图里出现多个类别CutMix 生成的训练样本恰好模拟了这种“局部证据竞争”场景。模型不能只看全局配色必须在局部区域独立做出判断这直接缓解了 3.1 节说的细粒度证据被全局平均淹没的问题。增强强度是超参不是越大越好。随机擦除Random Erasing如果擦除面积太大可能把酱汁或配料这些判别区域整个抹掉模型被迫去学背景这就是增强失真。一个有用的检查方法把增强后的样本可视化一批拿给没做过训练的人看如果人眼都看不出这是什么菜那模型大概率也学不到有效特征。后面第 5 章会专门讲这个坑的具体排查方式。4. 迁移学习ImageNet 预训练权重为什么能在食物上 work4.1 预训练特征的两段式结构底层通用、顶层专用大规模数据集上预训练出来的卷积网络其内部特征是分层的前几层学到的是边缘、颜色统计、基础纹理这些视觉基元不依赖任何具体任务中层开始出现物体部件后几层才逐渐对应完整物体的语义。食物图像和自然图像在浅层、中层共享大量结构——它们都是同一套光学成像规则下产生的自然照片光照、反光、遮挡、纹理形成方式是一致的。所以预训练权重里那部分通用特征可以原样复用。关键是“哪一层开始变得任务相关”。对于食物识别分类头是必须换掉的原来输出 1000 个类别概率的全连接层需要改成输出自己类别数的线性层。这个新分类头是随机初始化的它是整个网络里唯一没有“经验”的部分因此训练初期它的梯度特别大。这也是为什么微调比从头训练更稳定的原因从头训练要让一个随机初始化的网络在小数据集上重新发现“边缘→纹理→部件”的完整层级数据量根本不够预训练权重已经把这条最难走的路走完了剩下只是把高层语义重新对齐到食物类别上。4.2 冻结与解冻按数据量决定主干网络的梯度开关微调不是简单地把所有参数一起训练。小数据集最怕的是随机初始化的分类头带着大梯度反向传播把主干里精心学到的通用特征冲乱。常见策略是按数据量决定梯度开关数据量冻结范围主干学习率建议几百张冻结整个主干不更新只训练分类头配合强增强几千张冻结前一半微调后一半1e-4分两阶段解冻几万张全部解冻1e-4 ~ 3e-4全量微调配早停冻结不是把参数删掉而是把参数的 requires_grad 设为 False让优化器跳过它。解冻顺序也有讲究先训分类头等分类头稳定了再打开高层高层稳定了再打开中层。下面的代码演示了最常见的两阶段实现import torchvision.models as models import torch.nn as nn backbone models.resnet18(pretrainedTrue) num_classes 20 backbone.fc nn.Linear(backbone.fc.in_features, num_classes) # 第一阶段冻结全部主干只训练分类头 for name, param in backbone.named_parameters(): if not name.startswith(fc): param.requires_grad False # 第二阶段解冻 layer4只让最顶层主干参与微调 def unfreeze_from(prefix): for name, param in backbone.named_parameters(): if name.startswith(prefix): param.requires_grad True unfreeze_from(layer4)这段代码的关键点在于 named_parameters 的命名规则。fc对应分类头layer4是 ResNet 最后一个残差阶段。解冻 layer4 后优化器需要单独给它的参数配小学习率因为它的位置最接近新分类头受随机初始化的影响最大最容易学偏。如果数据量允许解冻到 layer3学习率还要再小一点。我在实际项目里见过不少“全量解冻 统一学习率”直接翻车的情况loss 曲线一路飙升最后只能重新加载预训练权重再来这个坑在第 5 章会细说。提示冻结阶段也要对分类头用相对大一些的学习率比如 1e-3主干解冻后统一降到 1e-4。分开管理两组参数能省很多调参时间。4.3 微调参数学习率、batch size、正则项联动微调的学习率不能照搬从头训练的经验。预训练主干已经接近一个局部最优点步长太大一步就跨出去了。常见设定是主干 1e-4 量级新分类头 1e-3 量级。这种不对称是有原因的分类头是随机初始化需要相对快的速度收敛主干已经稳了只能小步修整。不这样做主干特征会在几个 epoch 内被大梯度“洗掉”模型退化成一个从头训练的小网络。warmup 在微调里尤其重要。刚开头的几个 step随机分类头的梯度大直接贴上去会干扰主干预训练特征。先用 35 个 epoch 把学习率从接近 0 线性升到目标值让分类头先站稳再让主干做精细调整。反向的 cosine 退火在后期也很有用学习率平滑降到很低让模型在局部最优附近收敛而不是在边界震荡。优化器方面SGD 带动量在微调任务里仍然稳但 AdamW 配合 cosine schedule 在细粒度任务里越来越常见。batch size 的影响比想象中大BN 层的统计量依赖 batch 的多样性batch 太小比如 8统计量噪声大模型容易震荡显存允许时尽量用 32 或 64。但这三条是联动的大 batch 配大学习率是通用的 scaling rule小 batch 配大学习率基本必炸。最后的兜底是早停监控验证集的宏平均 F1连续多个 epoch 不涨就停而不是死磕训练集 loss。5. 常见问题排查食物识别训练中的五个翻车现场5.1 验证集精度虚高一上测试就崩数据泄漏现象验证集准确率 98%训练完信心满满拿真实拍摄的照片一测只有 70%。原因数据划分太随意。很多食物图片来自视频帧或同一批探店素材同一个菜在不同角度、不同光线下的连续帧被随机拆进了训练集和验证集验证集里全是训练集的“近亲”。模型在验证集上不是在泛化是在回忆几乎一样的画面。解决按“来源批次”分组划分。同一道菜的所有图片只进训练集或只进验证集而不是按文件名随机切。划分完成后抽样检查训练集和验证集之间的最近邻图像如果发现大量像素级相似的图对就说明泄漏还在。这一步做完再谈模型调优。5.2 整体 loss 在降冷门类的召回始终上不去类别不均衡现象训练曲线很漂亮整体准确率 86%但看每一类的召回率米饭、面条这类大类别 95%某些地方菜不到 10%模型几乎把所有样本都预测成了头部类别。原因softmax 交叉熵的梯度被大类别主导冷门类样本少贡献的梯度被淹没。整体 loss 下降掩盖了尾部类别毫无进展的事实。解决先给损失函数加类别权重w_c 1 / sqrt(freq_c)或换成 focal loss把注意力压向难分的尾部样本。评估指标同步换成宏平均 F1 和宏平均召回率并且每个 epoch 都打印每个类别独立的准确率召回率盯着尾部类别的曲线看有没有抬头的趋势。只看整体指标是发现不了这个问题的。5.3 增强加过头真实场景识别率反而下降增强失真现象训练集上加 CutMix、Random Erasing 之后验证集确实涨了几个点但部署到实际场景对轻微模糊、亮度正常的图反而变差。原因Random Erasing 擦除面积太大把酱汁、配料这些核心判别区域抹掉了模型学不到食材证据只能靠背景、餐具、桌面纹理来凑判别信号。训练时这套信号在验证集上有效真换环境就失效。解决把擦除面积上限调低比如不超过原图的 15%概率降到 0.5 以下。更重要的是做增强样本可视化把增强后的图打印一批如果人眼已经认不出是什么菜说明增强强度越界了。用类激活映射回看训练后的模型如果高亮区域集中在盘子边缘而不是菜本体回到数据增强这一步重新调。5.4 解冻全层用大学习率预训练特征被冲掉灾难性遗忘现象预训练模型加载后直接全量解冻、学习率设成 1e-3前几个 epoch loss 不降反升之后走向过拟合精度还赶不上只训分类头的版本。原因随机初始化的分类头在早期产生的大梯度在解冻全层时直接回传到了整个主干预训练积累的通用特征被快速覆盖。这就是迁移学习里的灾难性遗忘一旦发生只能重载权重再来等于白训一轮。解决严格按 4.2 节的顺序走——先冻结主干训分类头稳定后再从高层往低层逐层解冻。解冻部分的学习率从 1e-4 起步同时加 35 个 epoch 的 warmup。训练初期如果 loss 冲高第一步不是调学习率而是检查是不是解冻得太早、层次太多。5.5 训练一版能 work部署换成量化推理后错乱预处理不一致现象训练和验证表现正常导出模型后在推理端同一张图输出完全错乱概率分布明显不对看起来又不是代码崩溃。原因部署端的前处理没有完全复刻训练端。常见的有三个一是通道顺序训练用 RGB推理端读图默认成了 BGR二是归一化参数不一致训练用的均值方差和推理端对不上三是缩放方式训练用 resize 拉伸到 224×224推理端用 letterbox 补边图像内容分布就变了。解决把预处理封装成唯一一份函数训练、验证、推理都调用它不要各自写一套。模型导出后拿同一张测试图打印训练端和推理端的输出向量做对比数值差异应该接近 0。如果差异明显逐个检查通道顺序、归一化参数、缩放方式三项变量。这个检查在量化推理时尤其重要量化本身会把数值扰动放大预处理再不一致误差根本查不清来源。6. 设计取舍模型容量、两阶段结构和评估口径之间的那杆秤食物识别项目做到后期瓶颈通常不在单个模型的准确率而在几个绕不开的设计取舍上。第一个取舍是“整图直接分类”还是“目标检测 裁剪再分类”。整图分类简单一个 CNN 端到端出概率但如果真实场景是“一桌多菜”背景里其他菜会干扰判断两阶段先定位盘子再分类精度更高但要多维护一个检测器和一整套调度逻辑。取舍标准应该看部署场景一人一菜一桌整图分类够了食堂、家庭聚餐这类多物共存的场景检测前置的价值更大。第二个取舍是模型容量和数据量之间的平衡。大模型在数据量不够时学的是捷径而不是食材特征泛化更差小模型容量有限反而被迫抓住更本质的判别线索。我见过不少项目用 ResNet-50 级模型在几千张图上过拟合到 70%换轻量模型加数据增强反而涨到 85%。先评估手里有多少真实样本再去选模型顺序不能反。第三个取舍是量化精度。int8 量化能把模型体积压到四分之一算力要求也大幅降低但食物识别对颜色敏感浅层激活压缩后“红亮酱汁”和“深红汤汁”的边界容易糊掉。如果量化后准确率掉得厉害可以在校准阶段把前几层保留为 float16只量化深层特征这是部署精度和速度之间常见的折中。验证方法上我每次必做两件事混淆矩阵看哪两类菜最常互相认错把错误样本单独抽出来回看再用类激活映射检查模型的注意力落在哪里。这两步做完下一步是加数据、改损失还是换结构都有了明确依据。换做现在的我拿到任何一份食物识别项目都会先问三个问题数据是怎么划分的评估用的是宏平均还是整体准确率部署端预处理和训练端是否完全一致。从那以后我每次跑实验都强制走一遍混淆矩阵和 hard example 回看再决定下一步动模型还是动数据。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站