ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

残差网络(ResNet)与 ResNeXt 实战:从残差块到 18 层深度 CNN 的多框架实现指南

残差网络(ResNet)与 ResNeXt 实战:从残差块到 18 层深度 CNN 的多框架实现指南 文档教程人工智能深度学习NLP计算机视觉强化学习【免费下载链接】d2l-enInteractive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.项目地址https://gitcode.com/gh_mirrors/d2/d2l-en点击查看免费下载本指南基于 D2LDive into Deep Learning开源仓库 chapter_convolutional-modern/resnet.md 展开系统讲解残差网络ResNet与 ResNeXt 的数学动机、残差块设计、ResNet-18 模型组装以及分组卷积的加速原理并提供 MXNet、PyTorch、TensorFlow 与 JAX 四个框架的完整可运行代码。读完本文你将掌握残差块为何能训练更深网络、如何用1×1卷积调整通道与分辨率、如何用layer_summary观察特征图尺寸变化以及 ResNeXt 如何通过分组卷积以更低的计算代价获得更强表达力。从函数类Function Classes看网络加深的数学动机在设计更深网络之前必须先回答一个问题增加层数为什么可能提升网络的表达能力又为什么可能适得其反设 $\mathcal{F}$ 为某一特定网络架构连同学习率等超参数设置所能表示的函数集合即对任意 $f \in \mathcal{F}$都存在一组可通过训练获得的参数权重与偏置使其成立。假设 $f^$ 是我们真正想找到的真相函数通常我们无法恰好命中它只能在 $\mathcal{F}$ 内寻找最优近似 $f^_\mathcal{F}$。给定特征 $\mathbf{X}$ 与标签 $\mathbf{y}$这一过程可形式化为如下优化问题$$f^*_\mathcal{F} \stackrel{\textrm{def}}{} \mathop{\mathrm{argmin}}_f L(\mathbf{X}, \mathbf{y}, f) \textrm{ subject to } f \in \mathcal{F}.$$正则化regularization可以控制 $\mathcal{F}$ 的复杂度并获得一致性因此训练数据越多通常 $f^_\mathcal{F}$ 越好。直觉上设计一个更强大的架构 $\mathcal{F}$ 应当带来更好的结果即 $f^{\mathcal{F}}$ 优于 $f^*{\mathcal{F}}$。但前提是 $\mathcal{F} \subseteq \mathcal{F}$嵌套关系若函数类互不包含则无法保证增大函数类会逼近真相函数 $f^$$f^_{\mathcal{F}}$ 甚至可能更差。因此在深度神经网络中只有更大的函数类包含更小的函数类时增加复杂度才能严格提升网络的表达能力。具体做法是如果能把新加入的层训练成恒等映射 $f(\mathbf{x}) \mathbf{x}$新模型至少与原模型同样有效而新模型又可能找到更优解来拟合训练数据因此添加层数可能使训练误差更易降低。这正是 He et al. 2016 在超深视觉模型上思考的问题。他们提出的**残差网络Residual NetworkResNet**的核心思想是每一个新增的层都应该更容易地把恒等映射作为其元素之一。这一深刻思考最终导出了一个极其简单的解决方案——残差块Residual Block。凭借残差块ResNet 赢得了 2015 年 ImageNet 大规模视觉识别挑战赛ILSVRC其设计深刻影响了后续深度网络的构建残差块被加入循环网络Transformer 用它高效堆叠多层网络图神经网络与计算机视觉领域的众多模型也广泛使用这一基本概念。值得注意的是残差网络并非凭空出现早其一步的高速网络Highway Networks共享了部分动机但缺少了围绕恒等映射的优雅参数化设计。残差块Residual Blocks让恒等映射更容易被学到从直接学习映射到学习残差映射聚焦神经网络中的一个局部模块设输入为 $\mathbf{x}$我们希望通过学习得到的底层映射 $f(\mathbf{x})$ 被送入顶部的激活函数。在普通模块左图中虚线框内的部分必须直接学习$f(\mathbf{x})$而在残差块右图中虚线框内的部分只需学习残差映射$g(\mathbf{x}) f(\mathbf{x}) - \mathbf{x}$——这正是残差块名称的由来。当期望的底层映射恰好是恒等映射 $f(\mathbf{x}) \mathbf{x}$ 时残差映射退化为 $g(\mathbf{x}) 0$学起来容易得多只需把虚线框内上层权重层如全连接层或卷积层的权重与偏置推向零即可。右图中承载输入 $\mathbf{x}$ 直达加法运算符的那条实线被称为残差连接residual connection也叫捷径连接shortcut connection。借助残差连接输入可以更快地跨层前向传播。事实上残差块可以看作是多分支 Inception 块的一个特例它只有两个分支其中一个就是恒等映射。残差块的具体结构ResNet 沿用了 VGG 的完整 $3\times 3$ 卷积层设计。残差块包含两个输出通道数相同的 $3\times 3$ 卷积层每个卷积层后依次跟随批量归一化BatchNorm层与ReLU 激活函数随后跳过这两次卷积操作在最后的 ReLU 激活函数之前把输入直接加到输出上。这种设计要求两个卷积层的输出形状与输入相同才能相加。若需要改变通道数就必须引入一个额外的 $1\times 1$ 卷积层把输入变换到加法运算所需的形状。仓库中对应的原图见 img/resnet-block.svg。多框架实现Residual 残差块类以下代码在四个框架中实现了Residual类构造函数接收三个关键参数num_channels输出通道数两个主卷积层共享use_1x1conv是否使用1×1卷积调整输入的通道/分辨率默认Falsestrides步幅用于在需要时减半空间分辨率默认1。需要特别说明的是PyTorch 版本采用了LazyConv2d、LazyBatchNorm2d等惰性lazy层它们不在构造时立即绑定输入形状而是在第一次前向传播时才推断参数形状从而让num_channels等超参数的定义更加简洁JAX 版本则通过dataclass风格的字段声明超参数并以training标志控制 BatchNorm 的行为nn.BatchNorm(not self.training)表示训练时使用批统计量。%%tab mxnet class Residual(nn.Block): #save The Residual block of ResNet models. def __init__(self, num_channels, use_1x1convFalse, strides1, **kwargs): super().__init__(**kwargs) self.conv1 nn.Conv2D(num_channels, kernel_size3, padding1, stridesstrides) self.conv2 nn.Conv2D(num_channels, kernel_size3, padding1) if use_1x1conv: self.conv3 nn.Conv2D(num_channels, kernel_size1, stridesstrides) 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)%%tab pytorch class Residual(nn.Module): #save The Residual block of ResNet models. def __init__(self, num_channels, use_1x1convFalse, strides1): super().__init__() self.conv1 nn.LazyConv2d(num_channels, kernel_size3, padding1, stridestrides) self.conv2 nn.LazyConv2d(num_channels, kernel_size3, padding1) if use_1x1conv: self.conv3 nn.LazyConv2d(num_channels, kernel_size1, stridestrides) 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)%%tab tensorflow class Residual(tf.keras.Model): #save The Residual block of ResNet models. def __init__(self, num_channels, use_1x1convFalse, strides1): super().__init__() self.conv1 tf.keras.layers.Conv2D(num_channels, paddingsame, kernel_size3, stridesstrides) self.conv2 tf.keras.layers.Conv2D(num_channels, kernel_size3, paddingsame) self.conv3 None if use_1x1conv: self.conv3 tf.keras.layers.Conv2D(num_channels, kernel_size1, stridesstrides) 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)%%tab jax 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), paddingsame, stridesself.strides) self.conv2 nn.Conv(self.num_channels, kernel_size(3, 3), paddingsame) if self.use_1x1conv: self.conv3 nn.Conv(self.num_channels, kernel_size(1, 1), stridesself.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)这段代码生成两类网络当use_1x1convFalse时输入在 ReLU 非线性之前直接加到输出上当use_1x1convTrue时先通过1×1卷积调整通道与分辨率再做加法。两个分支相加后残差连接处的特征可以直接流过整个块这也是深层网络梯度能够顺畅回传的关键。验证残差块的形状行为输入输出同形状不需要1×1卷积当输入与输出形状相同时无需1×1卷积。以形状为(4, 3, 6, 6)批量 4、通道 3、高宽 6×6的随机张量为例%%tab mxnet, pytorch if tab.selected(mxnet): blk Residual(3) blk.initialize() if tab.selected(pytorch): blk Residual(3) X d2l.randn(4, 3, 6, 6) blk(X).shape%%tab tensorflow blk Residual(3) X d2l.normal((4, 6, 6, 3)) Y blk(X) Y.shape%%tab jax blk Residual(3) X jax.random.normal(d2l.get_key(), (4, 6, 6, 3)) blk.init_with_output(d2l.get_key(), X)[0].shape减半高宽、倍增通道需要1×1卷积我们也可以选择在增加输出通道数的同时把输出的高和宽减半此时通过use_1x1convTrue启用1×1卷积并在每个 ResNet 块的开头用strides2降低空间维度%%tab pytorch, mxnet, tensorflow blk Residual(6, use_1x1convTrue, strides2) if tab.selected(mxnet): blk.initialize() blk(X).shape%%tab jax blk Residual(6, use_1x1convTrue, strides(2, 2)) blk.init_with_output(d2l.get_key(), X)[0].shape注意 TensorFlow 与 JAX 采用channels_last通道在最后的张量布局(批量, 高, 宽, 通道)而 MXNet 与 PyTorch 采用channels_first布局(批量, 通道, 高, 宽)JAX 中步幅以元组(2, 2)传入。输入(4, 3, 6, 6)经过strides2的残差块后输出形状为(4, 6, 3, 3)即通道数从 3 增至 6、高宽从 6×6 减半为 3×3。ResNet 模型组装完整的深度网络主干b17×7 卷积 3×3 最大池化ResNet 的前两层与 GoogLeNet 相同输出 64 通道、步幅 2 的7×7卷积层后接步幅 2 的3×3最大池化层。区别在于ResNet 在每个卷积层之后都添加了批量归一化层。%%tab pytorch, mxnet, tensorflow class ResNet(d2l.Classifier): def b1(self): if tab.selected(mxnet): net nn.Sequential() net.add(nn.Conv2D(64, kernel_size7, strides2, padding3), nn.BatchNorm(), nn.Activation(relu), nn.MaxPool2D(pool_size3, strides2, padding1)) return net if tab.selected(pytorch): return nn.Sequential( nn.LazyConv2d(64, kernel_size7, stride2, padding3), nn.LazyBatchNorm2d(), nn.ReLU(), nn.MaxPool2d(kernel_size3, stride2, padding1)) if tab.selected(tensorflow): return tf.keras.models.Sequential([ tf.keras.layers.Conv2D(64, kernel_size7, strides2, paddingsame), tf.keras.layers.BatchNormalization(), tf.keras.layers.Activation(relu), tf.keras.layers.MaxPool2D(pool_size3, strides2, paddingsame)])%%tab jax 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), paddingsame), nn.BatchNorm(not self.training), nn.relu, lambda x: nn.max_pool(x, window_shape(3, 3), strides(2, 2), paddingsame)])残差块模块的组织方式GoogLeNet 使用四个由 Inception 块构成的模块而 ResNet 使用四个由残差块构成的模块每个模块内多个残差块共享相同的输出通道数。第一个模块的通道数与输入通道数相同——由于此前已经使用了步幅 2 的最大池化无需再降低高宽后续每个模块的第一个残差块把通道数翻倍、高宽减半%%tab mxnet d2l.add_to_class(ResNet) def block(self, num_residuals, num_channels, first_blockFalse): blk nn.Sequential() for i in range(num_residuals): if i 0 and not first_block: blk.add(Residual(num_channels, use_1x1convTrue, strides2)) else: blk.add(Residual(num_channels)) return blk%%tab pytorch d2l.add_to_class(ResNet) def block(self, num_residuals, num_channels, first_blockFalse): blk [] for i in range(num_residuals): if i 0 and not first_block: blk.append(Residual(num_channels, use_1x1convTrue, strides2)) else: blk.append(Residual(num_channels)) return nn.Sequential(*blk)%%tab tensorflow d2l.add_to_class(ResNet) def block(self, num_residuals, num_channels, first_blockFalse): blk tf.keras.models.Sequential() for i in range(num_residuals): if i 0 and not first_block: blk.add(Residual(num_channels, use_1x1convTrue, strides2)) else: blk.add(Residual(num_channels)) return blk%%tab jax d2l.add_to_class(ResNet) def block(self, num_residuals, num_channels, first_blockFalse): blk [] for i in range(num_residuals): if i 0 and not first_block: blk.append(Residual(num_channels, use_1x1convTrue, strides(2, 2), trainingself.training)) else: blk.append(Residual(num_channels, trainingself.training)) return nn.Sequential(blk)block方法的逻辑很清晰每个模块包含num_residuals个残差块除第一个模块first_blockTrue外每个模块的第一个残差块通过use_1x1convTrue, strides2完成通道翻倍与空间下采样其余残差块保持形状不变。组装网络与输出头把所有模块加入 ResNet每个模块使用两个残差块最后仿照 GoogLeNet 添加全局平均池化层再接全连接层输出%%tab pytorch, mxnet, tensorflow d2l.add_to_class(ResNet) def __init__(self, arch, lr0.1, num_classes10): super(ResNet, self).__init__() self.save_hyperparameters() if tab.selected(mxnet): self.net nn.Sequential() self.net.add(self.b1()) for i, b in enumerate(arch): self.net.add(self.block(*b, first_block(i0))) self.net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes)) self.net.initialize(init.Xavier()) if tab.selected(pytorch): self.net nn.Sequential(self.b1()) for i, b in enumerate(arch): self.net.add_module(fb{i2}, self.block(*b, first_block(i0))) self.net.add_module(last, nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.LazyLinear(num_classes))) self.net.apply(d2l.init_cnn) if tab.selected(tensorflow): self.net tf.keras.models.Sequential(self.b1()) for i, b in enumerate(arch): self.net.add(self.block(*b, first_block(i0))) self.net.add(tf.keras.models.Sequential([ tf.keras.layers.GlobalAvgPool2D(), tf.keras.layers.Dense(unitsnum_classes)]))# %%tab jax 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(i0))]) net.layers.extend([nn.Sequential([ # Flax does not provide a GlobalAvg2D layer lambda x: nn.avg_pool(x, window_shapex.shape[1:3], stridesx.shape[1:3], paddingvalid), lambda x: x.reshape((x.shape[0], -1)), nn.Dense(self.num_classes)])]) return net几个值得注意的实现细节PyTorch 版本在组装完毕后调用self.net.apply(d2l.init_cnn)初始化网络该函数定义于 d2l/torch.py对nn.Linear和nn.Conv2d层使用xavier_uniform_初始化权重MXNet 版本在self.net.initialize(init.Xavier())处显式使用 Xavier 初始化JAX/Flax 版本没有现成的全局平均池化层因此在 d2l/torch.py 对应的 JAX 后端实现中用nn.avg_pool配合window_shapex.shape[1:3]手动实现全局平均池化再reshape展平后接nn.Dense(num_classes)。从 18 层到 152 层ResNet-18 的由来每个模块有 4 个卷积层不含1×1卷积加上最前面的7×7卷积层和最后的全连接层共18 层因此该模型常被称为ResNet-18。通过配置模块中的通道数与残差块数量可以构造不同的 ResNet 变体例如更深达 152 层的 ResNet-152。虽然 ResNet 的主体架构与 GoogLeNet 相似但结构更简单、更易修改这些因素使其迅速被广泛采用。定义 ResNet-18四个模块分别含 2 个残差块通道数为 64、128、256、512%%tab pytorch, mxnet, tensorflow class ResNet18(ResNet): def __init__(self, lr0.1, num_classes10): super().__init__(((2, 64), (2, 128), (2, 256), (2, 512)), lr, num_classes)%%tab jax class ResNet18(ResNet): arch: tuple ((2, 64), (2, 128), (2, 256), (2, 512)) lr: float 0.1 num_classes: int 10观察特征图尺寸变化layer_summary训练之前先用layer_summary观察输入形状在各模块间的变化。与之前所有架构一样分辨率逐步下降、通道数逐步上升直到全局平均池化层聚合所有特征。该工具定义在 d2l/torch.py 的Classifier基类中它对self.net逐层前向传播并打印每层输出的形状。%%tab pytorch, mxnet ResNet18().layer_summary((1, 1, 96, 96))%%tab tensorflow ResNet18().layer_summary((1, 96, 96, 1))%%tab jax ResNet18(trainingFalse).layer_summary((1, 96, 96, 1))同样要注意张量布局差异PyTorch/MXNet 传入(1, 1, 96, 96)批量、通道、高、宽而 TensorFlow/JAX 传入(1, 96, 96, 1)批量、高、宽、通道。在 Fashion-MNIST 上训练 ResNet-18ResNet 在 Fashion-MNIST 数据集上训练与前面章节的实验保持一致。Fashion-MNIST 数据模块定义于 d2l/torch.py默认将图像缩放到resize指定的尺寸并转为张量。由于 ResNet 是较深的网络这里把 28×28 的原始图像放大到 96×96以保证下采样后仍有足够的空间分辨率。%%tab mxnet, pytorch, jax model ResNet18(lr0.01) trainer d2l.Trainer(max_epochs10, num_gpus1) data d2l.FashionMNIST(batch_size128, resize(96, 96)) if tab.selected(pytorch): model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn) trainer.fit(model, data)%%tab tensorflow trainer d2l.Trainer(max_epochs10) data d2l.FashionMNIST(batch_size128, resize(96, 96)) with d2l.try_gpu(): model ResNet18(lr0.01) trainer.fit(model, data)实验配置说明学习率lr0.01、训练 10 个 epochmax_epochs10、批量大小batch_size128PyTorch 版本在训练前用model.apply_init(...)依据首个训练批次的形状完成惰性层的实例化与初始化Trainer是 d2l/torch.py 中定义的通用训练基类负责加载数据、准备模型、配置优化器并逐 epoch 执行fit_epoch需要说明的是当前 torch 后端的Trainer.__init__中断言num_gpus 0No GPU support yet因此在该构建中训练默认走 CPU 路径TensorFlow 版本则通过d2l.try_gpu()在有 GPU 时自动使用 GPU。实验结论原文档直接给出的观察ResNet 是一个相当强大且灵活的网络。训练损失与验证损失的曲线之间存在显著差距训练损失明显更低。对于这种灵活度极高的网络更多的训练数据将显著有助于缩小这一差距并提升准确率——这正是深度模型对数据规模敏感性的直观体现。ResNeXt用分组卷积把 ResNet 推向更高效计算瓶颈与 Inception 的启发设计 ResNet 时面临一个挑战在单个块内权衡非线性与维度。我们既可以通过增加层数增加非线性也可以通过加宽卷积增加宽度另一种策略是增加能跨块携带信息的通道数但后者带来平方级的代价——把 $c_\textrm{i}$ 个通道变换为 $c_\textrm{o}$ 个通道的计算成本正比于 $\mathcal{O}(c_\textrm{i} \cdot c_\textrm{o})$相关讨论见 chapter_convolutional-neural-networks/channels.md。Inception 块让信息在块内以独立分组的形式流动这给 ResNet 块带来了启发把多组独立变换的思想应用到残差块上就得到了ResNeXt设计。与 Inception 中五花八门的各类变换不同ResNeXt 在所有分支中采用相同的变换从而最大限度地减少了对每个分支进行人工调参的需求。分组卷积Grouped Convolution的数学把从 $c_\textrm{i}$ 通道到 $c_\textrm{o}$ 通道的卷积拆分成 $g$ 组每组输入 $c_\textrm{i}/g$ 个通道、输出 $c_\textrm{o}/g$ 个通道这就是分组卷积。其计算成本按比例从 $\mathcal{O}(c_\textrm{i} \cdot c_\textrm{o})$ 降为 $\mathcal{O}(g \cdot (c_\textrm{i}/g) \cdot (c_\textrm{o}/g)) \mathcal{O}(c_\textrm{i} \cdot c_\textrm{o} / g)$即快了 $g$ 倍同时生成输出所需的参数量也从单个 $c_\textrm{i} \times c_\textrm{o}$ 矩阵降为 $g$ 个 $(c_\textrm{i}/g) \times (c_\textrm{o}/g)$ 的小矩阵同样是$g$ 倍缩减。下文假设 $c_\textrm{i}$ 与 $c_\textrm{o}$ 均可被 $g$ 整除。该设计唯一的挑战是$g$ 个分组之间不交换信息。ResNeXt 块用两种方式弥补把 $3\times 3$ 的分组卷积夹在两个 $1\times 1$ 卷积之间第二个 $1\times 1$ 卷积同时承担把通道数恢复回去的任务。收益在于我们只为 $1\times 1$ 卷积支付 $\mathcal{O}(c \cdot b)$ 的成本而 $3\times 3$ 卷积的成本只需 $\mathcal{O}(b^2 / g)$。与前述残差块实现类似这里的残差连接同样被广义化的$1\times 1$ 卷积替代。这一设计在现代 CNN 的通用设计中扮演着重要角色见 chapter_convolutional-modern/cnn-design.md。值得一提的是分组卷积的思想最早可追溯到 AlexNet 的实现当时为了把网络分布到显存有限的两块 GPU 上实现将每块 GPU 视为独立通道组且未产生不良影响。多框架实现ResNeXtBlockResNeXtBlock类接收两个关键参数groups$g$分组数与bot_mul瓶颈通道数系数中间瓶颈通道数 $b \text{round}(\text{num_channels} \times \text{bot_mul})$当需要降低表示的高宽时通过use_1x1convTrue, strides2添加步幅 2。%%tab mxnet class ResNeXtBlock(nn.Block): #save The ResNeXt block. def __init__(self, num_channels, groups, bot_mul, use_1x1convFalse, strides1, **kwargs): super().__init__(**kwargs) bot_channels int(round(num_channels * bot_mul)) self.conv1 nn.Conv2D(bot_channels, kernel_size1, padding0, strides1) self.conv2 nn.Conv2D(bot_channels, kernel_size3, padding1, stridesstrides, groupsbot_channels//groups) self.conv3 nn.Conv2D(num_channels, kernel_size1, padding0, strides1) self.bn1 nn.BatchNorm() self.bn2 nn.BatchNorm() self.bn3 nn.BatchNorm() if use_1x1conv: self.conv4 nn.Conv2D(num_channels, kernel_size1, stridesstrides) 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)%%tab pytorch class ResNeXtBlock(nn.Module): #save The ResNeXt block. def __init__(self, num_channels, groups, bot_mul, use_1x1convFalse, strides1): super().__init__() bot_channels int(round(num_channels * bot_mul)) self.conv1 nn.LazyConv2d(bot_channels, kernel_size1, stride1) self.conv2 nn.LazyConv2d(bot_channels, kernel_size3, stridestrides, padding1, groupsbot_channels//groups) self.conv3 nn.LazyConv2d(num_channels, kernel_size1, stride1) self.bn1 nn.LazyBatchNorm2d() self.bn2 nn.LazyBatchNorm2d() self.bn3 nn.LazyBatchNorm2d() if use_1x1conv: self.conv4 nn.LazyConv2d(num_channels, kernel_size1, stridestrides) 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)%%tab tensorflow class ResNeXtBlock(tf.keras.Model): #save The ResNeXt block. def __init__(self, num_channels, groups, bot_mul, use_1x1convFalse, strides1): super().__init__() bot_channels int(round(num_channels * bot_mul)) self.conv1 tf.keras.layers.Conv2D(bot_channels, 1, strides1) self.conv2 tf.keras.layers.Conv2D(bot_channels, 3, stridesstrides, paddingsame, groupsbot_channels//groups) self.conv3 tf.keras.layers.Conv2D(num_channels, 1, strides1) 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, stridesstrides) 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)%%tab jax 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), stridesself.strides, paddingsame, feature_group_countbot_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), stridesself.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)实现要点三层结构为1×1卷积降维/升维→3×3分组卷积 →1×1卷积恢复通道每个卷积后各跟一个 BatchNorm前两个卷积后接 ReLU分组体现在中间层PyTorch/MXNet 用groupsbot_channels//groupsTensorFlow 用同名groups参数JAX/Flax 用feature_group_countbot_channels//groups当use_1x1convTrue时残差分支由1×1卷积conv4加 BatchNormbn4构成用于对齐形状当中间通道数 $b$ 小于 $c$ 时该块即成为瓶颈残差块bottleneck residual block进一步压缩计算量。其使用方式与前述残差块完全类似use_1x1convFalse, strides1时输入输出同形状use_1x1convTrue, strides2时输出高宽减半。例如以num_channels32, groups16, bot_mul1构造块输入(4, 32, 96, 96)%%tab mxnet, pytorch blk ResNeXtBlock(32, 16, 1) if tab.selected(mxnet): blk.initialize() X d2l.randn(4, 32, 96, 96) blk(X).shape%%tab tensorflow blk ResNeXtBlock(32, 16, 1) X d2l.normal((4, 96, 96, 32)) Y blk(X) Y.shape%%tab jax blk ResNeXtBlock(32, 16, 1) X jnp.zeros((4, 96, 96, 32)) blk.init_with_output(d2l.get_key(), X)[0].shape总结与讨论嵌套函数类是理想的当增加容量时它让我们获得严格更强大而非微妙地不同的函数类。实现方式之一是让新增的层直接把输入透传到输出——残差连接恰好做到这一点。由此网络的归纳偏置从简单函数形如 $f(\mathbf{x}) 0$转变为简单函数形如 $f(\mathbf{x}) \mathbf{x}$。残差映射更容易学习恒等映射例如把权重层的参数推向零因此我们可以训练出有效的深层网络输入可以经由残差连接跨层快速前向传播从而支持训练更深得多的网络——原始 ResNet 论文就支持最深达 152 层的网络。残差网络的另一个好处是允许在训练过程中添加以恒等函数初始化的层毕竟层的默认行为就是让数据原样通过这在某些场景下可以加速超大规模网络的训练。历史脉络上残差连接之前已有使用门控单元的旁路路径来有效训练超过 100 层的高速网络Highway Networks而使用恒等函数作为旁路路径的 ResNet 在多项计算机视觉任务上表现卓越。残差连接深刻影响了后续卷积型与序列型深度网络的设计例如 Transformer 架构就采纳了残差连接连同其他设计选择并在语言、视觉、语音与强化学习等广泛领域遍地开花。ResNeXt则展示了卷积网络设计随时间的演进通过更节俭地使用计算、并以计算量换取激活规模通道数它得以用更低的成本获得更快更准的网络。另一种理解分组卷积的方式是把卷积权重想象成分块对角矩阵。类似的技巧还有很多例如 ShiftNet 仅通过向通道添加平移后的激活来模拟3×3卷积的效果在零计算成本的前提下提升函数复杂度。最后需要指出以上讨论的网络设计都相当依赖设计者的手工调参。在 chapter_convolutional-modern/cnn-design.md 中将讨论更自动化地获得高质量网络的策略特别是引出 RegNetX/Y 模型的**网络设计空间network design spaces**概念。练习对比 Inception 块与残差块的主要差异在计算量、准确率与可描述的函数类方面二者如何比较参照 ResNet 论文文献记录见 d2l.bib中的表 1实现不同深度的网络变体。对于更深的网络ResNet 引入了瓶颈bottleneck架构以降低模型复杂度请尝试实现它。在 ResNet 的后续版本中作者把卷积 → 批量归一化 → 激活的结构改为批量归一化 → 激活 → 卷积请自行完成这一改进参见论文Identity mappings in deep residual networks文献记录见 d2l.bib 的图 1。即使函数类是嵌套的为什么我们也不能无限制地增加函数复杂度赞分享文档教程人工智能深度学习NLP计算机视觉强化学习【免费下载链接】d2l-enInteractive deep learning book with multi-framework code, math, and discussions. Adopted at 500 universities from 70 countries including Stanford, MIT, Harvard, and Cambridge.项目地址https://gitcode.com/gh_mirrors/d2/d2l-en点击查看免费下载相关推荐动手学深度学习d2l-zh残差网络ResNet精讲从残差块原理到 ResNet-18 四框架实战动手学深度学习d2l zh残差网络ResNet精讲从残差块原理到 ResNet 18 四框架实战 导读 本文围绕《动手学深度学习》d2l zh仓库人工智能深度学习机器学习教程动手学深度学习残差网络 ResNet 深度解析——从残差块原理到 ResNet-18 完整实现动手学深度学习残差网络 ResNet 深度解析——从残差块原理到 ResNet 18 完整实现 本文以《动手学深度学习》D2L开源仓库 chapter_c人工智能深度学习机器学习教程Arcade平台游戏开发完整教程从零到发布 Arcade平台游戏开发完整教程从零到发布 Arcade是一个专为Python开发者设计的简单易用的2D游戏开发库特别适合初学者和想要快速创建平台游戏游戏开发创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表