1 前言

各位朋友大家好,欢迎来到月来客栈。在​​上一篇文章​​中,笔者花了很大的篇幅介绍完了GoogLeNet中的核心部分Inception模块。其本质上来说就是采用了不同尺度的卷积核对输入进行特征提取,然后再将各个部分得到的结果进行组合,最后在输入到下一层的网络中。只是作者选择了从另外一个相对晦涩的角度来进行切入,解释了Inception结构的目的以及合理性。

在接下来的这篇文章中,就让我们来看看GoogLeNet是如何通过Inception堆叠形成的,并且还在ILSVRC2014上大获成功。

2 GoogLeNet

首先我们需要知道的是在2014的ILSVR比赛中,谷歌使用的其实不止是一个模型,而是七个GooLeNet模型(每个模型均是以Inception为block构成)的ensemble。总的来说其中六个模型的网络结构一模一样,仅仅只是在训练的时候采用了不同的采样策略和输入顺序;而另外一个网络结构和其它的六个基本上差不多,只是在网络的中间部分也插入了一些分类器。


We independently trained 7 versions of the same GoogLeNet model (including one wider version), and performed ensemble prediction with them. These models were trained with the same initialization and learning rate policies. They differed only in sampling methodologies and the randomized input image order.


2.1 Most Common Instance of Inception

现在我们先来看看最普通,也就是那六个结构一样的网络模型。这个模型一共有22层(仅包含参数层),如果加上池化层就有27层。下面我们就来看看其到底长什么样。

GoogLeNet: Going deeper with convolutions_2d

图 1. 22层GoogLeNet参数图

如图1所示就是这个最基本的GoogLeNet网络结构的参数表了。咋一看是不是根本没看懂?很多人以为这张表对应的就是论文中的那个网络结构图,其实事实上并不是。在这里笔者就直接给出这个参数表所对应的网络图。不过图1中的params列给出的参数量好像有点问题(例如第一个卷积层的参数量应该是 7 × 7 × 3 × 64 ≈ 9400 7\times7\times3\times64\approx9400 7×7×3×649400),有兴趣的朋友也可以自己去研究一番。


GoogLeNet: Going deeper with convolutions_ide_02

图 2. 22层GoogLeNet网络结构图

如图2所示就是这个22层网络的结构图了。在图里,笔者几乎标出了所有输入输出和参数的相关信息,其中S表示步长,P表示填充的圈数,@后面的数字表示对应卷积核的个数。看到没,还别说图1中的这种表格形式应该是最简洁的描述这个网络结构的方式,只不过在没弄懂之前看起来不知所谓。试想一下,如果是你的话你会以一种什么样的形式来呈现这个网络的参数设置?

在图2所示的网络中,所有卷积操作之后(注意是所有)都使用了ReLU激活函数。同时还可以发现,在GoogLeNet网络结构中也采用了类似NIN中通过全局平均池化来处理输出的方式,只不过这里还额外的加了一个线性层来方便处理不同的分类任务。并且作者还说到,将全连接改为全局平均后,top-1准确率还提高了 0.6 % 0.6\% 0.6%


All the convolutions, including those inside the Inception modules, use rectified linear activation. All these reduction/projection layers use rectified linear activation as well.


以上就是这个22层GoogLeNet网络模型的全部信息。接下来,我们再来看看ensemble中的另一个模型长什么样。

2.2 Adding auxiliary classifiers of Inception

在论文中第5页左下角作者说到,基于这样一个观点——在先前ILSVRC大赛中拔得头筹的网络模型都认为,处于网络模型中间层的特征往往具有更强的判别能力,GoogLeNet也在网络的中间部分额外的添加了两个分类器。这种做法通常被认为是用于克服梯度消失以及增加网络泛化能力的有效手段。也就是说,作者通过在图2所示的网络结构中,额外加入了两个分类层来构成了ensemble中的第七个模型:


GoogLeNet: Going deeper with convolutions_卷积_03

图 3. 多分类层GoogLeNet

如图3所示就是根据图2中的网络所改进得到的,其中Dropout中的值表示丢掉神经元的比例。从图3可以看到,新增的两个分类器分别将Inception(4a)和Inception(4d)的输出作为输入,然后进行分类。同时,在这个网络训练的过程中,整个模型的损失函数将会把三个部分的损失加在一起,但是额外的两个辅助分类器仅仅只有30%的损失被累加到整体损失中。当模型训练完成后,在实际的推理过程中,额外的两个辅助分类器将会被忽略掉。虽然这样做看起来会有很好的效果,但是作者说到,新增的这部分仅仅只给带来了大约 0.5 % 0.5\% 0.5%准确率的提升。最后,当七个模型都被独立的训练完成后再采用集成的策略输出最后的预测结果。当然,在这里我们肯定不能像论文中那样来进行实验了,所以后续我们还是会以前面使用过的数据集进行实验。

