8.1. 深度卷积神经网络(AlexNet)
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

虽然卷积神经网络在计算机视觉和机器学习领域中广为人知,但 LeNet 的引入 (LeCun et al., 1995) 并没有立即主导该领域。虽然 LeNet 在早期的小型数据集上取得了良好的效果,但在更大、更真实的数据集上训练卷积神经网络的性能和可行性还有待建立。事实上,在 20 世纪 90 年代初到 2012 年的分水岭结果 (Krizhevsky et al., 2012) 之间的大部分时间里,神经网络常常被其他机器学习方法(如核方法 (Schölkopf and Smola, 2002)、集成方法 (Freund and Schapire, 1996) 和结构化估计 (Taskar et al., 2004))所超越。

对于计算机视觉而言,这种比较或许并不完全准确。也就是说,尽管卷积网络的输入由原始像素或轻度处理过的(例如,通过中心化)像素值组成,但从业者绝不会将原始像素输入到传统模型中。相反,典型的计算机视觉流程包括手动设计特征提取管道,例如 SIFT (Lowe, 2004)、SURF (Bay et al., 2006) 和视觉词袋 (Sivic and Zisserman, 2003)。特征不是被 *学习* 出来的,而是被 *精心设计* 出来的。大多数进展一方面来自于更有创意的特征提取想法,另一方面来自于对几何学的深刻洞见 (Hartley and Zisserman, 2000)。学习算法通常被认为是事后考虑的事情。

尽管在 20 世纪 90 年代已经有一些神经网络加速器可用,但它们还不足以强大到能够处理具有大量参数的深度多通道、多层卷积神经网络。例如,1999 年 NVIDIA 的 GeForce 256 每秒最多只能处理 4.8 亿次浮点运算(如加法和乘法),即 MFLOPS,并且没有任何有意义的编程框架来支持游戏以外的操作。如今的加速器每台设备能够执行超过 1000 TFLOPs 的运算。此外,当时的数据集仍然相对较小:在 60,000 张低分辨率 \(28 \times 28\) 像素图像上进行 OCR 被认为是一项极具挑战性的任务。除了这些障碍之外,训练神经网络的关键技巧,包括参数初始化启发式方法 (Glorot and Bengio, 2010)、随机梯度下降的巧妙变体 (Kingma and Ba, 2014)、非挤压激活函数 (Nair and Hinton, 2010) 和有效的正则化技术 (Srivastava et al., 2014) 都还不存在。

因此,经典的流程不是训练 *端到端*(从像素到分类)的系统,而是更像这样:

  1. 获取一个有趣的数据集。在早期,这些数据集需要昂贵的传感器。例如,1994 年的 Apple QuickTake 100 拥有高达 0.3 百万像素(VGA)的分辨率,能够存储多达 8 张图像,而售价高达 1000 美元。

  2. 基于光学、几何学、其他分析工具的知识,以及偶尔幸运研究生的偶然发现,用手工设计的特征来预处理数据集。

  3. 将数据通过一组标准的特征提取器,如 SIFT(尺度不变特征变换)(Lowe, 2004)、SURF(加速稳健特征)(Bay et al., 2006) 或任何其他手动调整的管道。

  4. 将得到的表示输入到你最喜欢的分类器中,很可能是一个线性模型或核方法,来训练一个分类器。

如果你和机器学习研究者交谈,他们会回答说机器学习既重要又优美。优雅的理论证明了各种分类器的性质 (Boucheron et al., 2005),而凸优化 (Boyd and Vandenberghe, 2004) 已成为获取这些分类器的主要方法。机器学习领域蓬勃发展、严谨且非常有用。然而,如果你和计算机视觉研究者交谈,你会听到一个非常不同的故事。他们会告诉你,图像识别的肮脏真相是,推动进步的是特征、几何 (Hartley and Zisserman, 2000, Hartley and Kahl, 2009) 和工程,而不是新颖的学习算法。计算机视觉研究者有理由相信,一个稍大或更干净的数据集,或者一个稍加改进的特征提取流程,对最终准确率的影响远大于任何学习算法。

import torch
from torch import nn
from d2l import torch as d2l
from mxnet import init, np, npx
from mxnet.gluon import nn
from d2l import mxnet as d2l

