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

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, 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中,两种块的部署遵循特定模式:

  1. 每个stage开始时使用Conv Block进行下采样
  2. 后续堆叠多个Identity Block保持维度
  3. 典型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_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 BlockConv 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保持分辨率。