第 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 可以写成:
输入图像 -> 卷积 -> 激活 -> 池化 -> 卷积 -> 激活 -> 池化 -> 全连接 -> 输出经典结构:
| 模型 | 特点 |
|---|---|
| LeNet | 早期手写数字识别 |
| AlexNet | 推动深度学习在 ImageNet 上突破 |
| VGG | 结构规整,使用多个小卷积核 |
| ResNet | 引入残差连接,便于训练深层网络 |
ResNet 的跳跃连接让网络更容易学习恒等映射,缓解深层网络退化问题。
13.6 数据增强
图像数据增强可以提升泛化能力。
常见方法:
- 随机裁剪。
- 随机翻转。
- 随机旋转。
- 平移和缩放。
- 颜色扰动。
数据增强要保证不改变样本语义。例如数字识别里,某些旋转可能会把 6 变得像 9,这种增强就要谨慎。
13.7 目标检测
图像分类只回答“图里是什么”,目标检测还要回答“在哪里”。
目标检测输出通常包括:
- 类别。
- 边界框位置。
- 置信度。
常见模型包括 R-CNN 系列和 YOLO 系列。
IoU 用来衡量预测框和真实框的重合程度。设预测框为
IoU 越大,说明预测框越接近真实框。
13.8 图像分割和 U-Net
图像分割要对每个像素分类,比目标检测更细。
| 任务 | 输出 |
|---|---|
| 图像分类 | 整张图一个类别 |
| 目标检测 | 目标框和类别 |
| 图像分割 | 每个像素的类别 |
U-Net 常用于医学图像分割。它的对称结构和跳跃连接可以融合浅层空间信息和深层语义信息,从而得到更精细的分割结果。
13.9 迁移学习在视觉中的使用
图像任务常用在 ImageNet 上预训练的模型作为特征提取器。
常见做法:
- 加载预训练 CNN。
- 去掉或替换最后的分类层。
- 冻结大部分卷积层。
- 在自己的数据上训练新的分类头。
如果数据较多,也可以微调整个网络。数据较少时,只训练顶部分类层通常更稳。
13.10 卷积输出尺寸
卷积层输出尺寸由输入大小、卷积核大小、padding 和 stride 决定。
单个空间维度上,输出大小为:
其中:
| 符号 | 含义 |
|---|---|
| 输入尺寸 | |
| 卷积核尺寸 | |
| padding 大小 | |
| stride 大小 | |
| 输出尺寸 |
例如输入是 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。卷积核不是只在二维平面上滑动,而是同时覆盖所有输入通道。
如果输入特征图尺寸是:
H x W x C_in一个卷积核的尺寸通常是:
K_h x K_w x C_in如果有
所以卷积层参数量是:
如果包含偏置,还要再加
13.13 CNN 中的层级特征
CNN 的不同层学习到的东西不同。
| 层级 | 常见特征 |
|---|---|
| 浅层 | 边缘、角点、颜色变化 |
| 中层 | 纹理、局部形状、简单部件 |
| 深层 | 物体部件、类别语义 |
这种层级结构不是人工写死的,而是通过反向传播从数据中学出来的。
如果训练数据和任务相关性强,深层特征会更贴近目标任务。例如在人脸识别中,深层可能关注眼睛、鼻子、脸型等结构;在医学图像中,深层可能关注病灶形态。
13.14 检测任务的基本思路
目标检测比分类难,因为模型要同时解决两个问题:
- 分类:目标是什么。
- 定位:目标在哪里。
早期 R-CNN 系列通常先生成候选区域,再对候选区域分类和回归边界框。YOLO 系列把检测看成一次前向传播中的密集预测,速度更快。
检测模型通常会预测:
| 输出 | 含义 |
|---|---|
| class score | 每个类别的置信度 |
| box coordinate | 边界框位置 |
| objectness | 当前位置是否有目标 |
边界框回归不是直接“画框”,而是让模型学习预测框的位置偏移。
13.15 分割任务为什么需要跳跃连接
图像分割要求输出和输入在空间上精细对齐。深层特征语义强,但分辨率低;浅层特征分辨率高,但语义弱。
U-Net 的跳跃连接把浅层特征传到解码器,帮助恢复细节。
在实现目标检测系统时,可以把流程分成几个层次:
深层特征:知道“是什么”
浅层特征:知道“在哪里”
跳跃连接:把语义和位置细节合起来这就是 U-Net 在医学图像分割中常见的原因:很多病灶区域边界细,需要同时保留空间细节和高级语义。
13.16 PyTorch 实操:MNIST 手写数字识别
参考脚本:C:\Users\YZJ\Desktop\机器学习\mnist_cnn.py
这个例子用一个简单 CNN 训练 MNIST 手写数字分类模型。MNIST 是灰度图像数据集,每张图片大小为 28 x 28,类别是 0 到 9。
整体流程:
加载 MNIST -> 定义 CNN -> 前向传播 -> 计算交叉熵损失 -> 反向传播 -> Adam 更新参数 -> 测试准确率13.17 数据加载
脚本使用 torchvision.datasets.MNIST 加载数据:
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() 会把图像转换成形状类似下面的张量:
batch_size x channels x height x widthMNIST 是灰度图,所以通道数是 1。
13.18 网络结构
脚本中的模型结构:
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)可以理解为:
1 通道输入
-> 32 个 3x3 卷积核
-> 64 个 3x3 卷积核
-> 2x2 最大池化
-> 展平
-> 128 维全连接层
-> 10 类输出前向传播:
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 尺寸变化
输入图像尺寸是:
1 x 28 x 28第一层卷积:
Conv2d(1, 32, 3)没有 padding,卷积核大小为 3 x 3,输出空间尺寸:
28 - 3 + 1 = 26所以输出为:
32 x 26 x 26第二层卷积:
Conv2d(32, 64, 3)输出空间尺寸:
26 - 3 + 1 = 24所以输出为:
64 x 24 x 24经过 2 x 2 最大池化后:
64 x 12 x 12展平后长度:
64 x 12 x 12 = 9216这就是 linear1 = nn.Linear(9216, 128) 中 9216 的来源。
13.20 训练过程
训练代码核心部分:
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 优化器:
opt = torch.optim.Adam(model.parameters(), lr=0.001)损失函数:
loss = nn.CrossEntropyLoss()CrossEntropyLoss 适合多分类任务,标签 y 直接使用类别编号,不需要手动转成 one-hot。
13.21 测试过程
测试时要切换到评估模式:
model.eval()预测类别:
pred = model(x).argmax(dim=1)argmax(dim=1) 表示在 10 个类别分数中选择最大值对应的类别。
准确率计算:
correct += (pred == y).sum().item()
total += y.size(0)
print(correct, total, correct / total)13.22 实操注意点
脚本中设备写的是:
torch.device("mps")这适合 Apple Silicon 设备。如果在 Windows 或没有 MPS 的环境中运行,可以改成:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")然后统一写:
x, y = x.to(device), y.to(device)
model = net().to(device)另外,脚本会保存模型到:
checkpoints/a.pt运行前需要确保 checkpoints 目录存在,否则保存模型时可能报错。