第6章 经典CNN架构¶
📌 章节定位:本文档隶属于计算机视觉教程体系,侧重CNN架构在视觉任务中的实际应用。 - 本文档重点:预训练模型的实际调用、迁移学习实战、模型选择指南、不同架构在CV任务中的性能对比 - 理论原理方向:如需深入了解架构设计的理论基础、核心创新点的数学原理(如残差连接的梯度传播、Inception模块的设计理论),请参考 深度学习/02-卷积神经网络/02-经典CNN架构.md
⚠️ 时效性说明:本章涉及的模型性能数据(如ImageNet准确率、参数量、推理速度等)可能随新版本发布而变化;请以论文原文和官方发布页为准。截至2026年2月,本章内容基于最新可获得的公开数据。
📚 章节概述¶
本章介绍计算机视觉领域的经典CNN架构,包括LeNet、AlexNet、VGG、GoogLeNet、ResNet等。理解这些架构的设计思想和演进历程,对于设计自己的网络架构非常重要。
学习时间:5-7天 难度等级:⭐⭐⭐⭐ 前置知识:第5章
🎯 学习目标¶
完成本章后,你将能够: - 理解经典CNN架构的设计思想 - 掌握ResNet等现代架构 - 熟练使用预训练模型 - 能够迁移学习解决实际问题
6.1 LeNet-5 (1998)¶
架构特点: - 第一个成功的CNN - 用于手写数字识别 - 奠定了CNN的基础结构
import torch
import torch.nn as nn
import torch.nn.functional as F
class LeNet5(nn.Module): # 继承nn.Module定义网络层
"""LeNet-5: 最早的经典CNN,用于手写数字识别 (输入: 1x32x32)"""
def __init__(self):
super(LeNet5, self).__init__()
# 第一个卷积层: 1个输入通道 -> 6个特征图, 5x5卷积核
self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2)
# 第二个卷积层: 6 -> 16个特征图
self.conv2 = nn.Conv2d(6, 16, kernel_size=5)
# 三个全连接层: 逐步降维 400 -> 120 -> 84 -> 10
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10) # 10类输出(数字0-9)
# 平均池化层: 2x2窗口, 步长2, 每次将特征图尺寸减半
self.pool = nn.AvgPool2d(2, 2)
def forward(self, x):
# 卷积 -> tanh激活 -> 池化(原始论文使用tanh而非ReLU)
x = self.pool(torch.tanh(self.conv1(x))) # -> 6x14x14
x = self.pool(torch.tanh(self.conv2(x))) # -> 16x5x5
# 展平为一维向量,送入全连接层
x = x.view(-1, 16 * 5 * 5) # 重塑张量形状
x = torch.tanh(self.fc1(x))
x = torch.tanh(self.fc2(x))
x = self.fc3(x) # 最后一层不加激活,交给损失函数处理
return x
6.2 AlexNet (2012)¶
架构特点: - ImageNet 2012冠军 - 引入ReLU激活函数 - 使用Dropout防止过拟合 - 使用GPU加速训练
class AlexNet(nn.Module):
"""AlexNet: ImageNet 2012冠军,开启深度学习时代 (输入: 3x224x224)"""
def __init__(self, num_classes=1000):
super(AlexNet, self).__init__()
# 特征提取部分:5个卷积层 + 3个最大池化层
self.features = nn.Sequential(
# 第1层: 大卷积核11x11, 步长4, 快速降低空间维度
nn.Conv2d(3, 64, kernel_size=11, stride=4, padding=2),
nn.ReLU(inplace=True), # 首次使用ReLU替代tanh,加速训练收敛
nn.MaxPool2d(kernel_size=3, stride=2),
# 第2层: 5x5卷积,通道数增加到192
nn.Conv2d(64, 192, kernel_size=5, padding=2),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2),
# 第3-5层: 连续3个3x3卷积,不进行池化
nn.Conv2d(192, 384, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(384, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.Conv2d(256, 256, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.MaxPool2d(kernel_size=3, stride=2), # 输出: 256x6x6
)
# 分类器部分:3个全连接层 + Dropout防止过拟合
self.classifier = nn.Sequential(
nn.Dropout(), # Dropout概率默认0.5,随机丢弃神经元防止过拟合
nn.Linear(256 * 6 * 6, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Linear(4096, num_classes), # 输出1000类(ImageNet)
)
def forward(self, x):
x = self.features(x) # 卷积特征提取
x = x.view(x.size(0), 256 * 6 * 6) # 展平特征图
x = self.classifier(x) # 全连接分类
return x
6.3 VGG (2014)¶
架构特点: - 使用小卷积核(3x3) - 网络更深(16-19层) - 结构简单规整
class VGG(nn.Module):
"""VGG-16: 全部使用3x3小卷积核,结构简洁规整 (输入: 3x224x224)"""
def __init__(self, num_classes=1000):
super(VGG, self).__init__()
# 特征提取: 5个卷积块,每块内全用3x3卷积 + MaxPool下采样
# 核心思想: 两个3x3卷积的感受野等效于一个5x5,但参数更少、非线性更强
self.features = nn.Sequential(
# Block 1: 64通道, 2个3x3卷积 (224x224 -> 112x112)
nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2),
# Block 2: 128通道, 2个3x3卷积 (112x112 -> 56x56)
nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2),
# Block 3: 256通道, 3个3x3卷积 (56x56 -> 28x28)
nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2),
# Block 4: 512通道, 3个3x3卷积 (28x28 -> 14x14)
nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2),
# Block 5: 512通道, 3个3x3卷积 (14x14 -> 7x7)
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(inplace=True),
nn.MaxPool2d(2, 2), # 最终输出: 512x7x7
)
# 分类器: 3个全连接层(参数量占比最大的部分)
self.classifier = nn.Sequential(
nn.Linear(512 * 7 * 7, 4096), # 25088 -> 4096
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, 4096),
nn.ReLU(inplace=True),
nn.Dropout(),
nn.Linear(4096, num_classes),
)
def forward(self, x):
x = self.features(x) # 卷积提取特征: 3x224x224 -> 512x7x7
x = x.view(x.size(0), -1) # 展平: 512*7*7 = 25088维向量
x = self.classifier(x) # 全连接分类
return x
6.4 GoogLeNet (2014)¶
架构特点: - 引入Inception模块 - 多尺度特征提取 - 使用1x1卷积降维
class Inception(nn.Module):
"""Inception模块: 并行使用不同大小卷积核,实现多尺度特征提取
参数说明:
ch1x1: 1x1分支的输出通道数
ch3x3red/ch3x3: 3x3分支的降维通道数和输出通道数
ch5x5red/ch5x5: 5x5分支的降维通道数和输出通道数
pool_proj: 池化分支的投影通道数
"""
def __init__(self, in_channels, ch1x1, ch3x3red, ch3x3, ch5x5red, ch5x5, pool_proj):
super(Inception, self).__init__()
# 分支1: 1x1卷积,直接提取通道间特征
self.branch1 = nn.Sequential(
nn.Conv2d(in_channels, ch1x1, kernel_size=1),
nn.ReLU(inplace=True)
)
# 分支2: 1x1降维 -> 3x3卷积,捕获中等尺度特征
self.branch2 = nn.Sequential(
nn.Conv2d(in_channels, ch3x3red, kernel_size=1), # 1x1卷积降维,减少计算量
nn.ReLU(inplace=True),
nn.Conv2d(ch3x3red, ch3x3, kernel_size=3, padding=1),
nn.ReLU(inplace=True)
)
# 分支3: 1x1降维 -> 5x5卷积,捕获较大尺度特征
self.branch3 = nn.Sequential(
nn.Conv2d(in_channels, ch5x5red, kernel_size=1), # 1x1卷积降维
nn.ReLU(inplace=True),
nn.Conv2d(ch5x5red, ch5x5, kernel_size=5, padding=2),
nn.ReLU(inplace=True)
)
# 分支4: 最大池化 -> 1x1卷积投影,保留池化特征
self.branch4 = nn.Sequential(
nn.MaxPool2d(kernel_size=3, stride=1, padding=1), # 保持空间尺寸不变
nn.Conv2d(in_channels, pool_proj, kernel_size=1),
nn.ReLU(inplace=True)
)
def forward(self, x):
# 四个分支并行计算,在通道维度(dim=1)拼接
# 输出通道数 = ch1x1 + ch3x3 + ch5x5 + pool_proj
return torch.cat([self.branch1(x), self.branch2(x), self.branch3(x), self.branch4(x)], 1) # torch.cat沿已有维度拼接张量
6.5 ResNet (2015)¶
架构特点: - 引入残差连接 - 解决梯度消失问题 - 可以训练很深的网络(152层)
class ResidualBlock(nn.Module):
"""基本残差块 (Basic Block): 用于ResNet-18/34
核心思想: 学习残差映射 F(x) = H(x) - x,而非直接学习 H(x)
"""
def __init__(self, in_channels, out_channels, stride=1):
super(ResidualBlock, self).__init__()
# 主路径: 两个3x3卷积 + BatchNorm
self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1)
self.bn2 = nn.BatchNorm2d(out_channels)
# 捷径连接 (shortcut/skip connection): 默认为恒等映射
self.shortcut = nn.Sequential()
# 当步长不为1(空间尺寸变化)或通道数不匹配时,用1x1卷积调整维度
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
# 主路径: conv -> bn -> relu -> conv -> bn
out = F.relu(self.bn1(self.conv1(x))) # F.xxx PyTorch函数式API
out = self.bn2(self.conv2(out))
# 残差连接: 输出 = F(x) + x,梯度可以直接通过捷径反向传播
out += self.shortcut(x)
out = F.relu(out) # 加完残差后再激活
return out
class ResNet(nn.Module):
"""ResNet: 通过残差连接实现超深网络训练 (输入: 3x224x224)"""
def __init__(self, block, num_blocks, num_classes=1000):
super(ResNet, self).__init__()
self.in_channels = 64
# stem层: 7x7大卷积核快速降维 (224x224 -> 112x112)
self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3)
self.bn1 = nn.BatchNorm2d(64)
# 4个残差层,通道数逐层倍增: 64 -> 128 -> 256 -> 512
self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1) # 56x56
self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2) # 28x28
self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2) # 14x14
self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2) # 7x7
self.fc = nn.Linear(512, num_classes)
def _make_layer(self, block, out_channels, num_blocks, stride):
"""构建一个残差层,包含多个残差块"""
# 第一个块可能需要下采样(stride=2),后续块步长为1
strides = [stride] + [1] * (num_blocks - 1)
layers = []
for stride in strides:
layers.append(block(self.in_channels, out_channels, stride))
self.in_channels = out_channels # 更新输入通道数
return nn.Sequential(*layers)
def forward(self, x):
# stem: 7x7卷积 + BN + ReLU + 最大池化
x = F.relu(self.bn1(self.conv1(x))) # -> 64x112x112
x = F.max_pool2d(x, kernel_size=3, stride=2, padding=1) # -> 64x56x56
# 四个残差阶段
x = self.layer1(x) # -> 64x56x56
x = self.layer2(x) # -> 128x28x28
x = self.layer3(x) # -> 256x14x14
x = self.layer4(x) # -> 512x7x7
# 全局平均池化: 将每个通道的特征图压缩为一个值
x = F.adaptive_avg_pool2d(x, (1, 1)) # -> 512x1x1
x = x.view(x.size(0), -1) # 展平 -> 512
x = self.fc(x)
return x
def ResNet18(num_classes=1000):
# ResNet-18: 每个阶段2个基本残差块,共 2+2+2+2=8 个块 (含16个卷积层 + stem + fc = 18层)
return ResNet(ResidualBlock, [2, 2, 2, 2], num_classes)
6.6 使用预训练模型¶
from torchvision import models
import torch.optim as optim
# 加载在ImageNet上预训练好的ResNet-18模型(包含已学习的权重)
resnet18 = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# 迁移学习: 替换最后的全连接层以适配新任务的类别数
num_features = resnet18.fc.in_features # 获取原FC层的输入特征维度 (512)
resnet18.fc = nn.Linear(num_features, 10) # 替换为10分类(如CIFAR-10)
# 冻结所有卷积层参数(保留预训练特征提取能力,避免被破坏)
for param in resnet18.parameters():
param.requires_grad = False
# 只解冻最后一层,仅微调分类头(大幅减少训练参数量和计算开销)
for param in resnet18.fc.parameters():
param.requires_grad = True
# 定义损失函数和优化器(只优化需要梯度的参数)
criterion = nn.CrossEntropyLoss() # 多分类交叉熵损失
optimizer = optim.Adam(resnet18.fc.parameters(), lr=0.001) # 较小学习率防止震荡
6.7 练习题¶
基础题¶
- 简答题:
- ResNet的残差连接有什么作用?
残差连接(skip connection)让网络学习残差映射 F(x)=H(x)-x 而非直接学习 H(x),缓解了深层网络的梯度消失和网络退化问题,使训练数百层网络成为可能,同时提供恒等映射的捷径路径。
- VGG为什么使用3x3卷积核?
两个3×3卷积等效于一个5×5的感受野,三个3×3等效于7×7,但参数更少(如 3×3²C²=27C² vs 7²C²=49C²),且引入更多非线性激活(更多ReLU层),增强了模型表达能力。
进阶题¶
- 编程题:
- 实现一个简单的ResNet块。
- 使用预训练模型进行迁移学习。
6.8 面试准备¶
大厂面试题¶
Q1: ResNet的残差连接解决了什么问题?
参考答案: - 梯度消失问题 - 网络退化问题 - 允许训练更深的网络 - 提供恒等映射路径
Q2: 为什么VGG使用多个3x3卷积代替大卷积核?
参考答案: - 参数更少 - 非线性更多 - 感受野相同 - 更容易训练
6.9 本章小结¶
核心知识点¶
- LeNet:第一个CNN
- AlexNet:引入ReLU、Dropout
- VGG:小卷积核、深层网络
- GoogLeNet:Inception模块
- ResNet:残差连接
下一步¶
下一章:07-目标检测.md - 学习目标检测
恭喜完成第6章! 🎉