2014年的Imagenet不太寻常,GoogLeNet(我们这儿先来讨论GoogLeNet Incepetion V1)与VGG一道成为当年的冠军。当然了,他们都做到了more deeper(Alexnet只有8层),但是也有一些不同的地方,VGG貌似更为直接的继承了AlexNet的框架,但是GoogLeNet却做了更加大胆的尝试,不仅仅做到了more deeper,而且more wider,虽然层数也只有22层,但是相对于AlexNet与VGG而言,在大小上还是小了很多。下面我们来看看它是怎么go deeper and wider but more smaller的。
说点题外话,虽然GoogLeNet取名为“GoogLeNet”而非“GoogleNet”,是向LeNet致敬,但不得不承认的一点就是网络结构里很难看到LeNet的影子。:)
我们大致清楚,增加网络的深度和宽度是直接提升网络性能的两个方法,而文章中也提出:获得高质量模型最保险的做法就是增加模型的深度(层数)或者是其宽度(层核或者神经元数)。但是不管哪一种方法带来的都是参数的增加,带来的后果就是计算量的增加,而且复杂的模型也将容易过拟合、难以优化。
我们先来将GoogLeNet的结构可视化一把:
不好意思这个有点长,但是我也没办法啊(谁叫它本身就张那样),不过请大家注意了,整个网络是由很多个红框框这样的就够重复组合而成的,现在再回去看一波,是不是对这个结构了就一目了然了O(∩_∩)O。
我们把红色方框的部分抽离出来,另外,再给它起个名字(当然只有Szegedy才有资格咯):Incepetion
可以看到Inception里有四个并行的线路。
- 单个 1×1卷积。
- 1×1 卷积接上 3×3 卷积。通常前者的通道数少于输入通道,这样减少后者的计算量。后者加上了padding=1使得输出的长宽的输入一致(看不懂?没关系的,我在后面说明一下让你秒懂)
- 同2,但换成了 5×5 卷积
- 和1类似,但卷积前用了最大池化层
最后将这四个并行线路的结果在通道这个维度上Concat(就是矩阵拼接)在一起。
大家可能就会有疑问了,为何要在input出来的时候加个1×1卷积,1×1的kennel去卷积不是啥事儿都没做嘛?不急,听我慢慢分析。我们先来做个假设,假设这样干:
就是先把那个1×1卷积给他抹掉,那么就是直接使用1×1、3×3以及5×5的卷积核对input进行操作,但是这样仍然会带来巨大的计算量(还不明白?接着看)。我们最开始提到过,GoogLeNet取名为“GoogLeNet”而非“GoogleNet”,是向LeNet致敬,但不得不承认的一点就是网络结构里很难看到LeNet的影子,其实它是Draw lessons from NIN,具体就是采用1×1卷积核来进行降维,这样,1x1的卷积核起到了降低feature map厚度、减少计算量的作用。
大家可能还是不太明白吧,没关系,咋来举个例子:
我们以3×3这个 path 为例,假设 input(也就是上层的 output)为128×100×100(channel×height×width,batch size略去,没写,事实上,batch size是不影响计算的):
- 不使用1×1的卷积,那么经过 kennel = 3×3,channel = 256,stride=1,pad=1 的卷积之后,其 output:256×100×100,那么其参数数量128×256×3×3
- 若是先将 output 经过 kennel = 1×1,channel = 64,的卷积经之后,再将 1×1 Conv 的 output 抛进 3×3 卷积层,还是设3×3 Conv 的kennel = 3×3,channel = 256,stride=1,pad=1。但参数数量将减少为128×64×1×1+64×256×3×3,参数将近减少了一半,而你把这个放到5×5 的那条path上面去推算一下,可能会更明显!你说腻害不腻害
现在大家明白了这个1×1的卷积层是拿来干啥的了吧
大家都 get 到这个 point 以后,我们现在再来对这个 “Incepetion模块” 进行一波总结:
- 采用了1×1、3×3以及5×5的不同的kennel,那么神经元将看到不同尺度的信息,最后的 concat 也就将不同尺度的features加以结合了;
- 1×1 卷积接上 3×3 卷积。将前者的通道数设置为少于输入通道,这样减少后者的计算量。而后者加上了padding=1使得输出的长宽与输入一致,5×5 的那个是一样的道理,padding=2 而已。输出的长宽与输入一致确保最后能 concat 在一起
- 文章说很多地方都表明pooling挺有效,所以Inception里面也嵌入了。至于pooling嘛,原文解释得不是很详细,只说了效果好,所以放上去了 ,大家有什么看法也可以留言交流
- 最后,网络使用的是average pooling,而不是全连接,结果当然要好不然人家也不这么干,而在最后它还是使用linear线性层,这么干是为了方便fine-tuning模型。
这样说大家就不会打我脸了吧(●ˇ∀ˇ●)
我们再来回归原点,讨论整个网络的思路go more deeper,go more wider:
- 深度,层数更深,文章采用了22层,为了避免上述提到的梯度消失问题(在反向传播更新参数时,越靠近输入梯度越小),GoogLeNet 巧妙的在不同深度处增加了两个loss来保证梯度回传消失的现象。
-
宽度,增加了多种核1x1,3x3,5x5,还有直接max pooling的。
但是如果简单的将这些应用到feature map上的话,concat起来的feature map厚度将会很大,所以在googlenet中为了避免这一现象提出的inception具有如下结构,在3x3前,5x5前,max pooling后分别加上了1x1的卷积核起到了降低feature map厚度的作用
以上我们其实都是介绍的是more wider,那么这儿再来说说如何解决more deeper的一些问题,我们看到GoogLeNet一共堆砌了多个Incepetion模块使得网络加深,因此在BP(Back Propagation )的时候梯度弥散的情况是存在的,而GoogLeNet里面的softmax0、softmax1就是来解决这个问题的具体就是在训练的时候,他们的损失误差乘以一个权值(GoogLeNet里设置为0.3)加到整体损失中。在应用的时候,这两个辅助分类器会被丢掉。GoogLeNet的实验表明,只需要一个辅助分类器就可以达到同样的效果(提升0.5%)。
补充一下,以后我还会接着介绍它的其他几个版本
下面我们就来看看大家最关心的实践吧(Mxnet+Gluon)O(∩_∩)O
首先我们定义Inception模块
from mxnet.gluon import nn
from mxnet import nd
class Inception(nn.Block):
def __init__(self, n1_1, n2_1, n2_3, n3_1, n3_5, n4_1, **kwargs):
# n*_* : the output of every path, for exzample
# n2_1 is the output of Conv 1*1 of path 2 & n2_3 is the output of Conv 3*3 of path 2
super(Inception, self).__init__(**kwargs)
# path 1
self.p1_conv_1 = nn.Conv2D(n1_1, kernel_size=1,
activation='relu')
# path 2
self.p2_conv_1 = nn.Conv2D(n2_1, kernel_size=1,
activation='relu')
self.p2_conv_3 = nn.Conv2D(n2_3, kernel_size=3, padding=1,
activation='relu')
# path 3
self.p3_conv_1 = nn.Conv2D(n3_1, kernel_size=1,
activation='relu')
self.p3_conv_5 = nn.Conv2D(n3_5, kernel_size=5, padding=2,
activation='relu')
# path 4
self.p4_pool_3 = nn.MaxPool2D(pool_size=3, padding=1,
strides=1)
self.p4_conv_1 = nn.Conv2D(n4_1, kernel_size=1,
activation='relu')
# define the forward propagation
def forward(self, x):
p1 = self.p1_conv_1(x)
p2 = self.p2_conv_3(self.p2_conv_1(x))
p3 = self.p3_conv_5(self.p3_conv_1(x))
p4 = self.p4_conv_1(self.p4_pool_3(x))
# concatenate the output of four paths together at the first dimension (channel)
return nd.concat(p1, p2, p3, p4, dim=1)
接着,我们就可以用这个Inception模块去构造GoogLeNet了
class GoogLeNet(nn.Block):
def __init__(self, num_classes, verbose=False, **kwargs):
super(GoogLeNet, self).__init__(**kwargs)
self.verbose = verbose
# add name_scope on the outer most Sequential
with self.name_scope():
# block 1
b1 = nn.Sequential()
b1.add(
nn.Conv2D(64, kernel_size=7, strides=2,
padding=3, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2)
)
# block 2
b2 = nn.Sequential()
b2.add(
nn.Conv2D(64, kernel_size=1),
nn.Conv2D(192, kernel_size=3, padding=1),
nn.MaxPool2D(pool_size=3, strides=2)
)
# block 3
b3 = nn.Sequential()
b3.add(
Inception(64, 96, 128, 16,32, 32),
Inception(128, 128, 192, 32, 96, 64),
nn.MaxPool2D(pool_size=3, strides=2)
)
# block 4
b4 = nn.Sequential()
b4.add(
Inception(192, 96, 208, 16, 48, 64),
Inception(160, 112, 224, 24, 64, 64),
Inception(128, 128, 256, 24, 64, 64),
Inception(112, 144, 288, 32, 64, 64),
Inception(256, 160, 320, 32, 128, 128),
nn.MaxPool2D(pool_size=3, strides=2)
)
# block 5
b5 = nn.Sequential()
b5.add(
Inception(256, 160, 320, 32, 128, 128),
Inception(384, 192, 384, 48, 128, 128),
nn.AvgPool2D(pool_size=2)
)
# block 6
b6 = nn.Sequential()
b6.add(
nn.Flatten(),
nn.Dense(num_classes)
)
# chain blocks together
self.net = nn.Sequential()
self.net.add(b1, b2, b3, b4, b5, b6)
# define the forward propagation,
def forward(self, x):
out = x
for i, b in enumerate(self.net):
out = b(out)
if self.verbose:
print('Block %d output: %s'%(i+1, out.shape))
return out
这就是整个GoogLeNet的Structure了