- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
《动手学深度学习》中文版(d2l-zh)在"现代卷积神经网络"一章中完整呈现了 GoogLeNet 的诞生背景、Inception 块设计思想与逐模块可运行实现。本文以 chapter_convolutional-modern/googlenet_origin.md 及其中文版 chapter_convolutional-modern/googlenet.md 为主体,结合仓库 d2l/ 下的多框架工具源码,系统讲解 GoogLeNet 如何在 2014 年 ImageNet 挑战赛中胜出,以及如何用 MXNet、PyTorch、TensorFlow 和 PaddlePaddle 从零实现它并在 Fashion-MNIST 上完成训练。
背景:为什么需要并行多尺度卷积
在 GoogLeNet 之前,流行的网络使用小到 $1 \times 1$、大到 $11 \times 11$ 的卷积核。论文《Going Deeper with Convolutions》(Szegedy 等,2015)的核心问题之一是:什么样大小的卷积核最合适?其洞察是:与其选择一个固定大小的卷积核,不如同时组合使用不同大小的卷积核——不同尺寸的滤波器可以有效地识别不同范围的图像细节,同时可以为不同滤波器分配不同数量的参数。本节介绍的 GoogLeNet 是原始模型的一个简化版本:省略了少数为稳定训练而添加的临时特性(如辅助分类器),因为如今更好的训练算法已使这些特性不再必要。
Inception 块:四条并行路径的构成
GoogLeNet 的基本卷积块被称为Inception 块(Inception block),很可能得名于电影《盗梦空间》(Inception)中的台词"我们需要走得更深"("We need to go deeper")。
如图fig_inception所示,Inception 块由四条并行路径组成:
- 路径 1:单个 $1 \times 1$ 卷积层,输出通道数为
c1; - 路径 2:$1 \times 1$ 卷积层(输出
c2[0]通道)后接 $3 \times 3$ 卷积层(输出c2[1]通道,padding=1); - 路径 3:$1 \times 1$ 卷积层(输出
c3[0]通道)后接 $5 \times 5$ 卷积层(输出c3[1]通道,padding=2); - 路径 4:$3 \times 3$ 最大汇聚层(
padding=1)后接 $1 \times 1$ 卷积层(输出c4通道)。
中间两条路径先在输入上执行 $1 \times 1$ 卷积,以减少通道数、降低模型复杂度;四条路径都使用合适的填充使输入输出高宽一致;最后把四条路径的输出在通道维度上连结,构成块的输出。Inception 块中通常调整的超参数是每层输出通道数。
下面是 MXNet(Gluon)版本的实现:
from d2l import mxnet as d2l from mxnet import np, npx from mxnet.gluon import nn npx.set_np() class Inception(nn.Block): # c1--c4是每条路径的输出通道数 def __init__(self, c1, c2, c3, c4, **kwargs): super(Inception, self).__init__(**kwargs) # 线路1,单1x1卷积层 self.p1_1 = nn.Conv2D(c1, kernel_size=1, activation='relu') # 线路2,1x1卷积层后接3x3卷积层 self.p2_1 = nn.Conv2D(c2[0], kernel_size=1, activation='relu') self.p2_2 = nn.Conv2D(c2[1], kernel_size=3, padding=1, activation='relu') # 线路3,1x1卷积层后接5x5卷积层 self.p3_1 = nn.Conv2D(c3[0], kernel_size=1, activation='relu') self.p3_2 = nn.Conv2D(c3[1], kernel_size=5, padding=2, activation='relu') # 线路4,3x3最大汇聚层后接1x1卷积层 self.p4_1 = nn.MaxPool2D(pool_size=3, strides=1, padding=1) self.p4_2 = nn.Conv2D(c4, kernel_size=1, activation='relu') def forward(self, x): p1 = self.p1_1(x) p2 = self.p2_2(self.p2_1(x)) p3 = self.p3_2(self.p3_1(x)) p4 = self.p4_2(self.p4_1(x)) # 在通道维度上连结输出 return np.concatenate((p1, p2, p3, p4), axis=1)PyTorch 版本的实现(注意 PyTorch 的卷积层默认不带激活函数,需在forward中显式调用F.relu):
from d2l import torch as d2l import torch from torch import nn from torch.nn import functional as F class Inception(nn.Module): # c1--c4是每条路径的输出通道数 def __init__(self, in_channels, c1, c2, c3, c4, **kwargs): super(Inception, self).__init__(**kwargs) # 线路1,单1x1卷积层 self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1) # 线路2,1x1卷积层后接3x3卷积层 self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1) self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1) # 线路3,1x1卷积层后接5x5卷积层 self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1) self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2) # 线路4,3x3最大汇聚层后接1x1卷积层 self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1) self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1) def forward(self, x): p1 = F.relu(self.p1_1(x)) p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) p4 = F.relu(self.p4_2(self.p4_1(x))) # 在通道维度上连结输出 return torch.cat((p1, p2, p3, p4), dim=1)TensorFlow(Keras)版本的实现(所有padding使用'same'以保持空间尺寸):
from d2l import tensorflow as d2l import tensorflow as tf class Inception(tf.keras.Model): # c1--c4是每条路径的输出通道数 def __init__(self, c1, c2, c3, c4): super().__init__() # 线路1,单1x1卷积层 self.p1_1 = tf.keras.layers.Conv2D(c1, 1, activation='relu') # 线路2,1x1卷积层后接3x3卷积层 self.p2_1 = tf.keras.layers.Conv2D(c2[0], 1, activation='relu') self.p2_2 = tf.keras.layers.Conv2D(c2[1], 3, padding='same', activation='relu') # 线路3,1x1卷积层后接5x5卷积层 self.p3_1 = tf.keras.layers.Conv2D(c3[0], 1, activation='relu') self.p3_2 = tf.keras.layers.Conv2D(c3[1], 5, padding='same', activation='relu') # 线路4,3x3最大汇聚层后接1x1卷积层 self.p4_1 = tf.keras.layers.MaxPool2D(3, 1, padding='same') self.p4_2 = tf.keras.layers.Conv2D(c4, 1, activation='relu') def call(self, x): p1 = self.p1_1(x) p2 = self.p2_2(self.p2_1(x)) p3 = self.p3_2(self.p3_1(x)) p4 = self.p4_2(self.p4_1(x)) # 在通道维度上连结输出 return tf.keras.layers.Concatenate()([p1, p2, p3, p4])中文版文档还提供了PaddlePaddle的变体(Inception(nn.Layer),结构与 PyTorch 完全对应,仅在 forward 中使用paddle.concat(x=[...], axis=1)),完整代码见 chapter_convolutional-modern/googlenet.md 第 122-154 行。
为什么这个网络如此有效?首先考虑滤波器的组合:它们以多种滤波器尺寸探索图像,意味着不同大小的滤波器可以高效识别不同范围的图像细节;同时,我们可以为不同滤波器分配不同数量的参数——这正是通道数分配比的设计自由度所在。
GoogLeNet 模型:九个 Inception 块的堆叠
如图fig_inception_full所示,GoogLeNet 共使用9 个 Inception 块和全局平均汇聚层的堆叠来生成估计:Inception 块之间的最大汇聚层降低维度;第一个模块类似 AlexNet 和 LeNet;Inception 块的组合从 VGG 继承;全局平均汇聚层避免了在末端堆叠全连接层(该思想来自 NiN,详见 chapter_convolutional-modern/nin.md)。
模块一(b1):大幅降采样
第一个模块使用 64 通道、$7 \times 7$ 卷积层(步幅 2、填充 3),后接 $3 \times 3$ 最大汇聚层(步幅 2、填充 1)。它对 $96 \times 96$ 的输入先做 $7 \times 7$ 大感受野特征提取,再下采样到 $24 \times 24$:
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))(Gluon 版本为nn.Conv2D(64, kernel_size=7, strides=2, padding=3, activation='relu')配合nn.MaxPool2D(pool_size=3, strides=2, padding=1);Keras 版本以padding='same'等效。)
模块二(b2):1×1 降维 + 3×3 扩维
第二个模块使用两个卷积层:先 64 通道 $1 \times 1$ 卷积,再 $3 \times 3$ 卷积将通道数扩大三倍到 192。这对应于 Inception 块中的第二条路径,随后接 $3 \times 3$ 最大汇聚层:
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1), nn.ReLU(), nn.Conv2d(64, 192, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))模块三(b3):两个 Inception 块串联
第三个模块串联两个完整的 Inception 块(PyTorch 的Inception(in_channels, c1, c2, c3, c4)需要显式传入输入通道数;Gluon/Keras 版本自动推导):
- 第一个块的输入为 192 通道,输出通道数 $64+128+32+32=256$,四路径输出通道数之比为 $64:128:32:32 = 2:4:1:1$;
- 第二、三条路径先把输入通道分别降到 $96/192 = 1/2$ 和 $16/192 = 1/12$,再接第二层卷积;
- 第二个块的输入为 256 通道,输出通道数增至 $128+192+96+64=480$,四路径之比为 $128:192:96:64 = 4:6:3:2$,其中第二、三路径分别先降到 $1/2$ 和 $1/8$。
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32), Inception(256, 128, (128, 192), (32, 96), 64), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))模块四(b4):五个 Inception 块串联
第四个模块更复杂,串联 5 个 Inception 块,其输出通道数分别是:
- $192+208+48+64=512$
- $160+224+64+64=512$
- $128+256+64+64=512$
- $112+288+64+64=528$
- $256+320+128+128=832$
通道分配思路与模块三一致:含 $3 \times 3$ 卷积的第二条路径输出通道最多,其次是仅含 $1 \times 1$ 卷积的第一条路径,再次是含 $5 \times 5$ 卷积的第三条路径和含 $3 \times 3$ 最大汇聚的第四条路径;第二、三条路径会先按比例减小通道数,且这些比例在各个 Inception 块中略有不同:
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64), Inception(512, 160, (112, 224), (24, 64), 64), Inception(512, 128, (128, 256), (24, 64), 64), Inception(512, 112, (144, 288), (32, 64), 64), Inception(528, 256, (160, 320), (32, 128), 128), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))模块五(b5):全局平均汇聚 + 输出层
第五模块包含输出通道数为 $256+320+128+128=832$ 和 $384+384+128+128=1024$ 的两个 Inception 块。第五模块后面紧跟输出层,同 NiN 一样使用全局平均汇聚层将每个通道的高和宽变成 1;最后把输出变成二维数组,再接一个输出个数为标签类别数(Fashion-MNIST 为 10)的全连接层:
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128), Inception(832, 384, (192, 384), (48, 128), 128), nn.AdaptiveAvgPool2d((1,1)), nn.Flatten()) net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))(Gluon 版本使用nn.GlobalAvgPool2D()后接nn.Dense(10);TensorFlow 版本则把net封装成函数net()返回Sequential([b1(), b2(), b3(), b4(), b5(), Dense(10)]),因为d2l.train_ch6()需要传入一个构建函数以便在strategy.scope()内完成模型构建与编译,从而利用可用的 CPU/GPU 设备。)
各模块输出形状验证
GoogLeNet 计算复杂,不如 VGG 那样便于修改通道数。为了使 Fashion-MNIST 上的训练短小精悍,本节将输入的高和宽从 224 降到 96,这简化了计算。以下代码逐层打印各模块输出的形状变化:
X = torch.rand(size=(1, 1, 96, 96)) for layer in net: X = layer(X) print(layer.__class__.__name__,'output shape:\t', X.shape)训练模型
和前面的章节一样,使用 Fashion-MNIST 数据集训练,训练前将图片转换为 $96 \times 96$ 分辨率。训练配置为:学习率lr=0.1、轮数num_epochs=10、批量大小batch_size=128:
lr, num_epochs, batch_size = 0.1, 10, 128 train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96) d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())从仓库源码看训练细节:d2l.load_data_fashion_mnist在 d2l/torch.py 中实现——当传入resize=96时,会在ToTensor()前插入transforms.Resize(96),随后以 4 个工作进程(get_dataloader_workers())加载 FashionMNIST 数据。d2l.train_ch6定义在 d2l/torch.py,它对Linear与Conv2d层用nn.init.xavier_uniform_做权重初始化,使用 SGD 优化器与交叉熵损失,在 GPU 上逐 epoch 训练并动态绘制训练损失、训练精度与测试精度曲线(MXNet、TensorFlow、PaddlePaddle 的对应实现分别在 d2l/mxnet.py、d2l/tensorflow.py、d2l/paddle.py 中)。
小结
- Inception 块相当于一个有 4 条路径的子网络,通过不同窗口形状的卷积层和最大汇聚层并行抽取信息,并使用 $1 \times 1$ 卷积层减少每像素级别上的通道维数,从而降低模型复杂度;最大汇聚层则降低分辨率。
- GoogLeNet 将多个设计精细的 Inception 块与其他层(卷积层、全连接层)串联起来,其中 Inception 块的通道数分配之比是在 ImageNet 数据集上通过大量实验得来的。
- GoogLeNet 和它的后继者们一度是 ImageNet 上最有效的模型之一,以较低的计算复杂度提供了类似的测试精度。
练习与思考
- 尝试 GoogLeNet 的后续版本,并实现运行、观察实验结果,包括:
- 添加批量规范化层(batch normalization),详见 chapter_convolutional-modern/batch-norm.md;
- 对 Inception 块进行调整(Inception-v2/v3,Szegedy 等 2016);
- 使用标签平滑(label smoothing)进行模型正则化;
- 加入残差连接(Inception-ResNet,Szegedy 等 2017),详见 chapter_convolutional-modern/resnet.md。
- 最小图像尺寸:GoogLeNet 能工作的最小图像尺寸是多少?(可结合各模块的填充与步幅推导空间尺寸的下限,并动手验证。)
- 参数规模对比:将 AlexNet、VGG 和 NiN 的模型参数大小与 GoogLeNet 比较。后两个网络架构是如何显著减少模型参数大小的?(提示:NiN 的全局平均汇聚替代全连接层、VGG 的重复块设计,都是减少参数的关键思想。)
延伸阅读
- 本章所属的"现代卷积神经网络"章节索引见 chapter_convolutional-modern/index.md,其中按历史脉络依次介绍了 AlexNet、VGG、NiN、GoogLeNet、批量规范化(batch normalization)、ResNet 与 DenseNet;
- 完整的多框架工具函数(数据加载、训练循环、动画绘制等)集中在仓库 d2l/ 目录下,包括 d2l/mxnet.py、d2l/torch.py、d2l/tensorflow.py 与 d2l/paddle.py。
- 人工智能
- 深度学习
- 机器学习
- 教程
【免费下载链接】d2l-zh
《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。
相关推荐
深入解析GoogLeNet:并行连接网络架构
深入解析GoogLeNet:并行连接网络架构 引言 在深度学习的发展历程中,2014年ImageNet图像识别挑战赛是一个重要的里程碑。Google团队提出的G
人工智能深度学习机器学习教程动手学深度学习:DenseNet 稠密连接网络原理与多框架实现详解
动手学深度学习:DenseNet 稠密连接网络原理与多框架实现详解 DenseNet(dense convolutional network)是《动手学深度学习
人工智能深度学习机器学习教程从零实现GoogLeNet/InceptionNet架构详解
从零实现GoogLeNet/InceptionNet架构详解 引言 GoogLeNet(也称为InceptionNet)是Google团队在2014年提出的深度
示例工程机器学习深度学习教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考