ResNet 残差块 PyTorch 实现:Identity Block 与 Conv Block 的 3 点核心差异
📅 2026/7/6 12:23:45
👁️ 阅读次数
📝 编程学习
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, stride=1): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False) 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_size=1, stride=stride, bias=False), 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卷积(第一个可能带stride=2)
- 捷径路径:1×1卷积 + BatchNorm
- 维度匹配规则:
- 空间维度:通过stride=2的卷积减半
- 通道维度:通过1×1卷积调整
def conv_block(in_channels, out_channels, stride=2): """Conv Block工厂函数""" return BasicBlock(in_channels, out_channels, stride=stride) # 输入输出维度对比示例 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, stride=1) 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 | 输出尺寸 | 块类型序列 |
|---|---|---|
| 1 | 56×56 | [Conv, Identity×2] |
| 2 | 28×28 | [Conv, Identity×3] |
| 3 | 14×14 | [Conv, Identity×5] |
| 4 | 7×7 | [Conv, Identity×2] |
完整实现示例:
class ResNet(nn.Module): def __init__(self, block, num_blocks, num_classes=1000): super().__init__() self.in_channels = 64 self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False) self.bn1 = nn.BatchNorm2d(64) self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1) self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2) self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2) self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2) 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 Block | Conv Block |
|---|---|---|
| 梯度路径 | 两条平行路径 | 主路径+转换路径 |
| 梯度消失风险 | 极低 | 较低 |
| 参数更新效率 | 高(直接相加) | 中等(需学习转换) |
| 典型应用场景 | 同一stage内的特征细化 | stage间的过渡 |
实验对比两者的梯度幅值:
# 梯度监测工具 def check_gradient(block, input_shape=(1,64,56,56)): x = torch.randn(*input_shape, requires_grad=True) y = block(x) y.mean().backward() return x.grad.abs().mean().item() identity = identity_block(64) conv = conv_block(64, 128) print(f"Identity Block平均梯度: {check_gradient(identity):.4f}") print(f"Conv Block平均梯度: {check_gradient(conv):.4f}")在实际项目中,合理搭配两种残差块可以构建出高效的深度网络。例如在图像分割任务中,通常会在编码器部分使用Conv Block进行下采样,在解码器部分使用Identity Block保持分辨率。
编程学习
技术分享
实战经验