☰
深度学习计算机毕设:基于Python CNN的食物识别实战解析
2026/10/22 9:11:08 网站建设 项目流程

这几年计算机毕设里,图像分类方向的题目一直不少,但“深度学习计算机毕设之基于python-CNN深度学习的食物识别”这个组合,是我见过讨论热度最高、也最容易做出效果的选题之一。Python写起来顺手,CNN又是图像识别领域绕不开的基础模型,食物识别本身还自带演示效果,拿手机拍张照片就能出结果,答辩现场非常直观。这篇文章不打算给你念理论,而是从我实际做毕设、帮几个学弟学妹调通项目的角度,把这个题目从选题逻辑、数据集处理、模型搭建、训练调参到部署演示的完整链路,一条一条拆开讲清楚,该给代码的地方给代码,该避坑的地方说避坑。

1. 项目定位:食物识别到底在解决什么问题

1.1 食物识别不只是一个“分类题目”

很多同学第一眼看到“食物识别”四个字,觉得它就是给图片打个标签:输入汉堡照片,输出“汉堡”。这话没错,但只说对了一半。作为毕设项目,食物识别真正考验的是你对图像分类全流程的理解,从数据预处理、网络结构设计、训练策略到模型部署,每个环节都要落地。食物照片和普通物体照片不一样,同一种食物在不同角度、不同光线、不同烹饪方式下差异很大,比如一碗牛肉面,汤多汤少、香菜多少都会影响判断;不同菜系里的红烧肉和炖肉从外观上又接近。这些难点让食物识别比简单的“猫狗分类”更有话可讲,做实验记录时也更容易体现你的工作量。

从应用角度看,食物识别对应的场景很具体:食堂结算台的菜品识别、App里拍照算热量、餐厅后厨的菜品盘点。哪怕原型做得比较简单,只要方向对,答辩时往这些场景上一靠,项目价值立刻就立体了。

1.2 技术选型:Python为什么是首选,CNN为什么能扛起这个任务

Python在这个项目里几乎是唯一选择。PyTorch、TensorFlow、Keras这些深度学习框架对Python的支持最完整,OpenCV、Pillow做图像处理也顺手,Flask搭演示界面更是几行代码的事。整个项目用纯Python串起来,语言层面上几乎不会遇到阻碍。

CNN能扛起食物识别,核心靠三点。第一是局部感受野,卷积核每次只关注图像的一个小区域,就像你辨认一碗菜时,先看主料、再看配菜、最后看汤汁颜色,逐步叠加判断。第二是参数共享,同一个卷积核会在整张图上滑动扫描,大大减少需要学习的参数数量,这对普通配置的电脑非常友好。第三是层次化特征提取,浅层卷积层学到的往往是边缘、色块这类基础特征,深层卷积层会在基础特征之上组合出纹理、形状甚至食材轮廓,这种自动化的特征学习,比SIFT、HOG这类手工特征强太多。

我当时测试过同样的数据集,用传统特征加SVM(支持向量机)跑出来的准确率在60%上下,换成CNN直接提升到80%以上。选CNN做这个题目,不是跟风,是技术路线上的必然。

2. 数据集与预处理:模型好不好,一半看数据

2.1 数据集怎么选:公开数据集优先

做食物识别,第一个绕不开的问题就是数据。我强烈建议优先用公开数据集,最常用的是Food-101,包含101类食物、每类1000张图,共101000张,其中750张训练、250张测试。类别覆盖了常见中餐、西餐、甜点、主食等,规模适中、图片质量稳定,非常适合作为毕设数据源。用公开数据集的好处是方便复现,别人跑同样的数据出了什么指标,你有了对比参照,答辩时也能站得住脚。

如果你的毕设题目要求必须有自己的数据,那么可以自己采集。当年我用手机拍了大约500张食堂菜品照片,再配合公开数据集做补充,把“自定义数据”和“公开数据”结合使用,在答辩陈述时就是一个亮点。但自己采集数据要注意两点:一是类别的数量要平衡,不能一个类别300张、另一个只有30张,不然模型会严重偏向样本多的类别;二是数据质量,模糊的、带严重水印的、主体不在画面中央的图片要尽早清理,宁缺毋滥,坏数据比少数据更伤模型。

2.2 预处理:从原始图片到Tensor的完整流程

图片不是直接喂给网络的。CNN输入要求固定尺寸,这里我用的是224×224,这是ImageNet预训练模型的标准尺寸。原始图片有横有竖有高清有模糊,必须统一缩放。最简单的做法是直接resize,但直接拉伸会导致长宽比失真,食物形状会变形。更好的方案是先在短边按比例缩放到256,再做中心裁剪成224×224,既不丢失主体信息,又保持比例自然。

