第3章〓卷积神经网络 计算机视觉是深度学习技术应用和发展的重要领域,而卷积神经网络(Convolutional Neural Network,CNN)作为典型的深度神经网络在图像和视频处理、自然语言处理等领域发挥着重要的作用。本章将介绍卷积神经网络的基本概念、组成及经典的卷积神经网络架构。此外,本章还将VGG神经网络应用到任务——中草药识别,结合真实案例情景和代码,剖析如何使用PaddlePaddle搭建卷积神经网络。学习本章,希望读者能够:  掌握卷积神经网络的基本组成和相关概念;  了解经典的卷积神经网络架构;  使用PaddlePaddle搭建卷积神经网络。 3.1图像分类问题描述 图像分类是计算机视觉基本任务之一。顾名思义,图像分类即给定一幅图像,计算机利用算法找出其所属的类别标签。相较于目标检测、实例分割、行为识别、轨迹跟踪等难度较大的计算机视觉任务,图像分类只需要让计算机看出图片里的物体类别,更为基础但极为重要。图像分类在许多领域都有着广泛的应用,例如,安防领域的智能视频分析和人脸识别、医学领域的中草药识别、互联网领域基于内容的图像检索和相册自动归类、农业领域的害虫识别等。 图像分类的过程主要包括图像的预处理、图像的特征提取以及使用分类器对图像进行分类,其中图像的特征提取是至关重要的一步。传统的图像分类算法提取图像的色彩、纹理和空间等特征,其在简单的图像分类任务中表现较好,但在复杂图像分类任务中表现不尽人意。在CNN提出之前, 人们通过人工设计的图像描述符对图像特征进行提取, 效果卓有成效,例如,尺度不变特征变换(ScaleInvariant Feature Transform, SIFT)、方向梯度直方图(Histogram of Oriented Gradient, HOG)和词袋模型(BagofWords, BoW)等,但是人工设计特征通常需要花费很大精力,并且不具有普适性。 随着智能信息时代的来临,深度学习应运而生。深度学习作为机器学习的一个分支,旨在模拟人类的神经网络系统构建深度人工神经网络,对输入的数据进行分析和解释,将数据的底层特征组合成抽象的高层特征,深度学习在计算机视觉、自然语言处理等人工智能领域发挥了不可替代的作用。作为深度学习的典型代表,卷积神经网络在计算机视觉任务中大放异彩,与人工提取特征的传统图像分类算法相比,卷积神经网络使用卷积操作对输入图像进行特征提取,有效地从大量样本中学习特征表达,模型泛化能力更强。这种基于“输入输出”的端到端的学习方法通常可以获得非常理想的效果,受到了学术界和工业界的广泛关注。本章将对卷积神经网及其应用加以详细论述。 3.2卷积神经网络 在第2章已经介绍,深度学习的模型框架包括三个部分: 建立模型、损失函数和参数学习。在此,本章损失函数和参数学习与第2章类似,不再另设章节特意说明,在本章建立的模型即是卷积神经网络。本节从卷积神经网络结构上的三大特点出发,详细介绍卷积神经网络的概念和结构。 卷积神经网络三大特点: (1) 局部连接: 相比全连接神经网络,卷积神经网络在进行图像识别的时候,不需要对整个图像进行处理,只需要关注图像中某些特殊的区域,如图31所示。 图31局部连接 (2) 权重共享: 卷积神经网络的神经元权重相同,如图32所示。 图32权重共享 (3) 下采样: 对图像像素进行下采样,并不会对物体进行改变。虽然下采样之后的图像尺寸变小了,但是并不影响对图像中物体的识别,如图33所示。 图33下采样 卷积神经网络利用其三大特点,实现减少网络参数,从而加快训练速度。下面介绍卷积神经网络的模型结构,并说明模型中的各层是如何实现了三大特点的。图34是一个典型的卷积神经网络结构,多层卷积和池化层组合作用在输入图片上,在网络的最后通常会加入一系列全连接层,ReLU激活函数一般加在卷积或者全连接层的输出上,网络中通常还会加入Dropout来防止过拟合。 图34卷积神经网络结构 (1) 卷积层: 卷积层用于对输入的图像进行特征提取。卷积的计算范围是在像素点的空间邻域内进行的,因此可以利用输入图像的空间信息。卷积核本身与输入图片大小无关,它代表了对空间邻域内某种特征模式的提取。比如,有些卷积核提取物体边缘特征,有些卷积核提取物体拐角处的特征,图像上不同区域共享同一个卷积核。当输入图片大小不一样时,仍然可以使用同一个卷积核进行操作。 (2) 池化层: 池化层通过对卷积层输出的特征图进行约减,实现了下采样。同时对感受域内的特征进行筛选,提取区域内最具代表性的特征,保留特征图中最主要的信息。 (3) 激活函数: 激活函数给神经元引入了非线性因素,对输入信息进行非线性变换,从而使得神经网络可以任意逼近任何非线性函数,然后将变换后的输出信息作为输入信息传给下一层神经元。 (4) 全连接层: 全连接层用于对卷积神经网络提取到的特征进行汇总,将多维的特征映射为二维的输出。 3.2.1卷积层 这一节将为读者介绍卷积算法的原理和实现方案,并通过具体的案例展示如何使用卷积对图片进行操作,主要包括卷积核、卷积计算、特征图、多输入通道、多输出通道。填充(Padding)、步幅(Stride)、感受野(Receptive Field)等概念在此不作介绍,读者可参考其他深度学习书籍。 1. 卷积核/特征图/卷积计算 卷积核(Kernel)也被叫作滤波器(Filter)。假设卷积核的高和宽分别为kh和kw,则将称为kh×kw卷积,比如3×5卷积,就是指卷积核的高为3、宽为5。卷积核中数值为对图像中与卷积核同样大小的子块像素点进行卷积计算时所采用的权重。卷积计算(Convolution): 图像中像素点具有很强的空间依赖性,卷积就是针对像素点的空间依赖性来对图像进行处理的一种技术。卷积滤波结果在卷积神经网络中被称为特征图(Feature Map)。 应用示例如下。 在卷积神经网络中,卷积层的实现方式实际上是数学中定义的互相关 (Crosscorrelation)运算,具体的计算过程如图35所示,每张图的左图表示输入数据是一个维度为6×6的二维数组,中间的图表示卷积核是一个维度为3×3的二维数组。 图35卷积计算过程 如图35所示,左边的图大小是6×6,表示输入数据是一个维度为6×6的二维数组; 中间的图大小是3×3,表示一个维度为3×3的二维数组,这个二维数组称为卷积核。先将卷积核的左上角与输入数据的左上角(即输入数据的(0, 0)位置)对齐,把卷积核的每个元素跟其位置对应的输入数据中的元素相乘,再把所有乘积相加,如图35(a)得到卷积输出的第一个结果F[0,0],以此类推,最终如图35(b)所示得到结果特征图和结果F[3,3]。 F[0,0]=10×1+10×2+10×1+10×0+10×0+10×0+10×(-1)+10×(-2)+10×(-1)=0 F[0,1]=10×1+10×2+10×1+10×0+10×0+10×0+10×(-1)+10×(-2)+10×(-1)=0 F[0,2]=10×1+10×2+10×1+10×0+10×0+10×0+10×(-1)+10×(-2)+10×(-1)=0 F[0,3]=10×1+10×2+10×1+10×0+10×0+10×0+10×(-1)+10×(-2)+10×(-1)=0 F[1,0]=10×1+10×2+10×1+10×0+10×0+10×0+0×(-1)+0×(-2)+0×(-1)=40 …… F[3,3]=0×1+0×2+0×1+0×0+0×0+0×0+0×(-1)+0×(-2)+0×(-1)=0 卷积核的计算过程可以用式(31)表示,其中a代表输入图片,b代表输出特征图,w是卷积核参数,它们都是二维数组,∑u,v表示对卷积核参数进行遍历并求和。 b[i,j]=∑u,va[i+u,j+v]·w[u,v](31) 举例说明,假如上图中卷积核大小是2×2,则u可以取0和1,v也可以取0和1,也就是说: b[i,j]=a[i+0,j+0]·w[0,0]+a[i+0,j+1]·w[0,1]+a[i+1,j+0]· w[1,0]+a[i+1,j+1]·w[1,1] 读者可以自行验证,当[i,j]取不同值时,根据式(31)计算的结果与上图中的例子是否一致。 2. 多输入通道场景 前面介绍的卷积计算过程比较简单,实际应用时,处理的问题要复杂得多。例如,对于彩色图片有RGB三个通道,需要处理多输入通道的场景,相应的输出特征图往往也会具有多个通道,而且在神经网络的计算中常常把一个批次的样本放在一起计算,所以卷积算子需要具有批量处理多输入和多输出通道数据的功能。 当输入含有多个通道时,对应的卷积核也应该有相同的通道数。假设输入图片的通道数为Cin,输入数据的形状是Cin×Hin×Win。计算过程如下所示。 (1) 对每个通道分别设计一个二维数组作为卷积核,卷积核数组的形状是Cin×kh×kw。 (2) 对任一通道Cin∈[0,Cin),分别用大小为kh×kw的卷积核在大小为Hin×Win的二维数组上做卷积。 (3) 将这Cin个通道的计算结果相加,得到的是一个形状为Hout×Wout的二维数组。 应用示例如下。 上面的例子中,卷积层的数据是一个二维数组,但实际上一张图片往往含有RGB三个通道,要计算卷积的输出结果,卷积核的形式也会发生变化。假设输入图片的通道数为3,输入数据的形状是3×Hin×Win,计算过程如图36所示。 图36多输入通道计算过程 (1) 对每个通道分别设计一个二维数组作为卷积核,卷积核数组的形状是3×kh×kw。 (2) 对任一通道Cin∈[0,3),分别用大小为kh×kw的卷积核在大小为Hin×Win的二维数组上做卷积。 (3) 将这3个通道的计算结果相加,得到的是一个形状为Hout×Wout的二维数组。 3. 多输出通道场景 如果希望检测多种类型的特征,这需要采用多个卷积核进行计算。所以一般来说,卷积操作的输出特征图也会具有多个通道Cout,这时需要设计Cout个维度为Cin×kh×kw的卷积核数组,其维度是Cout×Cin×kh×kw。 (1) 对任一输出通道Cout∈[0,Cout),分别使用上面描述的形状为Cin×kh×kw的卷积核对输入图片做卷积。 (2) 将这Cout个形状为Hout×Wout的二维数组拼接在一起,形成维度为Cout×Hout×Wout的三维数组。 应用示例如下。 假设输入图片的通道数为3个,希望检测n个种类型的特征,这时需要设计n个维度为3×kh×kw的卷积核,如图37所示。 图37多输出通道计算过程 3.2.2池化层 在图像处理中,由于图像中存在较多冗余信息,可用某一区域子块的统计信息(如最大值或均值等)来刻画该区域中所有像素点呈现的空间分布模式,以替代区域子块中所有像素点取值,这就是卷积神经网络中池化操作。池化操作对卷积结果特征图进行约减,实现了下采样,同时保留了特征图中主要信息。例如,当识别一张图像是否是人脸时,我们需要知道人脸左边有一只眼睛,右边也有一只眼睛,而不需要知道眼睛的精确位置,这时通过池化某一片区域的像素点来得到总体统计特征会显得很有用。池化常见方法有平均池化、最大池化。 (1) 平均池化: 计算区域子块所包含所有像素点的均值,将均值作为平均池化结果。 (2) 最大池化: 从输入特征图的某个区域子块中选择值最大的像素点作为最大池化结果。 如图38所示,对池化窗口覆盖区域内的像素取最大值,得到输出特征图的像素值。当池化窗口在图片上滑动时,会得到整张输出特征图。 图38最大池化 应用示例如下。 与卷积核类似,池化窗口在图片上滑动时,每次移动的步长称为步幅,当宽和高的移动大小不一样时,分别用sw和sh表示。也可以对需要进行池化的图片进行填充,填充方式与卷积类似,假设在第一行之前填充ph1行,在最后一行后面填充ph2行。在第一列之前填充pw1列,在最后一列之后填充pw2列,则池化层的输出特征图大小为: Hout=H+ph1+ph2-khsh+1 Wout=W+pw1+pw2-kwsw+1 在卷积神经网络中,通常使用2×2大小的池化窗口,步幅也使用2,填充为0,则输出特征图的尺寸为: Hout=H2 Wout=W2 通过这种方式的池化,输出特征图的高和宽都减半,但通道数不会改变。 这里以图38中的两个池化运算为例,此时,输入大小是4×4,使用大小为2×2的池化窗口进行运算,步幅为2。此时,输出尺寸的计算方式为: Hout=H+ph1+ph2-khsh+1=4+0+0-22+1=42=2 Wout=W+pw1+pw2-kwsw+1=4+0+0-22+1=42=2 如图38(a)所示,使用最大池化进行运算,则输出中的每一个像素均为池化窗口对应的 2×2 区域求最大值得到。其计算步骤如下。 (1) 池化窗口的初始位置为左上角,对应红色区域,此时输出为 40=max{0,-2,40,30}; (2) 由于步幅为2,所以池化窗口向右移动两个像素,对应绿色区域,此时输出为 30=max{-1,5,20,30}; (3) 遍历完第一行后,再从第三行开始遍历,对应黄色区域,此时输出为 40=max{40,30,0,10}; (4) 池化窗口向右移动两个像素,对应蓝色区域,此时输出为 40=max{17,24,20,10}。 3.2.3卷积优势 卷积操作具有四大优势: 保留空间信息、局部连接、权重共享、对不同层级卷积提取不同特征。具体如下。 1) 保留空间信息 在卷积运算中,计算范围是在像素点的空间邻域内进行的,它代表了对空间邻域内某种特征模式的提取。对比全连接层将输入展开成一维的计算方式,卷积运算可以有效地学习到输入数据的空间信息。 2) 局部连接 在卷积操作中,每个神经元只与局部的一块区域进行连接。对于二维图像,局部像素关联性较强,这种局部连接保证了训练后的滤波器能够对局部特征有最强的响应,使神经网络可以提取数据的局部特征。全连接与局部连接的对比如图39所示。 图39全连接与局部连接 同时,由于使用了局部连接,隐藏层的每个神经元仅与部分图像相连。例如,对于一幅1000×1000 的输入图像而言,下一个隐藏层的神经元数目同样为106个,假设每个神经元只与大小为10×10的局部区域相连,那么此时的权重参数量仅为10×10×106=108,相交密集链接的全连接层少了4个数量级。 3) 权重共享 卷积计算实际上是使用一组卷积核在图片上进行滑动,实现计算乘加和。因此,对于同一个卷积核的计算过程而言,在与图像计算的过程中,它的权重是共享的。这大大降低了网络的训练难度,图310为权重共享的示意图。这里还使用上边的例子,对于一幅1000×1000的输入图像,下一个隐藏层的神经元数目为106个,隐藏层中的每个神经元与大小为10×10的局部区域相连,因此有10×10个权重参数。将这10×10个权重参数共享给其他位置对应的神经元,也就是106个神经元的权重参数保持一致,那么最终需要训练的参数就只有这10×10个权重参数。 图310权重共享示意图 4) 对不同层级卷积提取不同特征 在CNN网络中,通常使用多层卷积进行堆叠,从而实现提取不同类型特征的作用。例如,浅层卷积提取的是图像中的边缘等信息; 中层卷积提取的是图像中的局部信息; 深层卷积提取的则是图像中的全局信息。这样,通过加深网络层数,CNN就可以有效地学习到图像从细节到全局的所有特征了。对一个简单的5层CNN进行特征图可视化后的结果如图311所示。 图311特征图可视化示意图 通过图311可以看到,Layer1和Layer2中,网络学到的基本上是边缘、颜色等底层特征; Layer3开始变得稍微复杂,学习到的是纹理特征; Layer4学习到了更高维的特征,比如狗头、鸡脚等; Layer5则学习到了更加具有辨识性的全局特征。 3.2.4模型实现 PaddlePaddle的paddle.nn包中使用Conv1D、Conv2D和Conv3D实现卷积层,它们分别表示一维卷积、二维卷积、三维卷积。 此处仅介绍自然语言处理中常用的一维卷积(Conv1D),其构造函数有三个参数: in_channels为输入通道的个数,out_channels为输出通道的个数,kernel_size为卷积核宽度。当调用该Conv1D对象时,输入数据形状为(batch,in_channels, seq_len),输出数据形状为(batch,out_channels, seq_len),其中在输入数据和输出数据中,seq_len表示输入的序列长度又表示输出的序列长度。卷积神经网络中卷积层代码如下所示。 01. #1.定义卷积层 02. import paddle 03. from paddle.nn import Conv1D 04. #定义一个一维卷积,输入通道大小为5,输出通道大小为2,卷积核宽度为4 05. conv1 = Conv1D(in_channels=5, out_channels=2, kernel_size=4) 06. #定义一个一维卷积,输入通道大小为5,输出通道大小为2,卷积核宽度为3 07. conv2 = Conv1D(in_channels=5, out_channels=2, kernel_size=3) 08. #输入数据批次大小为2,即输入两个序列,每个序列长为6,每个输入的维度为5 09. inputs = paddle.rand([2,5,6]) 10. output1 = conv1(inputs) 11. print("output1:",output1) 12. output2 = conv2(inputs) 13. print("output2:",output2) 运行结果如下。 output1: Tensor(shape=[2, 2, 3], dtype=float32, place=CUDAPlace(0), stop_gradient=False, [[[-0.22635436, -0.56034124, -0.49246365], [ 0.50104654, 0.06747232, 0.71192616]], [[-0.41800800, -0.27245334, -1.22654486], [ 0.81338280, 0.46928132, 1.25316608]]]) output2: Tensor(shape=[2, 2, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=False,[[[ 0.55459601, -0.11513655, 0.83680284, 0.11368199], [ 0.07029381, -0.14357673, 0.16201110, -0.27607250]], [[ 0.19907981, 0.59146804, -0.00985690, 0.49959880], [-0.28189474, 0.04756935, 0.50971091, 0.43118754]]]) 接下来需要调用paddle.nn包中定义的池化层类,主要有最大池化、平均池化等。与卷积层类似,各种池化方法也分为一维、二维和三维三种。例如,MaxPool1D是一维最大池化,其构造函数至少需要提供一个参数kerne_size,即池化层核的大小,也就是对多大范围内的输入进行聚合。如果对整个输入序列进行池化,则其大小应为卷积层输出的序列长度。池化层代码如下所示。 01. #2.池化层 02. from paddle.nn import MaxPool1D 03. pool1 = MaxPool1D(3) #第一个池化层核的大小是3 04. pool2= MaxPool1D(4) #第一个池化层核的大小是4 05. output_pool1 = pool1(output1) #执行一维最大化池化操作,即取每行输入的最大值 06. output_pool2 = pool1(output2) 07. print("output_pool1:",output_pool1) 08. print("output_pool2:",output_pool2) 运行结果如下。 output_pool1: Tensor(shape=[2, 2, 1], dtype=float32, place=CUDAPlace(0), top_gradient=False,[[[-0.22635436],[ 0.71192616]], [[-0.27245334], [ 1.25316608]]]) output_pool2: Tensor(shape=[2, 2, 1], dtype=float32, place=CUDAPlace(0), stop_gradient=False,[[[0.83680284], [0.16201110]], [[0.59146804], [0.50971091]]]) 由于output_pool1和output_pool2是两个独立的张量,为了进行下一步操作,还需要调用paddle.concat函数将它们拼接起来。在此之前,还需要调用squeeze函数将最后一个为1的维度删除,即将2行1列的矩阵变为1个向量。代码如下所示。 01. output_pool_squeeze1 = paddle.squeeze(output_pool1,axis=2) 02. print("output_pool_squeeze1 :",output_pool_squeeze1) 03. output_pool_squeeze2 = paddle.squeeze(output_pool2,axis=2) 04. print("output_pool_squeeze2 :",output_pool_squeeze2) 05. outputs_pool = paddle.concat(x=[output_pool_squeeze1,output_pool_squeeze2],axis=1) 06. print("outputs_pool :",outputs_pool) 运行结果如下。 output_pool_squeeze1 : Tensor(shape=[2, 2], dtype=float32, lace=CUDAPlace(0), stop_gradient=False, [[-0.22635436, 0.71192616],[-0.27245334, 1.25316608]]) output_pool_squeeze2 : Tensor(shape=[2, 2], dtype=float32, place=CUDAPlace(0), stop_gradient=False,[[0.83680284, 0.16201110],[0.59146804, 0.50971091]]) outputs_pool : Tensor(shape=[2, 4], dtype=float32, place=CUDAPlace(0), stop_gradient=False,[[-0.22635436, 0.71192616, 0.83680284, 0.16201110], [-0.27245334, 1.25316608, 0.59146804, 0.50971091]]) 池化后,再连接一个全连接层,实现其分类功能。全连接层实现代码如下。 01. from paddle.nn import Linear 02. #3.全连接层,输入维度为4,即池化层输出的维度 03. linear = Linear(4,2) 04. outputs_linear = linear(outputs_pool) 05. print("outputs_linear:",outputs_linear) 运行结果如下。 outputs_linear: Tensor(shape=[2, 2], dtype=float32, place=CUDAPlace(0), top_gradient=False, [[-1.26811194, -0.16523767], [-1.84709907, 0.34843248]]) 3.3经典的卷积神经网络 前面介绍了卷积神经网络的基本组成和常见概念。本节将按如图312所示的方式介绍几种典型的卷积神经网络架构。读者在了解卷积神经网络历史发展的同时,也可以加深对卷积神经网络组成的认识。 图312经典CNN发展概述 3.3.1LeNet LeNet是最早的卷积神经网络之一,其被提出用于识别手写数字和机器印刷字符。1998年,Yann LeCun第一次将LeNet卷积神经网络应用到图像分类上,在手写数字识别任务中取得了巨大成功。算法阐述了图像中像素特征之间的相关性,神经网络能够由参数共享的卷积操作所提取,同时也使用卷积、下采样(池化)和非线性映射这样的组合结构,是当前流行的大多数图像识别网络的基础。 LeNet通过连续使用卷积和池化层的组合提取图像特征,其架构如图313所示,这里展示的是MNIST手写体数字识别任务中的LeNet5模型。 图313LeNet模型网络结构示意图 第一模块: 包含5×5的6通道卷积和2×2的池化。卷积提取图像中包含的特征模式(激活函数使用Sigmoid),图像尺寸从28减小到24。经过池化层可以降低输出特征图对空间位置的敏感性,图像尺寸减到12。 第二模块: 和第一模块尺寸相同,通道数由6增加为16。卷积操作使图像尺寸减小到8,经过池化后变成4。 模块: 包含4×4的120通道卷积。卷积之后的图像尺寸减小到1,但是通道数增加为120。将经过第3次卷积提取到的特征图输入到全连接层。第一个全连接层的输出神经元的个数是64,第二、第三个全连接层的输出神经元个数是分类标签的类别数,对于手写数字识别的类别数是10。然后使用Softmax激活函数即可计算出每个类别的预测概率。 3.3.2AlexNet AlexNet是2012年ImageNet竞赛的冠军模型,其作者是神经网络领域三巨头之一的Hinton,他的学生Alex Krizhevsky也参与了模型的编写。AlexNet以极大的优势领先2012年ImageNet竞赛的第二名,因此也给当时的学术界和工业界带来了很大的冲击。此后,更多更深的神经网络相继被提出,比如优秀的VGG、GoogLeNet、ResNet等。 AlexNet与此前的LeNet相比,具有更深的网络结构,包含5层卷积和3层全连接,具体结构如图314所示。 图314AlexNet模型网络结构示意图 第一模块: 对于224×224的彩色图像,先用96个11×11×3的卷积核对其进行卷积,提取图像中包含的特征模式(步长为4,填充为2,得到96个54×54的卷积结果(特征图)); 然后以2×2大小进行池化,得到了96个27×27大小的特征图; 第二模块: 包含256个5×5的卷积和2×2池化,卷积操作后图像尺寸不变,经过池化后,图像尺寸变成13×13; 第三模块: 包含384个3×3的卷积,卷积操作后图像尺寸不变; 第四模块: 包含384个3×3的卷积,卷积操作后图像尺寸不变; 第五模块: 包含256个3×3的卷积和2×2的池化,卷积操作后图像尺寸不变,经过池化后变成256个6×6大小的特征图。 将经过第5次卷积提取到的特征图输入到全连接层,得到原始图像的向量表达。前两个全连接层的输出神经元的个数是4096,第三个全连接层的输出神经元个数是分类标签的类别数(ImageNet比赛的分类类别数是1000),然后使用Softmax激活函数即可计算出每个类别的预测概率。 3.3.3VGG 随着AlexNet在2012年的ImageNet大赛上大放异彩后,卷积神经网络进入了飞速发展的阶段。2014年,由Simonyan和Zisserman提出的VGG网络在ImageNet上取得了亚军的成绩。VGG的命名来源于论文作者所在的实验室Visual Geometry Group。VGG对卷积神经网络进行了改良,探索了网络深度与性能的关系,用更小的卷积核和更深的网络结构,取得了较好的效果,成为了CNN发展史上较为重要的一个网络。VGG中使用了一系列大小为3×3的小尺寸卷积核和池化层构造深度卷积神经网络,因为其结构简单、应用性极强而广受研究者欢迎,尤其是它的网络结构设计方法,为构建深度神经网络提供了方向。 图315是VGG16的网络结构示意图,有13层卷积和3层全连接层。VGG网络的设计严格使用3×3的卷积层和池化层来提取特征,并在网络的最后面使用三层全连接层,将最后一层全连接层的输出作为分类的预测。VGG中还有一个显著特点: 每次经过池化层(Max pooling)后特征图的尺寸减小一半,而通道数增加一倍(最后一个池化层除外)。在VGG中每层卷积将使用ReLU作为激活函数,在全连接层之后添加Dropout来抑制过拟合。使用小的卷积核能够有效地减少参数的个数,使得训练和测试变得更加有效。比如使用两层3×3 卷积层,可以得到感受野为5的特征图,而比使用5×5的卷积层需要更少的参数。由于卷积核比较小,可以堆叠更多的卷积层,加深网络的深度,这对于图像分类任务来说是有利的。VGG模型的成功证明了增加网络的深度,可以更好地学习图像中的特征模式。 图315VGG模型网络结构示意图 3.3.4GoogLeNet GoogLeNet是2014年ImageNet比赛的冠军,它的主要特点是网络不仅有深度,还在横向上具有“宽度”。从名字GoogLeNet可以知道这是谷歌工程师设计的网络结构,而名字GoogLeNet更是致敬了LeNet。GoogLeNet中最核心的部分是其内部子网络结构Inception,该结构灵感来源于NIN(Network In Network)。 由于图像信息在空间尺寸上的巨大差异,如何选择合适的卷积核来提取特征就显得比较困难了。空间分布范围更广的图像信息适合用较大的卷积核来提取其特征; 而空间分布范围较小的图像信息则适合用较小的卷积核来提取其特征。为了解决这个问题,GoogLeNet提出了一种被称为Inception模块的方案,如图316所示。 图316Inception模块结构示意图 图316(a)是Inception模块的设计思想,使用3个不同大小的卷积核对输入图片进行卷积操作,并附加最大池化,将这4个操作的输出沿着通道这一维度进行拼接,构成的输出特征图将会包含经过不同大小的卷积核提取出来的特征,从而达到捕捉不同尺度信息的效果。Inception模块采用多通路(Multipath)的设计形式,每个支路使用不同大小的卷积核,最终输出特征图的通道数是每个支路输出通道数的总和,这将会导致输出通道数变得很大,尤其是使用多个Inception模块串联操作的时候,模型参数量会变得非常大。 为了减小参数量,Inception模块使用了图316(b)中的设计方式,在每个3×3和5×5的卷积层之前,增加1×1的卷积层来控制输出通道数; 在最大池化层后面增加1×1卷积层减小输出通道数。基于这一设计思想,形成了图316(b)中所示的结构。 3.3.5ResNet 相较于VGG的19层和GoogLeNet的22层,ResNet可以提供18、34、50、101、152甚至更多层的网络,同时获得更好的精度。但是为什么要使用更深层次的网络呢?同时,如果只是网络层数的堆叠,那么为什么前人没有获得ResNet一样的成功呢?ResNet是2015年ImageNet比赛的冠军,将识别错误率降低到了3.6%,这个结果甚至超出了正常人眼识别。通过前面几个经典模型学习,我们可以发现随着深度学习的不断发展,模型的层数越来越多,网络结构也越来越复杂。那么是否加深网络结构,就一定会得到更好的效果呢?从理论上来说,假设新增加的层都是恒等映射,只要原有的层学出跟原模型一样的参数,那么深模型结构就能达到原模型结构的效果。换句话说,原模型的解只是新模型的解的子空间,在新模型的解的空间里应该能找到比原模型的解对应的子空间更好的结果。但是实践表明,增加网络的层数之后,训练误差往往不降反升。 He Kaiming等提出了残差网络ResNet来解决上述问题,其基本思想如图317所示。如图317(a)表示增加网络的时候,将x映射成y=F(x)输出。如图317(b)对图317(a)作了改进,输出y=F(x)+x。这时不是直接学习输出特征y的表示,而是学习y-x。如果想学习出原模型的表示,只需要将F(x)的参数全部设置为0,则y=x是恒等映射。 图317残差块设计思想 图318表示出了ResNet50的结构,一共包含49层卷积和1层全连接,所以被称为ResNet50。 图318ResNet50模型网络结构示意图 视频讲解 3.4案例: 图像分类网络VGG在中草药识别任务中的应用 本节利用PaddlePaddle框架搭建VGG网络,实现中草药识别,如图319所示。本案例旨在通过中草药识别来让读者对图像分类问题初步了解,同时理解和掌握如何使用PaddlePaddle搭建一个经典的卷积神经网络。本案例支持在实训平台或本地环境操作,建议使用AI Studio实训平台。 图319中草药  实训平台: 如果选择在实训平台上操作,无须安装实验环境。实训平台集成了实验必需的相关环境,代码可在线运行,同时还提供了免费算力,可以做到即使实践复杂模型也无算力之忧。  本地环境: 如果选择在本地环境上操作,需要安装Python3.7、PaddlePaddle开源框架等实验必需的环境,具体要求及实现代码请参见百度PaddlePaddle官方网站。 3.4.1方案设计 本案例的实现方案如图320所示,对于一幅输入的中草药图像,首先使用卷积神经网络VGG提取特征,获取特征表示; 然后使用分类器(3层全连接+Softmax)获取属于每个中草药类别的概率值。在训练阶段,通过模型输出的概率值与样本的真实标签构建损失函数,从而进行模型训练; 在推理阶段,选出概率最大的类别作为最终的输出。 图320方案设计 3.4.2整体流程 中草药识别流程如图321所示,包含如下7个步骤。 (1) 数据处理: 根据网络接收的数据格式,完成相应的数据集准备及数据预处理操作,保证模型正常读取。 (2) 模型构建: 设计卷积网络结构(模型的假设空间)。 (3) 训练配置: 声明模型实例,加载模型参数,指定模型采用的寻解算法(定义优化器)并加载数据。 (4) 模型训练: 执行多轮训练不断调整参数,以达到较好的效果。 (5) 模型保存: 将模型参数保存到指定位置,便于后续推理或继续训练使用。 (6) 模型评估: 对训练好的模型进行评估测试,观察准确率和loss。 (7) 模型推理及可视化: 使用一张中草药图片来验证模型识别的效果,并可视化推理结果。 图321中草药识别流程 3.4.3数据处理 1. 数据集介绍 本案例数据集data/data105575/Chinese Medicine.zip来源于互联网,分为五个类别,共902张图片,其中,百合180张图片,枸杞185张图片,金银花180张图片,槐花167张图片,党参190张图片,部分图片如图322所示。 图322五类中草药 2. 数据预处理 图像分类网络对输入图片的格式、大小有一定的要求。数据预处理指将数据是输入到模型前,需要对数据进行预处理操作,使图片满足网络训练以及预测的需要。本案例主要应用了如下方法: (1) 图像解码: 将图像转为NumPy格式。 (2) 调整图片大小: 将原图片中短边尺寸统一缩放到256。 (3) 图像裁剪: 将图像的长宽统一裁剪为224×224,确保模型读入的图片数据大小统一。 (4) 归一化(Normalization): 通过规范化手段,把输入图像的分布改变成均值为0,方差为1的标准正态分布,使得最优解的寻优过程明显会变得平缓,训练过程更容易收敛。 (5) 通道变换: 图像的数据格式为[H,W,C](即高度、宽度和通道数),而神经网络使用的训练数据的格式为[C,H,W],因此需要对图像数据重新排列,例如[224,224,3]变为[3,224,224]。 对于图像分类问题,除了以上对图片数据进行处理外,还需要对数据作以下的处理: 解压原始数据集; 按照比例划分训练集与验证集,乱序生成数据列表; 定义数据读 图323数据集文件目录结构 取器和转换图片; 加载数据集。 1) 解压原始数据集 首先使用zipfile模块来解压原始数据集,将src_path路径下的zip包解压至target_path目录下,解压后可以在AI Studio观察到数据集文件目录结构如图323所示。代码如下所示。 01. # 引入需要的模块 02. import os 03. import zipfile 04. import random 05. import json 06. import paddle 07. import sys 08. import numpy as np 09. from PIL import Image 10. import matplotlib.pyplot as plt 11. from paddle.io import Dataset 12. random.seed(200) 13. 14. def unzip_data(src_path,target_path): 15. if(not os.path.isdir(target_path + "Chinese Medicine")): 16. z = zipfile.ZipFile(src_path, 'r') 17. z.extractall(path=target_path) 18. z.close() 2) 按照比例划分训练集与验证集 本案例定义get_data_list()遍历文件目录和图片,按照7∶1的比例划分训练集与验证集,之后打乱数据集的顺序并生成数据列表,生成的训练集数据的格式如图324所示。代码如下所示。 图324训练集数据 01. def get_data_list(target_path,train_list_path,eval_list_path): 02. ''' 03.生成数据列表 04.''' 05. #存放所有类别的信息 06. class_detail = [] 07. #获取所有类别保存的文件夹名称 08. data_list_path=target_path+"Chinese Medicine/" 09.class_dirs = os.listdir(data_list_path) 10.#总的图像数量 11.all_class_images = 0 12.#存放类别标签 13.class_label=0 14.#存放类别数目 15.class_dim = 0 16.#存储要写进eval.txt和train.txt中的内容 17.trainer_list=[] 18.eval_list=[] 19.#读取每个类别,['baihe', 'gouqi','jinyinhua','huaihua','dangshen'] 20.for class_dir in class_dirs: 21. if class_dir != ".DS_Store": 22. class_dim += 1 23. #每个类别的信息 24. class_detail_list = {} 25. eval_sum = 0 26. trainer_sum = 0 27. #统计每个类别有多少张图片 28. class_sum = 0 29. #获取类别路径 30. path = data_list_path + class_dir 31. # 获取所有图片 32. img_paths = os.listdir(path) 33. for img_path in img_paths:# 遍历文件夹下的每张图片 34. name_path = path + '/' + img_path # 每张图片的路径 35. if class_sum % 8 == 0:# 每8张图片取一个作验证数据 36. eval_sum += 1# test_sum为测试数据的数目 37. eval_list.append(name_path + "\t%d" % class_label + "\n") 38. else: 39. trainer_sum += 1 40. trainer_list.append(name_path + "\t%d" % class_label + "\n") #trainer_sum测试数据的数目 41. class_sum += 1 #每类图片的数目 42. all_class_images += 1#所有类图片的数目 43. 44. # 说明的JSON文件的class_detail数据 45. class_detail_list['class_name'] = class_dir #类别名称 46. class_detail_list['class_label'] = class_label #类别标签 47. class_detail_list['class_eval_images'] = eval_sum #该类数据的测试集数目 48. class_detail_list['class_trainer_images'] = trainer_sum #该类数据的训练集数目 49. class_detail.append(class_detail_list) 50. #初始化标签列表 51. train_parameters['label_dict'][str(class_label)] = class_dir 52. class_label += 1 53. 54. #初始化分类数 55. train_parameters['class_dim'] = class_dim 56. 57. #乱序 58. random.shuffle(eval_list) 59. with open(eval_list_path, 'a') as f: 60. for eval_image in eval_list: 61. f.write(eval_image) 62. 63. random.shuffle(trainer_list) 64. with open(train_list_path, 'a') as f2: 65. for train_image in trainer_list: 66. f2.write(train_image) 67. 68. # 说明的JSON文件信息 69. readjson = {} 70. readjson['all_class_name'] = data_list_path#文件父目录 71. readjson['all_class_images'] = all_class_images 72. readjson['class_detail'] = class_detail 73. jsons = json.dumps(readjson, sort_keys=True, indent=4, separators=(',', ': ')) 74. with open(train_parameters['readme_path'],'w') as f: 75. f.write(jsons) 76. print ('生成数据列表完成!') 77. 78. train_parameters = { 79. "src_path":"/home/aistudio/data/data124873/Chinese Medicine.zip", #原始数据集路径 80. "target_path":"/home/aistudio/data/", #要解压的路径 81. "train_list_path": "/home/aistudio/data/train.txt",#train.txt路径 82. "eval_list_path": "/home/aistudio/data/eval.txt",#eval.txt路径 83. "label_dict":{}, #标签字典 84. "readme_path": "/home/aistudio/data/readme.json",#readme.json路径 85. "class_dim": -1,#分类数 86. } 87. src_path=train_parameters['src_path'] 88. target_path=train_parameters['target_path'] 89. train_list_path=train_parameters['train_list_path'] 90. eval_list_path=train_parameters['eval_list_path'] 91. 92. # 调用解压函数解压数据集 93. unzip_data(src_path,target_path) 94. 95. # 划分训练集与验证集,乱序,生成数据列表 96. #每次生成数据列表前,首先清空train.txt和eval.txt 97. with open(train_list_path, 'w') as f: 98. f.seek(0) 99. f.truncate() 100. with open(eval_list_path, 'w') as f: 101. f.seek(0) 102. f.truncate() 103. #生成数据列表 104. get_data_list(target_path,train_list_path,eval_list_path) 3) 定义数据读取器 接下来,定义数据读取器类dataset,用来加载训练和验证时要使用的数据,也包括图片格式的修改: 图片转为RGB格式、数据维度由(H, W, C)转为(C, H, W)、图片大小resize为224×224,其过程与2.6节定义方式相同。代码如下所示。 01. # 定义数据读取器 02. class dataset(Dataset): 03. def __init__(self, data_path, mode='train'): 04. """ 05. 数据读取器 06. :param data_path: 数据集所在路径 07. :param mode: train or eval 08. """ 09. super().__init__() 10. self.data_path = data_path 11. self.img_paths = [] 12. self.labels = [] 13. 14. if mode == 'train': 15. with open(os.path.join(self.data_path, "train.txt"), "r", encoding="utf-8") as f: 16. self.info = f.readlines() 17. for img_info in self.info: 18. img_path, label = img_info.strip().split('\t') 19. self.img_paths.append(img_path) 20. self.labels.append(int(label)) 21. 22. else: 23. with open(os.path.join(self.data_path, "eval.txt"), "r", encoding="utf-8") as f: 24. self.info = f.readlines() 25. for img_info in self.info: 26. img_path, label = img_info.strip().split('\t') 27. self.img_paths.append(img_path) 28. self.labels.append(int(label)) 29. 30. def __getitem__(self, index): 31. """ 32. 获取一组数据 33. :param index: 文件索引号 34. :return: 35. """ 36. # 第一步打开图像文件并获取label值 37. img_path = self.img_paths[index] 38. img = Image.open(img_path) 39. if img.mode != 'RGB': 40. img = img.convert('RGB') 41. img = img.resize((224, 224), Image.BILINEAR) 42. #img = rand_flip_image(img) 43. img = np.array(img).astype('float32') 44. img = img.transpose((2, 0, 1)) / 255 45. label = self.labels[index] 46. label = np.array([label], dtype="int64") 47. return img, label 48. 49. def print_sample(self, index: int = 0): 50. print("文件名", self.img_paths[index], "\t标签值", self.labels[index]) 51. 52. def __len__(self): 53. return len(self.img_paths) 4) 加载数据集 最后,使用paddle.io.DataLoader模块实现数据加载,并且指定训练用参数: 训练集批次大batch_size为32,乱序读入; 验证集批次大小为8,不打乱顺序。通过大量实验发现,模型对最后出现的数据印象更加深刻。训练数据导入后,越接近模型训练结束,最后几个批次数据对模型参数的影响越大。为了避免模型记忆影响训练效果,需要进行样本乱序操作。如果数据预处理耗时较长,推荐使用paddle.io.DataLoader API中的num_workers参数,设置进程数量,实现多进程读取数据。代码如下所示。 01. #训练数据加载 02. train_dataset = dataset('/home/aistudio/data',mode='train') 03. train_loader = paddle.io.DataLoader(train_dataset, batch_size=32, shuffle=True) 04. #评估数据加载 05. eval_dataset = dataset('/home/aistudio/data',mode='eval') 06. eval_loader = paddle.io.DataLoader(eval_dataset, batch_size = 8, shuffle=False) 至此,完成了数据读取、提取数据标签信息、批量读取和加速等过程,接下来将处理好的数据输入到神经网络。 3.4.4模型构建 本案例使用VGG网络进行中草药识别。VGG是当前最流行的CNN模型之一,于2014年由Simonyan和Zisserman在ICLR 2015会议上的论文Very Deep Convolutional Networks for Largescale Image Recognition提出。VGG命名于论文作者所在的实验室Visual Geometry Group。VGG设计了一种大小为3×3的小尺寸卷积核和池化层组成的基础模块,通过堆叠上述基础模块构造出深度卷积神经网络,该网络在图像分类领域取得了不错的效果,在大型分类数据集ILSVRC上,VGG模型仅有6.8%的top5 test error。VGG模型一经推出就很受研究者们的欢迎,因为其网络结构的设计合理,总体结构简明,且可以适用于多个领域。VGG的设计为后续研究者设计模型结构提供了思路。 如图325VGG网络所示,VGG网络所有的3×3卷积都是等长卷积,包括步长和填充为1,因此特征图的尺寸在每个模块内大小不变。特征图每经过一次池化,其高度和宽度减少一半(1/2 Pool),作为弥补通道数增加一倍,最后通过三层全连接层和Softmax层输出结果。 图325VGG网络结构 VGG网络引入“模块化”的设计思想,将不同的层进行简单的组合构成网络模块,再用模块来组装成完整网络,而不是以“层”为单位组装网络。定义类ConvPool实现“模块化”,通过类函数add_sublayer创建网络层列表,其中包括卷积、ReLU、池化,并在前向计算函数forward中通过named_children()实现模块内网络层的先后顺序,代码如下所示。 01. # 定义卷积池化网络 02. class ConvPool(paddle.nn.Layer): 03. '''卷积+池化''' 04. def __init__(self, 05. num_channels, 06. num_filters, 07. filter_size, 08. pool_size, 09. pool_stride, 10. groups, 11. conv_stride=1, 12. conv_padding=1, 13. ): 14. super(ConvPool, self).__init__() 15. 16. # groups代表卷积层的数量 17. for i in range(groups): 18. self.add_sublayer(#添加子层实例 19. 'bb_%d' % i, 20. paddle.nn.Conv2D(# layer 21. in_channels=num_channels, #通道数 22. out_channels=num_filters,#卷积核个数 23. kernel_size=filter_size,#卷积核大小 24. stride=conv_stride, #步长 25. padding = conv_padding, #padding 26. ) 27. ) 28. self.add_sublayer( 29. 'relu%d' % i, 30. paddle.nn.ReLU() 31. ) 32. num_channels = num_filters 33. 34. 35. self.add_sublayer( 36. 'Maxpool', 37. paddle.nn.MaxPool2D( 38. kernel_size=pool_size,#池化核大小 39. stride=pool_stride#池化步长 40. ) 41. ) 42. 43. def forward(self, inputs): 44. x = inputs 45. for prefix, sub_layer in self.named_children(): 46. # print(prefix,sub_layer) 47. x = sub_layer(x) 48. return x 接下来,根据上述模块ConvPool定义网络VGGNet,在构造函数init()中多次调用ConvPool模块,生成VGGNet的每一个模块实例,再通过前向计算函数forward完成计算图,代码如下所示。注意: 输出由三个全连接层组成,全连接层之间使用Dropout层防止过拟合。 01. # VGG网络 02. class VGGNet(paddle.nn.Layer): 03. def __init__(self): 04. super(VGGNet, self).__init__() 05. # 5个卷积池化操作 06. self.convpool01 = ConvPool( 07. 3, 64, 3, 2, 2, 2) #3:通道数,64: 卷积核个数,3:卷积核大小,2:池化核大小,2:池化步长,2:连续卷积个数 08. self.convpool02 = ConvPool( 09. 64, 128, 3, 2, 2, 2) 10. self.convpool03 = ConvPool( 11. 128, 256, 3, 2, 2, 3) 12. self.convpool04 = ConvPool( 13. 256, 512, 3, 2, 2, 3) 14. self.convpool05 = ConvPool( 15. 512, 512, 3, 2, 2, 3) 16. self.pool_5_shape = 512 * 7* 7 17. # 三个全连接层 18. self.fc01 = paddle.nn.Linear(self.pool_5_shape, 4096) 19. self.drop1 = paddle.nn.Dropout(p=0.5) 20. self.fc02 = paddle.nn.Linear(4096, 4096) 21. self.drop2 = paddle.nn.Dropout(p=0.5) 22. self.fc03 = paddle.nn.Linear(4096, train_parameters['class_dim']) 23. 24. def forward(self, inputs, label=None): 25. # print('input_shape:', inputs.shape) #[8, 3, 224, 224] 26. """前向计算""" 27. out = self.convpool01(inputs) 28. # print('convpool01_shape:', out.shape) #[8, 64, 112, 112] 29. out = self.convpool02(out) 30. # print('convpool02_shape:', out.shape) #[8, 128, 56, 56] 31. out = self.convpool03(out) 32. # print('convpool03_shape:', out.shape) #[8, 256, 28, 28] 33. out = self.convpool04(out) 34. # print('convpool04_shape:', out.shape) #[8, 512, 14, 14] 35. out = self.convpool05(out) 36. # print('convpool05_shape:', out.shape) #[8, 512, 7, 7] 37. 38. out = paddle.reshape(out, shape=[-1, 512*7*7]) 39. out = self.fc01(out) 40. out = self.drop1(out) 41. out = self.fc02(out) 42. out = self.drop2(out) 43. out = self.fc03(out) 44. 45. if label is not None: 46. acc = paddle.metric.accuracy(input=out, label=label) 47. return out, acc 48. else: 49. return out 3.4.5训练配置 本案例使用Adam优化器。2014年12月,Kingma和Lei Ba提出了Adam优化器。该优化器对梯度的均值(即一阶矩估计,First Moment Estimation)和梯度的未中心化的方差(即二阶矩估计,Second Moment Estimation)进行综合计算,获得更新步长。Adam优化器实现起来较为简单,且计算效率高,需要的内存更少,梯度的伸缩变换不会影响更新梯度的过程,超参数的可解释性强,且通常超参数无须调整或仅需微调。如下代码通过train_parameters.update更新参数字典,即:  输入图片的shape。  训练轮数。  训练时输出日志的迭代间隔。  训练时保存模型参数的迭代间隔。  优化函数的学习率。  保存的路径。 01. # 参数配置,要保留之前数据集准备阶段配置的参数,所以使用update更新字典 02. train_parameters.update({ 03. "input_size": [3, 224, 224], 04. "num_epochs": 35, 05. "skip_steps": 10, 06. "save_steps": 100, 07. "learning_strategy": { 08. "lr": 0.0001 09.}, 10. "checkpoints": "/home/aistudio/work/checkpoints" 11. }) 为了更直观地看到训练过程中的loss和acc变化趋势,需要实现画出折线图的函数,代码如下所示。 01. # 折线图,用于观察训练过程中loss和acc的走势 02. def draw_process(title,color,iters,data,label): 03. plt.title(title, fontsize=24) 04. plt.xlabel("iter", fontsize=20) 05. plt.ylabel(label, fontsize=20) 06. plt.plot(iters, data,color=color,label=label) 07. plt.legend() 08. plt.grid() 09. plt.show() 3.4.6模型训练 训练模型并调整参数的过程,观察模型学习的过程是否正常,如损失函数值是否在降低。本案例考虑到时长因素,只训练了35个epoch,每个epoch都需要在训练集与验证集上运行,并打印出相应的loss、准确率以及变化图,如图326所示。训练步骤如下: (1) 模型实例化。 (2) 配置loss函数。 (3) 配置参数优化器。 (4) 开始训练,每经过skip_step打印一次日志,每经过save_step保存一次模型。 (5) 训练完成后画出acc和loss变化图。 图326acc图和loss图 代码如下所示。 01. model = VGGNet() 02. model.train() 03. # 配置loss函数 04. cross_entropy = paddle.nn.CrossEntropyLoss() 05. # 配置参数优化器 06. optimizer = paddle.optimizer.Adam(learning_rate=train_parameters['learning_strategy']['lr'], parameters=model.parameters()) 07. 08. steps = 0 09. Iters, total_loss, total_acc = [], [], [] 10. 11. for epo in range(train_parameters['num_epochs']): 12. for _, data in enumerate(train_loader()): 13. steps += 1 14. x_data = data[0] 15. y_data = data[1] 16. predicts, acc = model(x_data, y_data) 17. loss = cross_entropy(predicts, y_data) 18. loss.backward() 19. optimizer.step() 20. optimizer.clear_grad() 21. if steps % train_parameters["skip_steps"] == 0: 22. Iters.append(steps) 23. total_loss.append(loss.numpy()[0]) 24. total_acc.append(acc.numpy()[0]) 25. #打印中间过程 26. print('epo: {}, step: {}, loss is: {}, acc is: {}'\ 27. .format(epo, steps, loss.numpy(), acc.numpy())) 28. #保存模型参数 29. if steps % train_parameters["save_steps"] == 0: 30. save_path = train_parameters["checkpoints"]+"/"+"save_dir_" + str(steps) + '.pdparams' 31. print('save model to: ' + save_path) 32. paddle.save(model.state_dict(),save_path) 33. paddle.save(model.state_dict(),train_parameters["checkpoints"]+"/"+"save_dir_final.pdparams") 34. draw_process("training loss","red",Iters,total_loss,"training loss") 35. draw_process("training acc","green",Iters,total_acc,"training acc") 本节尝试改变batch_size优化模型。batch_size指的是一次训练所选取的样本数。在网络训练过程中,batch_size过大或者过小都会影响训练的性能和速度,如果batch_size过小,那么花费时间多,同时梯度振荡严重,不利于收敛; 如果batch_size过大,那么不同batch的梯度方向没有任何变化,容易陷入局部极小值。例如,在本案例中,我们直接使用神经网络通常设置的batch_size=16,训练35轮之后模型在验证集上的准确率为0.825。在合理范围内,增大batch_size会提高显存的利用率,提高大矩阵乘法的并行化效率,减少每轮需要训练的迭代次数。在一定范围内,batch size越大,其确定的下降方向越准,引起训练时准确率振荡越小。在本案例中,我们设置batch_size=32,同样训练35轮,模型在验证集上的准确率为0.842。当然,过大的batch_size同样会降低模型性能。在本案例中,我们设置batch_size=48,训练35轮之后模型在验证集上的准确率为0.817。从以上的实验结果,可以清楚地了解到,在模型优化的过程中,找到合适的batch_size是很重要的。 3.4.7模型评估和推理 1. 模型评估 使用验证集来评估训练过程保存的最后一个模型,首先加载模型参数,之后遍历验证集进行预测并输出平均准确率。与训练部分的代码不同,模型评估不需要参数优化,因此使用验证模式model_eval.eval()。代码如下所示。 01. # 模型评估 02. # 加载训练过程保存的最后一个模型 03. model__state_dict = paddle.load('work/checkpoints/save_dir_final.pdparams') 04. model_eval = VGGNet() 05. model_eval.set_state_dict(model__state_dict) 06. model_eval.eval() 07. accs = [] 08. # 开始评估 09. for _, data in enumerate(eval_loader()): 10. x_data = data[0] 11. y_data = data[1] 12. predicts = model_eval(x_data) 13. acc = paddle.metric.accuracy(predicts, y_data) 14. accs.append(acc.numpy()[0]) 15. print('模型在验证集上的准确率为: ',np.mean(accs)) 2. 模型推理 模型推理阶段首先采用与训练过程同样的图片转换方式对测试集图片进行预处理,然后使用训练过程保存的最后一个模型预测测试集中的图片。代码如下所示。 01. import time 02. def load_image(img_path): 03. ''' 04. 预测图片预处理 05. ''' 06. img = Image.open(img_path) 07. if img.mode != 'RGB': 08. img = img.convert('RGB') 09. img = img.resize((224, 224), Image.BILINEAR) 10. img = np.array(img).astype('float32') 11. img = img.transpose((2, 0, 1)) / 255 # HWC to CHW 及归一化 12. return img 13. 14. label_dic = train_parameters['label_dict'] 15. 16. # 加载训练过程保存的最后一个模型 17. model__state_dict = paddle.load('work/checkpoints/save_dir_final.pdparams') 18. model_predict = VGGNet() 19. model_predict.set_state_dict(model__state_dict) 20. model_predict.eval() 21. infer_imgs_path = os.listdir("infer") 22. # print(infer_imgs_path) 23. 24. # 预测所有图片 25. for infer_img_path in infer_imgs_path: 26. infer_img = load_image("infer/"+infer_img_path) 27. infer_img = infer_img[np.newaxis,:, : ,:] #reshape(-1,3,224,224) 28. infer_img = paddle.to_tensor(infer_img) 29. result = model_predict(infer_img) 30. lab = np.argmax(result.numpy()) 31. print("样本: {},被预测为:{}".format(infer_img_path,label_dic[str(lab)])) 32. img = Image.open("infer/"+infer_img_path) 33. plt.imshow(img) 34. plt.axis('off') 35. plt.show() 36. sys.stdout.flush() 37. time.sleep(0.5) 3.5本章小结 传统图像分类由多个阶段构成,框架较为复杂。基于CNN的深度学习模型采用端到端一步到位的方式,大幅提高了分类准确度。本章首先介绍了CNN的基本理论,包括卷积层、池化层等,并以代码实现了一个基本的CNN模型。接着,讲述了卷积神经网络的历史发展过程,介绍了多款经典的CNN模型。最后,使用PaddlePaddle构建了经典的VGG图像分类网络,并在中草药数据集上实现了中草药识别。通过本案例,读者将不仅会掌握卷积神经网络的相关原理,而且会进一步熟悉通过开源框架求解深度学习任务的实践过程。读者可以在此案例的基础上,尝试开发自己感兴趣的图像分类任务。