AI

深度学习之现代卷积神经网络

动手深度学习

Posted by LXG on March 31, 2026

动手深度学习

现代卷积神经网络

阶段 时间 核心思想 代表
传统视觉 2010前 手工特征 SIFT/HOG
CNN革命 2012 自动特征学习 AlexNet
深层网络 2014 加深网络 VGG
训练突破 2015 残差 ResNet
检测时代 2016 端到端检测 YOLO
轻量化 2017 高效计算 MobileNet
Transformer 2020 注意力机制 ViT

AlexNet

CNN 是一个“逐层抽象”的特征学习系统

  • 低层:边缘 / 颜色 / 纹理
  • 中层:局部结构,比如:鼻子,眼睛,轮廓
  • 高层:语义特征, 比如: 人 狗 飞机

深度学习之所以在2012年后爆发,是因为ImageNet提供了前所未有的大规模标注数据,使得高参数量的深度模型能够真正发挥优势,从而全面超越传统依赖小数据和人工特征的方法。

李飞飞通过构建 ImageNet,把“数据规模”带入计算机视觉,使深度学习模型第一次在真实世界任务中大幅超越传统方法,从而引爆了整个AI时代。

AlexNet和LeNet 对比

维度 LeNet AlexNet
年代 1998 2012
任务 手写数字 ImageNet(1000类)
输入 28×28 灰度 224×224 RGB
深度 浅(5层左右) 深(8层)
参数量 ~6万 ~6000万
激活函数 Sigmoid ReLU
池化 AvgPool MaxPool
数据规模 小数据集 大规模数据
训练设备 CPU GPU
影响力 开创CNN 引爆深度学习

LeNet 证明了卷积神经网络“可行”,而 AlexNet 证明了它在大数据和GPU支持下“可以成为最强方法”,从而开启了深度学习时代。

AlexNet模型文字图


	Input: 224×224×3 (RGB Image)

	↓
	Conv1: 96 filters, 11×11, stride=4, padding=0
	→ ReLU
	→ MaxPool: 3×3, stride=2

	↓
	Conv2: 256 filters, 5×5, stride=1, padding=2
	→ ReLU
	→ MaxPool: 3×3, stride=2

	↓
	Conv3: 384 filters, 3×3, stride=1, padding=1
	→ ReLU

	↓
	Conv4: 384 filters, 3×3, stride=1, padding=1
	→ ReLU

	↓
	Conv5: 256 filters, 3×3, stride=1, padding=1
	→ ReLU
	→ MaxPool: 3×3, stride=2

	↓
	Flatten

	↓
	FC6: 4096
	→ ReLU
	→ Dropout

	↓
	FC7: 4096
	→ ReLU
	→ Dropout

	↓
	FC8: 1000 (类别数)

	↓
	Softmax

训练AlexNet模型


import torch
from torch import nn
from d2l import torch as d2l
import time

