import random
import torch
import torch.nn as nn
import numpy as np
import os
from torch.utils.data import DataLoader,Dataset
from PIL import Image
from tqdm import tqdm #看进度条
from torchvision.transforms import transforms
from torch import optim
import time
import matplotlib.pyplot as plt
1. 导入需要的库
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)
###############################################
2. 提高实验的可重复性
train_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.RandomResizedCrop(224),#将图片随机放大,并切出224*224
transforms.RandomResizedCrop(50),
transforms.ToTensor()
])
#验证集增广方式 只需变换维度和张量
val_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.ToTensor()
])
3. 用于数据增广, ToPILImage():将numpy的array和tensor转化为PILImage,RandomResizedCrop(224),将图片随机放大,并切出224*224大小,transforms.RandomResizedCrop(50):将图片在50度之内随机旋转,.ToTensor():将PILImage或array转化为tensor,训练集的图片需要进行数据增广,而验证集的图片只需转化为张量就行了。经过transform,实际的数据量没变,只不过每一个epoch里都会进行随机的变化,相当于增加了数据量
class FoodDataset(Dataset):
4. 写数据集 :三个函数
def __init__(self,path,mode):
x,y = self.read_file(path)
if mode == "train":
self.transform = train_transform
else:
self.transform = val_transform
self.x = x
self.y = torch.LongTensor(y) #规定
5. 初始化函数:需要传入你想定义的数据集的路径,和你的这个数据集的类型是什么 。xy通过readfile函数赋值,再根据数据集类型,进行不同的数据增广方式,再将x,y转变为数据集自身的属性,而不是局部变量,注意对x 和y进行的操作
def __getitem__(self,item):
return self.transform(self.x[item]), self.y[item]
6. 取样品函数: 要对x进行数据增广,y是分类,不用增广
def __len__(self):
return len(self.x)
7. 取长度函数
def read_file(self,path): #self在各个函数中传递
for i in tqdm(range(11)):
file_dir = path + "/%02d" % i # python也太jb灵活了吧
file_list = os.listdir(file_dir) # listdir会把上一行文件夹中所有文件都读出来
xi = np.zeros((len(file_list), HW, HW, 3),dtype = np.uint8) # 用矩阵将图片存放在一起,这个函数的第一个参数是shape,是一个元组,剩余两个参数可以不填。不规定dtype,默认为浮点数,规定读成整数
yi = np.zeros((len(file_list)))
for j, each in enumerate(file_list): # 调试可知file list是列表,而each就是图片名的字符串,j为list中的下标
img_path = file_dir + "/" + each # 字符串相相加 将图片读入计算机中
img = Image.open(img_path)
img = img.resize((HW, HW)) # 调试可知图片大小为512,要将其resize到224
xi[j, ...] = img # 省略号表示后面的参数自动等价
yi[j] = i # 将所取图片的下标也存进来,标签即i
# print(each)
if i == 0: # 第一个文件夹里的图片保存为x,
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
8. 读文件函数:读取11个文件夹下的各个图片,i相当于打开了每个文件,用字符串相加的方式得到每个图片每个文件夹的地址,用os.listdir()函数可以得到文件夹下所有图片的名称,组成一个列表。之后初始化两个零矩阵,xi存储取出来的图片信息,yi用来存储取出的类型信息,j为图片名在列表中的下标,each为其图片名,再用字符串相加的方式将文件夹名与图片名相加得到地址,用函数将图片读到img中,此时图片为512*512,要将其改变大小,变为我们需要的224*224,将图片信息存储到xi中,xi第一个参数为个数/下标,后面的参数可代表图片信息,图片信息为224*224*3所以用代码表示方式,…表示后面的参数自动等同为图片的参数,再将所取下标存下来,接下来判断,如果读的是第一个文件夹,就让xy等于xi yi,否则,就让后面的文件夹中提取出来的信息并在xy上,并返回xy
class myModel(nn.Module):
9. 写模型类
def __init__(self,num_class):
super(myModel, self).__init__()
#3*224*224 --> 512*7*7
self.layer1 = nn.Sequential(
nn.Conv2d(3,64,3,1,1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2)
)
#64*112*112 -->
# self.conv2 = nn.Conv2d(64, 128, 3, 1, 1)
# self.bn = nn.BatchNorm2d(128)
# self.relu2 = nn.ReLU()
# self.pool2 = nn.MaxPool2d(2)
self.layer2 = nn.Sequential(
nn.Conv2d(64, 128, 3, 1, 1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2)
)
self.layer3 = nn.Sequential(
nn.Conv2d(128, 256, 3, 1, 1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2)
)
self.layer4 = nn.Sequential(
nn.Conv2d(256, 512, 3, 1, 1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2)
)
self.pool1 = nn.MaxPool2d(2)
self.fc1 = nn.Linear(25088,1000)
self.relu = nn.ReLU()
self.fc2 = nn.Linear(1000,num_class)
10. 初始化函数: 对父类模型进行修改。Conv2d()接受5个参数,in_channel, out_channel, kernel_size, stride, padding, 第一层:输入3层,输出64层,用3*3的卷积核去卷,步长为1,并padding1,经过一系列处理,提取出图片中重要的特征,根据最后的参数512*7*7.写出最后的全连接层
def forward(self,x):
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.pool1(x)
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.relu(x)
x = self.fc2(x)
return x
11. 前向过程函数
def train_val(model, train_loader, val_loader, device, epochs, optimizer, save_path):
12. 训练流程函数: 需要传入几个参数
model = model.to(device)
plt_train_loss = []
plt_val_loss = []
plt_train_acc = []
plt_val_acc = []
max_val_acc = 0.0
13. 先将模型放在gpu上,用列表记录 训练和验证的loss,分类模型还讲究成功率,所以要定义训练和验证的准确率和最大的准确率,用于保存最佳模型
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()
14. 开始训练,每一轮都要定义该轮的训练和验证的loss,acc,还要记录此轮开始的时间
model.train() #将模型设置为训练模式
for batch_x,batch_y in train_loader: #一个epoch为训练所有数据,train_loader可以自动分批取出所有batch thought:train_loader代表的还是群体数据,只不过将其打乱分批
x, target = batch_x.to(device), batch_y.to(device) #将数据放在gpu上
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() #batloss是张量在gpu上,先取下来,再用item()取其数值
train_acc += np.sum(np.argmax(pred.detach().cpu().numpy(), axis= 1) == target.cpu().numpy()) #argmax()可以找到最大值所在的下标,直接写pred会报错,因为pred在gpu上,先从计算网上摘下来,放在cpu上,再转为numpy形式,注意在哪个维度上找最大值
plt_train_loss.append(train_loss/train_loader.dataset.__len__()) #求平均loss值
plt_train_acc.append(train_acc / train_loader.dataset.__len__())
15. 进入训练模式,在train_loader中取出特征和标签,将其放在gpu上并记为x,和target,将x经过模型,计算loss,此处为交叉熵损失,经过一系列操作后,更新了模型,将算出的loss取下来,取数值加到此轮的loss上,再计算准确率acc,计算方法:用np.argmax()找出pred的四份数据里的预测值最大的下标,并和实际比较,算出这四个中猜对的个数,(调试可知pred和前面那个都是array(4,),train_bat_loss()为tensor(),train_bat_loss.cpu().item()为float),通过loader将这一轮训练集数据取完之后,将得到的总的loss和acc将平均loss和acc记录在列表里
model.eval() #进入验证模式
with torch.no_grad(): #与训练集不同,验证集不能积累梯度 数据只要经过模型就一定会产生梯度
for batch_x, batch_y in val_loader:
x, target = batch_x.to(device), batch_y.to(device) # 将数据放在gpu上
pred = model(x) # 前向过程
val_bat_loss = loss(pred, target) # 算出loss
# val_bat_loss.backward() # 回传过程,算出梯度
# optimizer.step() # 更新参数
# optimizer.zero_grad() # 梯度归零 #不需要更新模型
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__())
16. 验证流程 :和训练流程相似,只是不计算梯度,不更新参数了
if val_acc > max_val_acc: #更新模型
torch.save(model,save_path)
max_val_acc = val_acc
print("[%03d/%03d] %2.2f secs Trainloss: %.6f Trainacc: %.6f | Valloss: %.6f Valacc: %.6f"%(epoch, epochs, time.time() - start_time, plt_train_loss[-1],plt_train_acc[-1], plt_val_loss[-1],plt_val_acc[-1]))
17. 存好最佳的模型
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()
18. 画图
model = myModel(11)
lr = 0.001
optimizer = optim.AdamW(model.parameters(), lr, weight_decay=0.001) #比sgd好用的优化器
device = "cuda" if torch.cuda.is_available() else "cpu"
epochs = 20
save_path = "model_save"
19. 定义的超参数 ,其中优化器改用AdamW
train_loader = DataLoader(train_set, batch_size=4, shuffle= True)
val_loader = DataLoader(val_set, batch_size= 4, shuffle= True)
# for bat_x,bat_y in train_loader:
# print(bat_x)
# pred = model(bat_x)
loss = nn.CrossEntropyLoss() #交叉熵损失,直接调用就行:传入模型输出和标签
train_val(model, train_loader, val_loader, device, epochs, optimizer, save_path)
20. 主体部分