反向传播全面解析:从直觉到手推实现与PyTorch实战

发布时间:2026/9/18 2:51:37
反向传播全面解析:从直觉到手推实现与PyTorch实战 1. 先说结论反向传播到底在算什么东西如果你刚接触深度学习十有八九会卡在“反向传播”这四个字上。网上教程一大堆公式密密麻麻什么链式法则、梯度、偏导数看着就头大。我最初学的时候也是这样背了一堆公式真到自己手写一个两层网络的时候还是懵的。其实反向传播干的事情特别简单它就是在算“每个参数对最终损失值的影响有多大”然后按照这个影响去调整参数让损失值变小。打个比方你在食堂排队打饭今天的大厨发挥失常菜做咸了。你要找出问题出在哪个环节——是盐放多了酱油倒多了还是豆瓣酱下手太重你得从“最后的味道太咸”这个结果出发一步步往前倒推估算每个环节分别贡献了多少咸味然后就知道下次该少放哪样调料了。反向传播做的事情和这个一模一样。神经网络的前向传播就是从输入到输出算一遍得到预测值和损失反向传播就是从损失出发一步步往回倒推算出每一个权重和偏置对这次损失“贡献”了多少——数学上这个贡献量就叫梯度然后按照梯度去修正参数。梯度大的说明这个参数是“罪魁祸首”要重点改梯度小的说明它影响不大稍微动一下就行。所以整篇文章我会从三个层面把这件事讲透第一层是直觉理解告诉你它背后的逻辑是什么第二层是数学推导用一个具体的小网络把每一步的数值都算出来给你看第三层是实操经验聊聊我在实际训练中踩过的坑和排查技巧。看完你至少能做到看得懂反向传播的推导过程能独立手算一个简单网络的全部梯度也知道训练中梯度异常时该从哪里排查。这篇文章不是纯理论科普更偏一份“手把手带你算一遍”的实操笔记适合正在学深度学习的初学者也适合那些公式看过就忘、想真正理解底层逻辑的读者。2. 核心直觉误差是怎么“传”回去的2.1 从“责任分摊”理解误差传播再深挖一层。反向传播这个名词里“反向”是相对于前向传播说的“传播”的是“误差”。但误差本身是一个数值它不会自己长腿跑回去真正往回传的其实是误差对各参数的敏感性信息。我换一种说法你感受一下。你开了一家奶茶店最近一个月营业额下滑。你要找原因把关键环节拆成产品口味、门店位置、定价策略、服务态度。假设你判断“口味”对营业额下降的贡献是40%而口味又取决于“茶叶品质”、“牛奶品牌”和“糖浆比例”三个因素那你会继续把这40%往下分分到这三个子因素头上。每一层都把上一层的“责任”按权重继续往下分配最终你就知道问题主要出在哪个环节。反向传播的“责任分摊”机制也是一层一层往回传的。这里的“责任”在数学上就是偏导数每一层把上层传回来的梯度乘上自己对参数的导数得到下一层要用的梯度。这个过程严格遵循链式法则所以反向传播的本质就是链式法则的工程化应用。有一个点我必须提醒你很多人以为反向传播是在“更新参数”时顺便算出来的。其实反向传播发生在参数更新之前它专门负责计算梯度真正的参数更新用的是梯度下降法。简单说反向传播是“算方向的”梯度下降是“迈步子的”两者一前一后配合。这一点搞混了后面看代码就容易犯糊涂。2.2 为什么非要从后往前算你可能想问想算每个参数的影响梯度我能不能直接对每个参数求偏导一个一个算理论上可以但实际行不通。原因在于神经网络里参数太多了——一个百层网络动辄上千万个参数而且参数之间层层嵌套直接对每个参数单独求导计算量会爆炸式增长还做了大量重复计算。反向传播的高明之处在于它复用了中间结果。我给你拆开看。假设网络有三层参数分别是 W1、W2、W3损失是 L。要算 dL/dW1从数学上看它等于 dL/da3 乘 da3/da2 乘 da2/da1 乘 da1/dW1。这里 da2、da3 这些中间量是公用的。反向传播从最后一层开始算先把 dL/da3 算出来存着然后往前一层用这个结果算 dL/da2再往前用 dL/da2 算 dL/da1每一步都只需要做局部的乘法总共只需要一次“倒序遍历”。这跟前向传播一个道理——前向传播时每一层的输出都会被下一层复用所以能高效算出最终结果。反向传播就是把这个过程反过来每一层的梯度都会被前一层复用。这种“一次正向算输出、一次反向算梯度”的方式让训练百万级参数的网络成为可能。2.3 一张计算图看懂来回路径为了把“来回”的关系讲清楚我建议你在纸上画一个计算图。假设一个最简单的结构输入 x经过线性变换 z wx b再过激活函数 a σ(z)最后算损失 L (a - y)²。画出来就是一个从左到右的链条x → z → a → L前向传播就是沿着箭头从左往右x 算出 zz 算出 aa 算出 L。反向传播就是沿箭头从右往左L 对 a 求导得到 dL/daa 对 z 求导得到 da/dzz 对 w 和 b 求导得到 dz/dw x、dz/db 1。然后把路径上的导数依次相乘就得到 dL/dw 和 dL/db。这个计算图思维非常重要。实际写代码的时候PyTorch 和 TensorFlow 的底层就是维护了这样一个图前向时在每个节点上记录计算关系反向时用自动微分沿图走一遍。理解了计算图你就理解了自动求导框架的工作原理后面自己写自定义层或者排查梯度问题时都大有帮助。3. 误差反向传播的数学原理与公式推导3.1 链式法则反向传播的发动机既然说反向传播的本质是链式法则那我们就先把链式法则掰开揉碎讲明白。链式法则是微积分里的基本规则如果有 y f(u)u g(x)那么 y 对 x 的导数就是dy/dx dy/du × du/dx这个公式看起来简单但它揭示了一个重要的性质复合函数的导数可以拆成多个局部导数的乘积。神经网络正是多层复合函数——上一层的输出是下一层的输入所以整体损失对任意参数的导数都能拆成从损失到该参数路径上所有局部导数之积。用前面的奶茶店例子来说营业额对“糖浆比例”的影响 营业额对“口味”的影响 × “口味”对“糖浆比例”的影响。链条有几步就乘几步中间哪怕隔了一百层逻辑也是这个。神经网络里局部导数主要出现在两个地方线性变换层 z wx b对 w 的局部导数就是 x对 b 的局部导数就是 1对 x 的局部导数就是 w激活函数层 a σ(z)局部导数就是 σ(z)具体是多少取决于激活函数长什么样。所以反向传播的每一步本质上都是在做“上层传来的梯度 × 本层的局部导数”然后把结果继续往前传。3.2 损失函数与激活函数的导数链式法则里的“局部导数”不是随便算的它取决于你选了什么损失函数、什么激活函数。这里我挑最常见的组合来拆解。先看损失函数。最常用的是均方误差MSE和交叉熵Cross Entropy。以 MSE 为例L (a - y)²它对自己的输入 a 求导是dL/da 2(a - y)注意这个导数有个很直观的意义预测值比真实值大导数为正比真实值小导数为负。正负号直接告诉你要把 a 往哪个方向调。再看激活函数。Sigmoid 在二分类里很常见它的表达式是σ(z) 1 / (1 e^(-z))它有一个非常优美的导数性质σ(z) σ(z) × (1 - σ(z))也就是说Sigmoid 的导数可以直接用它的输出值来算不需要重新算 z。比如输出是 0.7那导数就是 0.7 × 0.3 0.21。但这里埋着一个隐患如果输出接近 0 或者 1σ(z) 会非常小接近 0。梯度在这里乘上一个小数多传几层就会变得极小这就是所谓的梯度消失。后面我会专门讲怎么处理。ReLU 的导数更简单z 0 时σ(z) 1z ≤ 0 时σ(z) 0。简洁是简洁但 z ≤ 0 的神经元梯度永远为 0一旦网络参数初始化不好或者学习率太大大量神经元可能“死掉”。这些都是选择激活函数时要综合考虑的因素。3.3 从输出层到隐藏层梯度是怎么一步步传的链式法则和局部导数都准备好了现在我们把它们串起来看梯度从输出层到隐藏层到底是怎么一步步走的。我用一个稍微具体一点的结构来说明。假设网络有两层输入 x第一层线性变换 z1 W1x b1激活得到 h σ(z1)第二层线性变换 z2 W2h b2输出 a σ(z2)损失 L (a - y)²。反向传播的路径是这样的第一步从损失出发先算出输出层的“误差信号”δ2 dL/da × da/dz2 2(a - y) × σ(z2)这个 δ 表示的是“损失对第二层加权输入 z2 的敏感性”。有了 δ2我们就能立刻算出第二层参数的梯度dL/dW2 δ2 × h dL/db2 δ2第二步把误差信号往第一层传。根据链式法则δ1 δ2 × dz2/dh × dh/dz1 δ2 × W2 × σ(z1)注意这里有个关键点dz2/dh 就是 W2。也就是说误差信号从后面一层传回前面一层时后面层的权重 W2 参与了乘法。这也是“传播”的真正含义——每一层的误差信号等于上一层传来的误差信号乘以本层权重再乘以本层激活函数的导数。得到 δ1 之后第一层参数的梯度就好算了dL/dW1 δ1 × x dL/db1 δ1你会发现整个过程的模式高度统一每层的梯度 该层误差信号 δ × 该层的输入。这也是代码里可以用循环统一实现的原因。层数多了无非就是把第二步重复多遍。我实际写代码时反而不太记那些复杂的公式就把这个模式刻在脑子里出错概率会小很多。4. 手把手算一遍一个完整的小网络梯度计算4.1 搭建一个最简网络并选好参数理论推导说再多不如亲手算一次。这个环节我带你走一个完整的、带具体数值的示例你手边有纸笔的话最好跟着画一画感受到的差别会很大。我们搭一个最简网络“一个输入、一个隐藏神经元、一个输出神经元”输入x 0.5第一层权重w1 0.4偏置 b1 -0.1第二层权重w2 0.8偏置 b2 0.2激活函数两层都用 Sigmoid损失函数均方误差MSE真实标签y 1.0为了让你后面能对照代码验证我先把这些值列成一张表。参数符号初始值输入x0.5第一层权重w10.4第一层偏置b1-0.1第二层权重w20.8第二层偏置b20.2真实标签y1.0这个网络虽小但它包含了神经网络所有的核心要素线性变换、激活、多层嵌套、损失计算。你把它的梯度算明白大网络的原理也就通了。4.2 前向传播算出预测值和损失值反向传播之前先做前向传播。这一步的目的有两个得到预测值为反向传播准备必要的中间变量。第一步算第一层的线性输出z1 w1 × x b1 0.4 × 0.5 (-0.1) 0.2 - 0.1 0.1第二步经过激活函数得到隐藏层输出h σ(z1) 1 / (1 e^(-0.1)) ≈ 0.52498这里我保留五位小数后面梯度计算会有微小差异正常现象。第三步算第二层的线性输出z2 w2 × h b2 0.8 × 0.52498 0.2 0.41998 0.2 0.61998第四步经过激活函数得到最终输出a σ(z2) 1 / (1 e^(-0.61998)) ≈ 0.65013第五步算损失L (a - y)² (0.65013 - 1)² ≈ 0.12243前向传播到此结束。记住一个诀窍前向传播时把每个中间值都记下来z1、h、z2、a 后面反向传播都要用到。你手动实现时千万别只记最终损失不然回头算梯度时又要重算一遍。为了验证你的每一步没算错我做个中间结果表步骤结果z10.1h0.52498z20.61998a0.65013L0.122434.3 反向传播逐步求出每个参数的梯度现在进入正题。反向传播从损失开始一步步往后推。我先列出需要求的所有梯度dL/dw2、dL/db2、dL/dw1、dL/db1。第一步计算输出层的误差信号 δ2。先算 dL/dadL/da 2(a - y) 2 × (0.65013 - 1) -0.69974再算 Sigmoid 的导数。前面说过 σ(z2) a(1 - a)所以da/dz2 0.65013 × (1 - 0.65013) 0.65013 × 0.34987 ≈ 0.22746于是输出层误差信号δ2 dL/da × da/dz2 (-0.69974) × 0.22746 ≈ -0.15916负号说明预测值偏高了需要把输出往小调。第二步求第二层参数的梯度。因为 z2 w2 × h b2所以dL/dw2 δ2 × h (-0.15916) × 0.52498 ≈ -0.08355 dL/db2 δ2 × 1 -0.15916这两个梯度的含义很直白w2 增大一个单位损失大约会减少 0.0836所以我们应该增大 w2 来降低损失。这正是梯度下降要做的事。第三步把误差信号传到隐藏层。这里用的就是链式法则中的“权重参与传播”δ1 δ2 × dz2/dh × dh/dz1其中 dz2/dh w2 0.8dh/dz1 σ(z1) h(1 - h) 0.52498 × 0.47502 ≈ 0.24937。所以δ1 (-0.15916) × 0.8 × 0.24937 ≈ -0.03175你可以看出误差信号从输出层传到隐藏层数值从 -0.159 变成了 -0.032变小了约五分之一这就是梯度在传播中被“稀释”的直观体现。网络一深多乘几个小于 1 的数梯度很容易趋向于 0。第四步求第一层参数的梯度。因为 z1 w1 × x b1所以dL/dw1 δ1 × x (-0.03175) × 0.5 ≈ -0.01587 dL/db1 δ1 × 1 -0.03175到这里整个反向传播就算完了。我把结果汇总一下方便你对照检查梯度数值dL/dw2-0.08355dL/db2-0.15916dL/dw1-0.01587dL/db1-0.03175整个流程你回看一遍其实就两个动作不断重复算本层的误差信号 δ然后用 δ 乘以本层的输入得到权重梯度。我写代码的时候从来都是按这个套路来很难出错。4.4 用梯度做参数更新一次完整的迭代梯度算出来不算完还要用它更新参数这才是训练中的“一步”。最基础的更新方式是随机梯度下降SGD参数_new 参数_old - 学习率 × 梯度设学习率 η 0.1我们把四个参数都更新一遍w2_new 0.8 - 0.1 × (-0.08355) 0.8 0.008355 0.80836 b2_new 0.2 - 0.1 × (-0.15916) 0.2 0.015916 0.21592 w1_new 0.4 - 0.1 × (-0.01587) 0.4 0.001587 0.40159 b1_new -0.1 - 0.1 × (-0.03175) -0.1 0.003175 -0.09683注意一个关键点所有参数更新时用的都是更新前的梯度。也就是说先用旧参数算出所有梯度再用这些梯度统一更新参数。不能算完 w2 的新值就用新值去算 w1 的梯度那样就等于在同一批数据上反复变规则训练会震荡甚至不收敛。更新完你可以重新做一次前向传播验证z1_new 0.40159 × 0.5 (-0.09683) 0.10396h_new σ(0.10396) ≈ 0.52596z2_new 0.80836 × 0.52596 0.21592 0.64108a_new σ(0.64108) ≈ 0.65495L_new (0.65495 - 1)² ≈ 0.11905损失从 0.12243 降到了 0.11905确实变小了说明这次更新方向是对的。虽然只降了一点点但神经网络训练就是这么一步一步磨出来的。这也解释了为什么训练需要那么多轮迭代——每一步的改进都很微小但方向正确积累起来就非常可观。你可以试着改变学习率比如 η 0.5重新走一遍会发现损失降得更多但如果把 η 设得过大比如 η 5更新后的参数很可能导致损失反而变大这就是“步子太大扯到蛋”的直观体现。这个手算的体会比单纯看公式有用得多。5. 实操中的硬功夫如何检查与调试反向传播5.1 数值梯度检查验证你的反向传播对不对我见过很多初学者兴致勃勃地手写了一个反向传播结果训练死活不收敛。问题往往不是出在模型结构上而是梯度算错了。怎么快速判断梯度对不对有一个非常实用的办法数值梯度检查。数值梯度的思路很简单直接按导数的定义来算。导数是函数在某一点的切线斜率我们可以用很小的步长去近似它f(x) ≈ [f(x ε) - f(x - ε)] / (2ε)这里的 ε 一般取 1e-5 或 1e-6。用这个公式我们可以在不依赖反向传播的情况下直接估算每个参数的梯度。具体做法是对某个参数 w先让原始损失为 L0然后把 w 加上 ε重新前向传播得到 L_plus再把 w 减去 ε重新前向传播得到 L_minus最后套用上面的公式算出数值梯度和反向传播算出来的梯度对比。两者误差在 1e-6 到 1e-4 这个量级就说明反向传播实现正确。这个检查方法在框架里也很常用。PyTorch 中可以用 torch.autograd.gradcheck 来做手写实现时自己写个循环也不难。我强烈建议你在一开始搭网络时先做一次梯度检查再开始训练。别问问就是我在这上面踩过大坑——梯度符号搞反了训练不但不收敛loss 还越训越高排查了一个晚上才发现是 dL/da 的正负号写错了。数值梯度检查虽然有效但注意它只适合小网络。因为每检查一个参数就要做两次完整的前向传播大模型的参数动辄上亿根本检查不过来。实践上一般是拿一个几十个参数的小网络做检查确认逻辑对了再放大到真实模型。5.2 梯度消失与梯度爆炸反向传播的“常见病”反向传播在实际训练中最常见的两个问题就是梯度消失和梯度爆炸。只要你训过深度网络几乎一定会遇到。梯度消失是指反向传播过程中梯度越来越小传到浅层时几乎是 0导致前面的层根本得不到有效的更新。原因就是我们前面说的——每一层都要乘上激活函数的导数和权重值如果这些值的绝对值普遍小于 1多层相乘之后自然趋向于 0。以 Sigmoid 为例它的最大导数是 0.25。假如网络有十层每层的梯度都不超过 0.25十层之后梯度就缩小到原来的 0.25¹⁰约等于 9.5e-7基本上就是零了。梯度爆炸恰恰相反如果权重初始化得很大比如 w 普遍大于 1多层相乘之后梯度会指数级增长变成天文数字参数一步更新出去直接飞了。训练过程中你会看到 loss 变成 NaN或者瞬间跳到极大值大概率就是这个问题。应对这两个问题的常见手段我在实际工作中总结过一张速查表问题典型症状常见对策梯度消失loss 下降缓慢、浅层参数几乎不变用 ReLU 等激活函数替代 Sigmoid用残差连接使用 BatchNorm改用更好的初始化方法梯度爆炸loss 变成 NaN、参数更新后直接发散梯度裁剪减小学习率用合适的初始化方法加权重正则化两者兼有深层参数正常、浅层停滞使用残差网络结构适当增加 BatchNorm 层其中梯度裁剪是我在训练大模型时用得最多的手段之一。它的核心思想是给梯度设一个上限比如范数超过 1.0 就整体缩放到 1.0。这样既保留了梯度的方向又防止了爆炸。你训练 LSTM 或 Transformer 这类模型时一定要加这一步老实说基本是标配。5.3 反向传播实现中最常见的三个细节错误数值梯度检查能帮你发现梯度算错了但下面这几个细节错误很多人是察觉不到的因为它们不影响程序运行只是让训练效果变差。第一个错误是共享变量覆盖。你可能因为复用代码让两个层共享同一个权重矩阵。这时候反向传播会把两个分支的梯度累加起来如果你在更新参数前先改了其中一个梯度就被污染了。PyTorch 里用 Parameter 来管理权重共享权重时千万要小心梯度累积的语义。框架里默认就是累加的batch 训练时这个特性没问题但共享权重场景下需要用 detach 或者 clone 隔离。第二个错误是参数更新时没关梯度记录。你在做梯度检查或者正常训练时在更新参数之后又顺手做了什么操作比如记录了 loss 变化曲线结果这个操作被记录进了计算图下一次反向传播时计算图就没法释放显存越占越多。运行几次之后显存爆了你却找不到原因。正确做法是无关操作全部用 torch.no_grad() 包起来或者用 .detach() 切断连接。第三个错误是学习率与梯度量级不匹配。你以为梯度计算错了其实梯度是对的但学习率相对于梯度的量级不合适。比如梯度是 0.01学习率设成 1那参数一次就跳 0.01如果参数本身就只有 0.01 量级一次跳跃就把整个模型搞乱了。我的经验是先观察梯度的量级再反推学习率。梯度中位数在 0.01 附近学习率可以先试 0.001然后看 loss 曲线再调。6. 手写反向传播的完整代码与验证6.1 一个 40 行 Python 实现理论讲完直接上一份手写实现。这些代码没有任何深度学习框架的依赖只用 NumPy让你看到反向传播的本质就是几行加减乘除。import numpy as np def sigmoid(x): return 1 / (1 np.exp(-x)) def sigmoid_derivative(a): return a * (1 - a) # 初始化参数 x 0.5 y 1.0 w1, b1 0.4, -0.1 w2, b2 0.8, 0.2 eta 0.1 # 前向传播 z1 w1 * x b1 h sigmoid(z1) z2 w2 * h b2 a sigmoid(z2) loss 0.5 * (a - y) ** 2 # 注意这里用了 0.5是为了求导方便 print(f前向传播: a{a:.5f}, loss{loss:.5f}) # 反向传播 dL_da 2 * 0.5 * (a - y) # 等于 a - y delta2 dL_da * sigmoid_derivative(a) dL_dw2 delta2 * h dL_db2 delta2 delta1 delta2 * w2 * sigmoid_derivative(h) dL_dw1 delta1 * x dL_db1 delta1 print(f梯度: dL_dw1{dL_dw1:.5f}, dL_db1{dL_db1:.5f}, dL_dw2{dL_dw2:.5f}, dL_db2{dL_db2:.5f}) # 参数更新 w1 - eta * dL_dw1 b1 - eta * dL_db1 w2 - eta * dL_dw2 b2 - eta * dL_db2 print(f更新后: w1{w1:.5f}, b1{b1:.5f}, w2{w2:.5f}, b2{b2:.5f}) # 重新前向传播验证损失下降 z1 w1 * x b1 h sigmoid(z1) z2 w2 * h b2 a sigmoid(z2) loss 0.5 * (a - y) ** 2 print(f更新后前向传播: a{a:.5f}, loss{loss:.5f})细心的你会发现我在损失函数里加了一个 0.5。这是个小技巧因为 L 0.5(a - y)² 对 a 求导后就是 a - y少了系数 2式子更干净。虽然对于这个例子差别不大但实际代码里很多人都会这么写你看到了别奇怪。这段代码的核心就两点一是把前向传播的中间量 z1、h、z2、a 都保留下来反向传播时直接复用二是按照“误差信号 δ 乘以本层输入”的套路求梯度。整个反向传播部分加起来不到十行可见它的逻辑确实不复杂。6.2 把梯度检查代码写进去光写反向传播还不够我们还要用数值梯度来验证它对不对。下面这段代码就是在前面基础上加的梯度检查def forward(w1, b1, w2, b2): z1 w1 * x b1 h sigmoid(z1) z2 w2 * h b2 a sigmoid(z2) return 0.5 * (a - y) ** 2 def numerical_gradient(w1, b1, w2, b2, eps1e-6): grads [] for param, name in [(w1, w1), (b1, b1), (w2, w2), (b2, b2)]: orig param param eps loss_plus forward(w1, b1, w2, b2) param - 2 * eps loss_minus forward(w1, b1, w2, b2) param orig # 恢复原值 grad_num (loss_plus - loss_minus) / (2 * eps) grads.append((name, grad_num)) # 注意这里由于传参方式问题需要手动恢复 # 建议用字典或者更结构化的方式实现 return grads # 初始参数 w1, b1, w2, b2 0.4, -0.1, 0.8, 0.2 # 恢复上面的反向传播梯度 dL_da (a - y) delta2 dL_da * sigmoid_derivative(a) dL_dw2 delta2 * h dL_db2 delta2 delta1 delta2 * w2 * sigmoid_derivative(h) dL_dw1 delta1 * x dL_db1 delta1 print(数值梯度: , numerical_gradient(w1, b1, w2, b2)) print(反向梯度: , dL_dw1, dL_db1, dL_dw2, dL_db2)特别提醒一下这个代码里因为 Python 参数传递的特性数值梯度函数里的 parameter 是值传递修改不会影响外面的变量所以直接返回即可。实际项目中建议把参数包成一个字典或者列表来管理这样梯度检查写起来更优雅。这只是一份演示代码重点是理解思路数值梯度是“笨办法但绝对正确”反向传播是“聪明办法但有出错的可能”两者对不上就说明反向传播写错了。7. 从手算到框架PyTorch 里反向传播是怎么帮你代劳的7.1 autograd自动求导的原理与用法你理解了手写反向传播之后再回头看 PyTorch 里的 autograd就会觉得它一点不神秘。它就是把你手写的那套流程自动化了并且借助计算图把梯度传播做得既高效又通用。PyTorch 的核心机制是在你进行张量运算时自动记录操作历史构建一个计算图。每次调用 backward() 方法时它会从当前张量出发沿计算图反向遍历自动计算梯度然后把梯度存放到每个张量的 .grad 属性中。用代码看更直观import torch x torch.tensor(0.5, requires_gradTrue) w1 torch.tensor(0.4, requires_gradTrue) b1 torch.tensor(-0.1, requires_gradTrue) w2 torch.tensor(0.8, requires_gradTrue) b2 torch.tensor(0.2, requires_gradTrue) y torch.tensor(1.0) z1 w1 * x b1 h torch.sigmoid(z1) z2 w2 * h b2 a torch.sigmoid(z2) loss 0.5 * (a - y) ** 2 loss.backward() print(w1.grad.item(), b1.grad.item(), w2.grad.item(), b2.grad.item())这段代码输出的值跟前文手算的结果一致可能会有浮点数精度的小差异。你会发现你只需要定义前向传播然后调用一次 backward()所有梯度都有了。这正是反向传播的价值——它让“训练一个神经网络”变成了“定义网络结构 选择优化器 循环迭代”剩下的脏活累活框架全包了。但我要提醒你框架帮你算梯度不等于你不需要理解梯度。用完框架的人如果连梯度是什么都说不清一旦遇到训练异常比如梯度为 NaN、loss 不降、梯度在某个层就消失了你根本无从排查。反而那些手写过反向传播的人遇到这些问题三下五除二就能定位。7.2 优化器的工作就是在反向传播之后干一件事PyTorch 中你经常看到这样一段代码optimizer.zero_grad() loss.backward() optimizer.step()很多初学者对这三行的顺序不理解特别是为什么每次都要 zero_grad。这背后的原理其实跟我们手写梯度更新时“用更新前的梯度统一更新”是一个道理。zero_grad()把上一步留下来的梯度清零。因为 PyTorch 的梯度是累加的不清零的话上一次的梯度会叠加到这一次上面导致更新方向错乱backward()执行反向传播把本次迭代的梯度算出来放进每个参数的 .gradstep()优化器拿到梯度按照自己设定的规则SGD、Adam、RMSProp 等更新参数。顺序不能乱。如果你忘记了 zero_grad或者把 step 放在了 backward 前面训练就会出现诡异的问题。我以前有个项目loss 一直在震荡找了一天原因最后发现是 zero_grad 的位置写错了。另外提一嘴Adam 这些优化器和原始的 SGD 不只是“用学习率乘梯度”那么简单。Adam 会为每个参数维护一阶动量梯度均值和二阶动量梯度平方均值然后自适应地调整每个参数的学习率。但不管优化器多复杂它们做的事情本质上都是拿到梯度用它来更新参数。反向传播把方向算准了优化器才能迈好步子。8. 从反向传播延伸更进一步该学什么写到这里反向传播这件事本身已经讲透了。但既然你看到了这篇文章我猜你应该不满足于只懂这一个点我就多聊两句反向传播之外训练神经网络还需要补哪些知识。第一件事是掌握多种激活函数。Sigmoid 和 ReLU 各自的导数特性、优缺点你可以在实际训练中对比感受。Leaky ReLU、ELU、GELU 这些变体为什么出现它们改进了什么都是值得慢慢研究的内容。激活函数是反向传播中局部导数的主要来源之一对梯度影响极大理解它们等于理解梯度的“地形”。第二件事是理解各种优化器的差异。SGD 带着动量是什么效果Adam 和 RMSProp 在不同任务上表现有什么差别学习率调度是怎么工作的。你可以从反向传播的角度去理解它们梯度方向有了但这方向不一定是最优的优化器的职责就是在这个方向上走得更稳、更快、更不容易踩坑。第三件事是深入了解自动微分机制。PyTorch 的自动求导图有三种模式反向模式、正向模式、以及它们的组合。反向传播本质上就是反向模式自动微分。搞懂了计算图和自动微分你能很自然地理解为什么 Transformer 这类大模型可以在一次前向后高效算出所有参数梯度也能理解为什么显存不够时要用梯度检查点gradient checkpointing。最后我特别建议你亲手实现一个完整的、带反向传播的神经网络不要用框架。可以从今天这个例子开始扩展到两层网络再加上多分类的 Softmax 交叉熵损失用随机梯度下降训练在 mnist 这种小数据集上跑一圈。这个过程做完你对神经网络的理解会有一个质的飞跃——远比刷十个教程管用。我在实际项目中最大的体会是很多训练中的玄学问题比如 loss 曲线异常、梯度消失、收敛慢归根结底都是对反向传播的某个细节理解不到位。把这一关过了后面学习 CNN、RNN、Transformer会发现所有模型都在做同一件事——定义前向计算再反向传播算梯度然后更新参数。万变不离其宗。