阅读 90

微架构模型:GoogleNet

在这篇文章中,我们将讨论一种新的网络模型GoogleNet,它和我前面所讨论的模型有所不同,表现在:

  1. 移除了全连接层,而采用全局平均池化层(global average pooling)代替,大量减少参数数量,所以相对于AlexNet和VGGNe这种巨型模型,其需要训练的参数少得多,可以节约大量内存。
  2. 采用了微架构,而到目前为止,我们接触到的模型都是序列(sequential)模型,所谓序列,就是前一层的输出直接输出到下一层。但GoogleNet却采用了微架构,来自一个层的输出可以分成多个不同的路径并且稍后重新连接到一起。

GoogLeNet模型于2014年的一篇论文《Going Deeper With Convolutions》提出,其最大的贡献在于Inception模块(Inception有起初、开端的含义),这是一个适合卷积神经网络的构建模块,它选用多个过滤器大小的卷积,将模块转换为多级特征提取器。

Inception模块及其变种

Inception模块是一种微架构模块,所谓微架构,就是由深度学习从业者设计的小型构建块,它使得网络能够在增加网络深度的前提下更快地学习,而且更高效。而这些微架构构建块与诸如CONV、POOL等传统类型的层堆叠在一起,可以形成宏架构(macro-architecture)。

Inception模块背后的思想有两层含义:

  1. 在设计卷积层时,我们可能很难确定过滤器的大小。设计为5×5过滤器还是3x3过滤器,如果采用1×1过滤器会不会更好?如果我们反过来想,为什么不都用上,让模型来决定呢? 在Inception模块中,我们学习所有三个5×5、3×3和1×1过滤器(并行计算它们),将所得到的特征映射沿着通道维度连接起来。GoogLeNet体系结构中的下一层(可能是另一个Inception模块)接收这些连接的混合过滤器并执行相同的过程。总的来说,这个过程使GoogLeNet能够通过较小的卷积学习局部特征,较大卷积来学习抽象特征。
  2. 通过学习多个过滤器大小,我们可以将模块转换为多级特征提取器。5×5过滤器具有更大的接收尺寸,可以学习更多抽象功能。根据定义,1×1过滤器学习更多局部特征,而3×3过滤器在两者之间保持平衡。

GoogleNet最初引入的Inception模块如下图所示:

注: 在每个CONV层之后都紧跟一个激活函数(ReLU)。为节省空间,此激活函数并没包含在上面的网络图中。

从图中可以看到,输入层之后有四个不同的路径分支。Inception模块中的第一个分支只是从输入中学习一系列1×1局部特征。

第二条路径首先应用1×1卷积,不仅作为学习局部特征的一种形式,还可以减少维数。较大的卷积(即3×3和5×5)需要更多的计算。因此,如果我们可以通过应用1×1卷积来减少这些较大过滤器的输入维数,就可以减少网络所需的计算量。

第三个分支与第二个分支的逻辑相同,区别在于为了学习5×5过滤器。我们再次通过1×1卷积降低维数,然后将输出馈送到5×5过滤器。

Inception模块的第四个分支以1×1的步幅执行3×3最大池化 - 该分支通常被称为池投影分支。

最后,Inception模块的所有四个分支汇聚在一起,它们沿着通道维度连接在一起。在实现过程中要特别小心(通过零填充)以确保每个分支的输出具有相同的卷大小,从而允许连接输出。

Miniception

最初的Inception模块是为GoogLeNet设计的,在ImageNet数据集上训练(其中每个输入图像假设为224×224×3)并获得最好的精度。对于较小的数据集(具有较小的图像空间维度),我们可以简化Inception模块,只需要较少的网络参数。比如下图表示的Miniception:

  • :卷积模块,负责执行卷积、批量正则化和激活。

  • :Miniception模块执行两组卷积,一组用于1×1滤波器,另一组用于3×3滤波器,然后连接结果。在3×3滤波器之前不执行降维,因为我们将使用CIFAR-10数据集,输入已经很小。

  • :下采样模块,它同时应用卷积和最大池化以降低维度,然后在过滤器维度上连接。

将这些模块堆叠起来,可以组成称之为MiniGoogleNet的模型结构,如下图所示:

实现MiniGoogleNet

有了上面的模型定义,接下来我们就可以使用Keras框架来实现之。但在编码之前,我们先了解一下Keras中的两种类型的模型。

  • 序列(Sequential)模型: 在我们之前代码中用到的模型为序列模型,它是最简单的线性结构,从头到尾顺序连接,不分叉。其常见操作是 model.add 进行堆叠。比如:
