(1)VGG16:
先给出架构图:
实现:
class VGG16(nn.Module):
def __init__(self):
super().__init__()
self.feature_part = 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_part = 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.feature_part(x)
x = x.view(-1,512*7*7)
output = self.clf_part(x)
return output
这里与前面的实现方式略有不同,这次使用sequential函数统一装起来实现。把全连接层前面的卷积层和池化层的重复定义为特征提取部分,后面定义为分类输出部分。其他部分没什么太大区别,按照架构图上的参数填就可以了。
下面是用torchinfo的结果:
(2)NIN:
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(kernel_size=3,stride=2),
nn.Dropout(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(kernel_size=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
torchinfo结果:
从参数量可以看出NIN并不算一个性能很优秀的架构,但是NIN最大的优点就是让人们看到了1*1卷积核和用卷积层去代替线性层的可能性,为后面的GoogleNet提供了灵感来源。
(3)GoogleNet:
前面的架构都很优秀,但是下面我们要花比较多的篇幅来讲googlenet。
我们前面讲到的VGG非常优秀,但它在比赛上拿到的最高名次是亚军,而力压群雄拿到冠军的是
在ImageNet数据集上达到6.7%错误率的GoogLeNet。GoogLeNet由谷歌团队与众多大学合作研发,发表于论文《Going deeper with convolutions》。受到NiN网络的启发,谷歌引入了一种全新的网络架构:Inception block,并将使用Inception V1的网络架构称为GoogLeNet。Inception是电影《盗梦空间》的英文名称,很有趣的一点是该篇论文第一篇参考文献就是盗梦空间表情包出处。
在具体讲Inception块的理念之前,我们先回顾一下当时主流方法都有什么弊端。我们还是拿VGG鞭尸举例子,最让人老生常谈的就是VGG的参数量太多,已经到达一亿多的级别,各层之间的连接过于稠密,计算量太大很容易过拟合。为了解决这个问题,当时主流方法有下面几种:
1.使用分组卷积,用卷积代替全连接层等很多技巧办法用来减少神经元或者特征图之间的联系,让网络变得更加稀疏,不再变得这么稠密。
2.通过引入dropout的随机性来减少过拟合的情况,从某种角度讲也是一种随机的稀疏性
3.通过使用GPU来解决计算量大的问题
AlexNet和VGG都主要使用了2和3,NIN主要使用了1,但是都没有完全解决。使用分组卷积虽然能让网络变得稀疏,但是网络的学习能力会由于丢掉信息从而出现学习率波动甚至下降。使用dropout引入随机的稀疏和现代GPU架构的计算原理是冲突的,会造成大量的数据查找和缓存缺失带来的时间延迟。现代计算机天然的适合计算密集型分布类似的数据,所以其实并不适合稀疏性。
所以为了解决稀疏性与稠密的矛盾,谷歌给出的思路就是既要又要,顾名思义就是既要结构稀疏,又要计算稠密:用普通卷积层,池化层这些稠密元素组成
的块去逼近
一个稀疏架构,从而构造一种参数量与稀疏网络相似的稠密网络。
基于这样的理念,在经历过很多试验后首先搞出一个初始版本inception块如下:

但是这样的话,参数由于最后还要并在一起就会越来越厚,并不可行,这时候就受到了NIN网络的启发,他们看到1*1卷积核的妙用,又搞出了第二个版本:

其中具体的结构如下,其中图片的大小并不是28*28,这里只是举例:

下面来分析一下这种块有什么优势:
1.使用不同大小的卷积核可以提取到各个类型层次的信息,你也不需要纠结用什么尺寸的卷积核了,谷歌说我都要用。
2.原来串联的过程现在变成了并联,充分释放了计算效率。
3.最后就是第二个版本和前面版本的区别:1*1卷积核,那么这个卷积核是如何工作的,如下图:

具体的来说有以下几个好处:
1.能够很轻松的调整通道数,让各个层之间的连接变得更加自然
2.能够减少参数量,就像前面NIN使用的那样,我们可以先降下来卷积最后再升上去
3.能够跨通道信息交融,如上图最后生成的每一个小方块都是前一层每一个通道加权得来
4.由于不改变特征图的尺寸,所以还能够增加模型深度提高非线性表达能力。
最后我们给出完整的架构图:

下面开始实现部分:
由于模型结构过于复杂,所以我们需要定义基本元素单位,下面我们先定义卷积包和inception块
卷积包:
class BasicConv(nn.Module):
def __init__(self,in_channels, out_channels,**kwargs):
super().__init__()
self.conv = nn.Sequential(nn.Conv2d(in_channels, out_channels, bias=False, **kwargs),
nn.BatchNorm2d(out_channels),
nn.ReLU(inplace=True))
def forward(self,x):
x = self.conv(x)
return x
由于当时那时候batchnormalization技术还没有出来,但是后来都陆续使用上了,所以这里我们也用上,另外由于后续还有可能使用到其他参数,所以加上了**kwargs。
下面定义inception块:
class Inception(nn.Module):
def __init__(self
,in_channels : int
,ch1x1 : int
,ch3x3red : int
,ch3x3 : int
,ch5x5red : int
,ch5x5 : int
,pool_proj : int
):
super().__init__()
#1x1
self.branch1 = BasicConv(in_channels,ch1x1,kernel_size=1)
#1x1 + 3x3
self.branch2 = nn.Sequential(BasicConv(in_channels, ch3x3red, kernel_size=1)
,BasicConv(ch3x3red, ch3x3, kernel_size=3,padding=1))
#1x1 + 5x5
self.branch3 = nn.Sequential(BasicConv(in_channels, ch5x5red, kernel_size=1)
,BasicConv(ch5x5red, ch5x5, kernel_size=5, padding=2))
#pool + 1x1
self.branch4 = nn.Sequential(nn.MaxPool2d(kernel_size=3,stride=1, padding=1,ceil_mode=True)
,BasicConv(in_channels,pool_proj,kernel_size=1))
def forward(self,x):
branch1 = self.branch1(x)
branch2 = self.branch2(x)
branch3 = self.branch3(x)
branch4 = self.branch4(x)
outputs = [branch1, branch2, branch3, branch4]
return torch.cat(outputs, 1) #合并
需要说明的是传入的参数在开始那张表中都有,我们写的时候只需要照着抄数字就行。
另外还有一点没有提及的是,googlenet架构有三个输出,其中还有两个辅助分类器,最后再按照一定的权重输出的,所以我们还需要定义一个辅助分类器:
class AuxClf(nn.Module):
def __init__(self,in_channels : int, num_classes : int, **kwargs):
super().__init__()
self.feature_ = nn.Sequential(nn.AvgPool2d(kernel_size=5,stride=3)
,BasicConv(in_channels,128, kernel_size=1))
self.clf_ = nn.Sequential(nn.Linear(4*4*128, 1024)
,nn.ReLU(inplace=True)
,nn.Dropout(0.7)
,nn.Linear(1024,num_classes))
def forward(self,x):
x = self.feature_(x)
x = x.view(-1,4*4*128)
x = self.clf_(x)
return x
最后定义GooleNet大类:
class GoogLeNet(nn.Module):
def __init__(self,num_classes: int = 1000):
super().__init__()
#block1
self.conv1 = BasicConv(3,64,kernel_size=7,stride=2,padding = 3)
self.maxpool1 = nn.MaxPool2d(kernel_size=3,stride=2,ceil_mode = True)
#block2
self.conv2 = BasicConv(64,64,kernel_size=1)
self.conv3 = BasicConv(64,192,kernel_size=3, padding = 1)
self.maxpool2 = nn.MaxPool2d(kernel_size=3,stride=2,ceil_mode = True)
#block3
self.inception3a = Inception(192,64,96,128,16,32,32)
self.inception3b = Inception(256,128,128,192,32,96,64)
self.maxpool3 = nn.MaxPool2d(kernel_size=3,stride=2,ceil_mode = True)
#block4
self.inception4a = Inception(480,192,96,208,16,48,64)
self.inception4b = Inception(512,160,112,224,24,64,64)
self.inception4c = Inception(512,128,128,256,24,64,64)
self.inception4d = Inception(512,112,144,288,32,64,64)
self.inception4e = Inception(528,256,150,320,32,128,128)
self.maxpool4 = nn.MaxPool2d(kernel_size=3,stride=2,ceil_mode = True)
#block5
self.inception5a = Inception(832,256,160,320,32,128,128)
self.inception5b = Inception(832,384,192,384,48,128,128)
#clf
self.avgpool = nn.AdaptiveAvgPool2d((1,1))
self.dropout = nn.Dropout(0.4)
self.fc = nn.Linear(1024,num_classes)
#auxclf
self.aux1 = AuxClf(512, num_classes) #4a
self.aux2 = AuxClf(528, num_classes) #4d
def forward(self,x):
#block1
x = self.maxpool1(self.conv1(x))
#block2
x = self.maxpool2(self.conv3(self.conv2(x)))
#block3
x = self.inception3a(x)
x = self.inception3b(x)
x = self.maxpool3(x)
#block4
x = self.inception4a(x)
aux1 = self.aux1(x)
x = self.inception4b(x)
x = self.inception4c(x)
x = self.inception4d(x)
aux2 = self.aux2(x)
x = self.inception4e(x)
x = self.maxpool4(x)
#block5
x = self.inception5a(x)
x = self.inception5b(x)
#clf
x = self.avgpool(x)
x = torch.flatten(x,1)
x = self.dropout(x)
x = self.fc(x)
return x, aux2, aux1
torchinfo结果: