写点什么

深度学习应用篇 - 计算机视觉 - 图像分类 [2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet 模型结构、实现、模型特点详细介绍

  • 2023-06-07
    浙江
  • 本文字数:17677 字

    阅读完需:约 58 分钟

深度学习应用篇-计算机视觉-图像分类[2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet模型结构、实现、模型特点详细介绍

深度学习应用篇-计算机视觉-图像分类[2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet 模型结构、实现、模型特点详细介绍

1.LeNet(1998)

LeNet 是最早的卷积神经网络之一<sup>[1]</sup>,其被提出用于识别手写数字和机器印刷字符。1998 年,Yann LeCun 第一次将 LeNet 卷积神经网络应用到图像分类上,在手写数字识别任务中取得了巨大成功。算法中阐述了图像中像素特征之间的相关性能够由参数共享的卷积操作所提取,同时使用卷积、下采样(池化)和非线性映射这样的组合结构,是当前流行的大多数深度图像识别网络的基础。

1.1 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 激活函数即可计算出每个类别的预测概率。




提示:


卷积层的输出特征图如何当作全连接层的输入使用呢?


卷积层的输出数据格式是,在输入全连接层的时候,会自动将数据拉平,


也就是对每个样本,自动将其转化为长度为的向量,


其中,一个 mini-batch 的数据维度变成了的二维向量。



1.2 LeNet 模型实现

LeNet 网络的实现代码如下:


#导入需要的包import paddleimport numpy as npfrom 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
复制代码

1.3 LeNet 模型特点

  • 卷积网络使用一个 3 层的序列组合:卷积、下采样(池化)、非线性映射(LeNet-5 最重要的特性,奠定了目前深层卷积网络的基础);

  • 使用卷积提取空间特征;

  • 使用映射的空间均值进行下采样;

  • 使用进行非线性映射;

  • 多层神经网络(MLP)作为最终的分类器;

  • 层间的稀疏连接矩阵以避免巨大的计算开销。

1.4 LeNet 模型指标

LeNet-5 在 MNIST 手写数字识别任务上进行了模型训练与测试,论文中提供的模型指标如 图 2 所示。使用 distortions 方法处理后,error rate 能够达到 0.8%。



  • 参考文献


[1] Gradient-based learn- ing applied to document recognition.

2.AlexNet(2012)

AlexNet<sup>[1]</sup>是 2012 年 ImageNet 竞赛的冠军模型,其作者是神经网络领域三巨头之一的 Hinton 和他的学生 Alex Krizhevsky。


AlexNet 以极大的优势领先 2012 年 ImageNet 竞赛的第二名,也因此给当时的学术界和工业界带来了很大的冲击。此后,更多更深的神经网络相继被提出,比如优秀的 VGG,GoogLeNet,ResNet 等。

2.1 AlexNet 模型结构

AlexNet 与此前的 LeNet 相比,具有更深的网络结构,包含 5 层卷积和 3 层全连接,具体结构如 图 1 所示。



1)第一模块:对于的彩色图像,先用 96 个的卷积核对其进行卷积,提取图像中包含的特征模式(步长为 4,填充为 2,得到 96 个的卷积结果(特征图);然后以大小进行池化,得到了 96 个大小的特征图;


2)第二模块:包含 256 个的卷积和池化,卷积操作后图像尺寸不变,经过池化后,图像尺寸变成


3)第三模块:包含 384 个的卷积,卷积操作后图像尺寸不变;


4)第四模块:包含 384 个的卷积,卷积操作后图像尺寸不变;


5)第五模块:包含 256 个的卷积和的池化,卷积操作后图像尺寸不变,经过池化后变成 256 个大小的特征图。


将经过第 5 次卷积提取到的特征图输入到全连接层,得到原始图像的向量表达。前两个全连接层的输出神经元的个数是 4096,第三个全连接层的输出神经元个数是分类标签的类别数(ImageNet 比赛的分类类别数是 1000),然后使用 Softmax 激活函数即可计算出每个类别的预测概率。

2.2 AlexNet 模型实现

基于 Paddle 框架,AlexNet 的具体实现的代码如下所示:


#-*- coding:utf-8 -*-
#导入需要的包import paddleimport numpy as npfrom 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
复制代码

2.3 AlexNet 模型特点

AlexNet 中包含了几个比较新的技术点,也首次在 CNN 中成功应用了 ReLU、Dropout 和 LRN 等 Trick。同时 AlexNet 也使用了 GPU 进行运算加速。


