Skip to content

第 13 章 卷积神经网络

卷积神经网络主要用于图像和网格结构数据。它利用局部连接和参数共享,让模型更适合处理图像。

13.1 为什么图像适合用 CNN

图像有两个重要特点:

  • 局部像素之间关系很强。
  • 同一种局部模式可能出现在图像不同位置。

如果把图像直接展平成向量,再接全连接层,参数量会非常大,而且会破坏空间结构。CNN 使用卷积核在图像上滑动,能保留局部结构。

13.2 卷积层

卷积层用一个小窗口在输入图像上滑动,对局部区域做加权求和。

卷积核可以学习不同局部模式,例如边缘、角点、纹理等。

常见参数:

参数含义
kernel size卷积核大小
stride滑动步长
padding边缘填充
channel通道数

卷积层的输出通常叫特征图。多个卷积核会产生多个输出通道。

13.3 池化层

池化用于降低特征图尺寸,减少计算量,并增强一定的平移鲁棒性。

常见池化:

池化含义
Max Pooling取局部区域最大值
Average Pooling取局部区域平均值

现代网络中,有些结构会减少显式池化,改用带步长的卷积或全局平均池化。

13.4 感受野

感受野指输出特征图上一个位置对应输入图像中的区域大小。

浅层神经元感受野小,通常学习边缘、纹理等底层特征;深层神经元感受野更大,能组合出更抽象的物体部件和语义信息。

考试里常见问法是:CNN 靠近输入的层通常学习局部、底层特征,例如边缘和纹理。

13.5 典型 CNN 结构

一个简单 CNN 可以写成:

text
输入图像 -> 卷积 -> 激活 -> 池化 -> 卷积 -> 激活 -> 池化 -> 全连接 -> 输出

经典结构:

模型特点
LeNet早期手写数字识别
AlexNet推动深度学习在 ImageNet 上突破
VGG结构规整,使用多个小卷积核
ResNet引入残差连接,便于训练深层网络

ResNet 的跳跃连接让网络更容易学习恒等映射,缓解深层网络退化问题。

13.6 数据增强

图像数据增强可以提升泛化能力。

常见方法:

  • 随机裁剪。
  • 随机翻转。
  • 随机旋转。
  • 平移和缩放。
  • 颜色扰动。

数据增强要保证不改变样本语义。例如数字识别里,某些旋转可能会把 6 变得像 9,这种增强就要谨慎。

13.7 目标检测

图像分类只回答“图里是什么”,目标检测还要回答“在哪里”。

目标检测输出通常包括:

  • 类别。
  • 边界框位置。
  • 置信度。

常见模型包括 R-CNN 系列和 YOLO 系列。

IoU 用来衡量预测框和真实框的重合程度。设预测框为 Bp,真实框为 Bg

IoU=area(BpBg)area(BpBg)

IoU 越大,说明预测框越接近真实框。

13.8 图像分割和 U-Net

图像分割要对每个像素分类,比目标检测更细。

任务输出
图像分类整张图一个类别
目标检测目标框和类别
图像分割每个像素的类别

U-Net 常用于医学图像分割。它的对称结构和跳跃连接可以融合浅层空间信息和深层语义信息,从而得到更精细的分割结果。

13.9 迁移学习在视觉中的使用

图像任务常用在 ImageNet 上预训练的模型作为特征提取器。

常见做法:

  1. 加载预训练 CNN。
  2. 去掉或替换最后的分类层。
  3. 冻结大部分卷积层。
  4. 在自己的数据上训练新的分类头。

如果数据较多,也可以微调整个网络。数据较少时,只训练顶部分类层通常更稳。

13.10 卷积输出尺寸

卷积层输出尺寸由输入大小、卷积核大小、padding 和 stride 决定。

单个空间维度上,输出大小为:

O=I+2PKS+1

其中:

符号含义
I输入尺寸
K卷积核尺寸
Ppadding 大小
Sstride 大小
O输出尺寸

例如输入是 32 x 32,卷积核是 3 x 3,padding 为 1,stride 为 1,输出仍然是 32 x 32

padding 的作用不是“增加信息”,而是控制边缘如何参与卷积,并影响输出尺寸。

13.11 参数共享为什么有效

全连接层中,每个输出单元都连接所有输入像素。图像稍大时,参数量会迅速变大。

卷积层的参数共享指同一个卷积核在不同位置重复使用。这样有两个好处:

  • 参数量大幅减少。
  • 同一个局部模式可以在不同位置被检测出来。

例如一个边缘检测卷积核,不需要在图像左上角、右下角分别学习一套参数。只要这个局部模式出现,卷积核就有机会响应。

这也是 CNN 对图像平移具有一定鲁棒性的原因之一。但 CNN 并不是完全平移不变,stride、padding、池化和数据增强都会影响这种性质。

13.12 通道和多卷积核

彩色图像通常有 3 个输入通道:R、G、B。卷积核不是只在二维平面上滑动,而是同时覆盖所有输入通道。

如果输入特征图尺寸是:

text
H x W x C_in

一个卷积核的尺寸通常是:

text
K_h x K_w x C_in

如果有 Cout 个卷积核,就会得到 Cout 个输出通道。

所以卷积层参数量是:

Kh×Kw×Cin×Cout

如果包含偏置,还要再加 Cout 个偏置参数。

13.13 CNN 中的层级特征

CNN 的不同层学习到的东西不同。

层级常见特征
浅层边缘、角点、颜色变化
中层纹理、局部形状、简单部件
深层物体部件、类别语义

