Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现

二 架构对参数量/计算量的影响

在自建架构的时候,除了模型效果之外,我们还需要关注模型整体的计算效率。深度学习模型天生就需要大量数据进行训练,因此每次训练中的参数量和计算量就格外关键,因此在设计卷积网络时,我们希望相似预测效果下,参数量越少越好。为此我们必须理解卷积中的每层会如何影响模型整体的参数量和计算量。

模型参数是需要学习的参数,例如权重 和常数项 ,任何不需要学习、人为输入的超参数都不在“参数量”的计算范围内。对于卷积神经网络中的任意元素(层或函数),有两种方式影响模型的参数量:

1、这个层自带参数,其参数量与该层的超参数的输入有关

2、这个层会影响feature map的尺寸,影响整体像素量和计算量,从而影响全连接层的输入

全连接层、bn通过第一种方式影响参数,而池化、padding、stride等操作则通过第二种方法影响参数,卷积层通过两种方式影响参数,dropout、激活函数等操作不影响参数量。接下来,我们仔细说明一下卷积的参数量问题。

1 卷积层

1.1 参数量计算

一个卷积网络的卷积层究竟包含多少参数量,就是由卷积核的尺寸kernel_size、输入的通道数
in_channels,输出的通道数out_channels(卷积核的数量)共同决定的。其参数量如下: N parameters  = ( K H ∗ K W ∗ C in  ) ∗ C out  + C out  N_{\text {parameters }}=\left(K_{H} * K_{W} * C_{\text {in }}\right) * C_{\text {out }}+C_{\text {out }} Nparameters =(KHKWCin )Cout +Cout 其中,加号前面的部分是权重 2 2 2的数量,加号之后的是偏置 b b b的数量。这个公式是怎么来的呢?

对任意卷积核而言,参数量由卷积核的宽和高决定。在一次扫描中,一个通道上的所有像素是共享权重的(共享卷积核),但不同的通道却使用不同的权重(使用不同的卷积核),因此一次扫描中的权重=(1个卷积核上的参数量 * 需要扫描的通道数),其中需要扫描的通道数也就是被输入卷积层的通道/特征图数量 C i n C_{i n} Cin。同时,若存在偏置项,则每次扫描完毕之后,都要在新生成的特征图上加上1个偏置,因此一次扫描中的全部参数 = (卷积核参数量 * 需要扫描的通道数) +1。在卷积层中,一次扫描会输出一张特征图,需要输出 C o u t C_{out} Cout张特征图,就需要进行 C o u t C_{out} Cout次扫描,因此整个卷积层上的参数量 = 扫描次数 C o u t C_{out} Cout *(扫描一次的参数量 + 1),将 C o u t C_{out} Cout乘到括号中去,就得到了上面的式子。你可能在其他教材或其他地方看见不同的写法或计算逻辑,但是最终计算出的参数量一定是一致的。

来看两个简单的例子:

conv1 = nn.Conv2d(3,6,3) #(3 * 3 * 3)*6 + 6
conv2 = nn.Conv2d(6,4,3) #(3 * 3 * 6)*4 + 4
#检查一下结果
conv1.weight.numel()
#162
conv1.bias.numel()
#6
conv2.weight.numel()
#216
conv2.bias.numel()
#4

相对的,padding以及stride这些参数,不影响卷积层的所需要的参数量:

conv3 = nn.Conv2d(4,16,5,stride=2,padding=1)
# (5*5*4)*16 + 16
conv4 = nn.Conv2d(16,3,5,stride=3,padding=2)
# (5*5*16)*3 + 3
conv3.weight.numel()
#1600
conv3.bias.numel()
#16
conv4.weight.numel()
#1200
conv4.bias.numel()
#3

从卷积层的参数计算公式来看,较大的卷积核、较多的输入和输出都会对参数量影响较大,由于实际中使用的卷积核都很小,所以真正对卷积核参数有影响力的是输出和输入的特征图的数量。在较为复杂的架构中,卷积层的输出数量可能达到256、512、甚至更大的数字,巨大的数字足以让一个卷积层包含的参数达到百万级别。例如VGG16中,比较深的几个卷积层,他们的参数都在百万以上。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第1张图片
通常来说,如果我们希望减小卷积神经网络的参数量,那我们优先会考虑减少的就是输出的特征图数量。但随着网络加深,特征图是越来越小的,学习到更多深入的信息,特征图数量必然会增加(依照惯例,每经过一个池化层,就将特征图数量翻倍)。因此,如果希望消减卷积层的参数量,可以考虑不使用那么多卷积+池化的组合(不要那么深的深度),如果一定要保持深度,则在第一层时就使用较小的特征图数量,例如32

1.2 大尺寸卷积核vs小尺寸卷积核

在深度卷积网络使用的众多场景中,我们都默认使用小卷积核,虽然我们列举了各种各样的理由,但在卷积网络的发展历史上,真正让大家都放弃大卷积核、转而使用小卷积核的是VGG的论文中提出的一个事实:大尺寸卷积核的效果可由多个小尺寸卷积核累积得到。具体如下:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第2张图片
在讲解感受野时我们曾经使用过卷积层的俯视图,假设我们有两层核尺寸为3x3的卷积层,对于第二个卷积层输出的特征图而言,一个神经元映射到原始图像上的感受野尺寸为5x5。同样的图像,假设我们使用一层5x5的卷积层,也可以得到5x5的感受野。同样的,2个3x3卷积层将10x10的特征图缩小为了6x6,一个5x5卷积层也将特征图缩小到了6x6。可以说,在“捕获的信息量”、“压缩尺寸”这两个层次上,两个3x3的卷积层和一个5x5的卷积层获得了一样的结果。同理,我们也可以用三层3x3卷积核的卷积层替代一层7x7的卷积核,更大的卷积核亦然

对比一下,一个5x5卷积层在一次扫描中需要的参数是25个,2个3x3卷积层却只需要9 + 9 = 18个,因此两个3x3卷积层所需要的参数更少。当特征图数量巨大时,这一点点参数量的差异会被放大:假设输入的特征图数量为64,conv1和conv2输出的特征图也是64个,则有如下参数量:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第3张图片
两个3x3的卷积层总共需要7万+参数,而一个5x5的卷积层却需要10万+参数。对于VGG16这种重复架构的网络而言,如果将所有的3x3卷积核都替换成5x5卷积核,那整体参数量将增加3个亿。可见,3x3的两个卷积层不仅加深了深度,一定程度上让提取出的特征信息更“抽象”、更“复杂”,同时也让参数量大幅减少。这又给了我们一个坚定使用小卷积核的理由。

1.3 1x1卷积核