# 使用 nn.Sequential 搭建 AlexNet 风格网络(针对 Fashion-MNIST 的 10 分类任务)
net = nn.Sequential(
    # 这里使用一个11*11的更大窗口来捕捉对象。
    # 同时,步幅为4,以减少输出的高度和宽度。
    # 另外,输出通道的数目远大于LeNet
    nn.Conv2d(1, 96, kernel_size=11, stride=4, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 减小卷积窗口,使用填充为2来使得输入与输出的高和宽一致,且增大输出通道数
    nn.Conv2d(96, 256, kernel_size=5, padding=2), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    # 使用三个连续的卷积层和较小的卷积窗口。
    # 除了最后的卷积层,输出通道的数量进一步增加。
    # 在前两个卷积层之后,汇聚层不用于减少输入的高度和宽度
    nn.Conv2d(256, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 384, kernel_size=3, padding=1), nn.ReLU(),
    nn.Conv2d(384, 256, kernel_size=3, padding=1), nn.ReLU(),
    nn.MaxPool2d(kernel_size=3, stride=2),
    nn.Flatten(),
    # 这里,全连接层的输出数量是LeNet中的好几倍。使用dropout层来减轻过拟合
    nn.Linear(6400, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    nn.Linear(4096, 4096), nn.ReLU(),
    nn.Dropout(p=0.5),
    # 最后是输出层。由于这里使用Fashion-MNIST,所以用类别数为10,而非论文中的1000
    nn.Linear(4096, 10))

# 构造一个形状为 (batch_size, channel, height, width) = (1, 1, 224, 224) 的随机输入
# 通过逐层前向传播,打印每一层输出形状,便于理解特征图尺寸如何变化
X = torch.randn(1, 1, 224, 224)
for layer in net:
    X=layer(X)
    print(layer.__class__.__name__,'output shape:\t',X.shape)

# 读取 Fashion-MNIST 数据集,并将图像统一缩放到 224x224(与 AlexNet 输入尺寸匹配)
batch_size = 128
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)

# 设置训练超参数:
# - lr:学习率
# - num_epochs:训练轮数
lr, num_epochs = 0.01, 10
start_time = time.time()
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
end_time = time.time()
print(f"训练总时长: {end_time - start_time:.2f} 秒")


操作 输出尺寸 作用
输入 - 1×224×224 灰度图
Conv1 96×11×11, s=4 96×54×54 大感受野
Pool1 3×3, s=2 96×26×26 降采样
Conv2 256×5×5 256×26×26 提取复杂特征
Pool2 3×3, s=2 256×12×12 再降采样
Conv3 384×3×3 384×12×12 深层特征
Conv4 384×3×3 384×12×12 特征组合
Conv5 256×3×3 256×12×12 高级特征
Pool3 3×3, s=2 256×5×5 压缩
Flatten - 6400 展平
FC1 4096 4096 高维表示
FC2 4096 4096 深层表达
FC3 10 10 分类

运行结果


Conv2d output shape:     torch.Size([1, 96, 54, 54])
ReLU output shape:       torch.Size([1, 96, 54, 54])
MaxPool2d output shape:  torch.Size([1, 96, 26, 26])
Conv2d output shape:     torch.Size([1, 256, 26, 26])
ReLU output shape:       torch.Size([1, 256, 26, 26])
MaxPool2d output shape:  torch.Size([1, 256, 12, 12])
Conv2d output shape:     torch.Size([1, 384, 12, 12])
ReLU output shape:       torch.Size([1, 384, 12, 12])
Conv2d output shape:     torch.Size([1, 384, 12, 12])
ReLU output shape:       torch.Size([1, 384, 12, 12])
Conv2d output shape:     torch.Size([1, 256, 12, 12])
ReLU output shape:       torch.Size([1, 256, 12, 12])
MaxPool2d output shape:  torch.Size([1, 256, 5, 5])
Flatten output shape:    torch.Size([1, 6400])
Linear output shape:     torch.Size([1, 4096])
ReLU output shape:       torch.Size([1, 4096])
Dropout output shape:    torch.Size([1, 4096])
Linear output shape:     torch.Size([1, 4096])
ReLU output shape:       torch.Size([1, 4096])
Dropout output shape:    torch.Size([1, 4096])
Linear output shape:     torch.Size([1, 10])
training on cuda:0
loss 0.328, train acc 0.881, test acc 0.881
2568.7 examples/sec on cuda:0
RTX3060 训练总时长: 290.84 秒

如果换成 ImageNet(经典大数据集)

数据集 变化
ImageNet  
分辨率 224×224
数据量 100万+
类别 1000类
  • 数据量: 6万 → 100万 ≈ 16倍
  • 分辨率:28² → 224² ≈ 64倍
  • 通道:1 → 3 ≈ 3倍

训练时长可能变成:几天 ~ 一周(单卡 RTX3060)

使用块的网络(VGG)

经典卷积神经网络的基本组成部分是下面的这个序列:

  • 带填充以保持分辨率的卷积层;
  • 非线性激活函数,如ReLU;
  • 汇聚层,如最大汇聚层。

代码


# -*- coding: utf-8 -*-
import torch
from torch import nn
from d2l import torch as d2l

# 定义一个 VGG 风格的卷积模块构建函数
# num_convs:该模块中卷积层的数量
# in_channels:输入通道数
# out_channels:输出通道数(该模块内所有卷积层统一使用该通道数)
def vgg_block(num_convs, in_channels, out_channels):
    # 使用列表暂存模块中的各层,最后再打包成 nn.Sequential
    layers = []

    # 按照指定次数堆叠“卷积层 + ReLU 激活”结构
    for _ in range(num_convs):
        # 3x3 卷积核,padding=1 可保持特征图高宽不变(步幅默认为 1)
        layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))
        # 非线性激活函数,提升网络表达能力
        layers.append(nn.ReLU())
        # 下一层卷积的输入通道应等于当前层输出通道
        in_channels = out_channels

    # 模块末尾添加最大池化层,将特征图尺寸减半(高宽各除以 2)
    layers.append(nn.MaxPool2d(kernel_size=2, stride=2))

    # 将列表中的层按顺序封装为一个可直接调用的子网络模块
    return nn.Sequential(*layers)

# VGG 各卷积块配置,元组格式为 (卷积层数量, 输出通道数)
# 共有 5 个卷积块,通道数逐步增大以提取更高层语义特征
conv_arch = ((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))

