电子说
随着我们设计越来越深的网络,了解添加层如何增加网络的复杂性和表现力变得势在必行。更重要的是设计网络的能力,其中添加层使网络严格更具表现力而不仅仅是不同。为了取得一些进展,我们需要一点数学知识。
import torch from torch import nn from torch.nn import functional as F 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.6.1. 函数类
考虑F,特定网络架构(连同学习率和其他超参数设置)可以达到的功能类别。也就是说,对于所有 f∈F存在一些参数集(例如,权重和偏差),可以通过在合适的数据集上进行训练来获得。让我们假设f∗是我们真正想要找到的“真实”功能。如果它在F,我们的状态很好,但通常我们不会那么幸运。相反,我们将尝试找到一些fF∗这是我们最好的选择 F. 例如,给定一个具有特征的数据集 X和标签y,我们可以尝试通过解决以下优化问题来找到它:
(8.6.1)fF∗=defargminfL(X,y,f) subject to f∈F.
我们知道正则化 (Morozov,1984 年,Tikhonov 和 Arsenin,1977 年)可以控制复杂度F并实现一致性,因此更大的训练数据通常会带来更好的效果fF∗. 唯一合理的假设是,如果我们设计一个不同的、更强大的架构F′我们应该取得更好的结果。换句话说,我们期望fF′∗ 比“更好”fF∗. 然而,如果 F⊈F′甚至不能保证这会发生。实际上,fF′∗可能会更糟。如图 8.6.1所示,对于非嵌套函数类,较大的函数类并不总是向“真实”函数靠拢f∗. 例如,在图 8.6.1的左侧,虽然F3更接近f∗比F1,F6 远离并且不能保证进一步增加复杂性可以减少距离f∗. 对于嵌套函数类,其中 F1⊆…⊆F6在图 8.6.1右侧,我们可以从非嵌套函数类中避免上述问题。
图 8.6.1对于非嵌套函数类,更大(用面积表示)的函数类并不能保证更接近“真实”函数(f∗). 这不会发生在嵌套函数类中。
因此,只有当较大的函数类包含较小的函数类时,我们才能保证增加它们会严格增加网络的表达能力。对于深度神经网络,如果我们可以将新添加的层训练成恒等函数 f(x)=x,新模型将与原始模型一样有效。由于新模型可能会得到更好的解决方案来拟合训练数据集,因此添加的层可能更容易减少训练错误。
这是He等人提出的问题。( 2016 )在处理非常深的计算机视觉模型时考虑。他们提出的残差网络( ResNet )的核心思想是,每个附加层都应该更容易地包含身份函数作为其元素之一。这些考虑相当深刻,但它们导致了一个非常简单的解决方案,即残差块。凭借它,ResNet 在 2015 年赢得了 ImageNet 大规模视觉识别挑战赛。该设计对如何构建深度神经网络产生了深远的影响。例如,残差块已添加到循环网络中 (Kim等人,2017 年,普拉卡什等。, 2016 年)。同样,Transformers (Vaswani等人,2017 年)使用它们有效地堆叠多层网络。它也被用于图神经网络 (Kipf 和 Welling,2016 年),并且作为一个基本概念,它已被广泛用于计算机视觉 (Redmon 和 Farhadi,2018 年,Ren等人,2015 年)。请注意,残差网络早于高速公路网络 (Srivastava等人,2015 年)这有一些共同的动机,尽管没有围绕身份函数进行优雅的参数化。
8.6.2. 残差块
让我们关注神经网络的局部部分,如图 8.6.2所示。表示输入x. 我们假设我们想要通过学习获得的期望底层映射是f(x), 用作顶部激活函数的输入。左边虚线框内的部分必须直接学习映射f(x). 右边虚线框内的部分需要学习残差映射 g(x)=f(x)−x,这就是残差块的名称来源。如果身份映射 f(x)=x是所需的底层映射,残差映射相当于g(x)=0从而更容易学习:我们只需要将虚线框内的上层权重层(例如全连接层和卷积层)的权重和偏差推到零即可。右图说明了 ResNet的残差块,其中实线承载层输入x加法运算符称为残差连接(或快捷连接)。使用残差块,输入可以通过跨层的残差连接更快地向前传播。事实上,残差块可以被认为是多分支 Inception 块的一个特例:它有两个分支,其中一个是恒等映射。
图 8.6.2在一个规则块中(左),虚线框内的部分必须直接学习映射f(x). 在残差块中(右),虚线框内的部分需要学习残差映射 g(x)=f(x)−x, 进行身份映射f(x)=x更容易学习。
ResNet 完全遵循 VGG 的3×3卷积层设计。残差块有两个3×3具有相同输出通道数的卷积层。每个卷积层后跟一个批量归一化层和一个 ReLU 激活函数。然后,我们跳过这两个卷积操作,将输入直接添加到最终的 ReLU 激活函数之前。这种设计要求两个卷积层的输出必须与输入具有相同的形状,这样才能将它们相加。如果我们想改变频道的数量,我们需要引入一个额外的 1×1卷积层将输入转换为加法运算所需的形状。让我们看看下面的代码。
class Residual(nn.Module): #@save """The Residual block of ResNet models.""" def __init__(self, num_channels, use_1x1conv=False, strides=1): super().__init__() self.conv1 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1, stride=strides) self.conv2 = nn.LazyConv2d(num_channels, kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.LazyConv2d(num_channels, kernel_size=1, stride=strides) else: self.conv3 = None self.bn1 = nn.LazyBatchNorm2d() self.bn2 = nn.LazyBatchNorm2d() def forward(self, X): Y = F.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) Y += X return F.relu(Y)
class Residual(nn.Block): #@save """The Residual block of ResNet models.""" def __init__(self, num_channels, use_1x1conv=False, strides=1, **kwargs): super().__init__(**kwargs) self.conv1 = nn.Conv2D(num_channels, kernel_size=3, padding=1, strides=strides) self.conv2 = nn.Conv2D(num_channels, kernel_size=3, padding=1) if use_1x1conv: self.conv3 = nn.Conv2D(num_channels, kernel_size=1, strides=strides) else: self.conv3 = None self.bn1 = nn.BatchNorm() self.bn2 = nn.BatchNorm() def forward(self, X): Y = npx.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) return npx.relu(Y + X)
class Residual(nn.Module): #@save """The Residual block of ResNet models.""" num_channels: int use_1x1conv: bool = False strides: tuple = (1, 1) training: bool = True def setup(self): self.conv1 = nn.Conv(self.num_channels, kernel_size=(3, 3), padding='same', strides=self.strides) self.conv2 = nn.Conv(self.num_channels, kernel_size=(3, 3), padding='same') if self.use_1x1conv: self.conv3 = nn.Conv(self.num_channels, kernel_size=(1, 1), strides=self.strides) else: self.conv3 = None self.bn1 = nn.BatchNorm(not self.training) self.bn2 = nn.BatchNorm(not self.training) def __call__(self, X): Y = nn.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3: X = self.conv3(X) Y += X return nn.relu(Y)
class Residual(tf.keras.Model): #@save """The Residual block of ResNet models.""" def __init__(self, num_channels, use_1x1conv=False, strides=1): super().__init__() self.conv1 = tf.keras.layers.Conv2D(num_channels, padding='same', kernel_size=3, strides=strides) self.conv2 = tf.keras.layers.Conv2D(num_channels, kernel_size=3, padding='same') self.conv3 = None if use_1x1conv: self.conv3 = tf.keras.layers.Conv2D(num_channels, kernel_size=1, strides=strides) self.bn1 = tf.keras.layers.BatchNormalization() self.bn2 = tf.keras.layers.BatchNormalization() def call(self, X): Y = tf.keras.activations.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) if self.conv3 is not None: X = self.conv3(X) Y += X return tf.keras.activations.relu(Y)
此代码生成两种类型的网络:一种是我们在应用 ReLU 非线性之前将输入添加到输出 use_1x1conv=False,另一种是我们通过1×1添加前的卷积。 图 8.6.3说明了这一点。
图 8.6.3 ResNet 块有无1×1卷积,将输入转换为加法运算所需的形状。
现在让我们看一下输入和输出形状相同的情况,其中1×1不需要卷积。
blk = Residual(3) X = torch.randn(4, 3, 6, 6) blk(X).shape
torch.Size([4, 3, 6, 6])
blk = Residual(3) blk.initialize() X = np.random.randn(4, 3, 6, 6) blk(X).shape
(4, 3, 6, 6)
blk = Residual(3) X = jax.random.normal(d2l.get_key(), (4, 6, 6, 3)) blk.init_with_output(d2l.get_key(), X)[0].shape
(4, 6, 6, 3)
blk = Residual(3) X = tf.random.normal((4, 6, 6, 3)) Y = blk(X) Y.shape
TensorShape([4, 6, 6, 3])
我们还可以选择在增加输出通道数量的同时将输出高度和宽度减半。在这种情况下,我们使用 1×1卷积通过use_1x1conv=True. 这在每个 ResNet 块的开头派上用场,可以通过strides=2.
blk = Residual(6, use_1x1conv=True, strides=2) blk(X).shape
torch.Size([4, 6, 3, 3])
blk = Residual(6, use_1x1conv=True, strides=2) blk.initialize() blk(X).shape
(4, 6, 3, 3)
blk = Residual(6, use_1x1conv=True, strides=(2, 2)) blk.init_with_output(d2l.get_key(), X)[0].shape
(4, 3, 3, 6)
blk = Residual(6, use_1x1conv=True, strides=2) blk(X).shape
TensorShape([4, 3, 3, 6])
8.6.3. ResNet模型
ResNet 的前两层与我们之前描述的 GoogLeNet 相同:7×7具有 64 个输出通道且步幅为 2 的卷积层之后是3×3 步幅为2的最大池化层。不同之处在于ResNet中每个卷积层之后添加的批量归一化层。
class ResNet(d2l.Classifier): def b1(self): return nn.Sequential( nn.LazyConv2d(64, kernel_size=7, stride=2, padding=3), nn.LazyBatchNorm2d(), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
class ResNet(d2l.Classifier): def b1(self): net = nn.Sequential() net.add(nn.Conv2D(64, kernel_size=7, strides=2, padding=3), nn.BatchNorm(), nn.Activation('relu'), nn.MaxPool2D(pool_size=3, strides=2, padding=1)) return net
class ResNet(d2l.Classifier): arch: tuple lr: float = 0.1 num_classes: int = 10 training: bool = True def setup(self): self.net = self.create_net() def b1(self): return nn.Sequential([ nn.Conv(64, kernel_size=(7, 7), strides=(2, 2), padding='same'), nn.BatchNorm(not self.training), nn.relu, lambda x: nn.max_pool(x, window_shape=(3, 3), strides=(2, 2), padding='same')])
class ResNet(d2l.Classifier): def b1(self): return tf.keras.models.Sequential([ tf.keras.layers.Conv2D(64, kernel_size=7, strides=2, padding='same'), tf.keras.layers.BatchNormalization(), tf.keras.layers.Activation('relu'), tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')])
GoogLeNet 使用由 Inception 块组成的四个模块。然而,ResNet 使用了四个由残差块组成的模块,每个模块使用了几个具有相同输出通道数的残差块。第一个模块中的通道数与输入通道数相同。由于已经使用了步幅为 2 的最大池化层,因此没有必要减少高度和宽度。在每个后续模块的第一个残差块中,通道数与前一个模块相比增加了一倍,高度和宽度减半。
@d2l.add_to_class(ResNet) def block(self, num_residuals, num_channels, first_block=False): blk = [] for i in range(num_residuals): if i == 0 and not first_block: blk.append(Residual(num_channels, use_1x1conv=True, strides=2)) else: blk.append(Residual(num_channels)) return nn.Sequential(*blk)
@d2l.add_to_class(ResNet) def block(self, num_residuals, num_channels, first_block=False): blk = nn.Sequential() for i in range(num_residuals): if i == 0 and not first_block: blk.add(Residual(num_channels, use_1x1conv=True, strides=2)) else: blk.add(Residual(num_channels)) return blk
@d2l.add_to_class(ResNet) def block(self, num_residuals, num_channels, first_block=False): blk = [] for i in range(num_residuals): if i == 0 and not first_block: blk.append(Residual(num_channels, use_1x1conv=True, strides=(2, 2), training=self.training)) else: blk.append(Residual(num_channels, training=self.training)) return nn.Sequential(blk)
@d2l.add_to_class(ResNet) def block(self, num_residuals, num_channels, first_block=False): blk = tf.keras.models.Sequential() for i in range(num_residuals): if i == 0 and not first_block: blk.add(Residual(num_channels, use_1x1conv=True, strides=2)) else: blk.add(Residual(num_channels)) return blk
然后,我们将所有模块添加到 ResNet。这里,每个模块使用两个残差块。最后,就像 GoogLeNet 一样,我们添加了一个全局平均池化层,然后是全连接层输出。
@d2l.add_to_class(ResNet) def __init__(self, arch, lr=0.1, num_classes=10): super(ResNet, self).__init__() self.save_hyperparameters() self.net = nn.Sequential(self.b1()) for i, b in enumerate(arch): self.net.add_module(f'b{i+2}', self.block(*b, first_block=(i==0))) self.net.add_module('last', nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.LazyLinear(num_classes))) self.net.apply(d2l.init_cnn)
@d2l.add_to_class(ResNet) def __init__(self, arch, lr=0.1, num_classes=10): super(ResNet, self).__init__() self.save_hyperparameters() self.net = nn.Sequential() self.net.add(self.b1()) for i, b in enumerate(arch): self.net.add(self.block(*b, first_block=(i==0))) self.net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes)) self.net.initialize(init.Xavier())
@d2l.add_to_class(ResNet) def create_net(self): net = nn.Sequential([self.b1()]) for i, b in enumerate(self.arch): net.layers.extend([self.block(*b, first_block=(i==0))]) net.layers.extend([nn.Sequential([ # Flax does not provide a GlobalAvg2D layer lambda x: nn.avg_pool(x, window_shape=x.shape[1:3], strides=x.shape[1:3], padding='valid'), lambda x: x.reshape((x.shape[0], -1)), nn.Dense(self.num_classes)])]) return net
@d2l.add_to_class(ResNet) def __init__(self, arch, lr=0.1, num_classes=10): super(ResNet, self).__init__() self.save_hyperparameters() self.net = tf.keras.models.Sequential(self.b1()) for i, b in enumerate(arch): self.net.add(self.block(*b, first_block=(i==0))) self.net.add(tf.keras.models.Sequential([ tf.keras.layers.GlobalAvgPool2D(), tf.keras.layers.Dense(units=num_classes)]))
每个模块有 4 个卷积层(不包括 1×1卷积层)。与第一个一起 7×7卷积层和最后的全连接层,一共18层。因此,该模型通常被称为 ResNet-18。通过在模块中配置不同数量的通道和残差块,我们可以创建不同的 ResNet 模型,例如更深的 152 层 ResNet-152。虽然 ResNet 的主要架构与 GoogLeNet 相似,但 ResNet 的结构更简单,也更容易修改。所有这些因素导致了 ResNet 的快速和广泛使用。图 8.6.4描绘了完整的 ResNet-18。
图 8.6.4 ResNet-18 架构。
在训练 ResNet 之前,让我们观察输入形状在 ResNet 的不同模块之间是如何变化的。与之前的所有架构一样,分辨率会降低,而通道数量会增加,直到全局平均池化层聚合所有特征为止。
class ResNet18(ResNet): def __init__(self, lr=0.1, num_classes=10): super().__init__(((2, 64), (2, 128), (2, 256), (2, 512)), lr, num_classes) ResNet18().layer_summary((1, 1, 96, 96))
Sequential output shape: torch.Size([1, 64, 24, 24]) Sequential output shape: torch.Size([1, 64, 24, 24]) Sequential output shape: torch.Size([1, 128, 12, 12]) Sequential output shape: torch.Size([1, 256, 6, 6]) Sequential output shape: torch.Size([1, 512, 3, 3]) Sequential output shape: torch.Size([1, 10])
class ResNet18(ResNet): def __init__(self, lr=0.1, num_classes=10): super().__init__(((2, 64), (2, 128), (2, 256), (2, 512)), lr, num_classes) ResNet18().layer_summary((1, 1, 96, 96))
Sequential output shape: (1, 64, 24, 24) Sequential output shape: (1, 64, 24, 24) Sequential output shape: (1, 128, 12, 12) Sequential output shape: (1, 256, 6, 6) Sequential output shape: (1, 512, 3, 3) GlobalAvgPool2D output shape: (1, 512, 1, 1) Dense output shape: (1, 10)
class ResNet18(ResNet): arch: tuple = ((2, 64), (2, 128), (2, 256), (2, 512)) lr: float = 0.1 num_classes: int = 10 ResNet18(training=False).layer_summary((1, 96, 96, 1))
Sequential output shape: (1, 24, 24, 64) Sequential output shape: (1, 24, 24, 64) Sequential output shape: (1, 12, 12, 128) Sequential output shape: (1, 6, 6, 256) Sequential output shape: (1, 3, 3, 512) Sequential output shape: (1, 10)
class ResNet18(ResNet): def __init__(self, lr=0.1, num_classes=10): super().__init__(((2, 64), (2, 128), (2, 256), (2, 512)), lr, num_classes) ResNet18().layer_summary((1, 96, 96, 1))
Sequential output shape: (1, 24, 24, 64) Sequential output shape: (1, 24, 24, 64) Sequential output shape: (1, 12, 12, 128) Sequential output shape: (1, 6, 6, 256) Sequential output shape: (1, 3, 3, 512) Sequential output shape: (1, 10)
8.6.4. 训练
我们像以前一样在 Fashion-MNIST 数据集上训练 ResNet。ResNet 是一个非常强大和灵活的架构。捕获训练和验证损失的图说明了两个图之间的显着差距,训练损失明显较低。对于这种灵活性的网络,更多的训练数据将在缩小差距和提高准确性方面提供显着的好处。
model = ResNet18(lr=0.01) trainer = d2l.Trainer(max_epochs=10, num_gpus=1) data = d2l.FashionMNIST(batch_size=128, resize=(96, 96)) model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn) trainer.fit(model, data)
model = ResNet18(lr=0.01) trainer = d2l.Trainer(max_epochs=10, num_gpus=1) data = d2l.FashionMNIST(batch_size=128, resize=(96, 96)) trainer.fit(model, data)
model = ResNet18(lr=0.01) trainer = d2l.Trainer(max_epochs=10, num_gpus=1) data = d2l.FashionMNIST(batch_size=128, resize=(96, 96)) trainer.fit(model, data)
trainer = d2l.Trainer(max_epochs=10) data = d2l.FashionMNIST(batch_size=128, resize=(96, 96)) with d2l.try_gpu(): model = ResNet18(lr=0.01) trainer.fit(model, data)
8.6.5. ResNeXt
在 ResNet 的设计中遇到的挑战之一是给定块内的非线性和维度之间的权衡。也就是说,我们可以通过增加层数或增加卷积宽度来增加更多的非线性。另一种策略是增加可以在块之间传输信息的通道数量。不幸的是,后者会带来二次惩罚,因为摄取的计算成本ci 渠道和发射co渠道正比于 O(ci⋅co)(参见我们在 第 7.4 节中的讨论)。
我们可以从图 8.4.1的 Inception 块中得到一些启发 ,它有信息在不同的组中流经块。将多个独立组的思想应用于图 8.6.3的 ResNet 块导致了 ResNeXt 的设计(Xie et al. , 2017)。与 Inception 中变换的大杂烩不同,ResNeXt 在所有分支中采用 相同的变换,从而最大限度地减少了对每个分支进行手动调整的需要。
图 8.6.5 ResNeXt 块。分组卷积的使用g 团体是g比密集卷积快 1 倍。当中间通道的数量增加时,它是一个瓶颈残差块 b小于c.
从中分解一个卷积ci到co频道进入其中一个g大小组ci/g产生g 尺寸输出co/g非常恰当地称为分组卷积。计算成本(按比例)从 O(ci⋅co)到 O(g⋅(ci/g)⋅(co/g))=O(ci⋅co/g),即它是g倍更快。更好的是,生成输出所需的参数数量也从 ci×co矩阵到g更小的矩阵 (ci/g)×(co/g), 又是一个g次减少。在下文中,我们假设两者ci和co被整除g.
这种设计中唯一的挑战是,两者之间没有信息交换g团体。图 8.6.5的 ResNeXt 块 以两种方式修正了这一点:分组卷积与3×3内核夹在两个中间1×1卷积。第二个在改回频道数量方面起着双重作用。好处是我们只支付O(c⋅b)费用1×1内核,可以凑合O(b2/g)费用 3×3内核。类似于第 8.6.2 节中的残差块实现,残差连接被替换(因此泛化)为1×1卷积。
图 8.6.5中的右图提供了生成的网络块的更简洁的摘要。它还将在第 8.8 节中的通用现代 CNN 设计中发挥重要作用 。请注意,分组卷积的想法可以追溯到 AlexNet 的实现 (Krizhevsky等人,2012 年)。当在内存有限的两个 GPU 上分配网络时,该实现将每个 GPU 视为自己的通道,没有任何不良影响。
该类的以下实现ResNeXtBlock作为参数groups(g), 与bot_channels(b) 中间(瓶颈)渠道。最后,当我们需要减少表示的高度和宽度时,我们添加一个步幅2通过设置。use_1x1conv=True, strides=2
class ResNeXtBlock(nn.Module): #@save """The ResNeXt block.""" def __init__(self, num_channels, groups, bot_mul, use_1x1conv=False, strides=1): super().__init__() bot_channels = int(round(num_channels * bot_mul)) self.conv1 = nn.LazyConv2d(bot_channels, kernel_size=1, stride=1) self.conv2 = nn.LazyConv2d(bot_channels, kernel_size=3, stride=strides, padding=1, groups=bot_channels//groups) self.conv3 = nn.LazyConv2d(num_channels, kernel_size=1, stride=1) self.bn1 = nn.LazyBatchNorm2d() self.bn2 = nn.LazyBatchNorm2d() self.bn3 = nn.LazyBatchNorm2d() if use_1x1conv: self.conv4 = nn.LazyConv2d(num_channels, kernel_size=1, stride=strides) self.bn4 = nn.LazyBatchNorm2d() else: self.conv4 = None def forward(self, X): Y = F.relu(self.bn1(self.conv1(X))) Y = F.relu(self.bn2(self.conv2(Y))) Y = self.bn3(self.conv3(Y)) if self.conv4: X = self.bn4(self.conv4(X)) return F.relu(Y + X)
class ResNeXtBlock(nn.Block): #@save """The ResNeXt block.""" def __init__(self, num_channels, groups, bot_mul, use_1x1conv=False, strides=1, **kwargs): super().__init__(**kwargs) bot_channels = int(round(num_channels * bot_mul)) self.conv1 = nn.Conv2D(bot_channels, kernel_size=1, padding=0, strides=1) self.conv2 = nn.Conv2D(bot_channels, kernel_size=3, padding=1, strides=strides, groups=bot_channels//groups) self.conv3 = nn.Conv2D(num_channels, kernel_size=1, padding=0, strides=1) self.bn1 = nn.BatchNorm() self.bn2 = nn.BatchNorm() self.bn3 = nn.BatchNorm() if use_1x1conv: self.conv4 = nn.Conv2D(num_channels, kernel_size=1, strides=strides) self.bn4 = nn.BatchNorm() else: self.conv4 = None def forward(self, X): Y = npx.relu(self.bn1(self.conv1(X))) Y = npx.relu(self.bn2(self.conv2(Y))) Y = self.bn3(self.conv3(Y)) if self.conv4: X = self.bn4(self.conv4(X)) return npx.relu(Y + X)
class ResNeXtBlock(nn.Module): #@save """The ResNeXt block.""" num_channels: int groups: int bot_mul: int use_1x1conv: bool = False strides: tuple = (1, 1) training: bool = True def setup(self): bot_channels = int(round(self.num_channels * self.bot_mul)) self.conv1 = nn.Conv(bot_channels, kernel_size=(1, 1), strides=(1, 1)) self.conv2 = nn.Conv(bot_channels, kernel_size=(3, 3), strides=self.strides, padding='same', feature_group_count=bot_channels//self.groups) self.conv3 = nn.Conv(self.num_channels, kernel_size=(1, 1), strides=(1, 1)) self.bn1 = nn.BatchNorm(not self.training) self.bn2 = nn.BatchNorm(not self.training) self.bn3 = nn.BatchNorm(not self.training) if self.use_1x1conv: self.conv4 = nn.Conv(self.num_channels, kernel_size=(1, 1), strides=self.strides) self.bn4 = nn.BatchNorm(not self.training) else: self.conv4 = None def __call__(self, X): Y = nn.relu(self.bn1(self.conv1(X))) Y = nn.relu(self.bn2(self.conv2(Y))) Y = self.bn3(self.conv3(Y)) if self.conv4: X = self.bn4(self.conv4(X)) return nn.relu(Y + X)
class ResNeXtBlock(tf.keras.Model): #@save """The ResNeXt block.""" def __init__(self, num_channels, groups, bot_mul, use_1x1conv=False, strides=1): super().__init__() bot_channels = int(round(num_channels * bot_mul)) self.conv1 = tf.keras.layers.Conv2D(bot_channels, 1, strides=1) self.conv2 = tf.keras.layers.Conv2D(bot_channels, 3, strides=strides, padding="same", groups=bot_channels//groups) self.conv3 = tf.keras.layers.Conv2D(num_channels, 1, strides=1) self.bn1 = tf.keras.layers.BatchNormalization() self.bn2 = tf.keras.layers.BatchNormalization() self.bn3 = tf.keras.layers.BatchNormalization() if use_1x1conv: self.conv4 = tf.keras.layers.Conv2D(num_channels, 1, strides=strides) self.bn4 = tf.keras.layers.BatchNormalization() else: self.conv4 = None def call(self, X): Y = tf.keras.activations.relu(self.bn1(self.conv1(X))) Y = tf.keras.activations.relu(self.bn2(self.conv2(Y))) Y = self.bn3(self.conv3(Y)) if self.conv4: X = self.bn4(self.conv4(X)) return tf.keras.activations.relu(Y + X)
它的使用与前面讨论的完全相似ResNetBlock。例如,当使用 ( ) 时,输入和输出的形状相同。或者,设置 输出高度和宽度的一半。use_1x1conv=False, strides=1use_1x1conv=True, strides=2
blk = ResNeXtBlock(32, 16, 1) X = torch.randn(4, 32, 96, 96) blk(X).shape
torch.Size([4, 32, 96, 96])
blk = ResNeXtBlock(32, 16, 1) blk.initialize() X = np.random.randn(4, 32, 96, 96) blk(X).shape
(4, 32, 96, 96)
blk = ResNeXtBlock(32, 16, 1) X = jnp.zeros((4, 96, 96, 32)) blk.init_with_output(d2l.get_key(), X)[0].shape
(4, 96, 96, 32)
blk = ResNeXtBlock(32, 16, 1) X = tf.random.normal((4, 96, 96, 32)) Y = blk(X) Y.shape
TensorShape([4, 96, 96, 32])
8.6.6. 总结与讨论
嵌套函数类是可取的,因为它们允许我们在增加容量时获得更强大的函数类,而不是细微不同的函数类。实现这一点的一种方法是允许附加层简单地将输入传递到输出。残余连接允许这样做。因此,这改变了形式为简单函数的归纳偏差 f(x)=0看起来像简单的功能 f(x)=x.
残差映射可以更容易地学习身份函数,例如将权重层中的参数推为零。我们可以通过残差块来训练有效的深度神经网络。输入可以通过跨层的剩余连接更快地向前传播。因此,我们可以训练更深层次的网络。例如,最初的 ResNet 论文( He et al. , 2016 )允许多达 152 层。残差网络的另一个好处是它允许我们添加层,初始化为恒等函数,在培训过程。毕竟,层的默认行为是让数据不加改变地通过。在某些情况下,这可以加速超大型网络的训练。
在残差连接之前,引入了带有门控单元的旁路路径,以有效地训练超过 100 层的高速公路网络 (Srivastava等人,2015 年)。使用身份函数作为绕过路径,ResNet 在多个计算机视觉任务上表现非常出色。残差连接对后续深度神经网络的设计产生了重大影响,包括卷积和顺序性质。正如我们稍后将介绍的,Transformer 架构 (Vaswani等人,2017 年)采用残差连接(连同其他设计选择),并且在语言、视觉、语音和强化学习等不同领域普遍存在。
ResNeXt 是卷积神经网络的设计如何随着时间的推移而演变的一个例子:通过更节俭地计算并与激活的大小(通道数)进行权衡,它允许以更低的成本更快、更准确的网络. 查看分组卷积的另一种方法是考虑卷积权重的块对角矩阵。请注意,有很多这样的“技巧”可以提高网络的效率。例如,ShiftNet (Wu等人,2018 年)模仿了 3×3卷积,简单地通过向通道添加移位激活,提供增加的功能复杂性,这次没有任何计算成本。
到目前为止我们讨论的设计的一个共同特征是网络设计是相当手动的,主要依靠设计者的独创性来找到“正确的”网络超参数。虽然显然可行,但就人力时间而言,它也非常昂贵,并且无法保证结果在任何意义上都是最佳的。在 第 8.8 节中,我们将讨论一些以更自动化的方式获得高质量网络的策略。特别是,我们将回顾导致 RegNetX/Y 模型的 网络设计空间的概念(Radosavovic等人,2020 年)。
8.6.7. 练习
图 8.4.1中的 Inception 块 和残差块的主要区别是什么?他们如何在计算、准确性和他们可以描述的函数类别方面进行比较?
参考 ResNet 论文( He et al. , 2016 )中的表 1 来实现网络的不同变体。
对于更深的网络,ResNet 引入了一个“瓶颈”架构来降低模型的复杂性。尝试实施它。
在ResNet的后续版本中,作者将“卷积、批量归一化和激活”结构改为“批量归一化、激活和卷积”结构。自己进行此改进。参见He等人的图 1 。( 2016 )了解详情。
为什么我们不能无限制地增加函数的复杂性,即使函数类是嵌套的?
全部0条评论
快来发表一下你的评论吧 !