☰
基于CNN的食品图像分类实战:从数据到部署
2026/10/26 22:36:54 网站建设 项目流程

1. 项目概述:食品分类的深度学习实践

在计算机视觉领域,图像分类一直是最基础也最具实用价值的任务之一。我最近完成了一个基于卷积神经网络(CNN)的食品分类项目,能够准确识别11类常见食物。这个项目最初源于一个实际需求:餐饮连锁企业需要自动化处理顾客上传的食物照片,用于营养分析和点餐推荐。

食品分类看似简单,实则充满挑战。不同菜系间的相似性(如中式点心和西式甜点)、同一菜品在不同拍摄条件下的外观差异、以及餐具和背景的干扰,都让传统图像处理方法难以应对。而CNN凭借其局部感知和层次化特征提取的能力,特别适合处理这类问题。

这个项目从数据收集到模型部署共耗时6周,最终在测试集上达到了92.3%的准确率。下面我将详细分享整个实现过程,包括数据处理技巧、模型架构选择和调优经验,这些都是我在实际项目中踩过坑后才积累的实战心得。

2. 核心需求与技术选型

2.1 问题定义与业务场景

我们需要建立一个能区分11类食品的智能系统,具体类别包括:汉堡、披萨、寿司、沙拉、牛排、意面、煎饺、炒饭、汤品、甜点和水果。这些类别覆盖了快餐店最常见的食品类型,实际应用场景包括:

  • 自动菜单生成:根据顾客拍摄的照片推荐相似菜品
  • 营养分析:快速估算食物热量和营养成分
  • 餐饮管理:统计各类食品的消费频次

提示:选择食品类别时,建议考虑业务实际需求和数据可获得性的平衡。我们最初设定了20个类别,但发现某些小众食品样本量不足,最终精简为11类。

2.2 技术方案对比

传统图像处理方法(如SIFT特征+SVM分类器)在这个任务上表现不佳,测试准确率仅68%左右。深度学习方案中,我们对比了以下几种架构:

方法准确率推理速度模型大小适用性分析
ResNet5089.2%45ms98MB平衡性好,适合中等规模数据
MobileNetV285.7%22ms14MB移动端友好,牺牲少量精度
EfficientNetB090.1%38ms29MB效率高但需要更多调参
自定义CNN92.3%28ms8MB针对性设计,需领域知识

最终选择自建CNN架构而非预训练模型,主要基于三点考虑:

  1. 我们的食品图像与ImageNet差异较大,迁移学习优势有限
  2. 自定义模型可以针对食品特征优化网络结构
  3. 部署环境对模型大小有严格限制(<10MB)

3. 数据准备与增强策略

3.1 数据收集与标注

我们构建了一个包含35,000张图像的数据集,主要来源包括:

  • 公开数据集:Food-101、UEC-Food100
  • 餐饮合作伙伴提供的真实顾客照片
  • 人工拍摄的补充样本(控制光线和角度)

标注过程特别注意了以下几点:

  • 同一菜品在不同角度、光照条件下的多张样本
  • 去除含有多个食品类别的复杂场景
  • 平衡各类别的样本数量(每类约3,200张)

3.2 数据预处理流程

import cv2 import albumentations as A # 基础预处理 def preprocess(image): # 统一调整为256x256并保持长宽比 transform = A.Compose([ A.LongestMaxSize(max_size=256), A.PadIfNeeded(min_height=256, min_width=256, border_mode=cv2.BORDER_CONSTANT), A.Normalize() ]) return transform(image=image)["image"] # 训练时增强 train_transform = A.Compose([ A.RandomResizedCrop(224, 224), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.2), A.Rotate(limit=30), A.CoarseDropout(max_holes=8, max_height=16, max_width=16) ])

注意:食品图像增强要符合物理规律。我们避免使用垂直翻转和过度旋转,因为实际中很少会倒置拍摄食物。

3.3 解决类别不平衡的技巧

尽管我们尽量平衡了各类样本,但实际中某些类别(如汉堡)天然更容易获取数据。我们采用了两项对策:

  1. 样本加权:在损失函数中为稀有类别分配更高权重