# 根据给定的卷积块配置构建完整 VGG 网络
# conv_arch:卷积块结构定义,例如 ((1, 64), (1, 128), ...)
def vgg(conv_arch):
    # 用于存放每个卷积块
    conv_blks = []
    # Fashion-MNIST 为灰度图,输入通道数为 1
    in_channels = 1
    # 卷积层部分
    for (num_convs, out_channels) in conv_arch:
        # 逐块构建并追加到网络中
        conv_blks.append(vgg_block(num_convs, in_channels, out_channels))
        # 下一块的输入通道等于当前块输出通道
        in_channels = out_channels

    # 组装完整网络:卷积特征提取 + 展平 + 全连接分类器
    return nn.Sequential(
        *conv_blks, nn.Flatten(),
        # 全连接层部分
        # 输入维度 out_channels * 7 * 7 的原因:
        # 输入图片 224x224,经过 5 次 2x2/stride=2 池化后,空间尺寸变为 7x7
        nn.Linear(out_channels * 7 * 7, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
        nn.Linear(4096, 10))

# 先用标准 VGG 配置实例化网络
net = vgg(conv_arch)

# 用随机输入做一次前向传播,打印每个子模块输出形状,便于检查网络结构
X = torch.randn(size=(1, 1, 224, 224))
for blk in net:
    X = blk(X)
    print(blk.__class__.__name__,'output shape:\t',X.shape)

# 为了降低计算开销,这里把每个卷积块的通道数按比例缩小(VGG-11 简化版)
ratio = 4
# 例如 64->16, 128->32, ...,卷积层数量保持不变
small_conv_arch = [(pair[0], pair[1] // ratio) for pair in conv_arch]
net = vgg(small_conv_arch)

# 设置训练超参数
# lr(learning rate,学习率):
# 每次参数更新时沿梯度方向前进的步长大小。值越大收敛可能更快,但过大容易震荡或发散。
# 这里设为 0.05,适用于当前简化版 VGG + d2l.train_ch6 的默认优化配置。
# num_epochs(训练轮数):
# 完整遍历训练集的次数。10 表示模型会把训练数据完整学习 10 遍。
# batch_size(批量大小):
# 每次前向/反向传播使用的样本数。128 在训练速度与显存占用之间做折中。
lr, num_epochs, batch_size = 0.05, 10, 128
# 加载 Fashion-MNIST,并缩放到 224x224 以匹配 VGG 输入尺寸
# resize=224:把原始 28x28 图像放大到 224x224,使其适配 VGG 结构的输入分辨率假设。
# train_iter / test_iter:分别是训练集与测试集的小批量数据迭代器,供训练与评估阶段使用。
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
# 调用第 6 章封装的训练函数进行训练,优先使用可用 GPU
# 参数含义依次为:
# 1) net:待训练网络
# 2) train_iter:训练数据迭代器
# 3) test_iter:测试数据迭代器
# 4) num_epochs:训练轮数
# 5) lr:学习率
# 6) d2l.try_gpu():自动选择可用 GPU;若无 GPU,则退回 CPU
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

运行结果


Sequential output shape:         torch.Size([1, 64, 112, 112])
Sequential output shape:         torch.Size([1, 128, 56, 56])
Sequential output shape:         torch.Size([1, 256, 28, 28])
Sequential output shape:         torch.Size([1, 512, 14, 14])
Sequential output shape:         torch.Size([1, 512, 7, 7])
Flatten output shape:    torch.Size([1, 25088])
Linear output shape:     torch.Size([1, 4096])
ReLU output shape:       torch.Size([1, 4096])
Dropout output shape:    torch.Size([1, 4096])
Linear output shape:     torch.Size([1, 4096])
ReLU output shape:       torch.Size([1, 4096])
Dropout output shape:    torch.Size([1, 4096])
Linear output shape:     torch.Size([1, 10])
loss 0.175, train acc 0.935, test acc 0.916
1663.4 examples/sec on cuda:0
训练总时长: 420.75 秒

VGG11 模型结构

Block块


Block =
    Conv3×3
    ReLU
    Conv3×3
    ReLU
    MaxPool

整体结构


[输入] 1×224×224
   │
   ├── Block1
   │     Conv(1→64)
   │     ReLU
   │     MaxPool
   │     ↓
   │     64×112×112
   │
   ├── Block2
   │     Conv(64→128)
   │     ReLU
   │     MaxPool
   │     ↓
   │     128×56×56
   │
   ├── Block3
   │     Conv(128→256)
   │     ReLU
   │     Conv(256→256)
   │     ReLU
   │     MaxPool
   │     ↓
   │     256×28×28
   │
   ├── Block4
   │     Conv(256→512)
   │     ReLU
   │     Conv(512→512)
   │     ReLU
   │     MaxPool
   │     ↓
   │     512×14×14
   │
   ├── Block5
   │     Conv(512→512)
   │     ReLU
   │     Conv(512→512)
   │     ReLU
   │     MaxPool
   │     ↓
   │     512×7×7
   │
   ├── Flatten
   │     ↓
   │     25088
   │
   ├── FC1 (4096)
   │     ReLU + Dropout
   │
   ├── FC2 (4096)
   │     ReLU + Dropout
   │
   └── FC3 (10类输出)

规律1:尺寸变化

224 → 112 → 56 → 28 → 14 → 7 (每个Block减半)

规律2:通道变化

1 → 64 → 128 → 256 → 512 → 512 (逐步增加)

总结


① Backbone(特征提取)
   Block1~Block5

② Neck(无)
   VGG没有

③ Head(分类器)
   FC层

网络中的网络(NiN)

LeNet、AlexNet和VGG 三代模型的共同模式


输入
 ↓
卷积层 + 池化层(提取空间特征)
 ↓
Flatten(打平)
 ↓
全连接层(做分类)
 ↓
输出

问题

  • 问题1:Flatten 破坏空间结构
  • 问题2:参数爆炸
  • 问题3:不适合输入变化,更换分辨率麻烦

NiN块


# -*- coding: utf-8 -*-
import torch
from torch import nn
from d2l import torch as d2l
import time

# 定义 NiN(Network in Network)中的基础模块
# in_channels:输入通道数
# out_channels:输出通道数
# kernel_size:第一层卷积核大小(用于提取空间特征)
# strides:第一层卷积步幅
# padding:第一层卷积填充
def nin_block(in_channels, out_channels, kernel_size, strides, padding):
    # 模块结构:
    # 1) 普通卷积(负责空间信息建模)
    # 2) 1x1 卷积(相当于逐像素位置上的全连接,用于通道间特征重组)
    # 3) 再接一个 1x1 卷积,进一步增强非线性表达能力
    # 每层卷积后都使用 ReLU 激活函数
    return nn.Sequential(
        nn.Conv2d(in_channels, out_channels, kernel_size, strides, padding),
        nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU(),
        nn.Conv2d(out_channels, out_channels, kernel_size=1), nn.ReLU())

# 搭建完整 NiN 网络
# 整体思路:若干个 nin_block 提取特征,中间穿插池化降采样,最后用全局平均池化替代大规模全连接层
net = nn.Sequential(
    # 第一阶段:大卷积核 + 较大步幅,快速降低空间分辨率并提取初级特征
    nin_block(1, 96, kernel_size=11, strides=4, padding=0),
    # 最大池化进一步下采样,降低计算量并增强平移不变性
    nn.MaxPool2d(3, stride=2),
    # 第二阶段:5x5 卷积继续提取中层特征,padding=2 保持卷积前后高宽一致(在步幅为 1 时)
    nin_block(96, 256, kernel_size=5, strides=1, padding=2),
    nn.MaxPool2d(3, stride=2),
    # 第三阶段:3x3 卷积提取更高层语义特征
    nin_block(256, 384, kernel_size=3, strides=1, padding=1),
    nn.MaxPool2d(3, stride=2),
    # Dropout 在训练时随机失活部分神经元,缓解过拟合
    nn.Dropout(0.5),
    # 标签类别数是10
    nin_block(384, 10, kernel_size=3, strides=1, padding=1),
    # 自适应全局平均池化:把每个通道压缩为 1x1,得到每类一个响应值
    nn.AdaptiveAvgPool2d((1, 1)),
    # 将四维的输出转成二维的输出,其形状为(批量大小,10)
    nn.Flatten())

# 构造一个模拟输入(1 张 224x224 灰度图),逐层打印输出形状用于检查网络结构是否正确
X = torch.rand(size=(1, 1, 224, 224))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

# 设置训练超参数
# lr(学习率):控制参数更新步长,过大可能震荡,过小收敛较慢
# num_epochs(训练轮数):完整遍历训练集的次数
# batch_size(批量大小):每次迭代用于计算梯度的样本数量
lr, num_epochs, batch_size = 0.1, 10, 128
# 加载 Fashion-MNIST 数据,并将 28x28 图像缩放到 224x224 以匹配当前网络输入尺寸
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=224)
# 调用 d2l 封装的训练流程,自动选择可用 GPU(若不可用则退回 CPU)
# 记录训练开始时间与结束时间,并打印模型训练总耗时(单位:秒)
start_time = time.time()
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
end_time = time.time()
print(f"模型训练总时长: {end_time - start_time:.2f} 秒")

运行结果


Sequential output shape:         torch.Size([1, 96, 54, 54])
MaxPool2d output shape:  torch.Size([1, 96, 26, 26])
Sequential output shape:         torch.Size([1, 256, 26, 26])
MaxPool2d output shape:  torch.Size([1, 256, 12, 12])
Sequential output shape:         torch.Size([1, 384, 12, 12])
MaxPool2d output shape:  torch.Size([1, 384, 5, 5])
Dropout output shape:    torch.Size([1, 384, 5, 5])
Sequential output shape:         torch.Size([1, 10, 5, 5])
AdaptiveAvgPool2d output shape:  torch.Size([1, 10, 1, 1])
Flatten output shape:    torch.Size([1, 10])
training on cuda:0
loss 0.390, train acc 0.855, test acc 0.866
2336.1 examples/sec on cuda:0
模型训练总时长: 314.30 秒

模型文字图


输入: 1×224×224
   │
   ├── NiN Block1
   │     Conv(1→96, 11×11, stride=4)
   │     ReLU
   │     1×1 Conv(96→96)
   │     ReLU
   │     1×1 Conv(96→96)
   │     ReLU
   │     ↓
   │     96×54×54
   │
   ├── MaxPool(3×3, stride=2)
   │     ↓
   │     96×26×26
   │
   ├── NiN Block2
   │     Conv(96→256, 5×5)
   │     ReLU
   │     1×1 Conv(256→256)
   │     ReLU
   │     1×1 Conv(256→256)
   │     ReLU
   │     ↓
   │     256×26×26
   │
   ├── MaxPool
   │     ↓
   │     256×12×12
   │
   ├── NiN Block3
   │     Conv(256→384, 3×3)
   │     ReLU
   │     1×1 Conv(384→384)
   │     ReLU
   │     1×1 Conv(384→384)
   │     ReLU
   │     ↓
   │     384×12×12
   │
   ├── MaxPool
   │     ↓
   │     384×5×5
   │
   ├── Dropout
   │
   ├── NiN Block4(关键🔥)
   │     Conv(384→10, 3×3)
   │     ReLU
   │     1×1 Conv(10→10)
   │     ReLU
   │     1×1 Conv(10→10)
   │     ReLU
   │     ↓
   │     10×5×5
   │
   ├── Global Avg Pool
   │     ↓
   │     10×1×1
   │
   ├── Flatten
   │     ↓
   │     10

参数数量计算


输入: 1×224×224
   │
   ├── NiN Block1(≈ 30,336)
   │
   │   Conv(1→96, 11×11)
   │   = 96×1×11×11 + 96
   │   = 96×121 + 96
   │   = 11,712
   │
   │   1×1 Conv(96→96)
   │   = 96×96×1×1 + 96
   │   = 9,216 + 96
   │   = 9,312
   │
   │   1×1 Conv(96→96)
   │   = 96×96 + 96
   │   = 9,312
   │
   │   ↓ 输出: 96×54×54
   │
   ├── MaxPool(无参数)
   │
   ├── NiN Block2(≈ 746,240)
   │
   │   Conv(96→256, 5×5)
   │   = 256×96×5×5 + 256
   │   = 256×2400 + 256
   │   = 614,656
   │
   │   1×1 Conv(256→256)
   │   = 256×256 + 256
   │   = 65,792
   │
   │   1×1 Conv(256→256)
   │   = 256×256 + 256
   │   = 65,792
   │
   │   ↓ 输出: 256×26×26
   │
   ├── MaxPool(无参数)
   │
   ├── NiN Block3(≈ 1,180,800)
   │
   │   Conv(256→384, 3×3)
   │   = 384×256×3×3 + 384
   │   = 384×2304 + 384
   │   = 885,120
   │
   │   1×1 Conv(384→384)
   │   = 384×384 + 384
   │   = 147,840
   │
   │   1×1 Conv(384→384)
   │   = 384×384 + 384
   │   = 147,840
   │
   │   ↓ 输出: 384×12×12
   │
   ├── MaxPool(无参数)
   │
   ├── Dropout(无参数)
   │
   ├── NiN Block4(≈ 34,790)
   │
   │   Conv(384→10, 3×3)
   │   = 10×384×3×3 + 10
   │   = 10×3456 + 10
   │   = 34,570
   │
   │   1×1 Conv(10→10)
   │   = 10×10 + 10
   │   = 110
   │
   │   1×1 Conv(10→10)
   │   = 10×10 + 10
   │   = 110
   │
   │   ↓ 输出: 10×5×5
   │
   ├── Global Avg Pool(无参数)
   │
   ├── Flatten(无参数)
   │
   └── 输出: 10
   
30,336 + 746,240 + 1,180,800 + 34,790
----------------
≈ 1,992,166(约 199 万)

每层的含义理解


输入: 1×224×224
(灰度图,原始像素)

Block1(低级特征提取)


Conv(1→96, 11×11, stride=4)
👉 作用:快速提取“边缘、轮廓”等低级特征
👉 同时大步幅降采样(减少计算)

ReLU
👉 引入非线性(让模型能表达复杂关系)

1×1 Conv(96→96)
👉 作用:通道之间信息融合(像“像素级全连接”)

ReLU

1×1 Conv(96→96)
👉 再做一次非线性变换(增强表达能力)

ReLU

输出: 96×54×54
👉 已经变成“96种特征图”

MaxPool(3×3, stride=2)
👉 作用:
- 降低分辨率(54→26)
- 保留最强特征(增强鲁棒性)

Block2(中级特征)


Conv(96→256, 5×5)
👉 作用:
提取更复杂的结构(纹理、局部组合)

ReLU

1×1 Conv(256→256)
👉 作用:
通道重组(学习“哪些特征组合更重要”)

ReLU

1×1 Conv(256→256)
👉 再增强表达能力

ReLU

输出: 256×26×26
👉 特征更抽象

MaxPool
👉 进一步压缩空间(26→12)
👉 提高感受野

Block3(高级语义特征🔥)


Conv(256→384, 3×3)
👉 作用:
提取“类别相关特征”(比如衣服形状)

ReLU

1×1 Conv(384→384)
👉 通道级“MLP”,组合复杂语义

ReLU

1×1 Conv(384→384)
👉 进一步增强非线性表达

ReLU

输出: 384×12×12
👉 已经接近“语义层”

MaxPool
👉 空间进一步压缩(12→5)
👉 为分类做准备

Dropout
👉 作用:
随机丢弃神经元(防止过拟合)
👉 只在训练时生效

Block4(分类层🔥核心)

Block4 = 把“384维特征” → 转换成“10个类别的打分”

这384维是什么?

可以理解为

像不像:

  • 边缘
  • 纹理
  • 衣领
  • 袖子
  • 裤腿
  • 鞋底

这一步本质是:用10个“分类器”,去扫描每个位置,看它更像哪一类。


Conv(384→10, 3×3)
👉 作用:
把“特征”映射到“类别空间”
👉 10个通道 = 10个类别

ReLU

1×1 Conv(10→10)
👉 细化类别判断(像分类器)

ReLU

1×1 Conv(10→10)
👉 再增强分类能力

ReLU

输出: 10×5×5
👉 每个通道 = 一个类别的“响应图”

Global Average Pooling(关键🔥)

把5×5所有位置平均


10×5×5 → 10×1×1

👉 作用:
每个类别做“空间平均”

👉 本质:
整张图对每个类别进行“投票”

Flatten


👉 作用:
变成向量 (batch, 10)
👉 直接作为 logits

总结


像素
 ↓
低级特征(边缘)
 ↓
中级特征(纹理)
 ↓
高级特征(形状/语义)
 ↓
类别响应图(10个通道)
 ↓
全局平均(投票)
 ↓
分类结果

含并行连结的网络(GoogLeNet)


# -*- coding: utf-8 -*-
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
import time


# Inception 模块是 GoogLeNet 的核心结构:
# 通过并行的多分支卷积(1x1、3x3、5x5)和池化分支,
# 同时提取不同感受野的特征,再在通道维拼接。
class Inception(nn.Module):
    # c1--c4是每条路径的输出通道数
    def __init__(self, in_channels, c1, c2, c3, c4, **kwargs):
        super(Inception, self).__init__(**kwargs)
        # 线路1,单1x1卷积层
        self.p1_1 = nn.Conv2d(in_channels, c1, kernel_size=1)
        # 线路2,1x1卷积层后接3x3卷积层
        self.p2_1 = nn.Conv2d(in_channels, c2[0], kernel_size=1)
        self.p2_2 = nn.Conv2d(c2[0], c2[1], kernel_size=3, padding=1)
        # 线路3,1x1卷积层后接5x5卷积层
        self.p3_1 = nn.Conv2d(in_channels, c3[0], kernel_size=1)
        self.p3_2 = nn.Conv2d(c3[0], c3[1], kernel_size=5, padding=2)
        # 线路4,3x3最大汇聚层后接1x1卷积层
        self.p4_1 = nn.MaxPool2d(kernel_size=3, stride=1, padding=1)
        self.p4_2 = nn.Conv2d(in_channels, c4, kernel_size=1)

    def forward(self, x):
        # 路径1:1x1 卷积
        p1 = F.relu(self.p1_1(x))
        # 路径2:1x1 降维 + 3x3 卷积
        p2 = F.relu(self.p2_2(F.relu(self.p2_1(x))))
        # 路径3:1x1 降维 + 5x5 卷积
        p3 = F.relu(self.p3_2(F.relu(self.p3_1(x))))
        # 路径4:3x3 最大池化 + 1x1 卷积
        p4 = F.relu(self.p4_2(self.p4_1(x)))
        # 在通道维度上连结输出
        return torch.cat((p1, p2, p3, p4), dim=1)

# b1:网络前端的“Stem”阶段
# - 7x7 大卷积(stride=2)快速扩大感受野并降采样
# - 接最大池化进一步压缩空间尺寸
# - 输入通道 1(灰度图)-> 输出通道 64
b1 = nn.Sequential(nn.Conv2d(1, 64, kernel_size=7, stride=2, padding=3),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

# b2:过渡阶段
# - 先用 1x1 卷积做通道变换/特征重组
# - 再用 3x3 卷积提取更丰富局部模式
# - 最后池化降采样,为后续 Inception 模块做准备
b2 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=1),
                   nn.ReLU(),
                   nn.Conv2d(64, 192, kernel_size=3, padding=1),
                   nn.ReLU(),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

# b3:Inception 堆叠的第一组
# - 连续两个 Inception 模块并行提取多尺度特征
# - 末尾池化继续降低特征图分辨率
# - 通道数从 192 逐步提升到 480(拼接后)
b3 = nn.Sequential(Inception(192, 64, (96, 128), (16, 32), 32),
                   Inception(256, 128, (128, 192), (32, 96), 64),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

# b4:Inception 堆叠的主体部分(最深、计算量最大)
# - 共 5 个 Inception 模块,持续进行多分支特征提取
# - 通过不同的分支通道配置,平衡表达能力与计算开销
# - 末尾池化为最终分类阶段做下采样
b4 = nn.Sequential(Inception(480, 192, (96, 208), (16, 48), 64),
                   Inception(512, 160, (112, 224), (24, 64), 64),
                   Inception(512, 128, (128, 256), (24, 64), 64),
                   Inception(512, 112, (144, 288), (32, 64), 64),
                   Inception(528, 256, (160, 320), (32, 128), 128),
                   nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

# b5:Inception 堆叠的收尾阶段
# - 继续用 2 个 Inception 模块整合高层语义
# - 用全局平均池化把每个通道压成 1x1,显著减少全连接参数
# - Flatten 后得到分类器输入向量
b5 = nn.Sequential(Inception(832, 256, (160, 320), (32, 128), 128),
                   Inception(832, 384, (192, 384), (48, 128), 128),
                   # 全局平均池化:每个通道压缩为 1x1,减少参数量并增强泛化
                   nn.AdaptiveAvgPool2d((1,1)),
                   nn.Flatten())

# 最终分类层:1024 -> 10 类
net = nn.Sequential(b1, b2, b3, b4, b5, nn.Linear(1024, 10))

# 用随机输入(1,1,96,96)做一次前向传播,打印每个阶段输出形状
X = torch.rand(size=(1, 1, 96, 96))
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape:\t', X.shape)

# 训练超参数说明:
# lr:学习率,控制参数更新步长
# num_epochs:训练轮数,表示完整遍历训练集的次数
# batch_size:批量大小,表示每次迭代使用的样本数
lr, num_epochs, batch_size = 0.1, 10, 128
# 加载 Fashion-MNIST,并把输入尺寸缩放到 96x96(与当前网络输入设定一致)
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size, resize=96)
# 记录训练总时长(秒)
start_time = time.time()
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())
end_time = time.time()
print(f"模型训练总时长: {end_time - start_time:.2f} 秒")

模型理解

Inception = 同时用不同“感受野”去看同一张图,然后把结果拼起来


                输入
                 │
   ┌─────────────┼─────────────┐
   │             │             │             │
 1×1           1×1→3×3       1×1→5×5       Pool→1×1
   │             │             │             │
 小感受野      中感受野      大感受野      稳定特征
   │             │             │             │
   └─────────────┴─────────────┴─────────────┘
                    ↓
              concat(拼接)

模型文字图


输入: 192 × H × W
│
├────────────── 路径1 ──────────────
│  1×1 Conv (192 → 64)
│  参数 = 192×64×1×1 + 64 = 12,288 + 64 = 12,352
│
│  👉 作用:
│  - 通道融合(NiN思想)
│  - 提取细粒度特征
│
│  输出: 64 × H × W
│
├────────────── 路径2 ──────────────
│  1×1 Conv (192 → 96)
│  参数 = 192×96 + 96 = 18,432 + 96 = 18,528
│
│  3×3 Conv (96 → 128)
│  参数 = 96×128×3×3 + 128
│       = 96×128×9 + 128
│       = 110,592 + 128 = 110,720
│
│  👉 作用:
│  - 1×1:降维(关键🔥)
│  - 3×3:提取局部结构(纹理/边缘)
│
│  输出: 128 × H × W
│
├────────────── 路径3 ──────────────
│  1×1 Conv (192 → 16)
│  参数 = 192×16 + 16 = 3,072 + 16 = 3,088
│
│  5×5 Conv (16 → 32)
│  参数 = 16×32×5×5 + 32
│       = 16×32×25 + 32
│       = 12,800 + 32 = 12,832
│
│  👉 作用:
│  - 捕获更大范围(形状/整体结构)
│  - 1×1避免计算爆炸
│
│  输出: 32 × H × W
│
├────────────── 路径4 ──────────────
│  MaxPool (3×3)
│  参数 = 0
│
│  1×1 Conv (192 → 32)
│  参数 = 192×32 + 32 = 6,144 + 32 = 6,176
│
│  👉 作用:
│  - 提取稳定特征(抗噪声)
│  - 保留背景信息
│
│  输出: 32 × H × W
│
└────────────── concat ──────────────
   拼接通道(dim=1)
   
输出: (64 + 128 + 32 + 32) = 256 × H × W

输出: 256 × H × W
│
│  (后续经过多个 Inception 堆叠)
│
▼
假设最终输出:
1024 × 3 × 3
│
├────────────── Global Avg Pool ──────────────
│  参数 = 0
│
│  1024 × 3 × 3 → 1024 × 1 × 1
│
│  👉 作用:
│  - 每个通道做“全局投票”
│  - 消除空间维度
│
│
├────────────── Flatten ──────────────
│  1024 × 1 × 1 → 1024
│
│  👉 作用:
│  - 转为向量
│
│
└────────────── FC(Linear)──────────────
   Linear(1024 → 10)
   参数 = 1024×10 + 10 = 10,250

   👉 作用:
   - 输出最终类别分数(logits)

最终输出: 10类

GoogLeNet 优缺点

维度 优点 ✅ 缺点 ❌
计算效率 1×1 降维,大幅减少计算量 结构复杂,难以优化
参数量 比 VGGNet 小很多 仍不算极致轻量
特征能力 多尺度并行(1×1 / 3×3 / 5×5) 分支信息融合不够智能
泛化能力 使用 GAP,减少过拟合 训练调参复杂
工程实现 理论先进(模块化) 分支多,不利于硬件加速
扩展性 可堆叠 Inception Block 不如 ResNet 易扩展
训练难度 比 VGG 稍好 不如 ResNet 稳定

批量规范化


# -*- coding: utf-8 -*-
import torch
from torch import nn
from d2l import torch as d2l

# 手写批量归一化(Batch Normalization)核心计算函数
# X:输入张量(可能来自全连接层,也可能来自卷积层)
# gamma、beta:可学习的缩放与偏移参数
# moving_mean、moving_var:推理阶段使用的滑动均值与滑动方差
# eps:防止除零的小常数
# momentum:滑动平均更新系数
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
    # 通过is_grad_enabled来判断当前模式是训练模式还是预测模式
    if not torch.is_grad_enabled():
        # 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
        X_hat = (X - moving_mean) / torch.sqrt(moving_var + eps)
    else:
        assert len(X.shape) in (2, 4)
        if len(X.shape) == 2:
            # 使用全连接层的情况,计算特征维上的均值和方差
            mean = X.mean(dim=0)
            var = ((X - mean) ** 2).mean(dim=0)
        else:
            # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。
            # 这里我们需要保持X的形状以便后面可以做广播运算
            mean = X.mean(dim=(0, 2, 3), keepdim=True)
            var = ((X - mean) ** 2).mean(dim=(0, 2, 3), keepdim=True)
        # 训练模式下,用当前的均值和方差做标准化
        X_hat = (X - mean) / torch.sqrt(var + eps)
        # 更新移动平均的均值和方差
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
    Y = gamma * X_hat + beta  # 缩放和移位
    return Y, moving_mean.data, moving_var.data

class BatchNorm(nn.Module):
    # num_features:完全连接层的输出数量或卷积层的输出通道数。
    # num_dims:2表示完全连接层,4表示卷积层
    def __init__(self, num_features, num_dims):
        super().__init__()
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成1和0
        self.gamma = nn.Parameter(torch.ones(shape))
        self.beta = nn.Parameter(torch.zeros(shape))
        # 非模型参数的变量初始化为0和1
        self.moving_mean = torch.zeros(shape)
        self.moving_var = torch.ones(shape)

    def forward(self, X):
        # 如果X不在内存上,将moving_mean和moving_var
        # 复制到X所在显存上
        if self.moving_mean.device != X.device:
            self.moving_mean = self.moving_mean.to(X.device)
            self.moving_var = self.moving_var.to(X.device)
        # 调用手写 batch_norm 完成归一化,并保存更新后的滑动统计量
        Y, self.moving_mean, self.moving_var = batch_norm(
            X, self.gamma, self.beta, self.moving_mean,
            self.moving_var, eps=1e-5, momentum=0.9)
        return Y

# 基于 LeNet 风格网络插入 BatchNorm:
# 卷积层后接 BN + Sigmoid,再做池化;
# 全连接层后也使用 BN,帮助稳定训练并加速收敛。
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5), BatchNorm(6, num_dims=4), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), BatchNorm(16, num_dims=4), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
    nn.Linear(16*4*4, 120), BatchNorm(120, num_dims=2), nn.Sigmoid(),
    nn.Linear(120, 84), BatchNorm(84, num_dims=2), nn.Sigmoid(),
    nn.Linear(84, 10))