npx.set_np()
import jax
from flax import linen as nn
from jax import numpy as jnp
from d2l import jax as d2l
import tensorflow as tf
from d2l import tensorflow as d2l

8.1.1. 表示学习

另一种描述当时状况的方式是,流程中最重要的部分是表示。而在 2012 年之前,表示的计算大多是机械化的。事实上,设计一套新的特征函数、提高结果、并撰写方法,这些都在论文中占据了显著位置。SIFT (Lowe, 2004)、SURF (Bay et al., 2006)、HOG(方向梯度直方图)(Dalal and Triggs, 2005)、视觉词袋 (Sivic and Zisserman, 2003) 以及类似的特征提取器占据了主导地位。

另一组研究人员,包括 Yann LeCun、Geoff Hinton、Yoshua Bengio、Andrew Ng、Shun-ichi Amari 和 Juergen Schmidhuber,有不同的计划。他们相信特征本身应该被学习。此外,他们认为为了达到合理的复杂性,特征应该由多个共同学习的层级组成,每个层都有可学习的参数。在图像的情况下,最底层的层可能会检测边缘、颜色和纹理,类似于动物视觉系统处理其输入的方式。特别是,像通过稀疏编码获得的视觉特征的自动设计 (Olshausen and Field, 1996),在现代卷积神经网络出现之前一直是一个悬而未决的挑战。直到 Dean 等人 (2012),Le (2013) 的工作之后,从图像数据中自动生成特征的想法才获得了显著的关注。

第一个现代卷积神经网络 (Krizhevsky et al., 2012),以其发明者之一 Alex Krizhevsky 的名字命名为 *AlexNet*,很大程度上是 LeNet 的演进改进。它在 2012 年的 ImageNet 挑战赛中取得了优异的性能。

../_images/filters.png

图 8.1.1 AlexNet 第一层学习到的图像滤波器。图片由 Krizhevsky 等人 (2012) 提供。

有趣的是,在网络的最低层,模型学习到的特征提取器类似于一些传统的滤波器。 图 8.1.1 展示了低级图像描述符。网络中更高层的层级可能会在这些表示的基础上构建,以表示更大的结构,如眼睛、鼻子、草叶等。更高层的层级甚至可能表示整个物体,如人、飞机、狗或飞盘。最终,最后的隐藏状态学习到一个紧凑的图像表示,该表示总结了其内容,从而可以轻松地分离属于不同类别的数据。

AlexNet (2012) 和其前身 LeNet (1995) 共享许多架构元素。这就引出了一个问题:为什么花了这么长时间?一个关键的区别是,在过去的二十年里,可用的数据量和计算能力都显著增加。因此,AlexNet 要大得多:它是在更多的数据上训练的,并且是在比 1995 年可用的 CPU 快得多的 GPU 上训练的。

8.1.1.1. 缺失的要素:数据

具有多层的深度模型需要大量数据才能进入显著优于基于凸优化(例如,线性和核方法)的传统方法的阶段。然而,考虑到计算机有限的存储容量、相对昂贵的(成像)传感器以及 20 世纪 90 年代相对紧张的研究预算,大多数研究都依赖于微小的数据集。许多论文依赖于 UCI 数据集集合,其中许多只包含数百或(几)千张在低分辨率下捕获的图像,并且通常背景被人为地清理过。

2009 年,ImageNet 数据集发布了 (Deng 等人, 2009),挑战研究人员从 100 万个样本中学习模型,这些样本来自 1000 个不同的物体类别,每个类别 1000 个。类别本身基于 WordNet (Miller, 1995) 中最流行的名词节点。ImageNet 团队使用谷歌图片搜索为每个类别预筛选大量候选集,并利用亚马逊土耳其机器人众包平台来确认每张图片是否属于相关类别。这个规模是前所未有的,比其他数据集(例如 CIFAR-100 有 60,000 张图片)大一个数量级以上。另一个方面是,这些图像的分辨率相对较高,为 \(224 \times 224\) 像素,不像 8000 万张的 TinyImages 数据集 (Torralba 等人, 2008),它由 \(32 \times 32\) 像素的缩略图组成。这使得形成更高级别的特征成为可能。相关的竞赛,被称为 ImageNet 大规模视觉识别挑战赛 (Russakovsky 等人, 2015),推动了计算机视觉和机器学习研究的发展,挑战研究人员在比学术界之前考虑的更大规模上确定哪些模型表现最佳。最大的视觉数据集,如 LAION-5B (Schuhmann 等人, 2022) 包含数十亿张带有附加元数据的图像。