class_weights = torch.tensor([1.0, 0.9, 1.2, ..., 1.1]) # 根据样本量倒数设置 criterion = nn.CrossEntropyLoss(weight=class_weights)
  1. 动态过采样:在每个epoch中,对少数类样本进行复制或增强

4. CNN模型架构设计

4.1 网络结构详解

我们的自定义CNN架构如下图所示(实际代码实现):

Input(224x224x3) ↓ ConvBlock(64, kernel=7, stride=2, padding=3) + BN + ReLU ↓ MaxPool(kernel=3, stride=2) ↓ ResidualBlock[128] × 2 ↓ ResidualBlock[256] × 3 ↓ ResidualBlock[512] × 3 ↓ GlobalAveragePooling ↓ Dense(1024) + GELU ↓ Dropout(0.5) ↓ Dense(11) # 输出层

关键设计考量:

  • 使用较大的初始卷积核(7x7)捕捉食品的整体形态
  • 引入残差连接缓解深层网络梯度消失问题
  • 中间层使用ReLU,末端使用GELU激活函数
  • 全局平均池化替代全连接层减少参数量

4.2 核心组件实现

import torch import torch.nn as nn import torch.nn.functional as F class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(out_channels) self.shortcut = nn.Sequential() if stride != 1 or in_channels != out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride), nn.BatchNorm2d(out_channels)) def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += self.shortcut(x) return F.relu(out) class FoodCNN(nn.Module): def __init__(self, num_classes=11): super().__init__() self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3) self.bn1 = nn.BatchNorm2d(64) self.pool = nn.MaxPool2d(kernel_size=3, stride=2) # 残差块组 self.layer1 = self._make_layer(64, 128, 2) self.layer2 = self._make_layer(128, 256, 3, stride=2) self.layer3 = self._make_layer(256, 512, 3, stride=2) self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.fc1 = nn.Linear(512, 1024) self.dropout = nn.Dropout(0.5) self.fc2 = nn.Linear(1024, num_classes) def _make_layer(self, in_channels, out_channels, blocks, stride=1): layers = [ResidualBlock(in_channels, out_channels, stride)] for _ in range(1, blocks): layers.append(ResidualBlock(out_channels, out_channels)) return nn.Sequential(*layers) def forward(self, x): x = F.relu(self.bn1(self.conv1(x))) x = self.pool(x) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = F.gelu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x

4.3 激活函数选择经验

我们在不同层尝试了多种激活函数:

  • 中间层:ReLU计算高效且缓解梯度消失
  • 末端全连接层:GELU表现更好,因其平滑特性适合分类任务

实验对比结果:

激活函数组合验证准确率训练稳定性
全部ReLU91.1%偶尔震荡
全部GELU91.8%稳定但耗时
ReLU+GELU92.3%最稳定

5. 模型训练与优化

5.1 训练配置

from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR model = FoodCNN().to(device) optimizer = AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=20, eta_min=1e-5) criterion = nn.CrossEntropyLoss() # 混合精度训练 scaler = torch.cuda.amp.GradScaler()

关键参数选择依据:

  • AdamW优化器:比普通Adam更适合视觉任务
  • 初始学习率1e-3:通过LR Finder确定
  • 余弦退火调度:帮助跳出局部最优

5.2 训练过程监控

我们使用WandB记录以下指标:

  • 训练/验证损失
  • 类别级精确率、召回率
  • 混淆矩阵
  • 激活值分布

实操心得:食品分类中,某些类别(如炒饭和煎饺)容易混淆,需要特别关注这些类别对的F1分数变化。

5.3 关键训练技巧

  1. 渐进式图像尺寸:前5epoch用128x128,后15epoch用224x224
  2. 标签平滑:设置ε=0.1缓解过拟合
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
  1. 知识蒸馏:用EfficientNetB4作为教师模型提供软标签

6. 模型评估与部署

6.1 性能指标

在保留测试集(3,500张图)上的表现:

指标数值
总体准确率92.3%
平均精确率91.8%
平均召回率92.1%
F1分数91.9%
推理速度28ms (RTX 3060)

