现代卷积神经网络
| 阶段 | 时间 | 核心思想 | 代表 |
|---|---|---|---|
| 传统视觉 | 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

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

重复次数: 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 结构(尤其是残差网络)的目标检测系统。