8.3. 网络中的网络(NiN)¶ 在 SageMaker Studio Lab 中打开 Notebook
LeNet、AlexNet和VGG都有一个共同的设计模式:通过一系列的卷积层和汇聚层来利用空间结构提取特征,然后通过全连接层对表示进行后处理。AlexNet和VGG对LeNet的改进主要在于这两个网络如何加宽和加深这两个模块。
这种设计带来了两个主要的挑战。首先,架构末端的大量全连接层消耗了巨量的参数。例如,即使像VGG-11这样简单的模型也需要一个巨大的矩阵,在单精度(FP32)下占用近400MB的RAM。这对于计算是一个重大的障碍,尤其是在移动和嵌入式设备上。毕竟,即使是高端手机的RAM也不超过8GB。在VGG发明的那个时代,这个数字要小一个数量级(iPhone 4S有512MB)。因此,将大部分内存用于图像分类器是难以自圆其说的。
其次,同样不可能在网络的早期添加全连接层来增加非线性的程度:这样做会破坏空间结构,并可能需要更多的内存。
网络中的网络(NiN)块 (Lin et al., 2013) 提供了一种替代方案,能够用一个简单的策略解决这两个问题。它们的提出基于一个非常简单的洞见:(i)使用 \(1 \times 1\) 卷积来增加跨通道激活的局部非线性;(ii)使用全局平均汇聚来整合最后一个表示层中的所有位置。请注意,如果没有增加的非线性,全局平均汇聚不会那么有效。让我们来详细探讨一下。
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.3.1. NiN块¶
回顾 7.4.3节。我们在其中说过,卷积层的输入和输出由四维张量组成,其轴对应于样本、通道、高度和宽度。还记得全连接层的输入和输出通常是对应于样本和特征的二维张量。NiN背后的思想是在每个像素位置(对于每个高度和宽度)应用一个全连接层。由此产生的 \(1 \times 1\) 卷积可以被看作是独立作用于每个像素位置的全连接层。
图 8.3.1阐释了VGG和NiN及其块之间的主要结构差异。请注意NiN块中的差异(初始卷积后跟着 \(1 \times 1\) 卷积,而VGG保留了 \(3 \times 3\) 卷积)以及末尾不再需要一个巨大的全连接层。
图 8.3.1 比较VGG和NiN的架构及其块的架构。¶
def nin_block(out_channels, kernel_size, strides, padding):
return nn.Sequential(
nn.LazyConv2d(out_channels, kernel_size, strides, padding), nn.ReLU(),
nn.LazyConv2d(out_channels, kernel_size=1), nn.ReLU(),
nn.LazyConv2d(out_channels, kernel_size=1), nn.ReLU())
def nin_block(num_channels, kernel_size, strides, padding):
blk = nn.Sequential()
blk.add(nn.Conv2D(num_channels, kernel_size, strides, padding,
activation='relu'),
nn.Conv2D(num_channels, kernel_size=1, activation='relu'),
nn.Conv2D(num_channels, kernel_size=1, activation='relu'))
return blk
def nin_block(out_channels, kernel_size, strides, padding):
return nn.Sequential([
nn.Conv(out_channels, kernel_size, strides, padding),
nn.relu,
nn.Conv(out_channels, kernel_size=(1, 1)), nn.relu,
nn.Conv(out_channels, kernel_size=(1, 1)), nn.relu])
def nin_block(out_channels, kernel_size, strides, padding):
return tf.keras.models.Sequential([
tf.keras.layers.Conv2D(out_channels, kernel_size, strides=strides,
padding=padding),
tf.keras.layers.Activation('relu'),
tf.keras.layers.Conv2D(out_channels, 1),
tf.keras.layers.Activation('relu'),
tf.keras.layers.Conv2D(out_channels, 1),
tf.keras.layers.Activation('relu')])
8.3.2. NiN模型¶
NiN使用了与AlexNet相同的初始卷积大小(它是在AlexNet之后不久提出的)。卷积核大小分别为 \(11\times 11\)、\(5\times 5\) 和 \(3\times 3\),输出通道的数量与AlexNet相匹配。每个NiN块后面都跟着一个步幅为2、窗口形状为 \(3\times 3\) 的最大汇聚层。
NiN与AlexNet和VGG的第二个显著区别是,NiN完全避免了全连接层。取而代之的是,NiN使用一个输出通道数等于标签类别数的NiN块,然后是一个全局平均汇聚层,从而产生一个logits向量。这种设计显著减少了所需的模型参数数量,尽管可能会增加训练时间。
class NiN(d2l.Classifier):
def __init__(self, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
self.net = nn.Sequential(
nin_block(96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2d(3, stride=2),
nin_block(256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2d(3, stride=2),
nin_block(384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2d(3, stride=2),
nn.Dropout(0.5),
nin_block(num_classes, kernel_size=3, strides=1, padding=1),
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten())
self.net.apply(d2l.init_cnn)
class NiN(d2l.Classifier):
def __init__(self, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
self.net = nn.Sequential()
self.net.add(
nin_block(96, kernel_size=11, strides=4, padding=0),
nn.MaxPool2D(pool_size=3, strides=2),
nin_block(256, kernel_size=5, strides=1, padding=2),
nn.MaxPool2D(pool_size=3, strides=2),
nin_block(384, kernel_size=3, strides=1, padding=1),
nn.MaxPool2D(pool_size=3, strides=2),
nn.Dropout(0.5),
nin_block(num_classes, kernel_size=3, strides=1, padding=1),
nn.GlobalAvgPool2D(),
nn.Flatten())
self.net.initialize(init.Xavier())
class NiN(d2l.Classifier):
lr: float = 0.1
num_classes = 10
training: bool = True
def setup(self):
self.net = nn.Sequential([
nin_block(96, kernel_size=(11, 11), strides=(4, 4), padding=(0, 0)),
lambda x: nn.max_pool(x, (3, 3), strides=(2, 2)),
nin_block(256, kernel_size=(5, 5), strides=(1, 1), padding=(2, 2)),
lambda x: nn.max_pool(x, (3, 3), strides=(2, 2)),
nin_block(384, kernel_size=(3, 3), strides=(1, 1), padding=(1, 1)),
lambda x: nn.max_pool(x, (3, 3), strides=(2, 2)),
nn.Dropout(0.5, deterministic=not self.training),
nin_block(self.num_classes, kernel_size=(3, 3), strides=1, padding=(1, 1)),
lambda x: nn.avg_pool(x, (5, 5)), # global avg pooling
lambda x: x.reshape((x.shape[0], -1)) # flatten
])
class NiN(d2l.Classifier):
def __init__(self, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
self.net = tf.keras.models.Sequential([
nin_block(96, kernel_size=11, strides=4, padding='valid'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
nin_block(256, kernel_size=5, strides=1, padding='same'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
nin_block(384, kernel_size=3, strides=1, padding='same'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
tf.keras.layers.Dropout(0.5),
nin_block(num_classes, kernel_size=3, strides=1, padding='same'),
tf.keras.layers.GlobalAvgPool2D(),
tf.keras.layers.Flatten()])
我们创建一个数据样本来看看每个块的输出形状。
NiN().layer_summary((1, 1, 224, 224))
Sequential output shape: torch.Size([1, 96, 54, 54])
MaxPool2d output shape: torch.Size([1, 96, 26, 26])
Sequential output shape: torch.Size([1, 256, 26, 26])
MaxPool2d output shape: torch.Size([1, 256, 12, 12])
Sequential output shape: torch.Size([1, 384, 12, 12])
MaxPool2d output shape: torch.Size([1, 384, 5, 5])
Dropout output shape: torch.Size([1, 384, 5, 5])
Sequential output shape: torch.Size([1, 10, 5, 5])
AdaptiveAvgPool2d output shape: torch.Size([1, 10, 1, 1])
Flatten output shape: torch.Size([1, 10])
NiN().layer_summary((1, 1, 224, 224))
Sequential output shape: (1, 96, 54, 54)
MaxPool2D output shape: (1, 96, 26, 26)
Sequential output shape: (1, 256, 26, 26)
MaxPool2D output shape: (1, 256, 12, 12)
Sequential output shape: (1, 384, 12, 12)
MaxPool2D output shape: (1, 384, 5, 5)
Dropout output shape: (1, 384, 5, 5)
Sequential output shape: (1, 10, 5, 5)
GlobalAvgPool2D output shape: (1, 10, 1, 1)
Flatten output shape: (1, 10)
[22:45:22] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
NiN(training=False).layer_summary((1, 224, 224, 1))
Sequential output shape: (1, 54, 54, 96)
function output shape: (1, 26, 26, 96)
Sequential output shape: (1, 26, 26, 256)
function output shape: (1, 12, 12, 256)
Sequential output shape: (1, 12, 12, 384)
function output shape: (1, 5, 5, 384)
Dropout output shape: (1, 5, 5, 384)
Sequential output shape: (1, 5, 5, 10)
function output shape: (1, 1, 1, 10)
function output shape: (1, 10)
NiN().layer_summary((1, 224, 224, 1))
Sequential output shape: (1, 54, 54, 96)
MaxPooling2D output shape: (1, 26, 26, 96)
Sequential output shape: (1, 26, 26, 256)
MaxPooling2D output shape: (1, 12, 12, 256)
Sequential output shape: (1, 12, 12, 384)
MaxPooling2D output shape: (1, 5, 5, 384)
Dropout output shape: (1, 5, 5, 384)
Sequential output shape: (1, 5, 5, 10)
GlobalAveragePooling2D output shape: (1, 10)
Flatten output shape: (1, 10)
8.3.3. 训练¶
和之前一样,我们使用Fashion-MNIST来训练模型,使用的优化器与我们用于AlexNet和VGG的相同。
model = NiN(lr=0.05)
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 = NiN(lr=0.05)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer.fit(model, data)
model = NiN(lr=0.05)
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 = NiN(lr=0.05)
trainer.fit(model, data)
8.3.4. 小结¶
NiN的参数比AlexNet和VGG少得多。这主要源于它不需要巨大的全连接层。相反,它在网络主体的最后阶段之后使用全局平均汇聚来聚合所有图像位置。这避免了昂贵的(学习的)降维操作,并用简单的平均值取而代之。当时让研究人员感到惊讶的是,这种平均操作并没有损害准确性。请注意,对低分辨率表示(具有许多通道)进行平均也增加了网络可以处理的平移不变性的量。
选择更少的宽核卷积,并用 \(1 \times 1\) 卷积替换它们,进一步有助于减少参数。它可以在任何给定位置提供跨通道的大量非线性。 \(1 \times 1\) 卷积和全局平均汇聚都对后来的CNN设计产生了重大影响。
8.3.5. 练习¶
为什么每个NiN块有两个 \(1\times 1\) 卷积层?将它们的数量增加到三个。将它们的数量减少到一个。会发生什么变化?
如果用 \(3 \times 3\) 卷积替换 \(1 \times 1\) 卷积会发生什么变化?
如果用全连接层替换全局平均汇聚会发生什么(速度、准确率、参数数量)?
计算NiN的资源使用情况。
参数数量是多少?
计算量是多少?
训练期间需要多少内存?
预测期间需要多少内存?
将 \(384 \times 5 \times 5\) 的表示一步减少到 \(10 \times 5 \times 5\) 的表示可能会有什么问题?
使用VGG中导致VGG-11、VGG-16和VGG-19的结构设计决策来设计一系列类NiN网络。