8.1.1.2. 缺失的要素:硬件

深度学习模型是计算周期的贪婪消费者。训练可能需要数百个 epoch,每次迭代都需要通过许多层的计算昂贵的线性代数运算来传递数据。这是 20 世纪 90 年代和 21 世纪初,基于更高效优化的凸目标的简单算法更受青睐的主要原因之一。

图形处理单元(GPU)被证明是使深度学习变得可行的游戏规则改变者。这些芯片早先是为加速图形处理以造福电脑游戏而开发的。特别是,它们针对高吞吐量的 \(4 \times 4\) 矩阵-向量乘积进行了优化,这对于许多计算机图形任务是必需的。幸运的是,这种数学运算与计算卷积层所需的数学惊人地相似。大约在那个时候,NVIDIA 和 ATI 开始为通用计算操作优化 GPU (Fernando, 2004),甚至将它们作为 *通用 GPU*(GPGPU)进行市场推广。

为了提供一些直观的理解,让我们考虑一下现代微处理器(CPU)的核心。每个核心都相当强大,以高时钟频率运行,并拥有大型缓存(高达几兆字节的 L3 缓存)。每个核心都非常适合执行各种指令,带有分支预测器、深度流水线、专用执行单元、推测执行以及许多其他使其能够运行具有复杂控制流的各种程序的附加功能。然而,这种明显的优势也是它的致命弱点:通用核心的制造成本非常高。它们在具有大量控制流的通用代码上表现出色。这需要大量的芯片面积,不仅用于实际的 ALU(算术逻辑单元)进行计算,还用于所有上述的附加功能,以及内存接口、核心之间的缓存逻辑、高速互连等。与专用硬件相比,CPU 在任何单一任务上的表现都相对较差。现代笔记本电脑有 4-8 个核心,即使是高端服务器,每个插槽也很少超过 64 个核心,这仅仅是因为它不具成本效益。

相比之下,GPU 可以包含数千个小型处理元件(NIVIDA 最新的 Ampere 芯片有多达 6912 个 CUDA 核心),通常分组为更大的组(NVIDIA 称之为 warps)。NVIDIA、AMD、ARM 和其他芯片供应商之间的细节略有不同。虽然每个核心相对较弱,以约 1GHz 的时钟频率运行,但正是这些核心的总数使得 GPU 的速度比 CPU 快了几个数量级。例如,NVIDIA 最近的 Ampere A100 GPU 为专门的 16 位精度(BFLOAT16)矩阵-矩阵乘法提供超过 300 TFLOPs 的性能,为更通用的浮点运算(FP32)提供高达 20 TFLOPs 的性能。与此同时,CPU 的浮点性能很少超过 1 TFLOPs。例如,亚马逊的 Graviton 3 在 16 位精度运算上达到 2 TFLOPs 的峰值性能,这个数字与苹果 M1 处理器的 GPU 性能相似。

GPU 在 FLOPs 方面远快于 CPU 有很多原因。首先,功耗往往随核心频率的 *平方* 增长。因此,对于一个运行速度快四倍(一个典型数字)的 CPU 核心的功耗预算,你可以使用 16 个速度为 \(\frac{1}{4}\) 的 GPU 核心,这将产生 \(16 \times \frac{1}{4} = 4\) 倍的性能。其次,GPU 核心要简单得多(事实上,很长一段时间它们甚至 *不能* 执行通用代码),这使得它们更节能。例如,(i) 它们通常不支持推测性求值,(ii) 通常不可能对每个处理元件进行单独编程,以及 (iii) 每个核心的缓存往往要小得多。最后,深度学习中的许多操作需要高内存带宽。在这方面,GPU 再次大放异彩,其总线宽度至少是许多 CPU 的 10 倍。

