ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【深度学习】CNN(卷积神经网络)从零开始教程

【深度学习】CNN(卷积神经网络)从零开始教程

写在前面

这篇文档不假设你有任何 CNN 基础。我们会从"人眼怎么看图片"出发,一步一步引出 CNN 的每个核心概念,让你读完后能说清楚:卷积在干什么、为什么要池化、BatchNorm 有什么用、Dropout 怎么防过拟合、整个网络是怎么把一张图变成一个分类结果的。


第一章 从人眼说起——为什么需要 CNN

1.1 人是怎么认出一只猫的

你看到一张猫的照片,大脑不会一个像素一个像素地扫描。你的视觉系统是这样工作的:

  1. 先看局部:这里有个尖尖的轮廓(耳朵)、这里有一团柔和的纹理(毛发)、这里有两个圆形高光(眼睛)
  2. 再组合:尖耳朵 + 圆眼睛 + 胡须 = 猫脸
  3. 最后判断:这是一只猫

这个过程的关键词是:局部特征 → 组合 → 判断。

1.2 MLP 为什么不擅长看图

MLP(全连接网络)处理图片的方式是:先把整张图拍成一条长长的数字串,然后一股脑全部连接。

问题在于:

  • 它不知道哪些像素是邻居。左上角的像素和右下角的像素,在拍平后的向量里可能离得很远,也可能离得很近,完全取决于拍平顺序
  • 它没有"局部"的概念。不会先看耳朵再看眼睛,而是试图从 3072 个数字里直接找规律
  • 参数爆炸。一张 32×32×3 的图拍平就有 3072 个输入,如果下一层有 1024 个神经元,光这一层就有 3072×1024 ≈ 300 万个参数

1.3 CNN 的核心思想

CNN 的设计哲学直接模仿了人眼的工作方式:

  1. 用小窗口扫描(卷积)→ 提取局部特征
  2. 逐层抽象(多层卷积 + 池化)→ 从边缘 → 纹理 → 部件 → 整体
  3. 最后做判断(全连接层)→ 输出分类结果

下面我们逐一拆解。

1.4 一句话总结

CNN 本质上就是一条流水线:卷积看局部 → 池化缩尺寸 → 逐层叠加从低级到高级 → 全连接做决策。整个过程是监督学习——每张图都有标签,模型对着标签学。它需要的全部组件可以用一张表概括:

组件做什么在流水线里的位置
卷积(Conv)用小窗口提取局部特征前面,反复出现
ReLU把负数归零,引入非线性每次卷积之后
BatchNorm校准数据分布,稳定训练卷积和 ReLU 之间
池化(MaxPool)缩小尺寸,保留关键信息每个阶段末尾
Dropout随机关闭神经元,防过拟合池化之后或全连接层之间
全连接层(Linear)综合所有特征,输出分类得分最后
交叉熵损失衡量预测和标签的差距训练时计算
优化器(SGD/Adam)根据梯度更新参数每步训练

后面的章节会逐个讲清楚每个组件的原理。


第二章 卷积(Convolution)——CNN 的灵魂

2.1 什么是卷积核

想象你有一个3×3 的小方格,里面填了 9 个数字。这就是一个卷积核(也叫滤波器/filter)。

卷积核示例(边缘检测): ┌────┬────┬────┐ │ -1 │ -1 │ -1 │ ├────┼────┼────┤ │ -1 │ 8 │ -1 │ ├────┼────┼────┤ │ -1 │ -1 │ -1 │ └────┴────┴────┘

2.2 卷积运算的过程

把这个 3×3 的小窗口放在图片的左上角,窗口覆盖了图片的 3×3 区域。然后:

  1. 把窗口里的 9 个像素值和卷积核里的 9 个数字对应位置相乘
  2. 把 9 个乘积加起来,得到一个数
  3. 把这个数写到输出图的对应位置