除了尺寸,还要做归一化。PyTorch里常用的均值方差是[0.485, 0.456, 0.406]和[0.229, 0.224, 0.225],这一组数值是ImageNet数据集统计出来的。训练时用这组参数做标准化,模型收敛更快。不少新手漏掉归一化,或者只把像素除以255,效果都差一大截。

数据增强是毕设提分的关键手段。食物照片在不同App里拍出来的色调差异很大,适当做随机旋转、水平翻转、亮度扰动、对比度扰动,能够让模型学会对颜色和位置的鲁棒性。我的经验是:训练集做增强,验证集只做同样的尺寸变换和归一化,不加随机操作,否则验证集指标波荡太大会影响判断。

下面是一个我用在Food-101上的预处理配置,在PyTorch里可以直接套用:

import torchvision.transforms as transforms train_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.2, contrast=0.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]), ])

每次epoch训练时,图像都会因为随机翻转、旋转而“变样”,相当于用一份数据训练出多份数据的效果,对抑制过拟合非常有效。我在Food-101前50类上做对比实验,不加增强的验证准确率大约79%,加上增强后稳步升到85%左右,提升非常明显。

3. CNN建模:从零搭一个轻量网络,还是迁移学习

3.1 从零搭建轻量CNN的正确姿势

自己写CNN可以快速理解模型结构,但从零搭建不等于把层随便堆叠起来。我见过很多人上来就堆了七八个卷积层,然后发现训练特别慢、效果还不好。一个合理的轻量CNN,卷积块数量3到5个就够了,每层通道数从32、64、128逐渐增加,这样既能提取足够特征,又不会把参数量撑爆。

这里给出我自己用过的FoodNet结构,在食物识别任务上实测效果不错,代码也清晰:

import torch import torch.nn as nn class FoodNet(nn.Module): def __init__(self, num_classes=101): super(FoodNet, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), nn.Conv2d(128, 256, kernel_size=3, padding=1), nn.BatchNorm2d(256), nn.ReLU(inplace=True), nn.MaxPool2d(kernel_size=2, stride=2), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(256, 512), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(512, num_classes), ) def forward(self, x): x = self.features(x) x = self.classifier(x) return x

有几个细节别忽略。第一,MaxPool2d放在激活之后,这是CNN的常见惯例。第二,每层卷积之后都接了BatchNorm2d,它可以稳定中间特征分布,让模型对初始化更不敏感,训练速度也会快很多。第三,Dropout设为0.5,是全连接层专用的正则化手段,训练时随机丢弃一半神经元,大招就是为对抗过拟合准备的。

输入尺寸如果是224×224,图像经过4次下采样,尺寸从224变成14×14,再用AdaptiveAvgPool2d压成1×1的区域传给全连接层。这里用自适应平均池化而不是直接Flatten,是一件很省心的事:不管输入尺寸是多少,全连接层的输入维度都是256,网络即使用在Mobile端图片上也能保持兼容。

3.2 迁移学习:用预训练模型做微调,效果又稳又好

如果你更追求准确率,或者训练数据量不大,我更推荐迁移学习路线。所谓迁移学习,就是用一个在ImageNet上预训练好的模型,比如ResNet18、VGG16、MobileNetV2,把最后全连接层替换成你自己数据集的类别数,然后用食物图片去微调整个网络。

ImageNet有一千多万张图片、一千多个类别,模型在这些数据上已经学会了非常通用的特征提取能力。食物识别的底层特征,比如边缘、纹理、颜色分布,和ImageNet里大量自然图像是相通的。你不需要从零去重新学这些特征,只需要让预训练模型“适应”食物的特殊模式即可。这相当于一个已经见过千万张图的人,再教他分辨具体菜品,自然比一个没见过世面的菜鸟上手快得多。

代码上,以ResNet18为例:

import torchvision.models as models import torch.nn as nn model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) in_features = model.fc.in_features model.fc = nn.Linear(in_features, 101) # 可选:冻结除最后的全连接层之外的所有层,先训练新加的层 for name, param in model.named_parameters(): if name.startswith('fc'): param.requires_grad = True else: param.requires_grad = False

冻结是一种过渡策略。先把预训练模型的参数锁住,只更新新加的fc层,跑5个epoch之后,再解冻所有层、用较小的学习率去微调整个人。这么做的逻辑是:模型一开始对食物特征完全不了解,如果所有参数一上来就一起更新,很容易破坏预训练学到的通用特征,得不偿失;分阶段训练相当于先学新任务,再精调旧知识,效果稳定。