回到 2012 年。当 Alex Krizhevsky 和 Ilya Sutskever 实现了一个可以在 GPU 上运行的深度 CNN 时,取得了重大突破。他们意识到 CNN 中的计算瓶颈,即卷积和矩阵乘法,都是可以在硬件中并行化的操作。他们使用两块 NVIDIA GTX 580(每块有 3GB 内存,每块都能达到 1.5 TFLOPs 的性能,这在十年后对于大多数 CPU 来说仍然是一个挑战),实现了快速卷积。他们的 cuda-convnet 代码非常出色,以至于在好几年里它都是行业标准,并推动了深度学习热潮的头几年。

8.1.2. AlexNet

AlexNet 采用了一个 8 层的 CNN,在 2012 年的 ImageNet 大规模视觉识别挑战赛中以巨大优势获胜 (Russakovsky 等人, 2013)。该网络首次表明,通过学习获得的特征可以超越手动设计的特征,打破了计算机视觉领域的先前范式。

AlexNet 和 LeNet 的架构惊人地相似,如 图 8.1.2 所示。请注意,我们提供了一个稍微简化的 AlexNet 版本,去除了一些在 2012 年为了使模型能够适应两块小型 GPU 而需要的设计怪癖。

../_images/alexnet.svg

图 8.1.2 从 LeNet(左)到 AlexNet(右)。

AlexNet 和 LeNet 之间也存在显著差异。首先,AlexNet 比相对较小的 LeNet-5 要深得多。AlexNet 由八层组成:五个卷积层、两个全连接隐藏层和一个全连接输出层。其次,AlexNet 使用 ReLU 而不是 sigmoid 作为其激活函数。让我们在下面深入探讨细节。

8.1.2.1. 架构

在 AlexNet 的第一层,卷积窗口的形状是 \(11\times11\)。由于 ImageNet 中的图像比 MNIST 图像高和宽八倍,ImageNet 数据中的物体往往占据更多像素,并带有更多视觉细节。因此,需要一个更大的卷积窗口来捕捉物体。第二层的卷积窗口形状减小到 \(5\times5\),然后是 \(3\times3\)。此外,在第一、第二和第五个卷积层之后,网络添加了窗口形状为 \(3\times3\)、步幅为 2 的最大汇聚层。此外,AlexNet 的卷积通道数是 LeNet 的十倍。

在最后一个卷积层之后,是两个巨大的全连接层,各有 4096 个输出。这些层需要近 1GB 的模型参数。由于早期 GPU 的内存有限,最初的 AlexNet 使用了双数据流设计,以便它的两块 GPU 各自负责存储和计算模型的一半。幸运的是,现在的 GPU 内存相对充足,所以我们现在很少需要将模型拆分到多个 GPU 上(我们版本的 AlexNet 模型在这一方面与原始论文有所不同)。

8.1.2.2. 激活函数

此外,AlexNet 将 sigmoid 激活函数改为更简单的 ReLU 激活函数。一方面,ReLU 激活函数的计算更简单。例如,它没有 sigmoid 激活函数中的指数运算。另一方面,当使用不同的参数初始化方法时,ReLU 激活函数使模型训练更容易。这是因为,当 sigmoid 激活函数的输出非常接近 0 或 1 时,这些区域的梯度几乎为 0,因此反向传播无法继续更新某些模型参数。相比之下,ReLU 激活函数在正区间的梯度始终为 1(第 5.1.2 节)。因此,如果模型参数没有正确初始化,sigmoid 函数可能会在正区间得到几乎为 0 的梯度,这意味着模型无法有效训练。

8.1.2.3. 容量控制和预处理

AlexNet 通过 dropout(第 5.6 节)控制全连接层的模型复杂度,而 LeNet 仅使用权重衰减。为了进一步增强数据,AlexNet 的训练循环添加了大量的图像增强,如翻转、裁剪和颜色变化。这使得模型更加鲁棒,更大的样本量有效地减少了过拟合。有关此类预处理步骤的深入回顾,请参见 Buslaev 等人 (2020)