在众多的小卷积核中,小到极致的就是1x1尺寸的卷积核。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第4张图片
1x1的卷积核上只有一个权重,每次进行卷积操作时,该权重会与原始图像中每个像素相乘,并得到特征图上的新像素,因此1x1卷积也被叫做“逐点卷积”(Pointwise Convolution)。这种计算方式和矩阵 * 常数一致,同时,其本质也非常像我们在CV第一堂课时直接给像素直接乘上一个值来改变图像的某些属性的做法:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第5张图片
当卷积核尺寸设置为1x1之后,实际的卷积操作流程并没有发生改变。在一次扫描中,一个通道上的所有像素是共享1x1卷积核中这唯一一个权重,不同的通道使用不同的权重,因此一次扫描中的权重 =(1 * 需要扫描的通道数),若存在偏置项,则每次扫描完毕之后,都要在新生成的特征图上加上1个偏置,因此一次扫描中的全部参数 = (1 * 需要扫描的通道数) +1。在卷积层中,一次扫描会输出一张特征图,需要输出 C out  C_{\text {out }} Cout 张特征图,就需要进行 C out  C_{\text {out }} Cout 次扫描,因此整个卷积层上的参数量 = 扫描次数 C out  C_{\text {out }} Cout *(扫描一次的参数量 + 1),因此1x1卷积核下的参数量为: N parameters  = C in  ∗ C out  + C out  N_{\text {parameters }}=C_{\text {in }} * C_{\text {out }}+C_{\text {out }} Nparameters =Cin Cout +Cout 比起普通卷积核,1x1卷积核的参数量是原来的 1 k 2 \frac{1}{k^{2}} k21,其中 k k k是卷积核尺寸。但由于只有一个像素大小,所以1x1的卷积核不像普通卷积核一样可以捕捉到特征图/原始图像上“一小块的信息”,即它无法识别高和宽维度上相邻元素之间构成的模式。然而,由于1x1卷积核可以在不增加也不减少信息的情况下维持特征图的尺寸(相对的,普通的卷积层必须在增加padding的情况下才能够维持特征图的尺寸),因此1x1卷积核可以完整地将缩小特征图时会损失掉的“位置信息”传输到下一层网络中。这是它在提取特征这个过程中的小优势

在实际中,1x1卷积的重要作用之一就是加深CNN的深度。1x1卷积不会改变特征图的尺寸,因此可以被用于加深CNN的深度,让卷积网络获得更好的特征表达。这个性质被论文《Network in Network》所使用,并在架构NiN中发挥了重要的作用。NiN是AlexNet诞生不久之后被提出的架构,虽然也是2014年的论文,但早于VGG之前诞生,其架构如下:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第6张图片
在NiN的架构中,存在着一种特殊的层:MLP layer。虽然在NiN的论文中,MLP layer是被看成是一个独立的单元来说明,但从其结构、操作和输出的特征图来看,MLP layer毫无疑问就是1x1的卷积层。NiN是以每个3x3卷积层后紧跟2个1x1卷积层组成一个block,并重复3个block达成9层卷积层架构的网络。之后我们会简单复现NiN的架构。

1x1卷积层不会改变特征图的尺寸,这个性质虽然有用,但和使用padding的卷积层差异不是特别大。从今天的眼光来看,1x1卷积核在加深深度方面最关键的作用还是用在卷积层之间,用于调整输出的通道数,协助大幅度降低计算量和参数量,从而协助加深网络深度,这一作用又被称为“跨通道信息交互”

在VGG架构中,我们串联不缩小特征图大小的数个卷积层,每层输出128、256或512个特征图。当输出256个特征图的卷积层串联时,输入和输出的特征图数目很多,会使得整个卷积层的参数量变得很巨大。为此,我们可以如下图右侧所示的架构,在两个含有256个特征图的输出之间使用(1x1, 3x3,1x1)的三个卷积层来代替原始的3x3卷积层。在右侧架构中,虽然两个含有256特征图并没有直接交互,但他们之间的信息通过1x1卷积层进行了交换,这也是这个架构的作用被称为“跨通道信息交互”的原因。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第7张图片
这种在核尺寸为1x1的2个卷积层之间包装其他卷积层的架构被称为瓶颈设计(bottleneck design),也可简称叫做瓶颈或bottleneck,它被广泛使用在各种深层网络当中,代表了CNN目前为止最高水平架构之一的残差网络ResNet的论文中也使用了瓶颈架构。从直觉上来说,通道数目缩小意味着提取的信息量会变少,但瓶颈设计基本只会出现在超过100层的深度网络中,实践经验证明这样的架构在深度网络中几乎不会造成信息损失,但带来的参数量的骤减却是肯定的,因此瓶颈设计在现实中应用非常广泛。以上图的3x3卷积层的瓶颈设计为例,具体参数量如下所示:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第8张图片
可以看到,虽然最后都输出了256个相同尺寸的特征图,并且所有信息都经过了3x3的卷积核的扫描,但瓶颈架构所需要的参数量只有2.6万个,一个3x3卷积层所需要的参数却有59万个。对于百层以上的深层神经网络来说,这个参数差异足以让人放弃一些性能,也要坚持使用瓶颈设计。

1.4 减少参数量:分组卷积与深度分离卷积

除了1x1卷积之外,分组卷积(Grouped Convolution)也是一种高效的减少参数量的形式。要理解分组卷积,我们最好先理解下面这张图像(注意,在此图中,卷积图中的“格子”不代表具体像素数):Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第9张图片
如图所示,图像左侧是全连接层,右侧是卷积层。对于普通的全连接层,连接是存在于神经元之间,上一层所有的神经元都与下一层所有的神经元相连接。在普通全连接层上,每个神经元都携带一个特征,一个连接上存在一个权重w,每个特征值都与权重值相乘并进入下一层,而下一层的神经元会将上层收到的全部信息进行加和,所以下一层神经元上的值是 z = w 1 x 1 + w 2 x 2 … … w n x n z=w_{1} x_{1}+w_{2} x_{2} \ldots \ldots w_{n} x_{n} z=w1x1+w2x2wnxn

