资讯动态

反向传播传的是什么?梯度信号与责任分配彻底讲透

发布时间:2026/10/14 12:27:12 来源:尧图企业网站定制
你是不是也有过这种困惑明明机器学习入门书里把反向传播的公式推导一步步列出来了张量求导、链式法则都看得懂但合上书闭上眼还是会问一句——这玩意儿到底“传”的是个什么东西数据在正向传播里从输入层流到输出层这个好理解可反向传播呢它在往回传的究竟是误差是梯度还是一个听起来很玄的“信号”我当初纠结了很久直到自己动手推了一个极简网络才真正想明白。答案是反向传播传的不是误差本身而是“误差对每个权重的影响程度”——也就是损失函数对每个参数的梯度。它传的是一种“责任分配”。你某个预测错了错多少每一层每个神经元该承担多少责任这个责任量就是梯度。下游层把责任汇总、拆分一级一级往回甩最后每个权重拿到一个属于自己的修正量。这篇文章就用最直白的方式把这个过程彻底说透。1. 先搞清楚正向传播在干什么1.1 一个最小例子输入怎么变成输出在聊反向传播之前得先把正向传播的账目理清楚。举个最精简的例子假设有一个神经网络只有一层隐藏层激活函数用 Sigmoid输出层不加激活损失用均方误差。输入是一个二维向量 (x [x_1, x_2])隐藏层有两个神经元输出是一个标量。正向传播的每一笔操作都是明确的线性变换加非线性激活。隐藏层神经元 (h_1) 的计算是[ z_1 w_{11}x_1 w_{21}x_2 b_1 ] [ h_1 \sigma(z_1) ]这个 (z_1) 叫“预激活值”也就是线性组合的结果。(w_{11}) 表示输入 (x_1) 到隐藏神经元 (h_1) 的权重(b_1) 是偏置。激活函数 (\sigma) 的作用是引入非线性让网络有能力拟合复杂的映射关系。如果去掉这一层非线性不管堆多少层线性变换最终都等价于一层线性模型能力就废了。输出层只有一个神经元它接收隐藏层两个神经元的输出再做一次线性组合[ y v_1 h_1 v_2 h_2 c ]这里我用 (v) 表示输出层权重避免和隐藏层权重 (w) 混淆。整个正向过程就是从 (x) 出发按照当前参数 ([w_{11}, w_{21}, w_{12}, w_{22}, b_1, b_2, v_1, v_2, c]) 一步步计算最终得到预测值 (y)。此时网络还不知道自己错得有多离谱它只知道“我算出了一个数”。1.2 损失函数把“差距”变成一个数有了预测值 (y)还需要知道它跟真实标签 (t) 差多远。这个“差距”就用损失函数来度量。最直观的均方误差[ L \frac{1}{2}(t - y)^2 ]为什么系数用 (\frac{1}{2})纯粹是为了求导时消掉平方带来的 2简化梯度表达式不影响优化方向。这只是一个数学上的偷懒技巧不是必须的。现在整个网络的“表现好坏”都浓缩到了 (L) 这个数上。机器学习训练的本质就是让这个数尽量小。而要让 (L) 变小唯一能动的旋钮就是网络里所有的 (w)、(v)、(b)、(c)。问题就变成了这些参数分别对 (L) 有多大影响这个影响量数学上就是偏导数 (\partial L / \partial w)也就是梯度。你可以把 (L) 想象成一个以所有参数为自变量的复杂函数每个参数在某个取值处都有一个“坡度和坡向”。梯度指向的是 (L) 增大最快的方向那么负梯度就是让 (L) 减小最快的方向。反向传播干的事情就是把每个参数的 (\partial L / \partial w) 算出来然后我们沿着负梯度方向微调参数。这就是梯度下降的骨架。所以正向传播解决的是“给定参数算出预测值”反向传播解决的是“给定误差算出每个参数的梯度”。一个向前流一个往回传两者配合起来才能完成一步参数更新。2. 反向传播到底传了什么2.1 误差信号的本质很多人会误以为反向传播传的是“误差值”。比如预测 (y0.8)目标 (t0.2)误差是 (0.6)就把这个 (0.6) 往回传。这不够准确。真正往回传的是损失函数对每一层输出的偏导数专业一点叫“误差信号”。还是用上面的例子。输出层直接对 (y) 求偏导[ \frac{\partial L}{\partial y} y - t ]这里因为 (L \frac12(t-y)^2)所以 (\partial L/\partial y y - t)。当 (y0.8, t0.2) 时这个值是 (0.6)。你看恰好在数值上等于误差这容易让人产生混淆。但如果输出层用了 Sigmoid 激活或者损失函数换成交叉熵这个偏导就不再是简单的差值了。所以请记住这个定义反向传播传的是 (\partial L / \partial z)、(\partial L / \partial h) 这类梯度而不是 (L) 本身。每一个中间变量都会收到一个“上游传来的梯度”它代表了“最终损失对这个变量有多敏感”。2.2 链式法则梯度的分解与累乘反向传播的数学引擎是链式法则。如果 (L) 依赖 (y)(y) 依赖 (h)(h) 依赖 (z)(z) 依赖 (w)那么梯度可以逐层拆解[ \frac{\partial L}{\partial w} \frac{\partial L}{\partial y}\cdot\frac{\partial y}{\partial h}\cdot\frac{\partial h}{\partial z}\cdot\frac{\partial z}{\partial w} ]这个式子看起来就是一堆偏导数的乘积。但乘积只是表象真正的机械操作是“逐层传递”。从输出层开始先算出 (\partial L/\partial y)把它当作上游信号然后计算下一层局部导数 (\partial y/\partial h)两者相乘得到 (\partial L/\partial h)再继续把这个结果传下去乘以 (\partial h/\partial z)得到 (\partial L/\partial z)最后乘以 (\partial z/\partial w)得到对权重的梯度。每一步都只涉及“当前层”的局部计算不需要重新算整个网络。这就是反向传播高效的原因一次正向传播得到所有激活值一次反向传播得到所有梯度。时间复杂度大约相当于两次正向传播而不是“对每个参数单独求极限”那样莽撞。2.3 直观理解责任分配我一直觉得比数学公式更重要的是一个生活化类比。想象你是项目负责人最终项目搞砸了损失了 100 万。这 100 万不是凭空产生的它跟每个环节、每个决策都有关系。你要追责就得从结果倒推项目失败是因为交付延期交付延期是因为开发速度慢开发速度慢是因为需求文档不清晰需求文档不清晰是因为前期调研不足。每一项都要分清楚“该承担多少责任”。这里的“责任”就是梯度。神经网络也一样。输出层预测错了先看每个输出神经元对误差负多大责任接着看隐藏层的每个神经元通过加权连接传导给输出层的误差应该按权重比例分摊多少责任再看这些中间变量对每个权重有多依赖。这一层层往回追责的过程就是反向传播。注意一个细节责任在层层传导中会乘上权重和激活函数的导数。如果权重很小责任会被削弱如果激活函数导数接近 0比如 Sigmoid 饱和区责任会进一步衰减。这就是后面要说的梯度消失问题。3. 手动算一遍反向传播保姆级推导3.1 网络设定与符号约定理论说得再漂亮不如实际手算一次。我挑一个极小网络把所有中间步骤都摊开。设定如下输入(x_1 0.5, x_2 0.2)真实标签(t 0.7)隐藏层两个神经元激活函数为 Sigmoid(\sigma(z) \frac{1}{1e^{-z}})输出层一个神经元不加激活直接线性输出初始参数随便给一组便于计算的数[ w_{11}0.4,\ w_{21}0.3,\ w_{12}0.6,\ w_{22}0.5 ] [ b_10.1,\ b_20.2 ] [ v_10.8,\ v_20.9,\ c0.1 ]这些值没有特殊含义就是为了让手算过程清晰。你完全可以从随机初始化开始关键看梯度推导的逻辑。3.2 前向计算先算隐藏层预激活值[ z_1 w_{11}x_1 w_{21}x_2 b_1 0.4\times0.5 0.3\times0.2 0.1 0.2 0.06 0.1 0.36 ] [ z_2 w_{12}x_1 w_{22}x_2 b_2 0.6\times0.5 0.5\times0.2 0.2 0.3 0.1 0.2 0.6 ]经过 Sigmoid[ h_1 \sigma(0.36) \frac{1}{1e^{-0.36}} \approx 0.5890 ] [ h_2 \sigma(0.6) \frac{1}{1e^{-0.6}} \approx 0.6457 ]输出层[ y v_1h_1 v_2h_2 c 0.8\times0.5890 0.9\times0.6457 0.1 ] [ 0.4712 0.5811 0.1 1.1523 ]损失[ L \frac12(t-y)^2 \frac12(0.7-1.1523)^2 \approx \frac12\times0.2046 0.1023 ]前向结果预测值 1.1523偏高。接下来就要用反向传播看每个参数应该怎么调整。3.3 反向计算每层的梯度反向传播第一步从最终损失开始。输出层的局部梯度[ \frac{\partial L}{\partial y} y - t 1.1523 - 0.7 0.4523 ]这个上游信号记为 (\delta_y 0.4523)。接着算出对输出层权重 (v_1) 和 (v_2) 的梯度。因为 (y v_1h_1 v_2h_2 c)有[ \frac{\partial L}{\partial v_1} \frac{\partial L}{\partial y}\cdot h_1 0.4523\times0.5890 \approx 0.2664 ] [ \frac{\partial L}{\partial v_2} 0.4523\times0.6457 \approx 0.2921 ]偏置 (c) 的梯度就是 (\partial L/\partial c 0.4523)。然后要把误差信号传到隐藏层。先看 (h_1) 对损失的影响[ \frac{\partial L}{\partial h_1} \frac{\partial L}{\partial y}\cdot\frac{\partial y}{\partial h_1} 0.4523\times v_1 0.4523\times0.8 0.3618 ]这里 (\partial y/\partial h_1 v_1)。同理[ \frac{\partial L}{\partial h_2} 0.4523\times v_2 0.4523\times0.9 0.4071 ]隐藏层输出 (h_1 \sigma(z_1))所以从 (h_1) 继续传信号到预激活值 (z_1)要乘上 Sigmoid 的导数。Sigmoid 导数有个简便表达[ \sigma(z) \sigma(z)(1-\sigma(z)) ]于是[ \frac{\partial L}{\partial z_1} \frac{\partial L}{\partial h_1}\cdot h_1(1-h_1) 0.3618\times0.5890\times(1-0.5890) ] [ 0.3618\times0.5890\times0.4110 \approx 0.0875 ]同理[ \frac{\partial L}{\partial z_2} 0.4071\times0.6457\times(1-0.6457) ] [ 0.4071\times0.6457\times0.3543 \approx 0.0931 ]现在有了隐藏层预激活值的信号 (\delta_{z_1}) 和 (\delta_{z_2})再看它对权重的影响[ \frac{\partial L}{\partial w_{11}} \frac{\partial L}{\partial z_1}\cdot x_1 0.0875\times0.5 0.04375 ] [ \frac{\partial L}{\partial w_{21}} \frac{\partial L}{\partial z_1}\cdot x_2 0.0875\times0.2 0.0175 ] [ \frac{\partial L}{\partial w_{12}} \frac{\partial L}{\partial z_2}\cdot x_1 0.0931\times0.5 0.04655 ] [ \frac{\partial L}{\partial w_{22}} \frac{\partial L}{\partial z_2}\cdot x_2 0.0931\times0.2 0.01862 ]偏置[ \frac{\partial L}{\partial b_1} 0.0875,\quad \frac{\partial L}{\partial b_2} 0.0931 ]看出来规律没有整个反向传播在梯度计算层面就是反复做两件事乘以“连接处的传递系数”再乘以“输入侧的值”。从损失到输出层乘以权重 (v)从输出层到隐藏层输出乘以 (v) 的数值从隐藏输出到预激活乘以 Sigmoid 导数从预激活到权重乘以输入值 (x)。每一层都只做局部乘法模式极其统一。3.4 权重更新梯度下降拿到梯度之后用梯度下降更新参数。更新规则是[ \text{param}{\text{new}} \text{param}{\text{old}} - \eta \cdot \frac{\partial L}{\partial \text{param}} ]设学习率 (\eta 0.1)输出层权重 (v_1) 更新为[ v_1^{\text{new}} 0.8 - 0.1\times0.2664 0.77336 ](v_2) 更新为[ v_2^{\text{new}} 0.9 - 0.1\times0.2921 0.87079 ]隐藏层权重 (w_{11}) 更新为[ w_{11}^{\text{new}} 0.4 - 0.1\times0.04375 0.395625 ]其余参数同理。注意这里所有梯度都是正值损失函数关于这些参数是递增的所以要减小参数来降低损失。如果某个梯度是负的参数反而要增大。这正是“负梯度方向”的含义。如果你用新参数重新正向计算一次会发现 (y) 从 1.1523 往 (0.7) 的方向靠近了一点。每迭代一次损失就小一点。过程中所有梯度计算依赖前向缓存的值一旦权重更新缓存的激活值就过时了因此每一步训练必须先重新正向传播再反向传播。4. 从代码视角看反向传播4.1 自动求导框架做了什么现在主流的深度学习框架都提供自动求导你根本不需要手写这些梯度公式。但框架不是玄学它们本质上是在做两件事记录计算图然后按拓扑序自动应用链式法则。TensorFlow 和 PyTorch 这类框架在正向传播时会把每个操作乘法、加法、激活函数封装成节点同时保存操作需要的输入值。等到调用backward()的时候框架从损失节点出发沿着计算图逆序走一遍每个节点调用自己注册的“局部梯度”函数再乘以上游传来的梯度。这就是课程里常说的“前向建图反向求导”。理解这一点很有用。当你自定义一个操作时框架需要你实现两个方法一个计算前向结果另一个计算“输入梯度 上游梯度 × 局部导数”。局部导数通常是常数或函数表达式。如果你提供的梯度公式写错了框架也不会知道因为它只是机械相乘。4.2 一个简单的 Numpy 实现示例为了展示反向传播的“传信号”过程我用 Numpy 手写一个极简两层网络。没有框架封装每一步都看得见。import numpy as np def sigmoid(z): return 1 / (1 np.exp(-z)) def sigmoid_derivative(h): return h * (1 - h) # 训练数据单个样本 x np.array([0.5, 0.2]) t np.array([0.7]) # 初始化参数 w np.array([0.4, 0.3, 0.6, 0.5]).reshape(2, 2) b np.array([0.1, 0.2]) v np.array([0.8, 0.9]) c np.array([0.1]) # 学习率 eta 0.1 # 前向传播 z x w b # 得到形状 (2,) h sigmoid(z) # 隐藏层输出 y h v c # 输出层 # 损失 L 0.5 * (t - y)**2 # 反向传播 delta_y (y - t) # 输出层上游信号 grad_v delta_y * h # 对输出层权重的梯度 grad_c delta_y # 对输出层偏置的梯度 delta_h delta_y * v # 传给隐藏层输出的信号 delta_z delta_h * sigmoid_derivative(h) # 传给预激活值的信号 grad_w delta_z.reshape(-1, 1) * x # 外积得到 (2, 2) grad_b delta_z # 参数更新 v - eta * grad_v c - eta * grad_c w - eta * grad_w b - eta * grad_b注意grad_w delta_z.reshape(-1, 1) * x这一步就是前面推导的“(\partial L/\partial w_{ij} \delta_{z_i}\cdot x_j)”的向量版。delta_z是每个隐藏神经元收到的上游信号x是该神经元的输入两者做外积得到权重的梯度矩阵。实际跑一遍你会发现参数更新行为跟手算完全一致。这就是反向传播的全部秘密传递一串数字这串数字叫“梯度信号”每个节点拿到它之后乘上自己的局部导数再继续往后传。4.3 常见实现细节激活函数与数值稳定性用 Numpy 手写反传时有几个容易踩坑的地方。第一是激活函数导数公式不够鲁棒。比如 Sigmoid 直接实现为1/(1np.exp(-z))当z非常小时np.exp(-z)可能溢出。对于手写示例这问题不大但在实际训练中更推荐梯度稳定的实现比如 Tanh 的导数可以用1 - h^2ReLU 的导数直接判断输入正负。框架内部也都针对数值稳定性做了优化。第二是损失函数的选择。均方误差加 Sigmoid 输出层会产生梯度饱和当输出接近 0 或 1 时Sigmoid 导数趋近 0导致信号快速衰减。如果是分类任务更常见的是输出层用 Softmax 加交叉熵两者结合产生的梯度在数学上恰好是预测概率 - 真实分布既简洁又避免了饱和。这也是为什么很多入门课强调“激活函数和损失函数要配套使用”。第三是参数更新的顺序。反向传播算出梯度后必须用更新前的参数重新计算梯度不能更新了一半再继续用旧梯度。手写代码时要注意先算所有梯度再统一更新参数。梯度下降本身就是“整批算完梯度然后一起走一步”。PyTorch 里的optimizer.step()做的事就是这个。5. 常见问题与排查心得5.1 梯度消失/爆炸是怎么来的理解了反向传播传的是梯度乘积之后梯度消失和梯度爆炸就很容易解释了。如果网络特别深梯度在每一层都要乘上一堆因子。Sigmoid 的导数最大值只有 0.25多层相乘之后信号指数级缩小最终远离输出层的地方梯度趋近于零——这就是梯度消失。如果每层权重初始化过大导数乘积的模都大于 1多层累乘后梯度指数级膨胀——这就是梯度爆炸。你可以做个简单实验一个 20 层的网络每层局部乘子按平均 0.5 算20 次方约等于百万分之一第一层的权重基本收不到有效信号。这也是为什么现代网络普遍用 ReLU 系列激活函数、残差连接、更合理的初始化策略。ReLU 在正区间的导数为 1不会主动缩小信号残差连接则给梯度提供了一条“直通高速路”绕过非线性层直接传到浅层。5.2 为什么有的层参数更新慢训练时你可能会发现靠近输入层的参数似乎总是不怎么变或者变化幅度远小于靠近输出层的参数。这并不一定是 bug而是反向传播中的梯度信号在逐层衰减或异常放大的现象。可以从上面手算的例子看出隐藏层权重 (w_{11}) 的梯度 0.04375明显小于输出层权重 (v_1) 的梯度 0.2664。这不是偶然而是因为信号每传一层都要经过激活函数导数这一关——Sigmoid 导数在类似 0.36 这个位置大约只有 0.24直接把信号砍掉了四分之三。如果某个层参数更新特别慢可以检查这一层输入值的尺度。比如输入特征没有归一化数值普遍很小那么 (\partial L/\partial w \delta \cdot x) 也会整体偏小导致这个权重看起来“学不动”。解决办法通常是做标准化比如 Batch Normalization把每层输入拉回到合理区间。5.3 调试技巧检查梯度我强烈建议初学阶段做一次“梯度检查”用数值梯度验证手写或框架实现的解析梯度。数值梯度的原理是导数定义[ \frac{\partial L}{\partial w} \approx \frac{L(w\epsilon) - L(w-\epsilon)}{2\epsilon} ]也就是给参数加一个极小的扰动分别计算两次前向损失用中心差分近似梯度。(\epsilon) 一般取 1e-5 左右。如果解析梯度和数值梯度在相对误差小于 1e-4 左右说明反向传播代码基本正确。这个技巧在调试自定义算子时特别有用。我曾经在实现某个自定义激活函数时把导数公式里的负号写丢了导致训练完全发散。用梯度检查一测立刻发现某个位置的数值梯度和解析梯度差了十万八千里问题定位速度比瞎猜快得多。哪怕你不自己实现算子理解这个思路也能帮助你更快判断“到底是模型问题还是框架问题”。最后分享一个实际体会回头再看“反向传播传的是什么”这个问题你会发现它并不玄乎传的是梯度信号是误差对各层中间变量的“敏感度”是每个参数该承担的责任。但光知道定义还不够我自己的体会是真正确彻理解反向传播靠的不是看十遍公式而是亲手把一个小网络从零推导一遍再用 Numpy 实现一遍跑几个样本观察梯度变化。等你看到某一个权重因为微小的梯度而缓慢调整、损失一点一点下降的时候那种“原来如此”的感觉会非常强烈。一个额外的小技巧如果你在阅读框架源码时觉得计算图复杂可以把图中的节点都标注上“它收到了什么信号它会传出什么梯度”用一张手写的计算图单向流一遍。反向传播从来没有跨层跳跃它就是从输出层出发一步一步往输入层走。每一步只做局部乘法和加法这就是它能够高效运作的根本原因。希望这篇拆解能帮你彻底打通这个关键环节。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