源码
python">import copy
import timeimport torch
from torchvision.datasets import FashionMNIST
from torchvision import transforms
import torch.utils.data as Data
import numpy as np
import matplotlib.pyplot as plt
from model import LeNet
import torch.nn as nn
import pandas as pddef train_val_data_process():train_data = FashionMNIST(root='./data',train=True,transform=transforms.Compose([transforms.Resize(size=28), transforms.ToTensor()]),download=True)train_data, val_data = Data.random_split(train_data, [round(0.8*len(train_data)), round(0.2*len(train_data))])train_dataloader = Data.DataLoader(dataset=train_data,batch_size=32,shuffle=True,num_workers=2)val_dataloader = Data.DataLoader(dataset=val_data,batch_size=32,shuffle=True,num_workers=2)return train_dataloader, val_dataloaderdef train_model_process(model, train_dataloader, val_dataloader, num_epochs):# 设定训练所用到的设备,有GPU用GPU没有GPU用CPUdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")# 使用Adam优化器,学习率为0.001optimizer = torch.optim.Adam(model.parameters(), lr=0.001)# 损失函数为交叉熵函数criterion = nn.CrossEntropyLoss()# 将模型放入到训练设备中model = model.to(device)# 复制当前模型的参数best_model_wts = copy.deepcopy(model.state_dict())# 初始化参数# 最高准确度best_acc = 0.0# 训练集损失列表train_loss_all = []# 验证集损失列表val_loss_all = []# 训练集准确度列表train_acc_all = []# 验证集准确度列表val_acc_all = []# 当前时间since = time.time()for epoch in range(num_epochs):print("Epoch {}/{}".format(epoch, num_epochs-1))print("-"*10)# 初始化参数# 训练集损失函数train_loss = 0.0# 训练集准确度train_corrects = 0# 验证集损失函数val_loss = 0.0# 验证集准确度val_corrects = 0# 训练集样本数量train_num = 0# 验证集样本数量val_num = 0# 对每一个mini-batch训练和计算for step, (b_x, b_y) in enumerate(train_dataloader):# 将特征放入到训练设备中b_x = b_x.to(device)# 将标签放入到训练设备中b_y = b_y.to(device)# 设置模型为训练模式model.train()# 前向传播过程,输入为一个batch,输出为一个batch中对应的预测output = model(b_x)# 查找每一行中最大值对应的行标pre_lab = torch.argmax(output, dim=1)# 计算每一个batch的损失函数loss = criterion(output, b_y)# 将梯度初始化为0optimizer.zero_grad()# 反向传播计算loss.backward()# 根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值的作用optimizer.step()# 对损失函数进行累加train_loss += loss.item() * b_x.size(0)# 如果预测正确,则准确度train_corrects加1train_corrects += torch.sum(pre_lab == b_y.data)# 当前用于训练的样本数量train_num += b_x.size(0)for step, (b_x, b_y) in enumerate(val_dataloader):# 将特征放入到验证设备中b_x = b_x.to(device)# 将标签放入到验证设备中b_y = b_y.to(device)# 设置模型为评估模式model.eval()# 前向传播过程,输入为一个batch,输出为一个batch中对应的预测output = model(b_x)# 查找每一行中最大值对应的行标pre_lab = torch.argmax(output, dim=1)# 计算每一个batch的损失函数loss = criterion(output, b_y)# 对损失函数进行累加val_loss += loss.item() * b_x.size(0)# 如果预测正确,则准确度train_corrects加1val_corrects += torch.sum(pre_lab == b_y.data)# 当前用于验证的样本数量val_num += b_x.size(0)# 计算并保存每一次迭代的loss值和准确率# 计算并保存训练集的loss值train_loss_all.append(train_loss / train_num)# 计算并保存训练集的准确率train_acc_all.append(train_corrects.double().item() / train_num)# 计算并保存验证集的loss值val_loss_all.append(val_loss / val_num)# 计算并保存验证集的准确率val_acc_all.append(val_corrects.double().item() / val_num)print("{} train loss:{:.4f} train acc: {:.4f}".format(epoch, train_loss_all[-1], train_acc_all[-1]))print("{} val loss:{:.4f} val acc: {:.4f}".format(epoch, val_loss_all[-1], val_acc_all[-1]))if val_acc_all[-1] > best_acc:# 保存当前最高准确度best_acc = val_acc_all[-1]# 保存当前最高准确度的模型参数best_model_wts = copy.deepcopy(model.state_dict())# 计算训练和验证的耗时time_use = time.time() - sinceprint("训练和验证耗费的时间{:.0f}m{:.0f}s".format(time_use//60, time_use%60))# 选择最优参数,保存最优参数的模型torch.save(best_model_wts, "G:/PycharmProjects/LeNet/best_model.pth")train_process = pd.DataFrame(data={"epoch":range(num_epochs),"train_loss_all":train_loss_all,"val_loss_all":val_loss_all,"train_acc_all":train_acc_all,"val_acc_all":val_acc_all,})return train_processdef matplot_acc_loss(train_process):# 显示每一次迭代后的训练集和验证集的损失函数和准确率plt.figure(figsize=(12, 4))plt.subplot(1, 2, 1)plt.plot(train_process['epoch'], train_process.train_loss_all, "ro-", label="Train loss")plt.plot(train_process['epoch'], train_process.val_loss_all, "bs-", label="Val loss")plt.legend()plt.xlabel("epoch")plt.ylabel("Loss")plt.subplot(1, 2, 2)plt.plot(train_process['epoch'], train_process.train_acc_all, "ro-", label="Train acc")plt.plot(train_process['epoch'], train_process.val_acc_all, "bs-", label="Val acc")plt.xlabel("epoch")plt.ylabel("acc")plt.legend()plt.show()if __name__ == '__main__':# 加载需要的模型LeNet = LeNet()# 加载数据集train_data, val_data = train_val_data_process()# 利用现有的模型进行模型的训练train_process = train_model_process(LeNet, train_data, val_data, num_epochs=20)matplot_acc_loss(train_process)
从代码里可以看出,模型训练代码主要可以分为:数据加载,模型训练,可视化展示。
数据加载
python">def train_val_data_process():train_data = FashionMNIST(root='./data',train=True,transform=transforms.Compose([transforms.Resize(size=28), transforms.ToTensor()]),download=True)train_data, val_data = Data.random_split(train_data, [round(0.8*len(train_data)), round(0.2*len(train_data))])train_dataloader = Data.DataLoader(dataset=train_data,batch_size=32,shuffle=True,num_workers=2)val_dataloader = Data.DataLoader(dataset=val_data,batch_size=32,shuffle=True,num_workers=2)return train_dataloader, val_dataloader
加载FashionMNIST数据集
- 使用
FashionMNIST
类从torchvision.datasets
模块中加载FashionMNIST数据集。 root='./data'
参数指定数据集的下载和存储位置,存放位置在项目的根目录/data文件夹。train=True
参数指定加载训练集数据。transform=transforms.Compose([...])
参数定义了数据预处理操作,包括将图像大小调整为28x28像素,并将图像数据转换为PyTorch张量。download=True
参数表示如果数据集不存在于指定位置,则自动下载;如果数据集已经存在,则不用下载。
划分训练集和验证集
- 使用
Data.random_split
函数将加载的训练集数据随机划分为训练集和验证集。 - 划分比例为80%训练集和20%验证集,通过
round
函数确保划分后的数据集大小为整数。通常训练集的数据个数要大于验证集的数据个数
创建数据加载器
- 为训练集和验证集分别创建
DataLoader
实例。 batch_size=32
参数指定每个批次加载的数据样本数量。注意batch_size受设备显存影响,如果设置的过大,在训练时会出现爆显存的情况。同样,如果设备显存足够,推荐增加batch_size的值,可以提升训练的速度。shuffle=True
参数表示在每个epoch开始时打乱数据顺序,增加模型的泛化能力。num_workers=2
参数指定使用2个工作线程来加速数据加载过程。
返回数据加载器
- 函数最后返回训练集和验证集的数据加载器,供后续的训练和验证过程使用。
模型训练
模型训练的代码算是整个项目里最复杂的了,但是吃透了这块代码,之后可以大量复用到其他项目里,提升开发效率。
训练参数设定
python"> # 设定训练所用到的设备,有GPU用GPU没有GPU用CPUdevice = torch.device("cuda" if torch.cuda.is_available() else "cpu")# 使用Adam优化器,学习率为0.001optimizer = torch.optim.Adam(model.parameters(), lr=0.001)# 损失函数为交叉熵函数criterion = nn.CrossEntropyLoss()# 将模型放入到训练设备中model = model.to(device)# 复制当前模型的参数best_model_wts = copy.deepcopy(model.state_dict())# 初始化参数# 最高准确度best_acc = 0.0# 训练集损失列表train_loss_all = []# 验证集损失列表val_loss_all = []# 训练集准确度列表train_acc_all = []# 验证集准确度列表val_acc_all = []# 当前时间since = time.time()
一上来就是重量级,因为训练参数对模型训练影响巨大,其中的核心是优化器和损失函数。
优化器设置
这次训练我们使用adam优化器,并设置学习率为0.001。
adam优化器的基本原理:
python">自适应调整学习率:Adam优化器可以根据历史梯度信息来自适应地调节学习率。在训练初期,它使用较大的学习率以快速收敛;在训练后期,它使用较小的学习率以更准确地找到损失函数的最小值。
动量参数调整:Adam优化器能够调整动量参数,以平衡上一次梯度和当前梯度对参数更新的影响。这有助于避免过早陷入局部极小值,从而提高训练的稳定性和效果。
参数更新归一化:Adam优化器对参数的更新进行了归一化处理,使得每个参数的更新都有一个相似的量级。这有助于加快收敛速度并提高训练效果。
L2正则化:Adam优化器结合了L2正则化的思想,在更新时对参数进行正则化,从而防止神经网络过度拟合训练数据。
从基本原理可以看出,adam优化器包括其他的优化器,本质上都是帮我们动态调节学习率,并结合其他策略,优化梯度下降过程。
学习率的基本原理:
python">学习率决定了在每一次训练迭代中,模型权重更新的步长大小。简而言之,它控制了模型参数的更新速度。较高的学习率可以加速模型的收敛,但可能会导致模型不稳定,出现震荡;而较低的学习率虽然可以保证模型的稳定性,但收敛速度会变慢,训练耗时增长
从基本原理可以看出,学习率设置过大或者过小都存在负面影响,但是设置较小的值可以保证模型训练的稳定性,因此通常会设置一个较小的值。
损失函数
我们这次使用的是分类任务里常用的交叉熵损失函数。
交叉熵损失函数举例:
设我们有一个二分类问题,比如识别一张图片是否是猫。我们的模型对一张图片进行了预测,给出了这张图片是猫的概率(即模型的预测值),同时我们也知道这张图片的真实标签(即是猫还是不是猫)。
真实标签:这张图片实际上是一只猫,所以真实标签 y1 = 1。
模型预测:模型预测这张图片是猫的概率为 0.8,即 y2 = 0.8。
根据交叉熵损失函数的公式(对于二分类问题):
L = -(y1 * log(y2) + (1 - y1) * log(1 - y2))
我们可以将真实标签和模型预测值代入公式中计算损失:
L = -(1 * log(0.8) + (1 - 1) * log(1 - 0.8))
= -log(0.8)
≈ 0.22314(使用了自然对数)
这个损失值表示模型预测的概率分布与真实分布之间的差异。理想情况下,如果模型完全准确,预测值应该是1(即模型100%确定这张图片是猫),此时交叉熵损失为0,因为-log(1) = 0。而在实际中,模型的预测可能会有一些误差,所以损失值会大于0。
保存权重
python">best_model_wts = copy.deepcopy(model.state_dict())
model.state_dict()
是一个非常重要的方法,它返回了一个包含模型所有权重(参数)和偏置的字典。这个字典的键是每一层参数的名称,而值则是对应的参数(通常是torch.Tensor
对象)。
然后,使用了Python标准库中的copy
模块的deepcopy()
函数来创建一个这些权重和偏置的深拷贝。最后,将这个深拷贝的权重字典赋值给变量best_model_wts
。这个变量现在保存了模型在某个时刻(比如训练过程中的最佳时刻)的权重和偏置。
循环训练
训练阶段
在训练阶段,模型通过前向传播计算预测,通过损失函数计算误差,然后通过反向传播更新权重。
python">for step, (b_x, b_y) in enumerate(train_dataloader):
在循环的时候,我们会通过enumerate方式来获取dataloader里面每个批次的参数。注意这里是“批次的参数”而不是“每个参数”。enumerate(train_dataloader)
会为train_dataloader
中的每个批次生成一个索引(step
)和该批次的数据((b_x, b_y)
)。其中,step
是当前批次的索引(从0开始计数),b_x
是当前批次的特征数据,b_y
是当前批次对应的标签数据。之所以这样操作是因为我们是按batch为单位进行训练的。
前向传播
python"> # 前向传播过程,输入为一个batch,输出为一个batch中对应的预测output = model(b_x)# 查找每一行中最大值对应的行标pre_lab = torch.argmax(output, dim=1)# 计算每一个batch的损失函数loss = criterion(output, b_y)
前向传播主要做的事情是:
- 将一个batch的特征数据输入到模型里,通过计算得到输出数据。
- 将输出数据以单列的形式展开,去查找这一列数据的最大值。这一步操作的目的是获取到这次运算的“答案”。也就是说,通过运算后概率最大的选项,就是这一次运算选择出的结果。比如这次运算后。识别为猫的概率是0.2,识别为狗的概率是0.8,那么就认为这一次的识别结果是狗。
- 将标签数据和输出数据进行损失函数计算。
反向传播
python"> #将梯度初始化为0optimizer.zero_grad()# 反向传播计算loss.backward()# 根据网络反向传播的梯度信息来更新网络的参数,以起到降低loss函数计算值的作用optimizer.step()# 对损失函数进行累加train_loss += loss.item() * b_x.size(0)# 如果预测正确,则准确度train_corrects加1train_corrects += torch.sum(pre_lab == b_y.data)
反向传播主要做的事情是:
- 每个批次开始训练前先把梯度清零,保证每个批次的训练独立。
- 反向传播计算,并更新网络参数。
- 计算损失函数累加值
- 计算预测正确的次数
这里比较复杂的是损失函数累加和预测正确次数累加。
python">train_loss += loss.item() * b_x.size(0)
这里的loss是当前批次的损失函数,loss.item()是将损失值(一个torch.Tensor
对象)转换为一个Python标量(scalar) 。b_x.size(0)是b_x数据的第一个维度,也就是数据个数。
这样的计算得到了每个批次的样本总损失,并累加,到后面可以计算在整个数据集上的平均损失,更好地评估损失性能。
python">train_corrects += torch.sum(pre_lab == b_y.data)
由前面的内容可以知道,pre_lab是当前批次的预测结果。随后进行逐元素比较操作,它会比较pre_lab
和b_y
(或b_y.data
)中的每个元素,如果相等则返回True
,说明预测成功;否则返回False
。结果是一个布尔型张量,其形状与pre_lab
和b_y
相同。 torch.sum()计算的是张量中所有元素的和,这样就可以计算出该批次中预测正确的样本数量。
之后我们可以通过正确样本数量的累积,来计算在整个数据集上的平均精度。
验证阶段
在验证阶段,我们使用验证集来评估模型的性能,但不更新模型的权重。
通过代码可以看出,验证阶段只进行正向传播,不进行反向传播(验证不需要调整模型参数)。计算出的loss也只进行统计,方便后面我们观察模型在验证集上的表现
模型保存
python"> # 计算并保存验证集的准确率val_acc_all.append(val_corrects.double().item() / val_num)if val_acc_all[-1] > best_acc:# 保存当前最高准确度best_acc = val_acc_all[-1]# 保存当前最高准确度的模型参数best_model_wts = copy.deepcopy(model.state_dict())# 选择最优参数,保存最优参数的模型torch.save(best_model_wts, "G:/PycharmProjects/LeNet/best_model.pth")
在模型保存阶段,我们会在每个轮次结束后,计算验证集的准确率,并随时更新当前的准确率。当前轮次的准确率达到最高记录时,就将模型参数进行深拷贝,存在本地文件里。
运行
python">if __name__ == '__main__':# 加载需要的模型LeNet = LeNet()# 加载数据集train_data, val_data = train_val_data_process()# 利用现有的模型进行模型的训练train_process = train_model_process(LeNet, train_data, val_data, num_epochs=20)matplot_acc_loss(train_process)
运行比较简单,就是加载模型,加载数据,开始训练。这里我们设置训练轮次为20,可以根据loss曲线来判断训练是否已经完成收敛,如果还没有收敛,则需要增加轮次。
从训练的图表可以看到,训练到20轮,loss值逐渐降低,acc值也小幅度逐渐提升,这个情况下我们可以继续增加轮次。这个时候我们的准确度是0.6413。
当训练的轮数增加到25轮时,可以看到在23,24轮左右的时候acc和loss都有明显的变化,准确度进一步提升到了0.7947。需要注意的是,轮次不是越大越好,轮次太大可能会导致过拟合现象。