然后窗口往右移一格,重复上述过程。移到头了就换下一行。这样扫完整张图,就得到了一张新的"特征图"。

输入图片(5×5) 卷积核(3×3) 输出特征图(3×3) ┌───┬───┬───┬───┬───┐ ┌───┬───┬───┐ │ 1 │ 2 │ 3 │ 0 │ 1 │ ┌───┬───┬───┐ │ │ │ │ ├───┼───┼───┼───┼───┤ │ 1 │ 0 │-1 │ ├───┼───┼───┤ │ 0 │ 1 │ 2 │ 3 │ 0 │ × │ 1 │ 0 │-1 │ → │ │ │ │ ├───┼───┼───┼───┼───┤ │ 1 │ 0 │-1 │ ├───┼───┼───┤ │ 1 │ 0 │ 1 │ 2 │ 1 │ └───┴───┴───┘ │ │ │ │ ├───┼───┼───┼───┼───┤ └───┴───┴───┘ │ 2 │ 1 │ 0 │ 1 │ 0 │ ├───┼───┼───┼───┼───┤ │ 0 │ 1 │ 2 │ 1 │ 1 │ └───┴───┴───┴───┴───┘

2.3 卷积能检测什么

不同的卷积核能检测不同的模式:

卷积核类型能检测到的内容直觉理解
横向边缘核水平方向的边界像用手指横着划过图片,能感受到上下颜色变化
纵向边缘核垂直方向的边界像用手指竖着划
模糊核平滑区域像隔着磨砂玻璃看

关键点:在 CNN 里,卷积核的数值不是人工设计的,而是训练出来的。网络会自动学习"什么样的卷积核对分类最有帮助"。

2.4 通道(Channel)

彩色图片有 3 个通道(R、G、B)。卷积核也必须是 3 个通道的——实际上是一个3×3×3的小方块。

但我们通常不只用一个卷积核,而是同时用很多个。比如Conv2d(3, 64, kernel_size=3)的意思是:

  • 输入有 3 个通道
  • 同时使用64 个不同的卷积核
  • 每个卷积核扫一遍图片,产生一张特征图
  • 64 个卷积核就产生64 张特征图,堆在一起形成 64 个通道的输出

你可以理解为:64 个"观察员"从 64 种不同的角度去看同一张图片,有的专门看边缘,有的专门看颜色变化,有的专门看纹理。

2.5 padding(填充)

卷积核在图片上滑动时,边缘位置是"覆盖不全"的。什么意思?看这张图:

5×5 图片中,每个像素被 3×3 卷积核覆盖的次数: ┌───┬───┬───┬───┬───┐ │ 1 │ 2 │ 3 │ 2 │ 1 │ ← 角落只被扫到 1 次 ├───┼───┼───┼───┼───┤ │ 2 │ 4 │ 6 │ 4 │ 2 │ ├───┼───┼───┼───┼───┤ │ 3 │ 6 │ 9 │ 6 │ 3 │ ← 中心被扫到 9 次 ├───┼───┼───┼───┼───┤ │ 2 │ 4 │ 6 │ 4 │ 2 │ ├───┼───┼───┼───┼───┤ │ 1 │ 2 │ 3 │ 2 │ 1 │ └───┴───┴───┴───┴───┘

原因很简单:卷积核必须完整地落在图片里面才能计算。角落像素只有一种摆法——核贴着角放;而中心像素周围空间充足,核可以在 9 个位置覆盖到它。边缘像素参与计算的次数少,信息提取不充分,而且输出尺寸也会缩小(5×5 输入经 3×3 核卷积后只剩 3×3)。

通用公式:输出尺寸 = 输入尺寸 − 卷积核尺寸 + 1 = 5 − 3 + 1 =3

padding 怎么解决这个问题

padding=1的意思是:在图片四周补一圈 0(零填充),让输入从 5×5 变成 7×7,这样卷积后输出尺寸就和原来一样大了。