# 训练超参数:
# lr:学习率,控制参数更新步长
# num_epochs:训练轮数(完整遍历训练集的次数)
# batch_size:每个小批量样本数
lr, num_epochs, batch_size = 1.0, 10, 256
# 加载 Fashion-MNIST 数据集(保持原始 28x28 分辨率)
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
# 调用 d2l 封装训练流程,自动选择 GPU(若可用)或回退到 CPU
d2l.train_ch6(net, train_iter, test_iter, num_epochs, lr, d2l.try_gpu())

BatchNorm 不是为了“让数据变好看”,而是为了“让训练更容易”

对比参数规范化和没有规范化的训练曲线

学习率 0.1

bn_comparison_curve


重复次数: 3
随机种子: [42, 43, 44]
无批量规范化平均训练时长: 13.77 秒
有批量规范化平均训练时长: 15.31 秒
最终测试准确率(无BN, mean±std): 0.5227 ± 0.0408
最终测试准确率(有BN, mean±std): 0.8243 ± 0.0332

学习率 1.0

bn_comparison_curve_lr_1


重复次数: 3
随机种子: [42, 43, 44]
无批量规范化平均训练时长: 13.72 秒
有批量规范化平均训练时长: 15.03 秒
最终测试准确率(无BN, mean±std): 0.7782 ± 0.0231
最终测试准确率(有BN, mean±std): 0.8648 ± 0.0104