带着这个流程,我们来回顾一下在卷积层中,输入的数个特征图是怎样被处理的:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第10张图片
Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第11张图片
我们使用 X X X表示输入的每张特征图,对每张特征图,尺寸为k*k的卷积核 w k w_{k} wk会分别进行扫描,生成与原通道数量相同的单独的图像(使用数学符号 f f f表示)。之后,全部的特征图 f f f会被加和并加上偏置 B B B,生成输出卷积层的一张特征图(用数学符号 F F F表示)。如果使用数学符号,就可以表示为: F = w k , r ∗ X r + w k , g ∗ X g + w k , b ∗ X b + B = f r + f g + f b + B \begin{aligned} &F=w_{k, r} * X_{r}+w_{k, g} * X_{g}+w_{k, b} * X_{b}+B\\ &=f_{r}+f_{g}+f_{b}+B \end{aligned} F=wk,rXr+wk,gXg+wk,bXb+B=fr+fg+fb+B这个公式看着是不是和 z = w X + b z=\boldsymbol{w} \boldsymbol{X}+b z=wX+b很像呢?只不过这里 w w w X X X之间的星号不是相乘,而是卷积操作。实际上卷积层在每张特征图上进行扫描,生成单独的特征图之后,也需要一个加和过程才能够生成下一层的输入,这一点和全连接层的“加和”过程是一模一样的,因此,我们可以将卷积层也表达为“特征图与特征图相连”的形式。并且在这张图像上,每个“连接”上都是一个k * k尺寸的卷积核。依据图像再回顾卷积层的参数计算公式 N parameters  = ( K H ∗ K w ∗ C in  ) ∗ C out  + C out  N_{\text {parameters }}=\left(K_{H} * K_{w} * C_{\text {in }}\right) * C_{\text {out }}+C_{\text {out }} Nparameters =(KHKwCin )Cout +Cout ,想必非常容易了(实际上,1x1卷积核就是所有连接上都只有一个w的卷积方式)。

结合这张图与参数计算公式,很容易想到三种消减参数量的办法:消减输入特征图数量,消减输出特征图数量,消减每个连接上的核的尺寸,或者消减输入特征图与输出特征图之间的连接数量。分组卷积就是通过给输入特征图及输出特征图分组来消减连接数量的卷积方式Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第12张图片
我们来看分组卷积的具体操作:首先,分组卷积需要一个超参数groups,当groups=g时,则表示“将特征图分成g组”。我们可以让groups为任何正整数,但惯例来说,特征图数目一般都是偶数,因此分组数一般也是偶数。当确定g的数量之后,我们将输入的特征图和输出的特征图都分成g组。如上图,输入特征图数量为4,输出特征图数量为8,g=2,于是我们便将输入特征图和输出特征图分别分为2组。分组之后,一组输入特征图负责生成一组输出特征图。如图所示,上方两个输入特征图只负责上方4个输出特征图,在不考虑偏置的情况下,这个操作需要的参数为 3 * 3 * 2 * 4 = 72。同理,下方两个输入特征图只负责下方的4个输出特征图,因此需要的参数也为72个。分贝生成4个特征图后,再堆叠在一起,形成总共8个输出特征图。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第13张图片
对于分组卷积而言,若用数学公式来表示,则有:  group  1 = ( K H ∗ K w ∗ C in  g ) ∗ C out  g  group  2 = ( K H ∗ K w ∗ C in  g ) ∗ C out  g  total  =  group  1 + g r o u p 2 = ( K H ∗ K w ∗ C in  g ) ∗ C out  g ∗ g = 1 g ( K H ∗ K w ∗ C in  ∗ C out  ) \begin{aligned} \text { group } 1 &=\left(K_{H} * K_{w} * \frac{C_{\text {in }}}{g}\right) * \frac{C_{\text {out }}}{g} \\ \text { group } 2 &=\left(K_{H} * K_{w} * \frac{C_{\text {in }}}{g}\right) * \frac{C_{\text {out }}}{g} \\ \text { total } &=\text { group } 1+g r o u p 2 \\ &=\left(K_{H} * K_{w} * \frac{C_{\text {in }}}{g}\right) * \frac{C_{\text {out }}}{g} * g \\ &=\frac{1}{g}\left(K_{H} * K_{w} * C_{\text {in }} * C_{\text {out }}\right) \end{aligned}  group 1 group 2 total =(KHKwgCin )gCout =(KHKwgCin )gCout = group 1+group2=(KHKwgCin )gCout g=g1(KHKwCin Cout )若考虑偏置,则有:  group  1 = ( K H ∗ K w ∗ C i n g ) ∗ C out  g + C out  g  group  2 = ( K H ∗ K w ∗ C i n g ) ∗ C out  g + C o u t g  total  =  group  1 + g r o u p 2 = ( ( K H ∗ K w ∗ C i n g ) ∗ C out  g + C o u t g ) ∗ g = 1 g ( K H ∗ K w ∗ C i n ∗ C o u t ) + C o u t \begin{aligned} \text { group } 1 &=\left(K_{H} * K_{w} * \frac{C_{i n}}{g}\right) * \frac{C_{\text {out }}}{g}+\frac{C_{\text {out }}}{g} \\ \text { group } 2 &=\left(K_{H} * K_{w} * \frac{C_{i n}}{g}\right) * \frac{C_{\text {out }}}{g}+\frac{C_{o u t}}{g} \\ \text { total } &=\text { group } 1+g r o u p 2 \\ &=\left(\left(K_{H} * K_{w} * \frac{C_{i n}}{g}\right) * \frac{C_{\text {out }}}{g}+\frac{C_{o u t}}{g}\right) * g \\ &=\frac{1}{g}\left(K_{H} * K_{w} * C_{i n} * C_{o u t}\right)+C_{o u t} \end{aligned}  group 1 group 2 total =(KHKwgCin)gCout +gCout =(KHKwgCin)gCout +gCout= group 1+group2=((KHKwgCin)gCout +gCout)g=g1(KHKwCinCout)+Cout不难发现,分组的存在不影响偏置,偏置只与输出的特征图数量有关。这个公式可以在pytorch中被轻松验证。

conv1 = nn.Conv2d(4,8,3) #(3 * 3 * 4)*8 + 8 = 296
conv1_ = nn.Conv2d(4,8,3,groups=2) # ((3 * 3 * 4)*8)/2 + 8 = 152
#检查一下结果
conv1.weight.numel()
#288
conv1.bias.numel()
#8
conv1_.weight.numel()
#144
conv1_.bias.numel()
#8
#如果输入了奇数group呢?
conv2 = nn.Conv2d(4,8,3,groups=3)

直接报错,无法运行。
在这里插入图片描述
可以看到,分组卷积可以有效减少参数量。虽然在之前的课程中没有提到过,但实际上AlexNet所使用的架构中包含groups=2的分组卷积,因此在AlexNet的论文中架构看起来是这样的:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第14张图片
groups参数最大可以取到和 C i n C_{in} Cin C o u t C_{out} Cout中较小的那个值一样大。通常来说在应用分组卷积时,应当是输入的特征图尺寸更小,我们称groups = C i n C_{in} Cin的分组卷积叫做“深度卷积(Depthwise Convolution)。深度卷积的参数量为:  parameters  = 1 g ( K H ∗ K w ∗ C in  ∗ C out  ) + C out  \text { parameters }=\frac{1}{g}\left(K_{H} * K_{w} * C_{\text {in }} * C_{\text {out }}\right)+C_{\text {out }}  parameters =g1(KHKwCin Cout )+Cout 其中 g = c i n g=c_{i n} g=cin, 则有:
 parameters  = K H ∗ K w ∗ C out  + C out  \text { parameters }=K_{H} * K_{w} * C_{\text {out }}+C_{\text {out }}  parameters =KHKwCout +Cout 比起普通卷积,参数量是原来的 1 C i n \frac{1}{C_{i n}} Cin1倍,当特征图数量巨大时,分组卷积可以节省非常多的参数。当表示在图像上,深度卷积所展示的链接方式为:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第15张图片
