基于CNN的11类食品分类
1.CNN

2.池化层

3.过拟合与欠拟合

4.迁移学习
微调,比如只微调最后几层的权重(比如把 “识别 1000 类” 改成 “识别你的 11类(猫 / 狗)”),用你的小样本数据稍微改一改就行。
场景:
你手里只有 3000 张食物照片(苹果、面包、蛋糕…),数量很少。
微调训练做了什么?
- VGG 已经会看图片(红色、圆形、松软),这些能力直接白嫖
- 我不让模型重新学 “怎么看图片”,只教它认你的 11 种食物
- 用你这 3000 张少量食物图,轻轻调整一下模型的参数
- 模型瞬间就学会了食物分类
问:什么是迁移学习?为什么要用预训练模型?
答:迁移学习就是把在大数据集上预训练好的模型权重,迁移到我们的小样本任务上,只做微调。用预训练模型的原因是,我们的食品数据集样本量少,从零训练模型很难学到通用的图像特征,而预训练模型在 ImageNet 百万级数据集上学到了通用的图像特征,只需要微调就能适配我们的项目,大幅提升准确率,同时加快模型收敛。
复试专用回答(流畅且体现思考)
各位老师,我项目的代码里,实际运行的是预训练 VGG 模型(预训练模型)而不是自己定义的模型:
- 我先定义了一个基础 CNN 模型(myModel),但测试发现从零训练时,因为食品数据集样本量少,模型收敛慢、准确率只有 10% 左右;
- 所以我注释掉了自定义模型的调用,改用
initialize_model加载带预训练权重的 VGG 模型(开启了use_pretrained=True),实际训练、验证、半监督学习全流程,都是基于这个预训练 VGG 模型完成的; - 这样的设计也做了对比实验,能清晰体现迁移学习对小样本分类任务的提升作用。
5.半监督学习
半监督学习的 “核心价值”—— 补充食品专属的训练数据:
项目里有大量无标签的食品图片(no_label_path对应的路径),这些图片是 “食品专属” 的,但没有人工标注;你用训练到一定程度的预训练 VGG 模型,给这些无标签图片做预测,只选 “模型 99% 确定类别” 的图片(比如模型确定这张是 “面包”),打上 “伪标签” 后加入训练集 —— 相当于把 “无标签的食品图片” 变成了 “可靠的标注数据”,直接扩充了食品分类任务的训练数据量;这些数据是 “食品专属” 的,能让预训练 VGG 模型进一步学习食品的细节特征,而不是只依赖通用特征。
- vgg它已经见过 1000 类东西(ImageNet),学会了 “通用识人特征”(比如人有眼睛、鼻子、嘴巴);
- 你的标注食品数据(3000 多),相当于教它 “认 11 个熟人”(比如张三、李四、王五),但只教了 30 次,它只记得这 30 次的样子(比如张三只穿红衣服);
- 半监督学习就是找了 100 张 “没标名字但 99% 确定是张三 / 李四” 的照片(无标签高置信度图片),补充教它 “张三也穿蓝衣服、李四也戴眼镜”——不是学 “人有眼睛” 这种通用特征,而是学 “张三 / 李四的专属特征”,最终它能认各种样子的张三 / 李四,而不只是红衣服的张三。
6.批量归一化
我在自定义 CNN 模型中加入 BatchNorm 层,核心是通过标准化输入分布提升训练效率和泛化能力:
- 加快收敛:BatchNorm 解决了 “内部协变量偏移” 问题 —— 标准化每一层的输入数据为均值 0、方差 1 的稳定分布,让模型不用反复适应输入分布的变化,聚焦学习特征本身,收敛速度提升数倍;
- 缓解过拟合:BatchNorm 基于批次样本计算均值 / 方差,引入轻微的随机正则化效果,降低模型对训练数据细节(比如食品图片的背景、角度)的依赖,增强泛化能力;
归一化核心:把不同范围的数,统一缩到 0-1 之间,公平参与计算;
好处:模型训练更快,不容易过拟合
7.VGG分类识别模型(误区!!!)
VGG是预训练模型不是训练好的模型,他只是有通用的识别特征,但如何识别需要进行训练,他现在只是有特征而不会对图片进行识别分类
VGG 与 TrainVal:食物分类项目揭秘
完全没问题!我严格结合你的「food-11食物分类项目」,用最接地气的例子,把 VGG预训练模型 + 你的数据 + train_val微调训练 讲得明明白白!
先重申你的项目固定信息:
-
分类目标:11种食物(苹果、面包、蛋糕、饼干、芝士、鸡蛋……)
-
你的数据:带标签的食物图片、无标签食物图片
-
核心任务:让模型看到食物图片 → 说出是11类里的哪一种
一、先看:VGG 天生会什么?(通用特征)
VGG 看过百万张图片,只学会了「看图的基本功」,完全不认识你的食物:
-
能看出颜色:红、黄、棕、白
-
能看出形状:圆、方、长条
-
能看出纹理:光滑、酥脆、松软
-
能看出轮廓:边缘、大小
举个直观的:
VGG 看到一张苹果图片,它只能识别出:
「这是红色 + 圆形 + 光滑的东西」
但它绝对不知道这东西叫「苹果」,更不知道这是你项目里的类别0。
二、再看:train_val + 你的食物数据,教了VGG什么?(微调核心)
这就是针对性教学!把 VGG 会的「通用特征」和你的11类食物强行绑定!
我拿你数据集中的 3类典型食物 举例子:
例子1:教 VGG 认【苹果(类别0)】
-
你的训练数据:
train_loader里几百张带标签的苹果图 -
喂给模型:train_val 函数一批批(16张)喂给 VGG
-
教学过程:
-
VGG 提取特征:红色、圆形、光滑
-
train_val 告诉它:这个特征组合 = 类别0(苹果)
-
优化器(AdamW)调整模型参数
-
-
学会后:
-
只要看到「红色圆形光滑」→ 直接输出类别0
例子2:教 VGG 认【面包(类别1)】
-
你的数据:面包的图片
-
VGG 原有能力:黄色、长条、松软纹理
-
train_val 教学:这个组合 = 类别1(面包)
-
结果:一眼认出面包
例子3:教 VGG 认【饼干(类别3)】
-
VGG 原有能力:浅棕色、方形、酥脆纹理
-
train_val 教学:这个组合 = 类别3(饼干)
三、终极总结(针对你的项目)
1. 没有你的训练(train_val)
VGG 就是个**「睁眼瞎」**:
能看到食物的颜色形状,但完全不知道这是你项目里的哪一类食物,输出的11个分数全是乱的。
2. 有你的训练(train_val + 食物数据)
VGG 变成**「食物识别专家」**:
把它天生会的通用视觉特征,精准适配到你的11类食物上,看到图片就能输出正确的类别(0~10)。
3. 半监督还在帮它巩固
你无标签文件夹里的食物图片,模型学会后99%确定是苹果,train_val 就用这些图再教一遍,让它认的更准。
一句话戳穿本质(你的项目专属)
**VGG 自带「眼睛」,
train_val 用你的食物数据教它「认字」,
让它从「能看清图片」→ 变成「能分清你的11种食物」!**的
import random
import torch
import torch.nn as nn
import numpy as np
import os
from PIL import Image #读取图片数据
from torch.utils.data import Dataset, DataLoader
from tqdm import tqdm
from torchvision import transforms
import time
import matplotlib.pyplot as plt
from model_utils.model import initialize_model
#生成固定的随机数
def seed_everything(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
#################################################################
seed_everything(0)
###############################################
#224*224
HW = 224
#数据增广 训练时才进行数据增广,让模型见到尽可能多的数据
train_transform = transforms.Compose(
[
transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224
transforms.RandomResizedCrop(224),
transforms.RandomRotation(50),
transforms.ToTensor()
]
)
#我要进行多个数据变换,而验证机不需要进行数据增广,来增加正确率
val_transform = transforms.Compose(
[
#把图片转化成专用模式
transforms.ToPILImage(), #224, 224, 3模型 :3, 224, 224
#再翻译成模型能懂的格式
transforms.ToTensor()
]
)
class food_Dataset(Dataset):
def __init__(self, path, mode="train"):
self.mode = mode
#半监督模式,只读入x
if mode == "semi":
self.X = self.read_file(path)
else:
self.X, self.Y = self.read_file(path)
self.Y = torch.LongTensor(self.Y) #标签转为长整形
if mode == "train":
#数据增广
self.transform = train_transform
else:
#训练集调用不含数据增广的方法集
self.transform = val_transform
def read_file(self, path):
if self.mode == "semi":
file_list = os.listdir(path)
#定义x(存储图片)数组
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
# 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list):
img_path = os.path.join(path, img_name)
img = Image.open(img_path)
img = img.resize((HW, HW))
xi[j, ...] = img
print("读到了%d个数据" % len(xi))
return xi
else:
for i in tqdm(range(11)):
file_dir = path + "/%02d" % i
file_list = os.listdir(file_dir)
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8)
yi = np.zeros(len(file_list), dtype=np.uint8)
# 列出文件夹下所有文件名字
for j, img_name in enumerate(file_list):
img_path = os.path.join(file_dir, img_name)
img = Image.open(img_path)
img = img.resize((HW, HW))
xi[j, ...] = img
yi[j] = i
#存11类图片,如果是第0类,怎创建x,y数组,否则直接与前边的类别的数组直接合并(else)
if i == 0:
X = xi
Y = yi
else:
X = np.concatenate((X, xi), axis=0)
Y = np.concatenate((Y, yi), axis=0)
print("读到了%d个数据" % len(Y))
return X, Y
def __getitem__(self, item):
if self.mode == "semi":
return self.transform(self.X[item]), self.X[item]
else:
return self.transform(self.X[item]), self.Y[item]
def __len__(self):
return len(self.X)
class semiDataset(Dataset):
def __init__(self, no_label_loder, model, device, thres=0.99):
x, y = self.get_label(no_label_loder, model, device, thres)
if x == []:
self.flag = False
else:
self.flag = True
self.X = np.array(x)
self.Y = torch.LongTensor(y)
self.transform = train_transform
def get_label(self, no_label_loder, model, device, thres):
model = model.to(device)
pred_prob = []
labels = []
x = []
y = []
soft = nn.Softmax()
with torch.no_grad():
for bat_x, _ in no_label_loder:
bat_x = bat_x.to(device)
pred = model(bat_x)
pred_soft = soft(pred)
pred_max, pred_value = pred_soft.max(1)
pred_prob.extend(pred_max.cpu().numpy().tolist())
labels.extend(pred_value.cpu().numpy().tolist())
for index, prob in enumerate(pred_prob):
if prob > thres:
x.append(no_label_loder.dataset[index][1]) #调用到原始的getitem
y.append(labels[index])
return x, y
def __getitem__(self, item):
return self.transform(self.X[item]), self.Y[item]
def __len__(self):
return len(self.X)
def get_semi_loader(no_label_loder, model, device, thres):
semiset = semiDataset(no_label_loder, model, device, thres)
if semiset.flag == False:
return None
else:
semi_loader = DataLoader(semiset, batch_size=16, shuffle=False)
return semi_loader
#第二大步 定义模型
class myModel(nn.Module):
#num_class分类个数
def __init__(self, num_class):
super(myModel, self).__init__()
#3 *224 *224 -> 512*7*7 -> 拉直 -》全连接分类
#表示64个卷积核,卷积核大小为3,步长1,padding为1
self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) # 64*224*224
#归一化通道数
# 归一化核心:把不同范围的数,统一缩到 0-1 之间,公平参与计算;
# 好处:模型训练更快,不容易过拟合
self.bn1 = nn.BatchNorm2d(64)
# 加了 ReLU 就能学 “复杂规律”(比如 “红色 + 圆形 = 苹果”)。
self.relu = nn.ReLU()
#池化
self.pool1 = nn.MaxPool2d(2) #64*112*112
#相比于第0层,写法简便
self.layer1 = nn.Sequential(
nn.Conv2d(64, 128, 3, 1, 1), # 128*112*112
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2) #128*56*56
)
self.layer2 = nn.Sequential(
nn.Conv2d(128, 256, 3, 1, 1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2) #256*28*28
)
self.layer3 = nn.Sequential(
nn.Conv2d(256, 512, 3, 1, 1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2) #512*14*14
)
self.pool2 = nn.MaxPool2d(2) #512*7*7
self.fc1 = nn.Linear(25088, 1000) #25088->1000
self.relu2 = nn.ReLU()
self.fc2 = nn.Linear(1000, num_class) #1000-11
#模型流程
#3×224×224图片 → conv1筛出64种基础特征(红、弧线)→ layer1-layer2拼出红色圆形 →
# layer3-pool2提炼出苹果核心轮廓 → fc1-fc2打11个得分 → 选最高分(苹果)
#x为图片
def forward(self, x):
x = self.conv1(x)
x = self.bn1(x)
x = self.relu(x)
x = self.pool1(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.pool2(x)
#拉直 类比:把16个“512层7×7的礼盒”,
# 全拆成16串“25088颗珠子”,方便后续打包
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.relu2(x)
x = self.fc2(x)
return x
def train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path):
model = model.to(device)
semi_loader = None
plt_train_loss = []
plt_val_loss = []
#模型准确率
plt_train_acc = []
plt_val_acc = []
#保存准确率最高的模型
max_acc = 0.0
for epoch in range(epochs):
train_loss = 0.0
val_loss = 0.0
train_acc = 0.0
val_acc = 0.0
semi_loss = 0.0
semi_acc = 0.0
start_time = time.time()
# model.train()
# for batch_x, batch_y in train_loader:
# x, target = batch_x.to(device), batch_y.to(device)
# pred = model(x)
# train_bat_loss = loss(pred, target)
# train_bat_loss.backward()
# optimizer.step() # 更新参数 之后要梯度清零否则会累积梯度
# optimizer.zero_grad()
# train_loss += train_bat_loss.cpu().item()
# #每一轮的准确率 pred为预测值
# train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
model.train()
# ✅ 新增1:获取本轮训练的总批次数(比如3080个数据,batch_size=16,总批次就是3080/16≈193)
total_batches = len(train_loader)
# ✅ 新增2:初始化批次计数器,从0开始数
current_batch = 0
for batch_x, batch_y in train_loader:
# ✅ 新增3:每跑一个批次,计数器+1,并打印进度
current_batch += 1
print(f"👉 训练进度:第{current_batch}/{total_batches}个批次 | 本轮第{epoch + 1}/{epochs}轮")
# 下面的代码和原来完全一样,不用改!
x, target = batch_x.to(device), batch_y.to(device)
pred = model(x)
train_bat_loss = loss(pred, target)
train_bat_loss.backward()
optimizer.step() # 更新参数 之后要梯度清零否则会累积梯度
optimizer.zero_grad()
train_loss += train_bat_loss.cpu().item()
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
plt_train_loss.append(train_loss / train_loader.__len__())
#准确率
plt_train_acc.append(train_acc/train_loader.dataset.__len__()) #记录准确率,
if semi_loader!= None:
for batch_x, batch_y in semi_loader:
x, target = batch_x.to(device), batch_y.to(device)
pred = model(x)
semi_bat_loss = loss(pred, target)
semi_bat_loss.backward()
optimizer.step() # 更新参数 之后要梯度清零否则会累积梯度
optimizer.zero_grad()
semi_loss += train_bat_loss.cpu().item()
semi_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
print("半监督数据集的训练准确率为", semi_acc/train_loader.dataset.__len__())
model.eval()
with torch.no_grad():
for batch_x, batch_y in val_loader:
x, target = batch_x.to(device), batch_y.to(device)
pred = model(x)
val_bat_loss = loss(pred, target)
val_loss += val_bat_loss.cpu().item()
val_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis=1) == target.cpu().numpy())
plt_val_loss.append(val_loss / val_loader.dataset.__len__())
plt_val_acc.append(val_acc / val_loader.dataset.__len__())
if epoch%3 == 0 and plt_val_acc[-1] > 0.6:
semi_loader = get_semi_loader(no_label_loader, model, device, thres)
if val_acc > max_acc:
torch.save(model, save_path)
max_acc = val_loss
print('[%03d/%03d] %2.2f sec(s) TrainLoss : %.6f | valLoss: %.6f Trainacc : %.6f | valacc: %.6f' % \
(epoch, epochs, time.time() - start_time, plt_train_loss[-1], plt_val_loss[-1], plt_train_acc[-1], plt_val_acc[-1])
) # 打印训练结果。 注意python语法, %2.2f 表示小数位为2的浮点数, 后面可以对应。
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title("loss")
plt.legend(["train", "val"])
plt.show()
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
plt.title("acc")
plt.legend(["train", "val"])
plt.show()
# path = r"F:\pycharm\beike\classification\food_classification\food-11\training\labeled"
# train_path = r"F:\pycharm\beike\classification\food_classification\food-11\training\labeled"
# val_path = r"F:\pycharm\beike\classification\food_classification\food-11\validation"
#注释掉的路径!!!
#train_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\training\labeled"
#val_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\validation"
#no_label_path = r"F:\pycharm\beike\classification\food_classification\food-11_sample\training\unlabeled\00"
#修改后的路径!!!
train_path=r"D:\ligeproject\第四五节_分类代码\food_classification\food-11_sample\training\labeled"
val_path = r"D:\ligeproject\第四五节_分类代码\food_classification\food-11_sample\validation"
no_label_path = r"D:\ligeproject\第四五节_分类代码\food_classification\food-11_sample\training\unlabeled\00"
train_set = food_Dataset(train_path, "train")
val_set = food_Dataset(val_path, "val")
no_label_set = food_Dataset(no_label_path, "semi")
train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
val_loader = DataLoader(val_set, batch_size=16, shuffle=True)
no_label_loader = DataLoader(no_label_set, batch_size=16, shuffle=False)
# model = myModel(11)
model, _ = initialize_model("vgg", 11, use_pretrained=True)
#学习率 学习率(lr)是模型训练时的“调整步长”
lr = 0.001
#交叉熵损失
loss = nn.CrossEntropyLoss()
#优化器
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)
#设备
device = "cuda" if torch.cuda.is_available() else "cpu"
save_path = "model_save/best_model.pth"
epochs = 15
thres = 0.99
train_val(model, train_loader, val_loader, no_label_loader, device, epochs, optimizer, loss, thres, save_path)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)