很多同学入门计算机视觉时,都经历过这样一个阶段:从开源仓库下载了完整的图像分类项目,用现成的预训练权重跑通了 demo,准确率还挺高;可一旦把自己的业务图片丢进去,效果立刻崩掉。想调参,不知道从哪入手;想改成自己的任务,又看不懂网络定义里的每一行代码在干什么。
问题出在哪?出在很多教程把“学会调用模型”当成了“学会深度学习”。模型层的复制粘贴,替代不了底层原理的理解。计算机视觉最核心的技术主线,其实只有一条:神经网络通过前向传播做预测,通过反向传播学参数;CNN 在神经网络基础上,用卷积和池化解决图像特征提取问题;ResNet 则在 CNN 基础上,用残差学习解决了更深的网络难以训练的问题。三步走完,就是从理论到工程的关键路径。
这篇文章不打算堆砌花哨的网络结构,而是把这条主线彻底讲透。读完你会搞清楚:反向传播到底在算什么?卷积层为什么比全连接层更适合图像?ResNet 的恒等映射为什么能解决梯度消失和网络退化?文章会提供 numpy 手写反向传播、PyTorch 搭建 CNN 与 ResNet 残差块的完整代码,方便你边读边练。本文涉及的知识点,基本覆盖了计算机视觉入门阶段最关键的几个核心概念,值得收藏备用。
1. 为什么要把反向传播、CNN 和 ResNet 放在一起学
很多初学者的学习路径是割裂的:先学“反向传播的公式推导”,再学“CNN 的网络结构”,最后看一篇“ResNet 论文解读”。结果每个知识点都似懂非懂,遇到实际问题时依然无法串联起来。
从技术演进的角度看,这三个知识点不是三个并列章节,而是一条连续的升级线:
| 技术 | 要解决的核心问题 |
|---|---|
| 反向传播 | 神经网络的参数如何自动学习 |
| CNN | 如何用少量参数高效提取图像特征 |
| ResNet | 网络加深到一定程度后,如何还能被稳定训练 |
先有反向传播,神经网络才真正“可学习”;面对图像数据,全连接网络参数量太大且无视空间结构,于是 CNN 登场;CNN 越深效果越好,但深到一定程度后梯度消失、退化问题凸显,于是 ResNet 登场。每一次升级都对应一个真实且具体的工程痛点。理解了痛点,你就不会再问“为什么要加这一层”“为什么这里要用 1x1 卷积”这类问题。
这篇文章适合以下几类读者:
- 刚入门深度学习,跑过分类 demo,但对底层原理不清晰的初学者。
- 想转计算机视觉方向,正在补核心基础的非 CV 方向开发。
- 已经在用 PyTorch 做项目,但需要系统梳理“神经网络如何训练、CNN 如何设计、ResNet 为什么有效”的工程师。
如果你处在“代码能跑但心里没底”的阶段,这篇文章的性价比会非常高。
2. 神经网络的核心:从感知机到前向传播
2.1 感知机与神经元的基本计算
神经网络的最小单位是“神经元”。最早的模型叫感知机,它的计算方式非常朴素:把输入做加权求和,再加上偏置,然后通过一个激活函数输出。
单个神经元的输出可以写成:
z = w1*x1 + w2*x2 + ... + wn*xn + b a = f(z)其中w是权重,b是偏置,f是激活函数。权重决定“每个输入对结果的影响程度”,偏置决定“神经元被激活的难易程度”。训练神经网络的核心任务,就是找到一组最优的w和b,让预测结果逼近真实标签。
单个感知机只能解决线性可分问题,能力有限。把大量神经元组织成“层”,并且堆叠多层,就形成了多层感知机。每一层接收上一层的输出,经过线性变换和激活函数后,再传给下一层。
2.2 激活函数为什么不可或缺
如果网络只有线性变换,那么多层线性变换最终可以合并成一次线性变换。也就是说,没有激活函数,无论网络多深,表达能力都和单层线性模型等价。激活函数的本质是给网络引入非线性。
常用于分类任务的激活函数有:
- Sigmoid:将输入压缩到 0 到 1 之间,公式为 σ(z) = 1 / (1 + e^(-z)),适合二分类输出,但两端导数趋近于 0,容易梯度消失。
- Tanh:将输入压缩到 -1 到 1 之间,比 Sigmoid 均值更接近 0,但仍存在饱和区。
- ReLU:max(0, z),计算简单,正区间导数恒为 1,能有效缓解梯度消失,是目前 CNN 中最常用的激活函数。
从工程角度看,ReLU 的巨大优势是“导数稳定”。它在正区间导数恒为 1,多个梯度相乘时不会因为激活函数导数过小而快速衰减。这一点在后面理解 ResNet 时还会反复出现。
2.3 前向传播的矩阵表达
实际工程中不会写 for 循环逐个神经元计算,而是用矩阵运算批量完成。一个两层的全连接网络,前向传播可以写成:
Z1 = X @ W1 + b1 A1 = σ(Z1) Z2 = A1 @ W2 + b2 A2 = σ(Z2)输入矩阵X的形状是[N, D],N是样本数,D是特征数。W1和W2是两层的权重矩阵,b1和b2是偏置。A1是隐藏层的输出,A2是最终输出。
这里的计算过程就是“前向传播”。前向传播本身只是完成了一次预测,真正让模型从随机初始化变成有效模型的,是接下来的反向传播。
3. 反向传播算法:让网络真正“学会”的发动机
3.1 损失函数与梯度下降
要让网络学会正确分类,首先需要一个衡量“预测到底有多差”的指标,这个指标就是损失函数。分类任务常用交叉熵损失,回归任务常用均方误差损失。
有了损失值之后,问题变成:如何通过调整w和b让损失值不断变小?答案就是梯度下降。梯度的方向是函数值上升最快的方向,所以参数沿着梯度的反方向更新,就能让损失下降:
θ = θ - η * ∂L/∂θ其中η是学习率。学习率太大,参数更新过猛,损失可能震荡甚至发散;学习率太小,网络收敛缓慢。调参时最先检查的往往就是这一项。
3.2 链式法则与两层网络推导
损失函数是网络输出的函数,而网络输出又依赖于每一层的权重。想求“损失对深层参数的梯度”,必须依赖微积分里的链式法则。这就是反向传播算法的数学基础。
以一个两层网络为例:
z1 = W1 x + b1 a1 = σ(z1) z2 = W2 a1 + b2 a2 = σ(z2) L = 0.5 * ||y - a2||^2其中x是输入,y是标签,L是损失。要更新输出层权重W2,需要计算∂L/∂W2,用链式法则展开:
∂L/∂W2 = (∂L/∂a2) * (∂a2/∂z2) * (∂z2/∂W2)定义误差信号:
δ2 = (a2 - y) * σ'(z2)那么:
∂L/∂W2 = δ2 * a1^T ∂L/∂b2 = δ2继续往更浅的一层传播,得到隐藏层的误差信号:
δ1 = (W2^T * δ2) * σ'(z1) ∂L/∂W1 = δ1 * x^T ∂L/∂b1 = δ1这就是反向传播的核心过程:先做一次前向传播,算出每层的输出;再根据损失,从输出层逐层往回计算梯度并更新参数。为什么要叫“反向”?因为误差信号从输出层开始,逐层向输入层方向传播。
3.3 用 numpy 手动实现一次反向传播
理论知识看再多,都不如手写一个最小实现来得直观。下面用 numpy 实现一个 2-4-1 结构的两层网络,解决 XOR 异或问题。XOR 是非线性可分问题,单层感知机无法解决,但两层网络可以。
import numpy as np np.random.seed(42) # 网络结构:2 -> 4 -> 1 w1 = np.random.randn(2, 4) * 0.5 b1 = np.zeros((1, 4)) w2 = np.random.randn(4, 1) * 0.5 b2 = np.zeros((1, 1)) # XOR 数据集 X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=np.float32) y = np.array([[0], [1], [1], [0]], dtype=np.float32) def sigmoid(z): return 1.0 / (1.0 + np.exp(-z)) learning_rate = 0.5 for epoch in range(10000): # 前向传播 z1 = X @ w1 + b1 a1 = sigmoid(z1) z2 = a1 @ w2 + b2 a2 = sigmoid(z2) loss = np.mean((a2 - y) ** 2) # 反向传播 d_a2 = 2 * (a2 - y) / len(X) d_z2 = d_a2 * a2 * (1 - a2) d_w2 = a1.T @ d_z2 d_b2 = np.sum(d_z2, axis=0, keepdims=True) d_a1 = d_z2 @ w2.T d_z1 = d_a1 * a1 * (1 - a1) d_w1 = X.T @ d_z1 d_b1 = np.sum(d_z1, axis=0, keepdims=True) # 参数更新 w2 -= learning_rate * d_w2 b2 -= learning_rate * d_b2 w1 -= learning_rate * d_w1 b1 -= learning_rate * d_b1 if epoch % 1000 == 0: print(f"Epoch {epoch:4d}, Loss: {loss:.6f}") print("训练后的预测结果(应接近 0,1,1,0):") print(np.round(a2, 4))运行这段代码,会看到 Loss 随着迭代次数增加而下降。训练结束后,预测结果会接近 XOR 的目标输出。关键逻辑在于:d_z2是输出层误差信号,d_z1是隐藏层误差信号,而隐藏层误差信号正是由输出层误差信号乘以w2.T反向传递回来的。这就是“反向”二字的由来。
4. 卷积神经网络:面向图像的结构设计
4.1 全连接层处理图像的三大难题
把全连接网络直接用于图像,会立刻遇到三个问题:
第一,参数量爆炸。一张 32x32 的彩色图片,展开后是 3072 维。如果全连接网络第一个隐藏层有 128 个神经元,仅这一层就需要约 39 万个参数。如果是更大的图片,参数量会迅速膨胀到无法训练。
第二,破坏空间结构。图像中相邻像素有强相关性,边缘、纹理、颜色变化都体现在局部区域。把像素打平成向量后,局部信息被彻底打散,网络很难学到“物体是由局部特征组合而成”的规律。
第三,难以保持平移不变性。同一只猫在图片中位置移动一点,全连接网络需要重新学习才能识别,因为它对“每个像素位置”单独建模。
4.2 卷积层:局部感受野与参数共享
CNN 的解决方案是用卷积层替代全连接层。卷积层有两个核心设计:局部连接和参数共享。
局部连接是指每个输出神经元只与输入的一小块区域相连,这一小块区域就是“感受野”。比如 3x3 卷积核,输出位置的每个值只由输入中对应位置附近 3x3 区域的像素计算而来。
参数共享是指同一个卷积核以滑窗方式遍历整张图像,所有位置共享同一组权重。这意味着无论物体出现在图像的左上角还是右下角,同一个卷积核都能检测出同一种特征。
一个具体实例能直观说明参数量的差距。输入是 32x32x3 的图像,如果卷积层使用 32 个 3x3 卷积核,参数量为:
3 * 3 * 3 * 32 + 32 = 896而同等情况下全连接层的参数量高达几十万。CNN 之所以能处理图像,正是因为这“一减一