跟我一起学PyTorch-06:卷积神经网络CNN
以全连接层为基础的深度神经网络DNN是整个深度学习的基石。要说应用最广、影响最大的深度神经网络,那就是卷积神经网络(Convolutional Neural Network,CNN)。卷积神经网络虽然发布的时间较早,但直到2006年Hilton解决深度神经网络的训练问题后才焕发生机。卷积神经网络现在几乎是图像识别研究的标配。
简单回顾卷积神经网络的发展历程。日本科学家福岛邦彦在1986年提出了Neocognitron(神经认知机),直接启发了后来的卷积神经网络。Yann LeCun于1998年提出的卷积神经网络LeNet,首次使用了多层级联的卷积结构,可对手写数字进行有效识别。2012年,Alex依靠卷积神经网络AlexNet夺得ILSVRC 2012比赛的冠军,吹响了卷积神经网络研究的号角。AlexNet成功应用了ReLU、Dropout、最大池化、LRN(Local Response Normalization,局部响应归一化)、GPU加速等新技术,启发了后续更多的技术创新,加速了卷积神经网络和深度学习的研究。从此,深度学习研究进入了蓬勃发展的新阶段。2014年Google提出的GoogleNet,运用Inception Module这个可以反复堆叠、高效的卷积神经结构,获得了当年ImageNet ILSVRC比赛的冠军,同年的亚军VGGNet全程使用3x3的卷积,成功训练了深度达19层的网络。2015年,微软提出了ResNet,包含残差学习模块,成功训练了152层的网络,一举拿下当年的ILSVRC比赛的冠军。
卷积神经网络技术的发展风起云涌,尽管卷积神经网络最初是为了解决计算机视觉等问题设计的,现在其应用范围不仅仅局限于图像和视频领域,也可用于时间序列信号,比如音频信号等。本章主要通过卷积神经网络在计算机视觉上的应用来介绍卷积神经网络的原理以及如何使用PyTorch实现卷积神经网络。
本章首先介绍人类视觉和计算机视觉的基本原理,以及计算机视觉中特征的提取和选择;然后介绍卷积神经网络的主体思想和整体结构,并将详细讲解卷积层和池化层的网络结构、PyTorch对这些网络的支持、如何设置每一层神经网络的配置,以及更加复杂的卷积神经网络结构,如AlexNet、VGGNet、ResNet等;最后在MNIST数据集上通过PyTorch使用卷积神经网络实现图片分类。
1.计算机视觉
1.人类视觉和计算机视觉
视觉是人类观察和认识世界非常重要的手段。据统计,人类从外部世界获取的信息约80%是来自视觉,既说明视觉信息量巨大,同时又体现了视觉功能的重要性。同时,人类视觉是如此的功能强大,在很短的时间里,迅速地辨识视线中的物体,在人的视觉系统中,人的眼睛捕捉物体得到光信息。这些光信息经过处理,运送到大脑的视觉皮层,分析得到以下信息:有关物体的空间、色彩、形状和纹理等。有了这些信息,大脑做出对该物体的辨识。对于人类而言,通过视觉来辨识数字、识别图片中的物体或者找出图片中人脸的轮廓是非常简单的事情。然而对于计算机而言,让计算机识别图片中的内容就不是一件容易的事情。计算机视觉希望借助计算机程序来处理、分析和理解图片中的内容,使得计算机可以从图片中自动识别各种不同模式的目标和对象。
在深度学习出现之前,图像识别的一般过程是,前端为特征提取,后端为模式识别算法。后端的模式识别算法包括K近邻算法、支持向量机、神经网络等。对于不同的识别场景和越来越复杂的识别目标,寻找合适的前端特征显得尤为重要。
2.特征提取
对于特征提取,抽象于人的视觉原理,提取有关轮廓、色彩、纹理、空间等相关的特征。以色彩为例,它是一种现在仍然在广泛使用的特征,称为颜色直方图特征,这是一种简单、直观、对实际图片颜色进行数字化表达的方式。颜色的值用RGB三原色进行表示,颜色直方图的横轴表示颜色的RGB值,表示该物品所有颜色的集合,纵轴表示整个图像具有每个颜色值像素的数量,这样计算机可以对图像进行颜色表征。
image.png以纹理特征为例,橘子会有凸凹不平的纹理,而苹果的纹理则非常光滑。这种局部的纹理刻画,如何通过特征抽象表示出来?Gabor特征可以用来描述图像纹理信息的特征,Gabor滤波器的频率和方向与人类的视觉系统类似,特别适合于纹理表示与判别。SIFT(Scale Invariant Feature Transform,尺寸不变特征变换)特征是一种检测局部特征的算法,该算法通过把图片中特征点用特征向量进行描述,该特征向量具有对象缩放、平移、旋转不变的特性,对于光照、仿射和投影变换也有一定的不变性。
image.png形状特征也是图像特征的重要一类,HOG(Histogram of Oriented Gridients)特征就是其中的一种。HOG是一种描述图像局部梯度方向和梯度强度分布的特征。其核心内容是:在边缘具体位置未知的情况下,边缘方向的分布也可以很好地表示目标的外形轮廓。
image.png上述特征提取算法提取的特征还是有局限的,尽管在颜色为黑白的数据集MNIST上的最好结果的错误率为0.54%,但是在大型和复杂的数据ImageNet ILSVRC比赛的最好结果的错误率也在26%以上,而且难以突破。同时,提取的特征只在特定的场合有效,场景变化后,需要重新提取特征和调整模型参数。卷积神经网络能够自动提取特征,不必人为地提取特征,这样提取的特征能够达到更好的效果。同时,它不需要将特征提取和分类训练两个过程分开,在训练的过程中自动提取特征、循环迭代、自动选取最优的特征。
3.数据集
对于卷积神经网络的成功,计算机视觉领域的几大数据集可谓功不可没。在计算机视觉中有以下几大基础数据集。
(1)MNIST
MNIST数据集是用作手写体识别的数据集。MNIST数据集包括60000张训练图片和10000张测试图片。如图所示。其中,每一张图片都是0~9中的一个数字。图片尺寸为28x28。由于数据集中数据相对比较简单,人工标注错误率仅为0.2%。
(2)CIFAR
CIFAR数据集是一个图像分类数据集。CIFAR数据集分为Cifar-10和Cifar-100两个数据集。CIFAR数据集中的图片为32x32的彩色图片,这些图片是由Alex Krizhenevsky教授、Vinod Nair博士和Geoffrey Hilton教授整理的。Cifar-10数据集收集了来自10个不同种类的60000张图片,这些种类有飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。在Cifar-10数据集上,人工标注的正确率是94%。
(3)ImageNet
ImageNet数据集是一个大型图片数据集,由斯坦福大学的李飞飞教授带头整理而成。在ImageNet中,近1500万张图片关联到WordNet中20000个名词同义词集上。ImageNet每年举行计算机视觉相关的竞赛——Image Large Scala Visual Recognition Challenge,即ILSVRC。ImageNet数据集涵盖了计算机视觉的各个研究方向,其用作图像分类的数据集是ILSVRC2012图像分类数据集。ILSVRC2012数据集和Cifar-10数据集一样,识别图像中主要物体,其中包含了来自1000个种类的120万张图片,每张图片只属于一个种类,大小从几K到几M不等。卷积神经网络在此数据集上一战成名。
2.卷积神经网络
计算机视觉作为人工智能的重要领域,在2006年后取得了很多突破性的进展。本章介绍的卷积神经网络就是这些突破性进展背后的技术基础。在前面章节中介绍的神经网络每两层的所有节点都是两两相连的,所以称这种网络结构为全连接层网络结构。可将只包含全连接层的神经网络称为全连接神经网络。
image.png卷积神经网络利用卷积结构减少需要学习的参数量,从而提高反向传播算法的训练效率。如下图所示,在卷积神经网络中,第一个卷积层会直接接受图像像素级的输入,每一个卷积操作只处理一小块图像,进行卷积操作后传递给后面的网络,每一层卷积都会提取数据中最有效的特征。这种方法可以提取到图像中最基础的特征,比如不同方向的拐角或者边,而后进行组合和抽象成更高阶的特征,因此卷积神经网络对图像缩放、平移和旋转具有不变性。
image.png在图像处理中,图像是一个或者多个二维矩阵,如前面提到的MNIST手写体图片是一个28x28的二维矩阵。传统的神经网络都是采用全连接的方式,即输入层到隐含层的神经元都是全连接的,这样导致参数量巨大,使得网络训练耗时甚至难以训练,并且容易过拟合,而卷积神经网络则通过局部连接、权值共享等方法避免这一困难。如下图所示。
image.png对于一个200x200的输入图像而言,如果下一个隐含层的神经元数目为10000个,采用全连接则有200x200
x10000=400000000个权值参数,如此巨大的参数几乎难以训练;而采用局部连接,隐含层的每个神经元仅与图像中4x4的局部图像相连接,那么此时的权值参数个数为4x4x10000=160000,大大减少了参数的个数。
尽管大大减少了参数个数,但是参数数量依然较多。能否再进一步减少参数呢?方法就是权值共享。一个卷积层可以有多个不同的卷积核,而每一个卷积核都对应一个滤波后映射出的新图像,同一个新图像中每一个像素都来自完全相同的卷积核,就是卷积核的权值共享。具体的做法是,在局部连接中隐含层的每一个神经元连接的是一个4x4的局部图像,因此有4x4个权值参数,将这4x4个权值参数共享给剩下的神经元,也就是说隐含层中4x10000个神经元的权值参数相同,那么此时不管隐含层神经元的数目是多少,需要训练的参数就是这4x4个权值参数(也就是卷积核的大小),如下图所示。
image.png这大概就是卷积神经网络的神奇之处,尽管只有这么少的参数,依旧有出色的性能。但是,这样仅提取了图像的一种特征,如果要多提取一些特征,可以增加多个卷积核,不同的卷积核能够得到图像的不同映射下的特征,称为特征映射。如果有100个卷积核,最终的权值参数也仅为100x100=10000个而已。另外,偏置参数也是共享的,同一种滤波器共享一个偏置参数。
总结一下,卷积神经网络的要点就是卷积层中的局部连接、权值共享和池化层中下采样。局部连接、权值共享和下采样降低了参数量,使得训练复杂度大大降低,并降低了过拟合的风险。同时还赋予了卷积神经网络对于平移、形变、缩放的某种程度的不变性,提高了模型的泛化能力。
一般的卷积神经网络由卷积层、池化层、全连接层、Softmax层组成,这四者构成了常见的卷积神经网络。
(1)卷积层。卷积层是卷积神经网络最重要的部分,也是卷积神经网络得名的缘由。卷积层中每一个节点的输入是上一层神经网络中的一小块,卷积层试图将神经网络中的每一小块进行更加深入的分析,从而得到抽象程度更高的特征。
(2)池化层。池化层的神经网络不会改变三维矩阵的深度,但是它将缩小矩阵的大小。池化层将分辨率较高的图片转换为分辨率较低的图片。
(3)全连接层。经过多轮的卷积层和池化层处理后,卷积神经网络一般会接1到2层全连接层来给出最后的分类结果。
(4)Softmax层。Softmax层主要用于分类问题。
1.卷积层
下图显示了卷积神经网络中最重要的部分,可称之为卷积核(kernel)或滤波器(filter)。在PyTorch文档中将这个结构称为卷积核,因此这里我们也统称为卷积核。如图所示,卷积核将当前层神经网络上的一个子节点矩阵转换为下一层神经网络上的一个节点矩阵。
image.png在卷积层中,卷积核所处理的节点矩阵的长、宽都是人工指定的,这个节点矩阵的尺寸称为卷积核的尺寸。卷积核处理的深度和当前层神经网络节点矩阵的深度是一致的,即便节点矩阵是三维的,卷积核的尺寸只需要指定两个维度。一般而言,卷积核的尺寸是3x3和5x5。如上图中,左边表示输入的数据,输入的数据尺寸为3x32x32,中间表示卷积核,右边每一个小圆点表示一个神经元,图中有5个神经元。假设卷积核尺寸为5x5,卷积层中每个神经元会有输入数据中3x5x5区域的权重,一共75个权重。这里再次强调下卷积核的深度必须为3,和输入数据保持一致。
在卷积层中,还需要说明神经元的数量,以及它们的排列方式、滑动步长和边界填充。
(1)卷积核的数量就是卷积层的输出深度,如上图所示的5个神经元,该参数是用户指定的,和使用的卷积核数量一致。
(2)卷积核计算运算时必须指定滑动步长。比如步长为1,说明卷积核每次移动一个像素点;步长为2,卷积核会滑动2个像素点。滑动的操作使得输出的数据变得更少。
(3)边界填充如果为0,可以保证输入和输出在空间上尺寸一致;如果边界填充大于0,可以确保在进行卷积操作时不损失边界信息。
那么,输出的尺寸最终如何计算呢?在PyTorch中,可以用一个公式来计算,就是floor((W-F+2P)/S+1)。其中floor表示向下取整操作,W表示输入数据的大小,F表示卷积层中卷积核的尺寸,S表示步长,P表示边界填充0的数量。比如输入是5x5,卷积核是3x3,步长是1,填充的数量是0,那么根据公式就能得到(5 - 3 + 2 x 0)/ 1 + 1 = 3,输出的空间大小为3 x 3;如果步长为2,那么(5 - 3 + 2 x 0)/ 2 + 1 = 2,输出空间的大小为2 x 2。
以一维空间来说明卷积操作,如下图所示。其中,输入数据大小为5,卷积核的大小为3,填充为1,滑动步长分别为1和2的卷积操作结果如下:
image.png在PyTorch中,类nn.Conv2d()是卷积核模块。卷积核及其调用的例子如下:
nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0,dilation=1,groups=1,bias=True)
# 方形卷积核和等长的步长
m = nn.Conv2d(16,33,3,stride=2)
# 非方形卷积核,非等长的步长和边界填充
m = nn.Conv2d(16,33,(3,5),stride=(2,1),padding=(4,2))
# 非方形卷积核,非等长的步长,边界填充和空间间隔
m = nn.Conv2d(16,33,(3,5),stride=(2,1),padding=(4,2),dilation=(3,1))
input = autograd.Variable(torch.randn(20,16,50,100))
output = m(input)
在nn.Conv2d()中,in_channels表示输入数据体的深度,out_channels表示输出数据体的深度,kernel_size表示卷积核的大小,stride表示滑动步长,padding表示0边界填充个数,dilation表示数据体的空间间隔,groups表示输入数据体和输出数据体在深度上的关联,bias表示偏置。
2.池化层
通常会在卷积层后面插入池化层,其作用是逐渐减低网络的空间尺寸,达到减少网络中参数的数量,减少计算资源使用的目的,同时也能有效控制过拟合。
池化层一般有两种形式:Max Pooling和Mean Pooling。下面以Max Pooling来说明池化层的具体内容。池化层操作不改变模型的深度,对输入数据在深度上切片作为输入,不断地滑动窗口,取这些窗口的最大值作为输出结果,减少它的空间尺寸。池化层的效果如图所示。
image.png下图说明了池化层的具体计算过程。以窗口大小为2,滑动步长为2举例:每次都是从2x2的窗口中选择最大的数值,同时每次滑动2个步长进入新的窗口。
池化层为什么有效?图片特征具有局部不变性,也就是说,即便通过下采样也不会丢失图片拥有的特征。由于这种特性,可以将图片缩小再进行卷积处理,大大降低卷积计算的时间。最常用的池化层尺寸是2x2,滑动步长是2,对图像进行下采样,将其中75%的信息丢弃,选择其中最大的保留下来,这样也能达到去除一些噪声信息的目的。
image.png在PyTorch中,池化层包括nn.MaxPool2d和nn.AvgPool2d等。下面以nn.MaxPool2d为例进行说明:
nn.MaxPool2d(kernel_size,stride=None,padding=0,dilation=1,return_indices=False,ceil_mode=False)
# 方形窗口尺寸为3,等长滑动步长为2
m = nn.MaxPool2d(3,stride=2)
# 非方形窗口,非等长滑动步长
m = nn.MaxPool2d((3,2),stride=(2,1))
input = autograd.Variable(torch.randn(20,16,50,32))
output = m(input)
在nn.MaxPool2d中,kernel_size,stride,padding,dilation参数在nn.Conv2d中已经解释过,return_indices表示是否返回最大值所处的下标,ceil_mode表示使用方格代替层结构。
3.经典卷积神经网络
这里介绍三种经典的卷积神经网络:LeNet,AlexNet,VGGNet。这三种卷积神经网络的结构不算特别复杂,有兴趣的也可以了解GoogleNet和ResNet。
(1)LeNet
LeNet具体指的是LeNet-5。LeNet-5模型是Yann LeCun教授于1998年提出的,它是第一个成功应用于数字识别的卷积神经网络。在MNIST数据集上,LeNet-5模型可以达到约99.2%的正确率。LeNet-5模型总共有7层,包括2个卷积层,2个池化层,2个全连接层和1个输出层。下图是LeNet-5的模型架构。
image.pngLeNet-5在PyTorch中的实现如下:
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
out = F.relu(self.conv1(x))
out = F.max_pool2d(out, 2)
out = F.relu(self.conv2d(out))
out = F.max_pool2d(out, 2)
out = out.view(out.size(0), -1)
out = F.relu(self.fc1(out))
out = F.relu(self.fc2(out))
out = self.fc3(out)
return out
(2)AlexNet
2012年,Hilton的学生Alex Krizhevsky提出了卷积神经网络模型AlexNet。AlexNet在卷积神经网络上成功运用了ReLU,Dropout和LRN等技巧。在ImageNet的竞赛上,AlexNet以领先第二名10%的准确率而夺得冠军,成功地展示了深度学习的威力。AlexNet的网络结构如下图所示。
image.png上图看起来有点复杂,这是由于当时GPU计算能力不强,AlexNet使用了两个GPU并行计算,现在可以用一个GPU替换。以单个GPU的AlexNet模型为例,包括5个卷积层,3个池化层,3个全连接层。其中卷积层和全连接层包含有ReLU层,在全连接层中还有Dropout层。具体参数配置如下图所示:
image.png具体的参数配置可以查看PyTorch源代码。下面给出PyTorch实现AlexNet模型的卷积神经网络程序。
class AlexNet(nn.module):
def __init__(self, num_classes):
super(AlexNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(64, 256, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Conv2d(192, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
)
self.classifier = nn.Sequential(
nn.Dropout(),
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x)
x = x.view(x.size(0), 256 * 6 * 6)
x = self.classifier(x)
return x
(3)VGGNet
VGGNet是牛津大学计算机视觉组和Google DeepMind公司的研究人员一起研发的卷积神经网络。通过堆叠3x3的小型卷积核和2x2的最大池化层,VGGNet成功构筑了深达19层的卷积神经网络。VGGNet取得了2014年ImageNet比赛的第二名,由于拓展性强,迁移到其他图片数据上的泛化性比较好,因此可用作迁移学习。下图显示的是VGGNet各个级别的的网络结构图。虽然从A到E每一级网络逐层变深,但是网络的参数量并没有增长很多,因为参数量主要都消耗在最后3个全连接层。前面的卷积层参数很深,参数量并不是很多,但是在训练时计算量大,比较耗时。D和E模型就是VGGNet-16和VGGNet-19。
image.png下面给出PyTorch实现VGGNet模型的卷积神经网络程序。
cfg = {
'VGG11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'VGG13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
'VGG19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}
class VGG(nn.Module):
def __init__(self, vgg_name):
super(VGG, self).__init__()
self.features = self._make_layers(sfg[vgg_name])
self.classifier = nn.Linear(512, 10)
def forward(self, x):
out = self.features(x)
out = out.view(out.size(0), -1)
out = self.classifier(out)
return out
def _make_layers(self, cfg):
layers = []
ln_channels = 3
for x in cfg:
if x == 'M':
layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
else:
layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1),
nn.BatchNorm2d(x),
nn.ReLU(inplace=True)]
in_channels = x
layers += [nn.AvgPool2d(kernel_size=1, stride=1)]
return nn.Sequential(*layers)
3.MNIST数据集上的卷积神经网络的实现
本节介绍如何使用PyTorch实现一个简单的卷积神经网络,使用的数据集是MNIST,预期可以达到97%的准确率。该神经网络由2个卷积层和3个全连接层构成。通过这个例子我们可以掌握设计卷积神经网络的特征以及参数的配置。
1.配置库和配置参数
import torch
from torch import nn,optim
import torch.nn.functional as F
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import transforms,datasets
# 配置参数
torch.manual_seed(1) # 设置随机种子,确保结果可重复
batch_size = 128 # 批处理大小
learning_rate = 1e-2 # 学习率
num_epoches = 10 # 训练次数
2.加载MNIST数据集
# 下载训练集MNIST(手写数字数据)
train_dataset = datasets.MNIST(root='./data', # 数据集保存路径
train=True, # 训练数据集
transform=transforms.ToTensor(), # 转为Tensor
download=True) # 下载数据
test_dataset = datasets.MNIST(root='./data',
train=False, # 测试数据集
transform=transforms.ToTensor())
# 训练数据的加载方式:每次从train_dataset中随机(shuffle=True)选择batch_size个样本作为一个批次返回,因此所选择的数据可能会重复
train_loader = DataLoader(train_dataset,batch_size=batch_size,shuffle=True)
# 测试数据的加载方式:每次从test_dataset中选择batch_size个不同的样本作为一个批次返回,要覆盖到所有测试样本
test_loader = DataLoader(test_dataset,batch_size=batch_size,shuffle=False)
3.创建CNN模型
class Cnn(nn.Module):
def __init__(self,in_dim,n_class): # 28*28*1
super(Cnn,self).__init__()
# 定义卷积层
self.conv = nn.Sequential(
nn.Conv2d(in_dim,6,3,stride=1,padding=1), # 28*28
nn.ReLU(True),
nn.MaxPool2d(2,2), # 14*14
nn.Conv2d(6,16,5,stride=1,padding=0), # 10*10*16
nn.ReLU(True),
nn.MaxPool2d(2,2)) # 5*5*16
# 定义全连接层
self.fc = nn.Sequential(
nn.Linear(400,120), # 400 = 5*5*16
nn.Linear(120,84),
nn.Linear(84,n_class))
# 前向传播
def forward(self,x):
out = self.conv(x)
out = out.view(out.size(0),400) # 400 = 5*5*16
out = self.fc(out)
return out
model = Cnn(1,10) # 图片大小是28*28,10是数据的种类
print(model)
输出如下:
Cnn(
(conv): Sequential(
(0): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace)
(2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(3): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(4): ReLU(inplace)
(5): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(fc): Sequential(
(0): Linear(in_features=400, out_features=120, bias=True)
(1): Linear(in_features=120, out_features=84, bias=True)
(2): Linear(in_features=84, out_features=10, bias=True)
)
)
4.训练模型
# 定义损失:交叉熵损失
criterion = nn.CrossEntropyLoss()
# 定义优化器:随机梯度下降SGD
optimizer = optim.SGD(model.parameters(),lr=learning_rate)
# 共训练num_epoches轮
for epoch in range(num_epoches):
running_loss = 0.0 # 当前损失
running_acc = 0.0 # 当前准确度
# 训练集:60000,批处理:128
for i,data in enumerate(train_loader,1):
img,label = data
img = Variable(img)
label = Variable(label)
out = model(img) # 前向传播
loss = criterion(out,label) # 这个损失是当前批次的平均损失
running_loss += loss.item() * label.size(0) # 累计损失大小,乘积表示当前批次的总损失
_,pred=torch.max(out,1) # 多分类问题的类别取概率最大的类别
num_correct = (pred == label).sum() # 当前批次预测正确的个数
running_acc += num_correct.item() # 累计预测正确的个数
optimizer.zero_grad() # 梯度清零
loss.backward() # 误差反向传播
optimizer.step() # 梯度更新
# 每训练一轮,打印一次信息
print('Train {} epoch, Loss: {:.6f}, Acc: {:.6f}'.format(epoch+1,running_loss/(len(train_dataset)),running_acc/(len(train_dataset))))
输出如下:
Train 1 epoch, Loss: 1.366457, Acc: 0.626950
Train 2 epoch, Loss: 0.411538, Acc: 0.877867
Train 3 epoch, Loss: 0.294756, Acc: 0.911117
Train 4 epoch, Loss: 0.232250, Acc: 0.930517
Train 5 epoch, Loss: 0.188249, Acc: 0.943533
Train 6 epoch, Loss: 0.159317, Acc: 0.952500
Train 7 epoch, Loss: 0.139562, Acc: 0.957750
Train 8 epoch, Loss: 0.125770, Acc: 0.962000
Train 9 epoch, Loss: 0.115610, Acc: 0.964783
Train 10 epoch, Loss: 0.108309, Acc: 0.966983
5.评估模型
# 由于训练和测试的BatchNorm,Drop等配置不同,所以需要说明是模型评估
model.eval()
eval_loss = 0
eval_acc = 0
for data in test_loader:
img,label = data
img = Variable(img)
# 测试不需要 label = Variable(label)
out = model(img) # 前向传播
loss = criterion(out,label) # 当前批次的平均损失
eval_loss += loss.item() * label.size(0) # 累计损失
_,pred = torch.max(out,1) # 多分类问题的类别取概率最大的类别
num_correct = (pred==label).sum() # 当前批次预测正确的个数
eval_acc += num_correct.item() # 累计预测正确的个数
# 打印测试集上的评估结果
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(eval_loss/(len(test_dataset)),eval_acc * 1.0 / (len(test_dataset))))
输出如下:
Test Loss: 0.091677, Acc: 0.972800