AlexNet 将 LeNet 的思想发扬光大,把 CNN 的基本原理应用到了很深很宽的网络中。AlexNet 主要使用到的新技术点如下:


  • 成功使用 ReLU 作为 CNN 的激活函数,并验证其效果在较深的网络超过了 Sigmoid,成功解决了 Sigmoid 在网络较深时的梯度弥散问题。虽然 ReLU 激活函数在很久之前就被提出了,但是直到 AlexNet 的出现才将其发扬光大。

  • 训练时使用 Dropout 随机忽略一部分神经元,以避免模型过拟合。Dropout 虽有单独的论文论述,但是 AlexNet 将其实用化,通过实践证实了它的效果。在 AlexNet 中主要是最后几个全连接层使用了 Dropout。

  • 在 CNN 中使用重叠的最大池化。此前 CNN 中普遍使用平均池化,AlexNet 全部使用最大池化,避免平均池化的模糊化效果。并且 AlexNet 中提出让步长比池化核的尺寸小的观点,这样池化层的输出之间会有重叠和覆盖,提升了特征的丰富性。

  • 提出了 LRN 局部响应归一化层,对局部神经元的活动创建竞争机制,使得其中响应比较大的值变得相对更大,并抑制其他反馈较小的神经元,增强了模型的泛化能力。

  • 使用 CUDA 加速深度卷积网络的训练,利用 GPU 强大的并行计算能力,处理神经网络训练时大量的矩阵运算。AlexNet 使用了两块 GTX 580 GPU 进行训练,单个 GTX 580 只有 3GB 显存,这限制了可训练的网络的最大规模。因此作者将 AlexNet 分布在两个 GPU 上,在每个 GPU 的显存中储存一半的神经元的参数。因为 GPU 之间通信方便,可以互相访问显存,而不需要通过主机内存,所以同时使用多块 GPU 也是非常高效的。同时,AlexNet 的设计让 GPU 之间的通信只在网络的某些层进行,控制了通信的性能损耗。

  • 使用数据增强,随机地从 大小的原始图像中截取大小的区域(以及水平翻转的镜像),相当于增加了倍的数据量。如果没有数据增强,仅靠原始的数据量,参数众多的 CNN 会陷入过拟合中,使用了数据增强后可以大大减轻过拟合,提升泛化能力。进行预测时,则是取图片的四个角加中间共 5 个位置,并进行左右翻转,一共获得 10 张图片,对他们进行预测并对 10 次结果求均值。同时,AlexNet 论文中提到了会对图像的 RGB 数据进行 PCA 处理,并对主成分做一个标准差为 0.1 的高斯扰动,增加一些噪声,这个 Trick 可以让错误率再下降 1%。

2.4 AlexNet 模型指标

AlexNet 作为 ImageNet 2012 比赛的冠军算法,在 ImageNet 测试集上达到了 15.3% 的 top-5 error rate,远远超过第二名(SIFT+FVs)的 26.2% 。如 图 2 所示。



  • 参考文献


[1] Imagenet classification with deep convolutional neural networks.

3.VGG(2012)

随着 AlexNet 在 2012 年的 ImageNet 大赛上大放异彩后,卷积神经网络进入了飞速发展的阶段。2014 年,由 Simonyan 和 Zisserman 提出的 VGG<sup>[1]</sup>网络在 ImageNet 上取得了亚军的成绩。VGG 的命名来源于论文作者所在的实验室 Visual Geometry Group,其对卷积神经网络进行了改良,探索了网络深度与性能的关系,用更小的卷积核和更深的网络结构,取得了较好的效果,成为了 CNN 发展史上较为重要的一个网络。VGG 中使用了一系列大小为 3x3 的小尺寸卷积核和池化层构造深度卷积神经网络,因为其结构简单、应用性极强而广受研究者欢迎,尤其是它的网络结构设计方法,为构建深度神经网络提供了方向。

3.1 VGG 模型结构

图 1 是 VGG-16 的网络结构示意图,有 13 层卷积和 3 层全连接层。VGG 网络的设计严格使用的卷积层和池化层来提取特征,并在网络的最后面使用三层全连接层,将最后一层全连接层的输出作为分类的预测。


VGG 中还有一个显著特点:每次经过池化层(maxpooling)后特征图的尺寸减小一倍,而通道数增加一倍(最后一个池化层除外)。


