Pytorch进阶|【Pytorch进阶一】基于LeNet的CIFAR10图像分类


【Pytorch进阶一】基于LeNet的CIFAR10图像分类

  • 一、LeNet网络介绍
  • 二、CIFAR10数据集介绍
  • 三、程序架构介绍
    • 3.1 LeNet模型(model.py)
    • 3.2 训练(train.py)
    • 3.3 测试(predict.py)
  • 四、总结

说明:Pytorch进阶系列都是基于 UP主霹雳吧啦WZ的视频教程下复现、总结出来的。非常好的前辈!
一、LeNet网络介绍 LeNet是很早之前针对手写字体识别提出来的模型。除了输入、输出层外,LeNet模型共包含7层网络(两层卷积、两层池化、三层全连接层)。麻雀虽小,五脏俱全,LeNet规模虽小,但包含了我们深度学习过程中常见的基本模块:卷积、池化、全连接等。LeNet模型具体的网络结构如下图所示。
Pytorch进阶|【Pytorch进阶一】基于LeNet的CIFAR10图像分类
文章图片

若对卷积、池化、全连接等相关术语不太明白的同学可参考链接:同济子豪兄的Bilibili讲解,相信大家看完子豪兄的讲解会更加深入地理解图像的卷积池化操作的具体意义。再回到LeNet模型上来。正是由于模型的规模小、包含的模块具有代表性,所以对于刚入门的同学来说,学习LeNet可以为后面更加复杂的模型做铺垫。由上结构图可知,可总结为:输入–卷积–池化–卷积–池化–全连接–全连接–全连接-输出。
进一步描述,输入层为32X32的图像,经一层卷积后,为28X28大小;经一层池化后,为14X14大小(池化为原来的一半)。依次类推,LeNet模型结构可归纳为输入(32X32)–卷积(28X28)–池化(14X14)–卷积(10X10)–池化(5X5)–全连接(120)–全连接(84)–全连接(10)-输出(分类结果)。请注意,这里没有加入图像的通道数。比如RGB图像的通道数为3,而灰度图像的通道数为1等等。
具体在程序代码中如何实现,见目录三。
二、CIFAR10数据集介绍 CIFAR是更具有普遍适用性的彩色图像数据集,同时,数据集可直接在torchvision中进行数据下载读取(代码里可以体现出来),处理方便操作简易。CIFAR分为Cifar10和Cifar100。其中CIFAR10数据集包含50000张32X32X3图像训练集,10000张32X32X3图像测试集。数据集图像一共包含10个类别的RGB彩色图片,分别为汽车(automobile)、蛙类(frog)、鸟类(bird)、船(ship)、飞机(airplane)、猫(cat)、狗(dog)、鹿(deer)、马(horse)和卡车(truck)。训练集每个类别各有5000张图像,测试集每个类别各有1000张图像。类别之间相互独立,图像不重叠。训练集的50000张图像被分为5个训练批次,每一批次为10000张图像,每一类随机选取1000张。测试集的10000张图像整体为一个测试批次,每一类各1000张。
Pytorch进阶|【Pytorch进阶一】基于LeNet的CIFAR10图像分类
文章图片

结合我们第一部分所分析的,LeNet网络最后的全连接输出为10,这里的‘10’刚好对应上我们CIFAR10数据集的10类,可理解成对于一张测试图像来说,LeNet模型所预测的10类不同的概率。我们取这10个数中的最大值,并认为最大值所对应的索引类别就是模型最终的预测结果。
三、程序架构介绍 接下来就是最重要的程序部分了。程序按照功能分为三个部分:建立LeNet模型、训练网络、测试网络性能。
Pytorch进阶|【Pytorch进阶一】基于LeNet的CIFAR10图像分类
文章图片