我们还可以将深度卷积与1x1卷积核结合使用。对输入特征图,我们首先进行深度卷积,产出一组特征图,然后再这组特征图的基础上执行1x1卷积,对特征图进行线性变换。两种卷积打包在一起成为一个block,这个block就叫做“深度可分离卷积”(Depthwise separable convolution),也被称为“分离卷积”(separable convolution)。对于深度可分离卷积的一个block,若不考虑偏置,则整个block的参数量为:
 parameters  = K H ∗ K w ∗ C out  depth  + C in  pair  ∗ C out  pair  \text { parameters }=K_{H} * K_{w} * C_{\text {out }}^{\text {depth }}+C_{\text {in }}^{\text {pair }} * C_{\text {out }}^{\text {pair }}  parameters =KHKwCout depth +Cin pair Cout pair 若原始卷积也不考虑偏置,则深度可分离卷积的参数比上原始卷积的参数的比例为:  ratio  = K H ∗ K w ∗ C out  depth  + C in  pair  ∗ C out  pair  ( K H ∗ K w ∗ C in  depth  ) ∗ C out  pair  = C out  depth  C i n depth  ∗ C out  pair  + C in  pair  K H ∗ K w ∗ C in  depth  \begin{aligned} &\text { ratio }=\frac{K_{H} * K_{w} * C_{\text {out }}^{\text {depth }}+C_{\text {in }}^{\text {pair }} * C_{\text {out }}^{\text {pair }}}{\left(K_{H} * K_{w} * C_{\text {in }}^{\text {depth }}\right) * C_{\text {out }}^{\text {pair }}}\\ &=\frac{C_{\text {out }}^{\text {depth }}}{C_{i n}^{\text {depth }} * C_{\text {out }}^{\text {pair }}}+\frac{C_{\text {in }}^{\text {pair }}}{K_{H} * K_{w} * C_{\text {in }}^{\text {depth }}} \end{aligned}  ratio =(KHKwCin depth )Cout pair KHKwCout depth +Cin pair Cout pair =Cindepth Cout pair Cout depth +KHKwCin depth Cin pair 假设 1 ∗ 1 1 * 1 11 卷积层不改变特征图数量, 则有 C i n pair  = C o u t pair  = C out  depth  C_{i n}^{\text {pair }}=C_{o u t}^{\text {pair }}=C_{\text {out }}^{\text {depth }} Cinpair =Coutpair =Cout depth , 则有 :
= 1 C i n depth  + C o u t pair  K H ∗ K w ∗ C i n depth  =\frac{1}{C_{i n}^{\text {depth }}}+\frac{C_{o u t}^{\text {pair }}}{K_{H} * K_{w} * C_{i n}^{\text {depth }}} =Cindepth 1+KHKwCindepth Coutpair 当输入特征图与输出特征图数目相等的时候,分子与分母上的 C i n C_{in} Cin就可以约掉了。注意,此处的 K K K是深度卷积中的核尺寸,与1x1卷积无关。我们也可以在代码中来验证这个式子:

#与图上不同,在代码中我们令输出的特征图数量与输入的特征图数量不相等,用以区别。输出特征图数量=8。
conv1 = nn.Conv2d(4,8,3, bias=False) #(3 * 3 * 4) * 8 = 288
conv_depthwise = nn.Conv2d(4,8,3,groups=4,bias=False) #1/4 * (3 * 3 * 4)*8 = 72
conv_pairwise = nn.Conv2d(8,8,1,bias=False) # 64
(conv_depthwise.weight.numel()+conv_pairwise.weight.numel())/(conv1.weight.numel
())
1/4 + 8/(4*3*3)

深度可分离卷积在2017年的论文《Xception: Deep Learning with Depthwise Separable
Convolutions》中被提出,现在是谷歌的深度学习模型GoogLeNet进化版中非常关键的block。论文中提出,分组卷积核深度可分离卷积不仅可以帮助卷积层减少参数量,更可以削弱特征图与特征图之间的联系来控制过拟合。更多详细内容可以参考课程附件中的论文。之后我们还会用到深度可分离卷积的相关内容。

在卷积层上,我们还可以进行更多更丰富的操作来减少参数量并提升模型的效果,之后若有机会我们会就其他卷积相关操作继续展开来讨论。

2 全连接层

卷积层上减少参数的操作非常丰富,但从经典架构来看,真正对CNN参数量“贡献”巨大的是全连接层。数据在进入全连接层时,需要将所有像素拉平,而全连接层中的一个像素点就对应着一个参数,因此全连接层所携带大量参数。为什么卷积网络里需要有全连接层呢?全连接层的作用主要有以下两个:

1、作为分类器,实现对数据的分类。在卷积网络中,卷积层和池化层的作用是提取特征,但提取出来的一张张特征图与我们希望要的对应类别的输出还相差很远,为了将信息转化为输出,我们需要在卷积和池化层的后面加上能够实现分类的结构,而全连接层是一切能够实现分类的结构中,较为简单、较为熟悉、同时成本也相对低的存在。本质上来说,卷积层提供了一系列有意义且稳定的特征值,构成了一个与输入图像相比维数更少的特征空间,而全连接层负责学习这个空间上的(可能是非线性的)函数关系,并输出预测结果。(其他可能的选择是,在卷积层后面放置一个SVM,或者放置其他机器学习算法作为分类器。)

2、作为整合信息的工具,将特征图中的信息进行整合。基于卷积层的输出来进行分类是一件困难的事情。由于卷积层输出的结果是特征图,因此我们有以下两种方式来进行分类:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第16张图片
由于我们在计算设置上的各种努力,卷积网络生成的特征图每张都由不同的卷积核扫描生成,因此每张都携带不同的信息,让不同特征图对应不同类别,很可能会损失掉一些本能够将样本判断正确的信息。而同时,又由于我们在参数设置上的各种努力(例如,使用奇数卷积核来保证图像不会失真太多),卷积层生成的特征图是自带位置信息的:任意像素映射到自己的特征图上的位置,与该像素的感受野映射到原图上的位置几乎是一致的,因此,若使用“特征图的不同区域”的信息来进行类别划分,可能会造成“只有某个区域的数据参与了某个标签的预测”的情况。在进行预测之前,将所有可能的信息充分混合、进行学习,对预测效果有重大的意义。全连接层能够确保所有信息得到恰当的“混合”,以保证预测的效果。