原始输入(5×5): 补零后(7×7): ■ ■ ■ ■ ■ 0 0 0 0 0 0 0 ■ ■ ■ ■ ■ 0 ■ ■ ■ ■ ■ 0 ■ ■ ■ ■ ■ → 0 ■ ■ ■ ■ ■ 0 ■ ■ ■ ■ ■ 0 ■ ■ ■ ■ ■ 0 ■ ■ ■ ■ ■ 0 ■ ■ ■ ■ ■ 0 0 ■ ■ ■ ■ ■ 0 0 0 0 0 0 0 0 补零后用 3×3 核卷积:输出 = 7 − 3 + 1 = 5×5 ✓ 尺寸不变!

加了 padding 后的通用公式:

输出尺寸=输入尺寸+2×padding−卷积核尺寸+1\text{输出尺寸} = \text{输入尺寸} + 2 \times \text{padding} - \text{卷积核尺寸} + 1输出尺寸=输入尺寸+2×padding−卷积核尺寸+1

对于 3×3 的核,padding=1 刚好让输出 = 输入。对于 5×5 的核,需要 padding=2。规律就是padding = (核尺寸 − 1) / 2。

补的 0 不会"污染"特征吗?实际上影响很小——边缘的 0 只参与最外圈的卷积计算,而且网络会自动学会忽略这些零值。

这样就不会因为卷积操作而不断缩小图片了。

2.6 stride(步长)

默认情况下卷积核每次移动一格(stride=1)。如果设 stride=2,就每次跳两格,输出尺寸会减半。本项目里卷积层都用 stride=1,尺寸缩减交给池化层来做。


第三章 激活函数 ReLU——给网络注入"非线性"

3.1 为什么需要激活函数

假设没有激活函数,网络里全是线性运算(矩阵乘法 + 加法)。不管你叠多少层:

输出=W3⋅(W2⋅(W1⋅x))=(W3⋅W2⋅W1)⋅x=W′⋅x\text{输出} = W_3 \cdot (W_2 \cdot (W_1 \cdot x)) = (W_3 \cdot W_2 \cdot W_1) \cdot x = W' \cdot x输出=W3​⋅(W2​⋅(W1​⋅x))=(W3​⋅W2​⋅W1​)⋅x=W′⋅x

多层线性变换等于一层线性变换。这意味着网络再深也只能画直线,无法学会复杂的弯弯曲曲的分类边界。

3.2 ReLU 是什么

ReLU(Rectified Linear Unit)是最简单也是最常用的激活函数:

ReLU(x)=max⁡(0,x)\text{ReLU}(x) = \max(0, x)ReLU(x)=max(0,x)

  • 输入是正数 → 原样输出
  • 输入是负数 → 变成 0
输入: [-2, 3, -0.5, 7, -1, 0] ReLU: [ 0, 3, 0, 7, 0, 0]

3.3 为什么要把负数归 0

这是理解 ReLU 最关键的一个问题。答案有三层:

第一,负数代表"没匹配到的信号"。卷积核在扫描图片时,如果某个区域不匹配它要检测的模式(比如一个边缘检测核扫到了一片平坦区域),输出就是负数。负数的含义是"这里没有我要找的东西"。ReLU 把它归 0,就是在说:没找到就是没找到,不保留这个无关信号。

第二,制造稀疏性。归 0 之后,网络中大量神经元的输出变成了 0——只有少部分被"激活"。这类似于人脑的工作方式:看到一张猫的图片,只有负责识别"尖耳朵"“胡须”"毛发纹理"的神经元会兴奋,其余保持沉默。这种稀疏激活让网络更高效、更不容易过拟合。

第三,这是引入非线性的最简单方式。正数区间是直线,零点处有一个"拐角",这就打破了线性。比 sigmoid 之类的函数计算快得多,梯度也不会消失。

3.4 为什么 ReLU 这么受欢迎