3.1 LeNet模型(model.py) 根据第一部分的分析,LeNet共有7层。torch的nn下有常见的卷积、池化、全连接等函数操作。定义一个LeNet类别,需要注意的是该类继承nn.Module。如果代码看得多的话,就会发现从class行到super行都是必需的,采取的都是固定的代码模式。从super行后的init初始化程序是在构建定义模型;forward部分是在构建前向传播。
用nn.Conv2d来定义卷积层,nn.Conv2d(3,16,5)中的3代表输入图像的通道数(CIFAR10数据集为彩色数据集,RGB格式),16代表输出的通道数,5代表卷积核的大小。同理,用nn.MaxPool2d来定义池化层,nn.Linear来定义全连接层。MaxPool2d的参数(2,2)含义是最大池化的池化核的大小为2X2,这样操作的根本目的在于保留图像特征的同时,缩减了图像的大小,加快网络模型的运行速度。全连接层nn.Linear的参数(120, 84),前者120代表输入的节点数为120,后者84代表输出的节点数为84。
再看forward前向传播的过程。输入变量x(3X32X32大小)穿插了前向传播的全部过程,先经过conv1一层卷积和relu一层激活,再传入下一层的池化。整个过程图像输入输出的大小可见代码注释部分。初始化和前向传播相搭配,构成了全部的LeNet网络。
跳出LeNet的代码,从通用的角度出发,在pytorch中,一个完整的网络模型似乎总要包含两个部分:init初始化和forward前向传播。在初始化的部分,我们要使用torch.nn中一系列函数来具体定义网络层(要注意self.的用法)。比如在LeNet函数中,从前到后有条理地定义了conv1和conv2卷积层、pool1和pool2两个池化层等等。放在其他更加复杂的网络模型中也是同样适用的。因为模型的不同体现在结构的不同和参数的不同上,但具体的定义方式是一样的,我们只需要修改一下括号里面的参数即可。
import torch.nn as nn import torch.nn.functional as Fclass LeNet(nn.Module): def __init__(self): super(LeNet, self).__init__() self.conv1 = nn.Conv2d(3, 16, 5) self.pool1 = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(16, 32, 5) self.pool2 = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(32*5*5, 120) self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10)def forward(self, x): x = F.relu(self.conv1(x))#input(3,32,32)output(16,28,28) x = self.pool1(x)#output(16,14,14) x = F.relu(self.conv2(x))#output(32,10,10) x = self.pool2(x)#output(32,5,5) x = x.view(-1, 32*5*5)#output(32*5*5) x = F.relu(self.fc1(x))#output(120) x = F.relu(self.fc2(x))#output(84) x = self.fc3(x)#output(10) return x

