一、前言即背景

        在深度学习的发展历程中,人们不断追求卷积神经网络(CNN)的深度。然而,在这期间的发展中,有一个棘手的问题摆在大家面前:当层数超过一定时,模型的性能反而下降。

        这种情况不是因为过拟合,而是在方向传播时出现了梯度消失梯度爆炸现象。

1.1 梯度消失 Gradient Vanishing

        什么是梯度消失?

        我们指导,深度神经网络训练靠的是反向传播,而方向传播靠的是 链式求导

        所谓链式求导,其实就是反向的 一层一层梯度相乘

        grad = (w1 * f1') *( w2 * f2')* ......

        w 是第 i 层的权重, fi' 是第 i 层激活函数的导数

        根据以上公式,我们可以知道,如果求出来的导数一直小于1,那么一直乘下去,要不了多久,grad 会无限小,就会被解释成0

如果你已经知道梯度消失的原理,那么梯度爆炸便容易理解,和上面相反,乘出来的结果越来越大,造成 grad 大小溢出。

        我曾经介绍过的 AlexNet 和 VGG 结构都无法解决这一问题。

AlexNet博客:

https://blog.csdn.net/Wu_Deng_Sheng/article/details/157254935?spm=1001.2014.3001.5501

VGG博客:

https://blog.csdn.net/Wu_Deng_Sheng/article/details/157257594?spm=1001.2014.3001.5501

        为了解决这一问题,2015年,何凯明等人提出了 ResNet(残差网络),改变了这一局面。

本文基于 PyTorch 手动实现 ResNet-18,以介绍它的整体框架。


二、实现步骤

2.1 环境配置

import torch
import torch.nn as nn

2.2 残差块

        核心就是在输出时,再加上原来的 x,这样就可以有效缓解梯度消失现象。

       

        根据上面的公式, y 对 x 求导,就会得到 F' + 1 ,这里的 +1 就很巧妙了,如果 F' 很小,接近0,要造成梯度消失的时候,这个 +1 便可以把梯度救回来,使其约等于1

class Residual_block(nn.Module):
    def __init__(self, input_channels, out_channels, down_sample=False, strides=1):
        super().__init__()
        # 第一个卷积层:3x3卷积,保持或改变空间尺寸(通过strides)
        self.conv1 = nn.Conv2d(input_channels, out_channels,
                               kernel_size=3, padding=1, stride=strides)
        # 第二个卷积层:3x3卷积,保持空间尺寸
        self.conv2 = nn.Conv2d(out_channels, out_channels,
                               kernel_size=3, padding=1, stride=1)
        
        # 维度匹配,让输入输出的维度一样,这样才可以在后面相加
        if input_channels != out_channels:
            # 用1x1卷积调整x的通道数和空间尺寸
            self.conv3 = nn.Conv2d(input_channels, out_channels,
                                   kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        
        # 批量归一化
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU()

    def forward(self, X):
        # 前向传播:卷积 -> BN -> ReLU -> 卷积 -> BN
        out = self.relu(self.bn1(self.conv1(X)))
        out = self.bn2(self.conv2(out))
        
        # 如果需要维度匹配,对X做1x1卷积
        if self.conv3:
            X = self.conv3(X)
        
        # 残差连接:F(x) + x,再通过ReLU
        out += X
        return self.relu(out)

2.3 ResNet-18 框架

        ResNet-18 由 初始卷积层+4个残差块+平均池化+全连接 组成

class MyResNet18(nn.Module):
    def __init__(self):
        super(MyResNet18, self).__init__()
        # 初始卷积层:224x224 → 112x112
        self.conv1 = nn.Conv2d(3, 64, 7, 2, 3)
        self.bn1 = nn.BatchNorm2d(64)
        self.pool1 = nn.MaxPool2d(3, stride=2, padding=1)  # 最大池化,进一步缩小尺寸
        self.relu = nn.ReLU()
        
        # 4个残差层r包含2个残差块
        self.layer1 = nn.Sequential(
            Residual_block(64, 64),   # 不改变通道和尺寸
            Residual_block(64, 64)
        )
        self.layer2 = nn.Sequential(
            Residual_block(64, 128, strides=2),  # 第一个块下采样(通道+128,尺寸/2)
            Residual_block(128, 128)
        )
        self.layer3 = nn.Sequential(
            Residual_block(128, 256, strides=2), # 下采样
            Residual_block(256, 256)
        )
        self.layer4 = nn.Sequential(
            Residual_block(256, 512, strides=2), # 下采样
            Residual_block(512, 512)
        )
        
        # 平均池化:512x7x7 → 512x1x1
        self.adv_pool = nn.AdaptiveAvgPool2d(1)
        self.flatten = nn.Flatten()  # 展平为向量
        self.fc = nn.Linear(512, 1000)  

    def forward(self, x):
        # 初始部分
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.pool1(x)
        
        # 4个残差层
        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

三、总结

 ResNet 的提出,对于模型深度的提升有了巨大帮助。

对 ResNet 的学习,我主要学习到了:

        ① 残差连接

        ② 如何实现维度匹配:通过1*1的卷积调整输入 x 的维度,以确保残差连接可以正常相加。

        

更多推荐