优点解释
计算快只是比较大小,比 sigmoid、tanh 等函数快得多
梯度不会消失正数区域梯度恒为 1,不会越乘越小(深层网络的大问题)
稀疏激活负数变 0 后,网络中只有部分神经元被激活,类似于大脑的工作方式

一句话总结:ReLU 就是给网络装了一个"开关"——每个神经元要么全力输出(正数),要么彻底关闭(负数变 0),让网络有了区分复杂模式的能力。


第四章 池化(Pooling)——聪明地缩小图片

4.1 为什么要池化

经过卷积后,特征图可能还是很大(比如 32×32,64 个通道)。如果直接堆更多卷积层,计算量会非常大。

池化的作用:

  1. 缩小尺寸:减少计算量和参数量
  2. 提取显著特征:只保留最重要的信息
  3. 增加平移不变性:即使目标在图片里稍微移动了位置,池化后的结果也差不多

4.2 最大池化(MaxPool)

本项目用的是MaxPool2d(kernel_size=2),意思是:

  1. 把特征图划分成 2×2 的小格子
  2. 每个格子里取最大值
  3. 输出尺寸变为原来的一半
输入(4×4): 输出(2×2): ┌────┬────┬────┬────┐ ┌────┬────┐ │ 1 │ 3 │ 2 │ 4 │ │ 3 │ 4 │ (取左上 2×2 的最大值 3,右上的最大值 4) ├────┼────┼────┼────┤ ├────┼────┤ │ 5 │ 2 │ 1 │ 0 │ → │ 5 │ 3 │ (取左下 2×2 的最大值 5,右下的最大值 3) ├────┼────┼────┼────┤ └────┴────┘ │ 0 │ 1 │ 3 │ 2 │ ├────┼────┼────┼────┤ │ 4 │ 2 │ 1 │ 3 │ └────┴────┴────┴────┘

4.3 为什么取最大值而不是平均值

最大池化保留的是最强烈的响应。比如某个卷积核是边缘检测器,那么 2×2 区域里最大的那个值就代表"这个区域里最明显的边缘强度"。微弱的信号(小值)可能只是噪声,丢掉也无妨。

当然也有平均池化(AvgPool),取的是平均值,保留的是区域的"整体感觉"。两种各有适用场景,但在分类任务里最大池化更常用。

4.4 池化后尺寸怎么变

本项目中图片尺寸的变化过程:

输入: 32×32 ↓ 第一次 MaxPool(2) 16×16 ↓ 第二次 MaxPool(2) 8×8 ↓ 第三次 MaxPool(2) 4×4

三次池化,尺寸缩了 8 倍(32 → 4),但通道数从 3 涨到了 256。空间信息换成了语义信息——图变小了,但每个位置包含的"含义"更丰富了。

"3 个通道"和"256 个通道"是完全不同的概念

最初的 3 个通道就是 RGB 三个颜色层——红、绿、蓝,这是人类定义的,每个通道只记录一种颜色的亮度。

后面涨到 64、128、256 个通道,含义完全不同——每个通道是一个卷积核扫描出来的"特征图"。比如第一层Conv2d(3, 64)有 64 个卷积核,训练后每个核学会检测一种模式:

第 1 个核 → 检测水平边缘 → 产出第 1 张特征图 第 2 个核 → 检测垂直边缘 → 产出第 2 张特征图 第 3 个核 → 检测红色色块 → 产出第 3 张特征图 ... 第 64 个核 → 检测对角线纹理 → 产出第 64 张特征图 64 张特征图堆在一起 = 64 个通道

到了第二层Conv2d(64, 128),128 个核在这 64 张特征图上做组合——比如"水平边缘 + 垂直边缘 = 角落",又产出 128 张更高级的特征图。

为什么越深通道越多?浅层检测的是边缘、颜色这类简单模式,几十种就够了。但深层要表达"猫耳朵"“车轮”"花瓣"这些复杂概念,种类远多于简单边缘,所以需要更多通道来编码。