3.2 训练(train.py) 【Pytorch进阶|【Pytorch进阶一】基于LeNet的CIFAR10图像分类】在构建好LeNet模型后,用from model import LeNet导入模型。我们可以用net = LeNet()实例化模型,并print(net) 打印模型的具体信息。torch.optim包用于网络模型的反向传播,起定义损失函数、优化器的作用。
import torch import torchvision import torch.nn as nn from model import LeNet import torch.optim as optim import torchvision.transforms as transforms import matplotlib as plt import numpy as npdef main(): transform = transforms.Compose( [transforms.ToTensor(), transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))])train_set = torchvision.datasets.CIFAR10(root='./data', train=True,download=True,transform=transform) train_loader = torch.utils.data.DataLoader(train_set,batch_size=36,shuffle=True,num_workers=0)val_set = torchvision.datasets.CIFAR10(root='./data',train=False,download=True,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_image, val_label = val_data_iter.next()classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')net = LeNet() loss_function = nn.CrossEntropyLoss()#定义了损失函数 optimizer = optim.Adam(net.parameters(),lr=0.001)#定义了优化器,lr学习率for epoch in range(5):running_loss = 0.0 for step,data in enumerate(train_loader, start=0): inputs, labels = dataoptimizer.zero_grad() outputs = net(inputs) loss = loss_function(outputs,labels) loss.backward() optimizer.step()running_loss += loss.item() if step % 500 == 499: with torch.no_grad():#不要去计算误差损失梯度(占用更多的算力) outputs = net(val_image) predict_y = torch.max(outputs,dim=1)[1] accuracy = torch.eq(predict_y,val_label).sum().item() / val_label.size(0)print('[%d, %5d] train_loss: %.3ftest_accuracy: %.3f' % (epoch + 1, step + 1, running_loss / 500, accuracy)) running_loss = 0.0 #print('Step1:',step) print('Finished Training') print('Step:',step)#总的step为1388,1388*36=49968 save_path = './Lenet.pth' torch.save(net.state_dict(), save_path)if __name__ == '__main__': main()

torchvision.datasets这个包中包含了很多常见的数据集,如CIFAR、SVHN、ImageNet等等。在本程序中用torchvision.datasets.CIFAR10加载CIFAR10数据集,参数root=‘./data’表示将数据集下载到当前目录下的data文件夹中。如果不存在data文件夹,则自动创建一个。当train为True时,变量train_set中保存的为训练集。download为True时自动下载数据集,第一次运行时可以设置为True,下载好数据集后再运行时,可以将download改为False,减省程序运算时间。transform为对图像预处理的参数,进行归一化和标准化等操作。需要注意,在绝大多数的情况下,不能将原始数据集的图像直接导入模型进行训练。导入之前需要对图像像素进行归一化、标准化等处理,可加快模型的计算速度。这里的transform就是这个道理。
torch.utils.data.DataLoader命令用来加载刚刚下载完成的数据集train_set,并将其分为一个一个批次,每个批次batch_size张图像,这里设置的batch_size值为36。shuffle为True时表示随机打乱数据集。在windows系统中,可将num_workers设置为0,设置为其他的数会报错。
注意一下epoch、batch_size、step三者之间的联系和区别:
1.epoch表示将整体的训练集训练几遍,这里的例子设置为5,我们当然可以设置其他的数,多训练几遍也是极好的哦。
2.batch_size是算法训练一个step时有多少张图像参与训练。这里的36表示,每36张图像训练一个step。
3.step就是训练时的每一步,和batch_size二者结合起来使用。
以本程序为例,统一起来对这三者做出说明。CIFAR10训练集有50000张图像,每一个step训练36张,则我们共有50000/36=1388个step。epoch是我们总共遍历这50000张图像5次(训练5次)。每隔500个step我们打印一下训练结果(loss损失值啊,识别精度啊之类的)。
train_set = torchvision.datasets.CIFAR10(root='./data', train=True,download=True,transform=transform) train_loader = torch.utils.data.DataLoader(train_set,batch_size=36,shuffle=True,num_workers=0)

torch.nn中有常见的多种损失函数,下面定义了交叉熵损失函数和Adam优化器。
loss_function = nn.CrossEntropyLoss()#定义了损失函数 optimizer = optim.Adam(net.parameters(),lr=0.001)#定义了优化器,lr学习率

下面三行代表网络模型反向传播,使用起来非常的简便。
loss = loss_function(outputs,labels) loss.backward() optimizer.step()

3.3 测试(predict.py) 训练完成后,我们会发现文件夹中多了一个pth文件,这是训练完成的网络模型权重文件。我们可以多训练几次,改改参数什么的,说不定测试效果会更好。同样的,我们实例化网络,并用net.load_state_dict(torch.load('Lenet.pth '))导入学习到的权重。
import torch import torchvision.transforms as transforms from PIL import Image from model import LeNetdef 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 '))im = Image.open('1.jpg') im = transform(im) im = torch.unsqueeze(im,dim=0)with torch.no_grad(): outputs = net(im) predict = torch.max(outputs,dim=1)[1].data.numpy() print(classes[int(predict)])if __name__ == '__main__': main()

我们从网上随便下载一张’plane’, ‘car’, ‘bird’, ‘cat’,‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, 'truck’的图像,下面的第二行程序将图像调整为32X32大小,因为随便下载的图像的大小肯定是不标准的。第三行是加入了一个通道。这是因为Pytorch要求输入的图像格式为[batch_size,channels, height,width],而我们的图像格式为[channels,height,width],所以需要进行转变。
im = Image.open('1.jpg') im = transform(im)# [C,H,W] im = torch.unsqueeze(im,dim=0)#[N,C,H,W]

之后就是取输出预测的10个概率值的最大值,并将最大值的索引对应到classes内中,输入图像和分类结果如下所示。Pytorch进阶|【Pytorch进阶一】基于LeNet的CIFAR10图像分类
文章图片

Pytorch进阶|【Pytorch进阶一】基于LeNet的CIFAR10图像分类
文章图片

四、总结 LeNet模型非常简单易学,本文基于CIFAR10数据集训练分类的,我们当然可以多多尝试其他的,比如改参数啊、改结构啊、改数据集啊等等。具体的我们可以参考霹雳吧啦WZ前辈的B站视频,非常好的前辈。霹雳吧啦WZ,欢迎同学们交流经验分享问题。
开源、分享、合作。

    推荐阅读