3.3 两条路线怎么选

这几条路线我都跑过,这里把实际感受整理成一个对比表格,方便你根据自己的设备、时间和期望效果来选择:

方案训练速度(10轮)验证准确率(Food-101前50类)显卡要求代码工作量
从零搭建FoodNet快,CPU可跑约85%无强制要求中
ResNet18迁移学习中等约93%建议有NVIDIA GPU低
VGG16迁移学习慢略高于ResNet18内存/显存占用高低
MobileNetV2迁移学习快约89%较低低

如果你是第一次做深度学习项目,我的建议是:先用从零搭建的FoodNet走通整个流程,理解每个层的意义,再用ResNet18迁移学习刷一轮更高的准确率。这样论文里既有自己的网络设计,又有一个强基线结果对比,内容会更扎实。

4. 训练实操:损失函数、优化器与调参细节

4.1 损失函数和优化器怎么配

食物识别是标准的多分类问题,损失函数直接用CrossEntropyLoss,这是PyTorch的交叉熵损失,内部已经把Softmax计算合并进去,所以不需要在模型最后一层加Softmax,训练时直接输出logits即可。

优化器优先选Adam,初始学习率设在1e-3,这是最稳妥的组合。Adam自带自适应学习率,对普通项目来说几乎不用操心学习率衰减策略,前几个epoch就能看到loss快速下降。当然,如果你做迁移学习到了微调阶段,我建议换成SGD配合momentum=0.9,动量的作用就像下山时给一个小推力,能让模型越过局部极小点,最终收敛效果往往比Adam更细一点。

一个需要注意的坑:优化器的weight_decay设置,也就是L2正则化强度,默认值通常是0。在从零搭建的网络里,我建议设置成5e-4,能够有效抑制权值过大带来的过拟合风险。如果你是做迁移学习,冻结层的时候可以开,解冻微调时保持同样的设置即可。

4.2 三个关键超参数怎么定

batch_size、learning_rate、epochs这三个参数会直接影响训练结果。

batch_size决定了每次更新梯度时用多少张图片。它在训练中同时影响稳定性和速度:batch太小,梯度方向波动大,损失曲线跳来跳去;batch太大,每个epoch会变慢,显存占用也高。我实测下来,16到64这个范围适合大多数毕设场景。显存是6GB的话,ResNet18用32不会爆,FoodNet可以堆到64。这是一个能直观看到训练曲线平滑程度和显存占用的估值,不一定完全准确,需要根据实际硬件调整。

learning_rate更关键,也是最容易翻车的点。初次训练直接用1e-3,如果训练了一个epoch发现loss几乎不动,可以试着降到1e-4;如果loss出现明显震荡,说明学习率过大。迁移学习时的全网络微调阶段,全局学习率建议降到1e-4或2e-5,这个时候再用1e-3会把预训练权重改动得面目全非。

epochs不建议一开始就定死。我的做法是设置一个较大的上限,比如30到50轮,配合早停机制也就是EarlyStopping:当验证集准确率连续5轮不再提升时,提前结束训练,并把表现最好的那一次模型参数保存下来。这样既能保证模型充分训练,又不浪费时间去跑无效轮次。

4.3 一段可以跑通的训练核心代码

这里给出一个训练脚本的核心片段。我故意把流程简化成“可读性第一”的版本,方便你读懂每一步后再扩展。

import torch import torch.nn as nn from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = FoodNet(num_classes=101).to(device) criterion = nn.CrossEntropyLoss() optimizer = Adam(model.parameters(), lr=1e-3, weight_decay=5e-4) scheduler = ReduceLROnPlateau(optimizer, mode='max', patience=2, factor=0.5) best_acc = 0.0 for epoch in range(30): model.train() running_loss = 0.0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in val_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() val_acc = correct / total scheduler.step(val_acc) print(f'Epoch {epoch+1:02d}, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.4f}') if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), 'best_model.pth') print('Saved new best model.')

这里用了ReduceLROnPlateau调度器,当验证集准确率两轮不涨时,就把学习率减半,这样前期的快收敛和后期的精细收敛能兼顾。另外,我建议每轮epoch结束后至少打印一次训练loss和验证准确率,不要累积好几轮才看一眼,食物训练集通常有十几万张图,一轮就要几分钟,通过日志尽早发现异常能省下大量时间。

5. 部署演示:让模型变成能用的Web接口

5.1 模型保存与加载的正确做法

