资讯动态

反向传播原理与手写实现:从链式法则到梯度下降

发布时间:2026/10/17 9:46:56 来源:尧图企业网站定制
说实话反向传播这四个字劝退了多少人学过深度学习的人。很多人第一次接触神经网络被矩阵运算、激活函数、损失函数一轮轮轰炸之后好不容易弄懂了前向传播是啥意思结果一看到 backprop 的推导就直接放弃治疗。市面上讲反向传播的资料其实不少但大部分要么太数学恨不得用十页纸去证明链式法则要么太黑盒直接调框架一行loss.backward()就完事讲完你只会调参问一句“backward()到底做了什么”立刻愣住。这篇文章我想换个讲法。不堆公式不跳步骤用一个真正能跑起来的例子从“为什么必须有反向传播”开始一直讲到它的数学内核、代码实现、以及我在实际训练里踩过的那一堆坑。适合刚入门想搞懂原理的同学也适合已经会用框架但总觉得哪里没通的人。你想把“会调库”变成“真懂”这篇值得你静下心读一遍。1. 反向传播到底在“反”什么1.1 训练的终极问题找一组让损失最小的权重神经网络的训练本质上只有一个目的找到一组权重让网络在给定数据集上的损失最小。损失函数衡量的是预测值和真实值的差距比如交叉熵、均方误差。训练就是一个不断调整权重的过程而调整权重的方向信息靠的就是梯度。梯度是个数学概念。对于多元函数梯度是一个向量每个分量是该函数对某个变量的偏导数。通俗点说梯度告诉你如果你把权重朝某个方向稍微动一下损失函数会上升得最快如果把权重朝相反方向动损失会下降得最快。所以梯度下降的核心操作就是算出梯度然后让权重沿着梯度的反方向走一小步损失就会变小反复迭代直到收敛。这里的关键点是我们算的不是一个权重变量的梯度而是网络中所有权重参数的梯度。一个稍微实际一点的网络参数都是百万甚至亿级的。怎么高效地把所有参数的梯度算出来这就是反向传播要干的事。1.2 为什么朴素想法数值梯度不好使在没有反向传播这个概念之前最容易想到的求梯度方法是数值差分。拿某个权重举例我给它稍微变大一点看损失涨了多少这也算梯度的近似。公式长这样[ \frac{\partial L}{\partial w} \approx \frac{L(w \epsilon) - L(w - \epsilon)}{2\epsilon} ]这个方法确实直观而且几乎不需要任何数学推导改一个数跑一次前向记下损失就能得到一个参数的梯度。但问题在于每计算一个参数的一次梯度就需要额外做两次完整的前向传播。参数少的时候还可以忍参数一多计算量直接爆炸。举个例子。一个只有四层、每层256个神经元的全连接网络光参数就有几十万个。每个参数都要通过两次前向来估计梯度一轮迭代就要跑几十万次前向而一次前向本身已经是上百万次运算。这还不算后续需要几百上千轮迭代。数值梯度在实际训练里根本跑不动它只能用来做一些小规模的验证手工检查模型十几个参数时还能用。反向传播恰好解决了这个效率问题它只需一次前向传播和一次反向传播就能把所有参数的梯度全部算出来。这个能力的数学基础就是链式法则。1.3 反向传播的巧劲一次反向全量梯度为什么一次反向就能算出所有梯度核心原因是神经网络的结构本身。网络是一个复合函数数据从输入层依次经过每一层的线性变换、激活函数最后到输出层产生预测值再和真实标签计算损失。损失对每一层权重的依赖是沿着这个链路一层层传递过去的。反向传播的原理其实一句话就能概括从损失函数出发沿着网络结构倒着往回走每经过一层就利用这一层前向传播时保存下来的中间结果用链式法则算出一个梯度再继续往前传。整个过程只需要一次反向遍历就能让网络中每一个参数都获得一个梯度值。这里有一个关键点前向传播时必须保存中间结果比如每一层的输出、激活值这些在反向传播时会被反复用到。学过计组的人应该熟悉这有点像“缓存”。如果为了省内存不存中间结果反向传播就必须重新计算那计算量会暴增。所以在真实框架里还有一个经典的权衡内存换速度或者速度换内存。很多训练框架的显存优化选项本质就是在调这个取舍。我见过不少人看完反向传播推导后觉得“懂了”但让他解释为什么一次反向就能算出所有参数的梯度他就开始含糊。其实只要抓住“链式法则 复用中间结果”这十个字后面的所有细节都能串起来。2. 计算图和链式法则反向传播的数学内核2.1 前向传播就是搭了一座计算桥为了把反向传播的逻辑讲清楚最好先引入“计算图”这个概念。计算图是一种有向无环图每个节点代表一个操作或一个变量边代表数据的流向。前向传播过程就是在计算图上从输入节点出发沿边依次计算每个节点值的过程。比如一个最简单的线性层输入是 x权重是 w偏置是 b那么输出就是 z w*xb。在这个计算图里w 和 x 相乘得到一个临时节点加上 b 得到 z之后再经过激活函数、损失函数最终得到损失 L。任何复杂的神经网络哪怕几十层上千层拆开看都是一堆这样的小操作连在了一起。而反向传播做的就是这个图的逆向遍历从最后面的损失节点开始沿边往回走一步一步计算每个中间变量的梯度。这个视角特别重要。因为在前向传播中每个节点只需要知道它的输入算出自己的输出而在反向传播中每个节点只需要知道“下游传来的梯度”结合自己的局部信息算出“传给上游的梯度”。这种模块化的设计让梯度的计算变得极其规整也正是现代深度学习框架能自动求导的基础。2.2 链式法则梯度怎么沿桥倒流链式法则是微积分里的基本结论复合函数求导可以拆成每一步导数的乘积。假设最终的损失函数是 L中间变量是 z而 z 又是变量 w 的函数那损失对 w 的梯度等于损失对 z 的梯度乘以 z 对 w 的导数。公式写出来就是[ \frac{\partial L}{\partial w} \frac{\partial L}{\partial z} \cdot \frac{\partial z}{\partial w} ]在神经网络里L 和 参数 w 之间往往隔着很多个中间变量所以链式法则就被不断嵌套形成了一条很长的乘积链。反向传播所做的其实就是从后往前逐步计算这条链路中每一段的局部导数然后把它们乘起来。这里要理清一个容易混淆的概念。很多人看到这个乘积链会误以为“权重越大梯度越大”。其实梯度大小还取决于每个局部导数的具体数值而局部导数普遍都是小于1的小数多层乘起来之后梯度很容易变得极小这就是后面要讲的梯度消失问题的根源。反向传播相比“一次性展开整个乘积链求导”的最大优势在于局部导数的计算是复用的。每一层的梯度计算只依赖它在反向传播中接到的上游梯度以及它自己前向传播时保存的输入和输出根本不关心更远层级的细节。这种“局部交互”让每个模块都能独立维护也让整个网络可以任意深。2.3 局部梯度每个“模块”只需管好自己局部梯度这个概念是理解反向传播工程实现的最关键一环。它的意思是每个计算节点只需关心自己的输出对输入的变化率不需要关心整个网络的其他部分。举个例子sigmoid 激活函数的导数可以仅由它的输出表示sigmoid 函数的导数是 σ(z)(1-σ(z))其中 σ(z) 就是它的输出。那意味着反向传播经过这个节点时只需要从前向传播里拿到当时的输出值就能轻松算出这条支路的局部梯度再乘以上游传来的梯度就可以继续往上一层传。这就是为什么很多框架里激活函数节点只需要缓存输出值而不需要缓存输入值。这个“模块只需管自己”的设计也让网络架构设计变得很自由。想换一种激活函数只需改这一个模块的前向计算和局部梯度函数其他部分完全不用动。深度学习框架里的自动求导本质就是每个算子都注册了前向函数和反向函数框架把反向函数串起来跑一遍而已。理解了局部梯度你会发现反向传播没有那么玄乎。它就是把那些你看不懂的导数公式拆成了一块块自己管自己的“零件”。每一个零件的数学都不难难的是搞清楚零件之间怎么传递信号。2.4 手推一个最简单的例子我不喜欢对着看起来很吓人的公式空谈。我们直接推一个最简单的小例子一个单输入、单输出、无偏置的线性层后面接一个均方误差损失。网络就是 y_hat w*x损失 L (y_hat - y)^2。前向传播先算 y_hat w*x再算 L (y_hat - y)^2反向传播从 L 开始往回走dL/dy_hat 2*(y_hat - y)dy_hat/dw x所以 dL/dw dL/dy_hat * dy_hat/dw 2*(y_hat - y) * x你看整个推导过程根本不需要网上的复杂公式只需要沿着链一步一步往回算就行。真实网络无非是把这个链变长、变量变成矩阵但逻辑完全一致。如果这一步你能完全想通反向传播的主干就算是拿到手了。3. 从小白到能跑通手写一个完整的反向传播说到这光看不练没有用。这一部分我直接用 Python 和 NumPy从零实现一个两层全连接网络用它来解决经典的异或XOR问题。这个任务非常简单但足够暴露反向传播的所有核心细节。我会把每一行代码都解释清楚保证你看完之后能把整个链条串通。3.1 网络结构与数据准备异或问题的数据是这样的输入是四个二维向量输出是一个数。当两个输入不同时输出1相同时输出0。(0, 0) - 0 (0, 1) - 1 (1, 0) - 1 (1, 1) - 0我们的网络结构是2个输入神经元4个隐藏层神经元1个输出神经元。隐藏层使用 ReLU 激活输出层使用 Sigmoid 激活损失函数用交叉熵。为什么选 4 个隐藏神经元因为异或问题本身只需要一个单隐层网络就能解决两个神经元理论上构造一条边界但为了训练稳定我习惯多给一点通道。隐藏层尝试用 ReLU 而不是 Sigmoid是考虑到梯度传播更顺畅这一点在第4部分会展开讲。3.2 前向传播代码代码第一件事是把前向传播写出来。前向传播就是把输入一层层送过去计算出预测值import numpy as np X np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) y np.array([[0], [1], [1], [0]]) np.random.seed(42) # 初始化权重和偏置 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)) # 前向传播 z1 X w1 b1 a1 np.maximum(0, z1) # ReLU激活 z2 a1 w2 b2 a2 1 / (1 np.exp(-z2)) # Sigmoid激活代码里的每一步都有对应物。z1 是线性变换输出a1 是ReLU之后的值z2 是第二层线性变换的结果a2 就是最终预测概率。注意 ReLU 的定义是 max(0, z)负值全部置零正值保留。这一操作在反向传播里也会发挥作用。3.3 反向传播手动推导现在到了重头戏手动实现反向传播。先从最终损失对输出层的梯度开始逐层往前推。交叉熵损失对 Sigmoid 输出的梯度有一个很简洁的形式。假设真实标签为 y预测概率为 a2交叉熵的导数是dL/dz2 a2 - y这个结果不是巧合而是交叉熵损失和 Sigmoid 结合之后的“链式法则约分”结果。单独对 Sigmoid 求导会得到 a2(1-a2)单独对交叉熵求导会得到 1/(a2(1-a2))相乘之后正好约成 a2 - y。这一步约分省事不少也是为什么实际工程里总把“损失函数最后一层激活”联合求导而不是分开算。接下来是损失对第二层权重 w2、偏置 b2 的梯度。由链式法则对 w2 的梯度dL/dz2 乘以上一层输出 a1对 b2 的梯度dL/dz2 直接求和矩阵写法是# 输出层梯度 dz2 a2 - y dw2 a1.T dz2 db2 np.sum(dz2, axis0, keepdimsTrue)然后把梯度继续往前传。z2 对 a1 的依赖是 w2所以 a1 收到的梯度是 dz2 w2.T。再经过 ReLU 的导数输入小于等于0的位置导数是0大于0的位置导数是1。于是隐藏层线性输出 z1 的梯度为da1 dz2 w2.T dz1 da1 * (z1 0) # ReLU的导数得到 dz1 之后第一层的权重梯度同样可以算dw1 X.T dz1 db1 np.sum(dz1, axis0, keepdimsTrue)到这里所有参数的梯度就全部算完了。注意每一行代码都对应了链式法则中的一个环节而中间用到的dz2、da1其实都是前向传播时可以自然得到的中间变量。3.4 参数更新和训练循环拿到梯度后参数更新只需要一个简单的减法。沿梯度反方向走一步步长由学习率决定learning_rate 0.5 w1 - learning_rate * dw1 b1 - learning_rate * db1 w2 - learning_rate * dw2 b2 - learning_rate * db2整个训练循环就是把前面两小节串起来反复迭代losses [] for epoch in range(10000): # 前向传播省略变量声明就行 z1 X w1 b1 a1 np.maximum(0, z1) z2 a1 w2 b2 a2 1 / (1 np.exp(-z2)) # 损失计算交叉熵 loss -np.mean(y * np.log(a2) (1 - y) * np.log(1 - a2)) losses.append(loss) # 反向传播 dz2 a2 - y dw2 a1.T dz2 db2 np.sum(dz2, axis0, keepdimsTrue) da1 dz2 w2.T dz1 da1 * (z1 0) dw1 X.T dz1 db1 np.sum(dz1, axis0, keepdimsTrue) # 更新权重 w1 - learning_rate * dw1 b1 - learning_rate * db1 w2 - learning_rate * dw2 b2 - learning_rate * db2跑完之后你可以打印最终的预测结果。训练到位的话输出会非常接近 0、1、1、0 的真实分布。比如训练一万轮之后我经常得到类似[0.002, 0.997, 0.996, 0.001]的结果。注意我这里给每个参数都单独写了更新只是为了把流程讲清楚。实际写的时候可以维护一个参数列表统一更新。而且实际框架里梯度计算和参数更新也是分离的优化器单独负责更新策略你完全不用手写这些。3.5 训练效果与验证如果你把上面的代码完整地跑一遍会发现损失曲线逐渐下降预测值越来越接近真实标签。这个过程本身就验证了反向传播的正确性如果某个梯度算错了损失的下降曲线会出现异常甚至发散。我个人的习惯是每次改完代码先看损失是否在正常下降再去看准确率。因为损失下降是梯度的直接反馈而准确率可能被阈值卡住误导判断。如果损失不降反升那我第一反应就是梯度方向有误或者学习率太大导致跨过了最优点而不是盲目改网络结构。这里还有个小技巧你可以故意把某个梯度的符号反过来训练大概率会瞬间发散。这种实验是理解“梯度到底在干啥”的最佳教学方式我推荐每个初学者都动手试一试。4. 训练中的经典翻车现场梯度问题与调试4.1 梯度消失和梯度爆炸反向传播学完之后你迟早会在实际训练中遇到两个最出名的敌人梯度消失和梯度爆炸。梯度消失通俗地说就是反向传播里的梯度信号随着网络层数加深变得越来越小最后传到浅层时几乎为零。浅层参数几乎得不到有效的更新网络学习极其缓慢。根源在链式法则的乘法效应如果每一层的局部梯度都小于1连乘几十层之后梯度指数级衰减。早期 Sigmoid 激活函数特别容易引发梯度消失因为它的导数最大也只有0.25。梯度爆炸则是反过来局部梯度大于1连乘之后梯度指数级增长参数更新量巨大训练直接发散损失飙到 nan。这个问题在某些初始化不当或者网络结构不合理的情况下特别常见。我的应对经验有三条。第一条是换激活函数ReLU 及其变体在正区间的导数恒为1能有效缓解梯度消失。第二条是合理的参数初始化避免一开始就让信号进入激活函数的饱和区。第三条是归一化手段比如批量归一化它能把每层的输入分布拉回到一个合适范围大幅增强训练的稳定性。至于梯度爆炸最直接的保命方案是梯度裁剪当梯度范数超过某个阈值时按比例把它缩回去。4.2 学习率调不好的典型症状学习率是最重要的超参数之一也是最容易掉进去的坑。大了模型振荡发散小了模型训练极慢甚至看着损失一直不降。怎么判断是不是学习率的问题一个经验之谈如果损失曲线呈锯齿状剧烈波动大概率是学习率偏高如果损失曲线平滑但像乌龟爬一样缓慢下降大概率是学习率偏低。还有一种情况是损失在初期下降后很快停滞到一个平台这可能是学习率不合适导致无法越过鞍点。具体操作上我通常会打印出每个 epoch 的损失曲线同时记录梯度范数。结合损失和梯度范数一起看比单独看一个指标好得多。学习率也不是一成不变的训练过程中往往需要手动或通过调度器调整比如 warmup 策略和余弦退火。这些在训练大模型时几乎是标配。4.3 梯度检查怀疑写错了就交给数值验证手动实现反向传播最常见的问题就是某个地方的维度不对或者某个公式的符号错了。这种错误经常会让你对着训练曲线怀疑人生。有一个非常实用的调试方法梯度检查。用数值梯度去近似解析梯度的值然后对比手写的反向传播计算出的梯度。如果两者差异很小说明反向传播的代码是正确的。数值梯度的计算方式很简单把某个参数加一个小量 eps再减去一个小量 eps分别计算损失看两个损失之差除以2倍的 eps 是多少。用公式写出来grad_numerical ≈ (L(weps) - L(w-eps)) / (2*eps)然后对比这个值和反向传播算出来的 grad_analytical。相对误差小于 1e-4 基本可以放心。强烈建议把反向传播代码写完以后第一步就做梯度检查而不是直接上去训练。如果先练出现问题时很难判断是梯度错了还是学习率不合适还是数值稳定性没处理好。先验证梯度正确再调训练参数能节省一大半的调试时间。4.4 初始化与激活函数的选择神经网络对初始权重非常敏感。如果初始权重全部为零那么所有神经元会完全对称每一层各个单元学到的东西也会一样如果初始权重过大激活值一旦落入饱和区梯度就可能消失或者爆炸。所以随机初始化必须设置合适的尺度。常用的方式包括 Xavier 初始化和 He 初始化。前者的原则是让每层的输入输出方差尽量一致适用 Sigmoid 这类激活函数后者专门针对 ReLU 家族因为 ReLU 会砍掉一半神经元需要把初始权重的方差调大一点来补偿。激活函数的选择同样直接影响反向传播。Sigmoid 在输出层作为概率输出很常见但在隐藏层已经很少用了。ReLU 的计算简单正区间导数恒为1梯度传播更好但它有个“神经元死亡”问题如果某个神经元一直输出负数它的梯度永远是0参数再也无法更新。一些改进版本比如 Leaky ReLU、ELU就是为了解决这个问题而出现的。选择激活函数的逻辑其实很简单先保证梯度能流动再来谈表达能力的优化。活下来的网络才有机会学习一上来梯度就断了的网络架构再精美也没用。5. 长期实战积累的几点心得5.1 不要急着调框架先把轮子造一遍我经常跟刚开始学深度学习的同学说先沉下心用 NumPy 写一个简单网络把反向传播完整实现一遍。不要一上来就调用框架的自动求导。原因很简单框架的backward()已经把所有的求导细节都封装好了最多秀了个 API根本体会不到梯度的流动过程。而自己手算一遍之后框架在你眼里就变成了一层透明的壳出了问题也能够定位到具体是哪一层的梯度出了问题。我见过不止一个同学模型训练 loss 变成 nan第一反应是检查数据第二反应是改网络结构兜兜转转搞了大半天最后发现是某个自定义算子的导数公式写错了。如果你自己从头实现过反向传播这种问题一眼就能看出个大概方向。5.2 从损失和梯度两个维度判断训练健康度在训练过程中你一定会遇到各种状况。我的标准判断顺序是先看损失是否在下降再看准确率是否跟随最后看梯度范数是否稳定。如果损失下降但准确率不动可能是数据标签或者类别不平衡有问题。如果损失在下降但梯度范数一直在振荡则考虑是不是学习率太高。如果损失几乎不变但要区分是梯度消失、学习率太低还是模型容量不足。这三种情况表现很像但解决办法完全不同。只有梯度的时候看“方向”有了梯度范数还能看“尺度”把两者结合着看你才真正知道网络在干什么。这是我反复强调的排查方法也是最容易被初学者忽略的。5.3 反向传播的理解深度会直接拉高你的学习天花板反向传播是一个很奇怪的知识点看起来只是微积分的一个应用但理解深度会决定你后续学什么都是事半功倍还是事倍功半。后面你去看 BN 的原理、看残差连接为什么有效、看 transformer 里的注意力矩阵是怎么求梯度的全部都要回到反向传播的基本逻辑。就拿残差连接来说它能够训练深层网络的根本原因就是因为给梯度多开了一条“高速公路”。如果没有反向传播这套理解框架残差连接在你眼里只是“把输入加回去”这样一个操作但你永远理解不了它为什么能缓解梯度消失。所以这篇标题虽然叫《从入门到入土》但真正的意思是从这一根主线出发后面还有大量算法等着你探索。反向传播不是终点反而是通往更深理解的起点。

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

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

免费获取报价 →
↑