model.add(Dense(32, activation='relu', input_dim=100))
model.add(Dropout(0.25))
复制代码
  • 函数式API:它比序列模型复杂,可以同时/分阶段输入变量,分阶段输出想要的模型。其常见形式是 output = Layer(parameters)(input) ,输入像函数参数一样传递进去,比如:
inputs = Input(shape=(784,))
# 输入inputs,输出x
x = Dense(64, activation='relu')(inputs)
# 输入x,输出x
x = Dense(64, activation='relu')(x)
复制代码

因为MiniGoogleNet并不是那种一条路走到黑的模型,所以我们不能选择序列模型,而应该选择函数式API来构建,代码如下:

class MiniGoogleNet:
  @staticmethod
  def conv_module(x, k, kx, ky, stride, channel_dim, padding="same"):
    # define a CONV => BN => RELU pattern
    x = Conv2D(k, (kx, ky), strides=stride, padding=padding)(x)
    x = BatchNormalization(axis=channel_dim)(x)
    x = Activation("relu")(x)

    return x


  @staticmethod
  def inception_module(x, num_k1x1, num_k3x3, channel_dim):
    # define two CONV module, then concatenate across the channel dimension
    conv_1x1 = MiniGoogleNet.conv_module(x, num_k1x1, 1, 1, (1, 1), channel_dim=channel_dim)
    conv_3x3 = MiniGoogleNet.conv_module(x, num_k3x3, 3, 3, (1, 1), channel_dim=channel_dim)
    x = concatenate([conv_1x1, conv_3x3], axis=channel_dim)

    return x


  @staticmethod
  def downsample_module(x, k, channel_dim):
    # define the CONV module and POOL, then concatenate across the channel dimension
    conv_3x3 = MiniGoogleNet.conv_module(x, k, 3, 3, (2, 2), channel_dim=channel_dim, padding="valid")
    pool = MaxPooling2D((3, 3), strides=(2, 2))(x)
    x = concatenate([conv_3x3, pool], axis=channel_dim)

    return x

  @staticmethod
  def build(width, height, depth, classes):
    input_shape = (width, height, depth)
    channel_dim = -1

    if K.image_data_format() == "channels_first":
      input_shape = (depth, width, height)
      channel_dim = 1

    inputs = Input(shape=input_shape)
    x = MiniGoogleNet.conv_module(inputs, 96, 3, 3, (1, 1), channel_dim=channel_dim)
    x = MiniGoogleNet.inception_module(x, 32, 32, channel_dim=channel_dim)
    x = MiniGoogleNet.inception_module(x, 32, 48, channel_dim=channel_dim)
    x = MiniGoogleNet.downsample_module(x, 80, channel_dim=channel_dim)

    x = MiniGoogleNet.inception_module(x, 112, 48, channel_dim=channel_dim)
    x = MiniGoogleNet.inception_module(x, 96, 64, channel_dim=channel_dim)
    x = MiniGoogleNet.inception_module(x, 80, 80, channel_dim=channel_dim)
    x = MiniGoogleNet.inception_module(x, 48, 96, channel_dim=channel_dim)
    x = MiniGoogleNet.downsample_module(x, 96, channel_dim=channel_dim)

    x = MiniGoogleNet.inception_module(x, 176, 160, channel_dim=channel_dim)
    x = MiniGoogleNet.inception_module(x, 176, 160, channel_dim=channel_dim)
    x = AveragePooling2D((7, 7))(x)
    x = Dropout(0.5)(x)

    # softmax classifier
    x = Flatten()(x)
    x = Dense(classes)(x)
    x = Activation("softmax")(x)

    model = Model(inputs, x, name="googlenet")

    return model
复制代码

接下来就是训练和测试模型,这个在前面的文章中介绍过,其步骤都差不多,所以在这里我也不再罗嗦,有兴趣的同学可以参考我在github上的完整代码。

写下这篇文章,我完成了《Deep Learning for Computer Vision with Python》的学习,其实后面还有一章节是讲残差网络(ResNet),但考虑到ResNet也是采用微架构,其实和GoogleNet差不多,就是模块构建块有些区别,所以就不打算写了。

其实这套书还有第三部,称为ImageNet Bundle,里面有更多大型项目的例子,考虑到我这边的硬件条件有限,就先不去研究这些复杂的例子。在后面的时间里,我将专注于移动终端上的机器学习,敬请关注。

以上实例均有完整的代码,点击阅读原文,跳转到我在github上建的示例代码。 另外,我在阅读《Deep Learning for Computer Vision with Python》这本书,在微信公众号后台回复“计算机视觉”关键字,可以免费下载这本书的电子版。

image