
简介这份资源面向计算机视觉与深度学习入门者围绕卷积神经网络在食物图像分类任务中的完整实践展开帮助读者理解从数据预处理、模型搭建到训练评估的全流程。压缩包共32个文件以Python脚本为主辅以pyc缓存、xml配置、tfrecords数据记录及md说明文档整体约17.28MB涵盖模型定义、图像变换、训练与测试等模块目录结构便于按环节查阅。已有6883人学习下载说明其在同类项目中具备一定参考价值。读者可从中获得可复现的CNN食物识别代码框架包括网络结构定义、数据读取与增强脚本、训练日志及模型权重并接触迁移学习等进阶思路适合作为课程设计或项目练手的实操素材。1. 从一张拍糊的餐盘照片说起卷积神经网络做食物图像识别到底能干什么朋友聚餐拍了一张照片光线偏暗、盘子还叠着两个菜你想知道这桌大概多少热量、都是什么菜系——这件事人眼扫一下就有答案但要让程序自动说出来就没那么轻松了。食物图像识别是典型的细粒度分类问题菜品种类多、类间差异小比如不同做法的鸡丁、类内差异大同一道菜不同摆盘、不同光照用传统特征工程基本是血泪经验堆出来的效果。卷积神经网络在这里的价值是把「边缘—纹理—局部形状—整体语义」这层层特征自动学出来省掉手工设计描述子的环节。这份资源就是围绕食物图像识别这条主线把数据准备、模型搭建、训练调参、推理验证串成一个能跑通的完整工程适合刚接触卷积神经网络、想拿一个真实场景练手图像分类的开发者也适合需要快速搭一个菜品识别 Demo 去做二次开发的人。2. 项目整体架构与技术选型为什么是卷积神经网络而不是别的方案2.1 任务定位与模型选型逻辑食物图像识别在工程上要先想清楚一件事你要的是「分类」还是「检测」。分类是给整张图打一个菜品标签检测是把图里每个菜都框出来再分类。这份资源走的是分类路线输入一张食物图片输出所属类别及置信度。选型上卷积神经网络是图像分类的主流骨架常见做法是用一个预训练骨干网络做特征提取再接一个全连接分类头。为什么不用传统机器学习因为食物图像的判别信息大量藏在纹理和局部组合里手工特征很难覆盖而卷积的局部感受野加权重共享天然适合这类空间结构数据。我一般会把整个工程拆成四层来看这样后面改哪一层都心里有数层次职责典型文件/模块数据层图像读取、划分、增强、批加载dataset / transforms 相关脚本模型层骨干网络、分类头、前向计算model 定义文件训练层损失、优化器、学习率、训练循环、权重保存train 脚本推理层加载权重、单图/批量预测、结果输出predict / inference 脚本这个分层不是摆设。实际改项目时八成的需求都落在数据层和推理层模型层反而动得少。把边界划清楚改起来才不会牵一发动全身。2.2 目录结构拿到源码先看哪几个文件一个合格的食物图像识别工程目录大致长这样不同实现命名会有出入但职责对应food_recognition/ ├── data/ │ ├── train/ # 按类别分子目录存放训练图 │ ├── val/ # 验证集同样按类别分目录 │ └── classes.txt # 类别名与索引的映射 ├── src/ │ ├── dataset.py # 数据集封装与图像增强 │ ├── model.py # 卷积网络结构定义 │ ├── train.py # 训练主流程 │ ├── predict.py # 单图推理 │ └── utils.py # 指标计算、日志、权重存取 ├── configs/ │ └── default.yaml # 超参数与路径配置 ├── requirements.txt └── README.md拿到源码后我的习惯是先看configs和dataset.py再看model.py最后才读train.py。原因很直接配置决定实验怎么跑数据集决定输入长什么样模型结构决定能改的空间训练脚本只是把前面几样串起来。很多人一上来就啃训练循环结果读完了还不知道输入张量的形状纯属绕远路。2.3 数据管线食物图像识别的第一道坎食物图像的数据管线比想象中麻烦。类别不均衡是常态——家常菜样本一大堆冷门菜只有几十张图像尺寸不统一手机拍的横竖都有还有标注噪声同一道菜被标成两个名字。工程上常见的处理顺序是先统一尺寸再做随机裁剪和翻转增强最后归一化到骨干网络要求的均值和方差。# dataset.py 核心片段图像增强与批加载 from torchvision import transforms from torch.utils.data import DataLoader, ImageFolder train_tf transforms.Compose([ transforms.Resize((256, 256)), # 先统一到稍大尺寸 transforms.RandomResizedCrop(224), # 随机裁剪到骨干网络输入尺寸 transforms.RandomHorizontalFlip(), # 食物左右翻转通常不改变类别 transforms.ColorJitter(0.2, 0.2, 0.2), # 应对不同光照与色温 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet 统计量 ]) val_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_ds ImageFolder(data/train, transformtrain_tf) val_ds ImageFolder(data/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4)这段代码里几个参数值得说清楚。RandomResizedCrop(224)的 224 是多数预训练骨干的标准输入边长改模型时这个数要跟着改。ColorJitter的三个 0.2 分别控制亮度、对比度、饱和度的扰动幅度食物场景下光照差异大这个增强很有必要但别开太大否则颜色特征被破坏反而掉点。Normalize用的均值方差是 ImageNet 统计量只要骨干是在 ImageNet 上预训练的就必须保持一致这是很多人翻车的地方——自己随便填一组数训练 loss 看着降验证集死活上不去。num_workers在 Windows 上建议设 0 或 2设大了容易卡在数据加载上。2.4 模型结构骨干加分类头的组合方式模型层通常是「预训练骨干 全局池化 全连接分类头」。骨干负责提特征池化把空间维度压掉分类头输出类别数。下面是一个可替换骨干的写法# model.py可切换骨干的食物分类网络 import torch.nn as nn from torchvision import models def build_model(num_classes, backboneresnet18, pretrainedTrue): if backbone resnet18: net models.resnet18(weightsIMAGENET1K_V1 if pretrained else None) in_features net.fc.in_features net.fc nn.Linear(in_features, num_classes) # 替换分类头 elif backbone mobilenet_v3: net models.mobilenet_v3_small(weightsIMAGENET1K_V1 if pretrained else None) in_features net.classifier[-1].in_features net.classifier[-1] nn.Linear(in_features, num_classes) else: raise ValueError(funsupported backbone: {backbone}) return netnum_classes必须和classes.txt的行数严格一致这是最常见的维度不匹配来源。pretrainedTrue时加载的是公开预训练权重食物数据集通常只有几千到几万张从零训练很容易过拟合用预训练权重做初始化是性价比最高的做法。resnet18轻、快适合先跑通mobilenet_v3更小适合后面往端侧部署。换骨干时唯一要改的就是分类头那一行因为不同网络的分类层字段名不一样resnet是fcmobilenet是classifier[-1]照抄会报错。3. 把项目跑起来环境、训练与推理的完整链路3.1 环境准备与依赖安装先确认 Python 版本卷积网络工程一般要求 3.8 以上。依赖装的时候建议用虚拟环境隔离避免和系统里其他项目的版本打架。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install -r requirements.txtCPU 版先跑通流程确认代码没问题再换 GPU 版这个顺序能省掉很多「到底是环境问题还是代码问题」的排查时间。requirements.txt里通常还会有numpy、pillow、pyyaml、tqdm这类基础库装完用python -c import torch; print(torch.__version__)验证一下能打印出版本号就说明主依赖没问题。3.2 数据组织与配置检查数据必须按类别分目录ImageFolder靠目录名推断标签。跑之前先做一次自检每个类别目录下至少要有几张图训练集和验证集的类别集合必须完全一致否则验证时会因为索引对不上而报错。配置文件里重点看三个路径训练集目录、验证集目录、权重保存目录。路径写错是新手最高频的翻车点报错信息往往只给一个FileNotFoundError不会告诉你到底哪个配置项错了。提示先把batch_size设小一点比如 8跑几十步确认 loss 在动、显存没爆再调回正常值。这个习惯能帮你快速区分「代码逻辑错」和「资源不够」。3.3 训练循环损失、优化器与学习率训练脚本的核心是四件事前向、算损失、反向、更新。食物分类用交叉熵损失优化器常用带动量的随机梯度下降或自适应优化器。下面是一个精简但完整的训练循环# train.py单轮训练与验证 import torch import torch.nn as nn from tqdm import tqdm def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0.0, 0, 0 for images, labels in tqdm(loader): images, labels images.to(device), labels.to(device) optimizer.zero_grad() logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (logits.argmax(1) labels).sum().item() total labels.size(0) return total_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for images, labels in loader: images, labels images.to(device), labels.to(device) logits model(images) total_loss criterion(logits, labels).item() * images.size(0) correct (logits.argmax(1) labels).sum().item() total labels.size(0) return total_loss / total, correct / totalmodel.train()和model.eval()的切换不能省它影响批归一化和随机失活的行为忘了切会导致验证结果忽高忽低看起来像玄学。optimizer.zero_grad()必须在反向传播前调用否则梯度会累加。学习率是另一个关键参数用预训练骨干时常见做法是给骨干设小学习率、给新分类头设大学习率因为骨干特征已经不错了分类头还是随机初始化的。如果只用一个统一学习率建议从 1e-3 起步观察验证集准确率连续几轮不涨就降一个数量级。3.4 推理与结果输出训练完拿到权重文件推理脚本要做的是加载模型结构、灌入权重、切到评估模式、对输入图做和验证集一致的预处理。这里最容易出错的是预处理不一致——训练时用了归一化推理时忘了预测结果会离谱到让你怀疑模型没训练。# predict.py单图推理 import torch from PIL import Image from model import build_model from dataset import val_tf def predict(image_path, weight_path, classes, devicecpu): model build_model(num_classeslen(classes), pretrainedFalse) model.load_state_dict(torch.load(weight_path, map_locationdevice)) model.to(device).eval() img Image.open(image_path).convert(RGB) tensor val_tf(img).unsqueeze(0).to(device) # 增加 batch 维度 with torch.no_grad(): logits model(tensor) prob torch.softmax(logits, dim1) conf, idx prob.max(1) return classes[idx.item()], conf.item()unsqueeze(0)是给单张图补一个 batch 维度模型前向要求输入是四维张量。softmax把 logits 转成概率max(1)取置信度最高的类别。map_location在 CPU 上加载 GPU 训练的权重时必须指定否则会报设备不匹配。实际用的时候置信度低于某个阈值比如 0.5的样本建议单独挑出来人工看食物识别里这类样本往往是光照极差或者根本没拍全硬给一个标签反而误导。4. 避坑与排查食物图像识别工程里最容易踩的五个坑4.1 训练准确率很高但验证集不动现象是训练集准确率一路涨到 95% 以上验证集卡在 40% 左右。原因通常是过拟合加数据分布不一致训练集和验证集的拍摄条件差异太大或者验证集类别和训练集没对齐。解决办法是先检查两个集合的类别目录是否完全一致再加大增强强度、加权重衰减必要时减少骨干的可训练层数只微调最后几层。4.2 损失变成 NaN现象是训练几步后 loss 直接变nan。原因多半是学习率过大或者输入数据里有异常值比如全黑图、损坏文件。解决方法是先把学习率降一个数量级再在数据加载时加一层过滤跳过无法解码的图片。归一化参数填错也会引发这个问题务必确认和预训练权重匹配。4.3 显存不够导致训练中断现象是跑到一半报显存溢出。原因是batch_size或输入分辨率太大。解决办法是降batch_size、减小输入尺寸或者用梯度累积模拟大 batch。别急着换更大显存的机器先把参数调下来很多问题不是硬件能解决的。4.4 推理结果和训练时对不上现象是训练日志里准确率不错单独拿图预测却总是错。原因是推理预处理和验证预处理不一致最常见的是漏了归一化或者尺寸裁剪方式不同。解决办法是把推理预处理直接复用验证集的transform不要另写一套。4.5 类别索引错位现象是预测出来的类别名总是差一位或者完全对不上。原因是classes.txt的顺序和训练时ImageFolder推断出的顺序不一致。ImageFolder按目录名字母序排索引手写类别文件时很容易按自己的习惯排。解决办法是训练结束后把train_ds.classes直接导出成文件推理时读同一份别手工维护。5. 二次开发怎么改换数据、换骨干、加部署的三条路径拿到这个工程之后真正有价值的是知道往哪个方向改。第一条路径是换数据把自己的菜品图按类别放进data/train和data/val改classes.txt和配置里的num_classes其余不用动。这里有个技巧新类别样本少的时候先冻结骨干只训分类头等 loss 稳定了再解冻最后几层一起微调比一上来全量微调稳得多。第二条路径是换骨干build_model里已经留了分支加一个新分支只要处理分类头字段名注意同步改输入尺寸和归一化参数。第三条路径是往部署走把模型导出成推理格式推理脚本里的预处理逻辑要原样搬过去别在部署端重写。验证改造是否成功我一般看三个数验证集准确率、单类召回率、混淆矩阵里最容易被混的几对类别。食物识别里「同类不同做法」和「不同类相似外观」是主要误差来源混淆矩阵能直接告诉你该补哪类数据。下面这个片段用来导出混淆矩阵改完模型跑一遍就能对比# 评估改造效果输出混淆矩阵 from sklearn.metrics import confusion_matrix import numpy as np def collect_preds(model, loader, device): model.eval() ys, ps [], [] with torch.no_grad(): for images, labels in loader: logits model(images.to(device)) ps.extend(logits.argmax(1).cpu().numpy()) ys.extend(labels.numpy()) return np.array(ys), np.array(ps) y_true, y_pred collect_preds(model, val_loader, device) cm confusion_matrix(y_true, y_pred) print(cm)confusion_matrix的行是真实类别、列是预测类别对角线越集中越好。看的时候别只盯总数重点看哪两类互相串那往往就是数据层面需要补样本或者加针对性增强的地方。改完一轮记得把权重文件名带上关键参数骨干、学习率、数据版本不然过几天回头根本分不清哪个文件是哪个实验的——这个后悔药我吃过不止一次。从那以后我每次改完模型结构都强制先跑一遍小样本过拟合测试确认模型能记住几十张图再上全量数据这样能把结构错误和数据错误分开。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。