3 实现

在这里,我们就以最普通也就是图2中所示的网络结构为例进行实现,然后再进行图片的分类的任务。

3.1 前向传播

从前面的介绍可知,GoogLeNet是由多个Inception模块构造而成,因此自然而然我们就会想到在实现的时候我们也可先写一个Inception模块,然后再复用即可。同时,我们可以发现,在Inception内部还有一个更小的单元就是四路卷积+ReLU的操作。因此,对于整个网络的构造来说,我们可以先实现最基本卷积小模块,然后再基于卷积模块实现Inception模块,最后根据Inception来实现整个网络。

3.1.1 基本卷积

在前面笔者介绍到,GoogLeNet中的每个卷积操作后都会跟上一个ReLU的操作,因此我们可以定义这么一个类:

class BasicConv2d(nn.Module):
def __init__(self, in_channels, out_channels,
kernel_size, stride=1, padding=0):
super(BasicConv2d, self).__init__()
self.conv = nn.Conv2d(in_channels,
out_channels,
kernel_size, stride,
padding, bias=False)
self.relu = nn.ReLU(inplace=True)
def forward(self, x):
x = self.conv(x)
return self.relu(x)

这样我们就完成了卷积模块的定义。同时,由于​​BasicConv2d​​​是继承自​​nn.Module​​​的,所以在后面我们也能像使用​​nn.Conv2d​​​一样来使用​​BasicConv2d​​。

3.1.2 Inception

在实现完卷积模块后就可以开始实现Inception模块了。从图2可知,一个Inception模块主要分为四条支路,所以我们在实现的时候也可以按四个部分来分别进行:

class Inception(nn.Module):
def __init__(self, in_channels, ch1x1,
ch3x3reduce, ch3x3, ch5x5reduce, ch5x5, pool_proj):
super(Inception, self).__init__()

conv_block = BasicConv2d
self.branch1 = conv_block(in_channels, ch1x1, kernel_size=1)
self.branch2 = nn.Sequential(
conv_block(in_channels, ch3x3reduce, kernel_size=1),
conv_block(ch3x3reduce, ch3x3, kernel_size=3, padding=1))

self.branch3 = nn.Sequential(
conv_block(in_channels, ch5x5reduce, kernel_size=1),
conv_block(ch5x5reduce, ch5x5, kernel_size=5, padding=2))

self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1),
conv_block(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)

如上就是整个Inception部分的实现,其中:


  • ​in_channels​​: 表示上一层输入的通道数;
  • ​ch1x1​​:表示 1x1卷积的个数;
  • ​ch3x3reduce​​: 表示3x3卷积之前1x1卷积的个数;
  • ​ch3x3​​:表示3x3卷积的个数;
  • ​ch5x5reduce​​:表示5x5卷积之前1x1卷积的个数
  • ​ch5x5​​:表示5x5卷积的个数;
  • ​pool_proj​​:表示池化后1x1卷积的个数。

在得到四个部分的输出后,在融合到一起即可。

3.1.3 GoogLeNet

在做完前期的几个准备工作后,我们就可以来一步步的实现GoogLeNet。根据图2所示的结构,我们可以定义如下的一类:

class GoogLeNet(nn.Module):
def __init__(self, num_classes=1000):
super(GoogLeNet, self).__init__()
conv_block, inception_block = BasicConv2d, Inception

self.conv1 = conv_block(3, 64, kernel_size=7, stride=2, padding=3)
self.maxpool1 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
self.conv2 = conv_block(64,64, kernel_size=1, stride=1, padding=0)
self.conv3 = conv_block(64,192, kernel_size=3, stride=1, padding=1)
self.maxpool2 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

# in_channels, ch1x1, ch3x3reduce, ch3x3, ch5x5reduce, ch5x5, pool_proj):
self.inception3a = inception_block(192, 64, 96, 128, 16, 32, 32)
self.inception3b = inception_block(256, 128, 128, 192, 32, 96, 64)
self.maxpool3 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

self.inception4a = inception_block(480, 192, 96, 208, 16, 48, 64)
self.inception4b = inception_block(512, 160, 112, 224, 24, 64, 64)
self.inception4c = inception_block(512, 128, 128, 256, 24, 64, 64)
self.inception4d = inception_block(512, 112, 144, 288, 32, 64, 64)
self.inception4e = inception_block(528, 256, 160, 320, 32, 128, 128)
self.maxpool4 = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

