python
import random
import torch
import torch.nn as nn
import numpy as np
import os
import time
import matplotlib.pyplot as plt
from matplotlib.font_manager import weight_dict
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。
# 这意味着代码中所有依赖随机数生成的操作都会使用相同的随机数序列,从而保证代码的可复现性。
# 比如说 a = random.randint(1,5) print(a) ,不管你运行多少次,他都是4,即你每次随机出来的a都是一样的
# ------------------------------------------------------------------------------------------------------------------
HW = 224 # 因为在该数据集中图片的大小都是512*512,但是在我们深度学习中默认图片是224*224
def read_file(path): # 函数功能:读入一个文件路径,它负责把路径的图片和标签读出来,即输出X和Y
for i in tqdm(range(11)): # 遍历training文件夹下labeled下面的11个文件夹,读取他们的数据
file_dir = os.path.join(path, "%02d" % i) # os.path.join(): 返回一个组合后的完整路径字符串,path是父目录,"%02d"%i 负责生成 00 到 10 这种固定格式的文件夹名
# 因为path路径下有00到10个文件夹,用os.path.join作为拼接,并用"%02d"%i来替换00------10文件夹
file_list = os.listdir(file_dir) # os.listdir(file_dir):返回一个列表,包含该目录(file_dir)下所有文件和文件夹的名称
xi = np.zeros((len(file_list), HW, HW, 3), dtype=np.uint8) # np.zeros:创建一个 "全零数组",有len(file_list)个小格子,每个格子的大小是 224×224×3
# xi表示装每一类的图片,因为for是0-11(11不取),刚好是labeled里面的所有文件夹,比如说x0表示第0类下所有的照片,x0有280个格子,每个格子可以放一个照片,每个格子的大小为(224,224,3)
yi = np.zeros(len(file_list), dtype=np.uint8) # # np.zeros:创建一个 "全零数组",有len(file_list)个小格子,即长度和当前文件夹的图片数量相同,每个格子初始化为0,可以存储100个标签值
for j, img_name in enumerate(tqdm((file_list))): # enumerate(file_list):同时获取图片的索引j和图片名称img_name,比如说第 1 张图j=0,第 2 张j=1
img_path = os.path.join(file_dir, img_name) # img_path得到的是file_dir文件夹下每个图片的地址
img = Image.open(img_path) # 使用PIL库打开图片文件
img = img.resize((HW, HW)) # 把该数据集中图片的尺寸改成224*224
# ------------------------------------------------------------------截止这一步,已经处理好了一张图片,接下来要把图片放到总的数据里
xi[j, ...] = img # 将处理后的图片数据存储到预分配的数组xi的第j个位置,...是省略号,相当于xi[j, :, :, :],表示第j张图片的所有像素和通道
yi[j] = i # 我们是从i文件夹下读的图片,所以它的标签是i
# ------------------------------------------------------------------截止这一步,已经处理好了一个文件夹图片(如00),接下来我们要把所有文件夹的图片放在X里面,
# 那我们可以初始用xi当做X,后面读取01文件夹,就可以直接添加在X里面,02,03同理
if i == 0:
X = xi
Y = yi
else:
X = np.concatenate((X, xi), axis=0) # 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( # 因为验证集上面不需要额外在对图片进行增广变换
[
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)
tranin_path = r"D:\李哥_深度学习\04.课程代码\第四五节_分类代码\food_classification\food-11_sample\training\labeled"
val_path = r"D:\李哥_深度学习\04.课程代码\第四五节_分类代码\food_classification\food-11_sample\validation"
train_set = food_Dataset(tranin_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)
# read_file(path)
class myModel(nn.Module): # 模型
def __init__(self, num_class): # 结果分为几类,在这里是11类
super(myModel, self).__init__()
self.conv1 = nn.Conv2d(3, 64, 3, 1, 1) # 原始的图片为3*224*224 ,经过conv1为64*224*224
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU()
self.pool1 = nn.MaxPool2d(2) # pool的通道数始终不变(输入 C=64,输出 C=64), 64*224*224 ---> 64*112*112
self.layer1 = nn.Sequential(
nn.Conv2d(64, 128, 3, 1, 1), # 64*112*112---> 128*112*112
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2) # 128*112*112--> 128*56*56
)
self.layer2 = nn.Sequential(
nn.Conv2d(128, 256, 3, 1, 1), # 128*56*56--->256*56*56
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2) # 256*56*56--> 256*28*28
)
self.layer3 = nn.Sequential(
nn.Conv2d(256, 512, 3, 1, 1), # 256*28*28--->512*28*28
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2) # 512*28*28--> 512*14*14
)
self.pool2 = nn.MaxPool2d(2) # 512*14*14--->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)
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)
# 记录所有轮次的loss
plt_train_loss = [] # 这是记录所有轮次的loss的平均值的列表, 即记录每个 epoch 的平均训练损失,比如说[第一个epoch的平均损失,第二个,....]
plt_val_loss = []
plt_train_acc = [] # 这是记录所有轮次的acc的平均值的列表, 即记录每个 epoch 的平均准确率,比如说[第一个epoch的准确率,第二个,....]
plt_val_acc = []
max_acc = 0.0
for epoch in range(epochs): # 冲锋的号角,训练开始,如果以后看不懂其他大佬写的代码,只要看到这个就知道开始了训练过程
train_loss = 0.0 # 记录累加当前 epoch 中所有训练批次的损失
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(即取四张图,batch_y为四张图对应的真实标签)
x, target = batch_x.to(device), batch_y.to(device) # 先把它放到你的设备上,target是Y,也就是真实值,一个包含4个元素的张量,因为batchsize为4,比如[0,3,1,7]就表示第一张图类别为0,第二张为3,....
pred = model(x) # 让数据通过模型,得到预测值
train_bat_loss = loss(pred, target) # 求target和pred的loss
train_bat_loss.backward() # 梯度回传(反向传播),这一步的目的利用链式法则,计算 "损失函数对每个参数的偏导数(梯度),梯度的作用:告诉优化器 "每个参数应该往哪个方向调整、调整多少" 才能减小损失
optimizer.step() # 更新模型的作用 ,根据反向传播得到的梯度,更新模型的所有参数
optimizer.zero_grad() # 作用:清空当前批次计算出的梯度(将所有参数的梯度重置为 0)。PyTorch 中梯度会累积(即下一批次的梯度会加上当前批次的梯度),如果不清空,会导致梯度计算错误(用多批次的累积梯度更新参数,不符合梯度下降逻辑)。
train_loss += train_bat_loss.cpu().item() # 作用:将当前批次的损失累加到 train_loss 中,即当前 epoch 中 所有批次的损失总和
# .cpu()是因为train_bat_loss的结果是张量放在了GPU上,需要把他放到CPU,item() 将张量转换为 Python 数值(如 25.6),方便累加。
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(),axis=1) == target.cpu().numpy()) # 简单来说统计当前批次中预测正确的样本数量
# pred.detach().cpu().numpy(): 先剥离pred的梯度信息(脱离计算图),再将其从 GPU(若存在)转移到 CPU,最后转换为 numpy 数组
# np.argmax(..., axis=1):表示从横轴去得到最大值的下标,target.cpu().numpy(): 将 GPU 上的真实标签张量(target)移到 CPU,再转成 numpy 数组
#
plt_train_loss.append(train_loss / train_loader.__len__()) # train_loss / train_loader.__len__() :计算当前 epoch 的平均训练损失
# train_loader.__len__():返回训练数据加载器(train_loader)中 "批次的总数,
# 如果你的训练集有 1000 个样本,batch_size=16,那么 train_loader 会分成 63 个批次,此时 train_loader.__len__() 的结果就是 63。
# 举例:假设当前 epoch 有 3 个批次,损失分别是 2.0、3.0、5.0,那么 train_loss=10.0,train_loader.__len__()=3,平均损失就是 10.0/3≈3.33。
plt_train_acc.append(train_acc / train_loader.dataset.__len__())
# 验证部分开始
model.eval() # 调整为验证模式
with torch.no_grad(): # 因为验证集是不能更新模型的,只看它的效果,也就是说不能更新梯度(ipad笔记上有)
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) # 对于验证集来说loss越小,代表模型效果越好
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) # PyTorch 提供的通用保存函数,model:要保存的模型实例,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来说一个"%",就是一个格式化的输出,这里有5个"%",即代表5个格式化输出
# 验证部分结束
# ----------------------------------------------------------------------------------------------------------------------
#可视化
plt.plot(plt_train_loss) # plt_train_loss 是一个列表,存储了每个 epoch 的平均训练损失(例如 [5.2, 4.8, 4.1, ..., 1.2]);
plt.plot(plt_val_loss) # 同上 ,横轴为 epoch 序号,纵轴为损失值
# 注:plt.plot() 处理 "单列表输入" 时的 默认行为,当你调用 plt.plot(plt_train_loss) 时,matplotlib 会默认,纵轴(y 轴):取列表中的 值,横轴(x 轴):取列表的 索引
plt.title("loss")
plt.legend(["train", "val"]) # 功能:添加图例(图表中的 "标签说明"),区分两条曲线分别代表什么。
# 列表 ["train", "val"] 与前面的 plt.plot() 顺序对应,必须要对应,因为plt_train_loss在前面。
plt.show()
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
# 注:plt.plot() 处理 "单列表输入" 时的 默认行为,当你调用 plt.plot(plt_train_loss) 时,matplotlib 会默认,纵轴(y 轴):取列表中的 值,横轴(x 轴):取列表的 索引
plt.title("acc")
plt.legend(["train", "val"]) # 功能:添加图例(图表中的 "标签说明"),区分两条曲线分别代表什么。
# 列表 ["train", "val"] 与前面的 plt.plot() 顺序对应,必须要对应,因为plt_train_loss在前面。
plt.show()
# 可视化结束
# -------------------------------------------------------------------------------------------------------------------
model = myModel(11)
lr = 0.001
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) # model.parameters(): 即返回模型中所有需要训练的参数,比如说在回归当中的W和b
device = "cuda" if torch.cuda.is_available() else "cpu"
save_path = "model_save/best_model_9_23.pth"
epochs = 15
train_val(model, train_loader, val_loader, device, epochs, optimizer, loss, save_path)
# for batch_x, batch_y in train_loader:
# pred = model(batch_x)
问题1:train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(),axis=1) == target.cpu().numpy()) # 简单来说就是统计当前批次的准确数量,这句话理解的对吗?
问题2:for batch_x, batch_y in train_loader:
x, target = batch_x.to(device), batch_y.to(device) ,其中x代表图片,y代表图片对应的标签,该怎么理解?