问题:该怪谁?
上一篇里,你把一个输入送过网络——也就是前向传播——末端吐出一个预测。你拿它和正确答案比较,得到一个数:损失(loss),衡量网络错得有多离谱。现在难题来了。网络的各层里散布着成千上万个权重。要想变好,它得对*每一个*权重弄清楚同一件事:如果我把你调大一丁点,损失会上升还是下降,幅度多大?
那个「损失会变多少」的数就是梯度(gradient)——严格说,是损失对那一个权重的偏导数。把每个权重的梯度都收集起来,你就得到一个巨大的箭头,指向让损失上升最快的方向。朝反方向迈一步——这就是梯度下降,下一篇会讲——网络就变好了。反向传播*不是*那个学习步骤。它是在学习发生之前,高效地把所有这些梯度算出来的那套机制。
你可以想象一种蛮力办法:动一下某个权重,把整个前向传播重跑一遍,看损失变了多少,对全部一百万个权重重复。这能行,但一百万个权重意味着每个训练样本要跑一百万次完整前向传播,慢得没指望。反向传播能算出*完全一样*的答案,代价却大约只是一次额外的传播——而正是这份高效,才让深度网络根本能够被训练。
一招制胜:链式法则
神经网络就是一长串简单运算层层叠起来:乘以权重、加上偏置、过一遍激活函数挤压一下、把结果喂给下一层,如此往复直到损失。当函数像这样层层嵌套时,微积分有一个精确的工具来求出最前端的一点变化如何一路波及到最末端:链式法则。它的想法简单到近乎让人难以置信——*把这条路径上各处的局部变化率乘起来*。
想象一排齿轮。若齿轮 A 转得比 B 快三倍,B 又比 C 快两倍,那么转动 A 就让 C 快六倍——你只需相乘,3 × 2。链式法则对函数说的正是这件事:损失对某个靠前权重的敏感度,等于从那个权重到损失之间每一环上微小敏感度的乘积。每一环都是一个*局部*问题——「当我的输入变化时,我的输出怎么变?」——而局部问题很好答。一个乘法节点、一个加法节点、一个 ReLU,它们的局部导数都简单得要命。
计算图:每一步运算的地图
要机械地执行链式法则,我们先把整个运算画成一张计算图:图中每个节点是一个小运算(一次乘法、一次加法、一次激活),箭头表示哪个结果喂给了哪个节点。前向传播不过是从左到右走一遍这张图,在每个节点上填进一个数。关键在于,每个节点还会记住自己的输入——回程时它会用得上。
然后我们反着走这张图,从右到左——*反向*传播的名字正源于此。我们从损失处出发,带着一个等于 1 的梯度(损失对自身的敏感度恰好是 1),把这个信号往回推。在每个节点上,我们问那个局部问题,把传进来的梯度乘以局部导数,再把乘积交给上游的节点。经过所有这些乘法之后,到达任何一个权重处的信号,*就是*它的梯度。一次回程,所有梯度一次到手。
当一个节点的输出分叉、喂给下游好几个地方时,从那几处流回来的梯度只需在该节点处*相加*——它影响过的每条路径上的「责任」被汇总起来。这一条规则(沿路径相乘、跨路径相加)就是整个算法的全部。正因如此,靠前的隐藏层里某个权重——它通过许多下游路线触及损失——才会正确地累积起所有这些路线上的责任。
# Forward: remember each node's inputs z = w * x + b # node sees x a = relu(z) # node sees z loss = (a - target)^2 # node sees a # Backward: start at 1, multiply local derivatives g_loss = 1 g_a = g_loss * 2*(a - target) # d loss / d a g_z = g_a * (1 if z > 0 else 0) # d relu / d z g_w = g_z * x # d z / d w -> gradient for w! g_b = g_z * 1 # d z / d b -> gradient for b!
自动微分:让机器替你做微积分
令人解脱的地方来了:你永远不必手推这些。现代框架会在前向传播运行时替你把计算图搭好,再自动反着回放一遍。这就是自动微分(常叫 autodiff 或 autograd)。它既不是你上学时做的符号代数,也不是第一节里那种摇摇晃晃的有限差分微调——它通过把每个基本运算已知的局部导数组合起来,算出精确的梯度。
实际操作中这意味着:你只用普通代码写出前向计算——各层、激活、损失——然后调用一句类似 loss.backward()。框架早已知道它跑过的每个乘法、加法、ReLU、softmax 的局部导数,于是反向遍历记录好的图,把梯度沉淀到每一个参数上。反向传播是「反向模式自动微分」在网络损失上的*具体*应用;自动微分则是那台通用引擎。
当梯度信号衰减时
那条「沿路径相乘」的规则有其阴暗面。把梯度送过很多层,你就是在把许多数连乘。如果这些局部导数大多小于 1——像 sigmoid 这类老激活函数就容易如此,因为除了一个窄带之外它的斜率都很小——乘积就会向零收缩。等信号传到最靠前的几层时,已是细若游丝。那些层几乎不更新,学得慢得令人痛苦。这就是著名的梯度消失问题。
反过来的情形同样会发生:如果局部因子大于 1,乘积可能炸成巨大的数,训练直接崩溃。这些都不是什么奇异的 bug——它们是链式法则连乘最直接、最诚实的后果,多年来正是它们让深度网络几乎无法训练。解锁现代深度学习的诸多进展,很大程度上就是在设法让这个反向信号活下去:像 ReLU 这种不挤压斜率的激活、谨慎的权重初始化,以及给梯度一条干净捷径回家的架构技巧。
诚实地说清反向传播是什么、不是什么很有必要。它精确、高效,是你听说过的几乎每一个网络背后的主力。但它不是大脑学习的方式——真实神经元并没有一个全局的反向通道,把误差信号沿着它们传上来的那些导线原路送回。它也不是智能;它是一套求斜率的程序。真正了不起的是:这套朴素的程序重复几十亿次,竟能把一堆数字带到那么远。
串起来看
于是这就是完整的循环——网络学习时,它对每一批数据重复,重复几百万次。反向传播是其中的第 2、3 步——一次性算出对每个权重而言「下坡」是哪个方向的那一部分。
- 前向传播:把输入送过各层,同时把每一步运算记录进计算图,最后算出损失。
- 为反向传播播种:从损失处出发,带上一个等于 1 的梯度。
- 反向传播:从右到左扫过计算图,在每个节点把传入梯度乘以局部导数、在路径汇合处相加,直到每个权重都拿到属于自己的梯度。
- 更新(这是优化器的活,不是反向传播):让每个权重朝其梯度的反方向迈一小步。然后带上下一批数据回到第 1 步。
这就是整台机器。链式法则提供数学,计算图提供组织,自动微分提供劳力,梯度下降把算出的梯度变成真正的学习。在本阶最后一篇里,我们将让一个完整的网络在真实数据上跑这个循环,看着损失曲线一路下落——那正是网络真正自我教学的时刻。