第四节(2). 自定义ResNet-18 网络

第四节(2). 自定义ResNet-18 网络 第一部分导入库与加载官方模型import torch import torch.nn as nn import torchvision.models as models resNet models.resnet18() print(resNet)解释导入必要的库torch(核心),nn(神经网络层),models(预训练模型库)。models.resnet18()直接调用 PyTorch 官方实现的 ResNet-18 模型默认未加载预训练权重只是结构。print(resNet)打印网络结构用于观察官方实现的层级细节。解释导入必要的库torch(核心),nn(神经网络层),models(预训练模型库)。models.resnet18()直接调用 PyTorch 官方实现的 ResNet-18 模型默认未加载预训练权重只是结构。print(resNet)打印网络结构用于观察官方实现的层级细节。第二部分自定义残差块 (Residual Block)这是 ResNet 的核心灵魂。class Residual_block(nn.Module): #save def __init__(self, input_channels, out_channels, down_sampleFalse, strides1): super().__init__() # 第一个卷积层可能进行下采样strides 1 self.conv1 nn.Conv2d(input_channels, out_channels, kernel_size3, padding1, stridestrides) # 第二个卷积层保持特征图大小不变 (stride1) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, padding1, stride 1) # 捷径连接 (Shortcut Connection) 的处理 # 如果输入通道数 ! 输出通道数或者需要下采样必须通过 1x1 卷积调整维度 if input_channels ! out_channels: self.conv3 nn.Conv2d(input_channels, out_channels, kernel_size1, stridestrides) else: self.conv3 None self.bn1 nn.BatchNorm2d(out_channels) self.bn2 nn.BatchNorm2d(out_channels) self.relu nn.ReLU()逐行解释 (__init__)input_channels,out_channels输入和输出的通道数。strides步长。如果是 2表示特征图宽高减半下采样。conv1,conv2两个 3×33×3 卷积。padding1保证在stride1时输出尺寸不变。conv3关键点。当输入和输出维度不一致通道数改变或尺寸改变时不能直接相加 XF(X)XF(X) 。这里用 1×11×1 卷积将 XX 映射到与 F(X)F(X) 相同的维度。这被称为Projection Shortcut。bn1,bn2批归一化 (Batch Normalization)加速收敛防止梯度消失。relu激活函数。def forward(self, X): # 主路径Conv - BN - ReLU - Conv - BN out self.relu(self.bn1(self.conv1(X))) out self.bn2(self.conv2(out)) # 注意这里没有接 ReLU留到加法后 # 捷径路径如果需要通过 conv3 调整维度 if self.conv3: X self.conv3(X) # 残差连接F(x) x out X return self.relu(out) # 加法后再激活逐行解释 (forward)实现了公式 YF(X,{Wi})XYF(X,{Wi​})X 。ReLU 的位置代码中采用的是 Pre-activation 还是 Post-activation官方 ResNet 通常是Conv - BN - ReLU - Conv - BN -Add- ReLU。此代码逻辑ReLU(BN(Conv(X)))-BN(Conv(out))-Add-ReLU。符合经典 ResNet 结构。为什么需要残差连接解决梯度消失/爆炸问题梯度可以通过 shortcut 直接传回浅层。解决退化问题 (Degradation Problem)深层网络理论上至少应该和浅层网络一样好恒等映射残差结构让网络更容易学习恒等映射 ( F(x)0F(x)0 )。什么时候需要 1×11×1 卷积当输入输出通道数不同或特征图尺寸不同下采样时必须对齐维度才能相加。BN 的作用规范化数据分布允许更大的学习率减少过拟合有轻微正则化效果。第三部分自定义 ResNet-18 整体架构class MyResNet18(nn.Module): def __init__(self): super(MyResNet18, self).__init__() # 初始卷积层7x7 卷积步长 2padding 3 (保持尺寸减半) # 输入 3 通道 (RGB)输出 64 通道 self.conv1 nn.Conv2d(3, 64, 7, 2, 3) self.bn1 nn.BatchNorm2d(64) self.pool1 nn.MaxPool2d(3, stride2, padding1) # 再次减半尺寸 self.relu nn.ReLU() # Layer 1: 2 个残差块输入 64 - 输出 64无下采样 self.layer1 nn.Sequential( Residual_block(64, 64), Residual_block(64, 64) ) # Layer 2: 2 个残差块输入 64 - 输出 128第一个块下采样 (strides2) self.layer2 nn.Sequential( Residual_block(64, 128, strides2), Residual_block(128, 128) ) # Layer 3: 2 个残差块输入 128 - 输出 256第一个块下采样 self.layer3 nn.Sequential( Residual_block(128, 256, strides2), Residual_block(256, 256) ) # Layer 4: 2 个残差块输入 256 - 输出 512第一个块下采样 self.layer4 nn.Sequential( Residual_block(256, 512, strides2), Residual_block(512, 512) ) self.flatten nn.Flatten() # 全局平均池化将 HxW 的特征图变成 1x1替代全连接层减少参数 self.adv_pool nn.AdaptiveAvgPool2d(1) # 全连接层输出 1000 类 (ImageNet 类别数) self.fc nn.Linear(512, 1000)解释Stem (主干)Conv1BNReLUMaxPool。负责初步提取特征并将图片尺寸缩小为原来的 1/4。4 个 Stage (层)Layer 1: 不改变尺寸通道 64。Layer 2, 3, 4: 每个层的第一个 Block 进行下采样尺寸减半通道翻倍。每个 Layer 包含 2 个 Block (ResNet-18 的特征ResNet-50 则是 3 个 Bottleneck Block)。尾部AdaptiveAvgPool2d(1)是 ResNet 的标准做法无论输入图片多大输出都是 512×1×1512×1×1 然后展平接全连接层。def forward(self, x): x self.conv1(x) x self.bn1(x) x self.relu(x) x self.pool1(x) x self.layer1(x) x self.layer2(x) x self.layer3(x) x self.layer4(x) x self.adv_pool(x) x self.flatten(x) x self.fc(x) return x解释定义数据流向依次经过 Stem - 4 个 ResLayer - 池化 - 分类头。第四部分实例化与参数量统计myres MyResNet18() def get_parameter_number(model): # p.numel() 计算张量中元素的总数 total_num sum(p.numel() for p in model.parameters()) # 只统计需要梯度的参数 (trainable) trainable_num sum(p.numel() for p in model.parameters() if p.requires_grad) return {Total: total_num, Trainable: trainable_num} print(get_parameter_number(myres.layer1)) print(get_parameter_number(myres.layer1[0].conv1)) print(get_parameter_number(resNet.layer1[0].conv1))解释get_parameter_number一个实用工具函数用于计算模型的参数量Model Size。p.numel()Number of Elements。例如一个 3×3×64×643×3×64×64 的卷积核参数量就是 3×3×64×643×3×64×64 ( bias)。打印对比myres.layer1的总参数量。自定义模型 layer1 第一个块的 conv1 参数量。官方模型 layer1 第一个块的 conv1 参数量。目的验证自己写的结构和官方结构在参数量上是否一致理论上应该一致。第五部分前向传播测试x torch.rand((1, 3, 224, 224)) # 模拟一张 RGB 图片尺寸 224x224Batch Size1 out resNet(x) # 跑通官方模型 out myres(x) # 跑通自定义模型解释创建一个随机张量模拟输入数据。分别输入两个模型如果没有报错且输出形状正确应该是[1, 1000]说明代码实现逻辑正确。