1.食品分类问题
样本数据:有十一类每一类有280张照片的带标签的图片,即有X和Y的 下面同理,无标签就是没有Y的数据。
任务:
通过预测,这个文件夹下的照片到底是哪一个类别的。
--------------------
数据处理模块
import random
import torch
import torch.nn as nn
import numpy as np
import os
from torch.utils.data import Dataset, DataLoader
from PIL import Image #读取图片数据
from tqdm import tqdm
from torchvision import transforms
from model_utils.data import train_transform
#在 Python 里,from ... import ... 这种语法能够让你从指定的模块里导入特定的类、函数或者变量。
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) #这里调用了 seed_everything 函数,并且将种子值设为 0。
# 这意味着代码中所有依赖随机数生成的操作都会使用相同的随机数序列,从而保证代码的可复现性。
#---------------------------------------------------------------------------------------------
HW =224
def read_file(path): #遍历11个子目录(编号00到10),获取每个子目录中的文件列表
for i in tqdm(range(11)):
file_dir = path +"/%02d"%i # 拼接子目录路径,即00-10
file_list = os.listdir(file_dir) # 获取子目录下的所有文件名
#xi和yi初始化为全零数组,后续通过循环填充实际像素值
xi = np.zeros((len(file_list),HW,HW,3),dtype=np.uint8) #四维 NumPy 数组(形状为 (图片数量, 224, 224, 3))
yi = np.zeros((len(file_list)),dtype=np.uint8) #一维 NumPy 数组(形状为 (图片数量,)),存储当前子目录下所有图片的标签
# 当前子目录是00(类别0),内有280张图片:
# xi形状为(280, 224, 224, 3),存储100张224x224的RGB图片数据,读到第一张照片就放在第一个格子里,第二个就放第二个格子
# yi形状为(280, ),存储100个标签值(全初始化为0)。
for j,img_name in enumerate(file_list): #列出文件夹下所有文件的名字
#通过j确定当前图片应存储在xi和yi的哪个位置
#当 j=0 时,处理第一个文件,数据存入 xi[0, ...] 和 yi[0]。
#当 j=1 时,处理第二个文件,数据存入 xi[1, ...] 和 yi[1]。
# 示例(假设 file_list 有 3 个文件):
# j img_name 数据存储位置
# 0 "0001.jpg" xi[0, ...], yi[0]
# 1 "0002.jpg" xi[1, ...], yi[1]
# 2 "0003.jpg" xi[2, ...], yi[2]
img_path = os.path.join(file_dir, img_name) #这个for循环的含义是:拿到00-10文件夹下的每一个文件的路径
img = Image.open(img_path) #把img_path下的文件读到img
img = img.resize((HW, HW)) #因为此时的图片书RGB三通道每个512*512,而在深度学习里面的图片应该是224*224的,所以把它进行一个转换
xi[j, ...] = img
yi[j, ...] = i
#-------------------------------------------------------------------------
#这段代码的作用是 将11个类别(00 - 10目录)中的所有图片数据拼接成一个完整的数据集。让我们分解分析一下这部分代码
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), #50°以内随机进行旋转
transforms.ToTensor() #变为张量,因为模型都是张量运行的
])
val_transform = transforms.Compose([ #验证的transform不用那么多花里胡哨的,验证和测试的时候用原始数据
transforms.ToPILImage(), #把224*224*3的图片改成3*224*224的形式
transforms.ToTensor() #变为张量,因为模型都是张量运行的
])
class foodDataset(Dataset):
def __init__(self, path,mode="train"): #读数据
self.X, self.Y =read_file(path) #建立一个read_file函数去读图片文件
self.Y =torch.LongTensor(self.Y) #在分类任务里面标签都是长整型,不再是回归任务里面的小数了
#self.transform:数据增广,把一张图片的各种各样的样子都出现一遍,核心目的是通过对原始数据进行合理变换,优化模型训练效果
if mode == "train": #规定了训练和验证的不同的transform模式
self.transform = train_transform
elif
self.transform = val_transform
def __getitem__(self, item):#取数据
return self.transform(self.X[item]), self.Y[item]#取数据的时候进行数据增广
def __len__(self):
return len(self.X) #返回数据集中图片的总数。
train_path = r"E:\深度学习\04.课程代码\第四五节_分类代码\food_classification\food-11_sample\training\labeled" #图片的路径放在了这里
train_set = foodDataset(train_path,mode="train")
train_loader = DataLoader(train_set, batch_size=4, shuffle=True)
val_path = r"E:\深度学习\04.课程代码\第四五节_分类代码\food_classification\food-11_sample\validation" #图片的路径放在了这里
val_set = foodDataset(val_path,mode="val")
val_loader = DataLoader(val_set, batch_size=4, shuffle=True)
for batch_x, batch_y in train_loader:
print(batch_x)
#-------------------------------------------------------------------------
#这段代码实现了一个图像分类任务的数据加载和预处理流程,包含以下核心功能:
#数据读取:从按类别分组的目录中加载图片和标签。
#数据增强:对训练集应用随机变换(裁剪、旋转)以提升模型泛化能力。
#数据格式转换:将图片转为 PyTorch 张量。
#数据加载器构建:生成可迭代的批次数据供模型训练使用。
模型部分:
#模型部分开始
class myModel(nn.Module):
def __init__(self, num_class): #定义一个继承自 nn.Module 的卷积神经网络类,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) #归一化 64 个通道,使训练更加稳定,加快收敛速度
self.relu = nn.ReLU()
self.pool1 = nn.MaxPool2d(2) #64*112*112
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) #展平操作,将多维特征转换为一维向量 (N,512,7,7) → (N,25088),不然没办法用全连接
x = self.fc1(x) #全连接层1,将特征映射到中间维度 (N,25088) → (N,1000)
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) #因为这个train_val不仅在可以这个项目用,在其他项目也可以用,故加一个这个。
plt_train_loss = [] #记录所有轮次的loss
plt_val_loss = []
plt_train_acc = []
plt_val_acc = []
max_acc = 0.00
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和y
x, target = batch_x.to(device), batch_y.to(device) #先把它放到你的设备上,target是Y,也就是真实值
pred = model(x) #让数据通过模型,得到预测值
train_bat_loss = loss(pred, target) #一批的loss
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())
#解释下1.pred.detach().cpu().numpy():把这个pred为矩阵
#np.argmax(pred.detach().cpu().numpy(), axis=1:
# argmax:取出最大值的下标,axis=1:即横轴取最大值下标
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, 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])
)
# 验证部分结束
可视化:
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()
训练配置与执行
model = myModel(11)
lr = 0.001
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=lr,weight_decay=1e-4) #优化器AdamW,比sgd更好用
#去了解AdamW
#对于sgd她是只看一个点的梯度,去找最陡的方向,只看当前点的梯度
#AdamW相比较于sgd有两个变化:1,梯度的变化 2.学习率的改变
#1,梯度的变化:
# AdamW在一个点的梯度不是看只看当前点的梯度,而是看前面哪些点的梯度然后再看当前点的梯度[AdamW=θ*old+(1-θ)new]其中θ可调整
#2. 学习率的改变
#AdamW会自动调整学习率
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)