很多同学训练完模型,扣着一张准确率截图就以为万事大吉。但毕设答辩时,最吸引老师目光的往往是“能跑起来的东西”。我自己在训练阶段保存模型时,用的是torch.save(model.state_dict(), 'best_model.pth'),只把参数保存下来。真正部署时,这个做法够用,但要注意必须重建出和训练时完全一样的模型结构,再执行load_state_dict:

model = FoodNet(num_classes=101) model.load_state_dict(torch.load('best_model.pth', map_location='cpu')) model.eval()

一个极易踩坑的点:model.eval()不是可选项。PyTorch在训练和推理两个阶段,对BatchNorm层和Dropout层的处理是完全不同的。不切换到eval模式,BatchNorm仍然会按训练批次统计均值方差,Dropout也还在随机丢神经元,那线上推断的结果就乱套了。我调试过一个现象,加载模型后预测结果时好时坏,排查半天,最后发现就是少写了这一行。

如果你想导出一个跨框架可用的模型,可以用torch.onnx.export,导出ONNX格式,这样不仅能摆脱PyTorch环境,还可以在其他推理框架里加载运算速度更快。但对毕设演示来说,直接加载.pth文件足够了。

5.2 用Flask做一个照片上传识别接口

Flask是搭建演示接口最轻量的选择,文件结构很简单,整体只需要一个Python文件和一个HTML模板。接口接收前端上传的图片,做和训练时完全一致的预处理,把图片变成Tensor,喂给模型,最后返回类别名和置信度。

这里写一个最小可运行的Flask接口:

from flask import Flask, request, jsonify from PIL import Image import torchvision.transforms as transforms import torch app = Flask(__name__) device = torch.device('cpu') model = FoodNet(num_classes=101) model.load_state_dict(torch.load('best_model.pth', map_location='cpu')) model.to(device) model.eval() labels = [...] # 101个食物类别名称列表,按数据集类别顺序排列 TEST_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]), ]) @app.route('/predict', methods=['POST']) def predict(): file = request.files.get('image') if file is None: return jsonify({'error': 'no image uploaded'}), 400 img = Image.open(file.stream).convert('RGB') tensor = TEST_TRANSFORM(img).unsqueeze(0) with torch.no_grad(): output = model(tensor) probs = torch.softmax(output, dim=1) top1_index = torch.argmax(probs, dim=1).item() confidence = probs[0, top1_index].item() return jsonify({ 'label': labels[top1_index], 'confidence': round(confidence, 4) }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)

注意几个细节。第一,图片转成RGB,避免遇到带透明通道的PNG图片时通道数不匹配。第二,unsqueeze(0)是为了把单张图片变成Batch维度为1的四维Tensor,否则模型会报维度错误。第三,推理阶段包在torch.no_grad()里,显存和内存都不会被中间梯度撑大,推理速度也会快不少。

前端就很简单了,一个文件上传框加上一个展示结果的标题就行。实际演示时,在电脑上开着这个服务,用浏览器访问localhost:5000,手机拍一张菜的照片传上去,两秒内出结果,这个效果比干讲PPT有说服力得多。

5.3 部署时三个容易忽略但影响体验的细节

第一点,标签顺序不能错。分类模型输出的是一个类别编号,不是类别名字。你在训练时使用DataLoader从哪里读的类别目录,部署时的labels列表就必须按照同样的索引顺序排列。我见过有同学在训练时类别是“宫保鸡丁、鱼香肉丝、麻婆豆腐”这样的自然顺序,但部署时手动写了个不同的顺序,结果模型预测完全错位,以为自己训练出了个废模型。

第二点,CPU环境没问题,不要以为部署一定要GPU。map_location='cpu'就可以把在GPU上训练的权重加载到CPU上。best_model.pth只要结构对应,跨设备加载完全没问题。食物识别这种单张图片推理,CPU上的模型推断时间大概在几百毫秒级别,完全满足演示需求。

第三点,遇到干扰时展示要优雅。连续输错图片,或者上传一张完全不相关的图片,接口能不能给出“置信度过低,无法识别”的反馈,这个细节会被老师在演示时注意到。在预测函数里加一个置信度阈值判断,低于0.5就返回“未识别”,会显得你考虑得周全。

6. 踩坑记录:毕设训练过程中常见的问题和排查思路

6.1 症状一:训练了很久,准确率还是上不去

如果loss一直在下降,但验证准确率徘徊在低位,首先排查数据类别顺序和预处理。我最常见的问题是标签错位,当自定义数据集目录结构和DataLoader的映射不匹配时,模型学习了十几轮,损失一路下降,但准确率一直是个位数或低位徘徊,因为它真的在认真学“错误答案”。

