简介:基于卷积神经网络的食物图像识别是一份完整的实战型资源,面向具有一定Python基础、想系统掌握图像分类与深度学习流程的学习者,解决如何从零构建CNN模型并完成食物类别辨识的问题。资源包为zip格式,共32个文件,以Python脚本为主,辅以TensorFlow模型定义、训练日志、tfrecords数据文件及项目配置信息,整体仅17.28MB,结构清晰,便于快速定位核心代码与数据。已有6701人学习下载,是较受欢迎的入门进阶项目。内容围绕数据预处理、网络搭建、训练评估和预测展开,可直接运行训练脚本复现模型,也可以参考其中的图像增强、迁移学习等实现思路。随包附带的预处理脚本和模型权重,能帮助读者省去大量环境配置和调参时间,快速对照自己的理解进行实验验证。无论是餐饮识别应用探索,还是深度学习课程作业,这份资源都能提供完整的参考链条。
1. 先泼盆冷水:CNN 做食物识别,80% 的功夫在数据集上
很多人拿到「基于卷积神经网络的食物图像识别」这类资源时,第一反应是去找个开源模型,把权重加载进来,跑一遍预测,然后等准确率数字跳出来。但实际上手你会发现,模型代码几乎是最不值钱的部分,真正卡住你的全在数据侧:类别怎么定、图片怎么拍、标注怎么对、样本不均衡怎么处理。整个项目从「能跑」到「能稳」的距离,其实是由数据清洗、预处理和训练策略共同决定的,决定胜负的往往是那另外 80% 的数据工程。
2. 数据集准备与图像预处理:把手机相册变成可训练的张量
2.1 采集策略:先定类别体系,再谈样本量
食物识别和通用图像分类有个显著差异:类内方差极大。同样叫「宫保鸡丁」,不同餐厅的出品在颜色、摆盘、装盘容器上的差异,可能比「宫保鸡丁」和「辣子鸡」之间的差异还要大。所以采集前先做类别体系设计,我一般会建议按两个原则收图。
第一是「一菜一类、不搞多标签」。比如「西红柿炒鸡蛋」和「番茄炒蛋」虽然在语义上等同,但训练时最好合并成一个类,避免同一种食物被拆成两个类导致分类边界混乱。第二是「容器噪声可控」。碗、盘、碟的变化属于背景干扰,如果数据量不够大,优先固定几种常见容器拍摄,模型更容易学到食物本身的可判特征。
样本量方面,每类尽量不低于 200 张原始图片。数据增强可以缓解过拟合,但没法凭空补足类别内的真实变化。某同学做过一个对比实验:每类 100 张做出来的模型,验证集 top-1 在 82% 左右;同类别的图片基础上每类补到 300 张,直接跳到 89% 上下。这里单位是「张原始图片」,不包括增强出来的副本——增强样本不该统计进原始基线里。
2.2 预处理流水线:缩放、归一化与数据增强的参数选择
预处理这步看着机械,其实对结果影响非常大,尤其对预训练模型。下面用 TensorFlow 2.x 的 Keras 接口写一个标准流水线,这段代码对所有 CNN 食物分类项目通用。
import tensorflow as tf from tensorflow.keras import layers def build_preprocess(image_size=(224, 224)): # 归一化:ImageNet 数据集的 RGB 均值/方差,匹配预训练权重分布 def normalize(image): image = tf.cast(image, tf.float32) / 255.0 mean = tf.constant([0.485, 0.456, 0.406]) std = tf.constant([0.229, 0.224, 0.225]) image = (image - mean) / std return image return normalize def build_augment(image_size=(224, 224)): # 数据增强:只在训练阶段使用,验证阶段不使用 return tf.keras.Sequential([ layers.RandomFlip("horizontal"), layers.RandomRotation(0.15), layers.RandomZoom(0.1, 0.1), ])归一化这一步的机理:预训练权重(比如 ImageNet 上的权重)是在特定分布下学出来的,输入图像的 RGB 分布如果和训练时差异太大,微调时梯度更新会出现明显的震荡。/ 255.0是常规缩放到 0~1,再用均值方差做标准化是复现 ImageNet 统计特征的标准做法,跑迁移学习时这两行缺一不可。
数据增强参数上,Horizontal Flip 对食物图安全,注意 Rotation 别超过 0.2 弧度。食物是有「方向感」的,比如摆盘时主菜朝向固定,旋转角度过大等于左图生成 30° 旋转版,反而会教坏模型。RandomZoom 的 0.1 是轻微缩放,模拟手机拍摄距离变化。
2.3 图像清洗:模糊检测与误标样本的排查手段
采集完图片先洗一遍,别急着进训练。用 OpenCV 做拉普拉斯方差检测,值低于阈值就标记为模糊,代码实现很简单。
import cv2 import glob blur_threshold = 80.0 for img_path in glob.glob("./raw_images/*.jpg"): img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() if laplacian_var < blur_threshold: print(f"疑似模糊: {img_path}, 方差={laplacian_var:.2f}")拉普拉斯方差衡量的是图像梯度能量。对焦好的食物图,边缘锐利,梯度方差通常在上百;严重虚化的手机照片方差可能低到十几。阈值按实际数据分布调整,先跑一遍收集全部方差值,取较低分位位置来设阈值。注意:方差高并不绝对等于清晰,运动模糊同样会有较高方差,所以这只能做第一层筛,误标的排查还是得靠人眼抽样。
3. 自制数据集与标注文件:从目录结构到训练集划分脚本
3.1 目录式数据集:用脚本生成训练清单
食物识别这种项目,绝大多数情况不需要 XML 或 JSON 标注,它本质是图像分类,不是目标检测。目录式数据集是最省事的选择:一个文件夹代表一个类别,文件夹名就是标签。
# 推荐目录结构 dataset/ train/ gongbaojiding/ # 宫保鸡丁 mapotofu/ # 麻婆豆腐 ... val/ gongbaojiding/ mapotofu/ ...手动切分容易犯的错是把同名料理全部塞到 train 或 val,没有做类别级分层采样。下面给一段分层切分脚本,按类别比例采样,同时输出每个类别的样本计数,方便确认数据分布。
python src/split_dataset.py \ --source ./raw_dataset \ --dest ./dataset \ --train_ratio 0.8 \ --seed 42source指向按类别文件夹分好的原始图片目录,dest是输出目录。train_ratio控制训练集占比,验证集与测试集按剩余各 50% 划分。seed固定随机种子,保证每次划分结果一致,方便复现实验。
切分完检查一下每个子目录的图片数量,别出现一个类 300 张训练图、另一个类只有 120 张的情况。类别数量差异超过 1:2 时,下一步就必须考虑采样策略。
3.2 TFRecord 转换:为什么自制小项目建议跳过
很多教程会引导你把图片转成 TFRecord 二进制格式。但如果你项目的训练图片总量在 2 万张以下,每张几 KB 到几百 KB,直接image_dataset_from_directory读目录反而是更优解。TFRecord 的主要优势是减少小文件随机读取的 IO 开销,但代价是写入流程复杂,改动类别或增删样本都要重新生成一遍。
我一般在两个场景下才转 TFRecord:一是图片量超过 10 万张,二是分布式多机训练。都不满足时,直接走目录流,简单且调试友好。某公司内部的一个食物识别 Demo 项目维持目录方式,单机训练一轮完整 epoch 大约 8 分钟,TFRecord 版本只快 30 秒,但可维护性下降不少,不值当。
3.3 类别不均衡:「均衡食谱」采样策略
餐饮场景天然存在长尾分布:米饭、面条这类主食出现频率远高于某些限定菜品。不均衡超过一定程度,模型会对高频类别偏向严重,低频类别的召回率跌到惨不忍睹。
处理方案按数据量分档:每类样本量在 500 张以上时,用加权采样让每个 batch 里低频类别出现概率更高;每类不足 200 张时,过采样(重复使用低频类图片)配合数据增强更有效。Keras 里做加权采样可以给每个类别设权重,抽样公式如下。
import numpy as np from sklearn.utils.class_weight import compute_class_weight # class_labels: 所有训练样本的标签列表 weights = compute_class_weight( class_weight="balanced", classes=np.unique(class_labels), y=class_labels ) class_weight_dict = dict(zip(np.unique(class_labels), weights))compute_class_weight按类别频率的倒数归一化出权重,低频类的样本在 loss 计算时获得更高贡献。注意这是损失权重,不是采样权重,它和过采样是两种机制。食品数据我通常两个结合用,先做带权采样增强低频类的出现率,再配合损失函数权重,效果会比单一策略更稳。
4. 模型选型与训练参数:从迁移学习微调开始
4.1 主干网络:轻量级还是高精度
食物图像识别的主干网络选择,直接决定了训练速度和最终精度上限。两套典型的方案:
如果用 EfficientNetB0 或 MobileNetV2 做基础,参数量低于 10M,batch size 可以开到 64 甚至更高,单卡训练一轮约 3-5 分钟,精度上在中等数据集猜能到 85% 以上。如果换成 ResNet50,初始精度会略高几个点,但显存占用翻倍、训练时间增长明显,好处是后续换成 ResNet101 时代码改动极小。
选型逻辑不应该是「越深越好」,而是「数据量决定模型容量」。每类图片不足 300 张时,大网络过拟合风险很高,数据增强都拉不回来。我一般会先跑一版 MobileNetV2 摸透数据特性,确认数据质量没问题再升级到 ResNet。很多项目最终稳定使用的反而是轻量网络。
4.2 迁移学习:冻结层与解冻层的边界
用预训练权重做初始化是唯一靠谱的起步方式。随机初始化从头训练一个 CNN 在 2 万张食物图上,精度常常只有 50%-60%,纯属浪费算力。迁移学习的核心是「冻结」与「解冻」的分层策略。
from tensorflow.keras.applications import MobileNetV2 base_model = MobileNetV2( include_top=False, weights="imagenet", input_shape=(224, 224, 3) ) base_model.trainable = False model = tf.keras.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dense(256, activation="relu"), layers.Dropout(0.3), layers.Dense(num_classes, activation="softmax") ])开始时冻结整个主干,只训练全连接层,这时学习率可以设高一些。跑完几个 epoch 之后再解冻主干的后半部分,使用很小的学习率进行微调。解冻部分的比例是玄学,没有数学定论,我常用的边界点是len(model.layers[0].layers) // 3,即解冻主干最后 1/3 层。
解冻的主干层一旦开始训练,必须用比 FC 层小 10 倍的 learning rate,否则预训练权重会被一波大梯度冲垮。关于这个,后面避坑章节还会展开。
4.3 训练超参数:学习率、batch size 与早停
一组在多个食物数据集上验证过还算稳的训练配置:
| 参数 | 数值 | 说明 |
|---|---|---|
| 优化器 | Adam 或 SGD + Momentum(0.9) | 数据量 5 万以内 Adam 收敛快 |
| 初始学习率(冻结主干) | 0.001 | FC 层用,Adam 下安全 |
| 微调学习率(解冻主干) | 0.0001 | 与 FC 层保持 10 倍差距 |
| Batch size | 32 或 64 | 显存不够就减半,优先保证做归一化 |
| EarlyStopping patience | 5~8 | 监控验证集 loss |
SGD 在老数据集上效果确实比 Adam 稳,但 Adam 调参成本更低,对新手友好很多。食物识别里数据分布相对固定,没有特别难优化的非凸地形,Adam 足以收敛到良好局部最优。
数据增强模块在过拟合严重时优先增强,不要轻易调大 Dropout。Dropout 从 0.3 加到 0.5 对精度的影响经常是负面的,食物特征本身密集且细节重要,大面积丢弃特征会让细粒度判别能力大幅下降。
4.4 损失与指标:top-1 和 top-5 的实际含义
食物分类的评估指标不宜只看 top-1 准确率。现实中一盘菜可能同时像两种做法,比如红烧肉和酱爆肉丁,人眼都容易搞混。此时 top-5 准确率比 top-1更能反映模型真实能力。
Keras 训练时可以直接加两个评估指标。
model.compile( optimizer=optimizer, loss="categorical_crossentropy", metrics=["accuracy", tf.keras.metrics.SparseTopKCategoricalAccuracy(k=5)] )top-5 的意义在项目汇报时很关键:如果sparse_top_k_categorical_accuracy高而 top-1 低,说明模型「知道」正确答案在候选集里,只是置信度分配不够尖锐。后续可以通过调整温度参数做软化处理,这种情况完全不慌。
5. 避坑指南:食物识别训练最常见的五个翻车点
5.1 现象:验证集和训练集的准确率都异常高,但实际预测时一团糟
原因:采集图片时,同一个视频抽帧或同一组连拍图片在切分前没有去重,造成同源图片同时进了训练和验证集,产生了数据泄漏。
解决:切分前先做图片去重。用感知哈希比对,或者退一步,按「图片来源批次」切分而不是按单张图片随机切分。比如某食堂拍摄项目,就按拍摄日期切:周一二三的数据进训练,周四进验证,周日进测试。这样才能避免同一天同一光线下的高度相似图片互相泄漏。
5.2 现象:验证 loss 先降后升,但训练 loss 一直在降
原因:标准过拟合的信号,增强强度不够或增强参数不合理。食物图不像常规分类图那样「怎么增强都像原图」,Rotation 角度过大和 brightness 调整超过临界值会把菜品颜色和纹理破坏掉。
解决:先把 RandomRotation 控制在 0.1~0.15 弧度,随机亮度变化只做微调幅度 0.1。再观察过拟合现象是否缓解。如果没缓解,优先增加原始图片量,这比继续强行增强更有用。
5.3 现象:某些类别召回率特别低,且集中在低亮度、热菜带蒸汽的图片
原因:预处理里没有加对比度增强,或训练集中这类图片本身偏暗。CNN 对全局光照变化还算鲁棒,但在极端低光照下,特征提取层很难激活到有效纹理。
解决:在数据增强序列中加入RandomContrast(0.2),另外针对暗光图做一次 CLAHE 直方图均衡化再入训练集。后者是 OpenCV 的cv2.createCLAHE(),只作用于灰度通道,避免 RGB 三个通道分别均衡产生色偏。
5.4 现象:模型一开始训练,loss 就在高位震荡像心电图
原因:学习率过大,或者迁移学习时主干网络的归一化层被设成了 trainable。大量实践表明,BatchNorm 层在解冻阶段要特别小心,它的均值和方差统计一旦被大学习率扰动,整个预训练特征分布立刻崩塌,这就是很多教程里「冻结主干但保留 BatchNorm 可训练」的迷惑操作的由来。准确的做法简化说——要么全冻结,要么解冻时给很低的学习率。
解决:冻结阶段直接base_model.trainable = False,微调阶段用 0.0001 的学习率。确认优化器传入的初始学习率没有被调度器覆盖成异常值。
5.5 现象:微调解冻后,精度不升反降
原因:解冻了过多层,包括 Stack 底层的通用特征层。底层学的是颜色边缘纹理这类领域无关特征,它们是可迁移的通用知识,不该被少样本食物数据「纠正」。
解决:解冻范围控制在主干网络最后 1/4 到 1/3 的层数,且解冻所有层后第一个 epoch 用最低的学习率跑一次 warmup,让 BatchNorm 统计量逐步适应新数据分布。我从那以后每轮微调都强制检查解冻范围,这个方法救活了好几个差点被丢弃的模型。
6. 模型验证与落地实践:别让模型死在测试准确率上
6.1 混淆矩阵:找到最容易被混淆的「双胞胎」
model.evaluate给出的只是均值数字,哪两个类互相打架完全看不到。食物识别场景下,混淆矩阵是高价值的一步。画一个标准混淆矩阵,找出互混比例最高的类别对。
import seaborn as sns import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix # y_true_label: 验证集真实类别索引 # y_pred_label: 模型预测类别索引 cm = confusion_matrix(y_true_label, y_pred_label) plt.figure(figsize=(12, 10)) sns.heatmap(cm, annot=False, cmap="Blues", fmt="d") plt.show()如果「红烧肉」和「回锅肉」混淆严重,先别急着优化模型,去看看这批训练图的标注是否规范——很多情况下是标注人员把回锅肉图贴进了红烧肉目录。数据修对了,混淆自然降下来。
6.2 置信度阈值与「拒识」策略
食物识别落地时有个典型场景:用户对着桌面拍了一张,可能是白米饭、可能是炒饭以外的某样东西,模型硬要给出一个类别的概率分布反而容易翻车。正确的做法是设置拒识阈值,低置信度直接返回「无法识别」。
推理阶段取 softmax 输出向量的最大值,小于 0.6 时标记为 unknown。阈值按业务容忍度调整:向用户推荐菜品的场景可以放宽到 0.5,医疗营养分析场景要收到 0.85。这个策略让误判率几乎减半,代价只是少量低清晰度图片被拒识。
6.3 部署前检查清单:推理链路的前后端一致性
训练时用 224×224 的输入尺寸,部署时如果直接 reszie 到不同尺寸,再走一遍归一化,精度会掉 2~4 个点。检查清单就三件事:
- 输入尺寸必须与训练完全一致,包括通道顺序 RGB
- 归一化参数必须复用训练脚本里的 mean/std,这些是超参数
- 推理阶段不能套用训练阶段的数据增强,也不要禁用奇怪的预处理操作
我见过太多项目因为推理端忘了除以 255,导致一张正常食物图被预测成「非食物」。这类问题看起来特别低级,但当它真实发生时排查难度不高,十分钟就能定位。
项目做完了别急着删脚本。从那以后我每次做类似资源复现,都会强制把预处理配置、数据集划分 seed 和训练超参数存成一个 JSON 文件归档。测试集和验证集分开,混淆矩阵跑完再考虑上线。希望帮到你。
本文还有配套的精品资源,点击获取