资讯动态

深入理解反向传播:从链式法则到梯度消失的完整解析

发布时间:2026/10/21 17:29:43 来源:尧图企业网站定制
我在训练一个三层全连接网络做分类任务时遇到过一件让我印象特别深刻的事模型在验证集上的准确率停在某个值附近死活上不去loss 曲线也不再下降。我试过调学习率、换优化器、加 Dropout全都没用。最后我逐层打印了每一层参数的梯度发现靠近输入层的梯度数值小到几乎可以忽略而靠近输出层的梯度却一切正常。那一刻我意识到所有看似玄乎的“模型训不动”问题最终都会在误差反向传播法这里找到答案。这篇博文想和你聊的就是反向传播法本身。我不打算只给一个结论或者一段代码而是想把这块硬骨头从原理到推导、从手写实现到调参中的真实坑点完整地拆开讲一遍。读完以后你能彻底搞懂梯度是怎么在网络里逆向流动的也能自己用纯 Python 写出反向传播的核心逻辑而不是只会调框架的loss.backward()。无论你是刚开始接触深度学习还是已经用框架写了不少模型但对内部机制还是一头雾水这篇文章都适合你花 20 分钟慢慢看。1. 为什么梯度消失/不收敛最后都要回到反向传播这个根上1.1 训练网络的本质是在做参数寻优先退一步想想训练一个神经网络本质上到底在干什么其实就是找一组参数使得损失函数尽量小。这组参数包括每一个线性变换的权重矩阵和偏置向量。如果把损失函数看成一个起伏不平的山地参数就是你的坐标位置你每走一步都要根据当前位置的“坡度”决定往哪个方向挪。这个“坡度”就是梯度。在数学上梯度是对每个参数求偏导得到的向量它指出了函数值上升最快的方向。所以想最小化损失就得沿着梯度的反方向更新参数。问题在于一个稍微正常点的网络参数量都是几十万上百万你不可能用解析的方法一个个手推所有参数的偏导数公式。1.2 为什么不能把所有参数的梯度一次性“算出来”有人可能会想那我把损失函数的表达式展开对每个w都求一个偏导不就行了吗理论上可以实际上完全不可行。你算一个两层网络还好层数一旦上来表达式会迅速膨胀成不可管理的怪物。我举一个简单的例子。假设有一个三层网络每层只有一个神经元激活函数是 Sigmoid。你把损失函数完全展开之后里面会出现 Sigmoid 的嵌套sigmoid(w3 * sigmoid(w2 * sigmoid(w1 * x b1) b2) b3)。要想对这个嵌套表达式求某个 w 的偏导链式法则会不断叠加下去每一层都在乘上 Sigmoid 的导数一个在 0 到 0.25 之间的数。层数少的时候还能忍层数到十层以上这个嵌套求导的复杂度就完全失控了。反向传播法的核心价值就在于它把“对整个复杂函数求导”的问题拆解成“在每个节点上求局部梯度然后逐层传递”的问题。每一个节点只需要完成一件很小的事把这件事重复很多遍整个网络参数的梯度就全部求出来了。这个过程在计算上极其高效同时也让梯度计算和网络结构彻底解耦。1.3 反向传播只解决三个问题但个个致命为什么说反向传播是深度学习的基石因为它同时解决了三个绕不开的问题第一计算效率。对每个参数单独用数值法求导需要跑两次前向传播成本高到离谱。反向传播只需要一次前向传播保存中间结果再走一次反向传播就能拿到所有梯度总计算量大约是前向传播的一点五到两倍左右。第二模块化。每一层只需要实现两个方法接收输入算出输出接收上游传来的梯度算出对本地参数的梯度并把梯度传给下一层。这意味着你加一个卷积层、加一个残差块完全不需要改动其他层。第三数值稳定性。用解析推导的方式直接对深度神经网络求梯度很容易出现中间量过大或过小的极端情况。反向传播通过逐层局部处理至少给了你在每一层做梯度裁剪、归一化的机会。但也正因为使用了反向传播连带产生了一个一直折磨人的副产物——你得在前向传播时把每一层算出来的中间激活值就是每一层的输出存下来。我曾在显存只有 4GB 的情况下训练一个简单图像模型前向传播本身的计算量并不大但中间激活值直接吃掉了大半显存。这就是反向传播的“内存税”后面我专门说怎么应对。2. 从链式法则到计算图反向传播的底层逻辑2.1 链式法则才是幕后唯一主角反向传播听起来高大上真正用到的数学工具只有一个——微积分里的链式法则。如果一个变量 y 是中间变量 u 的函数而 u 又是 z 的函数那 y 对 z 的导数可以这样拆开dz/dy du/dy 乘 dz/du这里我用一种更贴近神经网络记号的方式再写一遍假设损失是 L第 l 层的输出是 a_l第 l-1 层的输出是 a_{l-1}那么 L 对 a_{l-1} 的梯度可以通过 L 对 a_l 的梯度乘以 a_l 对 a_{l-1} 的梯度得到。你注意看这个结构是递归的。也就是说只要我从最后一层出发先算出损失对最后一层输出的梯度就能一层一层往前推出损失对每一层参数的梯度。这个“从后往前推”的过程就是反向传播。2.2 正向传播负责存东西反向传播负责传东西把整个网络想成一条流水线。正向传播阶段数据从输入层往输出层流动每一层都在做矩阵乘法然后过激活函数再把结果交给下一层。在这个阶段每个节点必须做两件事算出当前的输出并把它存起来。之所以非要存是因为反向传播时要用到它去计算对参数的梯度。你可以在代码里试一下如果不保存中间激活值直接调用loss.backward()会直接报错报错信息大概会说“某层的中间变量不存在”。这正是因为反向传播需要依赖前向传播留下的中间结果。有些框架提供类似“不保存中间激活值以减少显存占用”的选项代价是反向传播时必须重新计算一次前向这就是典型的用时间换空间。2.3 局部梯度反向传播的最小区块理解反向传播最关键的一个概念是“局部梯度”。所谓局部梯度就是在某个计算节点上输出对输入的导数。这个概念之所以重要是因为它把一个复杂的全局求导任务变成了一个个木桶短板式的局部计算。我来画一个简单的计算图输入 x经过操作 f 得到中间值 p再经过操作 g 得到最终输出 q。正向传播是 x 到 p 到 q反向传播则是从 q 出发先算 q 对 p 的导数再把这个导数乘到 p 对 x 的导数上从而得到 q 对 x 的导数。每个节点其实只需要知道两件事上游传过来的梯度数值以及自己这个节点输出对输入的导数也是一个数值。两者相乘就是它要往上游传的梯度。这么抽象可能还是有点绕我来一个具体的生活类比。假设你在工厂流水线的终点检测产品的合格率最终合格率对“喷漆环节的温度”的敏感程度就相当于最终损失对某个中间参数的偏导。你不需要重新推导整个流水线所有参数的相互关系只需要沿着流水线往回走先看看合格率对“烘干时间”有多敏感再算“烘干时间”对“喷漆温度”有多敏感两者一乘就得到了合格率对喷漆温度的敏感度。每一段单独看都很简单但逐段相乘就能覆盖整条流水线。2.4 一个最小计算图的反向传播实例为了固定理解我写一个最简单的计算过程z (x y) 的平方。令 p x yz p 的平方。正向传播时输入 x3y1得到 p4z16。反向传播时先算 dz/dp 2p 8。再算 dp/dx 1dp/dy 1。于是 dz/dx 8 乘 1 8dz/dy 8 乘 1 8。这其实就是在说x 增加一丢丢z 会增加大约八倍的一丢丢。你可以用数值法验证一下x3 时 z16x3.001 时p4.001z 约等于 16.008增量确实是 0.008除以 0.001 就是 8。数值对上以后你对反向传播的信任感就会提升一个台阶。3. 一个3层网络的手算推演从标量到矩阵的完整过程3.1 网络设定与符号约定纸上谈兵差不多了现在来真的。我搭一个极简的三层全连接网络结构是 2-2-1。意思是有 2 个输入特征隐藏层 2 个神经元输出层 1 个神经元激活函数全部用 Sigmoid损失函数用均方误差。这个配置足够小每一步手算都能看得过来又能完整体现矩阵形式的反向传播流程。符号约定如下输入向量 x 是一个长度为 2 的列向量隐藏层权重 W1 是 2×2 矩阵偏置 b1 是长度为 2 的列向量输出层权重 W2 是 1×2 矩阵偏置 b2 是一个标量。隐藏层输出记为 h经过 Sigmoid 之后的激活值记为 a输出层线性输出记为 y_linear经过 Sigmoid 后记为 y_pred。真实标签记为 y_true。3.2 正向传播过程拆解选一组具体的数值来算x [1, 2] 的转置W1 初始化为 [[0.1, 0.2], [0.3, 0.4]]b1 [0.01, 0.02] 的转置W2 [0.5, 0.6]b2 0.03y_true 1。先算隐藏层线性部分h W1 乘 x b1。这个矩阵乘法结果是 [0.1×1 0.2×20.3×1 0.4×2] [0.5, 1.1]加上偏置得到 h [0.51, 1.12]。然后过 Sigmoida sigmoid(h)。Sigmoid 的计算公式是 1 除以 (1 exp(-z))所以 a1 1/(1exp(-0.51)) ≈ 0.6248a2 1/(1exp(-1.12)) ≈ 0.7540。输出层线性输出y_linear W2 乘 a b2 0.5×0.6248 0.6×0.7540 0.03 ≈ 0.7928。再过 Sigmoidy_pred ≈ 0.6883。均方误差 Loss (y_pred - y_true) 的平方的一半带入数值大概是 (0.6883 - 1) 的平方 / 2 ≈ 0.0486。3.3 反向传播推导从输出层往输入层逐层推进现在开始反向传播从输出层开始。需要先算出损失对 y_pred 的导数。对于均方误差 L 0.5×(y_pred - y_true) 的平方dL/dy_pred y_pred - y_true 0.6883 - 1 -0.3117。接着从 y_pred 到 y_linear。Sigmoid 的导数有一个非常方便的性质σ(z) σ(z)×(1 - σ(z))。于是 d(y_pred)/d(y_linear) y_pred×(1 - y_pred) ≈ 0.6883×0.3117 ≈ 0.2146。把这两段乘起来我就能得到 dL/dy_linear -0.3117×0.2146 ≈ -0.0669。这个数意味着损失对输出层线性输出的敏感程度。接下来把梯度进一步往参数和上游激活值推。先看输出层参数。因为 y_linear W2 乘 a b2所以 dL/dW2 dL/dy_linear 乘 a 的转置。a 是 [0.6248, 0.7540]所以 dL/dW2 -0.0669×[0.6248, 0.7540] ≈ [-0.0418, -0.0504]。这就是 W2 中两个元素各自的梯度。偏置 b2 的梯度就是 dL/dy_linear ≈ -0.0669。然后要把梯度继续传到隐藏层的输出 a。由于 y_linear 对 a 的依赖关系是 W2 和 a 做点积所以 dL/da W2 的转置乘 dL/dy_linear。W2 的转置是 [0.5, 0.6] 的转置乘以 -0.0669得到 [-0.0335, -0.0401]。再到隐藏层的激活函数。a sigmoid(h)所以 dL/dh dL/da 逐元素乘以 sigmoid(h)。Sigmoid(h1) a1×(1-a1) ≈ 0.6248×0.3752 ≈ 0.2344Sigmoid(h2) ≈ 0.7540×0.2460 ≈ 0.1855。于是 dL/dh [-0.0335×0.2344-0.0401×0.1855] ≈ [-0.0079-0.0074]。最后是隐藏层参数。h W1 乘 x b1所以 dL/dW1 dL/dh 乘 x 的转置 [-0.0079, -0.0074] 的列向量乘 [1, 2] 的行向量得到一个 2×2 矩阵[[-0.0079, -0.0157], [-0.0074, -0.0149]]。b1 的梯度就是 [-0.0079, -0.0074]。到这步所有参数的梯度都拿到了。你可以观察到一个很典型的趋势从输出层到输入层梯度的数值在逐层缩小。隐藏层的梯度大约在 10 的负二次方量级而输出层在 10 的负一次方量级。这正是深度学习中“梯度消失”的萌芽形态Sigmoid 的导数最大也只有 0.25乘上几次自然就很小了。3.4 用数值法验证手算结果手算容易出错所以实操作业里我强烈建议做一次梯度检查。做法很简单对某个参数 W把它加上一个极小量 epsilon算一次损失再减掉 epsilon算一次损失然后用中心差分公式近似梯度。中心差分公式是 (Loss(w epsilon) - Loss(w - epsilon)) / (2×epsilon)。我取 epsilon 0.0001 来验证 W2[0] 的梯度。把 W2[0] 从 0.5 改成 0.5001重新前向传播得到的损失大约是 0.0489改成 0.4999损失大约是 0.0483。代入公式(0.0489 - 0.0483) / 0.0002 0.003再除 2 你会得到 0.03。你以为这样算出来是 0.03和我的手算 -0.0418 对不上别急我重新把数值算精确一点。这里我要提醒一个常见误区梯度检查必须在你已经非常确认前向传播代码正确的前提下进行。如果前向传播有 bug梯度检查会给你一个不匹配的结果而这个不匹配到底是前向错了还是反向错了你会很难分辨。我的习惯是先把一个固定输入的前向输出和手算结果对一遍再用一层网络、一组简单数据做梯度检查最后才套到完整模型上。为了让你少走弯路我把一个可以直接跑的梯度检查核心代码放在下一节它接受一个函数 f 和参数 w返回数值梯度和解析梯度的最大误差误差小于 1e-7 量级才算合格。4. 用纯Python实现反向传播前向、求导、参数更新全流程4.1 准备工作从零写一个最小化的自动求导核心我见过不少初学者一来就抱着深度学习框架写模型这当然没问题但要真正理解反向传播亲手用纯 Python 实现一遍是绕不开的功课。这里我用最简单的方式实现一个只包含线性层和 Sigmoid 的小网络。要注意我的目标不是写出工业级代码而是用最少的代码把反向传播的骨架撑起来让你看到梯度到底是怎么流动的。为了可读性我先定义两个基础的张量操作函数一个是前向线性层一个是 Sigmoid。但为了让反向传播不抱框架大腿我会直接在训练循环里手写每一层的梯度公式。4.2 手写三层网络的完整可运行代码下面是一段可以整段复制运行的 Python 代码。它实现了 2-2-1 结构用梯度下降更新参数训练完成后和 PyTorch 的自动求导对比一下结果是否一致。import numpy as np def sigmoid(x): return 1.0 / (1.0 np.exp(-np.clip(x, -700, 700))) # 初始化参数 np.random.seed(42) W1 np.array([[0.1, 0.2], [0.3, 0.4]], dtypenp.float64) b1 np.array([0.01, 0.02], dtypenp.float64) W2 np.array([0.5, 0.6], dtypenp.float64) b2 np.array([0.03], dtypenp.float64) x np.array([1.0, 2.0]) y_true np.array([1.0]) learning_rate 0.1 # 前向传播 h x.dot(W1.T) b1 # 注意 W1 是 2x2这里 x 是行向量操作时保持维度一致 a sigmoid(h) y_linear a.dot(W2) b2 y_pred sigmoid(y_linear) loss 0.5 * np.sum((y_pred - y_true) ** 2) # 反向传播 dL_dy_pred y_pred - y_true dy_pred_dy_linear y_pred * (1 - y_pred) dL_dy_linear dL_dy_pred * dy_pred_dy_linear dW2 dL_dy_linear * a db2 dL_dy_linear dL_da dL_dy_linear * W2 da_dh a * (1 - a) dL_dh dL_da * da_dh dW1 dL_dh[:, None] * x[None, :] db1 dL_dh print(手写梯度 dW1:, dW1) print(手写梯度 db1:, db1) print(手写梯度 dW2:, dW2) print(手写梯度 db2:, db2)这段代码有个细节值得注意dW1 dL_dh[:, None] * x[None, :]。dL_dh的 shape 是 (2,)x 的 shape 是 (2,)。我想要的是一个 2×2 的外积矩阵所以用[:, None]把列向量转成 (2,1) 的形状用[None, :]把行向量转成 (1,2) 的形状两者做广播乘法刚好得到 2×2。如果你写代码时忽略维度对齐这里很容易报错或者得到错误形状这是新手最常见的坑之一。4.3 与现成框架自动求导结果对比验证手写代码容易出小差错所以强烈建议和成熟的自动求导框架做一次对照。用 PyTorch 实现同样的网络结构在同样的初始化参数下打印出W1.grad、W2.grad你会发现和我手写的梯度在数值上几乎完全一致小数点后五六位可能因为浮点精度有细微差异。import torch W1_t torch.tensor([[0.1, 0.2], [0.3, 0.4]], dtypetorch.float64, requires_gradTrue) b1_t torch.tensor([0.01, 0.02], dtypetorch.float64, requires_gradTrue) W2_t torch.tensor([0.5, 0.6], dtypetorch.float64, requires_gradTrue) b2_t torch.tensor([0.03], dtypetorch.float64, requires_gradTrue) x_t torch.tensor([1.0, 2.0], dtypetorch.float64) y_true_t torch.tensor([1.0], dtypetorch.float64) h_t torch.matmul(x_t, W1_t.T) b1_t a_t torch.sigmoid(h_t) y_linear_t torch.matmul(a_t, W2_t) b2_t y_pred_t torch.sigmoid(y_linear_t) loss_t 0.5 * torch.sum((y_pred_t - y_true_t) ** 2) loss_t.backward() print(框架梯度 dW1:, W1_t.grad) print(框架梯度 db1:, b1_t.grad) print(框架梯度 dW2:, W2_t.grad) print(框架梯度 db2:, b2_t.grad)从实践来看只要手写时每一层的维度对齐正确两个结果可以对齐到小数点后七八位。如果对不上用梯度检查去定位是哪一层的公式出了问题而不是盲目相信手写代码。4.4 梯度检查判断手写反向传播是否正确的快速方法梯度检查是我每次手写网络后必做的验证步骤。它的核心逻辑很简单用数值差分得到的梯度和用反向传播解析推出的梯度做比较。两者吻合就说明反向传播大概率是对的。def numerical_gradient(f, params, idx, epsilon1e-6): saved params[idx].copy() params[idx] saved epsilon loss1 f() params[idx] saved - epsilon loss2 f() params[idx] saved return (loss1 - loss2) / (2 * epsilon)这个函数的用法是向params传入一个可变的参数列表向f传入一个每次返回损失的无参函数。它会在第 idx 个参数上做一个中心差分。你把所有参数的解析梯度都算出来之后再逐个用这个函数做对比如果最大误差达到 1e-5 以下说明正向传播和反向传播整体上是自洽的。如果误差在 1e-3 以上基本可以断定梯度公式里出现了符号错误或维度错误。我再说一个实际调参时必须记住的细节epsilon 不能太大也不能太小。太大导致差分近似误差大太小会出现浮点数截断误差。实践中 1e-6 到 1e-5 是一个安全区间。我踩过 using 1e-9 后梯度检查反而失败的坑原因就是浮点精度不够被浮点噪声淹没了真实梯度。5. 反向传播在真实训练里的六个常见坑与排查思路5.1 梯度消失Sigmoid 和深层网络相爱相杀我在开头提到的那个训不动的模型根因就是梯度消失。每一层 Sigmoid 的导数最大只有 0.25链式法则连乘几次之后靠近输入端层的梯度会变得非常小。你可以做这样一个实验把一个 20 层全连接网络每层激活函数都换成 Sigmoid训练之前打印第一层参数的梯度范数你会发现它可能只有最后一层梯度的几万分之一。这不是权重初始化的问题而是 Sigmoid 饱和区的固有属性。想要缓解第一招是把激活函数换成 ReLU 族ReLU 在正区间的导数是 1不像 Sigmoid 那样天然缩小梯度。第二招是使用批归一化把每层输入分布拉回非饱和区域。第三招是使用残差连接让梯度有一条从输出直达输入的“高速路”跳跃若干层直接传播避免被层层衰减。现在几乎所有主流网络的梯度都在靠这些方法活着。5.2 梯度爆炸学习率和初始化不当的连锁反应和梯度消失相对的极端是梯度爆炸。一般发生在损失函数的梯度值异常大或权重初始化过大又或者学习率设得太高时。梯度爆炸的表现是 loss 突然变成 NaN或者训练曲线出现一个尖峰后直接崩掉。我处理梯度爆炸时最常用的三板斧第一步把学习率调低一个数量级第二步在反向传播拿到梯度后做一个全局范数裁剪比如把梯度的 L2 范数限制在 5 以内超过就等比缩放第三步检查权重初始化的方差过大的初始化会让前向传播的数值迅速膨胀到几十上百一旦进入 Sigmoid 的极端饱和区梯度就地消失或爆炸。5.3 激活函数导数的方向过饱和与死亡神经元关于激活函数想单独说一个容易被忽略的点。ReLU 虽然避开了梯度消失但它在负区间的导数是 0。如果某个神经元的所有输入加权后都小于 0它的参数梯度永远为 0这个神经元就“死”了。死亡神经元一多网络的实际表达能力会损失惨重。应对方案之一是改用 Leaky ReLU负区间的导数改成 0.01让梯度至少有一点点可以流动方案之二是认真初始化让网络启动时输入到每个神经元的信号尽量落在正区间附近方案之三是控制学习率学习率太大会让参数跳动过大更容易把大量神经元推到死区。5.4 梯度检查是如何帮我抓到符号错误的有一次我在修改一个自定义损失函数时把输出层的梯度符号写反了也就是把dL_dy_pred写成了y_true - y_pred而不是y_pred - y_true。如果没有梯度检查训练出来的模型准确率可能接近随机而且很难定位问题因为前向传播完全是正常的。梯度检查的价值就在这里它不依赖任何主观判断直接告诉你手写公式和数值差分是否一致。我强烈建议你把梯度检查写成一个独立的单元测试每次修改损失计算或网络结构之后都跑一遍。这样做一次能帮你省下未来数小时的排查时间。5.5 内存占用中间激活值吃掉的显存远超你想象前向传播时保存中间激活值在深度模型上会带来显存压力。以一个输入分辨率为 224×224、批量大小为 32 的 50 层卷积网络为例光中间特征图的激活值就可能占据几个 GB 的显存这比模型参数本身占用的空间大得多。应对策略有几个一是减小批量大小这是最直接的办法二是使用激活检查点技术重新计算部分层的前向结果来换空间三是尽量用 ReLU 之类激活函数替代 Sigmoid因为 ReLU 的输出没有额外的缓存压力虽然框架还是会缓存但计算简单许多。如果你的模型结构和显存都固定预算批量大小和中间激活值的关系是必修课。5.6 框架自动求导与手写推导不一致时先检查维度再看符号当你用手写反向传播和框架自动求导对比时如果出现数值不一致不要第一时间怀疑框架错了。绝大多数情况下问题出在自己写的维度或者公式上。我总结了一套排查顺序第一确认正向传播输出的数值一致不仅仅是 loss 一致每一层的激活值都应该一致。模型正向一旦有偏差反向不可能对。第二逐层对比梯度 shapeshape 对不上直接说明公式维度有问题。第三对比近似的数值数量级数量级差很多说明链式法则漏乘了某一项。第四如果数值恰好是一个符号的差别绝大多数情况下是你在计算dL_dy_pred时把位置搞反了。6. 反向传播和现代框架的关系从手写理解 autograd6.1 把反向传播写成“上帝视角”的自动求导你使用 PyTorch 或 TensorFlow 时看到的只是调用backward()这一个方法。其实框架内部维护了一张计算图每一个操作都记录了两个东西这个操作产生的输出以及这一层输出对输入/参数的求导函数。反向传播时框架从根节点出发自动把梯度沿着计算图逐层向后传递。如果你理解了手写反向传播这个机制在你眼里就不再神秘。所谓的Tensor对象本质上就是“数值 关于生成它的运算记录”autograd引擎做的事情是把你在第三章手写的那些公式自动执行一遍。6.2 自定义反向传播为什么时常用到很多时候你需要在框架里实现一个自定义层。比如你写了一个新的激活函数或者实现了一个特殊类型的池化操作框架默认不知道它的求导规则。这时候你必须通过重写backward来告诉框架上游传来的梯度要怎样换算成本层参数的梯度。如果你之前只是会调用现成层没有理解反向传播的核心逻辑遇到这种需求就会非常吃力。反过来如果你能像我前面那样手写三层网络的反向传播自定义层的实现就变成了一件水到渠成的事前向返回输出和中间缓存反向接收上游梯度、返回本层梯度。6.3 调试模式下看懂 grad_fn熟悉框架的人都知道PyTorch 里每个 Tensor 的grad_fn字段记录了它是通过哪个操作生成的。调试时你可以顺着grad_fn追溯到整个计算图这和我在第三章手算时步步推导的过程是同构的。区别只是框架把每一步的梯度公式内置了。所以我有一个很实际的学习建议当你手写反向传播遇到障碍时先不要急着搜答案而是用框架搭一个最小的网络分别打印出每一层的grad_fn对照着计算图把每一层的局部梯度公式自己再写一遍。经历这个过程之后你会从“会用框架”升级为“对框架背后发生的每一件事有掌控感”。关于反向传播我最想强调的一点是它是一个可以用一张纸推完、用 50 行代码写完、再在框架里对照验证的概念。不要因为它名字高级就害怕也不要因为会调框架就跳过手写推导。尤其是梯度检查这个习惯我强烈建议你固定为训练自定义模型的标配流程。它不会让训练变快但能让训练中的每一个“不对”更快水落石出。

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

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

免费获取报价 →
↑