ResNet 残差块 PyTorch 实现:Identity Block 与 Conv Block 的 3 点核心差异

📅 2026/7/6 12:04:13
ResNet 残差块 PyTorch 实现:Identity Block 与 Conv Block 的 3 点核心差异
ResNet 残差块 PyTorch 实现Identity Block 与 Conv Block 的 3 点核心差异深度残差网络ResNet自2015年提出以来已成为计算机视觉领域的基石架构。其核心创新在于残差块Residual Block的设计而其中Identity Block与Conv Block的差异往往让初学者感到困惑。本文将深入解析这两种残差块在PyTorch实现中的关键区别并提供可直接集成到项目中的模块化代码。1. 残差网络基础与两种块的设计初衷残差网络的核心思想是通过引入跳跃连接Skip Connection解决深层网络的梯度消失问题。当网络深度增加时传统CNN会出现性能退化现象而ResNet通过让输入信号跨越若干层直接传递到后续层使网络能够轻松学习恒等映射。在标准ResNet架构中如ResNet34两种残差块交替出现Identity Block当输入输出维度一致时使用残差路径是简单的恒等映射Conv Block当需要改变特征图尺寸或通道数时使用残差路径包含1×1卷积import torch import torch.nn as nn class BasicBlock(nn.Module): 基础残差块模板 expansion 1 def __init__(self, in_channels, out_channels, stride1): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.shortcut nn.Sequential() if stride ! 1 or in_channels ! self.expansion * out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, self.expansion * out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(self.expansion * out_channels) ) def forward(self, x): out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(x) return F.relu(out)2. 维度处理1×1卷积的关键作用两种残差块最显著的区别在于如何处理维度变化。当特征图的空间尺寸高/宽或通道数需要调整时必须使用Conv Block。Conv Block的核心组件主路径两个3×3卷积第一个可能带stride2捷径路径1×1卷积 BatchNorm维度匹配规则空间维度通过stride2的卷积减半通道维度通过1×1卷积调整def conv_block(in_channels, out_channels, stride2): Conv Block工厂函数 return BasicBlock(in_channels, out_channels, stridestride) # 输入输出维度对比示例 x torch.randn(1, 64, 56, 56) # (batch, channels, height, width) block conv_block(64, 128) print(f输入形状: {x.shape} - 输出形状: {block(x).shape}) # 输出: 输入形状: torch.Size([1, 64, 56, 56]) - 输出形状: torch.Size([1, 128, 28, 28])相比之下Identity Block不改变输入输出维度def identity_block(in_channels): Identity Block工厂函数 return BasicBlock(in_channels, in_channels, stride1) x torch.randn(1, 256, 28, 28) block identity_block(256) print(f输入形状: {x.shape} - 输出形状: {block(x).shape}) # 输出: 输入形状: torch.Size([1, 256, 28, 28]) - 输出形状: torch.Size([1, 256, 28, 28])3. 网络结构中的部署策略在完整ResNet中两种块的部署遵循特定模式每个stage开始时使用Conv Block进行下采样后续堆叠多个Identity Block保持维度典型ResNet34结构示例Stage输出尺寸块类型序列156×56[Conv, Identity×2]228×28[Conv, Identity×3]314×14[Conv, Identity×5]47×7[Conv, Identity×2]完整实现示例class ResNet(nn.Module): def __init__(self, block, num_blocks, num_classes1000): super().__init__() self.in_channels 64 self.conv1 nn.Conv2d(3, 64, kernel_size7, stride2, padding3, biasFalse) self.bn1 nn.BatchNorm2d(64) self.maxpool nn.MaxPool2d(kernel_size3, stride2, padding1) self.layer1 self._make_layer(block, 64, num_blocks[0], stride1) self.layer2 self._make_layer(block, 128, num_blocks[1], stride2) self.layer3 self._make_layer(block, 256, num_blocks[2], stride2) self.layer4 self._make_layer(block, 512, num_blocks[3], stride2) self.avgpool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(512 * block.expansion, num_classes) def _make_layer(self, block, out_channels, num_blocks, stride): strides [stride] [1]*(num_blocks-1) layers [] for stride in strides: layers.append(block(self.in_channels, out_channels, stride)) self.in_channels out_channels * block.expansion return nn.Sequential(*layers) def forward(self, x): x F.relu(self.bn1(self.conv1(x))) x self.maxpool(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.avgpool(x) x torch.flatten(x, 1) x self.fc(x) return x def ResNet34(): return ResNet(BasicBlock, [3,4,6,3])4. 梯度传播特性的对比分析两种残差块在反向传播时表现出不同的梯度流动特性特性Identity BlockConv Block梯度路径两条平行路径主路径转换路径梯度消失风险极低较低参数更新效率高直接相加中等需学习转换典型应用场景同一stage内的特征细化stage间的过渡实验对比两者的梯度幅值# 梯度监测工具 def check_gradient(block, input_shape(1,64,56,56)): x torch.randn(*input_shape, requires_gradTrue) y block(x) y.mean().backward() return x.grad.abs().mean().item() identity identity_block(64) conv conv_block(64, 128) print(fIdentity Block平均梯度: {check_gradient(identity):.4f}) print(fConv Block平均梯度: {check_gradient(conv):.4f})在实际项目中合理搭配两种残差块可以构建出高效的深度网络。例如在图像分割任务中通常会在编码器部分使用Conv Block进行下采样在解码器部分使用Identity Block保持分辨率。