BatchNorm 的作用不是简单地把数据变成均值0、方差1,而是通过稳定每一层的输入分布,使梯度传播更加稳定,从而允许更大的学习率、加快收敛速度,并降低对初始化的敏感性。

BatchNorm 确实改变了数据的数值,但没有改变信息本身;它只是把数据映射到一个更适合神经网络学习的空间,并且模型可以通过 γ 和 β 自动调整甚至恢复原始分布。

残差网络 ResNet

ResNet 的本质不是“更深”,而是“让深度网络变得可训练”。

残差网络(ResNet思想)是现代CNN的“基础骨架”,但现在实际用得最多的是“在它基础上改进的模型”,而不是原始ResNet本身。

层级 状态
ResNet 原版 仍在用(baseline)
ResNet 改进版 ⭐主流
轻量化模型 ⭐嵌入式主流
Transformer/CNN混合 ⭐未来趋势

普通网络是在每一层“重写表示”,ResNet 是在“保留表示的基础上做增量修正”。

稠密连接网络(DenseNet)

DenseNet 的核心思想是:让每一层都直接接收“前面所有层的输出”。

MobileNet

MobileNet 是一种“专门为移动端和嵌入式设备设计的轻量级卷积神经网络”,核心目标是:在尽量少计算量下保持较好精度。

Yolo

YOLO 本质是一个“目标检测框架”,里面融合了多种 CNN 思想(残差、轻量化、特征融合等)。

YOLO 不是某一种 CNN,而是一个融合了多种 CNN 结构(尤其是残差网络)的目标检测系统。