2014 年,GoogLeNet赢得了 ImageNet 挑战赛 (Szegedy等人,2015 年) ,它使用的结构结合了 NiN (Lin等人,2013 年)、重复块 (Simonyan 和 Zisserman,2014 年)和卷积混合的优点内核。它也可以说是第一个在 CNN 中明确区分主干(数据摄取)、主体(数据处理)和头部(预测)的网络。这种设计模式在深度网络的设计中一直存在:由对图像进行操作的前 2-3 个卷积给出。他们从底层图像中提取低级特征。接下来是一组卷积块。最后,头部将目前获得的特征映射到手头所需的分类、分割、检测或跟踪问题。
GoogLeNet 的关键贡献是网络主体的设计。它巧妙地解决了卷积核的选择问题。而其他作品试图确定哪个卷积,范围从 1×1到11×11最好,它只是 连接多分支卷积。接下来我们介绍一个略微简化的 GoogLeNet 版本:最初的设计包括许多通过中间损失函数稳定训练的技巧,应用于网络的多个层。由于改进的训练算法的可用性,它们不再是必需的。
8.4.1. 起始块
GoogLeNet 中的基本卷积块称为Inception 块,源于电影 Inception的模因“我们需要更深入” 。
如图8.4.1所示,初始块由四个并行分支组成。前三个分支使用窗口大小为1×1,3×3, 和 5×5从不同的空间大小中提取信息。中间两个分支还加了一个1×1输入的卷积减少了通道的数量,降低了模型的复杂度。第四个分支使用3×3最大池化层,然后是1×1卷积层改变通道数。四个分支都使用适当的填充使输入和输出具有相同的高度和宽度。最后,每个分支的输出沿着通道维度连接起来,并构成块的输出。Inception 块的常用超参数是每层的输出通道数,即如何在不同大小的卷积之间分配容量。
class Inception(nn.Module):
# c1--c4 are the number of output channels for each branch
def __init__(self, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# Branch 1
self.b1_1 = nn.LazyConv2d(c1, kernel_size=1)
# Branch 2
self.b2_1 = nn.LazyConv2d(c2[0], kernel_size=1)
self.b2_2 = nn.LazyConv2d(c2[1], kernel_size=3, padding=1)
# Branch 3
self.b3_1 = nn.LazyConv2d(c3[0], kernel_size=1)
self.b3_2 = nn.LazyConv2d(c3[1], kernel_size=5, padding=2)
# Branch 4
self.b4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
self.b4_2 = nn.LazyConv2d(c4, kernel_size=1)
def forward(self, x):
b1 = F.relu(self.b1_1(x))
b2 = F.relu(self.b2_2(F.relu(self.b2_1(x))))
b3 = F.relu(self.b3_2(F.relu(self.b3_1(x))))
b4 = F.relu(self.b4_2(self.b4_1(x)))
return torch.cat((b1, b2, b3, b4), dim=1)
class Inception(nn.Block):
# c1--c4 are the number of output channels for each branch
def __init__(self, c1, c2, c3, c4, **kwargs):
super(Inception, self).__init__(**kwargs)
# Branch 1
self.b1_1 = nn.Conv2D(c1, kernel_size=1, activation='relu')
# Branch 2
self.b2_1 = nn.Conv2D(c2[0], kernel_size=1, activation='relu')
self.b2_2 = nn.Conv2D(c2[1], kernel_size=3, padding=1,
activation='relu')
# Branch 3
self.b3_1 = nn.Conv2D(c3[0], kernel_size=1, activation='relu')
self.b3_2 = nn.Conv2D(c3[1], kernel_size=5, padding=2,
activation='relu')
# Branch 4
self.b4_1 = nn.MaxPool2D(pool_size=3, strides=1, padding=1)
self.b4_2 = nn.Conv2D(c4, kernel_size=1, activation='relu')
def forward(self, x):
b1 = self.b1_1(x)
b2 = self.b2_2(self.b2_1(x))
b3 = self.b3_2(self.b3_1(x))
b4 = self.b4_2(self.b4_1(x))
return np.concatenate((b1, b2, b3, b4), axis=1)
class Inception(nn.Module):
# `c1`--`c4` are the number of output channels for each branch
c1: int
c2: tuple
c3: tuple
c4: int
def setup(self):
# Branch 1
self.b1_1 = nn.Conv(self.c1, kernel_size=(1, 1))
# Branch 2
self.b2_1 = nn.Conv(self.c2[0], kernel_size=(1, 1))
self.b2_2 = nn.Conv(self.c2[1], kernel_size=(3, 3), padding='same')
# Branch 3
self.b3_1 = nn.Conv(self.c3[0], kernel_size=(1, 1))
self.b3_2 = nn.Conv(self.c3[1], kernel_size=(5, 5), padding='same')
# Branch 4
self.b4_1 = lambda x: nn.max_pool(x, window_shape=(3, 3),
strides=(1, 1), padding='same')
self.b4_2 = nn.Conv(self.c4, kernel_size=(1, 1))
def __call__(self, x):
b1 = nn.relu(self.b1_1(x))
b2 = nn.relu(self.b2_2(nn.relu(self.b2_1(x))))
b3 = nn.relu(self.b3_2(nn.relu(self.b3_1(x))))
b4 = nn.relu(self.b4_2(self.b4_1(x)))
return jnp.concatenate((b1, b2, b3, b4), axis=-1)