基于上面的两个理由,再加上约定俗成,我们一般都会在形似AlexNet或VGG的网络中包含全连接层。但通常来说,一旦有可以替换全连接层、并不影响模型效果的手段,大家就会尝试将全连接层替代掉,因为全连接层所带来的参数量对算力有很高的要求,并且,全连接层的存在让CNN整体变得更容易过拟合。2012年时,Hinton团队提出了dropout来控制全连接层的过拟合,后来又有了batch normalization方法,现在的CNN架构已经不太容易过拟合了。但是全连接层令人头疼的参数量问题依然没有解决

在讨论各种替换全连接层的可能性之前,我们先来看使用全连接层时性价比较高的参数组合:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第17张图片
更多层,还是更多神经元?

对于CNN中的全连接层来说,在一个层上增加足够多的神经元,会比增加层效果更好。一般来说,CNN中的全连接层最多只有3-4层(包括输出层),过于多的层会增加计算的负担,还会将模型带入过拟合的深渊。对于小型网络,3层全连接层已是极限了。需要注意的是,在卷积层和全连接层的连接中,通常全连接的输出神经元个数不会少于输入的通道数。对于全连接层之间的连接,只要不是输出层,也很少出现输出神经元少于输入神经元的情况。对全连接层而言,更大的参数代表了更高的复杂度、更强的学习能力、更大的过拟合可能,因此对于小型网络来说,除非你的数据量庞大或数据异常复杂,尽量不使用1024以上的参数。

2.1 从卷积到全连接层

决定全连接层参数数量的有两个因素:最后一个卷积层上的特征图所含的像素量,以及我们在全连接层之间设定的输出神经元个数。在自建网络时,全连接层的参数输入一直是无数卷积网络新手的盲点。在互联网资料齐全、代码基本靠复制粘贴、架构基本照着架构图写的今天,许多深度学习的学习者甚至意识不到这个问题的存在(也有一部分理由是,tensorflow不需要输入in_channels和in_features,因此许多深度学习学者可能没有考虑过这个问题)。以下图为例,假设输入数据的结构为(10,3,229,229),没有架构图,请问两个箭头处应该分别填写什么数字呢?Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第18张图片
代码留给大家,大家可以自己先试试看,如果你能够顺利让数据通过Model、不报错的话,则说明你的参数设置正确了。

data = torch.ones(size=(10,3,229,229))
class Model(nn.Module):
    def __init__(self):
        super().__init__()
        
        #block1
        self.conv1 = nn.Conv2d(3,6,3)
        self.conv2 = nn.Conv2d(6,4,3)
        self.pool1 = nn.MaxPool2d(2)
        
        #block2
        self.conv3 = nn.Conv2d(4,16,5,stride=2,padding=1)
        self.conv4 = nn.Conv2d(16,3,5,stride=3,padding=2)
        self.pool2 = nn.MaxPool2d(2)
        
        #FC层
        self.linear1 = nn.Linear(   ,256)
        self.linear2 = nn.Linear(256,256)
        self.linear3 = nn.Linear(256,10)
        
    def forward(self,x):
        x = F.relu(self.conv1(x))
        x = self.pool1(F.relu(self.conv2(x)))
        x = F.relu(self.conv3(x))
        x = self.pool2(F.relu(self.conv4(x)))
        x = x.view(-1,   )
        
        x = F.relu(self.linear1(F.dropout(x,p=0.5)))
        x = F.relu(self.linear2(F.dropout(x,p=0.5)))
        output = F.softmax(self.linear3(x),dim=1)
        
        return output

首先你应当认识到,两个箭头处需要填写的数字是一样的,而这个数字就是最后一个卷积/池化层输出的特征图上所有的像素。通常在我们的架构中,这个输入会被写作如(64 * 7* 7)这样的形式,其中64就是最后一个卷积/池化层上输出的特征图的数量,而7*7就是我们的特征图尺寸。在一个架构中,输出的特征图数量是我们自己规定的,用上面的架构来看,就是128,因此唯一的问题就是特征图尺寸是多少。

在之前的课程中,我们使用torchinfo包中的summary来自动计算特征图尺寸,不难发现,要使用summary函数,前提是已经建好了能够顺利运行的model,但尴尬的是,当我们不知道架构中红色箭头处应该填写什么数字时,model是不可能跑通的。那怎么在模型架构不完整的情况下,找出最后一个池化层/卷积层上输出的特征图的尺寸呢?一种简单的方法是,将Model中所有的线性层都注释掉,只留下卷积层,然后将model输入summary进行计算,但有更简单的方法,使用另一种构筑神经网路架构的方式:nn.Sequential。

nn.Sequential是一种非常简单的构筑神经网络的方式,它可以将“以序列方式从前往后运行的层”打包起来,组合成类似于机器学习中的管道(Pipeline)的结构,以此避开建立类、继承类等稍微有些抽象的python代码。大多数深度学习课程和教材在最开始的时候就会介绍它,并且一直以它作为例子运行各类神经网络,我们来看具体的例子:

data = torch.ones(size=(10,3,229,229))
#不使用类,直接将需要串联的网络、函数等信息写在一个“序列”里 #重现上面的4个卷积层、2个池化层的架构
net = nn.Sequential(nn.Conv2d(3,6,3)
                   ,nn.ReLU(inplace=True)
                   ,nn.Conv2d(6,4,3)
                   ,nn.ReLU(inplace=True)
                   ,nn.MaxPool2d(2)
                   ,nn.Conv2d(4,16,5,stride=2,padding=1)
                   ,nn.ReLU(inplace=True)
                   ,nn.Conv2d(16,3,5,stride=3,padding=2)
                   ,nn.ReLU(inplace=True)
                   ,nn.MaxPool2d(2)
                   )
#nn.Sequential组成的序列不是类,因此不需要实例化,可以直接输入数据
net(data).shape
#torch.Size([10, 3, 9, 9])

看见最终的特征图的结构了吗? 3 ∗ 9 ∗ 9 3 * 9 * 9 399就是我们需要输入到红色箭头处的数字。同样的,我们也可以将nn.Sequential放入torch_receptive_field查看感受野的尺寸:

from torch_receptive_field import receptive_field
#net不是类所以不需要实例化
rfdict = receptive_field(net,(3,229,229))
#------------------------------------------------------------------------------
#        Layer (type)    map size      start       jump receptive_field 
#==============================================================================
#        0             [229, 229]        0.5        1.0             1.0 
#        1             [227, 227]        1.5        1.0             3.0 
#        2             [227, 227]        1.5        1.0             3.0 
#        3             [225, 225]        2.5        1.0             5.0 
#        4             [225, 225]        2.5        1.0             5.0 
#        5             [112, 112]        3.0        2.0             6.0 
#        6               [55, 55]        5.0        4.0            14.0 
#        7               [55, 55]        5.0        4.0            14.0 
#        8               [19, 19]        5.0       12.0            30.0 
#        9               [19, 19]        5.0       12.0            30.0 
#        10                [9, 9]       11.0       24.0            42.0 
#==============================================================================

