残差块

串联一个层改变函数类,我们希望能扩大函数类。
残差块加入快速通道来得到
f(x) = x + g(x)的结构
一个正常块(左图)和一个残差块(右图):
在这里插入图片描述
ResNet块细节(可以使用不同的残差块 )
在这里插入图片描述
核心:加了一条路径过去

# 传统网络:逐层变换
output = F(x)  # 直接学习映射

# ResNet:学习残差
output = F(x) + x  # 学习"差异",恒等映射作为捷径

ResNet块

高宽减半ResNet块(步幅2)
后接上多个高宽不变ResNet块
例如:

# 假设从Stage 1出来是56x56的特征图

# 1. 第一个块:高宽减半(stride=2)
输入: 56x56, 64通道
    ↓ 通过stride=2的卷积
输出: 28x28, 128通道  # 高宽减半,通道数翻倍

# 2. 后续多个块:保持高宽不变
输入: 28x28, 128通道
    ↓ 通过stride=1的卷积  
输出: 28x28, 128通道  # 高宽不变

输入: 28x28, 128通道
    ↓ 通过stride=1的卷积
输出: 28x28, 128通道  # 高宽不变
# ... 重复多次

ResNet架构

类似VGG和Google的总体架构,但替换成了ResNet块
在这里插入图片描述

总结

残差块使得很深的网络更加容易训练,甚至一千层的网络。
无论多深,总会把下面小的训练好,再去训练更深的。

代码

  • 残差块(ResNet的基本构建单元):
import torch.nn as nn
import torch.nn.functional as F

# from torch.nn import functional as F
# from d2l import torch as d2l

class Residual(nn.Module):
    def __init__(self,input_channels, num_channels, use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1, stride=strides)
        #输入通道、输出通道、卷积核大小、填充大小、步长(通常1/2)
        self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1)
        if use_1x1conv:
            self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides)
        else:
            self.conv3 = None
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
        self.relu = nn.ReLU(inplace=True)
    def forward(self,X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X                 #ResNet的核心
        return F.relu(Y)

1x1卷积的作用——适配器:
1、维度对齐器:确保残差连接两边维度匹配
2、下采样器:当strides=2时,同步下采样残差路径
3、通道调节器:改变通道数来适应主路径
以最小代价实现维度变换,让残差连接在各种维度变化下都能正常工作。

  • 输入和输出形状一致
blk = Residual(3, 3)
X = torch.rand(4, 3, 6, 6)
Y = blk(X)
Y.shape

在这里插入图片描述

  • 增加输出通道数的同时,减半输出的高和宽
blk = Residual(3, 6, use_1x1conv=True, strides=2)
blk(X).shape

在这里插入图片描述

  • ResNet网络
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                  nn.BatchNorm2d(64), nn.ReLU(),
                  nn.MaxPool2d(kernel_size=3, stride=2,padding=1))

def resnet_block(input_channels, num_channels, num_residuals, first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block:   #第一个块 高宽减半
            blk.append(
                Residual(input_channels, num_channels, use_1x1conv=True, strides=2)
            )
        else:
            blk.append(Residual(num_channels, num_channels))
    return blk

b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1,1)), nn.Flatten(), nn.Linear(512,10))
  • 观察ResNet中不同模型的输入形状是如何变化的
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__, 'output shape:\t', X.shape)

在这里插入图片描述
第一次运行返回前两层的是torch.size([1,64,55,55]),检查发现将b1的padding=1,padding代表周围填充的像素个数。
例1:padding=1
输入图像:224x224
↓ 周围填充1像素 → 变成226x226
↓ 7x7卷积,步长2 → 110x110
↓ 最大池化 → 55x55
例2:padding=3
输入图像:224x224
↓ 周围填充3像素 → 变成230x230
↓ 7x7卷积,步长2 → 112x112
↓ 最大池化 → 56x56

  • 训练
from d2l import torch as d2l
lr, num_epochs, batch_size = 0.05, 10, 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

解答

1、残差体现在什么地方?f(x)=x+g(x),所以g(x)可以视为f(x)的残差?
残差体现在网络层的输入与输出之间的“短路连接”。
网络不直接学习目标函数f(x),而是学习目标函数与输入x之间的差值g(x)=f(x)-x
,然后通过 输出=输入+残差 的方式实现。
ResNet的核心创新——让深度神经网络学习"残差"而不是"完整映射",极大缓解了深度网络的训练难题。
2、* resnet_block里的星号是什么意思
星号在Python中是“解包操作符” 。它把列表resnet_block()返回的多个元素解包成单独的参数传递给nn.Sequential()。

# resnet_block() 返回一个列表:[block1, block2]
blk_list = resnet_block(64, 64, 2, first_block=True)
# 假设返回:[Residual(64,64), Residual(64,64)]

# ❌ 错误:将整个列表作为一个参数传递
b2 = nn.Sequential(blk_list)
# 相当于:nn.Sequential([block1, block2])
# nn.Sequential 期望多个模块作为参数,不是一个列表!

# ✅ 正确:用星号解包列表
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
# 相当于:nn.Sequential(block1, block2)
# 星号把列表中的每个元素变成单独的参数

ResNet为什么能训练1000层的模型

y=f(x)
y’=g(f(x)) g假设是在f卷积层外又加了10个层
在这里插入图片描述

  • ResNet解决梯度消失问题的核心数学原理
    核心就是原本的g/y偏导有可能会很小,或消失。
    但是ResNet由于y’'存在两项相加,就算第二项很小也不影响,因为还有第一项起作用。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