文章目录
I. 卷积神经网络
先描述卷积神经⽹络中卷积层和池化层的⼯作原理,并解释填充、步幅、输⼊通道和输出通道的含义。在掌握了这些基础知识以后,我们将探究数个具有代表性的深度卷积神经⽹络的设计思路。这些模型包括最早提出的AlexNet,以及后来的使⽤重复元素的⽹络(VGG)、⽹络中的⽹络(NiN)、含并⾏连结的⽹络(GoogLeNet)、残差⽹络(ResNet)和稠密连接⽹络(DenseNet),且阐述的批量归⼀化和残差⽹络为训练和设计深度模型提供了两类重要思路。
二维卷积层
卷积神经⽹络(convolutional neural network)是含有卷积层(convolutional layer)的神经⽹络。本章中介绍的卷积神经⽹络均使⽤最常⻅的⼆维卷积层。它有⾼和宽两个空间维度,常⽤来处理图像数据。
⼆维互相关运算
卷积层得名于卷积(convolution)运算,但我们通常在卷积层中使⽤更加直观的互相关(crosscorrelation)运算。在⼆维卷积层中,⼀个⼆维输⼊数组和⼀个⼆维核(kernel)数组通过互相关运算输出⼀个⼆维数组。

在⼆维互相关运算中,卷积窗口从输⼊数组的最左上⽅开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。当卷积窗口滑动到某⼀位置时,窗口中的输⼊⼦数组与核数组按元素相乘并求和,得到输出数组中相应位置的元素。
\[\begin{split}0\times0+1\times1+3\times2+4\times3=19,\\ 1\times0+2\times1+4\times2+5\times3=25,\\ 3\times0+4\times1+6\times2+7\times3=37,\\ 4\times0+5\times1+7\times2+8\times3=43.\\\end{split}\]
二维卷积层
⼆维卷积层将输⼊和卷积核做互相关运算,并加上⼀个标量偏差来得到输出。卷积层的模型参数包括了卷积核和标量偏差。在训练模型的时候,通常我们先对卷积核随机初始化,然后不断迭代卷积核和偏差。
互相关运算和卷积运算
卷积运算与互相关运算类似。为了得到卷积运算的输出,只需将核数组左右翻转并上下翻转,再与输⼊数组做互相关运算。卷积层⽆论使⽤互相关运算或卷积运算都不影响模型预测时的输出。如⽆特别说明,提到的卷积运算均指互相关运算
特征图和感受野
⼆维卷积层输出的⼆维数组可以看作输⼊在空间维度(宽和⾼)上某⼀级的表征,也叫特征图(feature map)。
影响元素 $x$ 的前向计算的所有可能输⼊区域(可能⼤于输⼊的实际尺⼨)叫做 $x$ 的感受野(receptive field)。
填充和步幅
我们使⽤⾼和宽为3的输⼊与⾼和宽为2的卷积核得到⾼和宽为2的输出。⼀般来说,假设输⼊形状是$n_h \times n_{\omega}$,卷积核窗口形状是$k_h \times k_{\omega}$,那么输出形状将会是
$$(n_h - k_h +1) \times (n_{\omega} -k_{\omega} +1)$$
所以卷积层的输出形状由输⼊形状和卷积核窗口形状决定。我们将介绍卷积层的两个超参数,即填充和步幅。它们可以对给定形状的输⼊和卷积核改变输出形状。
填充
填充(padding)是指在输⼊⾼和宽的两侧填充元素(通常是0元素)。

⼀般来说,如果在⾼的两侧⼀共填充$p_h$⾏,在宽的两侧⼀共填充$p_{\omega}$列,那么输出形状将会是
$$(n_h - k_h +p_h+1) \times (n_{\omega} -k_{\omega} +p_{\omega} +1)$$
也就是说,输出的⾼和宽会分别增加$p_h$和$p_{\omega}$。
我们会设置$p_h = k_h - 1$和$p_{\omega} = k_{\omega} - 1$来使输⼊和输出具有相同的⾼和宽。这样会⽅便在构造⽹络时推测每个层的输出形状。
卷积神经⽹络经常使⽤奇数⾼和宽的卷积核,如1、 3、 5和7,所以两端上的填充个数相等。
当卷积核的⾼和宽不同时, 填充可以增加输出的⾼和宽。这常⽤来使输出与输⼊具有相同的⾼和宽。
步幅
卷积窗口从输⼊数组的最左上⽅开始,按从左往右、从上往下的顺序,依次在输⼊数组上滑动。我们将每次滑动的⾏数和列数称为步幅(stride)。

一般来说, 当⾼上步幅为$s_h$,宽上步幅为$s_{\omega}$时,输出形状为
$$[(n_h - k_h +p_h+s_h)/s_h] \times [(n_{\omega} -k_{\omega} +p_{\omega} +s_{\omega})/s_{\omega}/s_{\omega}].$$
如果设置$p_h = k_h - 1$和$p_{\omega} = k_{\omega} - 1$,那么输出形状将简化为$[(n_h + s_h - 1)/s_h] \times [(n_{\omega} + s_{\omega} - 1)/s_{\omega}]$。
更进⼀步,如果输⼊的⾼和宽能分别被⾼和宽上的步幅整除,那么输出形状将是$(n_h / s_h)\times (n_{\omega}/n_{\omega})$。
步幅可以减小输出的⾼和宽,例如输出的⾼和宽仅为输⼊的⾼和宽的$1/n$($n$为⼤于1的整数)。
多通道输入和输出
之前⽤到的输⼊和输出都是⼆维数组,但真实数据的维度经常更⾼。例如,彩⾊图像在⾼和宽2个维度外还有RGB(红、绿、蓝) 3个颜⾊通道。假设彩⾊图像的⾼和宽分别是$h$和$w$(像素),那么它可以表⽰为⼀个$3\times h \times w$的多维数组。我们将⼤小为3的这⼀维称为通道(channel)维。下面我们将介绍含多个输⼊通道或多个输出通道的卷积核。
多输入通道
当输⼊数据含多个通道时,我们需要构造⼀个输⼊通道数与输⼊数据的通道数相同的卷积核,从而能够与含多通道的输⼊数据做互相关运算。 假设输⼊数据的通道数为$c_i$,那么卷积核的输⼊通道数同样为$c_i$。
设卷积核窗口形状为$k_h \times k_w$。当$c_i = 1$时,我们知道卷积核只包含⼀个形状为$k_h \times k_w$的⼆维数组。当$c_i > 1$时,我们将会为每个输⼊通道各分配⼀个形状为$k_h \times k_w$的核数组。把这$c_i$个数组在输⼊通道维上连结,即得到⼀个形状为$c_i \times k_h \times kw$的卷积核。由于输⼊和卷积核各有$c_i$个通道,我们可以在各个通道上对输⼊的⼆维数组和卷积核的⼆维核数组做互相关运算,再将这$c_i$个互相关运算的⼆维输出按通道相加,得到⼀个⼆维数组。

