LeNet-5模型
https://blog.csdn.net/algorithmPro/article/details/117489941
LeNet-5网络结构
输入层:输入大小28x28,通道数为1。注意:本层不算LeNet-5的网络结构,一般情况下不将输入层视为网络层次结构之一。
C1-卷积层:输入大小28x28,通道数为1;输出大小28x28,通道数为6;卷积核大小为5x5;步长为1;边缘补零为2;激活函数为ReLU。
S2-池化层:输入大小28x28,通道数为6;输出大小14x14,通道数为6;池化核大小为2x2;步长为2;池化方式为最大池化。
C3-卷积层:输入大小14x14,通道数为6;输出大小10x10,通道数为16;卷积核大小为5x5;步长为1;边缘补零为0;激活函数为ReLU。
S4-池化层:输入大小10x10,通道数为16;输出大小5x5,通道数为16;池化核大小为2x2;步长为2;池化方式为最大池化。
C5-卷积层:输入大小5x5,通道数为16;输出大小1x1,通道数为120;卷积核大小为5x5;步长为1;边缘补零为0;激活函数为ReLU。注意:这层也可以看作全连接层,可以通过全连接的方法实现。
F6-全连接层:输入为120维向量;输出为84维向量;激活函数为ReLU。
OUTPUT-输出层:输入为84维向量;输出为10维向量。注意:该层也是全连接层,且不带激活函数。
对于网络的具体分析及卷积、池化、全连接原理详见:LeNet-5详解https://blog.csdn.net/qq_40714949/article/details/109863595
搭建模型
模型被定义后就需要对模型进行初始化和前向传播,在nn.Module
中需要__init__
和forward
两个函数对一个模型进行实现。__init__
函数即初始化,主要用于定义每一层的构成,如卷积、池化层等;forward
函数即前向传播,主要用于确定每一层之间的顺序,使得模型可以正常使用。这就跟我们制作手链一样,在初始化部分中确定我们的手链中要用哪些珠子,这些珠子分别是怎么样的;在前向传播过程中用线将每一个珠子穿起来,使得其成为一个完整的手链。
Conv2d2维卷积层
nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias, padding_mode)
in_channels:输入的通道数
out_channels:输出的通道数
kernel_size:卷积核的大小。若卷积核是方形,则只需要一个整数边长;若不是方形,则需要输入一个元组表示高和宽
stride:卷积核每次滑动的步长,默认为1
padding:设置边缘补零的大小(也就是在输入特征图外围增加几圈0)
dilation:控制卷积核之间的间距,默认为0;若使用空洞卷积则需要对该参数进行设置
groups:控制输入和输出之间的连接,平时不常用,若使用分组卷积则需要设置该参数
bias:是否设置偏置,默认为 True
padding_mode:边缘补零模式,默认为”zeros“
MaxPool2d最大池化层
nn.MaxPool2d(kernel_size, stride, padding, dilation, return_indices, ceil_mode)
return_indices:表示返回值中是否包含最大值位置的索引,默认为False
ceil_mode:其用于计算输出特征图形状的时候,是使用向上取整还是向下取整。False表示向下取整,True表示向上取整,默认为False
ReLU激活函数
nn.ReLU(inplace)
inplace:是否进行就地运算,就地运算可以节省内存,但是会使得输入数据发生改变,默认为False
一般情况下,我们不需要对ReLU设置参数
全连接层
nn.Linear(in_features, out_features, bias)
in_features:输入向量维度
out_features:输出向量维度
bias:是否设置偏置,默认为True
前向传播函数
需要传入self和输入的变量,一般写为x,即forward(self, x)
。然后我们在函数内把之前定义好的层按顺序调用,每一层在计算后会返回结果,我们需要一个变量进行保存,即c1 = self.C1(x)
,在最后,我们需要将最后一步的计算结果返回。当网络中不存在跳跃连接或密集连接等分支结构的情况下,我们可以直接用x
作为中间变量。
图像数据张量
在pytorch中,我们的图像数据以一个四维的张量传入模型,其形状为[batch, channels, h, w]
。其中,batch即批大小,我们一般会一次性将一批图像送进网络处理,这一批图像的数量即为批大小;channel即通道数,也就是之前卷积层的channels;h和w分别代表图像的高和宽。
x.view()
Conv2d和MaxPool2d都接受以上形状的输入,ReLU接受任意形状的输入,而Linear只接受传入一个二维的张量,形状为[batch, length]
,length表示长度,即向量的维度。在这里,我们需要把之前卷积层输出的四维张量转换为二维张量,而.view()
可以实现这个操作,我们在需要处理的张量上直接使用view方法,然后输入需要改变的维度,比如说我们最后一个卷积层生成的特征图形状为[batch, 120, 1, 1]
,我们要将其转换为[batch, 120]
,若已知batch的大小,我们就可以直接batch和120填入括号中。
x.size(0)
但是,在实际使用中batch大小可能会随着超参数的变化而改变,因此我们可以直接使用.size()
方法,在括号内填上维度即可返回所在维度的大小,如x是一个形状为[16, 3, 384, 256]
的张量,则x.size(0)
为16,即为batch的大小。之后我们需要在view中填入另一个参数,在这里我们知道是120,就可以直接填写120
不过我们在这边也可以填入-1,填写-1会让电脑自动帮我们计算这一栏所需参数的大小,这个方法在实际搭建模型的时候非常好用,因此大家一般都会写为-1。最后就需要一个变量保存这部分的返回值了,我们依然可以直接用x
来保存。综上,我们只需要x = x.view(x.size(0), -1)
这一句话即可。
torch.randn(1, 1, 28, 28)
定义一个随机张量
下载数据集
新建一个download_dataset.py
文件
新建一个文件夹data
torchvision.datasets
torchvision.datasets.MNIST(root, train, transform, target_transform, download)`
打开download_dataset.py
文件,然后导入torchvision
包,在torchvision.dataset
中有很多经典的数据集,我们可以将其下载下来。我们需要下载MNIST数据集,在这里可以使用一句话来完成:我们只需要设置root为我们的data文件夹,将download设置为True即可完成下载。
训练模型
初始化和导入模型
torch.utils
torch.utils.data.DataLoader
主要是对数据进行 batch 的划分。数据加载器,结合了数据集和取样器,并且可以提供多个线程处理数据集。
在训练模型时使用到此函数,用来把训练数据分成多个小组 ,此函数每次抛出一组数据 。直至把所有的数据都抛出。就是做一个数据的初始化。
好处:使用DataLoader的好处是,可以快速的迭代数据。用于生成迭代数据非常方便。
注意:除此之外,特别要注意的是输入进函数的数据一定得是可迭代的。如果是自定的数据集的话可以在定义类中用def__len__、def__getitem__定义。
定义超参数、数据集和DataLoader
超参数定义
Epoch = 5,batch_size = 64,lr = 0.001
torchvision.transforms.ToTensor()
导入我们下载的数据集了。MNIST包含训练集和测试集,在这里,我们只需要其训练集。因此,我们可以定义一个train_data
用于导入MNIST的训练集,并利用torchvision.transforms.ToTensor()
将形状为[h, w, channel]
,值为0~255之间的uint8
图像转换成形状为[channel, h ,w]
,值在0~1之间的torch.FloatTensor
:
train_data = torchvision.datasets.MNIST(root='./data/', train=True, transform=torchvision.transforms.ToTensor(), download=False)
.DataLoader
定义完训练集后我们需要定义一个DataLoader将train_data
中的数据喂给模型。在pytorch中,DataLoader的定义如下:
Data.DataLoader(dataset, batch_size, shuffle, sampler, batch_sampler, num_workers, collate_fn, pin_memory, drop_last, timeout, worker_init_fn, prefetch_factor, persistent_workers)
以下是其常用参数的介绍:
dataset:数据集,可以使用
Data.DataSet
类或者torchvision.datasets
batch_size:批大小,每次迭代送入模型的图像数量
shuffle:是否打乱数据集,默认为False
num_workers:使用的线程数,DataLoader支持多线程读取数据以提升效率,该值为0或1是使用单线程进行读取。一般情况下该值不要超过cpu的最大线程,如果使用GPU训练模型的话该值越大其显存占用也会越大,日常使用中需要根据电脑的配置进行调节。默认为0
pin_memory:是否使用锁页内存,可以理解为是否将数据集全部强制加载进内存,且不与虚拟内存进行交换,设为True的话可以使得模型的训练快一些,默认为False
drop_last:是否丢弃最后不足batch_size的数据。有时候,数据集并不能整除batch_size,最后一批图像的数量会小于batch_size,这个参数决定是否将这一批数据丢弃,默认为False
定义损失函数和优化器
损失函数nn.CrossEntropyLoss()
手写数字分类器是一个单标签分类问题,即每次预测结果的标签仅有一个,因此,我们可以使用交叉熵损失作为我们的损失函数,即nn.CrossEntropyLoss()
。
loss_function = nn.CrossEntropyLoss()
Adam
优化器
优化器我们使用当前常用的Adam
优化器,其定义为torch.optim.Adam(parameters, lr)
,我们需要传入模型的参数和初始学习率,模型参数我们可以使用model.parameters()
来获得,初始学习率即我们先前定义好的lr
。
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
model.parameters()
model.parameters()保存的是Weights和Bais参数的值。它会返回模型中的所有参数
梯度传播
自动求导torch.set_grad_enabled(True)
神经网络在训练时需要计算梯度,并在反向传播时候使用,因此我们要启用pytorch的自动求导机制,并利用其实现反向传播和参数更新。
在这里,我们只需要简单的一句torch.set_grad_enabled(True)
即可实现,其表示在接下来的计算中每一次运算产生的节点都是可以求导的。
model.train()
然后,我们需要使用model.train()
方法,该方法用于启用Batch Normalization层和Dropout层。虽然在我们的模型中并没有这两层,但是我们不妨将其加上该部分写法如下:
torch.set_grad_enabled(True)
model.train()
使用CUDA加速
神经网络的训练一般是在GPU上进行的,我们将模型转移至显卡只需要一句model.cuda()
即可,训练时我们也需要将我们的输入数据x
、标签y
分别使用.cuda()
传至显卡。这是大部分教程中常用的方法。
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model.to(device)
torch.device()
但是,我们可以通过torch.device()
来指定使用的设备device
,
.to()方法
然后通过.to()
方法将模型和数据放到指定的设备上,这样我们就可以通过定义device
来指定是在cpu还是显卡上进行训练了,而且在多显卡的情况下也可以指定使用其中的某一张显卡进行训练。
torch.cuda.is_available()
但是,并不是所有的设备都支持CUDA加速的,而torch.cuda.is_available()
可以判断本设备是否支持CUDA,如果支持就返回True,不支持就返回False。有了这个函数,我们就可以让其自动判断是否支持CUDA加速并自动选择设备了。而不支持CUDA加速的设备我们可以使用cpu来进行。
开始训练
训练步骤
在pytorch中,神经网络的训练一般是分以下几个步骤进行的:
- 获得DataLoader中的数据
x
和标签y
- 将优化器的梯度清零
- 将数据送入模型中获得预测的结果
y_pred
- 将标签和预测结果送入损失函数获得损失
- 将损失值反向传播
- 使用优化器对模型的参数进行更新
以上这六个步骤分别对应着代码中的六行,在pytorch中,只需要这六行即可完成一次迭代。但是,我们的数据集不仅仅有这一次迭代,而且我们也要遍历数据集不止一次。因此,我们首先需要一个循环用来遍历数据集,即Epoch
:
for epoch in range(Epoch):
在写好第一个循环后我们确定了遍历数据集的次数,然后,我们就要写每一次遍历中的迭代了。DataLoader
本质上是一个迭代器,因此我们在写这个循环的时候需要使用枚举enumerate()
的方法,在这个循环中,我们对每一次迭代进行实现,即step
:
for step, data in enumerate(train_loader):
在写好这两个循环后,我们就要写上面的那六个步骤了:
- 将
data
中的数据和标签取出,其中数据为x
,标签为y
。即x, y = data
- 通过调用
optimizer.zero_grad()
将优化器的梯度清零 - 将数据送入模型获得预测结果
y_pred
,在这里我们需要对数据进行处理,将其传至之前定义好的设备上,即y_pred = model(x.to(device, torch.float))
- 将标签和预测结果送入损失函数获得损失。由于损失函数的计算是在之前定义好的设备上的,因此我们需要将
y
也传至设备,同时,y_pred
的类型为torch.float
,但是其下标的类型为torch.long
,损失函数需要用到其下标,因此我们需要把y
的类型变化为与y_pred
的下标一致,即loss = loss_function(y_pred, y.to(device, torch.long))
- 将损失值反向传播,这边直接调用
loss.backward()
即可 - 使用优化器对模型的参数进行更新,在这边也只需要调用
optimizer.step()
即可
保存模型torch.save()
训练结束后,我们需要把模型保存,否则在之后的测试和使用中只能重新训练并调用,这是非常麻烦的。因此,将模型保存下来是一个非常必要的步骤。在pytorch中有两种方法用来保存模型,即保存整个模型和保存模型参数,这两方法都是通过torch.save()
实现的。
在本教程中,我们保存整个模型,这样可以把模型的定义和参数全保存在一个文件中,在接下来的测试中不需要导入并调用模型的定义文件,在不修改模型结构的前提下这种方法是较为方便的。我们把模型保存在工作文件夹的根目录中,并命名为LeNet.pkl
。把我们训练好的模型model
和路径放入torch.save()
中即可。写法如下
torch.save(model, './LeNet.pkl')
训练过程可视化
当运行train.py
文件后它只会生成一个模型文件,而训练过程中的损失和准确率我们却不得而知。因此,我们需要使得训练的过程可以被我们看见,以确定模型是否正确地被训练了。
我们可以在每隔一定的step
后输出当前损失和准确率的平均值。MNIST的训练集共有六万张图像,而我们的batch_size
是64且丢弃最后一批,因此在每个Epoch
中有937个step
,实际训练59968张图像。我们可以每迭代100次后输出当前Epoch
的损失和准确率的平均值,并输出当前处在哪一次Epoch
和step
。
首先,我们先在训练部分第一个循环内定义损失函数的平均值和平均准确率,这边的0.0
表示这两个变量类型为浮点型:
for epoch in range(Epoch):
running_loss = 0.0
acc = 0.0
for step, data in enumerate(train_loader):
然后,我们需要对每次计算产生的损失进行相加,把结果放在running_loss
中,因此,我们需要在反向传播后添加一个累加操作。由于loss
在我们之前定义的设备上,因此我们需要获得loss
的值,然后将其传回cpu并转换为float类型,即:
running_loss += float(loss.data.cpu())
在累加了损失后我们需要计算其准确率,在这里,我们的准确率可以使用预测正确的数量除以总数,因此,我们需要判断其预测的标签是多少。
y_pred
是一个二维的张量,其形状为[batch, channel]
,在这边channel是10,即十个数字。如果我们将batch中的任意一行提取出来就获得了一个10维的向量,向量里的每个数代表与其下标所对应的标签的相关性,相关性越大则代表越有可能是这个数字。
因此,我们需要获得这个向量中最大数的下标,在pytorch中,我们可以用**.argmax(dim)
方法**实现,输入维度dim
,即可返回这个维度下最大值的下标,即pred = y_pred.argmax(dim=1)
。在此基础上,我们就可以计算其预测正确的数量了,先获取pred
的值,然后传回cpu,用==
判断是否相等,然后相加即可:
acc += (pred.data.cpu() == y.data).sum()
之后,在训练部分的末尾添加判断,判断当前step
是否是该轮中的第100个,由于python的计数是从0开始的,所以第一百个step
实际上是99。这里只需要用if
和求模运算即可实现:
if step % 100 == 99:
实现判断后我们就需要将平均值给输出出来了,首先计算损失的平均值,即running_loss / step
,由于这边的step
是从0开始的,所以需要加1,即:
loss_avg = running_loss / (step + 1)
然后计算准确率,我们刚才计算了预测正确的数量,将其除以当前已预测图像的总数即可,总数量可以通过step * batch_size
求得,然后将其转换为float
类型,即:
acc_avg = float(acc / ((step + 1) * batch_size))
计算完以上数据后我们就可以依次将其输出了,使用print
来输出这些数据:
print('Epoch', epoch + 1, ',step', step + 1, '| Loss_avg: %.4f' % loss_avg, '|Acc_avg:%.4f' % acc_avg)
如果你照着写到了这里,那么你已经可以很好地对模型进行训练了,而且可以在训练过程中直接查看损失和准确率的变化,当你训练完毕并获得LeNet.pkl
文件后,恭喜你,你的第一个模型训练成功了!
拓展python数据处理方法——pkl格式文件
https://blog.csdn.net/m0_55196097/article/details/131718289
测试模型
初始化、导入模型和数据集
在工作文件夹内新建test.py
,依次导入以下包:
import torch
import torchvision
import torch.utils.data as Data
然后,我们就可以定义我们的测试集和DataLoader了。这里,我们选择MNIST的测试集用于模型的测试部分,导入方法和之前导入训练集是一样的,唯一的不同就是要把train
设为False
。
而在DataLoader部分,我们不需要打乱数据集,然后一批次只需要送入一张图像,因此我们需要对DataLoader进行一些修改。写法如下:
test_data = torchvision.datasets.MNIST(root='./data/', train=False, transform=torchvision.transforms.ToTensor(), download=False)
test_loader = Data.DataLoader(test_data, batch_size=1, shuffle=False)
之后,定义我们需要使用的设备:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
torch.load()
接下来,我们需要载入我们已经训练好的模型。在这里我们需要使用torch.load(f, map_location, pickle_module, pickle_load_args)
函数。
f
为我们模型文件的路径和名称,在这里是'./LeNet.pkl'
;
map_location
会重新映射使用的设备,一般情况下这个参数不需要任何的修改,但是如果你想要把一个用GPU训练的模型放在一个只有cpu的设备上时会发生一些错误,而这时就需要定义该参数了,我们可以在这里填上torch.device(device)
以避免这个错误的发生。其他的参数基本上不需要进行任何的设置。写法如下:
net = torch.load('./LeNet.pkl',map_location=torch.device(device))
然后将我们的模型传至相应的设备即可:net.to(device)
至此,我们设置好了需要的测试集、DataLoader和之前训练的模型,并将模型传到了接下来需要使用的设备上。
关闭梯度
net.eval()
在测试阶段,我们不需要对模型的参数进行更新,因此我们可以关闭自动求导功能,并使用net.eval()
方法屏蔽Dropout层、冻结BN层的参数,防止在测试阶段BN层发生参数更新,即:
torch.set_grad_enabled(False)
net.eval()
测试及输出结果
接下来,我们就要正式地测试我们的模型了。
首先,我们要获取测试集的大小,用于最后准确率的计算。我们可以先获取其数据,然后计算数据的大小来实现。这里,我们采用.size()
方法,获取其第0维度的大小:
length = test_data.data.size(0)
接着,我们要定义准确率,用于统计我们模型在测试集上的准确率:acc = 0.0
。
然后,我们需要遍历一次数据集,因此我们只需要一个for循环即可:
```go`
for i, data in enumerate(test_loader):
测试的部分与训练类似,也是将数据输入模型,然后获得输出,只不过不需要计算损失、反向传播、参数更新等步骤了。这里,我们只需要两步即可:
```go
x, y = data
y_pred = net(x.to(device, torch.float))
现在,我们获得了预测的结果,我们需要获得预测到的标签,并计算预测正确的数量。这里与训练部分是一致的:
pred = y_pred.argmax(dim=1)
acc += (pred.data.cpu() == y.data).sum()
然后,我们在每一次预测后输出其预测的结果和对应的真实值即可,在输出时,我们需要将这两者转换为整型。即:
print('Predict:', int(pred.data.cpu()), '|Ground Truth:', int(y.data))
最后,我们需要计算模型在测试集上的准确率,即用预测正确的个数除以测试集的大小,并将其输出出来。为了美观,我们可以把它写成百分比的形式,写法如下:
acc = (acc / length) * 100
print('Accuracy: %.2f' %acc, '%')
至此,模型测试完成。