跳转至

第6章 经典CNN架构

📌 章节定位:本文档隶属于计算机视觉教程体系,侧重CNN架构在视觉任务中的实际应用。 - 本文档重点:预训练模型的实际调用、迁移学习实战、模型选择指南、不同架构在CV任务中的性能对比 - 理论原理方向:如需深入了解架构设计的理论基础、核心创新点的数学原理(如残差连接的梯度传播、Inception模块的设计理论),请参考 深度学习/02-卷积神经网络/02-经典CNN架构.md

⚠️ 时效性说明:本章涉及的模型性能数据(如ImageNet准确率、参数量、推理速度等)可能随新版本发布而变化;请以论文原文和官方发布页为准。截至2026年2月,本章内容基于最新可获得的公开数据。

经典CNN架构图

📚 章节概述

本章介绍计算机视觉领域的经典CNN架构,包括LeNet、AlexNet、VGG、GoogLeNet、ResNet等。理解这些架构的设计思想和演进历程,对于设计自己的网络架构非常重要。

学习时间:5-7天 难度等级:⭐⭐⭐⭐ 前置知识:第5章

🎯 学习目标

完成本章后,你将能够: - 理解经典CNN架构的设计思想 - 掌握ResNet等现代架构 - 熟练使用预训练模型 - 能够迁移学习解决实际问题


6.1 LeNet-5 (1998)

架构特点: - 第一个成功的CNN - 用于手写数字识别 - 奠定了CNN的基础结构

Python
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加速训练

Python
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层) - 结构简单规整

Python
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卷积降维

Python
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层)

Python
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 使用预训练模型

Python
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 练习题

基础题

  1. 简答题
  2. ResNet的残差连接有什么作用?

    残差连接(skip connection)让网络学习残差映射 F(x)=H(x)-x 而非直接学习 H(x),缓解了深层网络的梯度消失和网络退化问题,使训练数百层网络成为可能,同时提供恒等映射的捷径路径。

  3. VGG为什么使用3x3卷积核?

    两个3×3卷积等效于一个5×5的感受野,三个3×3等效于7×7,但参数更少(如 3×3²C²=27C² vs 7²C²=49C²),且引入更多非线性激活(更多ReLU层),增强了模型表达能力。

进阶题

  1. 编程题
  2. 实现一个简单的ResNet块。
  3. 使用预训练模型进行迁移学习。

6.8 面试准备

大厂面试题

Q1: ResNet的残差连接解决了什么问题?

参考答案: - 梯度消失问题 - 网络退化问题 - 允许训练更深的网络 - 提供恒等映射路径

Q2: 为什么VGG使用多个3x3卷积代替大卷积核?

参考答案: - 参数更少 - 非线性更多 - 感受野相同 - 更容易训练


6.9 本章小结

核心知识点

  1. LeNet:第一个CNN
  2. AlexNet:引入ReLU、Dropout
  3. VGG:小卷积核、深层网络
  4. GoogLeNet:Inception模块
  5. ResNet:残差连接

下一步

下一章07-目标检测.md - 学习目标检测


恭喜完成第6章! 🎉