LeNet概述
Paper : Gradient-Based Learning Applied to Document Recognition
Code: Deci-AI/super-gradients
LeNet-5是Yann LeCun等人在多次研究后提出的最终卷积神经网络结构,一般LeNet即指代LeNet-5。LeNet-5是用来处理手写字符的识别问题的。LeNet-5阐述了图像中像素特征之间的相关性能够由参数共享的卷积操作所提取,同时使用卷积、下采样(池化)和非线性映射这样的组合结构,是当前流行的大多是深度图像识别网络的基础。
LeNet的网络结构
LeNet-5包含七层,不包括输入,每一层都包含可训练参数(权重),当时使用的输入数据是32*32像素的图像。下面逐层介绍LeNet-5的结构,并且,卷积层将用Cx表示,子采样层则被标记为Sx,完全连接层被标记为Fx,其中x是层索引。
LeNet-5结构:
输入:32x32的灰度图像,也就是一个通道,那么一个图像就是一个2维的矩阵,没有RGB三个通道。
Layer1:6个大小为5x5的卷积核,步长为1。因此,到这里的输出变成了28x28x6。
Layer2:2x2大小的池化层,使用的是average pooling,步长为2。那么这一层的输出就是14x14x6。
Layer3:16个大小为5x5的卷积核,步长为1。但是,这一层16个卷积核中只有10个和前面的6层相连接。也就是说,这16个卷积核并不是扫描前一层所有的6个通道。如下图,0 1 2 3 4 5这 6个卷积核是扫描3个相邻,然后是6 7 8这3个卷积核是扫描4个相邻,9 10 11 12 13 14这6个是扫描4个非相邻,最后一个15扫描6个。实际上前面的6通道每个都只有10个卷积核扫描到。
这么做的原因是打破图像的对称性,并减少连接的数量。如果不这样做的话,每一个卷积核扫描一层之后是10x10,一个核大小是5x5,输入6个通道,输出16个,所以是10x10x5x5x6x16=240000个连接。但实际上只有156000连接。训练参数的数量从2400变成了1516个。
Layer4:和第二层一样,2x2大小的池化层,使用的是average pooling,步长为2。
Layer5:全连接卷积层,120个卷积核,大小为1x1。
第四层结束输出为16x5x5,相当于这里16x5x5展开为400个特征,然后使用120神经元去做全连接,如下图所示:
Layer6:全连接层,隐藏单元是84个。
Layer7:输出层,输出单元是10个,因为数字识别是0-9。
LeNet小结
AlexNet 概述:
Paper : 《ImageNet Classification with Deep Convolutional Neural Networks》
Code: alexnet-pytorch
Alex Krizhevsky等人在2012年提出了首个应用于图像分类的卷积神经网络变体AlexNet。他们认为特征本身应该被学习,而且特征应该由多个共同学习的神经网络层组成,每个层都有可学习的参数。
如下图,在网络的最底层,模型学习到了一些类似于传统滤波器的特征抽取器。
AlexNet的更高层建立在这些底层表示的基础上,以表示更大的特征,如眼睛、鼻子、草叶等等。而更高的层可以检测整个物体,如人、飞机、狗或飞盘。最终的隐藏神经元可以学习图像的综合表示,从而使属于不同类别的数据易于区分。
AlexNet使用GPU代替CPU进行运算,使得在可接受的时间范围内模型结构能够更加复杂,它的出现证明了深层卷积神经网络在复杂模型下的有效性。
AlexNet的网络架构:
AlexNet和LeNet的架构非常相似,如下图所示。左侧是LeNet, 右侧是AlexNet。
AlexNet由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。 其次,AlexNet使用ReLU而不是sigmoid作为其激活函数。 下面,让我们深入研究AlexNet的细节。
模型设计
在AlexNet的第一层,卷积窗口的形状是11×11。 由于ImageNet中大多数图像的宽和高比MNIST图像的多10倍以上,因此,需要一个更大的卷积窗口来捕获目标。 第二层中的卷积窗口形状被缩减为5×5,然后是3×3。 此外,在第一层、第二层和第五层卷积层之后,加入窗口形状为3×3、步幅为2的最大汇聚层。 而且,AlexNet的卷积通道数目是LeNet的10倍。
在最后一个卷积层后有两个全连接层,分别有4096个输出。 这两个巨大的全连接层拥有将近1GB的模型参数。 由于早期GPU显存有限,原版的AlexNet采用了双数据流设计,使得每个GPU只负责存储和计算模型的一半参数。 幸运的是,现在GPU显存相对充裕,所以我们现在很少需要跨GPU分解模型(因此,我们的AlexNet模型在这方面与原始论文稍有不同)。
最大池化
在CNN中使用重叠的最大池化。此前CNN中普遍使用平均池化,AlexNet全部使用最大池化,避 免平均池化的模糊化效果。并且AlexNet中提出让步长比池化核的尺寸小,这样池化层的输出之间 会有重叠和覆盖,提升了特征的丰富性。
激活函数
此外,AlexNet将sigmoid激活函数改为更简单的ReLU激活函数。 一方面,ReLU激活函数的计算更简单,它不需要如sigmoid激活函数那般复杂的求幂运算。 另一方面,当使用不同的参数初始化方法时,ReLU激活函数使训练模型更加容易。 当sigmoid激活函数的输出非常接近于0或1时,这些区域的梯度几乎为0,因此反向传播无法继续更新一些模型参数。 相反,ReLU激活函数在正区间的梯度总是1。 因此,如果模型参数没有正确初始化,sigmoid函数可能在正区间内得到几乎为0的梯度,从而使模型无法得到有效的训练。
使用ReLU作为CNN的激活函数,并验证其效果在较深的网络超过了Sigmoid,成功解决了
Sigmoid在网络较深时的梯度弥散问题,此外,加快了训练速度,因为训练网络使用梯度下降 法,非饱和的非线性函数训练速度快于饱和的非线性函数。虽然ReLU激活函数在很久之前就被 提出了,但是直到AlexNet的出现才将其发扬光大。
Dropout
AlexNet通过暂退法(dropout)控制全连接层的模型复杂度,而LeNet只使用了权重衰减。
Dropout虽有单独的论文论述, 但是AlexNet将其实用化,通过实践证实了它的效果。在AlexNet中主要是最后几个全连接层使用 了Dropout。
数据增强
随机地从 256 × 256 256 \times 256 256×256 的原始图像中截取 224 × 224 224 \times 224 224×224 大小的区域(以及水平翻转的镜 像),相当于增加了 ( 256 × 224 ) 2 × 2 = 2048 (256 \times 224) 2 \times 2=2048 (256×224)2×2=2048 倍的数据量。如果没有数据增强,仅靠原始的 数据量,参数众多的CNN会陷入过拟合中,使用了数据增强后可以大大减轻过拟合,提升泛化能 力。进行预测时,则是取图片的四个角加中间共 5 个位置,并进行左右翻转,一共获得10张图 片,对他们进行预测并对10次结果求均值。
使用CUDA加速深度卷积网络的训练
利用GPU强大的并行计算能力,处理神经网络训练时大量 的矩阵运算。AlexNet使用了两块GTX580GPU进行训练,单个GTX580只有3GB显存,这限制了 可训练的网络的最大规模。因此作者将AlexNet分布在两个GPU上,在每个GPU的显存中储存一 半的神经元的参数。
AlexNet的代码实现
import torch
from torch import nn
from d2l import torch as d2l
net = nn.Sequential(
# 这里,我们使用一个11*11的更大窗口来捕捉对象。
# 同时,步幅为4,以减少输出的高度和宽度。
# 另外,输出通道的数目远大于LeNet
nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
# 使用三个连续的卷积层和较小的卷积窗口。
# 除了最后的卷积层,输出通道的数量进一步增加。
# 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.Flatten(),
# 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
nn.Linear(6400, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096), nn.ReLU(),
nn.Dropout(p=0.5),
# 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
nn.Linear(4096, 10))
我们构造一个高度和宽度都为224的单通道数据,来观察每一层输出的形状。
X = torch.randn(1, 1, 224, 224)
for layer in net:
X=layer(X)
print(layer.__class__.__name__,'output shape:\t',X.shape)
Conv2d output shape: torch.Size([1, 96, 54, 54])
ReLU output shape: torch.Size([1, 96, 54, 54])
MaxPool2d output shape: torch.Size([1, 96, 26, 26])
Conv2d output shape: torch.Size([1, 256, 26, 26])
ReLU output shape: torch.Size([1, 256, 26, 26])
MaxPool2d output shape: torch.Size([1, 256, 12, 12])
Conv2d output shape: torch.Size([1, 384, 12, 12])
ReLU output shape: torch.Size([1, 384, 12, 12])
Conv2d output shape: torch.Size([1, 384, 12, 12])
ReLU output shape: torch.Size([1, 384, 12, 12])
Conv2d output shape: torch.Size([1, 256, 12, 12])
ReLU output shape: torch.Size([1, 256, 12, 12])
MaxPool2d output shape: torch.Size([1, 256, 5, 5])
Flatten output shape: torch.Size([1, 6400])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 10])
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
lr, num_epochs = 0.01, 10
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.327, train acc 0.881, test acc 0.885
4149.6 examples/sec on cuda:0
AlexNet小结:
ZFNet概述
Paper :《Visualizing and Understanding Convolutional Networks 》
Code: pytorch/captum
ZFNet在2013年 ILSVRC 图像分类竞赛获得冠军,错误率11.19% ,比2012年的AlexNet降低了5%,ZFNet是由 Matthew D.Zeiler 和 Rob Fergus 在 AlexNet 基础上提出的大型卷积网络。ZFNet解释了为什么卷积神经网络可以在图像分类上表现的如此出色,以及研究了如何优化卷积神经网络。ZFNet提出了一种可视化的技术,通过可视化,我们就可以了解卷积神经网络中间层的功能和分类器的操作,这样就就可以找到较好的模型。ZFNet还进行消融实验来研究模型中的每个组件,它会对模型有什么影响。
ZFNet的网络结构
Z F N e t = ( conv + relu + maxpooling ) × 2 + ( conv + relu ) × 3 + f c × 2 + softmax Z F N e t=(\text { conv }+\text { relu }+\text { maxpooling }) \times 2+(\text { conv }+\text { relu }) \times 3+f c \times 2+\text { softmax } ZFNet=( conv + relu + maxpooling )×2+( conv + relu )×3+fc×2+ softmax
ZFNet 仅仅是在 AlexNet 上做了一些调参:
改变了 AlexNet 的第一层,即将卷积核的尺寸大小 11x11 变成 7x7,并且将步长 4 变成了 2。
ZFNet实际上是微调(Fine-tuning)了AlexNet, 并通过反卷积(Deconvolution) 的方式可视化各层的输出特征图,进一步解释了卷积操作在大型网络中效果显著的原因。
对卷积结果的可视化
作者将卷积核的计算结果(feature maps)映射回原始的像素空间(映射的方法为反卷积,反池化)并进行可视化。例如,下图Layer1区域最左上角的九宫格代表第一层卷积计算得到的前九张feature maps映射回原图像素空间后的可视化(称为f9)。第一层卷积使用96个卷积核,这意味着会得到96张feature maps,这里的前九张feature maps是指96个卷积核中值最大的9个卷积核对应生成的feature maps(这里称这9个卷积核为k9,即,第一层卷积最关注的前九种特征)。可以发现,这九种特征都是颜色和纹理特征,即蕴含语义信息少的结构性特征。
为了证明这个观点,作者又将数据集中的原始图像裁剪成小图,将所有的小图送进网络中,得到第一层卷积计算后的feature maps。统计能使k9中每个kernel输入计算结果最大的前9张输入小图,即9*9=81张,如下图红框中右下角所示。结果表明刚刚可视化的f9和这81张小图表征的特征是相似的,且一一对应的。由此证明卷积网络在第一层提取到的是一些颜色,纹理特征。
同理,观察Layer2和Layer3的可视化发现,第二次和第三次卷积提取到的特征蕴含的语义信息更丰富,不再是简单的颜色纹理信息,而是一些结构化的特征,例如蜂窝形状,圆形,矩形等等。那么网络的更深层呢?我们看下图:
在网络的深层,如第四层,第五层卷积提取到的是更高级的语义信息,如人脸特征,狗头特征,鸟腿鸟喙特征等等。
最后,越靠近输出端,能激活卷积核的输入图像相关性越少(尤其是空间相关性),例如Layer5中,最右上角的示例:feature map中表征的是一种绿色成片的特征,可是能激活这些特征的原图相关性却很低(原图是人,马,海边,公园等,语义上并不相干);其实这种绿色成片的特征是‘草地’,而这些语义不相干的图片里都有‘草地’。‘草地’是网络深层卷积核提取的是高级语义信息,不再是低级的像素信息,空间信息等等。
网络中对不同特征的学习速度
如下图所示,横轴表示训练轮数,纵轴表示不同层的feature maps映射回像素空间后的可视化结果:
由此可以看出,low-level的特征(颜色,纹理等)在网络训练的训练前期就可以学习到, 即更容易收敛;high-level的语义特征在网络训练的后期才会逐渐学到。 由此展示了不同特征的进化过程。这也是一个合理的过程,毕竟高级的语义特征,要在低级特征的基础上学习提取才能得到。
图像的平移、缩放、旋转对CNN的影响
下图是探究图片平移对卷积模型影响的实验,a1是五张不同的图片经过不同大小的左右平移后的结果。
a2是原始图片与经过平移后的图片分别送进卷积网络后,第一层卷积计算得到的feature maps之间的欧氏距离,可以看出当图片平移0个像素时(即图中横轴=0处),距离最小(等于0)。其他位置随着左右平移,得到的距离都会陡增或陡减。五条彩色曲线分别代表五张不同的原始图片。
a3是原始图片与经过平移后的图片分别送进卷积网络后,第七层卷积计算得到的feature maps之间的欧氏距离,可以看出趋势与a2类似;但是,增减的曲线变换更平缓,这一定程度上说明了网络的深层提取的是高级语义特征,而不是低级的颜色,纹理,空间特征。这种语义信息不会随着平移操作而轻易改变,例如狗的图片平移后还是狗。
这个性质叫做:卷积拥有良好的平移不变性。
最后,a4表示的是原始图片与经过平移后的图片分别送进卷积网络后,卷积网络最后的识别结果。可以看出识别准确率是相对平稳的,且在横轴x=0时,识别准确率较高(此时,图片不平移,识别物体基本在图片中心位置)。
下图探究图片缩放对卷积模型影响的实验,实验方法和表述与上面探讨平移时的设置类似。结果表明,网络的浅层相较于网络的深层对缩放操作更敏感;且最终的识别准确率较平稳。这个趋势跟探究平移操作对卷积模型影响的趋势类似,即:卷积操作也具有良好的缩放不变性。
下图是探究图片旋转对卷积模型影响的实验,可以看出旋转操作对卷积的影响正好与平移和缩放相反:卷积第一层对旋转的敏感程度较低,第七层对旋转的敏感程度高。这是因为颜色,纹理这些低级特征旋转前后还是相似的特征;但是目标级别的高级语义特征却不行,例如“特征9”旋转180°后变成了“特征6”. 看最终的识别准确率曲线也能发现旋转0°和350°时模型的识别准确率最高,因为此时旋转后模型最接近原始图片。对于某些存在对称性质的特征,例如原图中的电视,在旋转90°,180°,270°时都有不错的识别准确率。因此,卷积操作不具有良好的旋转不变性。
总结:
卷积的平移不变性是从滑动遍历这个操作带来的,不管一个特征出现在图中的什么位置,卷积核都可以通过滑动的方式,滑动到特征上面做识别。
卷积的缩放不变性则是从网络的层级结构中获得,不同层的卷积操作拥有不同尺寸的计算感受野 。至于旋转不变性缺失找不到对应的操作。
那么,为什么现在的一些成熟项目,例如人脸识别,图像分类等依然可以对旋转的图片做识别呢? 这是因为我们用大量的训练数据,旋转不变性可以从大量的训练数据中得到。其实,不仅是旋转不变性,卷积本身计算方法带来的平移不变性和缩放不变性也是脆弱的,大部分也是从数据集中学习到的。深度学习是一种基于数据驱动的算法。
改变卷积核的大小
ZFNet通过对AelxNet可视化发现,由于第一层的卷积核尺寸过大导致某些特征图失效(失效指的是一些值太大或太小的情况,容易引起网络的数值不稳定性,进而导致梯度消失或爆炸。图中的体现是(a)中的黑白像素块)。
此外,由于第一层的步长过大,导致第二层卷积结果出现棋盘状的伪影(例如(d)中第二小图和倒数第三小图)。因此ZFNet做了对应的改进。即将第一层 11X11步长为4的卷积操作变成 7X7步长为2的卷积。
ZFNet的实现代码
import torch.nn as nn
import torch
# 与AlexNet有两处不同: 1. 第一次的卷积核变小,步幅减小。 2. 第3,4,5层的卷积核数量增加了。
class ZFNet(nn.Module):
def __init__(self, num_classes=1000, init_weights=False):
super(ZFNet, self).__init__()
self.features = nn.Sequential(
nn.Conv2d(3, 96, kernel_size=7, stride=2, padding=2), # input[3, 224, 224] output[96, 111, 111]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[96, 55, 55]
nn.Conv2d(96, 256, kernel_size=5, padding=2), # output[256, 55, 55]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[256, 27, 27]
nn.Conv2d(256, 512, kernel_size=3, padding=1), # output[512, 27, 27]
nn.ReLU(inplace=True),
nn.Conv2d(512, 1024, kernel_size=3, padding=1), # output[1024, 27, 27]
nn.ReLU(inplace=True),
nn.Conv2d(1024, 512, kernel_size=3, padding=1), # output[512, 27, 27]
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # output[512, 13, 13]
)
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(512 * 13 * 13, 4096),
nn.ReLU(inplace=True),
nn.Dropout(p=0.5),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes),
)
if init_weights:
self._initialize_weights()
def forward(self, x):
x = self.features(x)
x = torch.flatten(x, start_dim=1)
x = self.classifier(x)
return x
def _initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
if m.bias is not None:
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
def zfnet(num_classes):
model = ZFNet(num_classes=num_classes)
return model
VGGNet概述
Paper : 《Very Deep Convolutional Networks for Large-Scale Image Recognition》
Code: pytorch/vision
VGGNet由牛津大学计算机视觉组合和Google DeepMind公司研究员一起研发的深度卷积神经网络。它探索了卷积神经网络的深度和其性能之间的关系,通过反复的堆叠33的小型卷积核和22的最大池化层,成功的构建了16~19层深的卷积神经网络。VGGNet获得了ILSVRC 2014年比赛的亚军和定位项目的冠军,在top5上的错误率为7.5%。目前为止,VGGNet依然被用来提取图像的特征。
VGGNet全部使用3*3的卷积核和2*2的池化核,通过不断加深网络结构来提升性能。网络层数的增长并不会带来参数量上的爆炸,因为参数量主要集中在最后三个全连接层中。同时,两个3*3卷积层的串联相当于1个5*5的卷积层,3个3*3的卷积层串联相当于1个7*7的卷积层,即3个3*3卷积层的感受野大小相当于1个7*7的卷积层。但是3个3*3的卷积层参数量只有7*7的一半左右,同时前者可以有3个非线性操作,而后者只有1个非线性操作,这样使得前者对于特征的学习能力更强。
VGGNet作者总结出LRN层作用不大,越深的网络效果越好,1*1的卷积也是很有效的,但是没有3*3的卷积效果好,因为3*3的网络可以学习到更大的空间特征。
VGGNet的网络结构
VGGNet一共有六种不同的网络结构A(VGG11)、A-LRN(VGG11-LRN)、B(VGG13)、C(VGG16-1)、D(VGG16-3)、E(VGG-19),这6种网络结构相似,都是由5层卷积层、3层全连接层组成,其中区别在于每个卷积层的子层数量不同,从A至E依次增加(子层数量从1到4),总的网络深度从11层到19层(添加的层以粗体显示)。
VGG11-LRN表示在第一层中采用了LRN的VGG-11;VGG16-1表示后三组卷积块中最后一层卷积采用卷积核尺寸为1x1, 相应的VGG16-3表示卷积核尺寸为3x3。
表格中的卷积层参数表示为“conv⟨感受野大小⟩-通道数⟩”,例如con3-128,表示使用3x3的卷积核,通道数为128。为了简洁起见,在表格中不显示ReLU激活功能。
以VGG-16为例,架构图如下:
VGG-16架构:13个卷积层+3个全连接层,前5段卷积网络(标号1-5),最后一段全连接网络(标号6-8),网络总共参数数量大约138M左右。。
(1) 输入层(Input):图像大小为224×224×3
(2) 卷积层1+ReLU:conv3 - 64(卷积核的数量):kernel size:3 stride:1 pad:1
像素:(224-3+2×1)/1+1=224 输出为224×224×64 64个feature maps
参数: (3×3×3)×64+64=1792
(3) 卷积层2+ReLU:conv3 - 64:kernel size:3 stride:1 pad:1
像素: (224-3+1×2)/1+1=224 输出为224×224×64 64个feature maps
参数: (3×3×64×64)+64=36928
(4) 最大池化层: pool2: kernel size:2 stride:2 pad:0
像素: (224-2)/2 = 112 输出为112×112×64,64个feature maps
参数: 0
(5) 卷积层3+ReLU:.conv3-128:kernel size:3 stride:1 pad:1
像素: (112-3+2×1)/1+1 = 112 输出为112×112×128,128个feature maps
参数: (3×3×64×128)+128=73856
(6) 卷积层4+ReLU:conv3-128:kernel size:3 stride:1 pad:1
像素: (112-3+2×1)/1+1 = 112 输出为112×112×128,128个feature maps
参数: (3×3×128×128)+128=147584
(7) 最大池化层:pool2: kernel size:2 stride:2 pad:0
像素: (112-2)/2+1=56 输出为56×56×128,128个feature maps。
参数: 0
(8) 卷积层5+ReLU:conv3-256: kernel size:3 stride:1 pad:1
像素: (56-3+2×1)/1+1=56 输出为56×56×256,256个feature maps
参数: (3×3×128×256)+256=295168
(9) 卷积层6+ReLU:conv3-256: kernel size:3 stride:1 pad:1
像素: (56-3+2×1)/1+1=56 输出为56×56×256,256个feature maps,
参数: (3×3×256×256)+256=590080
(10) 卷积层7+ReLU:conv3-256: kernel size:3 stride:1 pad:1
像素: (56-3+2×1)/1+1=56 输出为56×56×256,256个feature maps
参数: (3×3×256×256)+256=590080
(11) 最大池化层:pool2: kernel size:2 stride:2 pad:0
像素:(56 - 2)/2+1=28 输出为28×28×256,256个feature maps
参数: 0
(12) 卷积层8+ReLU:conv3-512:kernel size:3 stride:1 pad:1
像素:(28-3+2×1)/1+1=28 输出为28×28×512,512个feature maps
参数: (3×3×256×512)+512=1180160
(13) 卷积层9+ReLU:conv3-512:kernel size:3 stride:1 pad:1
像素:(28-3+2×1)/1+1=28 输出为28×28×512,512个feature maps
参数: (3×3×512×512)+512=2359808
(14) 卷积层10+ReLU:conv3-512:kernel size:3 stride:1 pad:1
像素:(28-3+2×1)/1+1=28 输出为28×28×512,512个feature maps
参数: (3×3×512×512)+512=2359808
(15) 最大池化层:pool2: kernel size:2 stride:2 pad:0,输出为14×14×512,512个feature maps。
像素:(28-2)/2+1=14 输出为14×14×512
参数: 0
(16) 卷积层11+ReLU:conv3-512:kernel size:3 stride:1 pad:1
像素:(14-3+2×1)/1+1=14 输出为14×14×512,512个feature maps
参数: (3×3×512×512)+512=2359808
(17) 卷积层12+ReLU:conv3-512:kernel size:3 stride:1 pad:1
像素:(14-3+2×1)/1+1=14 输出为14×14×512,512个feature maps
参数: (3×3×512×512)+512=2359808
(18) 卷积层13+ReLU:conv3-512:kernel size:3 stride:1 pad:1
像素:(14-3+2×1)/1+1=14 输出为14×14×512,512个feature maps,
参数: (3×3×512×512)+512=2359808
(19) 最大池化层:pool2:kernel size:2 stride:2 pad:0
像素:(14-2)/2+1=7 输出为7×7×512,512个feature maps
参数: 0
(20) 全连接层1+ReLU+Dropout:有4096个神经元或4096个feature maps
像素:1×1×4096
参数:7×7×512×4096 = 102760448
(21) 全连接层2+ReLU+Dropout:有4096个神经元或4096个feature maps
像素:1×1×4096
参数:4096×4096 = 16777216
(22) 全连接层3:有1000个神经元或1000个feature maps
像素:1×1×1000
参数:4096×1000=4096000
(23) 输出层(Softmax):输出识别结果,看它究竟是1000个可能类别中的哪一个。
VGGNet的代码实现
经典卷积神经网络的基本组成部分是下面的这个序列:
vgg_block
的函数来实现一个VGG块。该函数有三个参数,分别对应于卷积层的数量num_convs
、输入通道的数量in_channels
和输出通道的数量out_channels
import torch
from torch import nn
from d2l import torch as d2l
def vgg_block(num_convs, in_channels, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv2d(in_channels, out_channels,
kernel_size=3, padding=1))
layers.append(nn.ReLU())
in_channels = out_channels
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
VGG网络可以分为两部分:第一部分主要由卷积层和汇聚层组成,第二部分由全连接层组成。
VGG神经网络连接几个VGG块(在vgg_block
函数中定义)。其中有超参数变量conv_arch
。该变量指定了每个VGG块里卷积层个数
和输出通道数
。全连接模块则与AlexNet中的相同。
原始VGG网络有5个卷积块,其中前两个块各有一个卷积层,后三个块各包含两个卷积层。 第一个模块有64个输出通道,每个后续模块将输出通道数量翻倍,直到该数字达到512。由于该网络使用8个卷积层和3个全连接层,因此它通常被称为VGG-11。
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))
下面的代码实现了VGG-11。可以通过在conv_arch
上执行for循环来简单实现。
def vgg(conv_arch):
conv_blks = []
in_channels = 1
# 卷积层部分
for (num_convs, out_channels) in conv_arch:
conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
in_channels = out_channels
return nn.Sequential(
*conv_blks, nn.Flatten(),
# 全连接层部分
nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
nn.Linear(4096, 10))
net = vgg(conv_arch)
接下来,我们将构建一个高度和宽度为224的单通道数据样本,以观察每个层输出的形状。
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
X = blk(X)
print(blk.__class__.__name__,'output shape:\t',X.shape)
Sequential output shape: torch.Size([1, 64, 112, 112])
Sequential output shape: torch.Size([1, 128, 56, 56])
Sequential output shape: torch.Size([1, 256, 28, 28])
Sequential output shape: torch.Size([1, 512, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
Flatten output shape: torch.Size([1, 25088])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 10])
正如你所看到的,我们在每个块的高度和宽度减半,最终高度和宽度都为7。最后再展平表示,送入全连接层处理。
由于VGG-11比AlexNet计算量更大,因此我们构建了一个通道数较少的网络,足够用于训练Fashion-MNIST数据集。
ratio = 4
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch)
除了使用略高的学习率外,模型训练过程与 AlexNet类似。
lr, num_epochs, batch_size = 0.05, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.177, train acc 0.934, test acc 0.911
2562.3 examples/sec on cuda:0
VGGNet 小结:
VGG网络的特点是利用小的尺寸核代替大的卷积核,然后把网络做深。
NiN的概述
Paper : 《Network In Network》
Code: pytorch/vision
Network In NetWork(NIN) 是由新加坡国立大学的MinLin 等人提出的,在CIFAR-10和CIFAR-100分类任务中达到了SOTA结果。提出mlpconv,引入了1x1卷积和global average pooling,提出Network In Network(NIN),整个模型未使用全连接。模型不够深,迁移能力有待加强。
LeNet、AlexNet和VGG都有一个共同的设计模式:通过一系列的卷积层与汇聚层来提取空间结构特征;然后通过全连接层对特征的表征进行处理。 AlexNet和VGG对LeNet的改进主要在于如何扩大和加深这两个模块。
AlexNet和VGG都是先由卷积层构成的模块充分抽取空间特征,再由全连接层构成的模块来输出分类结果。然而,如果使用了全连接层,可能会完全放弃表征的空间结构,且参数量巨大。 因此NiN提出用1*1卷积代替全连接层,串联多个由卷积层和“全连接”层构成的小网络来构建⼀个深层网络。
NiN的网络结构
NIN由三层感知卷积层(MLPConv Layer) 构成。
每一层MLPConv Layer内部由若干个局部的全连接层和非线性激活函数组成,代替了传统卷积层中采用的线性卷积核。
线性卷积层和mlpconv都是将局部感受野(local receptive field)映射到输出特征向量。Mlpconv核使用带非线性激活函数的MLP,跟传统的CNN一样,MLP在各个局部感受野中共享参数的,滑动MLP核可以最终得到输出特征图。
优点是:1. 提供了网络层间映射的一种新可能;2. 增加了网络卷积层的非线性能力。
传统的CNN模型先是使用堆叠的卷积层提取特征,输入全连接层(FC)进行分类。这种结构沿袭自LeNet5,把卷积层作为特征抽取器,全连接层作为分类器。但是FC层参数数量太多,很容易过拟合,会影响模型的泛化性能。因此需要用Dropout增加模型的泛化性。
在NIN中,卷积层后不接全连接层(FC),而是将最后一个的mlpconv的输出每个特征图全局平均池化(global average pooling,GAP) ,而后softmax。
NiN的代码实现
卷积层的输入和输出由四维张量组成,张量的每个轴分别对应样本、通道、高度和宽度。 另外,全连接层的输入和输出通常是分别对应于样本和特征的二维张量。 NiN的想法是在每个像素位置(针对每个高度和宽度)应用一个全连接层。 如果我们将权重连接到每个空间位置,我们可以将其视为1×1卷积层,或作为在每个像素位置上独立作用的全连接层。 从另一个角度看,即将空间维度中的每个像素视为单个样本,将通道维度视为不同特征(feature)。
下图说明了VGG和NiN及它们的块之间主要架构差异。 NiN块以一个普通卷积层开始,后面是两个1×1的卷积层。这两个1×1卷积层充当带有ReLU激活函数的逐像素全连接层。 第一层的卷积窗口形状通常由用户设置。 随后的卷积窗口形状固定为1×1。
import torch
from torch import nn
from d2l import torch as d2l
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())
最初的NiN网络是在AlexNet后不久提出的,显然从中得到了一些启示。 NiN使用窗口形状为11×11、5×5和3×3的卷积层,输出通道数量与AlexNet中的相同。 每个NiN块后有一个最大汇聚层,汇聚窗口形状为3×3,步幅为2。
NiN和AlexNet之间的一个显著区别是NiN完全取消了全连接层。 相反,NiN使用一个NiN块,其输出通道数等于标签类别的数量。最后放一个全局平均汇聚层(global average pooling layer),生成一个对数几率 (logits)。NiN设计的一个优点是,它显著减少了模型所需参数的数量。然而,在实践中,这种设计有时会增加训练模型的时间。
net = nn.Sequential(
nin_block(1, 96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(96, 256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(256, 384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
# 标签类别数是10
nin_block(384, 10, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
# 将四维的输出转成二维的输出,其形状为(批量大小,10)
nn.Flatten())
我们创建一个数据样本来查看每个块的输出形状。
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)
Sequential output shape: torch.Size([1, 96, 54, 54])
MaxPool2d output shape: torch.Size([1, 96, 26, 26])
Sequential output shape: torch.Size([1, 256, 26, 26])
MaxPool2d output shape: torch.Size([1, 256, 12, 12])
Sequential output shape: torch.Size([1, 384, 12, 12])
MaxPool2d output shape: torch.Size([1, 384, 5, 5])
Dropout output shape: torch.Size([1, 384, 5, 5])
Sequential output shape: torch.Size([1, 10, 5, 5])
AdaptiveAvgPool2d output shape: torch.Size([1, 10, 1, 1])
Flatten output shape: torch.Size([1, 10])
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.363, train acc 0.865, test acc 0.879
3212.2 examples/sec on cuda:0
NiN小结
GoogLeNet的概述
Paper : 《Going Deeper with Convolutions》
Code: pytorch/vision
GoogleNet 网络是14年由 Google 团队提出,斩获该年 ImageNet 竞赛中 Classification Task(分类任务)第一名。之所以名为“GoogLeNet”而非“GoogleNet”,文章说是为了向早期的LeNet致敬。
GoogLeNet吸收了NiN中串联网络的思想,并在此基础上做了改进。 这篇论文的一个重点是解决了什么样大小的卷积核最合适的问题。 毕竟,以前流行的网络使用小到1×1,大到11×11的卷积核。 本文的一个观点是,有时使用不同大小的卷积核组合是有利的。
大量的文献表明可以将稀疏矩阵聚类为较为密集的子矩阵来提高计算性能,据此论文提出了名为Inception 的结构来实现此目的,既能保持网络结构的稀疏性,又能利用密集矩阵的高计算性能。
GoogLeNet的网络结构
但是,使用5x5的卷积核仍然会带来巨大的计算量。 为此,文章借鉴NIN2,采用1x1卷积核来进行降维。
例如:上一层的输出为100x100x128,经过具有256个输出的5x5卷积层之后(stride=1,pad=2),输出数据为100x100x256。其中,卷积层的参数为128x5x5x256。假如上一层输出先经过具有32个输出的1x1卷积层,再经过具有256个输出的5x5卷积层,那么最终的输出数据仍为为100x100x256,但卷积参数量已经减少为128x1x1x32 + 32x5x5x256,大约减少了4倍。
具体改进后的Inception Module如下图:
Inception块由四条并行路径组成。 前三条路径使用窗口大小为1×1、3×3和5×5的卷积层,从不同空间大小中提取信息。 中间的两条路径在输入上执行1×1卷积,以减少通道数,从而降低模型的复杂性。 第四条路径使用3×3最大汇聚层,然后使用1×1卷积层来改变通道数。 这四条路径都使用合适的填充来使输入与输出的高和宽一致,最后我们将每条线路的输出在通道维度上连结,并构成Inception块的输出。在Inception块中,通常调整的超参数是每层输出通道数。
GoogLeNet一共使用9个Inception块和全局平均汇聚层的堆叠来生成其估计值。Inception块之间的最大汇聚层可降低维度。 第一个模块类似于AlexNet和LeNet,Inception块的组合从VGG继承,全局平均汇聚层避免了在最后使用全连接层。
GoogLeNet的代码实现
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
class Inception(nn.Module):
# c1--c4是每条路径的输出通道数
def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# 线路1,单1x1卷积层
self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
# 线路2,1x1卷积层后接3x3卷积层
self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
# 线路3,1x1卷积层后接5x5卷积层
self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
# 线路4,3x3最大汇聚层后接1x1卷积层
self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)
def forward(self, x):
p1 = F.relu(self.p1_1(x))
p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
p4 = F.relu(self.p4_2(self.p4_1(x)))
# 在通道维度上连结输出
return torch.cat((p1, p2, p3, p4), dim=1)
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
第二个模块使用两个卷积层:第一个卷积层是64个通道、1×1卷积层;第二个卷积层使用将通道数量增加三倍的3×3卷积层。 这对应于Inception块中的第二条路径。
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
nn.ReLU(),
nn.Conv2d(64, 192, kernel_size=3, padding=1),
nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
第三个模块串联两个完整的Inception块。 第一个Inception块的输出通道数为64+128+32+32=256,四个路径之间的输出通道数量比为64:128:32:32=2:4:1:1
。 第二个和第三个路径首先将输入通道的数量分别减少到96/192=1/2
和16/192=1/12,然后连接第二个卷积层。第二个Inception块的输出通道数增加到128+192+96+64=480,四个路径之间的输出通道数量比为128:192:96:64=4:6:3:2。 第二条和第三条路径首先将输入通道的数量分别减少到128/256=1/2和32/256=1/8。
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
Inception(256, 128, (128, 192), (32, 96), 64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
第四模块更加复杂, 它串联了5个Inception块,其输出通道数分别是192+208+48+64=512、160+224+64+64=512、128+256+64+64=512、112+288+64+64=528和256+320+128+128=832。 这些路径的通道数分配和第三模块中的类似,首先是含3×3卷积层的第二条路径输出最多通道,其次是仅含1×1卷积层的第一条路径,之后是含5×5卷积层的第三条路径和含3×3最大汇聚层的第四条路径。 其中第二、第三条路径都会先按比例减小通道数。 这些比例在各个Inception块中都略有不同。
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
Inception(512, 160, (112, 224), (24, 64), 64),
Inception(512, 128, (128, 256), (24, 64), 64),
Inception(512, 112, (144, 288), (32, 64), 64),
Inception(528, 256, (160, 320), (32, 128), 128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
第五模块包含输出通道数为256+320+128+128=832和384+384+128+128=1024的两个Inception块。 其中每条路径通道数的分配思路和第三、第四模块中的一致,只是在具体数值上有所不同。 需要注意的是,第五模块的后面紧跟输出层,该模块同NiN一样使用全局平均汇聚层,将每个通道的高和宽变成1。 最后我们将输出变成二维数组,再接上一个输出个数为标签类别数的全连接层。
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
Inception(832, 384, (192, 384), (48, 128), 128),
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten())
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))
GoogLeNet模型的计算复杂,而且不如VGG那样便于修改通道数。 为了使Fashion-MNIST上的训练短小精悍,我们将输入的高和宽从224降到96,这简化了计算。下面演示各个模块输出的形状变化。
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)
Sequential output shape: torch.Size([1, 64, 24, 24])
Sequential output shape: torch.Size([1, 192, 12, 12])
Sequential output shape: torch.Size([1, 480, 6, 6])
Sequential output shape: torch.Size([1, 832, 3, 3])
Sequential output shape: torch.Size([1, 1024])
Linear output shape: torch.Size([1, 10])
和以前一样,我们使用Fashion-MNIST数据集来训练我们的模型。在训练之前,我们将图片转换为96×96分辨率。
lr, num_epochs, batch_size = 0.1, 10, 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.254, train acc 0.904, test acc 0.885
3570.5 examples/sec on cuda:0
GoogLeNet 小结
ResNet的概述
Paper : Deep Residual Learning for Image Recognition
Code: pytorch/vision/models/resnet.py
ResNet 网络是在 2015年 由微软实验室中的何凯明等几位大神提出,斩获当年ImageNet竞赛中分类任务第一名,目标检测第一名。获得COCO数据集中目标检测第一名,图像分割第一名。
ResNet的主要贡献:
ResNet的提出背景:
在ResNet提出之前,所有的神经网络都是通过卷积层和池化层的叠加组成的。
人们认为卷积层和池化层的层数越多,获取到的图片特征信息越全,学习效果也就越好。但是在实际的试验中发现,随着卷积层和池化层的叠加,不但没有出现学习效果越来越好的情况,反而两种问题:
ResNet 的结构
Residual 的计算方式
residual结构使用了一种shortcut的连接方式,也可理解为捷径。让特征矩阵隔层相加,注意F(X)和X形状要相同,所谓相加是特征矩阵相同位置上的数字进行相加。
如上,左图是一个正常的块,右图是一个残差块。
让我们聚焦于神经网络局部:如图所示,假设我们的原始输入为x,而希望学出的理想映射为f(x)(作为上方激活函数的输入)。 左图虚线框中的部分需要直接拟合出该映射f(x),而右图虚线框中的部分则需要拟合出残差映射f(x)−x。 残差映射在现实中往往更容易优化。 以本节开头提到的恒等映射作为我们希望学出的理想映射f(x),我们只需将右图虚线框内上方的加权运算(如仿射)的权重和偏置参数设成0,那么f(x)即为恒等映射。 实际中,当理想映射f(x)极接近于恒等映射时,残差映射也易于捕捉恒等映射的细微波动。 右图是ResNet的基础架构–残差块(residual block)。 在残差块中,输入可通过跨层数据线路更快地向前传播。
残差块
ResNet沿用了VGG完整的3×3卷积层设计。 残差块里首先有2个有相同输出通道数的3×3卷积层。 每个卷积层后接一个批量规范化层和ReLU激活函数。 然后我们通过跨层数据通路,跳过这2个卷积运算,将输入直接加在最后的ReLU激活函数前。 这样的设计要求2个卷积层的输出与输入形状一样,从而使它们可以相加。 如果想改变通道数,就需要引入一个额外的1×1卷积层来将输入变换成需要的形状后再做相加运算。
两种Residual
左侧残差结构称为 BasicBlock (上文刚介绍过),右侧残差结构称为 Bottleneck
BottleNeck的结构:
(1)其中第一层的1× 1的卷积核的作用是对特征矩阵进行降维操作,将特征矩阵的深度由256降为64;
第三层的1× 1的卷积核是对特征矩阵进行升维操作,将特征矩阵的深度由64升成256。
降低特征矩阵的深度主要是为了减少参数的个数。
如果采用BasicBlock,参数的个数应该是:256×256×3×3×2=1179648
采用Bottleneck,参数的个数是:1×1×256×64+3×3×64×64+1×1×256×64=69632
(2)先降后升为了主分支上输出的特征矩阵和捷径分支上输出的特征矩阵形状相同,以便进行加法操作。
注:CNN参数个数 = 卷积核尺寸×卷积核深度 × 卷积核组数 = 卷积核尺寸 × 输入特征矩阵深度 × 输出特征矩阵深度
ResNet的代码实现
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
class Residual(nn.Module): #@save
def __init__(self, input_channels, num_channels,
use_1x1conv=False, strides=1):
super().__init__()
self.conv1 = nn.Conv2d(input_channels, num_channels,
kernel_size=3, padding=1, stride=strides)
self.conv2 = nn.Conv2d(num_channels, num_channels,
kernel_size=3, padding=1)
if use_1x1conv:
self.conv3 = nn.Conv2d(input_channels, num_channels,
kernel_size=1, stride=strides)
else:
self.conv3 = None
self.bn1 = nn.BatchNorm2d(num_channels)
self.bn2 = nn.BatchNorm2d(num_channels)
def forward(self, X):
Y = F.relu(self.bn1(self.conv1(X)))
Y = self.bn2(self.conv2(Y))
if self.conv3:
X = self.conv3(X)
Y += X
return F.relu(Y)
如下图所示,此代码生成两种类型的网络: 一种是当use_1x1conv=False
时,应用ReLU非线性函数之前,将输入添加到输出。 另一种是当use_1x1conv=True
时,添加通过1×1卷积调整通道和分辨率。
下面我们来查看输入和输出形状一致的情况。
blk = Residual(3,3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
Y.shape
# torch.Size([4, 3, 6, 6])
我们也可以在增加输出通道数的同时,减半输出的高和宽。
blk = Residual(3,6, use_1x1conv=True, strides=2)
blk(X).shape
# torch.Size([4, 6, 3, 3])
ResNet的前两层跟之前介绍的GoogLeNet中的一样: 在输出通道数为64、步幅为2的7×7卷积层后,接步幅为2的3×3的最大汇聚层。 不同之处在于ResNet每个卷积层后增加了批量规范化层。
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
GoogLeNet在后面接了4个由Inception块组成的模块。 ResNet则使用4个由残差块组成的模块,每个模块使用若干个同样输出通道数的残差块。 第一个模块的通道数同输入通道数一致。 由于之前已经使用了步幅为2的最大汇聚层,所以无须减小高和宽。 之后的每个模块在第一个残差块里将上一个模块的通道数翻倍,并将高和宽减半。
下面我们来实现这个模块。注意,我们对第一个模块做了特别处理。
def resnet_block(input_channels, num_channels, num_residuals,
first_block=False):
blk = []
for i in range(num_residuals):
if i == 0 and not first_block:
blk.append(Residual(input_channels, num_channels,use_1x1conv=True, strides=2))
else:
blk.append(Residual(num_channels, num_channels))
return blk
接着在ResNet加入所有残差块,这里每个模块使用2个残差块。
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))
最后,与GoogLeNet一样,在ResNet中加入全局平均汇聚层,以及全连接层输出。
net = nn.Sequential(b1, b2, b3, b4, b5,
nn.AdaptiveAvgPool2d((1,1)),
nn.Flatten(), nn.Linear(512, 10))
每个模块有4个卷积层(不包括恒等映射的1×1卷积层)。 加上第一个7×7
卷积层和最后一个全连接层,共有18层。 因此,这种模型通常被称为ResNet-18。 通过配置不同的通道数和模块里的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。 虽然ResNet的主体架构跟GoogLeNet类似,但ResNet架构更简单,修改也更方便。这些因素都导致了ResNet迅速被广泛使用。 下图描述了完整的ResNet-18。
在训练ResNet之前,让我们观察一下ResNet中不同模块的输入形状是如何变化的。 在之前所有架构中,分辨率降低,通道数量增加,直到全局平均汇聚层聚集所有特征。
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
X = layer(X)
print(layer.__class__.__name__,'output shape:\t', X.shape)
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 64, 56, 56])
Sequential output shape: torch.Size([1, 128, 28, 28])
Sequential output shape: torch.Size([1, 256, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
AdaptiveAvgPool2d output shape: torch.Size([1, 512, 1, 1])
Flatten output shape: torch.Size([1, 512])
Linear output shape: torch.Size([1, 10])
同之前一样,我们在Fashion-MNIST数据集上训练ResNet。
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.011, train acc 0.997, test acc 0.915
4701.1 examples/sec on cuda:0
DenseNet 概述
Paper : 《Densely Connected Convolutional Networks》
Code: densenet.py
回想一下任意函数的泰勒展开式(Taylor expansion),它把这个函数分解成越来越高阶的项。在x接近0时,
f ( x ) = f ( 0 ) + f ′ ( 0 ) x + f ′ ′ ( 0 ) 2 ! x 2 + f ′ ′ ′ ( 0 ) 3 ! x 3 + … f(x)=f(0)+f^{\prime}(0) x+\frac{f^{\prime \prime}(0)}{2 !} x^2+\frac{f^{\prime \prime \prime}(0)}{3 !} x^3+\ldots f(x)=f(0)+f′(0)x+2!f′′(0)x2+3!f′′′(0)x3+…
同样, ResNet将函数展开为
f ( x ) = x + g ( x ) f(\mathbf{x})=\mathbf{x}+g(\mathbf{x}) f(x)=x+g(x)
也就是说,ResNet将 f f f分解为两部分:一个简单的线性项和一个复杂的非线性项。 那么再向前拓展一步,如果我们想将 f f f 拓展成超过两部分的信息呢? 一种方案便是DenseNet。
如图所示,ResNet和DenseNet的关键区别在于,DenseNet输出是连接(用图中的[,]
表示)而不是如ResNet的简单相加。 因此,在应用越来越复杂的函数序列后,我们执行从x
到其展开式的映射:
x → [ x , f 1 ( x ) , f 2 ( [ x , f 1 ( x ) ] ) , f 3 ( [ x , f 1 ( x ) , f 2 ( [ x , f 1 ( x ) ] ) ] ) , … ] \mathbf{x} \rightarrow\left[\mathbf{x}, f_1(\mathbf{x}), f_2\left(\left[\mathbf{x}, f_1(\mathbf{x})\right]\right), f_3\left(\left[\mathbf{x}, f_1(\mathbf{x}), f_2\left(\left[\mathbf{x}, f_1(\mathbf{x})\right]\right)\right]\right), \ldots\right] x→[x,f1(x),f2([x,f1(x)]),f3([x,f1(x),f2([x,f1(x)])]),…]
最后,将这些展开式结合到多层感知机中,再次减少特征的数量。 实现起来非常简单:我们不需要添加术语,而是将它们连接起来。 DenseNet这个名字由变量之间的“稠密连接”而得来,最后一层与之前的所有层紧密相连。 稠密连接如下图所示。
稠密网络主要由2部分构成:稠密块(dense block)和过渡层(transition layer)。 前者定义如何连接输入和输出,而后者则控制通道数量,使其不会太复杂。
DenseNet 的网络结构
恒等映射( identity mapping)
,使用了元素加法(Element-wise addition
)。有助于训练过程中的梯度的反向传播,从而能够训练出更深的网络。Channel-wise concatenation
), 实现特征重用,作为下一层的输入。这样不但减缓了梯度消失的现象,也使其可以在参数和计算量更少的情况下实现比ResNet更优的性能。DenseNet的网络结构
DenseNet的密集连接方式需要特征图大小保持一致,所以DenseNet网络中使用的是DenseBlock+Transition
的结构。
DenseBlock 是包含很多层的模块,在每个dense block中,特征映射的大小是相同的,因此它们可以很容易地concat在一起。层(dense layer)与层之间采用的是密集连接的方式。
Transition Layer 1×1 Conv
和2×2 average pooling
被用作两个相邻dense block之间的过渡层(Transition Layer)
。
在最后一个dense block的末尾,执行全局平均池化,然后附加一个softmax分类器。
Dense Block
在DenseBlock中,各个层的特征图大小一致,可以在通道(channel)维度上连接。DenseBlock中的非线性组合函数 H l ( ⋅ ) H_l(\cdot) Hl(⋅)采用的是 BN+ ReLU + 3x3 Conv
。输出是一个k
个输出通道的特征图。
x 5 = h 5 ( [ x 0 , x 1 , x 2 , x 3 , x 4 ] ) x_5=h_5([x_0,x_1,x_2,x_3,x_4]) x5=h5([x0,x1,x2,x3,x4])。
假定输入层的通道数是 k 0 k_0 k0,DenseBlock中各个层卷积之后均输出 k k k个特征图,即得到的特征图的通道数为 k k k,那么l层输入的通道数为 k 0 + ( l − 1 ) k k_0+(l−1)k k0+(l−1)k, 我们将 k k k称之为网络的增长率(growth rate)
。
因为每一层都接受前面所有层的特征图,即特征传递方式是直接将前面所有层的特征concat后传到下一层,一般情况下使用较小的K(比如12),要注意这个K的实际含义就是这层新提取出的特征。
Dense Block采用了激活函数在前、卷积层在后的顺序,即BN-ReLU-Conv
的顺序,这种方式也被称为pre-activation
。通常的模型relu等激活函数处于卷积conv、批归一化batchnorm之后,即Conv-BN-ReLU
,也被称为post-activation
。这个想法是从Pre-Activation ResNet 中得来。作者证明,如果采用post-activation设计,性能会变差。
Transition Layer
主要用来连接两个相邻的DenseBlock,并且降低特征图的大小。Transition Layer层包括一个1x1的卷积和2x2的AvgPooling,结构为BN+ReLU+1x1Conv+2x2AvgPooling
DenseNet-B (Bottleneck Layers)
由于后面层的输入通道数会非常大,DenseBlock内部可以采用bottleneck Layers
来减少为了减少模型的复杂度和大小,主要是原有的结构中增加1x1 Conv,BN+ReLU+1x1Conv+BN+ReLU+3x3Conv
,
这种结构也称作DenseNet-B
结构。
其中1x1 Conv的作用是固定输出通道数,达到降维的作用,1×1卷积输出的通道数通常是GrowthRate的4倍。即把上一层输入的 l × k l\times k l×k个通道数减少成 4 × k 4 \times k 4×k 个通道数。从而降低特征数量,提升计算效率。
当几十个Bottleneck相连接时,concat后的通道数会增加到上千,如果不增加1×1的卷积来降维,后续3×3卷积所需的参数量会急剧增加。
比如,输入通道数 64 64 64,增长率 K = 32 K=32 K=32,经过 15 15 15个Bottleneck,通道数输出为 64 + 15 × 32 = 544 64+15\times32=544 64+15×32=544,
如果不使用1×1卷积,第 16 16 16个Bottleneck层参数量是 3 × 3 × 544 × 32 = 156672 3\times3\times544\times32=156672 3×3×544×32=156672,
如果使用1×1卷积,第 16 16 16个Bottleneck层参数量是 1 × 1 × 544 × 128 + 3 × 3 × 128 × 32 = 106496 1\times1\times544\times128+3\times3\times128\times32=106496 1×1×544×128+3×3×128×32=106496,可以看到参数量大大降低。
DenseNet-C(Transition Layers)
在一个包含有m个特征图的dense block, Transition Layer产生 θ m \theta m θm 个特征图,其中 θ \theta θ 被看做是压缩系数
。
当 θ = 1 \theta =1 θ=1 时,通过过渡层后特征映射数量保持不变。当 θ < 1 \theta <1 θ<1 时,就被称为DenseNet-C
, 在实验中选取的是 θ = 0.5 \theta=0.5 θ=0.5
DenseNet-BC (Further Compression)
当同时采用了bottleneck
和transition layer
,且 θ < 1 \theta <1 θ<1, 这样的模型被称作DenseNet-BC。
Denseblock 优缺点
“deep supervision”
。误差信号可以很容易地传播到较早的层,所以较早的层可以从最终分类层获得直接监督。(2)减少参数与提升计算效率
对于每一层,ResNet中的参数数量与 C × C C×C C×C成正比,DenseNet中的参数数量与 l × k × k l\times k\times k l×k×k成正比。
由于k远小于C, 因此DenseNet 比 ResNet要小很多。
(3) 更多样化的特征
由于DenseNet中的每一层都接收前面所有的层作为输入,因此特征更加多样化,往往具有更丰富的模式。
(4) 保存了低纬度的特征
在标准的卷积网络中,最终输出只会利用提取最高层次的特征。
而在DenseNet中,它使用了不同层次的特征,倾向于给出更平滑的决策边界。这也解释了为什么训练数据不足时DenseNet表现依旧良好。
DenseNet的代码实现
import torch
from torch import nn
from d2l import torch as d2l
def conv_block(input_channels, num_channels):
return nn.Sequential(
nn.BatchNorm2d(input_channels), nn.ReLU(),
nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1))
一个denseblock由多个卷积块组成,每个卷积块使用相同数量的输出通道。 然而,在前向传播中,我们将每个卷积块的输入和输出在通道维上连结。
class DenseBlock(nn.Module):
def __init__(self, num_convs, input_channels, num_channels):
super(DenseBlock, self).__init__()
layer = []
for i in range(num_convs):
layer.append(conv_block(
num_channels * i + input_channels, num_channels))
self.net = nn.Sequential(*layer)
def forward(self, X):
for blk in self.net:
Y = blk(X)
# 连接通道维度上每个块的输入和输出
X = torch.cat((X, Y), dim=1)
return X
在下面的例子中,我们定义一个有2个输出通道数为10的DenseBlock
。 使用通道数为3的输入时,我们会得到通道数为3+2×10=23的输出。 卷积块的通道数控制了输出通道数相对于输入通道数的增长,因此也被称为增长率(growth rate)。
blk = DenseBlock(2, 3, 10)
X = torch.randn(4, 3, 8, 8)
Y = blk(X)
Y.shape
def transition_block(input_channels, num_channels):
return nn.Sequential(
nn.BatchNorm2d(input_channels), nn.ReLU(),
nn.Conv2d(input_channels, num_channels, kernel_size=1),
nn.AvgPool2d(kernel_size=2, stride=2))
对上一个例子中稠密块的输出使用通道数为10的过渡层。 此时输出的通道数减为10,高和宽均减半。
blk = transition_block(23, 10)
blk(Y).shape # torch.Size([4, 10, 4, 4])
我们来构造DenseNet模型。DenseNet首先使用同ResNet一样的单卷积层和最大汇聚层。
b1 = nn.Sequential(
nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
nn.BatchNorm2d(64), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
接下来,类似于ResNet使用的4个残差块,DenseNet使用的是4个稠密块。 与ResNet类似,我们可以设置每个稠密块使用多少个卷积层。 这里我们设成4,从而与 ResNet-18保持一致。 稠密块里的卷积层通道数(即增长率)设为32,所以每个稠密块将增加128个通道。
在每个模块之间,ResNet通过步幅为2的残差块减小高和宽,DenseNet则使用过渡层来减半高和宽,并减半通道数。
# num_channels为当前的通道数
num_channels, growth_rate = 64, 32
num_convs_in_dense_blocks = [4, 4, 4, 4]
blks = []
for i, num_convs in enumerate(num_convs_in_dense_blocks):
blks.append(DenseBlock(num_convs, num_channels, growth_rate))
# 上一个稠密块的输出通道数
num_channels += num_convs * growth_rate
# 在稠密块之间添加一个转换层,使通道数量减半
if i != len(num_convs_in_dense_blocks) - 1:
blks.append(transition_block(num_channels, num_channels // 2))
num_channels = num_channels // 2
与ResNet类似,最后接上全局汇聚层和全连接层来输出结果。
net = nn.Sequential(
b1, *blks,
nn.BatchNorm2d(num_channels), nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
nn.Linear(num_channels, 10))
lr, num_epochs, batch_size = 0.1, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
loss 0.140, train acc 0.950, test acc 0.838
5569.1 examples/sec on cuda:0
DenseNet小结
Darknet-19 的概述
Paper : 《YOLO9000: Better, Faster, Stronger》
Code: AlexeyAB/darknet
Darknet-19 是Joseph Redmon 于2016年提出的。
Darknet-19 是 YOLO v2 的 backbone。Darknet-19 总共有 19 层 conv 层, 5 个 maxpooling 层。Darknet-19 吸收了 VGG16, NIN 等网络的优点,网络结构小巧,但是性能强悍。
Darknet-19的网络结构
Darknet-53 的概述
Paper : YOLOv3: An Incremental Improvement
Code : pjreddie/darkne
Darknet-19 是Joseph Redmon 于2018年提出的。
Darknet-19 是 YOLO v3 的 backbone。Darknet-53 借鉴了 Resnet 的设计思想,引入了 shotcut 的概念。
Darknet-53的网络结构
Darknet53的结构如下图。左侧是 Darknet-53 的完整结构图,右侧是对左侧中的 Convolutional
层 和 Residual
层的细节展示。
Darknet-53 顾名思意,总共有 53 个卷积层,但是下面面的结构中只看到 52 个卷积层,其实 最后的 “Connected” 层也是卷积层(1x1 的卷积实现的类似 fc 的效果,所以也有资料把它称做 fc 层),“Connected” 层的具体参数如下。
[convolutional]
filters=1000
size=1
stride=1
pad=1
activation=linear
Darknet-53 输入 image size 为 256(没有 fc 层,显然可以随意修改输入 size),最后得到的 feature map size 为 8x8,stride 为 32(值得注意的是 5 次下采样,都不是通过 pooling 做的,而是通过 stride 为 2 的卷积层实现的,上图中蓝色框标出的位置,darknet-19 中是通过 max pooling)。
EfficientNetv1的概述
Paper :《EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks》
Code :efficientnet_pytorch
EfficientNetv1的网络结构
MBConv结构
MBConv其实就是MobileNetV3网络中的InvertedResidualBlock,但也有些许区别。一个是采用的激活函数不一样(EfficientNet的MBConv中使用的都是Swish激活函数),另一个是在每个MBConv中都加入了SE(Squeeze-and-Excitation)模块。
如图所示,MBConv结构主要由一个1x1的普通卷积(升维作用,包含BN和Swish),一个kxk的Depthwise Conv卷积(包含BN和Swish)k的具体值可看EfficientNet-B0的网络框架主要有3x3和5x5两种情况,一个SE模块,一个1x1的普通卷积(降维作用,包含BN),一个Droupout层构成。
搭建过程中还需要注意几点:
第一个升维的1x1卷积层,它的卷积核个数是输入特征矩阵channel的n 倍,n∈{1,6}。
当n = 1 时,不要第一个升维的1x1卷积层,即Stage2中的MBConv结构都没有第一个升维的1x1卷积层(这和MobileNetV3网络类似)。
关于shortcut连接,仅当输入MBConv结构的特征矩阵与输出的特征矩阵shape相同时才存在(代码中可通过stride==1 and inputc_channels==output_channels
条件来判断)。
SE模块如下所示,由一个全局平均池化,两个全连接层组成。第一个全连接层的节点个数是输入该MBConv特征矩阵channels的1/4。且使用Swish激活函数。第二个全连接层的节点个数等于Depthwise Conv层输出的特征矩阵channels,且使用Sigmoid激活函数。
Dropout层的dropout_rate在tensorflow的keras源码中对应的是drop_connect_rate后面会细讲(注意,在源码实现中只有使用shortcut的时候才有Dropout层)。
EfficientNet结构
下表为EfficientNet-B0的网络框架(B1-B7就是在B0的基础上修改Resolution,Channels以及Layers),可以看出网络总共分成了9个Stage
Stage1
就是一个卷积核大小为3x3步距为2的普通卷积层(包含BN和激活函数Swish)
Stage2~Stage8
都是在重复堆叠MBConv结构(最后一列的Layers表示该Stage重复MBConv结构多少次)
Stage9
由一个普通的1x1的卷积层(包含BN和激活函数Swish)一个平均池化层和一个全连接层组成。
表格中每个MBConv后会跟一个数字1或6,这里的1或6就是倍率因子n即MBConv中第一个1x1的卷积层会将输入特征矩阵的channels扩充为n倍,其中k3x3或k5x5表示MBConv中Depthwise Conv所采用的卷积核大小。Channels表示通过该Stage后输出特征矩阵的Channels。
EfficientNet(B0-B7)参数
input_size
代表训练网络时输入网络的图像大小
width_coefficient
代表channel维度上的倍率因子,比如在 EfficientNetB0中Stage1的3x3卷积层所使用的卷积核个数是32,那么在B6中就是32 × 1.8 = 57.6 32 \times 1.8=57.632×1.8=57.6接着取整到离它最近的8的整数倍即56,其它Stage同理。
depth_coefficient
代表depth维度上的倍率因子(仅针对Stage2到Stage8),比如在EfficientNetB0中Stage7的
,那么在B6中就是4 × 2.6 = 10.4接着向上取整即11.
drop_connect_rate
是在MBConv结构中dropout层使用的drop_rate,在官方keras模块的实现中MBConv结构的drop_rate是从0递增到drop_connect_rate的(具体实现可以看下官方源码,注意,在源码实现中只有使用shortcut的时候才有Dropout层)。还需要注意的是,这里的Dropout层是Stochastic Depth,即会随机丢掉整个block的主分支(只剩捷径分支,相当于直接跳过了这个block)也可以理解为减少了网络的深度。具体可参考《Deep Networks with Stochastic Depth》这篇文章。
dropout_rate
是最后一个全连接层前的dropout层(在stage9的Pooling与FC之间)的dropout_rate。
EfficientNetv2的概述
Paper: 《EfficientNetV2: Smaller Models and Faster Training》
Code : efficientnetv2
EfficientNetV1中存在的问题
训练图像的尺寸很大时,训练速度非常慢。
在网络浅层中使用Depthwise convolutions速度会很慢。
EfficientNetV2中做出的贡献
1)引入新的网络(EfficientNetV2),该网络在训练速度以及参数数量上都优于先前的一些网络。
2)提出了改进的渐进学习方法,该方法会根据训练图像的尺寸动态调节正则方法(例如dropout、data augmentation和mixup)。通过实验展示了该方法不仅能够提升训练速度,同时还能提升准确率。
3)通过实验与先前的一些网络相比,训练速度提升11倍,参数数量减少为1/6.8
EfficientNetv2的网络结构
下表展示了作者使用NAS搜索得到的EfficientNetV2-S模型框架(注意,在源码中Stage6的输出Channels是等于256并不是表格中的272,Stage7的输出Channels是1280并不是表格中的1792,后续论文的版本会修正过来)。
相比与EfficientNetV1,主要有以下不同:
Conv3x3
就是普通的3x3卷积 + 激活函数(SiLU)+ BNFused-MBConv
模块上面再讲EfficientNetV1存在问题章节有讲到过,模块名称后跟的1,4表示expansion ratio,k3x3表示kenel_size为3x3,下面是我自己重绘的结构图,注意当expansion ratio等于1时是没有expand conv的,还有这里是没有使用到SE结构的(原论文图中有SE)。注意当stride=1且输入输出Channels相等时才有shortcut连接。还需要注意的是,当有shortcut连接时才有Dropout层,而且这里的Dropout层是Stochastic Depth,即会随机丢掉整个block的主分支(只剩捷径分支,相当于直接跳过了这个block)也可以理解为减少了网络的深度。具体可参考《Deep Networks with Stochastic Depth》这篇文章。Paper :《CSPNET: A New Backbone that can Enhance Learning Capability of CNN》
Code : CrossStagePartialNetworks
Chien-Yao Wang等人在CVPR2019上发表的。文章提出了跨阶段局部网络(CrossStagePartialNetworks,CSPNet),以缓解以往的工作需进行大量推理计算的问题。作者把这个问题归结为网络优化中的重复梯度信息。
DenseNet的网络结构
上图为DenseNet单阶段结构的详细结构。DenseNet的每个阶段包含一个稠密块(Dense Block)和一个过渡层(Transition Layer),每个稠密块由 k k k个稠密层(Dense Layer)组成。其中,第 l l l层接收所有先前的层 x 0 , x 1 , . . . , x l − 1 x_0, x_1,..., x_{l-1} x0,x1,...,xl−1的特征图作为输入。
DenseNet的前向传播
方程为:
其中 ∗ * ∗ 表示卷积运算符, w i w_i wi和 x i x_i xi 分别表示第 i i i个稠密层的权值和输出。
DenseNet的权值更新
方程为:
其中 f f f 为权值更新函数, g i g_i gi为传播到第 i i i个稠密层的梯度。我们可以发现大量的梯度信息被用来更新不同稠密层的权值。这将导致无差异的稠密层反复学习同样的梯度信息。
跨阶段局部DenseNet (Cross Stage Partial DenseNet)
提出的CSPDenseNet的单阶段的架构如上图所示。
CSPDenseNet的一个阶段由局部密集块(Partial Dense Block)
和局部过渡层(Partial Transition Layer)
组成。
在局部密集块
中:把通道 x 0 = [ x 0 ′ , x 0 ′ ′ ] x_0=[x_0^{'},x_0^{''}] x0=[x0′,x0′′]的基础层特征映射成两部分。其中 x 0 ′ x_0^{'} x0′ 直接连接到阶段的末端, x 0 ′ ′ x_0^{''} x0′′进入到下一个密集块。
在局部过渡层
中:首先,Dense Layer K的输出 [ x 0 ′ ′ , x 1 , . . . , x k ] [x_0^{''},x_1,...,x_k] [x0′′,x1,...,xk]将会进入到下一个过渡层。然后这个过渡层的输出 x T x_T xT将和 x 0 ′ x_0^{'} x0′相连,并进入到另一个过渡层,最终生成输出 x U x_U xU
CSPDenseNet的前向传播
方程为:
CSPDenseNet的权值更新
方程为:
从上面的权值更新方程可以看出,来自密集层的梯度是单独积分的。而且,没有经过密集层的特征图 x 0 ′ x_0^{'} x0′ 也被单独积分。对于更新权值的梯度信息,两边不包含属于其他边的重复梯度信息。
总的来说,所提出的CSPDenseNet保留了DenseNet特性重用特性的优点,但同时通过截断梯度流防止了过多的重复梯度信息。该思想通过设计一种分层的特征融合策略来实现,并应用于局部过渡层。
局部密集块(Partial Dense Block)
设计局部稠密块的目的是:
局部过渡层(Partial Transition Layer)
设计局部过渡层的目的是使梯度组合的差异最大。局部过渡层是一种层次化的特征融合机制,它利用梯度流的聚合策略来防止不同的层学习重复的梯度信息。在这里,作者设计了两个CSPDenseNet变体来展示这种梯度流截断是如何影响网络的学习能力的。
Fusion First
:是将两部分生成的feature map进行拼接,然后进入过渡层。如果采用这种策略,将会损失大量的梯度信息。
Fusion Last
:对于fusion last策略,来自稠密块的输出将经过过渡层,然后与来自Part1的feature map进行连接。如果采用这种策略,由于梯度流被截断,梯度信息将不会被重用。
应用CSPNet到其它网路
CSPNet也可以轻松地应用于ResNet 和 ResNeXt,其架构如下图所示。由于只有一半的特征通道通过Res(X)Blocks,因此就不再需要引入bottleneck了。当固定浮点运算(FLOP)时,这使理论上的内存访问成本(MAC)下限成为可能。
看的准才能预测的准
作者建议使用EFM为每个锚点捕获合适的Field of View(FOV),以增强单阶段目标检测器的准确性。
对于分割任务,由于像素级标签通常不包含全局信息,因此通常更可取的是考虑使用更大的patches以获得更好的信息检索。但是,对于诸如图像分类和对象检测之类的任务,从图像级和边框框级标签观察时,某些关键信息可能会模糊不清。 Li等人发现,当从图像级标签中学习时,CNN经常会分散注意力并得出结论,这是两阶段目标检测器优于一级目标检测器的主要原因之一。
聚合特征金字塔
提出的EFM能够更好地聚合初始特征金字塔。 EFM基于YOLOv3 ,对每个真实物体精确分配了一个预选框。每个真值边界框对应于一个超过阈值IoU的预选框。如果预选框的大小等于网格单元的Field of View,则对于第s的网格单元,相应的边界框将由第s-1个scale的下界和第s+1个scale的上限局决定。因此,EFM会从三个比例尺中组合特征。
平衡计算开销
由于来自特征金字塔的串联特征图非常庞大,因此会引入大量的内存和计算成本。为了缓解该问题,我们采用了Maxout技术来压缩特征图。
上图描述了不同特征金字塔的融合策略
a) Feature Pyramid Network(FPN)
: 把当前尺度和前一个尺度的特征融合
b) Global Fusion Model(GFM)
: 从所有尺度上融合特征。
c) Exact Fusion Model(EFM)
: 根据anchor的大小融合特征。
深度学习领域内也在努力促使神经网络向小型化发展。在保证模型准确率的同时体积更小,速度更快。到了2016年直至现在,业内提出了SqueezeNet、ShuffleNet、MobileNet等轻量级网络模型。这些模型使移动终端、嵌入式设备运行神经网络模型成为可能。
SqueezeNet的概述
Paper : 《SqueezeNet: AlexNet-level accuracy with 50x fewer parameters and <0.5MB model size》
Code :DeepScale/SqueezeNet
Iandola, Forrest N等人在2016年提出了SqueezeNet, 考察了深度学习中除了精度之外的另一个重要因素:模型大小。
有两处值得学习的亮点:
使用以下三个策略来减少SqueezeNet设计参数
(1)使用1∗1卷积代替3∗3卷积:参数减少为原来的1/9
(2)减少输入通道数量:这一部分使用squeeze layers来实现
(3)将欠采样操作延后,可以给卷积层提供更大的激活图:更大的激活图保留了更多的信息,可以提供更高的分类准确率
其中,(1)和(2)可以显著减少参数数量,(3)可以在参数数量受限的情况下提高准确率
SqueezeNet的模型结构
如上图,使用的基础模块称为fire。包含三个卷积层,步长均为1。分为squeeze
和expand
两部分,分别压缩和扩展数据的通道数。
expand部分中,两个不同核尺寸的结果通过串接层合并输出。
fire模块有三个可调参数:
N N N : squeeze部分, 1 × 1 1 \times 1 1×1 卷积层的通道数
c 1 \mathrm{c}_1 c1 : expand部分, 1 × 1 1 \times 1 1×1 卷积层的通道数
c 2 \mathrm{c}_2 c2 : expand部分, 3 × 3 3 \times 3 3×3 卷积层的通道数
输入输出尺寸相同。输入通道数不限,输出通道数为 c 1 + c 2 \mathrm{c}_1+\mathrm{c}_2 c1+c2 。 在本文提出的SqueezeNet结构中, c 1 = c 2 = 4 N \mathrm{c}_1=\mathrm{c}_2=4 \mathrm{N} c1=c2=4N 。
SqueezeNet的网络结构
整个网络包含10层。
SqueezeNet的小结
MobileNet的概述
Paper :《MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications》
Code : applications/mobilenet.py
传统卷积神经网络,内存需求大,计算量大,导致无法在移动设备以及嵌入式设备上运行。在准确率小幅降低的前提下大大减少模型参数和运算量(相比VGG16准确率降低0.9%,但是模型参数只有VGG的1/32)
Mobilenet这篇论文是Google针对手机等嵌入式设备提出的一种轻量级的深层神经网络,取名为MobileNets。个人感觉论文所做工作偏向于模型压缩方面,核心思想就是卷积核的巧妙分解,可以有效减少网络参数。
MobileNet V1引入了深度可分离卷积,作为传统卷积层的有效替代。深度可分卷积通过将空间滤波与特征生成机制分离开来,有效地分解了传统卷积。深度可分离卷积由两个单独的层定义:用于空间滤波的轻量级深度卷积和用于特征生成的1x1点卷积。
网络中的亮点:
(1) Depthwise Convolusion (大大减少运算量和参数数量)
(2) 增加超参数 α \alpha α和 β \beta β。 其中 α \alpha α 是卷积层中卷积核的个数, β \beta β 是控制输入图像的大小。这两个超参数是人为设置的,并不是训练得到的。
MobileNetV2
相比于 MobileNet V1网络,准确率更高,模型更小。
MobileNet V2引入了线性瓶颈和反向残留结构,以便通过利用问题的低级别性质来制造更有效的层结构。由一个1x1升维卷积,然后是DW卷积和一个1x1投影层来定义。当且仅当输入和输出具有相同数量的通道时,才使用残差结构连接输入和输出。该结构在输入和输出处保持了一个紧凑的表示,同时扩展到内部的高维特征空间,以增加非线性全通道转换的表现力。
(1)Linear Bottlenecks: 引入了bottleneck结构。
(2)Inverted Residuals(倒残差结构): 将bottleneck结构变成了纺锤型,即resnet是先缩小为原来的1/4,再放大,他是放大到原来的6倍,再缩小。
(3)并且去掉了Residual Block最后的ReLU。
MobileNetV3
与MobileNet V2相比,MobileNet V3-Large在ImageNet分类上提高了3.2%,同时减少了20%的延迟。
MobileNet V3-Small比具有同等延迟的MobileNet V2模型提高了6.6%。
MobileNet V3-Large检测比MobileNet V2在COCO检测上的准确率高25%以上。
MobileNet V3-Large LRASPP比MobileNet V2 R-ASPP在城市景观分割类似精度上快34%。
MobileNetV3的亮点:
(1)更新了Block: 在倒残差的基础上进行简单的改动 ,加入了SE模块,更新了激活函数。
(2)使用NAS搜索参数
(3)重新设计耗时层结构
MobileNet的网络结构
MobileNetv1的结构图
整个网络不算平均池化层与softmax层,共28层;
在整个网络结构中步长为2的卷积较有特点,卷积的同时充当下采样的功能;
第一层之后的26层都为深度可分离卷积的重复卷积操作;
每一个卷积层(含常规卷积、深度卷积、逐点卷积)之后都紧跟着批规范化和ReLU激活函数;
最后一层全连接层不使用激活函数。
MobileNetv2的结构图
MobileNetV2中主要引入线性瓶颈结构和反向残差结构。
MobileNetV2网络模型中有共有17个Bottleneck层(每个Bottleneck包含两个逐点卷积层和一个深度卷积层),一个标准卷积层(conv),两个逐点卷积层(pw conv),共计有54层可训练参数层。MobileNetV2中使用线性瓶颈(Linear Bottleneck)和反向残差(Inverted Residuals)结构优化了网络,使得网络层次更深了,但是模型体积更小,速度更快了。
MobileNetv3的结构图
MobileNetV3-Large
MobileNetV3-Small
MobileNetV3分为Large和Small两个版本,Large版本适用于计算和存储性能较高的平台,Small版本适用于硬件性能较低的平台。
Large版本共有15个bottleneck层,一个标准卷积层,三个逐点卷积层。
Small版本共有12个bottleneck层,一个标准卷积层,两个逐点卷积层。
MobileNetV3中引入了5×5大小的深度卷积代替部分3×3的深度卷积。引入Squeeze-and-excitation(SE)模块和h-swish(HS)激活函数以提高模型精度。结尾两层逐点卷积不使用批规范化(Batch Norm),MobileNetV3结构图中使用NBN标识。
网络结构上相对于MobileNetV2的结尾部分做了优化,去除三个高阶层,如上图所示。去除后减少了计算量和参数量,但是模型的精度并没有损失。
值得一提的是,不论是Large还是Small版本,都是使用神经架构搜索(NAS)技术生成的网络结构。
ShffleNet的概述
Paper :《ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices》
Code : models/shufflenetv1.py
ShuffleNet是旷视科技最近提出的一种计算高效的CNN模型,其和MobileNet和SqueezeNet等一样主要是想应用在移动端。所以,ShuffleNet的设计目标也是如何利用有限的计算资源来达到最好的模型精度,这需要很好地在速度和精度之间做平衡。ShuffleNet的核心是采用了两种操作:pointwise group convolution
和channel shuffle
,这在保持精度的同时大大降低了模型的计算量。目前移动端CNN模型主要设计思路主要是两个方面:模型结构设计和模型压缩。ShuffleNet和MobileNet一样属于前者,都是通过设计更高效的网络结构来实现模型变小和变快,而不是对一个训练好的大模型做压缩或者迁移。下面我们将详细讲述ShuffleNet的设计思路,网络结构及模型效果,最后使用Pytorch来实现ShuffleNet网络。
channel shuffle具体是怎么实现的呢?下图标绿框部分即为channel shuffle的操作,即从得到的特征图中提取出不同组别下的通道,并将他们组合在一起,最终channel shaffle完成后的结果如(c)中黄色虚线框所示。
上图中从绿框传变成黄色虚线框的过程即为channel shaffle通道重排过程,现将其分解出来,看看channel shaffle通道重排具体过程,如下图:
ShuffleNet的结构
(a)是最原始的残差结构,(b)、(c)都是shuffleNet的单元结构。先来看看(a)是如何变成(b)的,首先是用逐点分组卷积代替了逐点卷积,同时在其后跟了一个channel shuffle操作,然后中间的3x3DWConv没有变化,最后的逐点卷积依旧换成了逐点分组卷积。(c)是用来降采样的,和(b)主要区别在shortcut分支中采用了3x3d的步长为2的AVG Pool、主分支中DWConv中的步长变成了2以及最后使用的是Concat而不是add,这样的结构会使输入图像尺寸减半,通道数翻倍。
Xception的概述
Paper :《Xception: Deep Learning with Depthwise Separable Convolutions》
Code : research/deeplab
传统的卷积操作同时对输入的feature mapping的跨通道交互性(cross-channel correlations)、空间交互性(spatial correlations)进行了映射。
Inception系列结构着力于将上述过程进行分解,在一定程度上实现了跨通道相关性和空间相关性的解耦。
文章在Inception的基础上进行改进,使用深度可分离卷积(depthwise separate convolution)替代传统的Inception块,实现跨通道相关性和空间相关性的完全解耦。此外,文章还引入了残差连接,最终提出了Xception的网络结构。
传统Conv进行操作时,同时在输入的通道维度和空间维度进行了操作,跨通道相关性和空间相关性的耦合性很高。
例如,对于h*h*c的feature mapping,卷积核尺寸为s*s,但实际上卷积核尺寸为s*s*c。在进行一次卷积操作时,实际上对feature mapping中的一个s*s*c进行了信息融合,其中s*s为空间维度,c为通道维度。
InceptionV3中的原始模块
Inception结构在一定程度上对跨通道相关性和空间相关性进行了解耦。
先对feature mapping进行pointwise convolutions(1*1 Conv),以实现跨通道相关性的独立映射;然后进行分组卷积,将映射空间划分为若干个子空间,在子空间内进行卷积操作。
可以发现的是,Inception结构在一定程度上实现了跨通道相关性和空间相关性的解耦,但并未实现完全解耦(子空间的卷积操作)。
第一层简化
Figure1 里基本都用了1 * 1 卷积进行降维,在这里可以说是在通道维度上进行处理。随后又处理了空间维度。
简化了一下 变成了Figure2。
第二层简化
将所有的1 * 1 卷积合并到了一起,并且画了一条隔阂一样的东西,这里的意思就是每个3 * 3 的卷积取这条隔阂里的三分之一处理,且互不重合,假设1 * 1 之后生成了30个通道,则后面的3 * 3 卷积各处理其中的10个通道。
多分支卷积中使用1*1 Conv进行降维等价于先进行1*1 Conv后在进行分组卷积。以简化版的Inception为例,Figure 3 给出了Inception的等价形式。
最后的简化
这里作者直接提出了 ‘Extreme’版本的处理方法,在之前是 假设了1 * 1 之后有30个通道,分给了三个卷积,每个卷积分到10个通道,而这里作者的 ‘Extreme’版本是 直接将这30个通道分给30个卷积去处理,即单独的对每一个空间信息做处理。
所以整体下来 ‘Extreme’ 版本 就是 1 * 1处理通道维度之间的信息,3 * 3 卷积处理每一个通道维度内的空间维度信息。
这个思想结构被称为 ,深度可分离卷积 (depthwise separable convolution & separable convolution),
depthwise convolution
(空间维度的卷积,也就是长宽方向卷积)
pointwise convolution
(通道维度的卷积)
Xception的结构
了Xception的具体结构如下图,其中共包括36层卷积,分为14个stage。
Xception包含三个部分:输入部分,中间部分和结尾部分;其中所有卷积层和可分离卷积层后面都使用Batch Normalization处理,所有的可分离卷积层使用一个深度乘数1(深度方向并不进行扩充)
GhostNet的概述
Paper :《GhostNet: More Features from Cheap Operations》
Code : huawei-noah/Efficient-AI-Backbones
在优秀CNN模型中,特征图存在冗余是非常重要的,但是很少有人在模型结构设计上考虑特征图冗余问题(The redundancy in feature maps)。
GhostNet就从特征图冗余问题出发,提出一个仅通过少量计算(cheap operations)就能生成大量特征图的结构——Ghost Module。
Ghost Module通过怎么样的操作生成特征图呢?这个操作是,一系列线性操作(a series of linear transformations)
在这里,经过线性操作生成的特征图称为ghost feature maps,而被操作的特征图称为intrinsic feature maps。
即插即用:Ghost Module是一个即插即用模块,可以无缝衔接现有的CNN中。
采用Ghost Module组成的Ghost bottlenecks,设计出Ghost Net,在ILSVRC-2012上top1超过Mobilenet-V3,并且参数更少。
GhostNet的网络结构
GhostModule
通常的卷积如图2(a)所示,而Ghost Module则分为两步操作来获得与普通卷积一样数量的特征图(这里需要强调,是数量一样)。
第一步:少量卷积(比如正常用32个卷积核,这里就用16个,从而减少一半的计算量)
第二步:cheap operations,如图中的Φ表示,从问题3中可知,Φ是诸如3*3的卷积,并且是逐个特征图的进行卷积(Depth-wise convolutional)。
Ghost Bottlenecks
Ghost Bottlenecks ,结构与ResNet的是类似的,并且与mobilenet-v2一样在第二个module之后不采用ReLU激活函数。
左边是stride=1的Ghost Bottlenecks,右边是stride=2的Ghost Bottlenecks,目的是为了缩减特征图大小。
GhostNet
Ghost Net结构与MobileNet-V3类似,并且用了SE结构,如下表,其中#exp表示G-bneck的第一个G-Module输出特征图数量
DenseNet
https://towardsdatascience.com/review-densenet-image-classification-b6631a8ef803
https://blog.csdn.net/frighting_ing/article/details/121582735
CSPNet
https://blog.csdn.net/wjinjie/article/details/110952648
MobilNet
从MobileNet看轻量级神经网络的发展
MobileNet V1、V2、V3网络结构