self.inception5a = inception_block(832, 256, 160, 320, 32, 128, 128)
self.inception5b = inception_block(832, 384, 192, 384, 48, 128, 128)
self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
self.dropout = nn.Dropout(0.5)
self.fc = nn.Linear(1024, num_classes)

到这里,我们定义完了整个前向传播中所需要用到的功能函数,下面只需要在​​forward​​中使用这些部件完成整个前向传播过程即可:

def forward(self, x):

x = self.conv1(x)
x = self.maxpool1(x) # torch.Size([1, 64, 56, 56])

x = self.conv2(x)
x = self.conv3(x)
x = self.maxpool2(x)# torch.Size([1, 192, 28, 28])

x = self.inception3a(x)
x = self.inception3b(x)
x = self.maxpool3(x)# torch.Size([1, 480, 14, 14])

x = self.inception4a(x)
x = self.inception4b(x)
x = self.inception4c(x)
x = self.inception4d(x)
x = self.inception4e(x)
x = self.maxpool4(x) # torch.Size([1, 832, 7, 7])

x = self.inception5a(x)
x = self.inception5b(x)
x = self.avgpool(x)# torch.Size([1, 1024, 1, 1])
x = torch.flatten(x, 1)
x = self.dropout(x)
x = self.fc(x) # torch.Size([1,1000])
return x

由于这部分代码并没有涉及到新的知识点,所以这里就不再赘述。随便提一句的是,这些代码笔者也是根据Pytorch官方提供的代码修改而来。

3.2 训练网络

在原始论文中,作者使用的是ILSVRC2014中的数据集,但显然我们一般不具备这样的条件。因此,接下来我们会使用Fashion MNIST和CIFAR10这两个数据集来进行实验,并将得到的结果同之前的进行对比。在完成好GoogLeNet模型的定义后,我们就可以定义如下的一个类来训练模型:

class MyModel:
def __init__(self,
batch_size=128,
epochs=800,
learning_rate=0.0001):
self.batch_size = batch_size
self.epochs = epochs
self.learning_rate = learning_rate
self.net = GoogLeNet(10)
def train(self):
train_iter, test_iter = load_dataset(batch_size=self.batch_size, resize=96)
......

if __name__ == '__main__':
model = MyModel()
model.train()

由于这部分代码同之前的相比几乎没有任何变化,所以就不贴在这里占用篇幅了,见示例代码即可[1]。同时,因为这两个数据集原始大小只有 32 × 32 32\times32 32×32,所以这里我们将其resize到了 96 × 96 96\times96 96×96的大小[2]。最后,需要注意的是Fashion MNIST数据集图片的通道数为1,所以在使用它的时候需要将上面​​self.conv1 = conv_block(in_channels=3,...)​​​中的​​in_channels​​设置为1。

3.3 训练结果

如下表所示就是最近我们陆续介绍的这5个模型在Fashion MNIST和CIFAR10上的测试结果。当然,这种比法显然是不公平的,因为前面的几个模型都只是迭代了5轮,而由于后面的网络越来越多因此也就使用了更多的迭代次数。但是可以明确的是,这些模型的表达能力肯定是从上到下依次提升的。

模型

Fashion MNIST

CIFAR10

Epochs

LeNet5

0.8703

5

AlexNet

0.9043

5

VGG19

0.9054

5

NIN

0.8643

800

GoogLeNet

0.9273

0.8357

60/800

虽然从上面的结果来看,GoogLeNet在CIFAR10上的结果还不如NIN的表现,但这并不代表GoogLeNet就比NIN差。这可能是因为我们这里并没有采用像论文中那样的集成策略和训练策略,同时也没有对数据进行增强处理。但不得不承认的是,Inception的思想是非常值得借鉴与学习。

4 总结

在这篇文章中,笔者首先介绍了如何通过Inception来构造GoogLeNet网络模型;接着介绍了如何在基础的GoogLeNet中再加入额外的两个分类器;最后还介绍了如何通过Pytorch来实现GoogLeNet网络,并同时在Fashion MNIST和CIFAR10这两个数据集上进行了实验。同时,在读完这篇论文后,我们最应该掌握的两个点就是Inception本身的这种结构以及对于 1 × 1 1\times1 1×1卷积的理解。在下一篇的文章中,我们将开始学习卷积网络中的最最常用的一个操作Batch Normaliztion。

本次内容就到此结束,感谢您的阅读!如果你觉得上述内容对你有所帮助,欢迎关注并传播本公众号!青山不改,绿水长流,我们月来客栈见!