一句话:3 个通道 = 3 种颜色(给人看的),256 个通道 = 256 种学到的特征(给网络用的)。


第五章 批归一化(Batch Normalization)——网络的"校准器"

5.1 问题背景:内部协变量偏移

训练神经网络时,每一层的输入分布会随着前面层参数的更新而不断变化。今天第 3 层收到的数据分布是这样的,明天参数更新后又变了。这种现象叫内部协变量偏移(Internal Covariate Shift)。

后果:后面的层需要不断适应前面层输出分布的变化,训练变得又慢又不稳定。

5.2 BatchNorm 做了什么

对于一个 batch 的数据,BatchNorm 在每个通道上做以下操作:

  1. 计算均值和方差:统计当前 batch 在该通道上所有像素值的均值 μ 和方差 σ²
  2. 标准化:把每个值减去均值、除以标准差,拉到均值 0、方差 1

x^=x−μσ2+ϵ\hat{x} = \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}}x^=σ2+ϵ​x−μ​

  1. 缩放和平移:再乘以一个可学习的参数 γ,加上一个可学习的参数 β

y=γ⋅x^+βy = \gamma \cdot \hat{x} + \betay=γ⋅x^+β

第 3 步很重要——如果网络觉得"这个通道不需要均值 0、方差 1",它可以通过学习 γ 和 β 把分布调回去。也就是说,BatchNorm 给了网络选择权:可以标准化,也可以不标准化。

5.3 BatchNorm 的好处

好处解释
训练更快每层输入分布稳定了,梯度传播更顺畅,可以用更大的学习率
不那么依赖初始化即使初始权重不太好,BatchNorm 也能把数据"矫正"过来
轻微的正则化效果因为每个 batch 的统计量有随机性,相当于给训练加了一点噪声

5.4 在本项目里怎么用的

Conv2d(3, 64) → BatchNorm2d(64) → ReLU

固定套路:卷积 → BatchNorm → 激活。BatchNorm 放在激活函数之前,先把卷积输出的分布"校准"好,再送给 ReLU 决定开关。


第六章 Dropout——随机"翘课"防过拟合

6.1 什么是过拟合

训练集上表现很好,但新数据上表现很差。就像一个学生背完了所有历年真题的答案,但遇到新题就不会做了。

6.2 Dropout 的原理

训练时,每一步随机选择一部分神经元暂时关掉(输出置 0),剩下的神经元正常工作。

正常网络: Dropout(0.3) 后: ○ ─ ○ ─ ○ ○ ─ ✕ ─ ○ (✕ 表示被关掉的神经元) ○ ─ ○ ─ ○ ○ ─ ○ ─ ✕ ○ ─ ○ ─ ○ ✕ ─ ○ ─ ○ ○ ─ ○ ─ ○ ○ ─ ○ ─ ○

每一步被关掉的神经元都是随机的,所以每一步训练的其实是一个"残缺版"的网络。

6.3 为什么 Dropout 能防过拟合

直觉解释有两个角度:

角度一:防止共适应

如果某几个神经元总是"抱团"一起工作,它们可能合作记住了训练数据的某些特殊模式(噪声),而不是学到真正有用的规律。Dropout 随机拆散它们,迫使每个神经元独立地学有用的特征。

角度二:集成学习

每次 Dropout 关掉不同的神经元,相当于训练了一个不同的小网络。最终的网络可以看作是指数级多个小网络的集成,而集成学习天然能降低过拟合。

6.4 训练 vs 推理

  • 训练时:随机关掉神经元(比如 30%)
  • 推理时(测试/部署):所有神经元都打开,但每个神经元的输出乘以 (1 - dropout_rate),保证输出的期望值和训练时一致

PyTorch 的nn.Dropout已经自动处理了这个切换,你只需要在评估时调用model.eval()就行。

