李沐学AI笔记——29残差网络
残差块
串联一个层改变函数类,我们希望能扩大函数类。
残差块加入快速通道来得到
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’'存在两项相加,就算第二项很小也不影响,因为还有第一项起作用。
更多推荐

所有评论(0)