多通道输出
当输⼊通道有多个时,因为我们对各个通道的结果做了累加,所以不论输⼊通道数是多少,输出通道数总是为1。设卷积核输⼊通道数和输出通道数分别为$c_i$和$c_o$,⾼和宽分别为$k_h$和$k_w$。
如果希望得到含多个通道的输出,我们可以为每个输出通道分别创建形状为$c_i \times k_h \times k_w$的核数组。将它们在输出通道维上连结,卷积核的形状即$c_o \times c_i \times k_h \times kw$。在做互相关运算时,每个输出通道上的结果由卷积核在该输出通道上的核数组与整个输⼊数组计算而来。
1 × 1卷积
最后我们讨论卷积窗口形状为$1\times 1(k_h = k_w = 1)$的多通道卷积层。我们通常称之为1 × 1卷积层,并将其中的卷积运算称为1 × 1卷积。1 × 1卷积失去了卷积层可以识别⾼和宽维度上相邻元素构成的模式的功能。实际上, 1 × 1卷积的主要计算发⽣在通道维上。假设我们将通道维当作特征维,将⾼和宽维度上的元素当成数据样本,那么1 × 1卷积层的作⽤与全连接层等价。

- 1 × 1卷积层通常⽤来调整⽹络层之间的通道数,并控制模型复杂度 。
- 假设将通道维当作特征维,将高和宽维度上的元素当成数据样本,那么1×1卷积层的作用与全连接层等价。
- 1×1卷积层通常用来调整网络层之间的通道数,并控制模型复杂度。
池化层
池化(pooling)层,它的提出是为了缓解卷积层对位置的过度敏感性。
⼆维最⼤池化和平均池化
同卷积层⼀样,池化层每次对输⼊数据的⼀个固定形状窗口(⼜称池化窗口)中的元素计算输出。不同于卷积层⾥计算输⼊和核的互相关性,池化层直接计算池化窗口内元素的最⼤值或者平均值。该运算也分别叫做最⼤池化或平均池化。在二维最大池化中,池化窗口从输入数组的最左上方开始,按从左往右、从上往下的顺序,依次在输入数组上滑动。当池化窗口滑动到某一位置时,窗口中的输入子数组的最大值即输出数组中相应位置的元素。

阴影部分为第一个输出元素及其计算所使用的输入元素。输出数组的高和宽分别为2,其中的4个元素由取最大值运算maxmax得出:
\[\begin{split}\max(0,1,3,4)=4,\\ \max(1,2,4,5)=5,\\ \max(3,4,6,7)=7,\\ \max(4,5,7,8)=8.\\\end{split}\]
⼆维平均池化的⼯作原理与⼆维最⼤池化类似,但将最⼤运算符替换成平均运算符。池化窗口形状为$p \times q$的池化层称为$p \times q$池化层,其中的池化运算叫作$p \times q$池化。
填充和步幅
同卷积层⼀样,池化层也可以在输⼊的⾼和宽两侧的填充并调整窗口的移动步幅来改变输出形状。池化层填充和步幅与卷积层填充和步幅的⼯作机制⼀样。
多通道
在处理多通道输⼊数据时,池化层对每个输⼊通道分别池化,而不是像卷积层那样将各通道的输⼊按通道相加。这意味着池化层的输出通道数与输⼊通道数相等。
II. LeNet

LeNet模型
LeNet分为卷积层块和全连接层块两个部分。下⾯我们分别介绍这两个模块。
卷积层块⾥的基本单位是卷积层后接最⼤池化层:卷积层⽤来识别图像⾥的空间模式,如线条和物体局部,之后的最⼤池化层则⽤来降低卷积层对位置的敏感性。卷积层块由两个这样的基本单位重复堆叠构成。在卷积层块中,每个卷积层都使⽤5 × 5的窗口,并在输出上使⽤sigmoid激活函数。第⼀个卷积层输出通道数为6,第⼆个卷积层输出通道数则增加到16。这是因为第⼆个卷积层⽐第⼀个卷积层的输⼊的⾼和宽要小,所以增加输出通道使两个卷积层的参数尺⼨类似。卷积层块的两个最⼤池化层的窗口形状均为2 × 2,且步幅为2。由于池化窗口与步幅形状相同,池化窗口在输⼊上每次滑动所覆盖的区域互不重叠。
卷积层块的输出形状为(批量⼤小, 通道, ⾼, 宽)。当卷积层块的输出传⼊全连接层块时,全连接层块会将小批量中每个样本变平(flatten)。也就是说,全连接层的输⼊形状将变成⼆维,其中第⼀维是小批量中的样本,第⼆维是每个样本变平后的向量表⽰,且向量⻓度为通道、⾼和宽的乘积。全连接层块含3个全连接层。它们的输出个数分别是120、 84和10,其中10为输出的类别个数。
LeNet实现
'''LeNet in PyTorch.'''
import torch.nn as nn
import torch.nn.functional as F
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
self.fc1 = nn.Linear(16*5*5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
out = F.relu(self.conv1(x))
out = F.max_pool2d(out, 2)
out = F.relu(self.conv2(out))
out = F.max_pool2d(out, 2)
out = out.view(out.size(0), -1)
out = F.relu(self.fc1(out))
out = F.relu(self.fc2(out))
out = self.fc3(out)
return out
III. AlexNet

AlexNet模型
2012年, AlexNet横空出世。这个模型的名字来源于论⽂第⼀作者的姓名Alex Krizhevsky 。AlexNet使⽤了8层卷积神经⽹络,并以很⼤的优势赢得了ImageNet 2012图像识别挑战赛。它⾸次证明了学习到的特征可以超越⼿⼯设计的特征,从而⼀举打破计算机视觉研究的前状。

LeNet vs. AlexNet

第⼀,与相对较小的LeNet相⽐, AlexNet包含8层变换,其中有5层卷积和2层全连接隐藏层,以及1个全连接输出层。下⾯我们来详细描述这些层的设计。AlexNet第⼀层中的卷积窗口形状是11 × 11。因为ImageNet中绝⼤多数图像的⾼和宽均⽐MNIST图像的⾼和宽⼤10倍以上, ImageNet图像的物体占⽤更多的像素,所以需要更⼤的卷积窗口来捕获物体。第⼆层中的卷积窗口形状减小到5 × 5,之后全采⽤3 × 3。此外,第⼀、第⼆和第五个卷积层之后都使⽤了窗口形状为3 × 3、步幅为2的最⼤池化层。而且, AlexNet使⽤的卷积通道数也⼤于LeNet中的卷积通道数数⼗倍。紧接着最后⼀个卷积层的是两个输出个数为4,096的全连接层。
第⼆, AlexNet将sigmoid激活函数改成了更加简单的ReLU激活函数。⼀⽅⾯, ReLU激活函数的计算更简单,例如它并没有sigmoid激活函数中的求幂运算。另⼀⽅⾯, ReLU激活函数在不同的参数初始化⽅法下使模型更容易训练。这是由于当sigmoid激活函数输出极接近0或1时,这些区域的梯度⼏乎为0,从而造成反向传播⽆法继续更新部分模型参数;而ReLU激活函数在正区间的梯度恒为1。因此,若模型参数初始化不当, sigmoid函数可能在正区间得到⼏乎为0的梯度,从而令模型⽆法得到有效训练。
第三, AlexNet通过丢弃法dropout来控制全连接层的模型复杂度。而LeNet并没有使⽤丢弃法。
第四, AlexNet引⼊了⼤量的图像增⼴,如翻转、裁剪和颜⾊变化,从而进⼀步扩⼤数据集来缓解过拟合。
AlexNet实现
import torch
import torch.nn as nn
import torchvision
class AlexNet(nn.Module):
def __init__(self,num_classes=1000):
super(AlexNet,self).__init__()
self.feature_extraction = nn.Sequential(
nn.Conv2d(in_channels=3,out_channels=96,kernel_size=11,stride=4,padding=2,bias=False),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3,stride=2,padding=0),
nn.Conv2d(in_channels=96,out_channels=192,kernel_size=5,stride=1,padding=2,bias=False),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3,stride=2,padding=0),
nn.Conv2d(in_channels=192,out_channels=384,kernel_size=3,stride=1,padding=1,bias=False),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=384,out_channels=256,kernel_size=3,stride=1,padding=1,bias=False),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=256,out_channels=256,kernel_size=3,stride=1,padding=1,bias=False),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=0),
)
self.classifier = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(in_features=256*6*6,out_features=4096),
nn.Dropout(p=0.5),
nn.Linear(in_features=4096, out_features=4096),
nn.Linear(in_features=4096, out_features=num_classes),
)
def forward(self,x):
x = self.feature_extraction(x)
x = x.view(x.size(0),256*6*6)
x = self.classifier(x)
return x
if __name__ =='__main__':
model = AlexNet()
print(model)
III. VGG