class AlexNet(d2l.Classifier):
    def __init__(self, lr=0.1, num_classes=10):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(
            nn.LazyConv2d(96, kernel_size=11, stride=4, padding=1),
            nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2),
            nn.LazyConv2d(256, kernel_size=5, padding=2), nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.LazyConv2d(384, kernel_size=3, padding=1), nn.ReLU(),
            nn.LazyConv2d(384, kernel_size=3, padding=1), nn.ReLU(),
            nn.LazyConv2d(256, kernel_size=3, padding=1), nn.ReLU(),
            nn.MaxPool2d(kernel_size=3, stride=2), nn.Flatten(),
            nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(p=0.5),
            nn.LazyLinear(4096), nn.ReLU(),nn.Dropout(p=0.5),
            nn.LazyLinear(num_classes))
        self.net.apply(d2l.init_cnn)
class AlexNet(d2l.Classifier):
    def __init__(self, lr=0.1, num_classes=10):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential()
        self.net.add(
            nn.Conv2D(96, kernel_size=11, strides=4, activation='relu'),
            nn.MaxPool2D(pool_size=3, strides=2),
            nn.Conv2D(256, kernel_size=5, padding=2, activation='relu'),
            nn.MaxPool2D(pool_size=3, strides=2),
            nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
            nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
            nn.Conv2D(256, kernel_size=3, padding=1, activation='relu'),
            nn.MaxPool2D(pool_size=3, strides=2),
            nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
            nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
            nn.Dense(num_classes))
        self.net.initialize(init.Xavier())
class AlexNet(d2l.Classifier):
    lr: float = 0.1
    num_classes: int = 10
    training: bool = True

    def setup(self):
        self.net = nn.Sequential([
            nn.Conv(features=96, kernel_size=(11, 11), strides=4, padding=1),
            nn.relu,
            lambda x: nn.max_pool(x, window_shape=(3, 3), strides=(2, 2)),
            nn.Conv(features=256, kernel_size=(5, 5)),
            nn.relu,
            lambda x: nn.max_pool(x, window_shape=(3, 3), strides=(2, 2)),
            nn.Conv(features=384, kernel_size=(3, 3)), nn.relu,
            nn.Conv(features=384, kernel_size=(3, 3)), nn.relu,
            nn.Conv(features=256, kernel_size=(3, 3)), nn.relu,
            lambda x: nn.max_pool(x, window_shape=(3, 3), strides=(2, 2)),
            lambda x: x.reshape((x.shape[0], -1)),  # flatten
            nn.Dense(features=4096),
            nn.relu,
            nn.Dropout(0.5, deterministic=not self.training),
            nn.Dense(features=4096),
            nn.relu,
            nn.Dropout(0.5, deterministic=not self.training),
            nn.Dense(features=self.num_classes)
        ])
class AlexNet(d2l.Classifier):
    def __init__(self, lr=0.1, num_classes=10):
        super().__init__()
        self.save_hyperparameters()
        self.net = tf.keras.models.Sequential([
            tf.keras.layers.Conv2D(filters=96, kernel_size=11, strides=4,
                                   activation='relu'),
            tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
            tf.keras.layers.Conv2D(filters=256, kernel_size=5, padding='same',
                                   activation='relu'),
            tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
            tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same',
                                   activation='relu'),
            tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same',
                                   activation='relu'),
            tf.keras.layers.Conv2D(filters=256, kernel_size=3, padding='same',
                                   activation='relu'),
            tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
            tf.keras.layers.Flatten(),
            tf.keras.layers.Dense(4096, activation='relu'),
            tf.keras.layers.Dropout(0.5),
            tf.keras.layers.Dense(4096, activation='relu'),
            tf.keras.layers.Dropout(0.5),
            tf.keras.layers.Dense(num_classes)])

我们构建一个高和宽均为 224 的单通道数据样本,来观察每一层的输出形状。它与 图 8.1.2 中的 AlexNet 架构相匹配。