6.5 Dropout vs Dropout2d

类型关掉什么适用场景
Dropout随机关掉单个神经元全连接层
Dropout2d随机关掉整个通道卷积层

为什么卷积层要关掉整个通道?因为同一个通道内的像素是高度相关的(它们是同一个卷积核产生的),只关掉单个像素几乎没效果,需要把整个通道一起关掉才有意义。


第七章 全连接层(Fully Connected Layer)——最终的"投票器"

7.1 全连接层的角色

经过多层卷积和池化后,图片已经被压缩成了一个高维特征向量(比如 256×4×4 = 4096 维)。这个向量包含了图片的"语义摘要"。

全连接层的任务就是:根据这个摘要,给 100 个类别打分。

7.2 怎么理解"全连接"

特征向量(4096 维) → 隐藏层(512 维) → 输出层(100 维) 每个输入连接 每个输入连接 第 i 个数 = 属于第 i 类的得分 每个输出 每个输出

"全连接"就是:每一个输入节点都和每一个输出节点有连接。4096 → 512 这一层就有 4096×512 ≈ 200 万个参数。

7.3 为什么不直接从特征图接到 100 类

理论上可以,但中间加一个隐藏层(4096 → 512 → 100)给了网络一个"缓冲":先把 4096 维的特征压缩到 512 维,去掉冗余信息,再做最终分类。实践中这样效果更好。


第八章 把积木拼起来——完整的 CNN 架构

8.1 本项目 CNN 的完整数据流

让我们跟踪一张 32×32×3 的图片,看它怎么一步一步变成 100 个分数:

输入: [batch, 3, 32, 32] ═══════ 第一阶段:检测低级特征(边缘、颜色块)═══════ Conv2d(3→64, 3×3, pad=1) → [batch, 64, 32, 32] 64 个卷积核同时扫描 BatchNorm2d(64) → [batch, 64, 32, 32] 校准分布 ReLU → [batch, 64, 32, 32] 激活 Conv2d(64→64, 3×3, pad=1) → [batch, 64, 32, 32] 进一步提取 ReLU → [batch, 64, 32, 32] 激活 MaxPool2d(2) → [batch, 64, 16, 16] ★ 尺寸减半 Dropout2d(0.3) → [batch, 64, 16, 16] 随机关通道 ═══════ 第二阶段:检测中级特征(纹理、局部形状)═══════ Conv2d(64→128, 3×3, pad=1) → [batch, 128, 16, 16] 通道数翻倍 BatchNorm2d(128) → [batch, 128, 16, 16] 校准 ReLU → [batch, 128, 16, 16] 激活 Conv2d(128→128, 3×3, pad=1) → [batch, 128, 16, 16] 进一步提取 ReLU → [batch, 128, 16, 16] 激活 MaxPool2d(2) → [batch, 128, 8, 8] ★ 尺寸再减半 Dropout2d(0.3) → [batch, 128, 8, 8] 随机关通道 ═══════ 第三阶段:检测高级特征(部件、语义)═══════ Conv2d(128→256, 3×3, pad=1) → [batch, 256, 8, 8] 通道数再翻倍 BatchNorm2d(256) → [batch, 256, 8, 8] 校准 ReLU → [batch, 256, 8, 8] 激活 MaxPool2d(2) → [batch, 256, 4, 4] ★ 尺寸再减半 ═══════ 分类阶段 ═══════ Flatten → [batch, 4096] 拍平 Linear(4096→512) → [batch, 512] 压缩 ReLU → [batch, 512] 激活 Dropout(0.3) → [batch, 512] 防过拟合 Linear(512→100) → [batch, 100] ★ 输出 100 个类别的得分 最终: 取 100 个得分中最大的那个对应的类别,作为预测结果

8.2 特征是怎么逐层抽象的

