· 深度学习
反向传播:梯度是怎么从最后一层传回来的
从一个只有两个参数的小例子起步:先算链式法则,再推广到多层网络,最后落到计算图与 PyTorch 的 backward。核心不是某条公式,而是「为什么必须从后往前算」。
用一个只有两个参数的小例子起步:先算链式法则,再把它推广到多层网络,最后落到计算图和 PyTorch 的 backward 函数。核心不是某条公式,而是「为什么必须从后往前算」。
对每个参数单独重新推一遍梯度
每个参数都从损失函数重新走到起点,把沿途的偏导重算一次。参数一多,同一条链上的中间量被反复求值,重复量随网络深度和宽度同时膨胀。
从后往前,把上游的梯度乘进来
先算损失对最后一个输出的偏导,再乘本层局部偏导往下传。每个中间量只算一次,算完立刻被下游复用。
从一个只有两个参数的模型开始
- 最简模型:输入 x 经过一个线性变换得到 y,即 y = wx + b,待定参数只有 w 和 b。
- 损失用最小二乘:L = ½(y − y_gt)²,其中 y_gt 是真实值。
- 取 x = 1.5、y_gt = 0.8、初始 w = 0.8、b = 0.2,整条链路就可以手算了。
先看一个能算到底的线性拟合例子。输入 x 经过一个线性方程得到 y,写成 y = wx + b。整个模型只有两个待定参数:权重 w 和偏置 b。
评价好坏用最小二乘,也就是把预测值与真实值之差平方,再乘 ½:L = ½(y − y_gt)²。这里的 y_gt 是基准数值,即真实值;乘 ½ 是为了求导时能和平方项的 2 抵消,让结果干净。
给定一组具体数值:x = 1.5,真实值 y_gt = 0.8,参数初始化为 w = 0.8、b = 0.2。
先把数字算一遍:预测 1.4,损失 0.18
- 代入得 y = 0.8 × 1.5 + 0.2 = 1.4,离真实值 0.8 还有 0.6 的差距。
- 损失 L = ½(1.4 − 0.8)² = 0.18,这就是要压低的目标。
- 此时所有数值都已就位,接下来唯一的问题是:w 和 b 该往哪个方向调。
把数值代进去:y = 0.8 × 1.5 + 0.2 = 1.4。模型预测 1.4,而真实值是 0.8,差了 0.6。
损失函数随之确定:L = ½ × (1.4 − 0.8)² = 0.18。这个 0.18 就是当前参数配置下的「错误程度」,训练的目标就是把它压下去。
现在所有数值都算出来了,问题也随之变得具体:该怎么调整 w 和 b,才能让 L 变小?
往哪调、调多少,都由偏导数决定
- 按梯度下降的做法,先算损失 L 对 w 和 b 的偏导数,再沿梯度的反方向更新参数。
- 麻烦在于 L 并不直接是 w 的函数——它隔着 y 这一层,没法直接对 w 求导。
- 于是问题变成:如何把「L 对 w 的偏导」拆成几段能分别求出的部分。
按梯度下降的思路,下一步很明确:计算损失函数 L 对 w 和 b 的梯度,也就是两个偏导数,然后沿着梯度的反方向把参数往下推。
但这里有个结构上的障碍。L 的表达式里只有 y,没有 w。y 才是 w 的函数,L 是 y 的函数。想求 L 对 w 的偏导,中间隔着一层。
解法是把这段距离拆开:既然 L 依赖 y、y 依赖 w,那就分别求出「L 怎么随 y 变」和「y 怎么随 w 变」,再把两段接起来。这正是链式法则要做的事。
链式法则:先算那个最关键的中间量
- 先算中间量 ∂L/∂y = y − y_gt,它把「预测与真实的距离」直接量化出来。
- 再把两段相乘:∂L/∂w = (∂L/∂y) · (∂y/∂w) = (y − y_gt) · x。
- 对 b 同理:∂L/∂b = (∂L/∂y) · (∂y/∂b),而 ∂y/∂b = 1,所以它直接等于中间量。
为了更容易算出 L 对 w 和 b 的偏导,先算一个中间量:L 对 y 的偏导。对 ½(y − y_gt)² 求导,得到 y − y_gt,也就是预测值与真实值之差。
有了这个中间量,L 对 w 的偏导就可以写成两段的乘积:L 对 y 的偏导,乘以 y 对 w 的偏导。而 y = wx + b 对 w 求导等于 x,所以结果是 (y − y_gt) · x。
对 b 完全同理,只是 y 对 b 求导等于 1,因此 ∂L/∂b 就等于那个中间量 y − y_gt。
这种把复合函数拆成相乘的求导方法,就是高中就学过的链式法则。它的价值在于:把一个够不着的导数,拆成几个够得着的导数相乘。
一次反向,两个梯度同时到手
- 代入数值:∂L/∂w = 0.6 × 1.5 = 0.9,∂L/∂b = 0.6。
- 取学习率 0.1,更新后 w = 0.8 − 0.09 = 0.71,b = 0.2 − 0.06 = 0.14。
- 新预测 y = 1.205,比原来的 1.4 更接近 0.8;损失从 0.18 降到 0.082。
把数值代进去。y − y_gt = 1.4 − 0.8 = 0.6,乘以 x = 1.5,得到 ∂L/∂w = 0.9;∂L/∂b 直接等于 0.6。
注意这两个梯度是「一趟算出来」的:中间量 y − y_gt 只算了一次,两个参数各自乘上自己的局部偏导就分别得到了梯度。这种复用,正是后面所有优化的来源。
设学习率为 0.1,参数沿反梯度方向更新:w = 0.8 − 0.1 × 0.9 = 0.71,b = 0.2 − 0.1 × 0.6 = 0.14。
用新参数重新前向:y = 0.71 × 1.5 + 0.14 = 1.205。与真实值 0.8 的距离从 0.6 缩到 0.405,损失函数也同步变小到 0.082。一次迭代,方向是对的。
「反向传播」这个名字从哪来
- 上面那套沿着数据流的反方向、由输出向输入逐层求导的流程,就是反向传播算法。
- 「反向」指的不是数值取负,而是计算顺序与正向传播相反。
- 在图上,它表现为一串从右往左的箭头,把输出的误差一层层分配回去。
回头看刚才做的事:正向是数据从左往右流,x 进去、y 出来、误差出来;而求梯度的过程恰好相反,从损失函数出发,一路往左退回到 x 和参数。
这种沿着黄色箭头、从后向前计算参数梯度的方法,就是反向传播算法。「反向」说的不是符号取负,而是计算方向与正向传播相反。
它的效果可以这样理解:输出端的误差被逐层「分配」回每个参数,每个参数分到的份额,就是它对这个误差该负的责任大小。
加一层:参数一多,为什么必须换算法
- 把模型改成两级:x 经第一个线性变换得到 y₁,再经第二个得到 y₂,最后算损失。
- 待求参数变成四个——w₁、b₁、w₂、b₂,链路变长,重复计算的机会也变多。
- 关键观察:求某个参数的梯度时,链上的后半段是公共的,前面已经算过,不必重来。
现在加一点难度。x 先经过一个线性变换得到 y₁,再经过另一个线性变换得到 y₂,最后用损失函数评价 y₂。这是正向传播的过程。
要得到 w₁、b₁、w₂、b₂ 这四个参数的梯度,如果每个参数都独立地从损失函数往回推一遍,那条公共的链路就要被反复走多次。
而从后往前依次计算就能避免这件事:先算损失对 y₂ 的偏导,再算对 y₁ 的偏导,往下逐级传递。每一步用到的上游梯度,都是上一步刚算出来的结果,直接在缓存里取,不需要第二次求值。
反向传播的实质:一个加速梯度计算的工程手段
- 反向传播算法就是神经网络中加速计算参数梯度值的方法。
- 它没有改变梯度的数学定义,改变的是求值的顺序与复用方式。
- 一次正向、一次反向,就能拿到全部参数的梯度,而不是每个参数各跑一遍。
由此可以给反向传播一个准确的定义:它就是神经网络中加速计算参数梯度值的方法。
注意这句话的重心在「加速」。梯度该是多少,数学上早就定好了;反向传播没有改变定义,它改变的是求这些值的顺序——把链式法则里的公共部分提到前面算一次,然后一路乘下去。
这个区别在网络只有两个参数时微不足道,在真实网络里却是决定性的:一次正向传播加一次反向传播,就能拿到全部参数的梯度,而不是每个参数各跑一遍完整链路。
但计算机不这么算:先拆成最小的运算单元
- 真实框架里,前向不会直接算 w₁x + b₁,而是拆成两个最小单元:先乘、再加。
- 定义中间变量 u₁ = w₁ × x,于是 y₁ = u₁ + b₁;第二个线性变换同理。
- 拆分后的特点是:每个方块里的运算都极简单且可复制,只有输入的变量不同。
上面那种「一个线性变换一步到位」的写法,方便人理解,却不是计算机里的真实做法。框架中的运算是更加模块化的。
以刚才的例子为例,先有三个叶子变量:x、w₁、b₁。然后定义一个新的中间变量 u₁,令 u₁ = w₁ × x。这样第一个线性变换的结果就可以写成 y₁ = u₁ + b₁。第二个线性变换用同样的方式再拆一遍。
拆完之后能看出一个明显的特点:每个方块里只做一件事——要么相乘,要么相加,输入变量不同而已,但运算本身高度统一、可复制。
计算图:由单元运算和变量构成的流程图
- 把变量当作数据、把单元运算当作节点连起来,得到的流程图叫计算图。
- 计算图上的每条边都代表一个有名字的变量,正向传播就是沿边把数值填进去。
- 一旦结构固定,求导就可以在图上机械地执行,不需要人再推导公式。
这种由单元运算和变量构成的计算流程图,被称为计算图。
它的结构与神经网络本身一一对应:方块是运算,箭头是数据流,每个箭头上流动的都是一个有名字的变量。正向传播就是沿着箭头,把已知数值一个个填进去,最终算出损失。
计算图的意义在于:它把「求导」这件依赖数学推导的事,变成了一件可以在图上按规则执行的机械操作。只要图结构确定,梯度就能沿着图自动算出来,不需要人再对每个网络手写公式。
反向的起点:损失对最后一个输出的偏导
- 反向传播的第一步没有可复用的上游,必须直接算出 ∂L/∂y₂。
- 这个值靠损失函数的解析式得到,是整个反向链路的种子。
- 后面所有变量的梯度,都由这个种子逐级乘出来。
现在用计算图反向求出损失函数对每一个变量的梯度表达式。第一步必须手算:损失函数 L 对最后输出变量 y₂ 的偏导,记作 ∂L/∂y₂。
这一步没有捷径,因为它位于链路的末端,没有上游梯度可以借。它只能由损失函数的形式直接求出来,也正因为如此,它是整条反向链路的种子。
把这一步做对之后,往下的每一步都有固定套路:拿上游传来的梯度,乘上本地的局部偏导,传给下游。
一个值得注意的细节:加法节点的偏导等于 1
- 沿反向方向看 u₂ 这个变量,按链式法则:∂L/∂u₂ = (∂L/∂y₂) · (∂y₂/∂u₂)。
- 因为 y₂ = u₂ + b₂,加法对 u₂ 求偏导恒等于 1。
- 于是 ∂L/∂u₂ 直接等于 ∂L/∂y₂——加法节点把上游梯度原封不动地传下去。
沿着反向传播的方向继续走,下一个遇到的是 u₂。按链式法则,L 对 u₂ 的偏导等于 L 对 y₂ 的偏导,再乘以 y₂ 对 u₂ 的偏导。
关键在于后一项。y₂ = u₂ + b₂,加法运算对 u₂ 求偏导,结果恒等于 1。这不是巧合,而是加法节点的普遍性质:它对任何一个加数求导,导数都是 1。
所以最终的 ∂L/∂u₂ 就等于 ∂L/∂y₂。用工程的眼光看:加法节点在反向时相当于一根直通管道,梯度进来多少,出去还是多少,不做任何缩放。
全部梯度:黄色是历史,绿色是已知
- 同样的做法可以推到图上所有变量,得到完整的梯度表达式集合。
- 黄色项来自前一步的反向计算,绿色项来自正向传播时存下的数值。
- 真正需要的只是四个参数的梯度,其余中间量的梯度都只是过路的中间结果。
用同样的道理,可以为图中所有变量的梯度写出表达式。写完之后会看到一个很整齐的规律:每一项要么是前面步骤已经得到的梯度,要么是正向传播时求出的数值。
式子里的黄色量,就是通过之前步骤得到的梯度;绿色量则是正向传播中已经算出的值。这两类都是已知的,直接相乘即可,不需要任何新的求导。
而我们真正需要的结果只有一个:w₁、b₁、w₂、b₂ 这四个参数的梯度。拿到它们,就可以交给梯度下降去更新参数了。其余中间变量的梯度,只是串起这条链的过路点。
在 PyTorch 里,它长这样
- 框架里每个单元运算都实现为一对函数:forward 做前向计算,backward 把上游梯度乘回本地偏导。
- 以乘法为例,forward 返回 x × y;backward 收到 grad_z 后,分别算出 grad_x = grad_z · y、grad_y = grad_z · x。
- 这就是链式法则的代码形态:本地偏导写死在 backward 里,上游梯度由参数传进来。
在深度学习框架里,计算图中元素的定义和使用都很直接。以乘法运算为例,在 PyTorch 中就可以写成下面这样:
class Multiply(torch.autograd.Function): @staticmethod def forward(ctx, x, y): ctx.save_for_backward(x, y) # 前向的输入留到反向用 z = x * y return z @staticmethod def backward(ctx, grad_z): x, y = ctx.saved_tensors grad_x = grad_z * y # dL/dx = dL/dz * dz/dx, dz/dx = y grad_y = grad_z * x # dL/dy = dL/dz * dz/dy, dz/dy = x return grad_x, grad_y
forward 以 x 和 y 作为输入,输出二者的乘积,这就是前向传播的计算;它顺手把输入存进 ctx,因为反向时还要用。
backward 的入参 grad_z,就是损失函数 L 对 z 的偏导数,也就是上游传下来的梯度。而 grad_x 和 grad_y 要算的,是 L 对 x 和 y 的偏导数——按链式法则,用 grad_z 分别乘上本地的偏导即可。乘法节点的两个本地偏导正好是「对方的值」,所以代码里就是乘 y 和乘 x。
总结:为什么亿级参数也能训得动
- 反向传播(backpropagation,BP 算法)是深度学习最核心的算法之一。
- 它用一次反向遍历快速算出全部梯度,再由梯度下降完成参数更新。
- 在计算机中,它以计算图的方式模块化实现:所有单元运算都定义了正向与反向函数。
完整回顾这条链路。反向传播的英文是 backpropagation,也常被称为 BP 算法,是深度学习最核心的算法之一。
它的作用可以压成一句话:利用反向传播快速计算梯度值,然后再利用梯度下降算法训练整个神经网络。前者负责算出「每个参数该负多少责任」,后者负责「照着这个责任去调整」。
而在计算机中,反向传播一般是使用计算图的方式进行模块化运算的:所有单元运算都配有一对 forward 和 backward 函数,图一搭好,梯度就能自动流过整张图。
这正是深度网络能训练起来的原因。参数规模达到数以亿计的时候,没有任何人能手推出每个参数的梯度;但只要每个基础运算的反向规则都被写好一次,剩下的工作就可以交给图去重复执行。
术语速查
按出现顺序排列。
- 反向传播backpropagation / BP
- 从损失函数出发,逆着正向数据流逐层回推,复用上游梯度算出全部参数梯度的算法。
- 链式法则Chain Rule
- 复合函数求导时把它拆成若干段偏导相乘,是反向传播全部推导的基础。
- 正向传播Forward Pass
- 把输入沿计算图一步步算到损失函数,同时把中间结果保存下来供反向使用。
- 计算图Computational Graph
- 以变量为边、单元运算为节点的有向流程图,把求导变成可在图上机械执行的操作。
- 梯度下降Gradient Descent
- 算出梯度后沿其反方向按学习率更新参数,逐步逼近损失函数的最小值。
- 偏导数Partial Derivative
- 多元函数中固定其余变量只对某一个变量求导,表示该方向上的变化率。
- 中间变量Intermediate Variable
- 为拆分运算而引入的临时量,如 u = w × x,是反向传播复用的对象。
- 学习率Learning Rate
- 控制每次参数更新幅度的系数,本例取 0.1,过大震荡、过小收敛慢。
- 自动微分Autograd
- 框架为每个单元运算预写好正反向函数,使梯度可沿图自动计算。
- 最小二乘Least Squares
- 以预测值与真实值之差的平方作为损失,本例取 L = ½(y − y_gt)²。