AlexNet().layer_summary((1, 1, 224, 224))
Conv2d output shape:         torch.Size([1, 96, 54, 54])
ReLU output shape:   torch.Size([1, 96, 54, 54])
MaxPool2d output shape:      torch.Size([1, 96, 26, 26])
Conv2d output shape:         torch.Size([1, 256, 26, 26])
ReLU output shape:   torch.Size([1, 256, 26, 26])
MaxPool2d output shape:      torch.Size([1, 256, 12, 12])
Conv2d output shape:         torch.Size([1, 384, 12, 12])
ReLU output shape:   torch.Size([1, 384, 12, 12])
Conv2d output shape:         torch.Size([1, 384, 12, 12])
ReLU output shape:   torch.Size([1, 384, 12, 12])
Conv2d output shape:         torch.Size([1, 256, 12, 12])
ReLU output shape:   torch.Size([1, 256, 12, 12])
MaxPool2d output shape:      torch.Size([1, 256, 5, 5])
Flatten output shape:        torch.Size([1, 6400])
Linear output shape:         torch.Size([1, 4096])
ReLU output shape:   torch.Size([1, 4096])
Dropout output shape:        torch.Size([1, 4096])
Linear output shape:         torch.Size([1, 4096])
ReLU output shape:   torch.Size([1, 4096])
Dropout output shape:        torch.Size([1, 4096])
Linear output shape:         torch.Size([1, 10])
AlexNet().layer_summary((1, 1, 224, 224))
Conv2D output shape:         (1, 96, 54, 54)
MaxPool2D output shape:      (1, 96, 26, 26)
Conv2D output shape:         (1, 256, 26, 26)
MaxPool2D output shape:      (1, 256, 12, 12)
Conv2D output shape:         (1, 384, 12, 12)
Conv2D output shape:         (1, 384, 12, 12)
Conv2D output shape:         (1, 256, 12, 12)
MaxPool2D output shape:      (1, 256, 5, 5)
Dense output shape:  (1, 4096)
Dropout output shape:        (1, 4096)
Dense output shape:  (1, 4096)
Dropout output shape:        (1, 4096)
Dense output shape:  (1, 10)
[22:28:16] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
AlexNet(training=False).layer_summary((1, 224, 224, 1))
Conv output shape:   (1, 54, 54, 96)
custom_jvp output shape:     (1, 54, 54, 96)
function output shape:       (1, 26, 26, 96)
Conv output shape:   (1, 26, 26, 256)
custom_jvp output shape:     (1, 26, 26, 256)
function output shape:       (1, 12, 12, 256)
Conv output shape:   (1, 12, 12, 384)
custom_jvp output shape:     (1, 12, 12, 384)
Conv output shape:   (1, 12, 12, 384)
custom_jvp output shape:     (1, 12, 12, 384)
Conv output shape:   (1, 12, 12, 256)
custom_jvp output shape:     (1, 12, 12, 256)
function output shape:       (1, 5, 5, 256)
function output shape:       (1, 6400)
Dense output shape:  (1, 4096)
custom_jvp output shape:     (1, 4096)
Dropout output shape:        (1, 4096)
Dense output shape:  (1, 4096)
custom_jvp output shape:     (1, 4096)
Dropout output shape:        (1, 4096)
Dense output shape:  (1, 10)
AlexNet().layer_summary((1, 224, 224, 1))
Conv2D output shape:         (1, 54, 54, 96)
MaxPooling2D output shape:   (1, 26, 26, 96)
Conv2D output shape:         (1, 26, 26, 256)
MaxPooling2D output shape:   (1, 12, 12, 256)
Conv2D output shape:         (1, 12, 12, 384)
Conv2D output shape:         (1, 12, 12, 384)
Conv2D output shape:         (1, 12, 12, 256)
MaxPooling2D output shape:   (1, 5, 5, 256)
Flatten output shape:        (1, 6400)
Dense output shape:  (1, 4096)
Dropout output shape:        (1, 4096)
Dense output shape:  (1, 4096)
Dropout output shape:        (1, 4096)
Dense output shape:  (1, 10)

8.1.3. 训练

虽然 AlexNet 是在 Krizhevsky 等人 (2012) 的工作中在 ImageNet 上训练的,但我们在这里使用 Fashion-MNIST,因为即使在现代 GPU 上,将一个 ImageNet 模型训练到收敛也可能需要数小时或数天。直接在 Fashion-MNIST 上应用 AlexNet 的问题之一是其图像分辨率(\(28 \times 28\) 像素)低于 ImageNet 图像。为了让它工作,我们将它们上采样到 \(224 \times 224\)。这通常不是一个明智的做法,因为它只是增加了计算复杂性而没有增加信息。尽管如此,我们在这里这样做是为了忠实于 AlexNet 架构。我们通过 d2l.FashionMNIST 构造函数中的 resize 参数来执行此调整大小操作。

