LeNet是最早的卷积神经网络之一[1],其被提出用于识别手写数字和机器印刷字符。1998年,Yann LeCun第一次将LeNet卷积神经网络应用到图像分类上,在手写数字识别任务中取得了巨大成功。算法中阐述了图像中像素特征之间的相关性能够由参数共享的卷积操作所提取,同时使用卷积、下采样(池化)和非线性映射这样的组合结构,是当前流行的大多数深度图像识别网络的基础。
LeNet通过连续使用卷积和池化层的组合提取图像特征,其架构如 图1 所示,这里展示的是用于MNIST手写体数字识别任务中的LeNet-5模型:
第一模块:包含5×5的6通道卷积和2×2的池化。卷积提取图像中包含的特征模式(激活函数使用Sigmoid),图像尺寸从28减小到24。经过池化层可以降低输出特征图对空间位置的敏感性,图像尺寸减到12。
第二模块:和第一模块尺寸相同,通道数由6增加为16。卷积操作使图像尺寸减小到8,经过池化后变成4。
第三模块:包含4×4的120通道卷积。卷积之后的图像尺寸减小到1,但是通道数增加为120。将经过第3次卷积提取到的特征图输入到全连接层。第一个全连接层的输出神经元的个数是64,第二个全连接层的输出神经元个数是分类标签的类别数,对于手写数字识别的类别数是10。然后使用Softmax激活函数即可计算出每个类别的预测概率。
提示:
卷积层的输出特征图如何当作全连接层的输入使用呢?
卷积层的输出数据格式是$[N, C, H, W]$,在输入全连接层的时候,会自动将数据拉平,
也就是对每个样本,自动将其转化为长度为$K$的向量,
其中$K = C \times H \times W$,一个mini-batch的数据维度变成了$N\times K$的二维向量。
LeNet网络的实现代码如下:
#导入需要的包 import paddle import numpy as np from paddle.nn import Conv2D, MaxPool2D, Linear ##组网 import paddle.nn.functional as F #定义 LeNet 网络结构 class LeNet(paddle.nn.Layer): def __init__(self, num_classes=1): super(LeNet, self).__init__() # 创建卷积和池化层 # 创建第1个卷积层 self.conv1 = Conv2D(in_channels=1, out_channels=6, kernel_size=5) self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) # 尺寸的逻辑:池化层未改变通道数;当前通道数为6 # 创建第2个卷积层 self.conv2 = Conv2D(in_channels=6, out_channels=16, kernel_size=5) self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) # 创建第3个卷积层 self.conv3 = Conv2D(in_channels=16, out_channels=120, kernel_size=4) # 尺寸的逻辑:输入层将数据拉平[B,C,H,W] -> [B,C*H*W] # 输入size是[28,28],经过三次卷积和两次池化之后,C*H*W等于120 self.fc1 = Linear(in_features=120, out_features=64) # 创建全连接层,第一个全连接层的输出神经元个数为64, 第二个全连接层输出神经元个数为分类标签的类别数 self.fc2 = Linear(in_features=64, out_features=num_classes) #网络的前向计算过程 def forward(self, x): x = self.conv1(x) # 每个卷积层使用Sigmoid激活函数,后面跟着一个2x2的池化 x = F.sigmoid(x) x = self.max_pool1(x) x = F.sigmoid(x) x = self.conv2(x) x = self.max_pool2(x) x = self.conv3(x) #尺寸的逻辑:输入层将数据拉平[B,C,H,W] -> [B,C*H*W] x = paddle.reshape(x, [x.shape[0], -1]) x = self.fc1(x) x = F.sigmoid(x) x = self.fc2(x) return x
LeNet-5在MNIST手写数字识别任务上进行了模型训练与测试,论文中提供的模型指标如 图2 所示。使用 distortions 方法处理后,error rate能够达到0.8%。
[1] Gradient-based learn- ing applied to document recognition.
AlexNet[1]是2012年ImageNet竞赛的冠军模型,其作者是神经网络领域三巨头之一的Hinton和他的学生Alex Krizhevsky。
AlexNet以极大的优势领先2012年ImageNet竞赛的第二名,也因此给当时的学术界和工业界带来了很大的冲击。此后,更多更深的神经网络相继被提出,比如优秀的VGG,GoogLeNet,ResNet等。
AlexNet与此前的LeNet相比,具有更深的网络结构,包含5层卷积和3层全连接,具体结构如 图1 所示。
1)第一模块:对于$224\times 224$的彩色图像,先用96个$11\times 11\times 3$的卷积核对其进行卷积,提取图像中包含的特征模式(步长为4,填充为2,得到96个$54\times 54$的卷积结果(特征图);然后以$2\times 2$大小进行池化,得到了96个$27\times 27$大小的特征图;
2)第二模块:包含256个$5\times 5$的卷积和$2\times 2$池化,卷积操作后图像尺寸不变,经过池化后,图像尺寸变成$13\times 13$;
3)第三模块:包含384个$3\times 3$的卷积,卷积操作后图像尺寸不变;
4)第四模块:包含384个$3\times 3$的卷积,卷积操作后图像尺寸不变;
5)第五模块:包含256个$3\times 3$的卷积和$2\times 2$的池化,卷积操作后图像尺寸不变,经过池化后变成256个$6\times 6$大小的特征图。
将经过第5次卷积提取到的特征图输入到全连接层,得到原始图像的向量表达。前两个全连接层的输出神经元的个数是4096,第三个全连接层的输出神经元个数是分类标签的类别数(ImageNet比赛的分类类别数是1000),然后使用Softmax激活函数即可计算出每个类别的预测概率。
基于Paddle框架,AlexNet的具体实现的代码如下所示:
#-*- coding:utf-8 -*- #导入需要的包 import paddle import numpy as np from paddle.nn import Conv2D, MaxPool2D, Linear, Dropout ##组网 import paddle.nn.functional as F #定义 AlexNet 网络结构 class AlexNet(paddle.nn.Layer): def __init__(self, num_classes=1): super(AlexNet, self).__init__() # AlexNet与LeNet一样也会同时使用卷积和池化层提取图像特征 # 与LeNet不同的是激活函数换成了‘relu’ self.conv1 = Conv2D(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=5) self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) self.conv2 = Conv2D(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2) self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) self.conv3 = Conv2D(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1) self.conv4 = Conv2D(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1) self.conv5 = Conv2D(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1) self.max_pool5 = MaxPool2D(kernel_size=2, stride=2) self.fc1 = Linear(in_features=12544, out_features=4096) self.drop_ratio1 = 0.5 self.drop1 = Dropout(self.drop_ratio1) self.fc2 = Linear(in_features=4096, out_features=4096) self.drop_ratio2 = 0.5 self.drop2 = Dropout(self.drop_ratio2) self.fc3 = Linear(in_features=4096, out_features=num_classes) def forward(self, x): x = self.conv1(x) x = F.relu(x) x = self.max_pool1(x) x = self.conv2(x) x = F.relu(x) x = self.max_pool2(x) x = self.conv3(x) x = F.relu(x) x = self.conv4(x) x = F.relu(x) x = self.conv5(x) x = F.relu(x) x = self.max_pool5(x) x = paddle.reshape(x, [x.shape[0], -1]) x = self.fc1(x) x = F.relu(x) # 在全连接之后使用dropout抑制过拟合 x = self.drop1(x) x = self.fc2(x) x = F.relu(x) # 在全连接之后使用dropout抑制过拟合 x = self.drop2(x) x = self.fc3(x) return x
AlexNet中包含了几个比较新的技术点,也首次在CNN中成功应用了ReLU、Dropout和LRN等Trick。同时AlexNet也使用了GPU进行运算加速。
AlexNet将LeNet的思想发扬光大,把CNN的基本原理应用到了很深很宽的网络中。AlexNet主要使用到的新技术点如下:
AlexNet 作为 ImageNet 2012比赛的冠军算法,在 ImageNet 测试集上达到了 15.3% 的 top-5 error rate,远远超过第二名(SIFT+FVs)的 26.2% 。如 图2 所示。
[1] Imagenet classification with deep convolutional neural networks.
随着AlexNet在2012年的ImageNet大赛上大放异彩后,卷积神经网络进入了飞速发展的阶段。2014年,由Simonyan和Zisserman提出的VGG[1]网络在ImageNet上取得了亚军的成绩。VGG的命名来源于论文作者所在的实验室Visual Geometry Group,其对卷积神经网络进行了改良,探索了网络深度与性能的关系,用更小的卷积核和更深的网络结构,取得了较好的效果,成为了CNN发展史上较为重要的一个网络。VGG中使用了一系列大小为3x3的小尺寸卷积核和池化层构造深度卷积神经网络,因为其结构简单、应用性极强而广受研究者欢迎,尤其是它的网络结构设计方法,为构建深度神经网络提供了方向。
图1 是VGG-16的网络结构示意图,有13层卷积和3层全连接层。VGG网络的设计严格使用$3\times 3$的卷积层和池化层来提取特征,并在网络的最后面使用三层全连接层,将最后一层全连接层的输出作为分类的预测。
VGG中还有一个显著特点:每次经过池化层(maxpooling)后特征图的尺寸减小一倍,而通道数增加一倍(最后一个池化层除外)。
在VGG中每层卷积将使用ReLU作为激活函数,在全连接层之后添加dropout来抑制过拟合。使用小的卷积核能够有效地减少参数的个数,使得训练和测试变得更加有效。比如使用两层$3\times 3$ 卷积层,可以得到感受野为5的特征图,而比使用$5 \times 5$的卷积层需要更少的参数。由于卷积核比较小,可以堆叠更多的卷积层,加深网络的深度,这对于图像分类任务来说是有利的。VGG模型的成功证明了增加网络的深度,可以更好的学习图像中的特征模式。
基于Paddle框架,VGG的具体实现如下代码所示:
#-*- coding:utf-8 -*- #VGG模型代码 import numpy as np import paddle #from paddle.nn import Conv2D, MaxPool2D, BatchNorm, Linear from paddle.nn import Conv2D, MaxPool2D, BatchNorm2D, Linear #定义vgg网络 class VGG(paddle.nn.Layer): def __init__(self): super(VGG, self).__init__() in_channels = [3, 64, 128, 256, 512, 512] # 定义第一个卷积块,包含两个卷积 self.conv1_1 = Conv2D(in_channels=in_channels[0], out_channels=in_channels[1], kernel_size=3, padding=1, stride=1) self.conv1_2 = Conv2D(in_channels=in_channels[1], out_channels=in_channels[1], kernel_size=3, padding=1, stride=1) # 定义第二个卷积块,包含两个卷积 self.conv2_1 = Conv2D(in_channels=in_channels[1], out_channels=in_channels[2], kernel_size=3, padding=1, stride=1) self.conv2_2 = Conv2D(in_channels=in_channels[2], out_channels=in_channels[2], kernel_size=3, padding=1, stride=1) # 定义第三个卷积块,包含三个卷积 self.conv3_1 = Conv2D(in_channels=in_channels[2], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) self.conv3_2 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) self.conv3_3 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) # 定义第四个卷积块,包含三个卷积 self.conv4_1 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) self.conv4_2 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) self.conv4_3 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) # 定义第五个卷积块,包含三个卷积 self.conv5_1 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) self.conv5_2 = Conv2D(in_channels=in_channels[5], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) self.conv5_3 = Conv2D(in_channels=in_channels[5], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) # 使用Sequential 将全连接层和relu组成一个线性结构(fc + relu) # 当输入为224x224时,经过五个卷积块和池化层后,特征维度变为[512x7x7] self.fc1 = paddle.nn.Sequential(paddle.nn.Linear(512 * 7 * 7, 4096), paddle.nn.ReLU()) self.drop1_ratio = 0.5 self.dropout1 = paddle.nn.Dropout(self.drop1_ratio, mode='upscale_in_train') # 使用Sequential 将全连接层和relu组成一个线性结构(fc + relu) self.fc2 = paddle.nn.Sequential(paddle.nn.Linear(4096, 4096), paddle.nn.ReLU()) self.drop2_ratio = 0.5 self.dropout2 = paddle.nn.Dropout(self.drop2_ratio, mode='upscale_in_train') self.fc3 = paddle.nn.Linear(4096, 1) self.relu = paddle.nn.ReLU() self.pool = MaxPool2D(stride=2, kernel_size=2) def forward(self, x): x = self.relu(self.conv1_1(x)) x = self.relu(self.conv1_2(x)) x = self.pool(x) x = self.relu(self.conv2_1(x)) x = self.relu(self.conv2_2(x)) x = self.pool(x) x = self.relu(self.conv3_1(x)) x = self.relu(self.conv3_2(x)) x = self.relu(self.conv3_3(x)) x = self.pool(x) x = self.relu(self.conv4_1(x)) x = self.relu(self.conv4_2(x)) x = self.relu(self.conv4_3(x)) x = self.pool(x) x = self.relu(self.conv5_1(x)) x = self.relu(self.conv5_2(x)) x = self.relu(self.conv5_3(x)) x = self.pool(x) x = paddle.flatten(x, 1, -1) x = self.dropout1(self.relu(self.fc1(x))) x = self.dropout2(self.relu(self.fc2(x))) x = self.fc3(x) return x
VGG 在 2014 年的 ImageNet 比赛上取得了亚军的好成绩,具体指标如 图2 所示。图2 第一行为在 ImageNet 比赛中的指标,测试集的Error rate达到了7.3%,在论文中,作者对算法又进行了一定的优化,最终可以达到 6.8% 的Error rate。
[1] Very deep convolutional networks for large-scale image recognition.
GoogLeNet[1]是2014年ImageNet比赛的冠军,它的主要特点是网络不仅有深度,还在横向上具有“宽度”。从名字GoogLeNet可以知道这是来自谷歌工程师所设计的网络结构,而名字中GoogLeNet更是致敬了LeNet。GoogLeNet中最核心的部分是其内部子网络结构Inception,该结构灵感来源于NIN(Network In Network)。
由于图像信息在空间尺寸上的巨大差异,如何选择合适的卷积核来提取特征就显得比较困难了。空间分布范围更广的图像信息适合用较大的卷积核来提取其特征;而空间分布范围较小的图像信息则适合用较小的卷积核来提取其特征。为了解决这个问题,GoogLeNet提出了一种被称为Inception模块的方案。如 图1 所示:
说明:
Google的研究人员为了向LeNet致敬,特地将模型命名为GoogLeNet。
Inception一词来源于电影《盗梦空间》(Inception)。
图1(a)是Inception模块的设计思想,使用3个不同大小的卷积核对输入图片进行卷积操作,并附加最大池化,将这4个操作的输出沿着通道这一维度进行拼接,构成的输出特征图将会包含经过不同大小的卷积核提取出来的特征,从而达到捕捉不同尺度信息的效果。Inception模块采用多通路(multi-path)的设计形式,每个支路使用不同大小的卷积核,最终输出特征图的通道数是每个支路输出通道数的总和,这将会导致输出通道数变得很大,尤其是使用多个Inception模块串联操作的时候,模型参数量会变得非常大。
为了减小参数量,Inception模块使用了图(b)中的设计方式,在每个3x3和5x5的卷积层之前,增加1x1的卷积层来控制输出通道数;在最大池化层后面增加1x1卷积层减小输出通道数。基于这一设计思想,形成了上图(b)中所示的结构。下面这段程序是Inception块的具体实现方式,可以对照图(b)和代码一起阅读。
提示:
可能有读者会问,经过3x3的最大池化之后图像尺寸不会减小吗,为什么还能跟另外3个卷积输出的特征图进行拼接?这是因为池化操作可以指定窗口大小$k_h = k_w = 3$,stride=1和padding=1,输出特征图尺寸可以保持不变。
Inception模块的具体实现如下代码所示:
#GoogLeNet模型代码 import numpy as np import paddle from paddle.nn import Conv2D, MaxPool2D, AdaptiveAvgPool2D, Linear ##组网 import paddle.nn.functional as F #定义Inception块 class Inception(paddle.nn.Layer): def __init__(self, c0, c1, c2, c3, c4, **kwargs): ''' Inception模块的实现代码, c1,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数 c2,图(b)中第二条支路卷积的输出通道数,数据类型是tuple或list, 其中c2[0]是1x1卷积的输出通道数,c2[1]是3x3 c3,图(b)中第三条支路卷积的输出通道数,数据类型是tuple或list, 其中c3[0]是1x1卷积的输出通道数,c3[1]是3x3 c4,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数 ''' super(Inception, self).__init__() # 依次创建Inception块每条支路上使用到的操作 self.p1_1 = Conv2D(in_channels=c0,out_channels=c1, kernel_size=1) self.p2_1 = Conv2D(in_channels=c0,out_channels=c2[0], kernel_size=1) self.p2_2 = Conv2D(in_channels=c2[0],out_channels=c2[1], kernel_size=3, padding=1) self.p3_1 = Conv2D(in_channels=c0,out_channels=c3[0], kernel_size=1) self.p3_2 = Conv2D(in_channels=c3[0],out_channels=c3[1], kernel_size=5, padding=2) self.p4_1 = MaxPool2D(kernel_size=3, stride=1, padding=1) self.p4_2 = Conv2D(in_channels=c0,out_channels=c4, kernel_size=1) def forward(self, x): # 支路1只包含一个1x1卷积 p1 = F.relu(self.p1_1(x)) # 支路2包含 1x1卷积 + 3x3卷积 p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) # 支路3包含 1x1卷积 + 5x5卷积 p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) # 支路4包含 最大池化和1x1卷积 p4 = F.relu(self.p4_2(self.p4_1(x))) # 将每个支路的输出特征图拼接在一起作为最终的输出结果 return paddle.concat([p1, p2, p3, p4], axis=1)
GoogLeNet的架构如 图2 所示,在主体卷积部分中使用5个模块(block),每个模块之间使用步幅为2的3 ×3最大池化层来减小输出高宽。
说明:
在原作者的论文中添加了图中所示的softmax1和softmax2两个辅助分类器,如下图所示,训练时将三个分类器的损失函数进行加权求和,以缓解梯度消失现象。
GoogLeNet的具体实现如下代码所示:
#GoogLeNet模型代码 import paddle from paddle import ParamAttr import paddle.nn as nn import paddle.nn.functional as F from paddle.nn import Conv2D, BatchNorm, Linear, Dropout from paddle.nn import AdaptiveAvgPool2D, MaxPool2D, AvgPool2D from paddle.nn.initializer import Uniform import math #全连接层参数初始化 def xavier(channels, filter_size, name): stdv = (3.0 / (filter_size**2 * channels))**0.5 param_attr = ParamAttr(initializer=Uniform(-stdv, stdv), name=name + "_weights") return param_attr class ConvLayer(nn.Layer): def __init__(self, num_channels, num_filters, filter_size, stride=1, groups=1, act=None, name=None): super(ConvLayer, self).__init__() self._conv = Conv2D( in_channels=num_channels, out_channels=num_filters, kernel_size=filter_size, stride=stride, padding=(filter_size - 1) // 2, groups=groups, weight_attr=ParamAttr(name=name + "_weights"), bias_attr=False) def forward(self, inputs): y = self._conv(inputs) return y #定义Inception块 class Inception(nn.Layer): def __init__(self, input_channels, output_channels, filter1, filter3R, filter3, filter5R, filter5, proj, name=None): ''' Inception模块的实现代码, c1,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数 c2,图(b)中第二条支路卷积的输出通道数,数据类型是tuple或list, 其中c2[0]是1x1卷积的输出通道数,c2[1]是3x3 c3,图(b)中第三条支路卷积的输出通道数,数据类型是tuple或list, 其中c3[0]是1x1卷积的输出通道数,c3[1]是3x3 c4,图(b)中第一条支路1x1卷积的输出通道数,数据类型是整数 ''' super(Inception, self).__init__() # 依次创建Inception块每条支路上使用到的操作 self._conv1 = ConvLayer(input_channels, filter1, 1, name="inception_" + name + "_1x1") self._conv3r = ConvLayer(input_channels, filter3R, 1, name="inception_" + name + "_3x3_reduce") self._conv3 = ConvLayer(filter3R, filter3, 3, name="inception_" + name + "_3x3") self._conv5r = ConvLayer(input_channels, filter5R, 1, name="inception_" + name + "_5x5_reduce") self._conv5 = ConvLayer(filter5R, filter5, 5, name="inception_" + name + "_5x5") self._pool = MaxPool2D(kernel_size=3, stride=1, padding=1) self._convprj = ConvLayer(input_channels, proj, 1, name="inception_" + name + "_3x3_proj") def forward(self, inputs): # 支路1只包含一个1x1卷积 conv1 = self._conv1(inputs) # 支路2包含 1x1卷积 + 3x3卷积 conv3r = self._conv3r(inputs) conv3 = self._conv3(conv3r) # 支路3包含 1x1卷积 + 5x5卷积 conv5r = self._conv5r(inputs) conv5 = self._conv5(conv5r) # 支路4包含 最大池化和1x1卷积 pool = self._pool(inputs) convprj = self._convprj(pool) # 将每个支路的输出特征图拼接在一起作为最终的输出结果 cat = paddle.concat([conv1, conv3, conv5, convprj], axis=1) cat = F.relu(cat) return cat class GoogLeNet(nn.Layer): def __init__(self, class_dim=1000): super(GoogLeNetDY, self).__init__() # GoogLeNet包含五个模块,每个模块后面紧跟一个池化层 # 第一个模块包含1个卷积层 self._conv = ConvLayer(3, 64, 7, 2, name="conv1") # 3x3最大池化 self._pool = MaxPool2D(kernel_size=3, stride=2) # 第二个模块包含2个卷积层 self._conv_1 = ConvLayer(64, 64, 1, name="conv2_1x1") self._conv_2 = ConvLayer(64, 192, 3, name="conv2_3x3") # 第三个模块包含2个Inception块 self._ince3a = Inception(192, 192, 64, 96, 128, 16, 32, 32, name="ince3a") self._ince3b = Inception(256, 256, 128, 128, 192, 32, 96, 64, name="ince3b") # 第四个模块包含5个Inception块 self._ince4a = Inception(480, 480, 192, 96, 208, 16, 48, 64, name="ince4a") self._ince4b = Inception(512, 512, 160, 112, 224, 24, 64, 64, name="ince4b") self._ince4c = Inception(512, 512, 128, 128, 256, 24, 64, 64, name="ince4c") self._ince4d = Inception(512, 512, 112, 144, 288, 32, 64, 64, name="ince4d") self._ince4e = Inception(528, 528, 256, 160, 320, 32, 128, 128, name="ince4e") # 第五个模块包含2个Inception块 self._ince5a = Inception(832, 832, 256, 160, 320, 32, 128, 128, name="ince5a") self._ince5b = Inception(832, 832, 384, 192, 384, 48, 128, 128, name="ince5b") # 全局池化 self._pool_5 = AvgPool2D(kernel_size=7, stride=7) self._drop = Dropout(p=0.4, mode="downscale_in_infer") self._fc_out = Linear( 1024, class_dim, weight_attr=xavier(1024, 1, "out"), bias_attr=ParamAttr(name="out_offset")) self._pool_o1 = AvgPool2D(kernel_size=5, stride=3) self._conv_o1 = ConvLayer(512, 128, 1, name="conv_o1") self._fc_o1 = Linear( 1152, 1024, weight_attr=xavier(2048, 1, "fc_o1"), bias_attr=ParamAttr(name="fc_o1_offset")) self._drop_o1 = Dropout(p=0.7, mode="downscale_in_infer") self._out1 = Linear( 1024, class_dim, weight_attr=xavier(1024, 1, "out1"), bias_attr=ParamAttr(name="out1_offset")) self._pool_o2 = AvgPool2D(kernel_size=5, stride=3) self._conv_o2 = ConvLayer(528, 128, 1, name="conv_o2") self._fc_o2 = Linear( 1152, 1024, weight_attr=xavier(2048, 1, "fc_o2"), bias_attr=ParamAttr(name="fc_o2_offset")) self._drop_o2 = Dropout(p=0.7, mode="downscale_in_infer") self._out2 = Linear( 1024, class_dim, weight_attr=xavier(1024, 1, "out2"), bias_attr=ParamAttr(name="out2_offset")) def forward(self, inputs): x = self._conv(inputs) x = self._pool(x) x = self._conv_1(x) x = self._conv_2(x) x = self._pool(x) x = self._ince3a(x) x = self._ince3b(x) x = self._pool(x) ince4a = self._ince4a(x) x = self._ince4b(ince4a) x = self._ince4c(x) ince4d = self._ince4d(x) x = self._ince4e(ince4d) x = self._pool(x) x = self._ince5a(x) ince5b = self._ince5b(x) x = self._pool_5(ince5b) x = self._drop(x) x = paddle.squeeze(x, axis=[2, 3]) out = self._fc_out(x) x = self._pool_o1(ince4a) x = self._conv_o1(x) x = paddle.flatten(x, start_axis=1, stop_axis=-1) x = self._fc_o1(x) x = F.relu(x) x = self._drop_o1(x) out1 = self._out1(x) x = self._pool_o2(ince4d) x = self._conv_o2(x) x = paddle.flatten(x, start_axis=1, stop_axis=-1) x = self._fc_o2(x) x = self._drop_o2(x) out2 = self._out2(x) return [out, out1, out2]
GoogLeNet 在 2014 年的 ImageNet 比赛上取得了冠军的好成绩,具体指标如 图3 所示。在测试集上Error rate 达到了6.67%。
[1] Going deeper with convolutions.
在目标检测领域的YOLO系列算法中,作者为了达到更好的分类效果,自己设置并训练了DarkNet网络作为骨干网络。其中,YOLOv2[1]首次提出DarkNet网络,由于其具有19个卷积层,所以也称之为DarkNet19。后来在YOLOv3[2]中,作者继续吸收了当前优秀算法的思想,如残差网络和特征融合等,提出了具有53个卷积层的骨干网络DarkNet53。作者在ImageNet上进行了实验,发现相较于ResNet-152和ResNet-101,DarkNet53在分类精度差不多的前提下,计算速度取得了领先。
DarkNet19中,借鉴了许多优秀算法的经验,比如:借鉴了VGG的思想,使用了较多的$3\times 3$卷积,在每一次池化操作后,将通道数翻倍;借鉴了network in network的思想,使用全局平均池化(global average pooling)做预测,并把$1\times 1$的卷积核置于$3\times 3$的卷积核之间,用来压缩特征;同时,使用了批归一化层稳定模型训练,加速收敛,并且起到正则化作用。DarkNet19的网络结构如 图1 所示。
DarkNet19精度与VGG网络相当,但浮点运算量只有其 $\frac{1}{5}$ 左右,因此运算速度极快。
DarkNet53在之前的基础上,借鉴了ResNet的思想,在网络中大量使用了残差连接,因此网络结构可以设计的很深,并且缓解了训练中梯度消失的问题,使得模型更容易收敛。同时,使用步长为2的卷积层代替池化层实现降采样。DarkNet53的网络结构如 图2 所示。
考虑到当前 Darknet19 网络使用频率较低,接下来主要针对Darknet53网络进行实现与讲解。
基于Paddle框架,DarkNet53的具体实现的代码如下所示:
import paddle from paddle import ParamAttr import paddle.nn as nn import paddle.nn.functional as F from paddle.nn import Conv2D, BatchNorm, Linear, Dropout from paddle.nn import AdaptiveAvgPool2D, MaxPool2D, AvgPool2D from paddle.nn.initializer import Uniform import math #将卷积和批归一化封装为ConvBNLayer,方便后续复用 class ConvBNLayer(nn.Layer): def __init__(self, input_channels, output_channels, filter_size, stride, padding, name=None): # 初始化函数 super(ConvBNLayer, self).__init__() # 创建卷积层 self._conv = Conv2D( in_channels=input_channels, out_channels=output_channels, kernel_size=filter_size, stride=stride, padding=padding, weight_attr=ParamAttr(name=name + ".conv.weights"), bias_attr=False) # 创建批归一化层 bn_name = name + ".bn" self._bn = BatchNorm( num_channels=output_channels, act="relu", param_attr=ParamAttr(name=bn_name + ".scale"), bias_attr=ParamAttr(name=bn_name + ".offset"), moving_mean_name=bn_name + ".mean", moving_variance_name=bn_name + ".var") def forward(self, inputs): # 前向计算 x = self._conv(inputs) x = self._bn(x) return x #定义残差块 class BasicBlock(nn.Layer): def __init__(self, input_channels, output_channels, name=None): # 初始化函数 super(BasicBlock, self).__init__() # 定义两个卷积层 self._conv1 = ConvBNLayer( input_channels, output_channels, 1, 1, 0, name=name + ".0") self._conv2 = ConvBNLayer( output_channels, output_channels * 2, 3, 1, 1, name=name + ".1") def forward(self, inputs): # 前向计算 x = self._conv1(inputs) x = self._conv2(x) # 将第二个卷积层的输出和最初的输入值相加 return paddle.add(x=inputs, y=x) class DarkNet53(nn.Layer): def __init__(self, class_dim=1000): # 初始化函数 super(DarkNet, self).__init__() # DarkNet 每组残差块的个数,来自DarkNet的网络结构图 self.stages = [1, 2, 8, 8, 4] # 第一层卷积 self._conv1 = ConvBNLayer(3, 32, 3, 1, 1, name="yolo_input") # 下采样,使用stride=2的卷积来实现 self._conv2 = ConvBNLayer( 32, 64, 3, 2, 1, name="yolo_input.downsample") # 添加各个层级的实现 self._basic_block_01 = BasicBlock(64, 32, name="stage.0.0") # 下采样,使用stride=2的卷积来实现 self._downsample_0 = ConvBNLayer( 64, 128, 3, 2, 1, name="stage.0.downsample") self._basic_block_11 = BasicBlock(128, 64, name="stage.1.0") self._basic_block_12 = BasicBlock(128, 64, name="stage.1.1") # 下采样,使用stride=2的卷积来实现 self._downsample_1 = ConvBNLayer( 128, 256, 3, 2, 1, name="stage.1.downsample") self._basic_block_21 = BasicBlock(256, 128, name="stage.2.0") self._basic_block_22 = BasicBlock(256, 128, name="stage.2.1") self._basic_block_23 = BasicBlock(256, 128, name="stage.2.2") self._basic_block_24 = BasicBlock(256, 128, name="stage.2.3") self._basic_block_25 = BasicBlock(256, 128, name="stage.2.4") self._basic_block_26 = BasicBlock(256, 128, name="stage.2.5") self._basic_block_27 = BasicBlock(256, 128, name="stage.2.6") self._basic_block_28 = BasicBlock(256, 128, name="stage.2.7") # 下采样,使用stride=2的卷积来实现 self._downsample_2 = ConvBNLayer( 256, 512, 3, 2, 1, name="stage.2.downsample") self._basic_block_31 = BasicBlock(512, 256, name="stage.3.0") self._basic_block_32 = BasicBlock(512, 256, name="stage.3.1") self._basic_block_33 = BasicBlock(512, 256, name="stage.3.2") self._basic_block_34 = BasicBlock(512, 256, name="stage.3.3") self._basic_block_35 = BasicBlock(512, 256, name="stage.3.4") self._basic_block_36 = BasicBlock(512, 256, name="stage.3.5") self._basic_block_37 = BasicBlock(512, 256, name="stage.3.6") self._basic_block_38 = BasicBlock(512, 256, name="stage.3.7") # 下采样,使用stride=2的卷积来实现 self._downsample_3 = ConvBNLayer( 512, 1024, 3, 2, 1, name="stage.3.downsample") self._basic_block_41 = BasicBlock(1024, 512, name="stage.4.0") self._basic_block_42 = BasicBlock(1024, 512, name="stage.4.1") self._basic_block_43 = BasicBlock(1024, 512, name="stage.4.2") self._basic_block_44 = BasicBlock(1024, 512, name="stage.4.3") # 自适应平均池化 self._pool = AdaptiveAvgPool2D(1) stdv = 1.0 / math.sqrt(1024.0) # 分类层 self._out = Linear( 1024, class_dim, weight_attr=ParamAttr( name="fc_weights", initializer=Uniform(-stdv, stdv)), bias_attr=ParamAttr(name="fc_offset")) def forward(self, inputs): x = self._conv1(inputs) x = self._conv2(x) x = self._basic_block_01(x) x = self._downsample_0(x) x = self._basic_block_11(x) x = self._basic_block_12(x) x = self._downsample_1(x) x = self._basic_block_21(x) x = self._basic_block_22(x) x = self._basic_block_23(x) x = self._basic_block_24(x) x = self._basic_block_25(x) x = self._basic_block_26(x) x = self._basic_block_27(x) x = self._basic_block_28(x) x = self._downsample_2(x) x = self._basic_block_31(x) x = self._basic_block_32(x) x = self._basic_block_33(x) x = self._basic_block_34(x) x = self._basic_block_35(x) x = self._basic_block_36(x) x = self._basic_block_37(x) x = self._basic_block_38(x) x = self._downsample_3(x) x = self._basic_block_41(x) x = self._basic_block_42(x) x = self._basic_block_43(x) x = self._basic_block_44(x) x = self._pool(x) x = paddle.squeeze(x, axis=[2, 3]) x = self._out(x) return x
在 YOLOv3 论文中,作者在 ImageNet 数据集上对比了 DarkNet 网络与ResNet 网络的精度及速度,如图3所示。可以看到DarkNet53的top-5准确率可以达到93.8%,同时速度也明显超过了ResNet101和ResNet152。
更多文章请关注公重号:汀丶人工智能
[1] YOLO9000: Better, Faster, Stronger
[2] YOLOv3: An Incremental Improvement