ResNet、DenseNet和EfficientNet等现代CNN架构的实践实现涉及使用常见的深度学习框架构建它们的核心组成部分。虽然完整的、生产就绪的实现涉及许多细节,但专注于基本构建模块能提供有益的实践经验。假设你拥有一个已安装PyTorch或TensorFlow/Keras的可用Python环境。环境设置确保你已安装了偏好的深度学习库。例如,使用pip:# For PyTorch pip install torch torchvision # For TensorFlow pip install tensorflow强烈建议使用GPU来训练这些模型,即使是小型数据集,但你也可以在CPU上执行这些构建步骤。实现残差块 (ResNet)回顾一下,ResNet的核心构想是残差块,它允许网络在需要时学习恒等映射,从而简化非常深层网络的训练。核心运算是$y = \mathcal{F}(x) + x$,其中$\mathcal{F}(x)$表示由几个堆叠层学习到的残差映射,$x$是块的输入(恒等连接)。一个典型的ResNet块由两到三个卷积层、批量归一化和ReLU激活函数组成。跳跃连接将输入$x$添加到卷积路径$\mathcal{F}(x)$的输出。基本残差块的结构digraph G { rankdir=LR; node [shape=box, style=filled, fillcolor="#a5d8ff"]; edge [color="#495057"]; input [label="输入 (x)", shape=ellipse, fillcolor="#ced4da"]; conv1 [label="卷积 3x3\n批量归一化\nReLU"]; conv2 [label="卷积 3x3\n批量归一化"]; add [label="+", shape=circle, fillcolor="#ffc9c9"]; relu_out [label="ReLU"]; output [label="输出 (y)", shape=ellipse, fillcolor="#ced4da"]; input -> conv1 [label=" "]; conv1 -> conv2 [label="F(x) 路径"]; conv2 -> add [label=" "]; input -> add [label=" 恒等连接 (x)", constraint=false]; // 跳跃连接 add -> relu_out [label=" "]; relu_out -> output [label=" "]; }一个包含两个卷积层的基本残差块结构。输入x在最终ReLU激活之前被添加到第二个卷积层的输出。实现示例 (PyTorch)我们来使用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)这是使用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)DenseNet的特点是其连接方式:块内的每个层都会接收来自所有前面层的特征图。与ResNet添加特征不同,DenseNet将它们拼接起来。密集块和过渡层的结构digraph G { subgraph cluster_0 { label = "密集块"; style=filled; fillcolor="#b2f2bb"; node [shape=box, style=filled, fillcolor="#d8f5a2"]; edge [color="#495057"]; input [label="输入", shape=ellipse, fillcolor="#ced4da"]; l1_out [label="批量归一化-ReLU-卷积\n(层 1)"]; l2_in_cat [label="拼接", shape=diamond, fillcolor="#ffec99"]; l2_out [label="批量归一化-ReLU-卷积\n(层 2)"]; l3_in_cat [label="拼接", shape=diamond, fillcolor="#ffec99"]; l3_out [label="批量归一化-ReLU-卷积\n(层 3)"]; block_out_cat [label="拼接", shape=diamond, fillcolor="#ffec99"]; input -> l1_out; input -> l2_in_cat; l1_out -> l2_in_cat; l2_in_cat -> l2_out; input -> l3_in_cat; l1_out -> l3_in_cat; l2_out -> l3_in_cat; l3_in_cat -> l3_out; input -> block_out_cat; l1_out -> block_out_cat; l2_out -> block_out_cat; l3_out -> block_out_cat; } subgraph cluster_1 { label = "过渡层"; style=filled; fillcolor="#a5d8ff"; node [shape=box, style=filled, fillcolor="#bac8ff"]; edge [color="#495057"]; trans_bn [label="批量归一化"]; trans_conv [label="卷积 1x1"]; trans_pool [label="平均池化 2x2"]; trans_out [label="输出", shape=ellipse, fillcolor="#ced4da"]; trans_bn -> trans_conv; trans_conv -> trans_pool; trans_pool -> trans_out; } block_out_cat -> trans_bn [lhead=cluster_1]; }一个密集块将输入与每个内部层的输出进行拼接。过渡层通过1x1卷积和池化减少通道数和空间维度。密集块内的每个“批量归一化-ReLU-卷积”单元通常由一个1x1卷积(瓶颈层,可选但普遍)和随后的一个3x3卷积组成。3x3卷积的输出通道数被称为growth_rate ($k$)。由于通道数迅速累积,密集块之间会使用过渡层来压缩特征图(通常将通道数减半)并降低空间分辨率。实现考量实现密集块需要妥善处理沿通道维度的张量拼接。PyTorch: 使用torch.cat(tensors, dim=1),其中tensors是要拼接的特征图列表,且dim=1是通道维度。TensorFlow/Keras: 使用tf.keras.layers.Concatenate(axis=-1)(如果使用通道优先格式,则为axis=3)。你可以为“批量归一化-ReLU-卷积”单元定义一个层或模块,然后在DenseBlock的前向传播中,将此单元迭代应用于块内所有先前层拼接而成的特征。TransitionLayer模块将包含批量归一化、一个1x1二维卷积和一个平均池化二维层。构建一个完整的DenseNet涉及创建多个密集块,并用过渡层隔开,这类似于ResNet阶段的构建方式。整合与后续步骤定义块: 为你选定的块创建类(例如,ResNet的BasicBlock或Bottleneck,DenseNet的DenseLayer和DenseBlock)。定义网络: 创建主要的网络类(例如,MyResNet(nn.Module)或MyDenseNet(tf.keras.Model))。此类将:定义一个初始卷积层和池化。实例化并堆叠自定义块,可能按阶段分组。使用辅助函数根据配置参数(例如,每个阶段的块数、通道维度)创建层/阶段。定义最后的层(例如,全局平均池化、用于分类的全连接层)。实例化并测试: 创建你的网络实例。传入一个模拟输入张量,检查前向传播是否无误运行,并验证不同阶段的输出形状。训练:加载数据集(例如,CIFAR-10,ImageNet子集)。使用框架提供的DataLoader(torch.utils.data.DataLoader,tf.data.Dataset)。定义损失函数(例如,nn.CrossEntropyLoss,tf.keras.losses.SparseCategoricalCrossentropy)。选择一个优化器(例如,torch.optim.AdamW,tf.keras.optimizers.Adam)。第二章涵盖了进阶优化器。编写训练循环:遍历周期和批次,执行前向传播,计算损失,执行反向传播,并更新优化器步长。实验与验证从小处开始: 在处理大型数据集之前,先在CIFAR-10这样的小型规范数据集上构建和测试。这使得迭代和调试更快。与预训练模型比较: torchvision.models和tf.keras.applications等框架提供了常用架构的预构建和通常是预训练版本。将你的块实现和整体结构与这些参考进行比较。它们是很好的学习资料。调整并查看: 更改超参数,例如块的数量、DenseNet中的growth_rate或ResNet中的通道维度。查看对参数数量、计算成本(例如,使用分析工具)以及可能的训练性能的影响。图示化: 使用TensorBoard或Netron等工具图示化网络图,并保证连接符合预期。这种实践操作巩固了你对架构构想如何转化为代码的理解。虽然我们专注于构建模块,但请记住,成功的深度学习还包括细致的训练、优化和数据处理,这些是后续章节的主题。通过构建这些基本结构,你将更好地准备好修改现有模型,甚至为特定的计算机视觉任务规划新颖架构。