在 VGG 中每层卷积将使用 ReLU 作为激活函数,在全连接层之后添加 dropout 来抑制过拟合。使用小的卷积核能够有效地减少参数的个数,使得训练和测试变得更加有效。比如使用两层 卷积层,可以得到感受野为 5 的特征图,而比使用的卷积层需要更少的参数。由于卷积核比较小,可以堆叠更多的卷积层,加深网络的深度,这对于图像分类任务来说是有利的。VGG 模型的成功证明了增加网络的深度,可以更好的学习图像中的特征模式。


3.2 VGG 模型实现

基于 Paddle 框架,VGG 的具体实现如下代码所示:


#-*- coding:utf-8 -*-
#VGG模型代码import numpy as npimport paddle#from paddle.nn import Conv2D, MaxPool2D, BatchNorm, Linearfrom 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
复制代码

3.3 VGG 模型特点

  • 整个网络都使用了同样大小的卷积核尺寸和最大池化尺寸

  • 卷积的意义主要在于线性变换,而输入通道数和输出通道数不变,没有发生降维。

  • 两个的卷积层串联相当于 1 个的卷积层,感受野大小为。同样地,3 个的卷积层串联的效果则相当于 1 个的卷积层。这样的连接方式使得网络参数量更小,而且多层的激活函数令网络对特征的学习能力更强。

  • VGGNet 在训练时有一个小技巧,先训练浅层的的简单网络 VGG11,再复用 VGG11 的权重来初始化 VGG13,如此反复训练并初始化 VGG19,能够使训练时收敛的速度更快。

  • 在训练过程中使用多尺度的变换对原始数据做数据增强,使得模型不易过拟合。

3.4 VGG 模型指标

VGG 在 2014 年的 ImageNet 比赛上取得了亚军的好成绩,具体指标如 图 2 所示。图 2 第一行为在 ImageNet 比赛中的指标,测试集的 Error rate 达到了 7.3%,在论文中,作者对算法又进行了一定的优化,最终可以达到 6.8% 的 Error rate。



  • 参考文献


[1] Very deep convolutional networks for large-scale image recognition.

4.GoogLeNet(2014)

GoogLeNet<sup>[1]</sup>是 2014 年 ImageNet 比赛的冠军,它的主要特点是网络不仅有深度,还在横向上具有“宽度”。从名字 GoogLeNet 可以知道这是来自谷歌工程师所设计的网络结构,而名字中 GoogLeNet 更是致敬了 LeNet。GoogLeNet 中最核心的部分是其内部子网络结构 Inception,该结构灵感来源于 NIN(Network In Network)。

4.1 GoogLeNet 模型结构

由于图像信息在空间尺寸上的巨大差异,如何选择合适的卷积核来提取特征就显得比较困难了。空间分布范围更广的图像信息适合用较大的卷积核来提取其特征;而空间分布范围较小的图像信息则适合用较小的卷积核来提取其特征。为了解决这个问题,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 个卷积输出的特征图进行拼接?这是因为池化操作可以指定窗口大小,stride=1 和 padding=1,输出特征图尺寸可以保持不变。




Inception 模块的具体实现如下代码所示:


#GoogLeNet模型代码import numpy as npimport paddlefrom 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 最大池化层来减小输出高宽。


  • 第一模块使用一个 64 通道的 7 × 7 卷积层。

  • 第二模块使用 2 个卷积层:首先是 64 通道的 1 × 1 卷积层,然后是将通道增大 3 倍的 3 × 3 卷积层。

  • 第三模块串联 2 个完整的 Inception 块。

  • 第四模块串联了 5 个 Inception 块。

  • 第五模块串联了 2 个 Inception 块。

  • 第五模块的后面紧跟输出层,使用全局平均池化层来将每个通道的高和宽变成 1,最后接上一个输出个数为标签类别数的全连接层。




说明:在原作者的论文中添加了图中所示的 softmax1 和 softmax2 两个辅助分类器,如下图所示,训练时将三个分类器的损失函数进行加权求和,以缓解梯度消失现象。


4.2 GoogLeNet 模型实现

GoogLeNet 的具体实现如下代码所示:


#GoogLeNet模型代码import paddlefrom paddle import ParamAttrimport paddle.nn as nnimport paddle.nn.functional as Ffrom paddle.nn import Conv2D, BatchNorm, Linear, Dropoutfrom paddle.nn import AdaptiveAvgPool2D, MaxPool2D, AvgPool2Dfrom paddle.nn.initializer import Uniformimport 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]
复制代码

