8.2. 使用块的网络(VGG)¶ 在 SageMaker Studio Lab 中打开 Notebook
虽然AlexNet证明了深度卷积神经网络卓有成效,但它没有提供一个通用的模板来指导后续的研究人员设计新的网络。在下面的章节中,我们将介绍一些常用于设计深度网络的启发式概念。
这一领域的进展与VLSI(超大规模集成电路)在芯片设计中的进展类似,工程师们从放置晶体管到逻辑元件再到逻辑块(Mead, 1980)。同样,神经网络架构的设计也变得越来越抽象,研究人员从考虑单个神经元的角度转向整个层,现在又转向块,即重复的层模式。十年后,这已经发展到研究人员使用整个训练好的模型来重新用于不同但相关的任务。这种大型预训练模型通常被称为基础模型(Bommasani et al., 2021)。
回到网络设计。使用块的想法首先来自牛津大学的视觉几何组(VGG),在他们同名的VGG网络中(Simonyan and Zisserman, 2014)。在任何现代深度学习框架中,使用循环和子程序都很容易实现这些重复的结构。
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 d2l import jax as d2l
import tensorflow as tf
from d2l import tensorflow as d2l
8.2.1. VGG块¶
卷积神经网络的基本构建模块是一系列如下层:(i)一个带填充以保持分辨率的卷积层,(ii)一个非线性激活函数(如ReLU),(iii)一个池化层(如最大池化)来降低分辨率。这种方法的一个问题是空间分辨率下降得相当快。特别是,这给网络在所有维度(\(d\))用尽之前设置了\(\log_2 d\)个卷积层的硬性限制。例如,在ImageNet的情况下,用这种方式不可能有超过8个卷积层。
Simonyan and Zisserman (2014)的关键思想是,以块的形式,在通过最大池化进行下采样之间使用多个卷积。他们主要感兴趣的是深层网络还是宽层网络性能更好。例如,两个\(3 \times 3\)卷积的连续应用所触及的像素与一个\(5 \times 5\)卷积相同。同时,后者使用的参数量(\(25 \cdot c^2\))与三个\(3 \times 3\)卷积(\(3 \cdot 9 \cdot c^2\))大致相同。在一个相当详细的分析中,他们表明,深而窄的网络明显优于它们对应的浅层网络。这让深度学习走上了一条追求更深网络的道路,对于典型应用,网络层数超过100层。堆叠\(3 \times 3\)卷积已成为后来深度网络的黄金标准(这一设计决策直到最近才被Liu et al. (2022)重新审视)。因此,小型卷积的快速实现已成为GPU上的主要部分(Lavin and Gray, 2016)。
回到VGG:一个VGG块由一系列带\(3\times3\)卷积核和填充为1(保持高宽)的卷积层,后跟一个步幅为2的\(2 \times 2\)最大池化层(每个块后高宽减半)组成。在下面的代码中,我们定义了一个名为vgg_block
的函数来实现一个VGG块。
下面的函数接受两个参数,分别对应于卷积层的数量num_convs
和输出通道的数量num_channels
。
def vgg_block(num_convs, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.LazyConv2d(out_channels, kernel_size=3, padding=1))
layers.append(nn.ReLU())
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
def vgg_block(num_convs, num_channels):
blk = nn.Sequential()
for _ in range(num_convs):
blk.add(nn.Conv2D(num_channels, kernel_size=3,
padding=1, activation='relu'))
blk.add(nn.MaxPool2D(pool_size=2, strides=2))
return blk
def vgg_block(num_convs, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv(out_channels, kernel_size=(3, 3), padding=(1, 1)))
layers.append(nn.relu)
layers.append(lambda x: nn.max_pool(x, window_shape=(2, 2), strides=(2, 2)))
return nn.Sequential(layers)
def vgg_block(num_convs, num_channels):
blk = tf.keras.models.Sequential()
for _ in range(num_convs):
blk.add(
tf.keras.layers.Conv2D(num_channels, kernel_size=3,
padding='same', activation='relu'))
blk.add(tf.keras.layers.MaxPool2D(pool_size=2, strides=2))
return blk
8.2.2. VGG网络¶
与AlexNet和LeNet一样,VGG网络可以分为两部分:第一部分主要由卷积层和池化层组成,第二部分由全连接层组成,与AlexNet中的全连接层相同。关键区别在于,卷积层被分组在保持维度不变的非线性变换中,然后是一个分辨率降低步骤,如图8.2.1所示。
图 8.2.1 从AlexNet到VGG。关键区别在于VGG由层块组成,而AlexNet的层都是单独设计的。¶
网络的卷积部分连续连接了图8.2.1中的几个VGG块(也在vgg_block
函数中定义)。这种卷积分组模式在过去十年中几乎保持不变,尽管具体操作的选择已经发生了相当大的改变。变量arch
由一个元组列表组成(每个块一个元组),其中每个元组包含两个值:卷积层的数量和输出通道的数量,这正是调用vgg_block
函数所需的参数。因此,VGG定义了一个网络家族,而不仅仅是一个特定的实例。要构建一个特定的网络,我们只需迭代arch
来组合这些块。
class VGG(d2l.Classifier):
def __init__(self, arch, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
conv_blks = []
for (num_convs, out_channels) in arch:
conv_blks.append(vgg_block(num_convs, out_channels))
self.net = nn.Sequential(
*conv_blks, nn.Flatten(),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
nn.LazyLinear(num_classes))
self.net.apply(d2l.init_cnn)
class VGG(d2l.Classifier):
def __init__(self, arch, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
self.net = nn.Sequential()
for (num_convs, num_channels) in arch:
self.net.add(vgg_block(num_convs, num_channels))
self.net.add(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 VGG(d2l.Classifier):
arch: list
lr: float = 0.1
num_classes: int = 10
training: bool = True
def setup(self):
conv_blks = []
for (num_convs, out_channels) in self.arch:
conv_blks.append(vgg_block(num_convs, out_channels))
self.net = nn.Sequential([
*conv_blks,
lambda x: x.reshape((x.shape[0], -1)), # flatten
nn.Dense(4096), nn.relu,
nn.Dropout(0.5, deterministic=not self.training),
nn.Dense(4096), nn.relu,
nn.Dropout(0.5, deterministic=not self.training),
nn.Dense(self.num_classes)])
class VGG(d2l.Classifier):
def __init__(self, arch, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
self.net = tf.keras.models.Sequential()
for (num_convs, num_channels) in arch:
self.net.add(vgg_block(num_convs, num_channels))
self.net.add(
tf.keras.models.Sequential([
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)]))
原始的VGG网络有五个卷积块,其中前两个各有一个卷积层,后三个各有两个卷积层。第一个块有64个输出通道,每个后续块将输出通道数加倍,直到达到512。由于该网络使用了8个卷积层和3个全连接层,因此通常被称为VGG-11。
VGG(arch=((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))).layer_summary(
(1, 1, 224, 224))
Sequential output shape: torch.Size([1, 64, 112, 112])
Sequential output shape: torch.Size([1, 128, 56, 56])
Sequential output shape: torch.Size([1, 256, 28, 28])
Sequential output shape: torch.Size([1, 512, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
Flatten output shape: torch.Size([1, 25088])
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])
VGG(arch=((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))).layer_summary(
(1, 1, 224, 224))
Sequential output shape: (1, 64, 112, 112)
Sequential output shape: (1, 128, 56, 56)
Sequential output shape: (1, 256, 28, 28)
Sequential output shape: (1, 512, 14, 14)
Sequential output shape: (1, 512, 7, 7)
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:40:53] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
VGG(arch=((1, 64), (1, 128), (2, 256), (2, 512), (2, 512)),
training=False).layer_summary((1, 224, 224, 1))
Sequential output shape: (1, 112, 112, 64)
Sequential output shape: (1, 56, 56, 128)
Sequential output shape: (1, 28, 28, 256)
Sequential output shape: (1, 14, 14, 512)
Sequential output shape: (1, 7, 7, 512)
function output shape: (1, 25088)
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)
VGG(arch=((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))).layer_summary(
(1, 224, 224, 1))
Sequential output shape: (1, 112, 112, 64)
Sequential output shape: (1, 56, 56, 128)
Sequential output shape: (1, 28, 28, 256)
Sequential output shape: (1, 14, 14, 512)
Sequential output shape: (1, 7, 7, 512)
Sequential output shape: (1, 10)
如你所见,我们在每个块将高度和宽度减半,最终达到7的高度和宽度,然后将表示展平以供网络的全连接部分处理。Simonyan and Zisserman (2014)描述了VGG的其他几种变体。事实上,在引入新架构时,提出具有不同速度-准确度权衡的网络家族已成为常态。
8.2.3. 训练¶
由于VGG-11的计算量比AlexNet大,我们构建一个通道数较少的网络。这对于在Fashion-MNIST上训练已经足够了。模型训练过程与8.1节中的AlexNet类似。再次观察验证损失和训练损失之间的紧密匹配,这表明只有少量的过拟合。
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn)
trainer.fit(model, data)
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer.fit(model, data)
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer.fit(model, data)
trainer = d2l.Trainer(max_epochs=10)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
with d2l.try_gpu():
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer.fit(model, data)
8.2.4. 小结¶
有人可能会说,VGG是第一个真正现代的卷积神经网络。虽然AlexNet引入了许多使深度学习能够大规模有效的组件,但可以说VGG引入了关键特性,如多卷积块和对深而窄网络的偏好。它也是第一个实际上是整个相似参数化模型家族的网络,为从业者提供了在复杂性和速度之间的充分权衡。这也是现代深度学习框架大放异彩的地方。不再需要生成XML配置文件来指定网络,而是通过简单的Python代码来组装这些网络。
最近,ParNet (Goyal et al., 2021)证明了通过大量并行计算,使用更浅的架构可以实现有竞争力的性能。这是一个令人兴奋的发展,并有希望在未来影响架构设计。不过,在本章的其余部分,我们将遵循过去十年的科学进步路径。
8.2.5. 练习¶
与AlexNet相比,VGG在计算上要慢得多,并且它也需要更多的GPU内存。
比较AlexNet和VGG所需的参数数量。
比较卷积层和全连接层中使用的浮点运算次数。
你如何减少全连接层产生的计算成本?
在显示与网络各层相关的维度时,我们只看到与八个块(加上一些辅助变换)相关的信息,尽管网络有11层。剩下的三层去哪里了?
使用VGG论文(Simonyan and Zisserman, 2014)中的表1来构建其他常见模型,如VGG-16或VGG-19。
将Fashion-MNIST的分辨率从\(28 \times 28\)上采样八倍到\(224 \times 224\)维度是非常浪费的。尝试修改网络架构和分辨率转换,例如,将其输入改为56或84维度。你能在不降低网络准确性的情况下做到这一点吗?请参阅VGG论文(Simonyan and Zisserman, 2014)以获取关于在下采样前添加更多非线性的想法。