VGG块
VGG块的组成规律是:连续使⽤数个相同的填充为1、窗口形状为3 × 3的卷积层后接上⼀个步幅为2、窗口形状为2 × 2的最⼤池化层。卷积层保持输⼊的⾼和宽不变,而池化层则对其减半。我们使⽤vgg_block函数来实现这个基础的VGG块,它可以指定卷积层的数量num_convs和输出通道数num_channels。

VGG模型
现在我们构造⼀个VGG⽹络。它有5个卷积块,前2块使⽤单卷积层,而后3块使⽤双卷积层。第⼀块的输出通道是64,之后每次对输出通道数翻倍,直到变为512。因为这个⽹络使⽤了13个卷积层和3个全连接层,所以经常被称为VGG-16。

VGG实现
import torch
import torch.nn as nn
import torchvision
def Conv3x3BNReLU(in_channels,out_channels):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels,out_channels=out_channels,kernel_size=3,stride=1,padding=1),
nn.BatchNorm2d(out_channels),
nn.ReLU6(inplace=True)
)
class VGGNet(nn.Module):
def __init__(self, block_nums,num_classes=1000):
super(VGGNet, self).__init__()
self.stage1 = self._make_layers(in_channels=3, out_channels=64, block_num=block_nums[0])
self.stage2 = self._make_layers(in_channels=64, out_channels=128, block_num=block_nums[1])
self.stage3 = self._make_layers(in_channels=128, out_channels=256, block_num=block_nums[2])
self.stage4 = self._make_layers(in_channels=256, out_channels=512, block_num=block_nums[3])
self.stage5 = self._make_layers(in_channels=512, out_channels=512, block_num=block_nums[4])
self.classifier = nn.Sequential(
nn.Linear(in_features=512*7*7,out_features=4096),
nn.Dropout(p=0.2),
nn.Linear(in_features=4096, out_features=4096),
nn.Dropout(p=0.2),
nn.Linear(in_features=4096, out_features=num_classes)
)
self._init_params()
def _make_layers(self, in_channels, out_channels, block_num):
layers = []
layers.append(Conv3x3BNReLU(in_channels,out_channels))
for i in range(1,block_num):
layers.append(Conv3x3BNReLU(out_channels,out_channels))
layers.append(nn.MaxPool2d(kernel_size=2,stride=2, ceil_mode=False))
return nn.Sequential(*layers)
def _init_params(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.stage1(x)
x = self.stage2(x)
x = self.stage3(x)
x = self.stage4(x)
x = self.stage5(x)
x = x.view(x.size(0),-1)
out = self.classifier(x)
return out
def VGG16():
block_nums = [2, 2, 3, 3, 3]
model = VGGNet(block_nums)
return model
def VGG19():
block_nums = [2, 2, 4, 4, 4]
model = VGGNet(block_nums)
return model
if __name__ == '__main__':
model = VGG16()
print(model)
input = torch.randn(1,3,224,224)
out = model(input)
print(out.shape)
IV. NiN
介绍的LeNet、 AlexNet和VGG在设计上的共同之处是:先以由卷积层构成的模块充分抽取空间特征,再以由全连接层构成的模块来输出分类结果。其中, AlexNet和VGG对LeNet的改进主要在于如何对这两个模块加宽(增加通道数)和加深。 本节我们介绍⽹络中的⽹络(NiN)提出了串联多个由卷积层和“全连接”层构成的小⽹络来构建⼀个深层⽹络。

NiN块
卷积层的输⼊和输出通常是四维数组(样本,通道,⾼,宽),而全连接层的输⼊和输出则通常是⼆维数组(样本,特征)。如果想在全连接层后再接上卷积层,则需要将全连接层的输出变换为四维。 NiN使⽤1×1卷积层来替代全连接层,从而使空间信息能够⾃然传递到后⾯的层中去。

NiN块是NiN中的基础块。它由一个卷积层加两个充当全连接层的1×11×1卷积层串联而成。其中第一个卷积层的超参数可以自行设置,而第二和第三个卷积层的超参数一般是固定的。
NiN模型
NiN是在AlexNet问世不久后提出的。它们的卷积层设定有类似之处。 NiN使⽤卷积窗口形状分别为11 × 11、 5 × 5和3 × 3的卷积层,相应的输出通道数也与AlexNet中的⼀致。每个NiN块后接⼀个步幅为2、窗口形状为3 × 3的最⼤池化层。

除使⽤NiN块以外, NiN还有⼀个设计与AlexNet显著不同: NiN去掉了AlexNet最后的3个全连接层,取而代之地, NiN使⽤了输出通道数等于标签类别数的NiN块,然后使⽤全局平均池化层对每个通道中所有元素求平均并直接⽤于分类。这⾥的全局平均池化层即窗口形状等于输⼊空间维形状的平均池化层。 NiN的这个设计的好处是可以显著减小模型参数尺⼨,从而缓解过拟合。然而,该设计有时会造成获得有效模型的训练时间的增加。
- NiN重复使⽤由卷积层和代替全连接层的1 × 1卷积层构成的NiN块来构建深层⽹络。
- NiN去除了容易造成过拟合的全连接输出层,而是将其替换成输出通道数等于标签类别数的NiN块和全局平均池化层。
- NiN的以上设计思想影响了后⾯⼀系列卷积神经⽹络的设计。
V. GoogLeNet

Inception块
GoogLeNet吸收了NiN中⽹络串联⽹络的思想,并在此基础上做了很⼤改进。在随后的⼏年⾥,研究⼈员对GoogLeNet进⾏了数次改进,本节将介绍这个模型系列的第⼀个版本。

Inception块⾥有4条并⾏的线路。前3条线路使⽤窗口⼤小分别是1 × 1、 3 ×3和5 × 5的卷积层来抽取不同空间尺⼨下的信息,其中中间2个线路会对输⼊先做1 × 1卷积来减少输⼊通道数,以降低模型复杂度。第四条线路则使⽤3 × 3最⼤池化层,后接1 × 1卷积层来改变 通道数。 4条线路都使⽤了合适的填充来使输⼊与输出的⾼和宽⼀致。最后我们将每条线路的输出在通道维上连结,并输⼊接下来的层中去。
Inception块中可以⾃定义的超参数是每个层的输出通道数,我们以此来控制模型复杂度。
GoogLeNet模型

GoogLeNet跟VGG⼀样,在主体卷积部分中使⽤5个模块(block),每个模块之间使⽤步幅为2的3×3最⼤池化层来减小输出⾼宽。
第⼀模块使⽤⼀个64通道的7 × 7卷积层。
第⼆模块使⽤2个卷积层:⾸先是64通道的1 × 1卷积层,然后是将通道增⼤3倍的3 × 3卷积层。它对应Inception块中的第⼆条线路。
第三模块串联2个完整的Inception块。第⼀个Inception块的输出通道数为64+128+32+32 = 256,其中4条线路的输出通道数⽐例为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。
第四模块更加复杂。它串联了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最⼤池化层的第四条线路。其中第⼆、第三条线路都会先按⽐例减小通道数。
第五模块有输出通道数为256 + 320 + 128 + 128 = 832和384 + 384 + 128 + 128 = 1024的两个Inception块。其中每条线路的通道数的分配思路和第三、第四模块中的⼀致,只是在具体数值上有所不同。需要注意的是,第五模块的后⾯紧跟输出层,该模块同NiN⼀样使⽤全局平均池化层来将每个通道的⾼和宽变成1。最后我们将输出变成⼆维数组后接上⼀个输出个数为标签类别数的全连接层。
- Inception块相当于⼀个有4条线路的⼦⽹络。它通过不同窗口形状的卷积层和最⼤池化层来并⾏抽取信息,并使⽤1 × 1卷积层减少通道数从而降低模型复杂度。
- GoogLeNet将多个设计精细的Inception块和其他层串联起来。其中Inception块的通道数分配之⽐是在ImageNet数据集上通过⼤量的实验得来的。
- GoogLeNet和它的后继者们⼀度是ImageNet上最⾼效的模型之⼀:在类似的测试精度下,它们的计算复杂度往往更低。
GoogLeNet实现
import torch
import torch.nn as nn
import torchvision
def ConvBNReLU(in_channels,out_channels,kernel_size):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=kernel_size, stride=1,padding=kernel_size//2),
nn.BatchNorm2d(out_channels),
nn.ReLU6(inplace=True)
)
class InceptionV1Module(nn.Module):
def __init__(self, in_channels,out_channels1, out_channels2reduce,out_channels2, out_channels3reduce, out_channels3, out_channels4):
super(InceptionV1Module, self).__init__()
self.branch1_conv = ConvBNReLU(in_channels=in_channels,out_channels=out_channels1,kernel_size=1)
self.branch2_conv1 = ConvBNReLU(in_channels=in_channels,out_channels=out_channels2reduce,kernel_size=1)
self.branch2_conv2 = ConvBNReLU(in_channels=out_channels2reduce,out_channels=out_channels2,kernel_size=3)
self.branch3_conv1 = ConvBNReLU(in_channels=in_channels, out_channels=out_channels3reduce, kernel_size=1)
self.branch3_conv2 = ConvBNReLU(in_channels=out_channels3reduce, out_channels=out_channels3, kernel_size=5)
self.branch4_pool = nn.MaxPool2d(kernel_size=3,stride=1,padding=1)
self.branch4_conv1 = ConvBNReLU(in_channels=in_channels, out_channels=out_channels4, kernel_size=1)
def forward(self,x):
out1 = self.branch1_conv(x)
out2 = self.branch2_conv2(self.branch2_conv1(x))
out3 = self.branch3_conv2(self.branch3_conv1(x))
out4 = self.branch4_conv1(self.branch4_pool(x))
out = torch.cat([out1, out2, out3, out4], dim=1)
return out
class InceptionAux(nn.Module):
def __init__(self, in_channels,out_channels):
super(InceptionAux, self).__init__()
self.auxiliary_avgpool = nn.AvgPool2d(kernel_size=5, stride=3)
self.auxiliary_conv1 = ConvBNReLU(in_channels=in_channels, out_channels=128, kernel_size=1)
self.auxiliary_linear1 = nn.Linear(in_features=128 * 4 * 4, out_features=1024)
self.auxiliary_relu = nn.ReLU6(inplace=True)
self.auxiliary_dropout = nn.Dropout(p=0.7)
self.auxiliary_linear2 = nn.Linear(in_features=1024, out_features=out_channels)
def forward(self, x):
x = self.auxiliary_conv1(self.auxiliary_avgpool(x))
x = x.view(x.size(0), -1)
x= self.auxiliary_relu(self.auxiliary_linear1(x))
out = self.auxiliary_linear2(self.auxiliary_dropout(x))
return out
class InceptionV1(nn.Module):
def __init__(self, num_classes=1000, stage='train'):
super(InceptionV1, self).__init__()
self.stage = stage
self.block1 = nn.Sequential(
nn.Conv2d(in_channels=3,out_channels=64,kernel_size=7,stride=2,padding=3),
nn.BatchNorm2d(64),
nn.MaxPool2d(kernel_size=3,stride=2, padding=1),
nn.Conv2d(in_channels=64, out_channels=64, kernel_size=1, stride=1),
nn.BatchNorm2d(64),
)
self.block2 = nn.Sequential(
nn.Conv2d(in_channels=64, out_channels=192, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(192),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
)
self.block3 = nn.Sequential(
InceptionV1Module(in_channels=192,out_channels1=64, out_channels2reduce=96, out_channels2=128, out_channels3reduce = 16, out_channels3=32, out_channels4=32),
InceptionV1Module(in_channels=256, out_channels1=128, out_channels2reduce=128, out_channels2=192,out_channels3reduce=32, out_channels3=96, out_channels4=64),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
)
self.block4_1 = InceptionV1Module(in_channels=480, out_channels1=192, out_channels2reduce=96, out_channels2=208,out_channels3reduce=16, out_channels3=48, out_channels4=64)
if self.stage == 'train':
self.aux_logits1 = InceptionAux(in_channels=512,out_channels=num_classes)
self.block4_2 = nn.Sequential(
InceptionV1Module(in_channels=512, out_channels1=160, out_channels2reduce=112, out_channels2=224,
out_channels3reduce=24, out_channels3=64, out_channels4=64),
InceptionV1Module(in_channels=512, out_channels1=128, out_channels2reduce=128, out_channels2=256,
out_channels3reduce=24, out_channels3=64, out_channels4=64),
InceptionV1Module(in_channels=512, out_channels1=112, out_channels2reduce=144, out_channels2=288,
out_channels3reduce=32, out_channels3=64, out_channels4=64),
)
if self.stage == 'train':
self.aux_logits2 = InceptionAux(in_channels=528,out_channels=num_classes)
self.block4_3 = nn.Sequential(
InceptionV1Module(in_channels=528, out_channels1=256, out_channels2reduce=160, out_channels2=320,
out_channels3reduce=32, out_channels3=128, out_channels4=128),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1),
)
self.block5 = nn.Sequential(
InceptionV1Module(in_channels=832, out_channels1=256, out_channels2reduce=160, out_channels2=320,out_channels3reduce=32, out_channels3=128, out_channels4=128),
InceptionV1Module(in_channels=832, out_channels1=384, out_channels2reduce=192, out_channels2=384,out_channels3reduce=48, out_channels3=128, out_channels4=128),
)
self.avgpool = nn.AvgPool2d(kernel_size=7,stride=1)
self.dropout = nn.Dropout(p=0.4)
self.linear = nn.Linear(in_features=1024,out_features=num_classes)
def forward(self, x):
x = self.block1(x)
x = self.block2(x)
x = self.block3(x)
aux1 = x = self.block4_1(x)
aux2 = x = self.block4_2(x)
x = self.block4_3(x)
out = self.block5(x)
out = self.avgpool(out)
out = self.dropout(out)
out = out.view(out.size(0), -1)
out = self.linear(out)
if self.stage == 'train':
aux1 = self.aux_logits1(aux1)
aux2 = self.aux_logits2(aux2)
return aux1, aux2, out
else:
return out
if __name__=='__main__':
model = InceptionV1()
print(model)
input = torch.randn(1, 3, 224, 224)
aux1, aux2, out = model(input)
print(aux1.shape)
print(aux2.shape)
print(out.shape)
IV. Batch Normalization
批量归一化层( batch normalization ), 它能让较深的神经⽹络的训练变得更加容易。 在模型训练时,批量归⼀化利⽤小批量上的均值和标准差,不断调整神经⽹络中间输出,从而使整个神经⽹络在各层的中间输出的数值更稳定。
批量归一化层
对全连接层和卷积层做批量归⼀化的⽅法稍有不同。下⾯我们将分别介绍这两种情况下的批量归⼀化。
对全连接层做批量归⼀化
通常,我们将批量归⼀化层置于全连接层中的仿射变换和激活函数之间。设全连接层的输⼊为$u$,权重参数和偏差参数分别为$W$和$B$,激活函数为$\phi$。设批量归⼀化的运算符为BN。那么,使⽤批量归⼀化的全连接层的输出为$$\phi(BN(x)),$$ 其中批量归⼀化输⼊$x$由仿射变换$$x=Wu + b$$得到。 考虑一个由$m$个样本组成的小批量,仿射变换的输出为一个新的小批量$\beta = {x^{(1)},\cdots,x^{(m)}}$。它们是批量归一化层的输入。对于小批量$\beta$中任意样本$x^{(i)}\in R^{(d)}, 1\le i \le m$,批量归一化层的输出同样是$d$维向量$$y^{(i)}=BN(x^{(i)}),$$并由以下几步求得。
首先,对小批量$\beta$求均值和方差:$$\mu_{\beta}\leftarrow \frac{1}{m}\sum_{i=1}^{m}x^{(i)},$$ $$\sigma^2_{\beta} \leftarrow \frac{1}{m} \sum_{i=1}^{m}{(x^{(i)}-\mu_{\beta}}^2,$$其中的平方计算是按元素求平方。
接下来对$x^{(i)}$标准化:$$\hat{x}^{(i)} \leftarrow \frac{x^{(i) - \mu_{\beta}}}{\sigma^2_{\beta} + \epsilon},$$这里$\epsilon$是一个很小的常数,保证分母大于0。 批量归⼀化层引⼊了两个可以学习的模型参数,拉伸(scale)参数$\gamma$和偏移(shift)参数$\beta$。这两个参数和$x(i)$形状相同,皆为$d$维向量。 它们与$\hat{x}^{(i)}$分别做按元素乘法(符号$\odot$)和加法计算: $$y^{(i)} \leftarrow \gamma \odot \hat{x^{(i)}} + \beta.$$ ⾄此,我们得到了$x(i)$的批量归⼀化的输出$y(i)$。
对卷积层做批量归一化
对卷积层来说,批量归⼀化发⽣在卷积计算之后、应⽤激活函数之前。如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归⼀化,且每个通道都拥有独⽴的拉伸和偏移参数,并均为标量。设小批量中有m个样本。在单个通道上,假设卷积计算输出的⾼和宽分别为$p$和$q$。我们需要对该通道中$m\times p \times q$个元素同时做批量归⼀化。对这些元素做标准化计算时,我们使⽤相同的均值和⽅差,即该通道中$m\times p\times q$个元素的均值和⽅差。
预测时的批量归⼀化
使⽤批量归⼀化训练时,我们可以将批量⼤小设得⼤⼀点,从而使批量内样本的均值和⽅差的计算都较为准确。将训练好的模型⽤于预测时,我们希望模型对于任意输⼊都有确定的输出。因此,单个样本的输出不应取决于批量归⼀化所需要的随机小批量中的均值和⽅差。⼀种常⽤的⽅法是通过移动平均估算整个训练数据集的样本均值和⽅差,并在预测时使⽤它们得到确定的输出。
VII. ResNet
让我们先思考⼀个问题:对神经⽹络模型添加新的层,充分训练后的模型是否只可能更有效地降低训练误差?理论上,原模型解的空间只是新模型解的空间的⼦空间。也就是说,如果我们能将新添加的层训练成恒等映射$f(x)=x$,新模型和原模型将同样有效。由于新模型可能得出更优的解来拟合训练数据集,因此添加层似乎更容易降低训练误差。然而在实践中,添加过多的层后训练误差往往不降反升。即使利⽤批量归⼀化带来的数值稳定性使训练深层模型更加容易,该问题仍然存在。针对这⼀问题,何恺明等⼈提出了残差⽹络(ResNet)。

残差块
考虑局部神经网络,设输入为$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卷积层来将输⼊变换成需要的形状后再做相加运算。
ResNet模型
ResNet的前两层跟之前介绍的GoogLeNet中的⼀样:在输出通道数为64、步幅为2的7 × 7卷积层后接步幅为2的3 × 3的最⼤池化层。不同之处在于ResNet每个卷积层后增加的批量归⼀化层。
GoogLeNet在后⾯接了4个由Inception块组成的模块。 ResNet则使⽤4个由残差块组成的模块,每个模块使⽤若⼲个同样输出通道数的残差块。第⼀个模块的通道数同输⼊通道数⼀致。由于之前已经使⽤了步幅为2的最⼤池化层,所以⽆须减小⾼和宽。之后的每个模块在第⼀个残差块⾥将上⼀个模块的通道数翻倍,并将⾼和宽减半。接着我们为ResNet加⼊所有残差块。这⾥每个模块使⽤2个残差块。最后,与GoogLeNet⼀样,加⼊全局平均池化层后接上全连接层输出。

ResNet实现
import torch
import torch.nn as nn
import torchvision
print("PyTorch Version: ",torch.__version__)
print("Torchvision Version: ",torchvision.__version__)
__all__ = ['ResNet50', 'ResNet101','ResNet152']
def Conv1(in_planes, places, stride=2):
return nn.Sequential(
nn.Conv2d(in_channels=in_planes,out_channels=places,kernel_size=7,stride=stride,padding=3, bias=False),
nn.BatchNorm2d(places),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
class Bottleneck(nn.Module):
def __init__(self,in_places,places, stride=1,downsampling=False, expansion = 4):
super(Bottleneck,self).__init__()
self.expansion = expansion
self.downsampling = downsampling
self.bottleneck = nn.Sequential(
nn.Conv2d(in_channels=in_places,out_channels=places,kernel_size=1,stride=1, bias=False),
nn.BatchNorm2d(places),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=places, out_channels=places, kernel_size=3, stride=stride, padding=1, bias=False),
nn.BatchNorm2d(places),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=places, out_channels=places*self.expansion, kernel_size=1, stride=1, bias=False),
nn.BatchNorm2d(places*self.expansion),
)
if self.downsampling:
self.downsample = nn.Sequential(
nn.Conv2d(in_channels=in_places, out_channels=places*self.expansion, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(places*self.expansion)
)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
residual = x
out = self.bottleneck(x)
if self.downsampling:
residual = self.downsample(x)
out += residual
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self,blocks, num_classes=1000, expansion = 4):
super(ResNet,self).__init__()
self.expansion = expansion
self.conv1 = Conv1(in_planes = 3, places= 64)
self.layer1 = self.make_layer(in_places = 64, places= 64, block=blocks[0], stride=1)
self.layer2 = self.make_layer(in_places = 256,places=128, block=blocks[1], stride=2)
self.layer3 = self.make_layer(in_places=512,places=256, block=blocks[2], stride=2)
self.layer4 = self.make_layer(in_places=1024,places=512, block=blocks[3], stride=2)
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc = nn.Linear(2048,num_classes)
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def make_layer(self, in_places, places, block, stride):
layers = []
layers.append(Bottleneck(in_places, places,stride, downsampling =True))
for i in range(1, block):
layers.append(Bottleneck(places*self.expansion, places))
return nn.Sequential(*layers)
def forward(self, x):
x = self.conv1(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def ResNet50():
return ResNet([3, 4, 6, 3])
def ResNet101():
return ResNet([3, 4, 23, 3])
def ResNet152():
return ResNet([3, 8, 36, 3])
if __name__=='__main__':
#model = torchvision.models.resnet50()
model = ResNet50()
print(model)
input = torch.randn(1, 3, 224, 224)
out = model(input)
print(out.shape)
VIII. DenseNet
ResNet中的跨层连接设计引申出了数个后续⼯作。本节我们介绍其中的⼀个:稠密连接⽹络(DenseNet)

将部分前后相邻的运算抽象为模块A和模块B。与ResNet的主要区别在于, DenseNet⾥模块B的输出不是像ResNet那样和模块A的输出相加,而是在通道维上连结。这样模块A的输出可以直接传⼊模块B后⾯的层。 DenseNet的主要构建模块是稠密块(dense block)和过渡层(transition layer)。前者定义了输⼊和输出是如何连结的,后者则⽤来控制通道数,使之不过⼤。
稠密块

DenseNet使⽤了ResNet改良版的“批量归⼀化、激活和卷积”结构, 类似于ResNet接下来使⽤的4个残差块, DenseNet使⽤的是4个稠密块。同ResNet⼀样,我们可以设置每个稠密块使⽤多少个卷积层。这⾥我们设成4,从而与上⼀节的ResNet-18保持⼀致。稠密块⾥的卷积层通道数(即增⻓率)设为32,所以每个稠密块将增加128个通道。
- 在跨层连接上,不同于ResNet中将输⼊与输出相加, DenseNet在通道维上连结输⼊与输出。
- DenseNet的主要构建模块是稠密块和过渡层。
DenseNet实现
import torch
import torch.nn as nn
import torchvision
print("PyTorch Version: ",torch.__version__)
print("Torchvision Version: ",torchvision.__version__)
__all__ = ['DenseNet121', 'DenseNet169','DenseNet201','DenseNet264']
def Conv1(in_planes, places, stride=2):
return nn.Sequential(
nn.Conv2d(in_channels=in_planes,out_channels=places,kernel_size=7,stride=stride,padding=3, bias=False),
nn.BatchNorm2d(places),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
)
class _TransitionLayer(nn.Module):
def __init__(self, inplace, plance):
super(_TransitionLayer, self).__init__()
self.transition_layer = nn.Sequential(
nn.BatchNorm2d(inplace),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=inplace,out_channels=plance,kernel_size=1,stride=1,padding=0,bias=False),
nn.AvgPool2d(kernel_size=2,stride=2),
)
def forward(self, x):
return self.transition_layer(x)
class _DenseLayer(nn.Module):
def __init__(self, inplace, growth_rate, bn_size, drop_rate=0):
super(_DenseLayer, self).__init__()
self.drop_rate = drop_rate
self.dense_layer = nn.Sequential(
nn.BatchNorm2d(inplace),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=inplace, out_channels=bn_size * growth_rate, kernel_size=1, stride=1, padding=0, bias=False),
nn.BatchNorm2d(bn_size * growth_rate),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=bn_size * growth_rate, out_channels=growth_rate, kernel_size=3, stride=1, padding=1, bias=False),
)
self.dropout = nn.Dropout(p=self.drop_rate)
def forward(self, x):
y = self.dense_layer(x)
if self.drop_rate > 0:
y = self.dropout(y)
return torch.cat([x, y], 1)
class DenseBlock(nn.Module):
def __init__(self, num_layers, inplances, growth_rate, bn_size , drop_rate=0):
super(DenseBlock, self).__init__()
layers = []
for i in range(num_layers):
layers.append(_DenseLayer(inplances + i * growth_rate, growth_rate, bn_size, drop_rate))
self.layers = nn.Sequential(*layers)
def forward(self, x):
return self.layers(x)
class DenseNet(nn.Module):
def __init__(self, init_channels=64, growth_rate=32, blocks=[6, 12, 24, 16],num_classes=1000):
super(DenseNet, self).__init__()
bn_size = 4
drop_rate = 0
self.conv1 = Conv1(in_planes=3, places=init_channels)
num_features = init_channels
self.layer1 = DenseBlock(num_layers=blocks[0], inplances=num_features, growth_rate=growth_rate, bn_size=bn_size, drop_rate=drop_rate)
num_features = num_features + blocks[0] * growth_rate
self.transition1 = _TransitionLayer(inplace=num_features, plance=num_features // 2)
num_features = num_features // 2
self.layer2 = DenseBlock(num_layers=blocks[1], inplances=num_features, growth_rate=growth_rate, bn_size=bn_size, drop_rate=drop_rate)
num_features = num_features + blocks[1] * growth_rate
self.transition2 = _TransitionLayer(inplace=num_features, plance=num_features // 2)
num_features = num_features // 2
self.layer3 = DenseBlock(num_layers=blocks[2], inplances=num_features, growth_rate=growth_rate, bn_size=bn_size, drop_rate=drop_rate)
num_features = num_features + blocks[2] * growth_rate
self.transition3 = _TransitionLayer(inplace=num_features, plance=num_features // 2)
num_features = num_features // 2
self.layer4 = DenseBlock(num_layers=blocks[3], inplances=num_features, growth_rate=growth_rate, bn_size=bn_size, drop_rate=drop_rate)
num_features = num_features + blocks[3] * growth_rate
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc = nn.Linear(num_features, num_classes)
def forward(self, x):
x = self.conv1(x)
x = self.layer1(x)
x = self.transition1(x)
x = self.layer2(x)
x = self.transition2(x)
x = self.layer3(x)
x = self.transition3(x)
x = self.layer4(x)
x = self.avgpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
def DenseNet121():
return DenseNet(init_channels=64, growth_rate=32, blocks=[6, 12, 24, 16])
def DenseNet169():
return DenseNet(init_channels=64, growth_rate=32, blocks=[6, 12, 32, 32])
def DenseNet201():
return DenseNet(init_channels=64, growth_rate=32, blocks=[6, 12, 48, 32])
def DenseNet264():
return DenseNet(init_channels=64, growth_rate=32, blocks=[6, 12, 64, 48])
if __name__=='__main__':
# model = torchvision.models.densenet121()
model = DenseNet121()
print(model)
input = torch.randn(1, 3, 224, 224)
out = model(input)
print(out.shape)
IX. MobileNets
深可分解卷积
MobileNet,正如其名,这是一个非常简单快速并且准确率也不错的CNN网络结构,它大大减少了网络层的参数数量,使得网络的前向传播和后向传播的运算量大幅减少,最终成为了一个效率极高的CNN网络。
标准卷积层输入 $D_F \times D_F \times M$ 的特征映射 $F$ 和生成一个 $D_F \times D_F \times N$ (这里论文应该是标记有误,下标应该是G)的特征映射G,其中 $D_F$ 是输入特征映射的宽度和高度,M 是输入的通道数(输入深度),$D_G$ 是输出特征映射的宽度和高度,N是输出的通道数(输出深度)。
标准卷积层卷积核 $K$ 的大小是 $D_K \times D_K \times M \times N$,其中 $D_K$ 认为是方框的维度,M是输入通道,N是提前定义的输出通道。
标准卷积在stride和padding给定的计算输出的特征映射的公式为:
\[ \mathbf{G}_{k, l, n}=\sum_{i, j, m} \mathbf{K}_{i, j, m, n} \cdot \mathbf{F}_{k+i-1, l+j-1, m} \]
计算开销是:
\[ D_K \times D_K \times M \times N \times D_F \times D_F \]
其中输入通道M,输出通道N,卷积核滤波器大小 $D_K \times D_K$ 和输入特征大小 $D_F \times D_F$。MobileNet的可分解卷积先去打破公式里面输出通道N和卷积核 $D_F \times D_F \times M$ 的这个部分。
标准卷积中卷积核的大小和联合特征影响生成新特征的操作,但其实滤波过程和特征结合可以分成两个步骤,这样能减少各方面的计算开销。
深可分解卷积有两层组成,深卷积和逐点(点态)卷积。用深卷积对每个输入通道(输入深度)应用单滤波器。逐点卷积就是一个标准的 $1\times 1$卷积,其中使用的深度是由要输出的深度决定的。MobileNets在这两种层上都应用BN和ReLU非线性单元。
深卷积网络对每个输入通道进行滤波可以被写为:
\[ \hat{\mathbf{G}}_{k, l, m}=\sum_{i, j} \hat{\mathbf{K}}_{i, j, m} \cdot \mathbf{F}_{k+i-1, l+j-1, m} \]
其中 $\tilde{K}$ 是深卷积和的大小为 $D_K \times D_K \times M$,第 m 个滤波器 $\tilde{K}$ 应用在输入特征 F 的第 m 个通道,生成输出特征映射 $\tilde{G}$ 的第 M 个通道。
深卷积的计算开销为:
\[ D_{K} \times D_{K} \times M \times D_{F} \times D_{F} \]
深卷积比标准卷积高效的多,然而它只对输入进行了滤波,还没有将所有的结果结合形成一个新的特征,所以一个附加的层用 $1\times 1$ 的卷积得把这些过滤器得到的特征结合在一起形成一个新的多通道特征。
所以整个深可分解卷积开销为:
\[ D_{K} \times D_{K} \times M \times D_{F} \times D_{F}+M \times N \times D_{F} \times D_{F} \]
减少的开销:
\[ \begin{aligned}
& \frac{D_{K} \cdot D_{K} \cdot M \cdot D_{F} \cdot D_{F}+M \cdot N \cdot D_{F} \cdot D_{F}}{D_{K} \cdot D_{K} \cdot M \cdot N \cdot D_{F} \cdot D_{F}} \\
=& \frac{1}{N}+\frac{1}{D_{K}^{2}}
\end{aligned}\]
可知一个 $3\times 3$ 的卷积用本文的方法在精度没减少多少的情况下开销是原先的1/8到1/9。
MobileNet 模型

宽乘法器
下图是正常的卷积和深可分解卷积的对比图。下采样用stride调节大小的卷积层代替,最后的平均pooling在全连接前将分辨率降到1,将深卷积和逐点卷积都算上,MobileNet28层。
MobileNet模型架构确实小又低延时,但是应用啥的可能需要更小更快的,所以介绍下一个简单的参数 $\alpha$,叫它宽乘法器,它角色扮演就是让每一层网络瘦一定的比例,例如输入层 $M$ 就变为 $\alpha M$,输出层 $N$ 就变为 $\alpha N$。深可分解网络开销就变为:
\[ D_{K} \times D_{K} \times \alpha M \times D_{F} \times D_{F}+\alpha M \times \alpha N \times D_{F} \times D_{F} \]
其中 $\alpha$ 属于0~1中间,这个计算开销和参数数量接近 $\alpha^2$ 的减少。应用了 $\alpha$ 后模型需要再训练的。
分辨率乘法器
从输入图片到每一层特征上都用这个分辨率乘法器 $\rho$ 数都用,减少输出的特征,减少了开销。
\[ D_{K} \times D_{K} \times \alpha M \times \rho D_{F} \times \rho D_{F}+\alpha M \times \alpha N \times \rho D_{F} \times \rho D_{F}\]
MobileNet实现
import torch
import torch.nn as nn
import torchvision
def BottleneckV1(in_channels, out_channels, stride):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels,out_channels=in_channels,kernel_size=3,stride=stride,padding=1,groups=in_channels),
nn.BatchNorm2d(in_channels),
nn.ReLU6(inplace=True),
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1),
nn.BatchNorm2d(out_channels),
nn.ReLU6(inplace=True)
)
class MobileNetV1(nn.Module):
def __init__(self, num_classes=1000):
super(MobileNetV1, self).__init__()
self.first_conv = nn.Sequential(
nn.Conv2d(in_channels=3,out_channels=32,kernel_size=3,stride=2,padding=1),
nn.BatchNorm2d(32),
nn.ReLU6(inplace=True),
)
self.bottleneck = nn.Sequential(
BottleneckV1(32, 64, stride=1),
BottleneckV1(64, 128, stride=2),
BottleneckV1(128, 128, stride=1),
BottleneckV1(128, 256, stride=2),
BottleneckV1(256, 256, stride=1),
BottleneckV1(256, 512, stride=2),
BottleneckV1(512, 512, stride=1),
BottleneckV1(512, 512, stride=1),
BottleneckV1(512, 512, stride=1),
BottleneckV1(512, 512, stride=1),
BottleneckV1(512, 512, stride=1),
BottleneckV1(512, 1024, stride=2),
BottleneckV1(1024, 1024, stride=1),
)
self.avg_pool = nn.AvgPool2d(kernel_size=7,stride=1)
self.linear = nn.Linear(in_features=1024,out_features=num_classes)
self.dropout = nn.Dropout(p=0.2)
self.softmax = nn.Softmax(dim=1)
self.init_params()
def init_params(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight)
nn.init.constant_(m.bias,0)
elif isinstance(m, nn.Linear) or isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.first_conv(x)
x = self.bottleneck(x)
x = self.avg_pool(x)
x = x.view(x.size(0),-1)
x = self.dropout(x)
x = self.linear(x)
out = self.softmax(x)
return out
if __name__=='__main__':
model = MobileNetV1()
print(model)
input = torch.randn(1, 3, 224, 224)
out = model(input)
print(out.shape)
X. ShuffleNet
Shuffle模型
ShuffleNet是Face++提出的一种轻量化网络结构,主要思路是使用Group convolution和Channel shuffle改进ResNet,可以看作是ResNet的压缩版本。