现在,我们可以开始训练 AlexNet 了。与 第 7.6 节 中的 LeNet 相比,这里的主要变化是使用了更小的学习率和更慢的训练速度,这是由于网络更深更宽、图像分辨率更高以及卷积成本更高。

model = AlexNet(lr=0.01)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
trainer.fit(model, data)
../_images/output_alexnet_180871_48_0.svg
model = AlexNet(lr=0.01)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
trainer.fit(model, data)
../_images/output_alexnet_180871_51_0.svg
model = AlexNet(lr=0.01)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
trainer.fit(model, data)
../_images/output_alexnet_180871_54_0.svg
trainer = d2l.Trainer(max_epochs=10)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
with d2l.try_gpu():
    model = AlexNet(lr=0.01)
    trainer.fit(model, data)
../_images/output_alexnet_180871_57_0.svg

8.1.4. 讨论

AlexNet 的结构与 LeNet 惊人地相似,但有许多关键的改进,既提高了准确性(dropout),也方便了训练(ReLU)。同样引人注目的是,深度学习工具方面取得了巨大的进步。2012 年需要几个月的工作,现在使用任何现代框架只需十几行代码即可完成。

回顾该架构,我们看到 AlexNet 在效率方面有一个致命弱点:最后两个隐藏层分别需要大小为 \(6400 \times 4096\)\(4096 \times 4096\) 的矩阵。这对应于 164 MB 的内存和 81 MFLOPs 的计算,这两者都是不小的开销,尤其是在较小的设备上,例如手机。这是 AlexNet 被我们将在后续章节中介绍的更高效架构所超越的原因之一。尽管如此,它是从浅层网络到如今使用的深度网络的关键一步。请注意,即使在我们的实验中参数数量远远超过训练数据量(最后两层有超过 4000 万个参数,在 6 万张图像的数据集上训练),也几乎没有过拟合:训练和验证损失在整个训练过程中几乎相同。这归功于现代深度网络设计中固有的改进正则化,例如 dropout。

尽管 AlexNet 的实现似乎只比 LeNet 多了几行代码,但学术界花了很多年才接受这种概念上的改变,并利用其出色的实验结果。这也是由于缺乏高效的计算工具。当时,既没有 DistBelief (Dean 等人, 2012) 也没有 Caffe (Jia 等人, 2014),而 Theano (Bergstra 等人, 2010) 仍然缺乏许多独特的特性。TensorFlow (Abadi 等人, 2016) 的出现极大地改变了这种情况。

8.1.5. 练习

  1. 根据上面的讨论,分析 AlexNet 的计算特性。

    1. 分别计算卷积层和全连接层的内存占用。哪一个占主导地位?

    2. 计算卷积层和全连接层的计算成本。

    3. 内存(读写带宽、延迟、大小)如何影响计算?它对训练和推理的影响有何不同?

  2. 你是一名芯片设计师,需要在计算和内存带宽之间进行权衡。例如,更快的芯片需要更多的功耗,并可能需要更大的芯片面积。更多的内存带宽需要更多的引脚和控制逻辑,因此也需要更多的面积。你如何进行优化?

  3. 为什么工程师们不再报告 AlexNet 的性能基准?

  4. 尝试在训练 AlexNet 时增加 epoch 的数量。与 LeNet 相比,结果有何不同?为什么?

  5. 对于 Fashion-MNIST 数据集,AlexNet 可能过于复杂,特别是由于初始图像的低分辨率。

    1. 尝试简化模型以加快训练速度,同时确保准确率不会显著下降。

    2. 设计一个更好的模型,可以直接在 \(28 \times 28\) 图像上工作。

  6. 修改批量大小,观察吞吐量(图像/秒)、准确率和 GPU 内存的变化。

  7. 将 dropout 和 ReLU 应用于 LeNet-5。它是否有所改进?你是否可以通过预处理来利用图像中固有的不变性来进一步改进?

  8. 你能让 AlexNet 过拟合吗?你需要移除或更改哪个特征来破坏训练?