4.3 GoogLeNet 模型特色

  • 采用不同大小的卷积核意味着不同大小的感受野,最后通过拼接实现不同尺度特征的融合;

  • 之所以卷积核大小采用 1、3 和 5,主要是为了方便对齐。设定卷积步长 stride=1 之后,只要分别设定 pad=0、1、2,那么卷积之后便可以得到相同维度的特征,然后这些特征就可以直接拼接在一起了;

  • 网络越到后面,特征越抽象,而且每个特征所涉及的感受野也更大了,因此随着层数的增加,3x3 和 5x5 卷积的比例也要增加。但是,使用 5x5 的卷积核仍然会带来巨大的计算量。 为此,文章采用 1x1 卷积核来进行降维。

4.4 GoogLeNet 模型指标

GoogLeNet 在 2014 年的 ImageNet 比赛上取得了冠军的好成绩,具体指标如 图 3 所示。在测试集上 Error rate 达到了 6.67%。



  • 参考文献


[1] Going deeper with convolutions.

5.DarkNet(YOLOv2、3)

在目标检测领域的 YOLO 系列算法中,作者为了达到更好的分类效果,自己设置并训练了 DarkNet 网络作为骨干网络。其中,YOLOv2<sup>[1]</sup>首次提出 DarkNet 网络,由于其具有 19 个卷积层,所以也称之为 DarkNet19。后来在 YOLOv3<sup>[2]</sup>中,作者继续吸收了当前优秀算法的思想,如残差网络和特征融合等,提出了具有 53 个卷积层的骨干网络 DarkNet53。作者在 ImageNet 上进行了实验,发现相较于 ResNet-152 和 ResNet-101,DarkNet53 在分类精度差不多的前提下,计算速度取得了领先。

5.1 DarkNet 模型结构

5.1.1 DarkNet19

DarkNet19 中,借鉴了许多优秀算法的经验,比如:借鉴了 VGG 的思想,使用了较多的卷积,在每一次池化操作后,将通道数翻倍;借鉴了 network in network 的思想,使用全局平均池化(global average pooling)做预测,并把的卷积核置于的卷积核之间,用来压缩特征;同时,使用了批归一化层稳定模型训练,加速收敛,并且起到正则化作用。DarkNet19 的网络结构如 图 1 所示。



DarkNet19 精度与 VGG 网络相当,但浮点运算量只有其 左右,因此运算速度极快。

5.1.2 DarkNet53

DarkNet53 在之前的基础上,借鉴了 ResNet 的思想,在网络中大量使用了残差连接,因此网络结构可以设计的很深,并且缓解了训练中梯度消失的问题,使得模型更容易收敛。同时,使用步长为 2 的卷积层代替池化层实现降采样。DarkNet53 的网络结构如 图 2 所示。



考虑到当前 Darknet19 网络使用频率较低,接下来主要针对 Darknet53 网络进行实现与讲解。

5.2 DarkNet 模型实现

基于 Paddle 框架,DarkNet53 的具体实现的代码如下所示:


import paddlefrom paddle import ParamAttrimport paddle.nn as nnimport paddle.nn.functional as Ffrom paddle.nn import Conv2D, BatchNorm, Linear, Dropoutfrom paddle.nn import AdaptiveAvgPool2D, MaxPool2D, AvgPool2Dfrom paddle.nn.initializer import Uniformimport 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
复制代码

5.3 DarkNet 模型特点

  • DarkNet53 模型使用了大量的残差连接,缓解了训练中梯度消失的问题,使得模型更容易收敛。

  • DarkNet53 模型使用步长为 2 的卷积层代替池化层实现降采样。

5.4 DarkNet 模型指标

在 YOLOv3 论文中,作者在 ImageNet 数据集上对比了 DarkNet 网络与 ResNet 网络的精度及速度,如图 3 所示。可以看到 DarkNet53 的 top-5 准确率可以达到 93.8%,同时速度也明显超过了 ResNet101 和 ResNet152。



更多文章请关注公重号:汀丶人工智能



  • 参考文献


[1] YOLO9000: Better, Faster, Stronger


[2] YOLOv3: An Incremental Improvement

发布于: 2023-06-07阅读数: 16
用户头像

本博客将不定期更新关于NLP等领域相关知识 2022-01-06 加入

本博客将不定期更新关于机器学习、强化学习、数据挖掘以及NLP等领域相关知识,以及分享自己学习到的知识技能,感谢大家关注!

评论

发布
暂无评论
深度学习应用篇-计算机视觉-图像分类[2]:LeNet、AlexNet、VGG、GoogleNet、DarkNet模型结构、实现、模型特点详细介绍_人工智能_汀丶人工智能_InfoQ写作社区