第一阶段(浅层) 第二阶段(中层) 第三阶段(深层) ┌─────────┐ ┌─────────┐ ┌─────────┐ │ 边缘 │ │ 纹理 │ │ 部件 │ │ 颜色变化 │ → │ 角落 │ → │ 物体轮廓 │ │ 亮度梯度 │ │ 简单形状 │ │ 语义特征 │ └─────────┘ └─────────┘ └─────────┘ 看"点" 看"线和面" 看"东西"

这就像一个画家素描的过程:先画轮廓线(边缘),再画纹理和阴影(中级特征),最后你就能看出画的是什么了(高级语义)。

8.3 参数量对比

层参数量说明
Conv2d(3→64, 3×3)3×64×3×3 + 64 =1,792很少!权重共享的功劳
Conv2d(64→64, 3×3)64×64×3×3 + 64 =36,928还是不多
Linear(4096→512)4096×512 + 512 =2,097,664一下子 200 万!

卷积层参数少是因为权重共享——一个 3×3 的卷积核在整张图上滑动时,用的是同一组参数。而全连接层每个连接都是独立的参数。


第九章 训练的全过程——从白纸到学会分类

9.0 CNN 是监督学习吗

本项目里的 CNN 是典型的监督学习——每张训练图片都带有标签(“猫”“汽车”“苹果”……),模型预测一个类别,和标签比较算损失,再调参数。有标准答案、对着答案学,就是监督学习。

不过 CNN 本身只是一种网络架构,不绑定学习方式。它也能用在别的范式里:

学习范式有没有标签CNN 怎么用例子
监督学习全有直接训练分类/检测本项目(CIFAR-100)
自监督学习全无自己给自己造任务SimCLR
半监督学习少量有 + 大量无结合有标签和无标签数据FixMatch

一句话:CNN 是工具,监督学习是使用方式。

9.1 前向传播

数据从输入层一路算到输出层,得到 100 个得分(logits)。

9.2 计算损失

用交叉熵损失函数衡量"预测和真实答案差多远"。如果给正确类别打了最高分,损失就小;否则损失就大。

9.3 反向传播

从损失出发,从后往前计算每个参数对损失的"贡献度"(梯度)。这个过程用的是微积分的链式法则。

你可以理解为:最终成绩不好(损失大),需要追责——从最后一层开始,问"是你的参数太大了还是太小了?该往哪个方向调?",一层一层往前问,直到第一层。

9.4 参数更新

优化器根据梯度更新参数:

θ新=θ旧−学习率×梯度\theta_{\text{新}} = \theta_{\text{旧}} - \text{学习率} \times \text{梯度}θ新​=θ旧​−学习率×梯度

9.5 重复

以上四步反复执行。每遍历完整个训练集一次,称为一个epoch。一般训练几十到几百个 epoch 后,网络就能学会分类了。

┌──────────────────────────────────────────┐ │ │ │ 输入图片 → 前向传播 → 得到预测 │ │ ↓ │ │ 计算损失 │ │ ↓ │ │ 反向传播 │ │ ↓ │ │ 更新参数 │ │ ↓ │ │ 下一个 batch ───────→│ │ │ └──────────────────────────────────────────┘

第十章 常见疑问解答

Q1:卷积核的初始值是怎么确定的?

随机初始化。PyTorch 默认用 Kaiming 初始化(也叫 He 初始化),专门为 ReLU 激活函数设计,能让每层输出的方差保持在合理范围内,避免一开始就梯度爆炸或消失。

Q2:为什么通道数越来越多(3 → 64 → 128 → 256)?

因为图片的空间尺寸在不断缩小(32 → 16 → 8 → 4),每个位置能"看到"的区域越来越大,需要表达的信息也越来越抽象。增加通道数就是给网络更多的"维度"来编码这些复杂信息。

可以类比:一段话翻译成英文可能只需要 10 个词,但翻译成摩尔斯电码就需要几百个字符。越底层的编码方式(像素值)越简单直接,越高层的编码(语义特征)越需要更多维度来表达。