使用nn.Sequential结构的好处多多,最明显的就是代码量的减少,至少在nn.Sequential中,我们可以不用再写一堆self.,而只需要按顺序列举数据会通过的类就可以了。由于nn.Sequential表示的是各个元素之间的串联计算过程,因此我们需要将架构写成输入数据的“运行管道”,让数据能够从上向下进行计算,因此我们实际上是将网络架构(各类层)和计算过程(激活函数、数据处理方式BN等)混写,这样做代码量会减少很多。当然,混写既是优点,也是缺点——混写之后,卷积层架构将不再像左侧一样清晰明显,比较不容易看出分割的blocks,因此在深度学习的入门阶段,需要熟悉网络架构的时候,我们并没有让大家采用nn.Sequential的形式来构建网络。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第19张图片
在较为复杂的网络架构中,我们通常利用nn.Sequential来区分网络的不同部分:例如,在普通CNN中,卷积层、池化层负责的是特征提取,全连接层负责的是整合信息、进行预测,因此我们可以使用nn.Sequential来区别这两部分架构Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第20张图片
以VGG16为例,使用nn.Sequential的架构如下:

class VGG16(nn.Module):
    def __init__(self):
        super().__init__()
        self.features_ = 
nn.Sequential(nn.Conv2d(3,64,3,padding=1),nn.ReLU(inplace=True)
                                       
,nn.Conv2d(64,64,3,padding=1),nn.ReLU(inplace=True)
                                       ,nn.MaxPool2d(2)
                                       
                                       
,nn.Conv2d(64,128,3,padding=1),nn.ReLU(inplace=True)
                                       
,nn.Conv2d(128,128,3,padding=1),nn.ReLU(inplace=True)
                                       ,nn.MaxPool2d(2)
                                       
                                       
,nn.Conv2d(128,256,3,padding=1),nn.ReLU(inplace=True)
                                       
,nn.Conv2d(256,256,3,padding=1),nn.ReLU(inplace=True)
                                       
,nn.Conv2d(256,256,3,padding=1),nn.ReLU(inplace=True)
                                       ,nn.MaxPool2d(2)
                                       
                                       
,nn.Conv2d(256,512,3,padding=1),nn.ReLU(inplace=True)
                                       
,nn.Conv2d(512,512,3,padding=1),nn.ReLU(inplace=True)
                                       
,nn.Conv2d(512,512,3,padding=1),nn.ReLU(inplace=True)
                                       ,nn.MaxPool2d(2)
                                       
                                       
,nn.Conv2d(512,512,3,padding=1),nn.ReLU(inplace=True)
                                       
,nn.Conv2d(512,512,3,padding=1),nn.ReLU(inplace=True)

,nn.Conv2d(512,512,3,padding=1),nn.ReLU(inplace=True)
                                       ,nn.MaxPool2d(2)
                                     )
        self.clf_ = nn.Sequential(nn.Dropout(0.5)
                                 ,nn.Linear(512*7*7,4096),nn.ReLU(inplace=True)
                                 ,nn.Dropout(0.5)
                                 ,nn.Linear(4096,4096),nn.ReLU(inplace=True)
                                 ,nn.Linear(4096,1000),nn.Softmax(dim=1)
                                 )              
        
    def forward(self,x):
        x = self.features_(x)
        x = x.view(-1, 512*7*7)
        output = self.clf_(x)
        return output
vgg = VGG16()
summary(vgg, input_size=(10, 3, 224, 224),device="cpu")
# ==========================================================================================
# Layer (type:depth-idx)                   Output Shape              Param #
# ==========================================================================================
# VGG16                                    --                        --
# ├─Sequential: 1-1                        [10, 512, 7, 7]           --
# │    └─Conv2d: 2-1                       [10, 64, 224, 224]        1,792
# │    └─ReLU: 2-2                         [10, 64, 224, 224]        --
# │    └─Conv2d: 2-3                       [10, 64, 224, 224]        36,928
# │    └─ReLU: 2-4                         [10, 64, 224, 224]        --
# │    └─MaxPool2d: 2-5                    [10, 64, 112, 112]        --
# │    └─Conv2d: 2-6                       [10, 128, 112, 112]       73,856
# │    └─ReLU: 2-7                         [10, 128, 112, 112]       --
# │    └─Conv2d: 2-8                       [10, 128, 112, 112]       147,584
# │    └─ReLU: 2-9                         [10, 128, 112, 112]       --
# │    └─MaxPool2d: 2-10                   [10, 128, 56, 56]         --
# │    └─Conv2d: 2-11                      [10, 256, 56, 56]         295,168
# │    └─ReLU: 2-12                        [10, 256, 56, 56]         --
# │    └─Conv2d: 2-13                      [10, 256, 56, 56]         590,080
# │    └─ReLU: 2-14                        [10, 256, 56, 56]         --
# │    └─Conv2d: 2-15                      [10, 256, 56, 56]         590,080
# │    └─ReLU: 2-16                        [10, 256, 56, 56]         --
# │    └─MaxPool2d: 2-17                   [10, 256, 28, 28]         --
# │    └─Conv2d: 2-18                      [10, 512, 28, 28]         1,180,160
# │    └─ReLU: 2-19                        [10, 512, 28, 28]         --
# │    └─Conv2d: 2-20                      [10, 512, 28, 28]         2,359,808
# │    └─ReLU: 2-21                        [10, 512, 28, 28]         --
# │    └─Conv2d: 2-22                      [10, 512, 28, 28]         2,359,808
# │    └─ReLU: 2-23                        [10, 512, 28, 28]         --
# │    └─MaxPool2d: 2-24                   [10, 512, 14, 14]         --
# │    └─Conv2d: 2-25                      [10, 512, 14, 14]         2,359,808
# │    └─ReLU: 2-26                        [10, 512, 14, 14]         --
# │    └─Conv2d: 2-27                      [10, 512, 14, 14]         2,359,808
# │    └─ReLU: 2-28                        [10, 512, 14, 14]         --
# │    └─Conv2d: 2-29                      [10, 512, 14, 14]         2,359,808
# │    └─ReLU: 2-30                        [10, 512, 14, 14]         --
# │    └─MaxPool2d: 2-31                   [10, 512, 7, 7]           --
# ├─Sequential: 1-2                        [10, 1000]                --
# │    └─Dropout: 2-32                     [10, 25088]               --
# │    └─Linear: 2-33                      [10, 4096]                102,764,544
# │    └─ReLU: 2-34                        [10, 4096]                --
# │    └─Dropout: 2-35                     [10, 4096]                --
# │    └─Linear: 2-36                      [10, 4096]                16,781,312
# │    └─ReLU: 2-37                        [10, 4096]                --
# │    └─Linear: 2-38                      [10, 1000]                4,097,000
# │    └─Softmax: 2-39                     [10, 1000]                --
# ==========================================================================================
# Total params: 138,357,544
# Trainable params: 138,357,544
# Non-trainable params: 0
# Total mult-adds (G): 154.84
# ==========================================================================================
# Input size (MB): 6.02
# Forward/backward pass size (MB): 1084.54
# Params size (MB): 553.43
# Estimated Total Size (MB): 1643.99
# ==========================================================================================

