从反向传播到ResNet:计算机视觉核心主线全解析
这次我们认真梳理一次计算机视觉的基础主线:反向传播、神经网络、CNN,再到 ResNet。很多朋友一开始直接去跑目标检测、图像分割的代码,效果走通了但心里不踏实,遇到 loss 不收敛、网络加深反而退化这类问题,不知道从哪里排查。把这条主线搞明白,那些现象就有了坐标系。
这篇文章不是概念罗列,而是按“你要解决什么问题 -> 数学怎么解决 -> 网络结构怎么设计 -> 代码怎么落地”的顺序展开。你会看到反向传播的完整计算链路、CNN 的局部感受野和参数共享为什么能大量节省参数、ResNet 的残差连接为什么能让上百层的网络稳定收敛,然后通过 PyTorch 亲手训练一个 CNN,并把残差块加进去对比效果。
适合两类读者:一类是刚接触深度学习、准备走计算机视觉方向的学生或工程师;另一类是已经会调库、但对底层原理和模型演进逻辑不够系统的开发者。阅读全文大约需要 20 分钟,建议把代码部分动手跑一遍。
1. 核心知识点速览
| 知识点 | 解决的核心问题 | 在计算机视觉中的位置 |
|---|---|---|
| 前向传播 | 输入数据如何被网络加工成预测结果 | 所有深度学习模型的推理基础 |
| 反向传播 | 模型参数如何根据损失值更新 | 所有有监督训练的核心机制 |
| 梯度下降 | 如何沿着损失下降方向调整参数 | 优化器的底层思想,SGD、Adam 都由此而来 |
| 卷积神经网络 CNN | 图像特征如何被自动提取 | 目标检测、分割、分类的骨干网络 |
| 局部感受野与权值共享 | 如何降低图像模型的参数规模 | CNN 相对全连接网络的关键改进 |
| 池化与采样 | 如何降低特征图尺寸并增强平移不变性 | CNN 中的标准结构,现代网络仍大量使用 |
| 残差连接 ResNet | 网络加深后如何避免退化与梯度消失 | 几乎所有现代 CNN 和 Transformer 的基础组件 |
| 批量归一化 BN | 如何稳定深层网络训练 | 配合残差块使用,能显著加速收敛 |
2. 先建立整体坐标系:深度学习为什么能统治计算机视觉
在深度学习成为主流之前,计算机视觉走的是“人工设计特征 + 分类器”的路线。比如传统方法会先用 SIFT、HOG 等方法提取图像特征,再交给 SVM 或 AdaBoost 做分类。这套流程有几个明显的痛点:特征设计高度依赖人工经验,换一个任务往往需要重新设计特征;其次是特征与分类器分开优化,整体效果很难达到最优。
深度学习改变了这个格局。它的核心思路是用一个多层神经网络,从原始的像素数据中自动学习到从底层边缘纹理到高层语义的层次化特征。前几层学会边缘和颜色,中间层学会纹理和局部形状,高层学会物体部件甚至完整语义。这种端到端学习的方式,让“特征提取”和“分类决策”在一个模型里联合优化。
计算机视觉的经典任务,包括图像分类、目标检测、语义分割、人体关键点检测、图像生成等,今天的 SOTA 模型几乎全都建立在卷积神经网络或带卷积结构的 Transformer 之上。所以不理解 CNN,就不可能真正理解这些应用。而 CNN 的训练和演进又离不开反向传播与残差连接这两个核心工程。
先看一个全局关系:神经网络用前向传播做预测,用反向传播做参数更新;CNN 是神经网络的一种结构设计,专门为图像输入做优化;ResNet 是在 CNN 基础上解决深度扩展问题的关键方案。接下来按这条线逐层拆开。
3. 前向传播:神经网络如何计算预测结果
一个神经网络本质上是一个嵌套的复合函数。以最简单的全连接网络为例,输入是一张图片展平后的向量,比如一张 28x28 的灰度图,展平后就是 784 维的向量。网络的每一层都是对输入做一次线性变换加一次非线性激活,数学形式是:
z = Wx + b
a = f(z)
其中 W 是权重矩阵,b 是偏置,f 是非线性激活函数。
激活函数是网络能够拟合复杂函数的关键。如果没有激活函数,多层线性变换叠加起来仍然是一次线性变换,网络再深也等价于一个线性模型,表达能力非常有限。引入非线性后,网络才能逼近任意复杂函数。
常用的激活函数有几种。Sigmoid 将输出压缩到 0 到 1 之间,适合输出概率,但存在梯度饱和问题;Tanh 将输出压缩到 -1 到 1 之间,是 Sigmoid 的平移缩放版本;ReLU 在正区间保持线性,在负区间置零,计算简单且能缓解梯度消失,是当前 CNN 中使用最广泛的激活函数。
前向传播的过程可以这样理解:第一层接收原始像素,计算出隐藏层的激活值;第二层接收上一层的结果,继续计算;如此逐层传递,直到最后一层输出一个向量,这个向量经过 Softmax 归一化后就是模型对每个类别的预测概率。模型训练之前参数是随机初始化的,所以最初的预测结果基本没有意义;训练的过程就是不断调整 W 和 b,让预测结果向真实标签靠近。
如果只看一次前向传播,数据集中的每一张图片都会经过相同的计算流程,得到各自的预测概率。这个过程本身没有学习行为,它的意义在于为反向传播提供一个可求导的计算图。理解这一点很重要:前向传播和反向传播共享同一个计算图,前向计算的结果会被反向传播复用,这也就是为什么训练时需要先做一次前向传播再反向更新。
4. 反向传播:神经网络真正“学习”的核心机制
4.1 损失函数:模型好坏的量化标准
要让网络学会分类,首先得有一个指标衡量当前参数好不好。这个指标就是损失函数。对于图像分类任务,最常用的是交叉熵损失;对于回归任务,常用均方误差。损失值越大,说明模型当前表现越差,训练的目标就是把损失值最小化。
以均方误差为例,单个样本的损失可以写成:
L = (1/2) * (y - a)^2
其中 y 是真实标签,a 是网络输出的预测值。交叉熵损失在分类任务中更常用,因为它能更好地度量两个概率分布之间的差异,梯度也往往更稳定。
4.2 梯度下降:沿着山坡往下走
损失函数是一个关于网络参数的巨大复合函数。我们想知道每个参数对损失的影响程度,这个影响程度就是梯度。梯度的方向是损失上升最快的方向,所以参数更新的方向应该是梯度的反方向,这就是梯度下降:
W = W - learning_rate * ∂L/∂W
learning_rate 是学习率,控制每一步更新的大小。学习率过大,参数会在最优值附近震荡甚至发散;学习率过小,收敛速度太慢。单次用一个样本更新参数是随机梯度下降 SGD;用小批量样本计算平均梯度再更新,就是小批量梯度下降,这是目前训练深度模型的默认方式。
4.3 链式法则:反向传播的数学基石
问题在于,损失函数对深层参数的梯度怎么计算?一个网络可能动辄几百层,直接求导几乎不可能。反向传播的核心工具是微积分里的链式法则。如果某个变量通过一系列中间变量影响最终损失,那么损失对最深层参数的梯度,可以分解为沿途每一层局部导数的乘积。
举个例子,假设网络只有两层:
输入 x,第一层得到 z1 = W1x + b1,经过激活函数得到 a1;第二层得到 z2 = W2a1 + b2,再经过激活函数得到预测 a2。损失 L 是关于 a2 的函数。
反向传播从输出层开始往回计算。先计算损失对输出层线性输出 z2 的梯度,也就是输出层误差:
δ2 = ∂L/∂a2 * σ'(z2)
然后这个误差通过权重矩阵 W2 传播回隐藏层:
δ1 = W2^T * δ2 * σ'(z1)
有了这两个误差项,各层参数的梯度就很容易得到:
∂L/∂W2 = δ2 * a1^T
∂L/∂W1 = δ1 * x^T
这就是反向传播的核心结构:先由后往前传播误差信号,再用误差信号计算每一个参数的梯度。整个过程只需要一次前向传播和一次反向传播,就能得到全部参数的梯度,效率远高于对每个参数单独求导。
4.4 梯度消失:深层网络最初遇到的重大障碍
按链式法则,梯度在反向传播时是逐层相乘的。如果层的梯度数值小于 1,多层相乘后梯度会指数级衰减,这就是梯度消失;如果大于 1,则可能指数级膨胀,导致梯度爆炸。梯度消失会让靠近输入层的参数几乎得不到有效更新,网络层数超过一定深度后训练基本失效。
早期解决这个问题的方式包括使用 ReLU 代替 Sigmoid、合理的参数初始化、归一化输入数据等,但这些方法只能缓解,不能根除深层网络的退化问题。真正从根本上改变局面的,是残差连接。
5. 卷积神经网络 CNN:为图像而生的结构设计
5.1 全连接网络处理图像的困境
如果把一张 224x224 的彩色图片直接展平,输入向量的维度是 150528。第一层如果设 1024 个神经元,这一层的参数量就超过 1.5 亿。更大的问题是,全连接网络对每一个像素位置都分配了独立的权重,完全没有利用图像“相邻像素相关性强、远处像素相关性弱”的空间结构。这不仅造成参数爆炸,还让模型极易过拟合。
5.2 局部感受野与权值共享
CNN 的核心思路有两条。第一条是局部感受野:卷积核只关注输入的一个局部区域,而不是全图。比如 3x3 的卷积核,一次只看 3x3 范围内的 9 个像素,这个局部区域就是感受野。通过多层堆叠,浅层看到的是小范围局部特征,深层通过感受野叠加能看到更大的范围,最终形成全局语义。
第二条是权值共享:同一个卷积核在整张图上滑动,权重是共享的。也就是说,检测垂直边缘的卷积核,无论在图像的左上角还是右下角,用的都是同一组权重。这背后是视觉特征的位置无关性假设,同时大幅降低了参数量。一个 3x3 的卷积核,输入通道为 3,输出通道为 64,参数量只有 3x3x3x64 等于 1728,相比全连接层的上亿参数是数量级的压缩。
5.3 卷积操作的具体计算
卷积操作的输入是一个多维张量,形状为 C_in x H x W。卷积核是一个小张量,形状为 C_in x k x k。输出特征图的尺寸由输入尺寸、卷积核尺寸、填充 padding 和步长 stride 共同决定,计算公式是:
H_out = (H - k + 2p) / s + 1
设输入为 28x28 的单通道图像,用 32 个 3x3 卷积核,padding 为 1,stride 为 1,得到输出就是 32 个通道的 28x28 特征图。如果 stride 设为 2,输出尺寸变为 14x14,相当于在卷积过程中直接下采样。
5.4 池化层:缩小特征图,提升鲁棒性
池化也是一种下采样操作,最常见的是最大池化和平均池化。2x2 最大池化取每个 2x2 区域里的最大值,输出尺寸减半。池化的作用是减小后续层的计算量,同时让特征对微小的平移和形变更鲁棒。现代 CNN 网络有逐渐用 stride 卷积替代池化的趋势,但池化的思想仍然体现在降采样结构中。
5.5 经典 CNN 结构演进脉络
LeNet-5 是最早的 CNN 之一,包含卷积层、池化层和全连接层,结构是“卷积-池化-卷积-池化-全连接-全连接-全连接”,用于手写数字识别,是理解 CNN 基本结构的最佳入口。
AlexNet 在 2012 年 ImageNet 竞赛中大幅领先第二名,使用 ReLU、Dropout、数据增广和 GPU 并行训练,点燃了深度学习在计算机视觉领域的爆发。
VGG 的贡献在于用多个小的 3x3 卷积核堆叠替代大卷积核,例如用两个 3x3 卷积替代一个 5x5 卷积,在感受野相同的情况下减少了参数量并增加了非线性。VGG 的结构非常规整,全部是 3x3 卷积和 2x2 池化的堆叠,这种规整性使它成为很多后续工作的基础。
GoogLeNet 引入了 Inception 模块,在同一层并行使用不同尺寸的卷积核,然后在通道维度拼接,让网络自己选择不同尺度的特征。这里一个关键技巧是使用 1x1 卷积做通道降维,在控制计算量的同时增加网络深度。
ResNet 的贡献则直接改变了网络的深度上限。它之前,单纯堆叠层数会导致训练集上的错误率升高;它之后,上百层甚至上千层的网络成为可能。
6. ResNet:残差连接让深度网络真正“深”下去
6.1 退化问题:不是过拟合,是优化失败
在 ResNet 提出之前,研究者观察到一种反直觉的现象:当网络层数增加到几十层以上时,训练误差不降反升。这个现象被称为退化问题。注意它跟过拟合不一样,过拟合是训练误差低但测试误差高;退化是训练误差本身就下不去,说明深层网络的优化非常困难。
理论上更深的网络至少应该不弱于浅层网络,因为把浅层网络复制过来,再把新增的层训练成恒等映射就能做到。但实践中让多层非线性堆叠去逼近恒等映射非常困难,这恰恰是残差连接的切入点。
6.2 残差块的数学形式
残差块把网络要学习的映射从原本的 F(x) 改成残差 F(x) - x,然后通过跳跃连接让输出为:
y = F(x, W) + x
这里的 F 通常是两个 3x3 卷积加上批量归一化和 ReLU。跳跃连接 shortcut 直接在输入和输出之间建立一条恒等映射通道,不增加额外的参数,也只是在输出上加一个加法操作。
当输入和输出通道数或尺寸不一致时,无法直接相加,这时需要用 1x1 卷积把输入变换到与输出相同的维度,或者在带 stride 的残差块中通过 1x1 卷积同时调整尺寸。
一个标准的 BasicBlock 残差块结构可以表示为:
输入 x -> 卷积 3x3 -> BN -> ReLU -> 卷积 3x3 -> BN -> 加上 x -> ReLU -> 输出
6.3 为什么残差连接有效
这个问题在学术上有多种解释,主流的理解集中在几个方面。
第一是梯度传播路径变短。在反向传播中,残差块的导数可以表示为:
∂L/∂x = ∂L/∂y * (1 + ∂F/∂x)
这个“1”保证了即使后面一项接近零,梯度也能直接回传到前面的层,梯度消失问题得到极大缓解。
第二是缓解优化难度。残差结构让网络在最坏情况下可以退化为一个较浅的网络,因为把 F 学到接近零,就等价于恒等映射。这让深层网络至少不会比浅层网络差,解决了退化问题。
第三是类似集成学习的解释。ResNet 可以看作多个路径的集成,不同深度的子网络组合在一起共同做预测,这让模型有更强的鲁棒性。
6.4 ResNet 的典型结构
ResNet 系列包括 ResNet-18、ResNet-34、ResNet-50、ResNet-101、ResNet-152 等,数字表示网络的总层数。ResNet-50 以上的版本使用 Bottleneck 结构,也就是先用 1x1 卷积降维,再用 3x3 卷积提取特征,最后用 1x1 卷积恢复维度,这样在增加深度的同时控制计算量。
ResNet 的这些设计被广泛应用。它不仅用于图像分类,还被用作目标检测、语义分割等任务的骨干网络;它的残差思想也被迁移到 Transformer 中,几乎所有现代深度学习模型都有残差连接的身影。
7. 从理论到代码:用 PyTorch 搭建并验证核心概念
7.1 环境准备
代码部分我建议使用 PyTorch。请按下面的顺序检查本机环境:
- Python 版本建议 3.8 以上。
- PyTorch 建议 1.10 以上,2.x 版本更佳。
- 如果使用 GPU,需要先确认 CUDA 版本与 PyTorch 版本匹配。
- torchvision 用于加载图片数据集。
- 没有 GPU 也能跑通下面的全部代码,只是训练时间更长。以 MNIST 手写数字识别为例,CPU 上跑 5 个 epoch 通常也只需要几分钟。
安装依赖的命令如下:
7.2 用 NumPy 手工实现一次反向传播
在跑 PyTorch 之前,先用纯 NumPy 写一个两层网络的前向传播和反向传播,能帮助你建立对梯度流动的直觉。
这段代码不依赖任何深度学习框架,只用了 NumPy 的矩阵运算。能看明白 dL_da2 到 dz2 再到 dW2 为什么这样传递,反向传播的理解就过关了。
7.3 搭建一个简单的 CNN
下面用 PyTorch 搭建一个小型 CNN,输入是 MNIST 的 28x28 单通道图像,结构参考 LeNet-5 的思路简化而来。
输入 28x28 的图像,经过第一次 3x3 卷积加 2x2 池化后变成 14x14,经过第二次同样操作后变成 7x7,64 个通道,所以全连接层输入维数是 64x7x7。
7.4 训练循环:让前向传播和反向传播跑起来
下面加载 MNIST 数据集并训练这个 CNN。torchvision 会自动下载数据集到本地。
到这里,你已经完成了一次完整的训练闭环:模型前向传播得到预测概率,交叉熵损失计算预测和真实标签的差距,反向传播计算每个参数的梯度,优化器用梯度更新参数。下面加上残差块验证 ResNet 的核心思想。
7.5 实现残差块并对比效果
这是本篇文章最值得动手验证的部分。在刚才的 SimpleCNN 中加入残差块,看看效果是否提升,loss 是否收敛更快。先定义一个 ResidualBlock:
再把带有残差块的网络定义出来:
这个网络可以直接用 CIFAR-10 数据集训练,输入是三通道的 32x32 图像。加载方式如下:
建议做一组对比实验:一份直接堆叠卷积层、不加残差连接,另一份使用 ResNetCIFAR,控制总层数大致相同。通常你会看到加入残差连接的模型 loss 更稳定、收敛更快,尤其当层数继续加深时,两者的差距会更加明显。
7.6 设备与性能观察
如果没有 GPU,训练 CIFAR-10 的 5 个 epoch 需要的时间会明显长于 MNIST。用 CPU 训练时,建议把 batch_size 调小到 32,减少单次计算的峰值内存占用。用 GPU 训练时,可以在数据加载后加上 .to(device) 把数据和模型搬到 GPU。显存占用主要取决于 batch size、输入图像分辨率和网络通道数,具体数值需要以本机环境为准。
8. 训练过程中的常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| loss 始终不下降 | 学习率过大或过小 | 打印前几步的 loss 和梯度数值 | 调整学习率,尝试从 1e-3 或 3e-4 开始 |
| loss 变为 NaN | 梯度爆炸、学习率过大或数据未归一化 | 检查是否有除以零、日志中梯度是否异常 | 降低学习率,添加梯度裁剪 |
| 训练集 loss 下降但验证集不下降 | 过拟合 | 比较训练集和验证集 loss 曲线 | 增加数据增强,引入 Dropout,降低模型容量 |
| 网络加深后面 loss 反而升高 | 退化问题 | 确认是否缺少残差连接 | 加入残差结构,或降低初始学习率 |
| 反向传播报维度错误 | 全连接层输入维数计算错误 | 打印各层输出形状 | 按公式 H_out = (H - k + 2p) / s + 1 核对 |
| 加载数据时证书或网络报错 | 下载源被屏蔽 | 设置镜像源或手动下载数据集 | 使用国内镜像,或手动放置数据集文件 |
| GPU 显存不足 | batch size 过大或分辨率过高 | 观察训练时显存占用 | 降低 batch size,降低分辨率,使用 AMP 混合精度 |
对于一个刚入门深度学习的人来说,最常见的错误发生在全连接层输入维数的计算上。建议在模型里临时打印每一层输出的形状,确认特征图尺寸符合预期,再决定全连接层的输入维度。
9. 学习路径与最佳实践
如果你是从零开始走计算机视觉方向,建议按下面的顺序推进。
先做一次手工反向传播实验。用 NumPy 实现一个两层网络,打印每一层的梯度形状,把“链式法则 + 梯度下降”这两个概念彻底吃透。不要急着跳到 PyTorch 的自动求导,手工实现能让你知道自动求导背后到底在做什么。
再跑一个 CNN 的分类任务。MNIST 是最适合起步的数据集,数据量小、类别简单、训练快。用 LeNet-5 或本文的 SimpleCNN 跑通后,尝试调整卷积核大小、通道数、是否加 padding,直观感受这些参数对输出尺寸和效果的影响。
然后实现残差块。把 ResNet 论文中的 BasicBlock 和 Bottleneck 结构读懂并自己写出来,分别在浅层网络和较深层网络上做对比实验。重点观察训练集 loss 的变化,理解为什么“加深不加残差”会退化。
最后再阅读经典论文。建议按 LeNet-5、AlexNet、VGG、GoogLeNet、ResNet 的顺序阅读。每篇论文只看两个点:网络结构设计解决了什么问题,和上一代相比关键改动是什么。对论文中的大部分公式先不做严格推导,重点是建立结构演进的时间线。
工程实践上有一个值得养成的习惯:每次修改网络结构时,先固定随机种子,分别记录基线版本和改进版本的 loss 曲线与准确率,形成结构清晰的实验记录。深度学习实验的调试很多时候依赖这种可控对比,而不是凭感觉改参数。
10. 总结
这条主线看起来很基础,但它是理解和改进现代模型的根。反向传播定义了深度学习“如何学”,CNN 解决了“图像特征怎么提取”,ResNet 解决了“网络怎么加深得更稳”。
最先要动手验证的是手工反向传播示例,确认梯度形状完全匹配参数形状;然后跑通 SimpleCNN 训练 MNIST,巩固整个训练闭环;最后实现残差块,逐步加深网络层数,观察退化问题的消失。
最容易踩的坑有两个:一是全连接层维数计算错误,二是网络加深后没有加残差连接却以为是学习率问题。建议收藏这份路线,之后再遇到 Transformer、目标检测、语义分割等复杂模型时,回看 CNN 和 ResNet 的结构设计,会发现很多组件都很眼熟。后续可以继续扩展的方向包括:用 ResNet 做特征提取器、迁移学习、轻量化网络设计,或者把 ResNet 的残差思想延伸到 Transformer 的残差层中。