这种层级结构不是人工写死的,而是通过反向传播从数据中学出来的。

如果训练数据和任务相关性强,深层特征会更贴近目标任务。例如在人脸识别中,深层可能关注眼睛、鼻子、脸型等结构;在医学图像中,深层可能关注病灶形态。

13.14 检测任务的基本思路

目标检测比分类难,因为模型要同时解决两个问题:

  • 分类:目标是什么。
  • 定位:目标在哪里。

早期 R-CNN 系列通常先生成候选区域,再对候选区域分类和回归边界框。YOLO 系列把检测看成一次前向传播中的密集预测,速度更快。

检测模型通常会预测:

输出含义
class score每个类别的置信度
box coordinate边界框位置
objectness当前位置是否有目标

边界框回归不是直接“画框”,而是让模型学习预测框的位置偏移。

13.15 分割任务为什么需要跳跃连接

图像分割要求输出和输入在空间上精细对齐。深层特征语义强,但分辨率低;浅层特征分辨率高,但语义弱。

U-Net 的跳跃连接把浅层特征传到解码器,帮助恢复细节。

在实现目标检测系统时,可以把流程分成几个层次:

text
深层特征:知道“是什么”
浅层特征:知道“在哪里”
跳跃连接:把语义和位置细节合起来

这就是 U-Net 在医学图像分割中常见的原因:很多病灶区域边界细,需要同时保留空间细节和高级语义。

13.16 PyTorch 实操:MNIST 手写数字识别

参考脚本:C:\Users\YZJ\Desktop\机器学习\mnist_cnn.py

这个例子用一个简单 CNN 训练 MNIST 手写数字分类模型。MNIST 是灰度图像数据集,每张图片大小为 28 x 28,类别是 09

整体流程:

text
加载 MNIST -> 定义 CNN -> 前向传播 -> 计算交叉熵损失 -> 反向传播 -> Adam 更新参数 -> 测试准确率

13.17 数据加载

脚本使用 torchvision.datasets.MNIST 加载数据:

python
trainset = datasets.MNIST(
    root="data",
    train=True,
    transform=transforms.ToTensor()
)
trainloader = DataLoader(trainset, batch_size=64, shuffle=True)

其中:

参数含义
root="data"数据集保存目录
train=True加载训练集
transforms.ToTensor()把图像转换成 PyTorch 张量
batch_size=64每次训练使用 64 张图片
shuffle=True每轮训练前打乱数据

ToTensor() 会把图像转换成形状类似下面的张量:

text
batch_size x channels x height x width

MNIST 是灰度图,所以通道数是 1

13.18 网络结构

脚本中的模型结构:

python
self.conv1 = nn.Conv2d(1, 32, 3)
self.conv2 = nn.Conv2d(32, 64, 3)
self.linear1 = nn.Linear(9216, 128)
self.linear2 = nn.Linear(128, 10)

可以理解为:

text
1 通道输入
-> 32 个 3x3 卷积核
-> 64 个 3x3 卷积核
-> 2x2 最大池化
-> 展平
-> 128 维全连接层
-> 10 类输出

前向传播:

python
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)
x = torch.flatten(x, 1)
x = self.linear1(x)
x = F.relu(x)
x = self.linear2(x)

最后一层输出的是 logits,不需要手动加 softmax,因为 nn.CrossEntropyLoss() 内部会处理。

13.19 尺寸变化

输入图像尺寸是:

text
1 x 28 x 28

第一层卷积:

text
Conv2d(1, 32, 3)

没有 padding,卷积核大小为 3 x 3,输出空间尺寸:

text
28 - 3 + 1 = 26

所以输出为:

text
32 x 26 x 26

第二层卷积:

text
Conv2d(32, 64, 3)

输出空间尺寸:

text
26 - 3 + 1 = 24

所以输出为:

text
64 x 24 x 24

经过 2 x 2 最大池化后:

text
64 x 12 x 12

展平后长度:

text
64 x 12 x 12 = 9216

这就是 linear1 = nn.Linear(9216, 128)9216 的来源。

13.20 训练过程

训练代码核心部分:

python
opt.zero_grad()
L = loss(model(x), y)
L.backward()
opt.step()

对应含义:

代码作用
opt.zero_grad()清空上一批次梯度
model(x)前向传播得到 logits
loss(model(x), y)计算交叉熵损失
L.backward()反向传播计算梯度
opt.step()优化器更新参数

脚本使用 Adam 优化器:

python
opt = torch.optim.Adam(model.parameters(), lr=0.001)

损失函数:

python
loss = nn.CrossEntropyLoss()

CrossEntropyLoss 适合多分类任务,标签 y 直接使用类别编号,不需要手动转成 one-hot。

13.21 测试过程

测试时要切换到评估模式:

python
model.eval()

预测类别:

python
pred = model(x).argmax(dim=1)

argmax(dim=1) 表示在 10 个类别分数中选择最大值对应的类别。

准确率计算:

python
correct += (pred == y).sum().item()
total += y.size(0)
print(correct, total, correct / total)

13.22 实操注意点

脚本中设备写的是:

python
torch.device("mps")

这适合 Apple Silicon 设备。如果在 Windows 或没有 MPS 的环境中运行,可以改成:

python
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

然后统一写:

python
x, y = x.to(device), y.to(device)
model = net().to(device)

另外,脚本会保存模型到:

python
checkpoints/a.pt

运行前需要确保 checkpoints 目录存在,否则保存模型时可能报错。

Powered by VitePress