Q3:如果不做池化会怎样?

  • 计算量会非常大(特征图一直是 32×32)
  • 每个卷积核只能看到 3×3 的局部区域,即使叠很多层,"视野"增长也很慢
  • 池化后,同样的 3×3 卷积核在下一层实际上"看到"了原图更大的区域

Q4:BatchNorm 在推理时怎么工作?

训练时用的是当前 batch 的均值和方差。推理时用的是训练过程中累积的全局均值和方差(移动平均)。这样推理时不依赖 batch 大小,单张图片也能正常工作。

Q5:为什么用 ReLU 而不用 sigmoid?

sigmoid 的输出范围是 (0, 1),在两端梯度几乎为 0(饱和区),多层叠加后梯度会指数级衰减(梯度消失问题)。ReLU 在正数区域梯度恒为 1,不存在这个问题。而且 ReLU 计算就是一个 max 操作,比 sigmoid 的指数运算快得多。

Q6:CNN 的"感受野"是什么意思?

感受野是指输出特征图上的一个像素,对应输入图片上多大的区域。

  • 第一个卷积层(3×3):感受野 = 3×3
  • 经过一次池化后再卷积:感受野变大了,因为池化缩小了特征图,后续的 3×3 核实际上覆盖了原图更大的区域
  • 层数越深,感受野越大,网络能"看到"的上下文越多

这也是为什么深层网络能识别"整体"而不仅仅是"局部"。


第十一章 从本项目的 CNN 到更强大的网络

本项目的 CNN 是一个较为基础的架构。如果你想了解它和业界主流网络的关系:

网络年份核心创新和本项目的关系
LeNet-51998最早的 CNN,用于手写数字识别本项目的 CNN 可以看作 LeNet 的加强版
AlexNet2012更深更宽,首次用 ReLU 和 Dropout本项目用了同样的 ReLU + Dropout 策略
VGGNet2014全部用 3×3 小卷积核,靠深度取胜本项目的"多层 3×3 卷积"思路和 VGG 一致
ResNet2015残差连接,解决了超深网络的退化问题本项目没有用残差连接,是下一步可以尝试的方向
EfficientNet2019同时优化深度、宽度和分辨率更高级的架构设计

本项目的 CNN 麻雀虽小五脏俱全,包含了理解现代 CNN 所需要的所有基础组件。掌握了这些之后,再去看 ResNet、DenseNet 等更复杂的网络,就只是在这些基础上做加法了。


总结:CNN 核心概念一页纸

┌─────────────────────────────────────────────────────────────┐ │ CNN 核心概念速查 │ ├─────────────┬───────────────────────────────────────────────┤ │ 卷积 │ 用小窗口扫描图片,提取局部特征 │ │ 通道 │ 多个卷积核 = 多个观察角度 │ │ padding │ 在图片周围补零,保持尺寸不变 │ │ stride │ 卷积核每次移动的步长 │ │ ReLU │ 负数变 0,引入非线性 │ │ MaxPool │ 取局部最大值,缩小尺寸,保留显著特征 │ │ BatchNorm │ 标准化每层输出,加速训练,稳定梯度 │ │ Dropout │ 随机关掉神经元/通道,防止过拟合 │ │ Dropout2d │ 关掉整个通道(专门给卷积层用的 Dropout) │ │ Flatten │ 把多维特征图拍成一维向量 │ │ 全连接层 │ 根据特征向量给每个类别打分 │ │ 交叉熵损失 │ 衡量预测和真实标签之间的差距 │ │ 反向传播 │ 从损失出发,计算每个参数该怎么调 │ │ 感受野 │ 输出的一个点对应输入的多大区域 │ │ 权重共享 │ 同一个卷积核在整张图上复用,大幅减少参数 │ └─────────────┴───────────────────────────────────────────────┘
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进