另一个原因往往是数据增强过度。旋转20度、亮度扰动0.8这些参数本身不是问题,但如果图像本身模糊,加上过强的抖动,会彻底破坏食物核心视觉信息。看到准确率低,先关掉所有增强,跑3个epoch验证;如果准确率立刻回归正常,说明增强强度需要下调。这是一个快速定位问题的方法。

学习率也是一个高频坑。过大时曲线会像锯齿一样震荡,过小时损失下降极其缓慢。处理方法是把最初几个epoch的训练打点日志画出来,如果loss每轮下降幅度非常小,把学习率乘以10再试;如果loss直接爆炸或变NaN,把学习率除以10。

6.2 症状二:训练集准确率很高,验证集准确率却低

这是典型的过拟合,说明模型把训练集背下来了。食物图片数量一般在一万张以下,分布比较复杂,模型确实很容易过拟合。第一方案是加大Dropout比例,从0.5提到0.7。第二方案是增强训练数据,加入随机裁剪和水平翻转。第三方案是回到迁移学习路线,用一个参数量小得多的预训练模型替换自己从头训练的网络。

我建议避免靠猜。把训练集和验证集误差曲线画在同一个坐标轴上,如果间隙从小变大且持续扩大,那就不用怀疑,就是过拟合。此时仔细检查数据加载器,确认训练集里的图像是否被意外泄漏进了验证集。我在拆分数据集时踩过一次很大的坑:把分布不好的数据用随机切分,同一个类别的同类图片同时出现在训练集和验证集里,导致验证集准确率虚高,换一批新图立刻露馅。正确的做法是严格按照目录结构分层切分,每一个类别在训练集和验证集里的图片不能共享文件。

6.3 症状三:训练速度慢,显存不够

设备性能不够是最现实的问题。在CPU上跑ResNet18,训练一轮Food-101大约要四十分钟,这种条件下,调参会变得非常吃力。建议先做规模缩放:减少数据到20个类别、图片尺寸降到128、batch_size降到16,先把流程跑通,再逐步扩大规模。迁移学习时也可以使用MobileNetV2,它在保证不错效果的同时,计算量比ResNet18低得多,CPU训练相对可以接受。

如果有GPU,显存不足时优先减小batch_size,而不是换小的模型。也可以用简化训练技巧:启用PyTorch的torch.cuda.amp.autocast做混合精度训练,显存占用能降低30%左右。如果键盘都按过一遍还提示满了,双击打开任务管理器检查是否有其他进程占据显存,实际训练时经常会发现是别的工具有进程占着资源。

6.4 症状四:加载预训练网络时报错或者效果差

联网加载预训练权重时经常失败,特别是ResNet、VGG这类权重文件很大的模型。此时可以手动下载权重文件到本地,再用如下方式加载:

import torchvision.models as models model = models.resnet18() model.load_state_dict(torch.load('resnet18_weights.pth', map_location='cpu'))

另外迁移学习效果差通常是因为冻结策略搞反了。如果直接冻结全部层只训练fc,且数据量不大,效果可能还没有从零训练一个轻量模型好。正确的策略是先冻结训练fc层,等准确率拉起来之后,再解冻最后两三个卷积层或者全部层一起微调,学习率调低,效果才会真正提升。

这里给出一个我整理过的问题速查表,当训练过程出现意外时先对照看:

现象可能原因排查与解决
loss不降学习率过低或过高尝试1e-4到1e-2之间切换
loss为NaN学习率过大或数据含异常值降低学习率,检查数据是否有空值
验证准确率过低标签错位、增强过度检查label映射,临时关掉增强
训练集高验证集低过拟合增Dropout、增增强、换迁移学习
加载预训练失败网络问题或权重路径错误手动下载权重到本地加载
CPU训练太慢模型参数过多缩小输入尺寸、用MobileNet、减数据量
预测时输出错乱漏掉model.eval()加载权重后手动调用model.eval()

写在后面

我最初做完这个项目、跑出Food-101上94%验证准确率的时候,最直观的感觉不是模型有多强,而是整个项目链路走完一遍之后,对深度学习训练的真实体感完全不一样了。以前看别人博客只记得网络架构和参数,真正上手后才知道一个数据增强参数的调整、一次学习率的衰减,都对最终效果影响那么大。你如果能亲手把“数据处理到训练再到部署”这条路跑通,无论后面的求职面试还是读研项目,都不亏。最后给一个小建议:毕设答辩前,把训练结果整理成一张清晰的图表,模型结构图、训练损失曲线、验证准确率提升过程、识别Demo演示,按这个顺序做成前后幻灯,讲的时候既顺又稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询