ResNet18深度残差网络详解

ResNet18 深度残差网络详解

目录

  1. 背景介绍
  2. 核心概念
  3. 代码结构详解
  4. 网络架构可视化
  5. 实战应用
  6. 常见问题解答

背景介绍

深度网络的挑战

在深度学习发展过程中,研究者们发现了一个反直觉的现象:更深的网络反而可能表现更差。这不是过拟合造成的,因为训练误差也更高。主要原因包括:

  1. 梯度消失/爆炸:深层网络中,梯度在反向传播时会指数级地减小或增大
  2. 退化问题:网络层数增加到一定程度后,准确率会饱和甚至下降

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)

设计要点:

  1. 3×3卷积核:在计算机视觉中,3×3是最常用的卷积核大小
  2. padding=1:保证当stride=1时,输出尺寸不变
  3. bias=False:因为BatchNorm会学习偏置,所以卷积层不需要
  4. 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?

  1. stride != 1:空间尺寸不匹配(下采样)
  2. 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×323
Conv132×3264
Layer132×3264
Layer216×16128
Layer38×8256
Layer44×4512
AvgPool1×1512
FC110

实战应用

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的好处:

  1. 加速收敛
  2. 允许使用更大的学习率
  3. 减少对初始化的依赖
  4. 具有轻微的正则化效果

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: 后续改进方向:

  1. ResNeXt:引入分组卷积
  2. DenseNet:密集连接
  3. SENet:通道注意力机制
  4. EfficientNet:复合缩放

总结

ResNet通过简单而优雅的残差学习框架,解决了深度网络训练的根本问题。其核心思想——“让网络学习残差而非完整映射”——已成为现代深度学习的基础设计原则之一。

理解ResNet不仅有助于使用这个强大的网络架构,更重要的是理解深度学习中的关键设计思想:

  1. 如何通过架构设计解决优化问题
  2. 简单的创新往往最有效
  3. 理论与实践的完美结合

微信公众号关注“CrazyNET”,获取更多资源!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值