6.2 错误分析

通过混淆矩阵发现主要错误类型:

  1. 煎饺与锅贴混淆(15%错误)
  2. 某些沙拉与水果拼盘混淆(12%错误)
  3. 深色酱汁的意面误判为炒面(9%错误)

解决方案:

  • 收集更多易混淆类别的样本
  • 添加注意力机制强化局部特征
  • 引入多任务学习(同时预测食材)

6.3 部署优化

使用TorchScript导出模型并进行以下优化:

# 模型量化 quantized_model = torch.quantization.quantize_dynamic( model, {nn.Linear}, dtype=torch.qint8) # ONNX导出 torch.onnx.export(model, dummy_input, "food_cls.onnx", opset_version=13)

部署后性能:

  • 模型大小从23MB压缩到3.7MB
  • CPU推理时间从120ms降至45ms
  • 内存占用减少60%

7. 常见问题与解决方案

7.1 训练不稳定

现象:损失值剧烈波动,准确率停滞可能原因:

  • 学习率过高
  • 批次内样本差异过大
  • 数据增强过于激进

解决方案:

# 梯度裁剪 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 调整数据增强强度 train_transform = A.Compose([ A.RandomResizedCrop(224, 224, scale=(0.8, 1.0)), # 减小裁剪范围 A.HorizontalFlip(p=0.3), # 降低翻转概率 # 其他增强... ])

7.2 类别间混淆严重

现象:某些类别持续相互误判解决方案:

  1. 特征空间可视化分析
from sklearn.manifold import TSNE features = extract_features(model, dataloader) # 提取倒数第二层特征 tsne = TSNE(n_components=2) vis_features = tsne.fit_transform(features)
  1. 针对性数据增强:对易混淆类别应用特定变换
  2. 修改损失函数:增加中心损失或三元组损失

7.3 实际部署效果下降

现象:测试集表现良好但真实场景准确率低可能原因:

  • 训练数据与真实数据分布差异
  • 拍摄条件不一致(光线、角度等)

解决方案:

  1. 建立持续数据收集管道
  2. 实现在线学习机制
  3. 添加预处理模块统一输入分布
# 自动白平衡预处理 def auto_white_balance(image): result = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) avg_a = np.average(result[:, :, 1]) avg_b = np.average(result[:, :, 2]) result[:, :, 1] = result[:, :, 1] - ((avg_a - 128) * 1.1) result[:, :, 2] = result[:, :, 2] - ((avg_b - 128) * 1.1) return cv2.cvtColor(result, cv2.COLOR_LAB2BGR)

8. 项目扩展与优化方向

在实际应用中,我们发现以下几个有价值的优化方向:

  1. 多模态融合:结合菜品名称文本信息提升准确率

    • 使用CLIP等跨模态模型
    • 简单实现:将菜名TF-IDF特征与图像特征拼接
  2. 细粒度分类:区分同一类别的不同变种

    • 如披萨可分为玛格丽特、海鲜、夏威夷等子类
    • 需要更精细的标注数据和更深的网络
  3. 营养信息预测:扩展模型输出热量和营养成分

    # 修改输出层 self.fc_calories = nn.Linear(1024, 1) # 热量预测 self.fc_nutrition = nn.Linear(1024, 5) # 五大营养素
  4. 异常检测:识别非食品图像或异常菜品

    • 使用自动编码器检测分布外样本
    • 输出置信度分数过滤低质量预测
  5. 边缘设备优化:

    • 转换为TensorFlow Lite或Core ML格式
    • 实现基于OpenCV的轻量级预处理
    // 示例:ARM NEON加速的预处理 void preprocess_neon(cv::Mat &input, float* output) { // NEON指令集优化实现... }

这个食品分类项目从技术验证到生产部署的全过程,让我深刻体会到CNN在实际业务中的应用价值。最大的收获是认识到:在特定领域任务中,经过精心设计的自定义网络往往能超越通用预训练模型,关键是要深入理解业务场景和数据特性。下一步我计划尝试Vision Transformer架构,并引入时序信息处理视频输入,进一步提升系统在动态场景下的表现。

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

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

立即咨询