可以看到,forward函数变得异常简单,整体代码量也缩小了。在构筑架构时,我们可以将代码稍作整理,让其更接近我们看到的网络架构,但是summary函数并不会对架构和层做这么多的区分,因此summary中的架构看上去就更长更深了。在实际构筑自己的神经网络时,我们常常会使用nn.Sequential来调试卷积层架构,并不断查看感受野的变化。

2.2 代替全连接层:1x1卷积核与全局平均池化(GAP)

虽然全连接层很有用,但它的参数量带来的计算成本的确是一个很大的问题。因此,研究者们曾经尝试找出各种方法,用来替代全连接层。其中流传比较广泛的方法之一,就是使用1x1卷积核来进行替代全连接层。虽然大部分持有此观点的材料的描述都模糊不清、甚至有胡言乱语之嫌,但是人们还是对1x1卷积核替代全连接层的效果深信不疑。那究竟可不可以呢?可以,但这么做的价值其实微乎其微,除了特殊的应用场景之外,既没有经典架构、也没有实际应用会这么做。我们来看看是怎么回事。

为什么人们说1x1卷积核可以替代全连接层呢?还记得之前绘制过的普通全连接层与卷积层在“链接”数量上的对比图吗?Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第21张图片
对于普通卷积层而言,每个连接上的w就是完整的卷积核,一般至少带有9个参数。当使用1x1卷积核时,每个w中就只有一个参数,这就让1x1卷积层和普通全连接层更加相似,只不过普通全连接层的连接是在神经元与神经元之间,而1x1卷积层的连接是在特征图与特征图之间。因此从数学公式来看,全连接层和1x1的卷积层之间是可以互相转换的。对于卷积层来说,只要让特征图的尺寸为1x1,再让卷积核的尺寸也为1x1,就可以实现和普通全连接层一模一样的计算了

在计算机视觉中,不包含全连接层,只有卷积层和池化层的卷积网络被叫做全卷积网络(fullyconvolutional network,FCN)。在无数减少全连接层的努力中,1x1卷积核可以在架构上完全替代掉全连接层,来看下面的例子:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第22张图片
这是一个4分类的例子。在卷积和池化层之后,我们得到的特征图是(5x5)共16张,通过三个线性层(包括输出层)、或1个5x5卷积层+3个1x1卷积层,都可以将最终输出结果转化为我们需要的4个类别。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第23张图片
在1x1卷积层替代全连接层的例子中,输出的特征图的个数必须和全连接层上的神经元个数一致,这样才能使用输出的特征图“替代”掉全连接层,但在这样的要求下,不难发现,卷积层所需要的参数量是更大的。因此,使用1x1卷积层代替全连接层不能减少参数量。同时,没有证据能表明将全连接层更换成1x1卷积层之后能够提升模型的拟合效果,所谓“跨通道信息交互”等等的效果,在之前的课程中已经说明,和参数量以及是否替换全连接层都无关。因此,虽然1x1卷积核可以替换全连接层,但这么做的价值其实非常小

如果要说1x1卷积核替换全连接层之后带来的最大的好处,那就是解放了输入层对图像尺寸的限制。在之前的学习中,我们已经知道卷积层和全连接层的连接处需要进行数据的“拉平”处理,并且需要人为手动输入全连接层的神经元数量,一旦无法正确计算卷积层输出的特征图尺寸,网络架构就会报错,无法运行。因为这个特点,输入卷积网络的图片的尺寸总是被严格规定的,一旦改变输入尺寸,网络架构就不能再使用了。而当整个架构中都只有卷积层的时候,无论如何调整输入图像的尺寸,网络都可以运行。比如上面的架构,输入尺寸是(3, 14, 14),最后输出的结果是4和(4, 1, 1)。现在我们将输入尺寸修改为(3,16,16),在普通CNN的架构中,数据就会因为无法通过全连接层而报错,但在FCN里就可以顺畅运行下去,最终输出(4, 2, 2)的结果。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第24张图片
无论多大的图像都能输出结果,这个性质在物体检测的实例中有一个有趣的应用。在物体检测中,我们需要判断一个物体位于图像的什么位置,因此需要使用小于图像尺寸的正方形区域对图像进行“滑窗”识别。在每一个窗口里,我们都需要执行一个单独的卷积网络,用以判断“物体是否在这个范围内”。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第25张图片
假设现在我们建立的网络是带有线性层的CNN网络,输入尺寸为14x14。那对于16x16尺寸的图像,就需要将下面四个14x14的区域分别输入CNN来进行判断,对每个区域输出“是”或“否”的结果。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第26张图片
但对于FCN而言,我们可以直接将这一张16x16的输入整个网络,最终会输出(2,2)大小的特征图。由于卷积网络层可以保留位置信息,所以这(2,2)的特征图中,每个像素的感受野都可以对应到原始图像中的相应区域,相当于使用一个网络一次性完成了对整个图像的四次扫描,并得到了四个相应的结果(2x2)。这种扫描方式比将图像切分成14x14的四块再运行4个CNN要高效得多,不过该应用仅限于物体检测中需要“滑窗”的场景。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第27张图片
总之,1x1卷积核的确可以替代全连接层,但效益不高。有些资料或文献会主张NiN网络使用1x1卷积替代了全连接层,但这一点不是非常严谨。在NiN的架构中,最后一个普通核尺寸的卷积核之后跟着的是MLP layers,并且这些MLP Layers最终将特征图数目缩小到了softmax公式要求的10个,因此说编号13、14的MLP Layers替代了普通CNN中全连接层的位置,也不是没有道理。不过在论文中,实现了全连接层的两个目标“整合信息”、“输出结果”的实际上是跟在MLP layers后的全局平均池化层(Global Average Pooling)。在NiN论文中,作者也明确表示,用来替代全连接层的是GAP层,我们来看看GAP层是如何运作的Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第28张图片
从之前1x1卷积核的例子来看,不难发现,只要在网络架构的最后能够将输出结果变成softmax函数可接受的格式,比如(n_class,1),并且确定用于生成这些输入值的信息是从之前的特征图中整合出来的,那任意架构在理论上来说都足以替代全连接层。GAP层就是这样的一个例子。GAP层的本质是池化层,它使用池化方式是平均池化,它的职责就是将上一层传入的无论多少特征图都转化成(n_class,1, 1)结构。为了能够将无论什么尺寸的特征图化为1x1的尺寸,GAP层所使用的核尺寸就等于输入的特征图尺寸。在NiN网络中,最后一个卷积层的输出是(10, 7, 7),因此全局平均池化层的核尺寸也是7x7,由于只能扫描一次,因此全局平均池化层不设置参数步长,一般也不会设置padding。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第29张图片
在PyTorch中,没有专门的GAP类,但我们可以使用普通的平均池化层,并令这个池化层的核尺寸为上层输入的特征图尺寸,以此来模拟全局平均池化。

