【深度学习基础篇07】食物分类项目全解析:从自定义模型到预训练微调
本文作为「深度学习基础篇」第 7 篇,以 food-11 食物分类为实战场景,完整讲解了图像分类项目从 0 到 1 的全流程:从基础包导入、按类别读取数据集、定义数据增广策略,到自定义 CNN 模型搭建、训练验证函数编写,再到迁移学习的核心应用(ResNet18 从零训练 vs 预训练微调),最后通过工程化封装实现多模型一键切换。文章不仅拆解了小数据集下 CNN 训练的核心痛点,还提供了完整可复用
1 包的导入
我们今天是需要完成一个事物分类的项目,这个项目之后可以更高成很多其他类型的,包装包装就能成一个不错的毕业论文项目。需要一定的python基础。
首先我们需要导入以下包们这些都是显而易见的
import random
import torch
import numpy as np
import os
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
- random:随机打乱数据集的顺序(避免模型学习到数据的排列规律)、随机选择训练样本(比如小批量采样)、设置随机种子(保证实验可复现)
- torch:深度学习核心包;创建张量(存储图像 / 标签数据)、定义计算设备(CPU/GPU)、执行反向传播(更新模型参数)、加载预训练模型(比如 ResNet)等。
- numpy:数值计算;处理原始图像数据(比如将 PIL 图像转为 numpy 数组)、数据预处理(归一化、维度变换)、与 torch 张量互转(
np.array()↔torch.tensor()),因为很多图像处理库(如 OpenCV)的输出都是 numpy 数组。 - os:系统函数相关包;在深度学习中,主要用在遍历数据集文件夹(比如遍历 “猫 / 狗” 分类文件夹读取图片)、拼接文件路径(避免不同系统路径分隔符问题)、创建保存模型的文件夹
- torch.nn:所有的自创模型都需要继承这个类,是所有自定义模型的基类,在分类项目中,你需要继承这个类来搭建自己的分类网络(比如定义卷积层、全连接层),
nn包还提供了分类任务必需的组件:损失函数(nn.CrossEntropyLoss)、激活函数(nn.ReLU)、池化层(nn.MaxPool2d)等。 - Dataset, DataLoader:用
Dataset自定义数据集类;2. 用DataLoader批量加载分类图像数据
2 数据读取
对于一个深度学习项目来说,最难的部分其实就是数据读取部分,因为其他部分都是已有的代码,直接复制粘贴就能有一个很不错的效果了,但是数据部分对于不同的项目,不同的数据,读取的方式都是各不相同的,需要各位充分发挥自己的主观能动性。
2.1 数据集结构:
先观察一下数据集文件结构:![![[image-185.png]]](https://i-blog.csdnimg.cn/direct/38ee9cf41fb140e5bf75055b27926a17.png)
training(训练集)- 用于训练模型,让模型学习食物的特征。
- 里面又分了
labeled(有标签数据)和unlabeled(无标签数据),这是为了支持半监督学习(用少量标注数据 + 大量无标注数据训练)。
validation(验证集)- 用于在训练过程中评估模型性能,调整超参数(如学习率、模型结构),避免过拟合。
- 里面直接按类别(00~10)存放图片,方便在训练时实时验证精度。
testing(测试集)- 用于最终评估模型的泛化能力,模拟模型在真实世界 unseen 数据上的表现。
- 通常测试集的标签是不公开的,需要提交预测结果到平台计算精度。
2.2 read_file(以我的文件路径为例)
首先需要以下两个包:
from PIL import Image #用于读取照片数据
from tqdm import tqdm #用于显示加载进度
tqdm可以显示如下所示的加载进度:![![[image-184.png]]](https://i-blog.csdnimg.cn/direct/af0dd97d058f433ab244caf2df531982.png)
HW = 224
def read_file(path):
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) #返回的是PIL 库的PIL.Image.Image对象(不是 numpy 数组 / 张量),它是对图像的 “封装”,里面包含了描述图片的所有核心信息,你可以把它理解为 “一张图片的完整档案”。
img = img.resize((HW,HW))
xi[j, ...] = img
yi[j] = i
if i == 0:
X = xi
Y = yi
else:
X = np.concatenate((X,xi),axis=0) #拼接后数组的第 0 维(样本数)累加,其他维度(224,224,3)保持不变,正好符合 “合并样本” 的需求;
Y = np.concatenate((Y,yi),axis=0)
print("读到了%d个训练数据"%len(Y))
return X,Y
path = r"D:\desktop\Ai\04_my_分类代码\food-11\training\labeled"
核心逻辑:
- 定义固定图像尺寸
HW=224,遍历 00-10 共 11 个类别文件夹; - 对每个类别文件夹,初始化存储图像的数组
xi(形状:样本数 ×224×224×3)和存储标签的数组yi; - 逐个读取图像,缩放后存入
xi,类别编号作为标签存入yi; - 拼接所有类别的
xi/yi得到整个数据集的X/Y,返回并打印样本总数。
2.3 图片增广
2.3.1 定义
数据增广(Data Augmentation)是在不改变图像核心语义的前提下,对原始图像进行随机的、可控的变换,从而生成更多 “新的” 训练样本的技术。简单来说,就是给同一张图片做 “花式变形”—— 比如旋转、裁剪、翻转、缩放等,让模型看到同一类物体的更多形态,却不会改变它的类别标签(比如一张 “苹果” 图片,旋转 30 度后依然是 “苹果”)。
2.3.2 作用
2.3.2.1 解决 “数据量不足” 问题
food-11 每个类别样本数有限,直接训练容易导致模型 “记死” 样本(过拟合)。通过增广,1 张图片能生成数十种不同形态的 “新样本”:
- 比如一张 “红烧肉” 图片,旋转 10°、裁剪局部、翻转后,就变成了 3 张 “新的红烧肉图片”;
- 增广后训练集的有效样本量大幅提升,模型能学习到更通用的特征(比如 “红烧肉的颜色 / 纹理”,而非 “某一张红烧肉图片的像素”)。
2.3.2.2 缓解过拟合,提升模型泛化能力
过拟合是新手做分类项目最容易遇到的问题:模型在训练集上准确率 99%,但在验证集 / 测试集上只有 60%—— 本质是模型 “死记硬背” 了训练图片的细节,却认不出稍微变形的同类图片。
- 数据增广强制模型学习 “不变的核心特征”:比如不管苹果是正放、侧放、旋转了多少度,模型都能识别出是苹果;
RandomRotation(50)就是典型:让模型见过不同旋转角度的食物,在真实场景中遇到非正放的食物时,依然能准确分类。
2.3.2.3 适配深度学习模型的 “数据饥渴” 特性
深度学习模型(比如 ResNet、EfficientNet)需要大量多样化的数据才能学出有效的特征。如果只用原始样本训练,模型的参数(数百万甚至上亿个)无法充分学习,最终效果差;而增广提供的多样化样本,能让模型的参数得到充分训练,挖掘出食物的本质特征(颜色、纹理、形状),而非无关细节(比如拍摄角度、光照)。![![[image-186.png]]](https://i-blog.csdnimg.cn/direct/1aa7796229564556bb056f1907a80f7d.png)
我们之后要写的train_transform就是一个典型的图像增广流水线,它把多个增广操作组合起来,对每张训练图像自动执行一系列变换:
from torchvision import transforms# 注意补充导入!这是PyTorch官方图像增广工具包
train_transform = transforms.Compose(
[
transforms.ToPILImage()
transforms.RandomResizedCrop(224),
transforms.RandomRotation(50),
transforms.ToTensor() #读取的数据格式: 224, 224, 3 需要的数据格式:3, 224, 224
]
)
| 增广操作 | 作用(通俗解释) | 对本项目的价值 |
|---|---|---|
transforms.ToPILImage() |
将 numpy 数组转为 PIL Image 对象 | 因为torchvision的增广操作只支持 PIL Image 对象,是衔接 numpy 和增广的桥梁 |
RandomResizedCrop(224) |
随机裁剪图像的一部分,再缩放到 224×224 | 模拟 “只看到食物的局部”(比如只看到披萨的一角),让模型关注核心纹理 |
RandomRotation(50) |
随机旋转 - 50°~+50° | 模拟食物的不同摆放角度(比如汉堡歪着放、面条碗旋转了角度) |
transforms.ToTensor() |
转为 PyTorch 张量,形状从 (H,W,C)→(C,H,W) | 适配 PyTorch 模型的输入格式(模型要求通道数在前),同时将像素值归一化到 0~1 |
2.3.3 代码
注意,训练集和验证集要调用的增广是不同的。在验证的时候,不需要做随即裁剪、随机旋转之类的步骤,只需要判断标签。
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()
]
)
2.4 food_Dataset
我们自己的写的food_Dataset函数是基于torch.utils.data中的Dataset,为了正常使用,必须重写这三个函数(可以理解为英语老师总是说的“固定搭配”)。
from torch.utils.data import Dataset,Dataloader
class food_Dataset(Dataset):
def __init__(self):
def __getitem__(self):
def __len__(self):
完善后:
train_path = r"D:\desktop\Ai\04_my_分类代码\food-11_sample\training\labeled" #自己运行代码来测试的时候使用这个路径
val_path = r"D:\desktop\Ai\04_my_分类代码\food-11_sample\validation"
train_set = food_Dataset(train_path, "train")
val_set = food_Dataset(val_path, "val")
train_loader = DataLoader(train_set, batch_size=4, shuffle=True)
val_loader = DataLoader(val_set, batch_size=4, shuffle=True)
class food_Dataset(Dataset):
def __init__(self, path, mode="train"):
self.X, self.Y = read_file(path)
self.Y = torch.LongTensor(self.Y) # 标签转为长整形
if mode == "train":#这是定义了增广规则,未真正调用增广
self.transform = train_transform
else:
self.transform = val_transform
def __getitem__(self, item):
return self.transform(self.X[item]), self.Y[item]
def __len__(self):
return len(self.Y)
3 模型创建及训练
3.1 myModel
这个部分对于初学者来说应该是整个深度学习里比较简单的部分,只要懂得了原理,之后只需要重复性的机械操作便可以轻松写完代码:
我们的核心目标就是:3 *224 *224 -> 512*7*7 -> 拉直 ->全连接分类
在卷积的每一层,顺序都是:卷积->归一化->激活->池化,这个顺序需要熟稔于心
而在全连接的每一层,则只需要全连接->激活->全连接->...即可
class myModel(nn.Module):
def __init__(self, num_class):
super(myModel, self).__init__()
#3 *224 *224 -> 512*7*7 -> 拉直 -》全连接分类
self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) # 64*224*224
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU()
self.pool1 = nn.MaxPool2d(2) #64*112*112
#本质上和第一段是一样的意义,但是这里用了nn.Sequential,相当于是把括号里的代码打包起来,方便之后的调用
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
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)
x = x.view(x.size()[0], -1) #拉直,将512*7*7拉直为25088
x = self.fc1(x)
x = self.relu2(x)
x = self.fc2(x)
return x
3.2 train_val
模型已经创建好了,我们现在可以准备好超参数,然后将他们放入模型中训练,逐步得到优质参数
这个部分也是可以直接复制粘贴的,几乎都是通用的,比如这里我们就用前几篇文章写过的train_val,直接复制过来就能用了!
#需要导入以下包
import time
import matplotlib.pyplot as plt
def train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path):
model = model.to(device)
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
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()
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__()) #记录准确率
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.__len__())
plt_val_acc.append(val_acc / val_loader.dataset.__len__())
if val_acc > max_acc:
torch.save(model.state_dict(), save_path)
max_acc = val_acc
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()
3.3 超参数及其他定义,函数入口
# 测试数据,自己运行代码来测试的时候使用这个路径
train_path = r"D:\desktop\Ai\04_my_分类代码\food-11_sample\training\labeled"
val_path = r"D:\desktop\Ai\04_my_分类代码\food-11_sample\validation"
# 完整数据
# train_path = r"D:\desktop\Ai\04_my_分类代码\food-11\training\labeled"
# val_path = r"D:\desktop\Ai\04_my_分类代码\food-11\validation"
train_set = food_Dataset(train_path, "train")
val_set = food_Dataset(val_path, "val")
train_loader = DataLoader(train_set, batch_size=4, shuffle=True)
val_loader = DataLoader(val_set, batch_size=4, shuffle=True)
model = myModel(11)
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
train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path)
4 迁移学习
我们自己的模型的训练效果其实是比较差的:![![[image-187.png]]](https://i-blog.csdnimg.cn/direct/629a120331ea4d40a0dcb8d590f11f3c.png)
可以看到,准确率大概也就0.4左右,不是很高。为此,我们可以使用别人的模型,也叫做迁移学习![![[image-193.png]]](https://i-blog.csdnimg.cn/direct/9ec98b177de944a2b81d638009d61a63.png)
4.1 定义
迁移学习是把已经在海量数据上训练好的 “通用特征提取器”(比如 ResNet、VGG、EfficientNet)“搬过来”,适配到自己的小数据集分类任务中的技术。
你可以把它理解为:
- 别人花了几百万张图片、几百个 GPU 小时,训练出了一个 “能看懂所有图片基本特征(边缘、纹理、形状)” 的模型;
- 你只需要在这个 “基础能力” 上,给模型补充 “食物分类” 的专属知识(用我们的 198 个 food-11 样本微调),就能快速得到效果远超自定义简单 CNN 的模型。
4.2 为什么迁移学习很重要
- 解决小数据集训练难题:food-11 每个类别样本少(你只有 198 个标注样本),自定义 CNN 连 “过拟合” 都难(参数太多、数据太少,模型学不到有效特征),而迁移学习的预训练模型已经掌握了图像的通用特征,只需要少量数据就能 “适配” 到食物分类;
- 大幅提升准确率:自定义 CNN 准确率约 40%,而用 ResNet/VGG 做迁移学习,准确率能轻松提升到 70%+(甚至更高);
- 节省训练成本:预训练模型的权重已经是 “最优解”,不需要从 0 训练数千万参数,只需要微调最后几层,训练时间从几小时缩短到几分钟。
4.3 实战(以 ResNet18 为例)
我们只需要把这个地方注释掉,让model赋值为我们想要迁移的模型即可![![[image-188.png]]](https://i-blog.csdnimg.cn/direct/0e107f4bf46d407cb9f5d5d2bc11a2a3.png)
4.3.1 微调(Fine-tuning)
微调(Fine-tuning)指先冻结预训练模型的大部分层,训练分类层;再解冻最后 1-3 层卷积层,用极小的学习率一起训练(预训练参数会被轻微调整,适配你的数据集)。
相当于你先用学霸的笔记,背会最后一页的 “food-11 分类”;再把笔记最后几页(和食物特征相关的层)稍微修改,让内容更贴合 “食物分类”。
from torchvision.models import resnet18
model = resnet18(pretrained=True)
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, 11)
测试看看结果:![![[image-189.png]]](https://i-blog.csdnimg.cn/direct/7f20a4c20c75402aa8391bacaded758e.png)
效果好像也不是很理想,没关系,我们调整一下参数:
我们将训练的batch_size改成16(之前是4):![![[image-190.png]]](https://i-blog.csdnimg.cn/direct/9638ab68049848b4b12904f1ed4d4024.png)
此时再运行模型:![![[image-191.png]]](https://i-blog.csdnimg.cn/direct/389e4b5ac8f541dc8d04d02ebbae0ba3.png)
可以看到准确率确实是高了很多(这也说明了深度学习确实是很玄学的,随便调整一个参数可能都会影响模型的准确度,需要我们多次调试)
4.3.2 只用架构、不用预训练参数 → 从零训练(Training from Scratch)
也叫 “从头训练”,指仅复用预训练模型的网络结构(比如 ResNet18 的卷积层、池化层布局),但所有参数都初始化为随机值,完全用自己的数据集从头训练。
相当于你抄了学霸的 “笔记框架”(比如笔记分 “卷积层 / 全连接层” 模块),但笔记里的内容(参数)都是你自己从零写的,没有参考学霸的内容。
只需要将pretrained设为false即可
from torchvision.models import resnet18
model = resnet18(pretrained=False)
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, 11)
一般来说我们都是进行微调而不是从零开始,因为我们自己的数据大概率不会很大,而大佬们的模型都是喂了大量的数据才有很好的准确率的.
我们可以运行一下试试:![![[image-192.png]]](https://i-blog.csdnimg.cn/direct/7336559cc12c4787b96435e2b2cb1ede.png)
很明显这个效果就差了很多
4.3.2.1 适用场景
- 你的数据集和预训练数据集差异极大(比如医学影像 vs 自然图像);
- 你有海量标注数据(远超预训练数据集规模);
- 缺点:对小数据集(如 198 个 food-11 样本)来说,效果极差,容易过拟合。
5 完整代码一览
为了方便大家学习,我们在这里放一下目前的完整代码:
import random
import torch
import numpy as np
import os
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from PIL import Image
from tqdm import tqdm
from torchvision import transforms
import time
import matplotlib.pyplot as plt
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)
################################################
HW = 224
def read_file(path):
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
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
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.X, self.Y = read_file(path)
self.Y = torch.LongTensor(self.Y) # 标签转为长整形
if mode == "train":#这是定义了增广规则,未真正调用增广
self.transform = train_transform
else:
self.transform = val_transform
def __getitem__(self, item):
return self.transform(self.X[item]), self.Y[item]
def __len__(self):
return len(self.Y)
class myModel(nn.Module):
def __init__(self, num_class):
super(myModel, self).__init__()
#3 *224 *224 -> 512*7*7 -> 拉直 -》全连接分类
self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) # 64*224*224
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU()
self.pool1 = nn.MaxPool2d(2) #64*112*112
#本质上和第一段是一样的意义,但是这里用了nn.Sequential,相当于是把括号里的代码打包起来,方便之后的调用
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
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)
x = x.view(x.size()[0], -1) # 拉直,将512*7*7拉直为25088
x = self.fc1(x)
x = self.relu2(x)
x = self.fc2(x)
return x
def train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path):
model = model.to(device)
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
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()
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__()) #记录准确率
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.__len__())
plt_val_acc.append(val_acc / val_loader.dataset.__len__())
if val_acc > max_acc:
torch.save(model.state_dict(), save_path)
max_acc = val_acc
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"D:\desktop\Ai\04_my_分类代码\food-11\training\labeled"
# 测试数据,自己运行代码来测试的时候使用这个路径
# train_path = r"D:\desktop\Ai\04_my_分类代码\food-11_sample\training\labeled"
# val_path = r"D:\desktop\Ai\04_my_分类代码\food-11_sample\validation"
# 完整数据
train_path = r"D:\desktop\Ai\04_my_分类代码\food-11\training\labeled"
val_path = r"D:\desktop\Ai\04_my_分类代码\food-11\validation"
train_set = food_Dataset(train_path, "train")
val_set = food_Dataset(val_path, "val")
train_loader = DataLoader(train_set, batch_size=16, shuffle=True)
val_loader = DataLoader(val_set, batch_size=16, shuffle=True)
from torchvision.models import resnet18
model = resnet18(pretrained=False)
in_features = model.fc.in_features
model.fc = nn.Linear(in_features, 11)
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 = 5
train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path)
6 一些小封装
我们一般会把所有的模型文件放在一个py文件中,封装起来,之后就可以直接调用:
import torch
import torch.nn as nn
import numpy as np
from timm.models.vision_transformer import PatchEmbed, Block
import torchvision.models as models
def set_parameter_requires_grad(model, linear_probing):
if linear_probing:
for param in model.parameters():
param.requires_grad = False # 一个参数的requires_grad设为false, 则训练时就会不更新
class MyModel(nn.Module): #自己的模型
def __init__(self,numclass = 2):
super(MyModel, self).__init__()
self.layer0 = nn.Sequential(
nn.Conv2d(in_channels=3,out_channels=64,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
) #112*112
self.layer1 = nn.Sequential(
nn.Conv2d(in_channels=64,out_channels=128,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(128),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
) #56*56
self.layer2 = nn.Sequential(
nn.Conv2d(in_channels=128,out_channels=256,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
) #28*28
self.layer3 = nn.Sequential(
nn.Conv2d(in_channels=256,out_channels=512,kernel_size=3,stride=1,padding=1,bias=True),
nn.BatchNorm2d(512),
nn.ReLU(inplace=True),
nn.MaxPool2d(2)
) #14*14
self.pool1 = nn.MaxPool2d(2)#7*7
self.fc = nn.Linear(25088, 512)
# self.drop = nn.Dropout(0.5)
self.relu1 = nn.ReLU(inplace=True)
self.fc2 = nn.Linear(512, numclass)
def forward(self,x):
x = self.layer0(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.pool1(x)
x = x.view(x.size()[0],-1)
#view 类似于reshape 这里指定了第一维度为batch大小,第二维度为适应的,即剩多少, 就是多少维。
# 这里就是将特征展平。 展为 B*N ,N为特征维度。
x = self.fc(x)
# x = self.drop(x)
x = self.relu1(x)
x = self.fc2(x)
return x
# def model_Datapara(model, device, pre_path=None):
# model = torch.nn.DataParallel(model).to(device)
#
# model_dict = torch.load(pre_path).module.state_dict()
# model.module.load_state_dict(model_dict)
# return model
#传入模型名字,和分类数, 返回你想要的模型
def initialize_model(model_name, num_classes, linear_prob=False, use_pretrained=True):
# 初始化将在此if语句中设置的这些变量。
# 每个变量都是模型特定的。
model_ft = None
input_size = 0
if model_name =="MyModel":
if use_pretrained == True:
model_ft = torch.load('model_save/MyModel')
else:
model_ft = MyModel(num_classes)
input_size = 224
elif model_name == "resnet18":
""" Resnet18
"""
model_ft = models.resnet18(pretrained=use_pretrained) # 从网络下载模型 pretrain true 使用参数和架构, false 仅使用架构。
set_parameter_requires_grad(model_ft, linear_prob) # 是否为线性探测,线性探测: 固定特征提取器不训练。
num_ftrs = model_ft.fc.in_features #分类头的输入维度
model_ft.fc = nn.Linear(num_ftrs, num_classes) # 删掉原来分类头, 更改最后一层为想要的分类数的分类头。
input_size = 224
elif model_name == "resnet50":
""" Resnet50
"""
model_ft = models.resnet50(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "googlenet":
""" googlenet
"""
model_ft = models.googlenet(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "alexnet":
""" Alexnet
"""
model_ft = models.alexnet(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier[6].in_features
model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
input_size = 224
elif model_name == "vgg":
""" VGG11_bn
"""
model_ft = models.vgg11_bn(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier[6].in_features
model_ft.classifier[6] = nn.Linear(num_ftrs,num_classes)
input_size = 224
elif model_name == "squeezenet":
""" Squeezenet
"""
model_ft = models.squeezenet1_0(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
model_ft.classifier[1] = nn.Conv2d(512, num_classes, kernel_size=(1,1), stride=(1,1))
model_ft.num_classes = num_classes
input_size = 224
elif model_name == "densenet":
""" Densenet
"""
model_ft = models.densenet121(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
num_ftrs = model_ft.classifier.in_features
model_ft.classifier = nn.Linear(num_ftrs, num_classes)
input_size = 224
elif model_name == "inception":
""" Inception v3
Be careful, expects (299,299) sized images and has auxiliary output
"""
model_ft = models.inception_v3(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, linear_prob)
# 处理辅助网络
num_ftrs = model_ft.AuxLogits.fc.in_features
model_ft.AuxLogits.fc = nn.Linear(num_ftrs, num_classes)
# 处理主要网络
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs,num_classes)
input_size = 299
else:
print("Invalid model_utils name, exiting...")
exit()
return model_ft, input_size
def prilearn_para(model_ft,linear_prob):
# 将模型发送到GPU
device = torch.device("cuda:0")
model_ft = model_ft.to(device)
# 在此运行中收集要优化/更新的参数。
# 如果我们正在进行微调,我们将更新所有参数。
# 但如果我们正在进行特征提取方法,我们只会更新刚刚初始化的参数,即`requires_grad`的参数为True。
params_to_update = model_ft.parameters()
print("Params to learn:")
if linear_prob:
params_to_update = []
for name,param in model_ft.named_parameters():
if param.requires_grad == True:
params_to_update.append(param)
print("\t",name)
else:
for name,param in model_ft.named_parameters():
if param.requires_grad == True:
print("\t",name)
#
# # 观察所有参数都在优化
# optimizer_ft = optim.SGD(params_to_update, lr=0.001, momentum=0.9)
def init_para(model):
def weights_init(model):
classname = model.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(model.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(model.weight.data, 1.0, 0.02)
nn.init.constant_(model.bias.data, 0)
model.apply(weights_init)
return model
这样的话,我们在main函数中直需要一行代码就可以调用了:
#模型和超参数
model, input_size = initialize_model(model_name, 11, use_pretrained=False)
7 项目总结
本文以 food-11 食物分类为实战场景,完成了深度学习图像分类项目的全流程实战,核心收获如下:
- 掌握了「数据读取→数据增广→数据集封装」的标准化流程,解决小数据集处理痛点;
- 吃透了自定义 CNN 的搭建逻辑,理解卷积层与全连接层的协作原理;
- 核心突破:掌握迁移学习的两种核心模式,学会用预训练模型(ResNet18)快速提升小数据集分类准确率;
- 学会工程化封装:通过 initialize_model 实现多模型一键切换,打造可复用的分类框架。
更多推荐
所有评论(0)