趋近智
ResNet、DenseNet和EfficientNet等现代CNN架构的实践实现涉及使用常见的深度学习 (deep learning)框架构建它们的核心组成部分。虽然完整的、生产就绪的实现涉及许多细节,但专注于基本构建模块能提供有益的实践经验。假设你拥有一个已安装PyTorch或TensorFlow/Keras的可用Python环境。
确保你已安装了偏好的深度学习 (deep learning)库。例如,使用pip:
# For PyTorch
pip install torch torchvision
# For TensorFlow
pip install tensorflow
强烈建议使用GPU来训练这些模型,即使是小型数据集,但你也可以在CPU上执行这些构建步骤。
回顾一下,ResNet的核心构想是残差块,它允许网络在需要时学习恒等映射,从而简化非常深层网络的训练。核心运算是,其中表示由几个堆叠层学习到的残差映射,是块的输入(恒等连接)。
一个典型的ResNet块由两到三个卷积层、批量归一化 (normalization)和ReLU激活函数 (activation function)组成。跳跃连接将输入添加到卷积路径的输出。
一个包含两个卷积层的基本残差块结构。输入
x在最终ReLU激活之前被添加到第二个卷积层的输出。
我们来使用PyTorch的nn.Module定义一个BasicBlock。
import torch
import torch.nn as nn
import torch.nn.functional as F
class BasicBlock(nn.Module):
expansion = 1 # 基本块不扩展通道
def __init__(self, in_planes, planes, stride=1):
super(BasicBlock, self).__init__()
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.shortcut = nn.Sequential()
# 如果步长不为1或输入/输出通道数不同,则投射快捷连接
if stride != 1 or in_planes != self.expansion*planes:
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, self.expansion*planes, kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(self.expansion*planes)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x) # 残差连接的加法
out = F.relu(out)
return out
# 使用示例:
# 创建一个输入通道数为64、输出通道数为64、步长为1的块
block = BasicBlock(in_planes=64, planes=64, stride=1)
# 创建一个改变维度和分辨率的块
# 输入通道数为64、输出通道数为128、步长为2
downsample_block = BasicBlock(in_planes=64, planes=128, stride=2)
# 使用模拟输入张量测试 (批量大小, 通道数, 高, 宽)
dummy_input = torch.randn(4, 64, 32, 32)
output = block(dummy_input)
print("输出形状 (同维度):", output.shape)
output_downsampled = downsample_block(dummy_input)
print("输出形状 (下采样):", output_downsampled.shape)
这是使用TensorFlow Keras API的等效示例。
import tensorflow as tf
from tensorflow.keras import layers
class BasicBlock(layers.Layer):
expansion = 1
def __init__(self, planes, stride=1, **kwargs):
super(BasicBlock, self).__init__(**kwargs)
self.conv1 = layers.Conv2D(planes, kernel_size=3, strides=stride, padding='same', use_bias=False)
self.bn1 = layers.BatchNormalization()
self.relu = layers.ReLU()
self.conv2 = layers.Conv2D(planes, kernel_size=3, strides=1, padding='same', use_bias=False)
self.bn2 = layers.BatchNormalization()
self.shortcut_conv = None
self.shortcut_bn = None
# 在构建完成后存储输入通道数
self.in_planes = None
def build(self, input_shape):
# 从输入形状动态确定输入通道数
self.in_planes = input_shape[-1]
planes = self.conv1.filters # 从conv1获取输出通道数
# 如果需要(步长不为1或通道数改变),定义快捷连接层
if self.conv1.strides[0] != 1 or self.in_planes != self.expansion * planes:
self.shortcut_conv = layers.Conv2D(self.expansion * planes, kernel_size=1,
strides=self.conv1.strides, use_bias=False);
self.shortcut_bn = layers.BatchNormalization()
super(BasicBlock, self).build(input_shape) # 确保调用基类的build方法
def call(self, x, training=None): # `training` 参数对批量归一化很重要
identity = x
out = self.conv1(x)
out = self.bn1(out, training=training)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out, training=training)
# 如果已定义,则应用快捷连接
if self.shortcut_conv is not None:
identity = self.shortcut_conv(x)
identity = self.shortcut_bn(identity, training=training)
out += identity # 残差连接的加法
out = self.relu(out)
return out
# 使用示例:
# 创建一个输出通道数为64、步长为1的块
# 输入形状 (批量大小, 高, 宽, 通道数) - 为构建提供模拟形状
dummy_input_shape = (4, 32, 32, 64)
block = BasicBlock(planes=64, stride=1)
block.build(dummy_input_shape) # 显式构建或传入输入数据
# 创建一个改变维度和分辨率的块
# 输出通道数为128、步长为2
downsample_block = BasicBlock(planes=128, stride=2)
downsample_block.build(dummy_input_shape)
# 使用模拟输入张量测试
dummy_input = tf.random.normal((4, 32, 32, 64))
output = block(dummy_input, training=False) # 传递 training=False 以用于推断模式下的批量归一化
print("输出形状 (同维度):", output.shape)
output_downsampled = downsample_block(dummy_input, training=False)
print("输出形状 (下采样):", output_downsampled.shape)
这些示例展示了核心处理步骤。构建一个完整的ResNet涉及分阶段堆叠这些块,通常通过使用stride=2的块来减少空间尺寸并增加阶段之间的通道深度。
DenseNet的特点是其连接方式:块内的每个层都会接收来自所有前面层的特征图。与ResNet添加特征不同,DenseNet将它们拼接起来。
一个密集块将输入与每个内部层的输出进行拼接。过渡层通过1x1卷积和池化减少通道数和空间维度。
密集块内的每个“批量归一化 (normalization)-ReLU-卷积”单元通常由一个1x1卷积(瓶颈层,可选但普遍)和随后的一个3x3卷积组成。3x3卷积的输出通道数被称为growth_rate ()。由于通道数迅速累积,密集块之间会使用过渡层来压缩特征图(通常将通道数减半)并降低空间分辨率。
实现密集块需要妥善处理沿通道维度的张量拼接。
torch.cat(tensors, dim=1),其中tensors是要拼接的特征图列表,且dim=1是通道维度。tf.keras.layers.Concatenate(axis=-1)(如果使用通道优先格式,则为axis=3)。你可以为“批量归一化-ReLU-卷积”单元定义一个层或模块,然后在DenseBlock的前向传播中,将此单元迭代应用于块内所有先前层拼接而成的特征。TransitionLayer模块将包含批量归一化、一个1x1二维卷积和一个平均池化二维层。
构建一个完整的DenseNet涉及创建多个密集块,并用过渡层隔开,这类似于ResNet阶段的构建方式。
BasicBlock或Bottleneck,DenseNet的DenseLayer和DenseBlock)。MyResNet(nn.Module)或MyDenseNet(tf.keras.Model))。此类将:
torch.utils.data.DataLoader,tf.data.Dataset)。nn.CrossEntropyLoss,tf.keras.losses.SparseCategoricalCrossentropy)。torch.optim.AdamW,tf.keras.optimizers.Adam)。第二章涵盖了进阶优化器。torchvision.models和tf.keras.applications等框架提供了常用架构的预构建和通常是预训练版本。将你的块实现和整体结构与这些参考进行比较。它们是很好的学习资料。growth_rate或ResNet中的通道维度。查看对参数数量、计算成本(例如,使用分析工具)以及可能的训练性能的影响。这种实践操作巩固了你对架构构想如何转化为代码的理解。虽然我们专注于构建模块,但请记住,成功的深度学习 (deep learning)还包括细致的训练、优化和数据处理,这些是后续章节的主题。通过构建这些基本结构,你将更好地准备好修改现有模型,甚至为特定的计算机视觉任务规划新颖架构。
这部分内容有帮助吗?
© 2026 ApX Machine LearningAI伦理与透明度•