ResNet18 深度残差网络详解
目录
背景介绍
深度网络的挑战
在深度学习发展过程中,研究者们发现了一个反直觉的现象:更深的网络反而可能表现更差。这不是过拟合造成的,因为训练误差也更高。主要原因包括:
- 梯度消失/爆炸:深层网络中,梯度在反向传播时会指数级地减小或增大
- 退化问题:网络层数增加到一定程度后,准确率会饱和甚至下降
ResNet的诞生
2015年,何恺明等人提出了ResNet(Residual Network,残差网络),通过引入"残差学习"框架优雅地解决了上述问题,使得训练数百层甚至上千层的网络成为可能。
核心概念
1. 残差学习(Residual Learning)
传统神经网络试图学习底层映射 H(x),而ResNet学习残差映射 F(x) = H(x) - x。
传统方法:直接学习 H(x)
ResNet:学习 F(x),然后 H(x) = F(x) + x
为什么这样更容易?
- 如果最优映射接近恒等映射,那么让F(x)→0比让H(x)→x更容易
- 提供了一条"高速公路",允许梯度直接流过
2. 跳跃连接(Skip Connection)
输入 x
|
├─────────────┐
| |
残差块 F(x) |
| |
└──── + ──────┘
|
H(x) = F(x) + x
代码结构详解
1. 导入必要的库
import torch.nn as nn
import torch.nn.functional as F
nn:PyTorch的神经网络模块,包含各种层的定义F:函数式接口,包含激活函数等操作
2. BasicBlock - 基本残差块
2.1 类定义和expansion
class BasicBlock(nn.Module):
expansion = 1
expansion:输出通道数相对于planes的倍数- BasicBlock的expansion为1,Bottleneck的为4
2.2 初始化方法详解
def __init__(self, in_planes, planes, stride=1):
super(BasicBlock, self).__init__()
参数说明:
in_planes:输入特征图的通道数planes:第一个卷积层输出的通道数stride:步长,控制下采样
2.3 主路径(Main Path)
# 第一个卷积层
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)
设计要点:
- 3×3卷积核:在计算机视觉中,3×3是最常用的卷积核大小
- padding=1:保证当stride=1时,输出尺寸不变
- bias=False:因为BatchNorm会学习偏置,所以卷积层不需要
- BatchNorm:批归一化,加速训练并提高稳定性
2.4 跳跃连接(Shortcut)
self.shortcut = nn.Sequential()
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)
)
何时需要调整shortcut?
stride != 1:空间尺寸不匹配(下采样)in_planes != planes:通道数不匹配
1×1卷积的作用:
- 改变通道数
- 改变空间尺寸(通过stride)
- 计算量小,参数少
2.5 前向传播
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x))) # 第一层:Conv→BN→ReLU
out = self.bn2(self.conv2(out)) # 第二层:Conv→BN(注意没有ReLU)
out += self.shortcut(x) # 残差连接
out = F.relu(out) # 最后的ReLU
return out
关键设计:
- 第二个卷积后不立即使用ReLU
- 先进行残差连接,再激活
- 这样可以保持恒等映射的可能性
3. ResNet主网络
3.1 初始化
def __init__(self, block, num_blocks, num_classes=10):
super(ResNet, self).__init__()
self.in_planes = 64
block:使用的残差块类型(BasicBlock或Bottleneck)num_blocks:[2,2,2,2]表示每个stage的块数num_classes:分类数,CIFAR-10为10
3.2 网络层次结构
# 初始层
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(64)
# 四个残差阶段
self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
# 分类器
self.linear = nn.Linear(512*block.expansion, num_classes)
3.3 构建残差层组
def _make_layer(self, block, planes, num_blocks, stride):
strides = [stride] + [1]*(num_blocks-1)
layers = []
for stride in strides:
layers.append(block(self.in_planes, planes, stride))
self.in_planes = planes * block.expansion
return nn.Sequential(*layers)
设计原理:
- 每个stage的第一个块负责下采样(如果需要)
- 后续块保持尺寸不变,加深特征提取
网络架构可视化
ResNet18 完整架构
输入图像 (3, 32, 32)
↓
Conv3x3, 64
↓
[BasicBlock×2] ← Layer1 (64通道)
↓
[BasicBlock×2] ← Layer2 (128通道, /2)
↓
[BasicBlock×2] ← Layer3 (256通道, /2)
↓
[BasicBlock×2] ← Layer4 (512通道, /2)
↓
AvgPool (4×4)
↓
FC (512→10)
↓
输出 (10)
特征图尺寸变化
| 层 | 输出尺寸 | 通道数 |
|---|---|---|
| 输入 | 32×32 | 3 |
| Conv1 | 32×32 | 64 |
| Layer1 | 32×32 | 64 |
| Layer2 | 16×16 | 128 |
| Layer3 | 8×8 | 256 |
| Layer4 | 4×4 | 512 |
| AvgPool | 1×1 | 512 |
| FC | 1 | 10 |
实战应用
1. 创建模型
# 创建ResNet18模型
model = ResNet18()
# 查看模型结构
print(model)
# 统计参数量
total_params = sum(p.numel() for p in model.parameters())
print(f"Total parameters: {total_params:,}")
2. 训练示例
import torch
import torch.optim as optim
# 模型、损失函数、优化器
model = ResNet18()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)
# 训练循环
def train_epoch(model, dataloader, criterion, optimizer):
model.train()
for batch_idx, (inputs, targets) in enumerate(dataloader):
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
3. 特征提取
# 使用预训练模型提取特征
def extract_features(model, x):
# 移除最后的全连接层
features = nn.Sequential(*list(model.children())[:-1])
with torch.no_grad():
feat = features(x)
feat = feat.view(feat.size(0), -1)
return feat
常见问题解答
Q1: 为什么CIFAR-10版本的ResNet与ImageNet版本不同?
A: 主要区别在于初始卷积层:
- CIFAR-10:
stride=1, kernel_size=3(输入32×32) - ImageNet:
stride=2, kernel_size=7(输入224×224)
CIFAR-10图像较小,不需要在开始就大幅下采样。
Q2: 为什么使用BatchNorm?
A: BatchNorm的好处:
- 加速收敛
- 允许使用更大的学习率
- 减少对初始化的依赖
- 具有轻微的正则化效果
Q3: 残差连接如何解决梯度消失?
A: 考虑反向传播时的梯度:
∂L/∂x = ∂L/∂H × ∂H/∂x = ∂L/∂H × (1 + ∂F/∂x)
即使 ∂F/∂x 很小,仍有常数1保证梯度流动。
Q4: 如何选择ResNet的深度?
A: 常见变体:
- ResNet18/34:使用BasicBlock,适合较小数据集
- ResNet50/101/152:使用Bottleneck,适合大规模数据集
一般原则:数据集越大、任务越复杂,可以使用更深的网络。
Q5: 如何改进ResNet?
A: 后续改进方向:
- ResNeXt:引入分组卷积
- DenseNet:密集连接
- SENet:通道注意力机制
- EfficientNet:复合缩放
总结
ResNet通过简单而优雅的残差学习框架,解决了深度网络训练的根本问题。其核心思想——“让网络学习残差而非完整映射”——已成为现代深度学习的基础设计原则之一。
理解ResNet不仅有助于使用这个强大的网络架构,更重要的是理解深度学习中的关键设计思想:
- 如何通过架构设计解决优化问题
- 简单的创新往往最有效
- 理论与实践的完美结合
微信公众号关注“CrazyNET”,获取更多资源!
1652

被折叠的 条评论
为什么被折叠?



