人工智能-python-深度学习-神经网络-ResNet
ResNet通过残差连接解决了深层网络的梯度消失和退化问题,其核心思想是让网络学习残差函数F(x)=H(x)-x而非直接映射H(x)。ResNet包含BasicBlock(两层3×3卷积)和Bottleneck(1×1降维→3×3→1×1升维)两种残差块结构,后者通过维度变换减少计算量。典型架构包含多个残差阶段,通过1×1卷积处理维度不匹配。PyTorch实现中需要注意BN、ReLU的顺序和投影s
ResNet(详解)—— 从动机到原理、到 PyTorch 实现(含 ResNet-18 / ResNet-50 代码与训练要点)
简介(一句话)
ResNet(Residual Network,He et al.)通过引入“跳跃 / 残差连接(shortcut / identity mapping)”来让非常深的网络也能稳定训练,直接缓解梯度消失问题,从而推动了深层网络(如 50/101/152 层)在 ImageNet 等任务上取得突破。
1) 为什么需要 ResNet —— 梯度消失与退化问题
- 梯度消失/爆炸:深层网络在反向传播时梯度经过很多层的乘积,容易变得极小或极大,导致前面层几乎不更新或训练不稳定。
- 退化问题(degradation):不是只有梯度消失——经验上,当网络逐渐加深时,即使表达能力理论上更强,训练误差有时反而增加(更深网络表现更差),说明优化困难。
- ResNet 的出发点:如果网络的一些层并不必要,最差也只需要这些层学习到恒等映射(identity),这样深网络至少不会比浅网络差。但直接学习恒等映射很难 —— 因此通过显式的恒等快捷连接,将学习目标改写为学习残差更容易(优化更稳定)。
2) 残差 / shortcut 的核心思想(数学表达)
把希望学习的映射记为 H ( x ) H(x) H(x)。ResNet 让层去学习残差函数:
H ( x ) = F ( x ) + x H(x) = F(x) + x H(x)=F(x)+x
其中 F ( x ) F(x) F(x) 是若干层(例如两层卷积+BN+ReLU)的复合映射, x x x 是输入(恒等捷径)。如果最优的 H ( x ) H(x) H(x) 恰好等于输入 x x x,那么网络只需把 F ( x ) F(x) F(x) 学为 0(比直接把 H 学为恒等更容易)。
反向传播角度:链式法则中的梯度可以直接沿着恒等捷径回传到浅层(跳过一些权重层),缓解梯度衰减,从而让更深的网络可以被有效训练。
3) 残差块结构:BasicBlock vs Bottleneck
-
BasicBlock(用于 ResNet-18 / ResNet-34)
两个 3×3 卷积(每个后面跟 BN + ReLU),定义 F ( x ) = ConvBNReLU ( ConvBN ( x ) ) F(x) = \text{ConvBNReLU}(\text{ConvBN}(x)) F(x)=ConvBNReLU(ConvBN(x))。输出为 y = F ( x ) + x y = F(x) + x y=F(x)+x。如果尺寸/通道不匹配则用下采样的投影 W s x W_s x Wsx(1×1 卷积)来匹配。 -
Bottleneck(用于 ResNet-50/101/152)
三层结构:1×1 (降维) → 3×3 → 1×1 (升维)
,其中最后的 1×1 卷积把通道扩到planes * 4
(expansion=4)。这种“瓶颈”设计在深层网络中能减少计算量和参数量同时保持表达能力。
为什么用 Bottleneck?
对很深网络,若直接用两层 3×3 的 BasicBlock,参数与 FLOPs 会非常大。Bottleneck 通过 1×1 的降维/升维来节约计算,同时三个卷积带来的非线性提升也有利于表达。
4) 架构总览与逐层尺寸变化(以 ImageNet 风格输入 3×224×224
为例)
ResNet 的典型流程(ImageNet 标准):
conv1
: 7×7, stride=2, padding=3 → 输出64 × 112 × 112
maxpool
: 3×3, stride=2 →64 × 56 × 56
layer1
(第一个 residual stage, stride=1)→64 × 56 × 56
layer2
(stride=2)→128 × 28 × 28
layer3
(stride=2)→256 × 14 × 14
layer4
(stride=2)→512 × 7 × 7
avgpool
→512 × 1 × 1
(或512*expansion
)→ flatten →fc
→ logits
ResNet-18 层数分布(blocks per stage):[2, 2, 2, 2]
(Total layers: 18)
ResNet-50(Bottleneck):[3, 4, 6, 3]
(Total layers: 50)
参数规模(常见值)(近似):
- ResNet-18 ≈ 11.7M 参数
- ResNet-50 ≈ 25.6M 参数
逐层参数计算示例(简单说明):卷积层参数数 = Cin × Cout × K × K
(不含 bias),例如 conv1
为 3 × 64 × 7 × 7 = 9,408
个权重。
5) 关键实现细节与变换策略
- 投影 shortcut(1×1 conv):当 block 的输出通道数或空间尺寸因 stride 变化而与输入不匹配时,使用
1×1
卷积对输入做投影(并通常跟 BN)以匹配维度:y = F(x) + W_s x
。 - stride 放在哪里:常见策略是在 block 的第一个(或第二个)卷积上使用 stride=2 做下采样;实现需要与 downsample 的投影 stride 保持一致。
- BatchNorm 的作用:BN 能加速训练、稳定收敛,是 ResNet 里重要的一环(原作即用了 BN)。
- Pre-activation ResNet(He 2016):把 BN→ReLU 放到卷积前(即 pre-activation),能进一步改善优化并在超深网络上提供更好泛化;许多后续实现/变体使用预激活结构。
- Identity mapping 的设计考量:直接把捷径做恒等(不带参数)在多数情况下优先,因为它不会扰动原始输入;只在必要时使用投影(1×1)以节省计算并保证灵活性。
- 训练技巧:权重初始化(Kaiming/He 初始化)、适当的学习率调度、数据增强等都是训练好 ResNet 的关键。
6) PyTorch 实现(完整代码:ResNet18 + ResNet50)
下面给出一个清晰、可复制的 PyTorch 实现(ImageNet 风格)。你可以直接复制到 .py
或 CSDN 代码块中使用。注意: torchvision 已有 models.resnet18
、models.resnet50
(你可以直接用 pretrained 权重),但下面是教学与可修改的版本。
# resnet_custom.py
import torch
import torch.nn as nn
import torch.nn.functional as F
def conv3x3(in_planes, out_planes, stride=1):
"""3x3 convolution with padding"""
return nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
padding=1, bias=False)
class BasicBlock(nn.Module):
expansion = 1 # 输出通道 = planes * expansion
def __init__(self, inplanes, planes, stride=1, downsample=None):
super().__init__()
self.conv1 = conv3x3(inplanes, planes, stride)
self.bn1 = nn.BatchNorm2d(planes)
self.relu = nn.ReLU(inplace=True)
self.conv2 = conv3x3(planes, planes)
self.bn2 = nn.BatchNorm2d(planes)
self.downsample = downsample # 用于投影匹配维度
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class Bottleneck(nn.Module):
expansion = 4 # bottleneck 最后一层输出通道 = planes * 4
def __init__(self, inplanes, planes, stride=1, downsample=None):
super().__init__()
# 1x1 reduce
self.conv1 = nn.Conv2d(inplanes, planes, kernel_size=1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
# 3x3
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride,
padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
# 1x1 expand
self.conv3 = nn.Conv2d(planes, planes * self.expansion, kernel_size=1, bias=False)
self.bn3 = nn.BatchNorm2d(planes * self.expansion)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.stride = stride
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.bn3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class ResNet(nn.Module):
def __init__(self, block, layers, num_classes=1000, zero_init_residual=False):
"""
block: BasicBlock 或 Bottleneck
layers: 每个 stage 的 block 数目,例如 [2,2,2,2](ResNet18)
"""
super().__init__()
self.inplanes = 64
# Initial conv (ImageNet style)
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
self.bn1 = nn.BatchNorm2d(64)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
# 4 个 stage
self.layer1 = self._make_layer(block, 64, layers[0])
self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
self.layer4 = self._make_layer(block, 512, layers[3], stride=2)
self.avgpool = nn.AdaptiveAvgPool2d((1,1))
self.fc = nn.Linear(512 * block.expansion, num_classes)
# 参数初始化
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
elif isinstance(m, nn.BatchNorm2d):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
elif isinstance(m, nn.Linear):
nn.init.normal_(m.weight, 0, 0.01)
nn.init.constant_(m.bias, 0)
# 可选:把每个 bottleneck 的最后一个 BN 的 gamma 初始化为 0,以便 residual 初始为 0 -> 更稳定(He et al. later trick)
if zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
nn.init.constant_(m.bn3.weight, 0)
elif isinstance(m, BasicBlock):
nn.init.constant_(m.bn2.weight, 0)
def _make_layer(self, block, planes, blocks, stride=1):
"""
planes: 本 stage 中 block 的“基本channels”(bottleneck 的内部 channels)
blocks: block 个数
stride: 第一个 block 的 stride(用于下采样)
"""
downsample = None
# 若 stride!=1 或输入通道 != 输出通道(含 expansion),就需要 downsample(1x1 conv)
if stride != 1 or self.inplanes != planes * block.expansion:
downsample = nn.Sequential(
nn.Conv2d(self.inplanes, planes * block.expansion,
kernel_size=1, stride=stride, bias=False),
nn.BatchNorm2d(planes * block.expansion),
)
layers = []
# 第一个 block 承担下采样(若需要)
layers.append(block(self.inplanes, planes, stride, downsample))
self.inplanes = planes * block.expansion
# 其余 blocks
for _ in range(1, blocks):
layers.append(block(self.inplanes, planes))
return nn.Sequential(*layers)
def forward(self, x):
# Input: (N, 3, 224, 224)
x = self.conv1(x) # -> (N,64,112,112)
x = self.bn1(x)
x = self.relu(x)
x = self.maxpool(x) # -> (N,64,56,56)
x = self.layer1(x) # -> (N,64,56,56)
x = self.layer2(x) # -> (N,128,28,28)
x = self.layer3(x) # -> (N,256,14,14)
x = self.layer4(x) # -> (N,512,7,7)
x = self.avgpool(x) # -> (N,512,1,1)
x = torch.flatten(x, 1)
x = self.fc(x)
return x
def resnet18(num_classes=1000, **kwargs):
return ResNet(BasicBlock, [2,2,2,2], num_classes=num_classes, **kwargs)
def resnet50(num_classes=1000, **kwargs):
return ResNet(Bottleneck, [3,4,6,3], num_classes=num_classes, **kwargs)
# 简单测试模型输出形状与参数量
if __name__ == "__main__":
model18 = resnet18(num_classes=1000)
model50 = resnet50(num_classes=1000)
x = torch.randn(1,3,224,224)
print("resnet18 out:", model18(x).shape)
print("resnet50 out:", model50(x).shape)
# 参数量
def count_params(m):
return sum(p.numel() for p in m.parameters() if p.requires_grad)
print("ResNet18 params: %.2fM" % (count_params(model18)/1e6))
print("ResNet50 params: %.2fM" % (count_params(model50)/1e6))
说明 & 建议:
- 若要直接使用预训练参数,推荐用
torchvision.models.resnet18(pretrained=True)
或models.resnet50(weights=...)
(新版 torchvision 用weights
API)。- 上面
zero_init_residual
可以开启(默认 False),在一些实现中能进一步稳定深层 ResNet 的训练。
7) 训练/调参建议(实践要点)
-
数据预处理(ImageNet 常用):
Resize(256)
→RandomCrop(224)
→RandomHorizontalFlip()
→ToTensor()
→Normalize(mean, std)
。 -
优化器与超参(原始 & 常用起点):
- 优化器:
SGD
(momentum=0.9) - 初始学习率:
lr = 0.1
(BATCH=256 时);若 batch 较小,按比例缩放或使用线性warmup。 - weight_decay =
1e-4
(或1e-5
视情况) - batch_size:尽可能大(128/256),受显存限制可小一些并调小 lr。
- epoch:一般 90(paper)或更多;使用 learning rate schedule(StepLR 或 CosineAnnealing,常见 Step: reduce by 0.1 at epochs 30, 60, 80)。
- 优化器:
-
学习率 warmup:在大 batch 或深模型上常用线性 warmup(前 5–10 个 epoch)有助于稳定初期训练。
-
数据增强与正则化:随机裁剪/翻转是基础;可加入 ColorJitter、Cutout、Mixup、AutoAugment 等以提高泛化。
-
BatchNorm 与小 batch 问题:当 batch 很小,BN 表现受影响,可用 GroupNorm 或 SyncBN(分布式)替代。
-
监控指标:记录 train/val loss、top1/top5 acc、lr schedule;保存最好 val 模型 checkpoint。
-
Fine-tune:若数据集小,优先用 ImageNet 预训练模型微调(冻结早期层,调最后 FC,逐步解冻)。
8) 性能、为什么有用、常见变体
-
性能:ResNet 让训练非常深的网络成为可能(原始论文成功训练到 152 层并取得良好 ImageNet 成果),并成为后续很多模型的基础骨干(Backbone)。
-
为什么有效:恒等捷径直接传递信息和梯度,剩余模块只需学习微小改变量,使优化变得更容易。
-
常见变体:
- Pre-activation ResNet(He 2016):BN+ReLU 放在卷积前,能进一步稳定训练并提升性能。
- ResNeXt:采用 “cardinality(组数)” 的思想,把卷积分成多个并列小分支增加表示能力(类似组卷积)。
- WideResNet:减少深度、增加每层宽度(通道数)来获得更好性能/更快训练。
- SE-ResNet:加入通道注意力(Squeeze-and-Excitation)提升表现。
- DenseNet:另一类密集连接思想,与残差不同但同样强调信息流动。
-
这些变体在保持 ResNet 优点的同时,从不同角度改进建模能力或训练效率。
9) 总结与实践建议
- ResNet 的核心贡献在于把难以训练的深网络转化为“易学的残差函数”问题,通过恒等捷径让信息和梯度更顺畅地流动。
- 如果你要写 CSDN 教程 / 复现:建议同时提供(1)理论解释(残差为何有用)和(2)可运行代码(上面给出的实现或 torchvision),以及(3)训练示例(超参、数据增强)与 debug 建议(学习率、BN、batch size)。
- 实践中,优先使用
torchvision.models.resnet50(pretrained=True)
做微调能节省大量训练资源;若想深入理解原理,则用上面自实现代码做小型实验(例如:移除捷径、替换为加法改为拼接等)来观察性能差异,会很直观。
附:快速粘贴的训练示例骨架(示例,简化)
# 简化训练循环示例(用 resnet18)
from torchvision import transforms, datasets
from torch.utils.data import DataLoader
import torch.optim as optim
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = resnet18(num_classes=1000).to(device)
# 数据(示例:ImageFolder)
train_tfms = transforms.Compose([
transforms.Resize(256),
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]),
])
train_ds = datasets.ImageFolder("data/train", transform=train_tfms)
train_loader = DataLoader(train_ds, batch_size=128, shuffle=True, num_workers=4)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=1e-4)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)
for epoch in range(90):
model.train()
for imgs, labels in train_loader:
imgs, labels = imgs.to(device), labels.to(device)
logits = model(imgs)
loss = criterion(logits, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
scheduler.step()
print(f"Epoch {epoch+1} done.")
🔚 总结
ResNet 的提出是深度学习发展史上的一个重要里程碑。它解决了随着网络层数加深而出现的梯度消失与退化问题,核心思想是通过残差结构(Residual Block)引入捷径连接(Shortcut Connection),让网络更容易学习恒等映射,从而保证深层网络的可训练性。
在实践中:
- ResNet-18 / ResNet-34 使用 BasicBlock,适合较轻量级的任务;
- ResNet-50 / ResNet-101 / ResNet-152 使用 Bottleneck 结构,在保证计算效率的同时提供更强的特征提取能力;
- ResNet 及其变体(ResNeXt、WideResNet、SE-ResNet 等)已成为图像分类、检测、分割等视觉任务的骨干网络(Backbone),并在自然语言处理、语音等领域也被广泛借鉴。
影响:ResNet 的思想极大推动了深度学习模型的发展,使得构建数百甚至上千层的网络成为可能,是现代卷积神经网络的基石之一。
一句话记忆:ResNet 的核心就是 —— 让网络学残差,而不是直接学映射。
更多推荐
所有评论(0)