拓冰建站拓冰建站
首页 / 资讯中心 / 正文

ResNet 残差学习 PyTorch 实现详解:Shortcut 连接、残差块结构与 CIFAR-10 训练全流程

ResNet 残差学习 PyTorch 实现详解Shortcut 连接、残差块结构与 CIFAR-10 训练全流程【免费下载链接】annotated_deep_learning_paper_implementations‍ 60 Implementations/tutorials of deep learning papers with side-by-side notes ; including transformers (original, xl, switch, feedback, vit, ...), optimizers (adam, adabelief, sophia, ...), gans(cyclegan, stylegan2, ...), reinforcement learning (ppo, dqn), capsnet, distillation, ... 项目地址: https://gitcode.com/gh_mirrors/an/annotated_deep_learning_paper_implementations本篇技术文章基于仓库中的 ResNet 实现文档 及其配套源码展开完整讲解残差学习Residual Learning的数学动机、ShortcutProjection/ResidualBlock/BottleneckResidualBlock/ResNetBase四个核心类的 PyTorch 实现并结合 CIFAR-10 训练脚本 给出可直接复现的超参数配置与训练流程。读完后你将能够理解 ResNet 为何能突破深度网络的退化问题、如何在源码层面实现恒等捷径与线性投影捷径以及如何用仓库提供的 labml 实验框架在 CIFAR-10 上训练一个带 bottleneck 结构的 ResNet。1. 退化问题ResNet 要解决的核心矛盾ResNet 是论文Deep Residual Learning for Image Recognition即仓库 papers/resnet.pdf 对应的 arXiv 1512.03385的 PyTorch 实现核心目标是克服深度网络的退化问题degradation problem当网络层数增加时精度先上升然后趋于饱和saturate继续加深后精度反而会下降论文指出更深的模型理论上应该至少不输于更浅的模型因为新增的层完全可以学成恒等映射identity mapping让深层网络“退化为”浅层网络的表现。然而实验表明简单地堆叠层数并训练网络去逼近恒等映射并不容易收敛。ResNet 的解法就是残差学习。2. 残差学习把 $H(x)$ 改写为 $\mathcal{F}(x) x$设某几层网络需要学习的映射为 $\mathcal{H}(x)$ResNet 不让网络直接拟合 $\mathcal{H}(x)$而是转而训练残差函数$$\mathcal{F}(x) \mathcal{H}(x) - x$$原映射改写为 $\mathcal{F}(x) x$。这样做的关键收益在于学习恒等映射 $\mathcal{H}(x) x$ 等价于学习让 $\mathcal{F}(x) 0$后者比直接拟合一个恒等变换更容易优化。写成参数化形式为$$\mathcal{F}(x, {W_i}) x$$当残差分支输出 $\mathcal{F}(x, {W_i})$ 与输入 $x$ 的特征图尺寸不一致通道数或空间尺寸不同时论文建议使用带学习权重 $W_s$ 的线性投影来对齐二者$$\mathcal{F}(x, {W_i}) W_s x$$文档中同时记录了论文的两个重要实验结论源码实现正是据此设计的论文对比过零填充zero padding与线性投影两种对齐方式发现线性投影效果更好当两个分支的特征图尺寸恰好一致时恒等映射比线性投影更好此时投影只是引入冗余参数。另外一个容易被忽略的细节$\mathcal{F}$ 必须包含不止一层网络否则 $\mathcal{F}(x, {W_i}) W_s x$ 这个和本身不包含非线性变换整个结构退化成一层线性层失去表达能力。这也是后续ResidualBlock至少包含两个卷积层的原因。3. 源码实现从 Shortcut 投影到完整网络完整实现位于 labml_nn/resnet/init.py由四个类组成ShortcutProjection、ResidualBlock、BottleneckResidualBlock、ResNetBase。3.1 ShortcutProjection实现 $W_s x$ 线性投影见 ShortcutProjectionclass ShortcutProjection(nn.Module): def __init__(self, in_channels: int, out_channels: int, stride: int): super().__init__() # 线性投影 $W_s x$1x1 卷积 批量归一化 self.conv nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride) self.bn nn.BatchNorm2d(out_channels) def forward(self, x: torch.Tensor): return self.bn(self.conv(x))三个参数分别对应文档中的定义in_channels输入 $x$ 的通道数out_channels残差分支 $\mathcal{F}(x, {W_i})$ 的通道数stride与残差分支的第一个卷积相同的步长。捷径分支使用同样的 stride是为了让 $W_s x$ 与 $\mathcal{F}(x, {W_i})$ 的空间尺寸严格对齐保证相加合法。实现上选择kernel_size1的卷积作为投影论文中即 1×1 卷积并在其后紧跟BatchNorm2d——论文建议在每个卷积操作之后都加批归一化该代码完全遵循了这一建议。3.2 ResidualBlock双 3×3 卷积的基本残差块见 ResidualBlock它实现了论文中的标准残差块对应文档中 residual_block.svg 的结构图包含两个 $3 \times 3$ 卷积层class ResidualBlock(nn.Module): def __init__(self, in_channels: int, out_channels: int, stride: int): super().__init__() # 第一个 3x3 卷积in_channels - out_channels携带 stride self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1) self.bn1 nn.BatchNorm2d(out_channels) self.act1 nn.ReLU() # 第二个 3x3 卷积out_channels - out_channelsstride 恒为 1 self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1) self.bn2 nn.BatchNorm2d(out_channels) # 捷径连接stride ! 1 或通道数变化时使用投影否则用恒等 if stride ! 1 or in_channels ! out_channels: self.shortcut ShortcutProjection(in_channels, out_channels, stride) else: self.shortcut nn.Identity() self.act2 nn.ReLU() def forward(self, x: torch.Tensor): shortcut self.shortcut(x) # 捷径分支 x self.act1(self.bn1(self.conv1(x))) # 残差分支第一层 x self.bn2(self.conv2(x)) # 残差分支第二层 return self.act2(x shortcut) # 相加后再激活几个与文档严格对应的设计要点卷积分工第一个卷积负责通道数映射in_channels→out_channels且携带 stride第二个卷积只改变特征、out_channels保持不变stride 恒为 1。当用 stride2 压缩特征图尺寸时out_channels会大于in_channels此时触发投影捷径捷径选择逻辑stride ! 1 or in_channels ! out_channels时启用ShortcutProjection否则使用nn.Identity()恒等捷径。这与第 2 节中“尺寸一致时用恒等、不一致时用 $W_s x$ 投影”的论文结论一一对应激活位置ReLU 只出现在第一层卷积之后act1以及两个分支相加之后act2第二层卷积与相加之间没有额外激活符合“$\mathcal{F}(x) x$ 后统一过非线性”的论文结构输入形状[batch_size, in_channels, height, width]。3.3 BottleneckResidualBlock1×1 - 3×3 - 1×1 瓶颈块见 BottleneckResidualBlock结构图见 bottleneck_block.svg用于 ResNet-50 等更深的变体由 $1 \times 1$、$3 \times 3$、$1 \times 1$ 三个卷积组成class BottleneckResidualBlock(nn.Module): def __init__(self, in_channels, bottleneck_channels, out_channels, stride): # 第一层 1x1 卷积压缩通道 in_channels - bottleneck_channels self.conv1 nn.Conv2d(in_channels, bottleneck_channels, kernel_size1) self.bn1, self.act1 nn.BatchNorm2d(bottleneck_channels), nn.ReLU() # 第二层 3x3 卷积在压缩后的“瓶颈”空间做卷积可携带 stride self.conv2 nn.Conv2d(bottleneck_channels, bottleneck_channels, kernel_size3, stridestride, padding1) self.bn2, self.act2 nn.BatchNorm2d(bottleneck_channels), nn.ReLU() # 第三层 1x1 卷积恢复通道 bottleneck_channels - out_channels self.conv3 nn.Conv2d(bottleneck_channels, out_channels, kernel_size1) self.bn3 nn.BatchNorm2d(out_channels) if stride ! 1 or in_channels ! out_channels: self.shortcut ShortcutProjection(in_channels, out_channels, stride) else: self.shortcut nn.Identity() self.act3 nn.ReLU()其设计意图源自 labml_nn/resnet/init.py 的文档注释bottleneck_channels小于in_channels代价较高的 $3 \times 3$ 卷积只在压缩后的低维空间执行这就是“瓶颈”名称的由来第一层 $1 \times 1$ 卷积降通道第三层 $1 \times 1$ 卷积升通道两个 1×1 卷积承担了“先降后升”的通道调度stride 只施加在中间的 $3 \times 3$ 卷积上。当 stride 1 时out_channels大于in_channels否则二者相等与基本残差块不同每层卷积之后都有 ReLUact1、act2最后一个act3在相加之后。3.4 ResNetBase堆叠残差块与全局平均池化见 ResNetBase这是不带最终分类层的 ResNet 主干backboneclass ResNetBase(nn.Module): def __init__(self, n_blocks: List[int], n_channels: List[int], bottlenecks: Optional[List[int]] None, img_channels: int 3, first_kernel_size: int 7): super().__init__() assert len(n_blocks) len(n_channels) assert bottlenecks is None or len(bottlenecks) len(n_channels) # 初始卷积img_channels - n_channels[0]stride2 self.conv nn.Conv2d(img_channels, n_channels[0], kernel_sizefirst_kernel_size, stride2, paddingfirst_kernel_size // 2) self.bn nn.BatchNorm2d(n_channels[0]) blocks [] prev_channels n_channels[0] for i, channels in enumerate(n_channels): # 每个新特征图尺度的第一个块 stride2其余块 stride1 stride 2 if len(blocks) 0 else 1 if bottlenecks is None: blocks.append(ResidualBlock(prev_channels, channels, stridestride)) else: blocks.append(BottleneckResidualBlock(prev_channels, bottlenecks[i], channels, stridestride)) prev_channels channels for _ in range(n_blocks[i] - 1): # 同一尺度内剩余块尺寸/通道均不变 if bottlenecks is None: blocks.append(ResidualBlock(channels, channels, stride1)) else: blocks.append(BottleneckResidualBlock(channels, bottlenecks[i], channels, stride1)) self.blocks nn.Sequential(*blocks) def forward(self, x: torch.Tensor): x self.bn(self.conv(x)) # 初始卷积 BN x self.blocks(x) # 残差瓶颈块堆叠 x x.view(x.shape[0], x.shape[1], -1) # [B, C, H, W] - [B, C, H*W] return x.mean(dim-1) # 全局平均池化 - [B, C]从源码结构看主干的组织规则是参数含义n_blocks每个特征图尺度下堆叠多少个块n_channels每个特征图尺度的输出通道数bottlenecks瓶颈块中间 $3 \times 3$ 卷积的通道数为None时使用基本残差块img_channels输入图像通道数RGB 为 3first_kernel_size初始卷积核大小默认 7对应 ImageNet 风格的大图输入下采样节奏初始卷积 stride2 先减半随后每个特征图尺度的第一个块再使用 stride2其余块 stride1从而实现“每过几个块特征图尺寸减半、通道数增加”的金字塔结构块间衔接同一尺度内块与块之间in_channels out_channels且 stride1因此走恒等捷径投影捷径只在尺度切换点出现参数开销被控制在最小输出表示最后用x.view(...)展平并mean(dim-1)做全局平均池化输出[batch_size, n_channels[-1]]的向量表示替代传统的大全连接层作为分类头的输入。4. CIFAR-10 训练实验完整可复现的配置训练脚本位于 labml_nn/resnet/experiment.py基于 labml 实验框架运行。实验配置继承自 CIFAR10Configs后者又组合了 数据集配置 与 MNISTConfigs 训练器。4.1 模型超参数默认值Configs 中定义了 ResNet 的默认结构参数class Configs(CIFAR10Configs): # 每个特征图尺度的块数 n_blocks: List[int] [3, 3, 3] # 每个特征图尺度的通道数 n_channels: List[int] [16, 32, 64] # 瓶颈块中间层通道数None 表示使用基本残差块 bottlenecks: Optional[List[int]] None # 初始卷积核大小CIFAR-10 的 32x32 小图用 3 而非默认的 7 first_kernel_size: int 3CIFAR-10 图像只有 32×32因此初始卷积核从默认的 7 改为 3避免过大的下采样损失。4.2 分类头构建模型由ResNetBase主干加一个线性分类层组成见 _resnetoption(Configs.model) def _resnet(c: Configs): base ResNetBase(c.n_blocks, c.n_channels, c.bottlenecks, img_channels3, first_kernel_sizec.first_kernel_size) classification nn.Linear(c.n_channels[-1], 10) # 10 类 CIFAR-10 model nn.Sequential(base, classification) return model.to(c.device)这正好印证了第 3.4 节的设计ResNetBase全局平均池化后输出[batch, 64]向量n_channels[-1] 64直接接入nn.Linear(64, 10)产生 10 类 logits送入训练器中的nn.CrossEntropyLoss定义于 labml_nn/experiments/mnist.py。4.3 实际训练用的覆盖配置main 函数 中通过experiment.configs覆盖了部分默认值得到实验实际使用的完整配置experiment.configs(conf, { bottlenecks: [8, 16, 16], # 启用瓶颈块中间层通道 8/16/16 n_blocks: [6, 6, 6], # 每个尺度堆叠 6 个瓶颈块 optimizer.optimizer: Adam, optimizer.learning_rate: 2.5e-4, epochs: 500, train_batch_size: 256, train_dataset: cifar10_train_augmented, valid_dataset: cifar10_valid_no_augment, })各配置项的落地位置bottlenecks: [8, 16, 16]一旦该值非NoneResNetBase即切换为BottleneckResidualBlock。此时每个块为ResNet-50 风格的 1×1/3×3/1×1 结构第 1 尺度从 16 通道压缩到 8 再升到 32第 2 尺度从 32 压缩到 16 再升到 32第 3 尺度从 64 压缩到 16 再升到 64。三个尺度的总块数为 66618 个瓶颈块每个瓶颈块含 3 个卷积加 1 个投影卷积尺度切换处主干总深度远超默认的[3,3,3]基本块配置optimizer.learning_rate: 2.5e-4学习率经由 OptimizerConfigs 解析optimizer: Adam会构造仓库自实现的Adamlabml_nn/optimizers/adam.py其betas(0.9, 0.999)、eps1e-08为该类默认值epochs: 500、train_batch_size: 256训练 500 个 epoch训练 batch 为 256数据集配置中valid_batch_size默认为 1024见 labml_nn/helpers/datasets.py数据增强cifar10_train_augmented对应 labml_nn/experiments/cifar10.py 中的训练集选项——RandomCrop(32, padding4)随机裁剪 RandomHorizontalFlip()随机水平翻转 以均值/标准差均为 0.5 的Normalize验证集cifar10_valid_no_augment不做增强仅做ToTensor与相同归一化保证验证指标不被增强噪声污染。4.4 训练循环底层机制conf.run()调用的是 TrainValidConfigs.run每轮先以is_trainTrue模式跑trainer()再跑validator()验证。每个 batch 的具体前向/反向逻辑继承自 MNISTConfigs.stepself.model.train(self.mode.is_train)切换训练/评估模式影响 BatchNorm 的统计量行为前向得到output计算CrossEntropyLoss并写入 tracker仅训练模式下执行loss.backward()、optimizer.step()、optimizer.zero_grad()并在每个 epoch 的最后一个 batch 记录模型参数与梯度tracker.add(model, self.model)inner_iterations 10MNISTConfigs表示每个 epoch 内训练/验证交替 10 次即每个 epoch 会把数据集切成 10 段轮流训练与验证让验证曲线更平滑地反映训练进程。实验入口为python -m labml_nn.resnet.experiment需安装labml框架与torchvision数据集在lab.get_data_path()指定路径自动下载。5. 小结与延伸阅读本篇围绕 labml_nn/resnet/readme.md 的完整内容展开动机退化问题——层数加深后精度饱和甚至下降而更深的网络本可通过恒等映射保持更浅网络的水平方法把 $H(x)$ 改学为残差 $\mathcal{F}(x) H(x) - x$令恒等映射对应 $\mathcal{F} 0$ 更易优化尺寸不一致时以 $W_s x$1×1 卷积 BN投影对齐一致时用恒等捷径$\mathcal{F}$ 必须至少两层以保证非线性实现ShortcutProjection投影捷径、ResidualBlock双 3×3 卷积、BottleneckResidualBlock1×1-3×3-1×1 瓶颈结构、ResNetBase尺度切换处 stride2 下采样 全局平均池化四个类共同构成可直接替换的模块实验CIFAR-10 上使用 18 个瓶颈块[6,6,6]、bottlenecks[8,16,16]、Adam 学习率 2.5e-4、500 epoch、batch 256训练集带随机裁剪/翻转增强。相关源码路径汇总文件内容labml_nn/resnet/init.py四个核心类的完整实现与逐行注释labml_nn/resnet/experiment.pyCIFAR-10 训练入口与全部超参数labml_nn/experiments/cifar10.pyCIFAR-10 数据集与增强管线labml_nn/experiments/mnist.py训练/验证步函数、损失与指标labml_nn/helpers/trainer.pyTrainValidConfigs训练循环labml_nn/optimizers/configs.py优化器选项解析含 Adam 构造【免费下载链接】annotated_deep_learning_paper_implementations‍ 60 Implementations/tutorials of deep learning papers with side-by-side notes ; including transformers (original, xl, switch, feedback, vit, ...), optimizers (adam, adabelief, sophia, ...), gans(cyclegan, stylegan2, ...), reinforcement learning (ppo, dqn), capsnet, distillation, ... 项目地址: https://gitcode.com/gh_mirrors/an/annotated_deep_learning_paper_implementations创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门