写在前面:
本文的侧重点在于Pytorch实战,对于网络的理论部分不做过多的介绍。
一、LeNet结构
了解目标分类网络的结构是复现网络的基础。
LeNet的结构很简单,主要由卷积层,最大池化层和全连接层组成。为了方便程序编写,我们在这里规定特征图的信息排列顺序为 [channel, width, height]
输入图像尺寸为:[3, 32, 32]
第一卷积层信息为:[in_channels=3, out_channels=16, kernel_size=5],经过第一卷积层后的特征图尺寸为:[16, 28, 28]
第一最大池化层信息为:[kernel_size=2, stride=2],经过第一最大池化层后的特征图尺寸为:[16, 14, 14]
第二卷积层信息为:[in_channels=16, out_channels=32, kernel_size=5],经过第二卷积层后的特征图尺寸为:[32, 10, 10]
第二最大池化层信息为:[kernel_size=2, stride=2],经过第二最大池化层后的特征图尺寸为:[32, 5, 5]
经过第一全连接层后输出的一维张量大小为:[120]
经过第二全连接层后输出的一维张量大小为:[84]
经过第三全连接层后输出的一维张量大小为:[num_class],num_class为需要分割的目标种类的数量 ,这里根据需要我们设定为10。
二、网络搭建
知晓LeNet结构之后,可以着手网络的搭建了。
新建module.py脚本,编写LeNet类,在其中编写两个函数,分别描述网络结构和正向传播过程。
import torch.nn as nn # torch.nn是专门为神经网络设计的模块化接口
import torch.nn.functional as f # nn.functional可以看作nn的子库
# 这样引入包的模块,可以通过as后面的简写名称调用函数功能
class LeNet(nn.Module): # 定义一个LeNet类,括号内的内容说明它继承于nn.Module这个超类
def __init__(self): # 定义子类的初始化函数,使得子类可以完成自身的特定功能
super().__init__() # 使用super函数调用超类的构造函数
self.conv1 = nn.Conv2d(3, 16, 5) # 卷积层1,参数类型分别为(in_channels,out_channels, kernel_size)
self.pool1 = nn.MaxPool2d(2, 2) # 池化层1,参数类型分别为(kernel_size, stride)
self.conv2 = nn.Conv2d(16, 32, 5) # 卷积层2
self.pool2 = nn.MaxPool2d(2, 2) # 池化层2
self.fc1 = nn.Linear(32*5*5, 120) # 全连接层1
self.fc2 = nn.Linear(120, 84) # 全连接层2
self.fc3 = nn.Linear(84, 10) # 全连接层3
def forward(self, x): # 定义正向传播过程,x表示的数据
x = f.relu(self.conv1(x)) # 当数据进入卷积层1后,让其通过relu激活函数。input(3, 32, 32) output(16, 28, 28)
x = self.pool1(x) # 通过第1个池化层。output(16, 14, 14)
x = f.relu(self.conv2(x)) # 数据进入卷积层2后,通过relu激活函数。output(32, 10, 10)
x = self.pool2(x) # 通过第2个卷积层。output(32, 5, 5)
x = x.view(-1, 32*5*5) # 通过view函数将二维张量展平为一维张量。output(32*5*5)
x = f.relu(self.fc1(x)) # 进入全连接层1之后通过relu激活函数。output(120)
x = f.relu(self.fc2(x)) # 进入全连接层2之后通过relu激活函数。output(84)
x = self.fc3(x) # 进入全连接层3,不需要通过激活函数了。output(10)
return x # 返回输出值
"""
注:
1、经过卷积之后的特征图尺寸大小N计算公式:
输入图片的尺寸大小:W×W
Kernel_size大小:F×F
步长大小:S
Padding操作数:P
N = (W-F+2P)/S+1
2、经过池化之后的特征图尺寸
由于kernel_size = stride = 2
所以认为通道数不变,特征图大小标为原来的一半
"""
三、网络训练
网络搭建好之后,就可以着手对网络进行训练了。训练所使用的数据集为CIFAR-10,训练的方法是通过编写train.py脚本来实现。通过这个脚本,我们希望能够对网络进行训练的时候在终端打印出训练信息,并且将训练结果保存在特定的.txt文件中。
import torch
import torchvision
import torch.nn as nn
from module import LeNet
import torch.optim as optim
import torchvision.transforms as transforms
def main():
transform = transforms.Compose( # 数据预处理工作
[transforms.ToTensor(), # 将数据和值重新排列
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) # 数据的标准化处理,传入的是每个通道的均值和标准差,均为0.5
# 训练部分,下载数据并进行训练,参数分别为存储路径,是否用于训练,是否下载,转变的图片来源
train_set = torchvision.datasets.CIFAR10(root='./data', train=True,
download=False, transform=transform)
# shuffle为询问是否对其打乱,False为不打乱。windows系统num_workers设为0
train_loader = torch.utils.data.DataLoader(train_set, batch_size=36,
shuffle=True, num_workers=0)
# 验证部分,下载数据并进行验证
val_set = torchvision.datasets.CIFAR10(root='./data', train=True,
download=False, transform=transform)
val_loader = torch.utils.data.DataLoader(val_set, batch_size=5000,
shuffle=False, num_workers=0)
val_data_iter = iter(val_loader) # 生成一个迭代器,迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退
val_images, val_label = next(val_data_iter) # 通过next访问下一个元素
net = LeNet() # 对网络进行实例化
loss_function = nn.CrossEntropyLoss() # 定义损失函数,使用的是交叉熵损失
optimizer = optim.Adam(net.parameters(), lr=0.001) # 定义优化器Adam,学习率为0.001
f = open(r'.\data.txt', 'w') # 打开txt文件,用于训练数据的记录
for epoch in range(5): # 循环迭代
running_loss = 0.0 # 用来累加训练中的损失
for step, data in enumerate(train_loader, start=0): # 用于将可遍历的对象组合为一个索引序列,同时列出数据和数据下标。序列:train_loader,起始下标:0
inputs, labels = data # data的数据结构为 [inputs, labels]
optimizer.zero_grad() # 每计算一个batch,需要调用一次,因为如果不清除历史梯度,就会对计算的历史梯度进行累加。变相实现很大的batch训练
outputs = net(inputs) # 输入网络得到输出
loss = loss_function(outputs, labels) # 计算损失
loss.backward() # 损失的后向传播
optimizer.step() # 通过优化器进行参数的更新
running_loss += loss.item() # 累加损失
# 打印统计数据
if step % 500 == 499: # 每次经过500步打印一次信息
with torch.no_grad():
outputs = net(val_images) # 将验证集的图像输入到网络中,得到输出
predict_y = torch.max(outputs, dim=1)[1] # dim1表示在维度1上找最大值,赋值给预测值y。[1]表示只需要知道索引值就可以
accuracy = torch.eq(predict_y, val_label).sum().item()/val_label.size(0)
# 精确度的计算。torch.eq()的作用是对两个张量Tensor进行逐元素的比较,若相同位置的两个元素相同,则返回True;若不同,返回False
# torch.eq().sum()就是将所有值相加,返回的是一个列表[x]
# torch.eq().sum().item()将列表的值返回为值x
# 分母是验证集中的所有标签的数量,第0个维度的数据数量。
print("[%d, %5d] | train_loss: %.3f | test_accuracy: %.3f" %
(epoch + 1, step + 1, running_loss / 500, accuracy)) # %5d说明长度为5;.3f表示保留小数点后三位
# 试图将训练结果保存到txt文件中,和终端打印出来的频率相同
f.write("epoch: %d step: %5d train_loss: %.3f test_accuracy: %.3f" %
(epoch + 1, step + 1, running_loss / 500, accuracy))
f.write('\n') # 写入数据,写完之后换行
running_loss = 0.0 # 归零
print('Finish Training') # 报告训练完成
f.close() # 不要忘记关闭文件
save_path = './LeNet.pth' # 设置权重文件保存位置
torch.save(net.state_dict(), save_path) # 将权重文件保存到对应路径中
if __name__ == '__main__':
main()
训练完5个epoch后,打开txt文件,可以打开data.txt看到训练结果。
epoch: 1 step: 500 train_loss: 1.760 test_accuracy: 0.422 epoch: 1 step: 1000 train_loss: 1.440 test_accuracy: 0.503 epoch: 2 step: 500 train_loss: 1.207 test_accuracy: 0.582 epoch: 2 step: 1000 train_loss: 1.158 test_accuracy: 0.626 epoch: 3 step: 500 train_loss: 1.030 test_accuracy: 0.671 epoch: 3 step: 1000 train_loss: 0.997 test_accuracy: 0.673 epoch: 4 step: 500 train_loss: 0.920 test_accuracy: 0.693 epoch: 4 step: 1000 train_loss: 0.916 test_accuracy: 0.717 epoch: 5 step: 500 train_loss: 0.830 test_accuracy: 0.740 epoch: 5 step: 1000 train_loss: 0.824 test_accuracy: 0.746
四、模型验证
网络训练好之后,需要对网络的训练结果进行检验。方法是编写predict.py脚本进行验证。在终端中打印出图像分类后的信息。
import torch
import torchvision.transforms as transforms
from PIL import Image
from module import LeNet
def main():
transform = transforms.Compose(
[transforms.Resize((32, 32)), # 缩放到合适的尺寸
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) # 标准化,和训练的时候一样
classes = ('plane', 'car', 'bird', 'cat', 'deer',
'dog', 'frog', 'horse', 'ship', 'truck') # 类别名称标签
net = LeNet() # 实例化网络
net.load_state_dict(torch.load('LeNet.pth')) # 导入训练完成后的权重文件
# 希望能够实现图片的顺序检测
for i in range(7):
im = Image.open('./test_picture/{}.jpg'.format(i+1))
im = transform(im)
im = torch.unsqueeze(im, dim=0)
with torch.no_grad():
outputs = net(im) # 输出的是经过网络之后的图片
predict = torch.max(outputs, dim=1)[1].numpy()
print(classes[int(predict)])
if __name__ == '__main__':
main()
所使用的验证图片一共有7张,存放在test_picture文件夹中。
终端打印出来的信息如下所示:
不难发现,前6张图片分类结果正确,最后1张出现了错误(狗:我长得很像鹿吗???)。实际上从侧面反映出此图像分类模型的效果并不好。也是可以理解的,毕竟网络结构过于简单,并且提出时间很久了。不过,练习搭建此网络还是很有意义的,正所谓千里之行始于足下。
写在后面的话:
作为一个刚刚涉及深度学习的小白,对于Python和Pytorch的掌握还是很不熟练,所以有些代码可能看起来非常的笨拙甚至于可笑,希望大家能够多多包涵。
在此感谢UP主:@霹雳吧啦Wz。本文的代码主要在其开源的代码的基础上进行修改,再次表示感谢。