假设输入feature为 $H\times W \times C$, 所有的 $1\times 1$ 卷积数为 C , $3\times 3$ Depthwise卷积数为 $k$,Group convolution都分为 $g$ 组。相比原始的ResNet缩小了超级多的计算量。所以ShuffleNet相当于保留ResNet结构,同时又压低计算量的改进版。
ShuffleNet的本质是将卷积运算限制在每个Group内,这样模型的计算量取得了显著的下降。然而导致模型的信息流限制在各个Group内,组与组之间没有信息交换,如图15,这会影响模型的表示能力。因此,需要引入组间信息交换的机制,即Channel Shuffle操作。同时Channel Shuffle是可导的,可以实现end-to-end一次性训练网络。

Shuffle实现
import torch
import torch.nn as nn
import torchvision
def Conv3x3BNReLU(in_channels,out_channels,stride,groups):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=3, stride=stride, padding=1,groups=groups),
nn.BatchNorm2d(out_channels),
nn.ReLU6(inplace=True)
)
def Conv1x1BNReLU(in_channels,out_channels,groups):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1,groups=groups),
nn.BatchNorm2d(out_channels),
nn.ReLU6(inplace=True)
)
def Conv1x1BN(in_channels,out_channels,groups):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1,groups=groups),
nn.BatchNorm2d(out_channels)
)
class ChannelShuffle(nn.Module):
def __init__(self, groups):
super(ChannelShuffle, self).__init__()
self.groups = groups
def forward(self, x):
'''Channel shuffle: [N,C,H,W] -> [N,g,C/g,H,W] -> [N,C/g,g,H,w] -> [N,C,H,W]'''
N, C, H, W = x.size()
g = self.groups
return x.view(N, g, int(C / g), H, W).permute(0, 2, 1, 3, 4).contiguous().view(N, C, H, W)
class ShuffleNetUnits(nn.Module):
def __init__(self, in_channels, out_channels, stride,groups):
super(ShuffleNetUnits, self).__init__()
self.stride = stride
out_channels = out_channels - in_channels if self.stride >1 else out_channels
mid_channels = out_channels // 4
self.bottleneck = nn.Sequential(
Conv1x1BNReLU(in_channels, mid_channels,groups),
ChannelShuffle(groups),
Conv3x3BNReLU(mid_channels, mid_channels, stride,groups),
Conv1x1BN(mid_channels, out_channels,groups)
)
if self.stride>1:
self.shortcut = nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
self.relu = nn.ReLU6(inplace=True)
def forward(self, x):
out = self.bottleneck(x)
out = torch.cat([self.shortcut(x),out],dim=1) if self.stride >1 else (out + x)
return self.relu(out)
class ShuffleNet(nn.Module):
def __init__(self, planes, layers, groups, num_classes=1000):
super(ShuffleNet, self).__init__()
self.stage1 = nn.Sequential(
Conv3x3BNReLU(in_channels=3,out_channels=24,stride=2, groups=1),
nn.MaxPool2d(kernel_size=3,stride=2,padding=1)
)
self.stage2 = self._make_layer(24,planes[0], groups, layers[0], True)
self.stage3 = self._make_layer(planes[0],planes[1], groups, layers[1], False)
self.stage4 = self._make_layer(planes[1],planes[2], groups, layers[2], False)
self.global_pool = nn.AvgPool2d(kernel_size=7, stride=1)
self.dropout = nn.Dropout(p=0.2)
self.linear = nn.Linear(in_features=planes[2]*7*7, out_features=num_classes)
self.init_params()
def _make_layer(self, in_channels,out_channels, groups, block_num, is_stage2):
layers = []
layers.append(ShuffleNetUnits(in_channels=in_channels, out_channels=out_channels, stride=2, groups=1 if is_stage2 else groups))
for idx in range(1, block_num):
layers.append(ShuffleNetUnits(in_channels=out_channels, out_channels=out_channels, stride=1, groups=groups))
return nn.Sequential(*layers)
def init_params(self):
for m in self.modules():
if isinstance(m,nn.Conv2d):
nn.init.kaiming_normal_(m.weight)
nn.init.constant_(m.bias,0)
elif isinstance(m, nn.BatchNorm2d) or isinstance(m, nn.Linear):
nn.init.constant_(m.weight,1)
nn.init.constant_(m.bias, 0)
def forward(self, x):
x = self.stage1(x)
x = self.stage2(x)
x = self.stage3(x)
x = self.stage4(x)
x = x.view(x.size(0), -1)
x = self.dropout(x)
out = self.linear(x)
return out
def shufflenet_g8(**kwargs):
planes = [384, 768, 1536]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=8)
return model
def shufflenet_g4(**kwargs):
planes = [272, 544, 1088]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=4)
return model
def shufflenet_g3(**kwargs):
planes = [240, 480, 960]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=3)
return model
def shufflenet_g2(**kwargs):
planes = [200, 400, 800]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=2)
return model
def shufflenet_g1(**kwargs):
planes = [144, 288, 576]
layers = [4, 8, 4]
model = ShuffleNet(planes, layers, groups=1)
return model
if __name__ == '__main__':
model = shufflenet_g1()
print(model)
input = torch.randn(1, 3, 224, 224)
out = model(input)
print(out.shape)
XI. 参考链接
- Modern Convolutional Neural Networks
- Gradient-based learning applied to document recognition
- ImageNet Classification with Deep Convolutional Neural Networks
- Very Deep Convolutional Networks for Large-Scale Image Recognition
- Network In Network
- Going deeper with convolutions
- Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift
- Deep Residual Learning for Image Recognition
- Densely Connected Convolutional Networks
- MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications
- ShuffleNet: An Extremely Efficient Convolutional Neural Network for Mobile Devices
Comments | NOTHING