data = torch.ones(10,7,7)
gap = nn.AvgPool2d(7)
gap(data).shape
#torch.Size([10, 1, 1])

使用1x1卷积核连接GAP的方式,NiN网络中完全没有使用全连接层,这让NiN网络整体的参数量减少不少,同时,GAP作为池化层,没有任何需要学习的参数,这让GAP的抗过拟合能力更强。在论文中,作者们还做了对比实验,证明GAP方法抗过拟合的能力更强:Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第30张图片
下一节,我们来复现一下NiN网络的架构。

3 NiN网络的复现

在之前的课程中,我们已经见过很多次NiN网络的架构了,经过这一节关于减少参数量和提升模型性能的讨论,我们已经对NiN网络中的每个细节都比较熟悉。现在,我们就使用nn.Sequential来打包实现一下NiN网络。Lesson 16.10&16.11&16.12&16.13 卷积层的参数量计算,1x1卷积核&分组卷积与深度可分离卷积&全连接层 nn.Sequential&全局平均池化,NiN网络复现_第31张图片
代码如下:

import torch
from torch import nn
from torchinfo import summary
data = torch.ones(size=(10,3,32,32))
class NiN(nn.Module):
    def __init__(self):
        super().__init__()
        
        self.block1 = 
nn.Sequential(nn.Conv2d(3,192,5,padding=2),nn.ReLU(inplace=True)
                                   ,nn.Conv2d(192,160,1),nn.ReLU(inplace=True)
                                   ,nn.Conv2d(160,96,1),nn.ReLU(inplace=True)
                                   ,nn.MaxPool2d(3,stride=2)
                                   ,nn.Dropout(0.25) 
                                   )
        #在原论文中并没有标明dropout的p为多少,一般来说,用于卷积层的dropout上的p值都会较小,因此设置了0.25              
        self.block2 = 
nn.Sequential(nn.Conv2d(96,192,5,padding=2),nn.ReLU(inplace=True)
                                   ,nn.Conv2d(192,192,1),nn.ReLU(inplace=True)
                                   ,nn.Conv2d(192,192,1),nn.ReLU(inplace=True)
                                   ,nn.MaxPool2d(3,stride=2)
                                   ,nn.Dropout(0.25)
                                   )
        self.block3 = 
nn.Sequential(nn.Conv2d(192,192,3,padding=1),nn.ReLU(inplace=True)
                                   ,nn.Conv2d(192,192,1),nn.ReLU(inplace=True)
                                   ,nn.Conv2d(192,10,1),nn.ReLU(inplace=True)
                                   ,nn.AvgPool2d(7,stride=1)
                                   ,nn.Softmax(dim=1)
                                   )
    def forward(self,x):
        output = self.block3(self.block2(self.block1(x)))
        return output
net = NiN()
net(data).shape
summary(net,(10,3,32,32))
# ==========================================================================================
# Layer (type:depth-idx)                   Output Shape              Param #
# ==========================================================================================
# nin                                      --                        --
# ├─Sequential: 1-1                        [10, 96, 15, 15]          --
# │    └─Conv2d: 2-1                       [10, 192, 32, 32]         14,592
# │    └─ReLU: 2-2                         [10, 192, 32, 32]         --
# │    └─Conv2d: 2-3                       [10, 160, 32, 32]         30,880
# │    └─ReLU: 2-4                         [10, 160, 32, 32]         --
# │    └─Conv2d: 2-5                       [10, 96, 32, 32]          15,456
# │    └─ReLU: 2-6                         [10, 96, 32, 32]          --
# │    └─MaxPool2d: 2-7                    [10, 96, 15, 15]          --
# │    └─Dropout: 2-8                      [10, 96, 15, 15]          --
# ├─Sequential: 1-2                        [10, 192, 7, 7]           --
# │    └─Conv2d: 2-9                       [10, 192, 15, 15]         460,992
# │    └─ReLU: 2-10                        [10, 192, 15, 15]         --
# │    └─Conv2d: 2-11                      [10, 192, 15, 15]         37,056
# │    └─ReLU: 2-12                        [10, 192, 15, 15]         --
# │    └─Conv2d: 2-13                      [10, 192, 15, 15]         37,056
# │    └─ReLU: 2-14                        [10, 192, 15, 15]         --
# │    └─MaxPool2d: 2-15                   [10, 192, 7, 7]           --
# │    └─Dropout: 2-16                     [10, 192, 7, 7]           --
# ├─Sequential: 1-3                        [10, 10, 1, 1]            --
# │    └─Conv2d: 2-17                      [10, 192, 7, 7]           331,968
# │    └─ReLU: 2-18                        [10, 192, 7, 7]           --
# │    └─Conv2d: 2-19                      [10, 192, 7, 7]           37,056
# │    └─ReLU: 2-20                        [10, 192, 7, 7]           --
# │    └─Conv2d: 2-21                      [10, 10, 7, 7]            1,930
# │    └─ReLU: 2-22                        [10, 10, 7, 7]            --
# │    └─AvgPool2d: 2-23                   [10, 10, 1, 1]            --
# │    └─Softmax: 2-24                     [10, 10, 1, 1]            --
# ==========================================================================================
# Total params: 966,986
# Trainable params: 966,986
# Non-trainable params: 0
# Total mult-adds (G): 2.01
# ==========================================================================================
# Input size (MB): 0.12
# Forward/backward pass size (MB): 48.61
# Params size (MB): 3.87
# Estimated Total Size (MB): 52.60
# ==========================================================================================

作为9层卷积层、最大特征图数目达到192的网络,NiN的参数量在百万之下,可以说都是归功于没有使用全连接层。不过,1x1卷积层所带来的参数量也不少,因此NiN可以说是在各方面都中规中矩的网络。从今天的眼光来看,NiN网络最大的贡献就是在于让人们意识到了1x1卷积层可能的用途,并且将“舍弃线性层”的议题摆在了研究者面前。受到NiN网络启发而诞生GoogLeNet以及ResNet都使用了1x1卷积层,并且在各种消减参数的操作下使网络变得更加深。

到这里,我们已经讲述了好几种降低参数量的操作,你还知道其他常用的降低参数量或计算量的手段吗?欢迎随时在群内与小伙伴们分享最新的论文和研究成果。从下一节开始,我们将开始了解视觉领域最前沿的数个模型。

你可能感兴趣的:(深度学习——PyTorch,cv)