手推反向传播与NumPy实现:从梯度推导到工程实践
2026/9/15 17:34:09 网站建设 项目流程

上周帮一个学弟调他写的第一个神经网络,他用 PyTorch 搭了个三分类模型,损失函数写得不对,但自己完全没察觉。我问他:这一层反向传播回传的梯度应该是多少?他愣了一下,说“框架会自动算”。这个场景我见了太多次,带新人、面候选人,大多数人的状态是:框架用得很溜,但一旦问到梯度从哪来、为什么这么算,就卡住了。要么背不出 sigmoid 的导数,要么说不清链式法则到底链在哪,要么觉得“反正有自动求导,会调就行”。

所以我一直坚持一个观点:反向传播这件事,至少要手推一遍,再用 numpy 从零实现一遍。不是为了造轮子,而是因为只有自己推过一遍,你才知道框架里的 backward 到底在干什么,遇到 loss 不下降、梯度爆炸、维度对不上这类问题时才不会两眼一抹黑。这篇博文就做这一件事:从单个神经元开始手推梯度,到单隐层网络完整推导,最后用 numpy 逐行实现,让代码和公式一一对应。顺便把数值梯度检查、常见误区这些平时文档里不写的东西一起讲清楚。

适合谁看?正在学深度学习的初学者,面试前想补基础的人,或者用框架用久了想回头补课的同学。你要是已经能徒手推三层网络的梯度,可以重点看后面的代码实现和踩坑部分。

1. 为什么我坚持手推反向传播——别再只会调框架

1.1 从一次面试被问倒说起

有一回我面一个候选人,简历上写着熟悉 TensorFlow 和 PyTorch,做过好几个项目。我问了个很基础的问题:一个两层网络,sigmoid 激活,MSE 损失,你写一下输出层的梯度。他沉默了很久,最后说“我用框架从来没写过这个”。

这不是态度问题,是思维方式的问题。框架把 backward 封装得实在太彻底了,loss.backward()一行下来,梯度就自动算好了,中间发生了什么完全不可见。可恰恰是这种“不可见”,会在关键时刻坑人。比如你发现 loss 不降,想检查是不是梯度计算有误,如果你完全不知道 backward 内部逻辑,你连排查的入口都找不到;再比如你想自定义一个 loss,或者在不支持自动求导的环境里部署模型,不懂手动推导就干不了活。

手推一遍的价值不在于考试,而在于建立“梯度直觉”:你知道 loss 对某一层参数的梯度,由哪些量组成,哪些量在前向时已经算过,哪些量必须从后面一层传回来。有了这种直觉,你再看 PyTorch 的autograd、再看 TensorFlow 的GradientTape,就是在看一个老朋友,而不是黑盒。

1.2 手推的本质:计算图、链式法则与“缓存”思想

反向传播的数学基础其实只有一句话:链式法则。但工程实现上有个关键技巧——缓存中间结果

我常用一个生活类比:你想算出你月底余额对每天咖啡消费的敏感度。直接算,你得把“咖啡价格→每天喝几杯→月总杯数→总支出→余额”这条路从头到尾捋一遍;如果提前在每一步都记下中间值,反向算的时候只需要把相邻两个变量的导数乘起来就行。神经网络也是这个道理。前向传播时,我们把每一层的线性输出Z和激活输出A都记住,反向传播时,每一层的梯度都是“上一层传回来的梯度 × 本层局部导数”,逐层往前乘。

这就是为什么反向传播叫“反向”传播:梯度从 loss 出发,经过输出层、隐藏层,一层一层往回传。误差像流水一样从尾部流向头部,每一层只跟它的下游(更靠输出的一侧)交换信息。这个视角很重要,因为后面你会看到,隐藏层的梯度公式里永远会出现“从输出层传来的项”——这是整个算法的骨架。

手推一遍,你还会发现一个有趣的事实:反向传播的计算量跟前向传播差不多,而不是像“对每个参数分别求导”那样,参数越多算得越多。如果用数值微分对每个参数单独扰动,一个有百万参数的网络要跑百万次前向;而反向传播只需一次前向加一次反向,所有梯度一次全出来。这个效率优势,正是它能撑起现代深度学习的原因。

2. 手推全流程:单隐层网络的梯度推导拆解

2.1 符号约定与前向传播

我们拿一个最经典的配置来推:单隐层全连接网络,二分类,sigmoid 激活,交叉熵损失。别嫌它简单,所有深度网络的反向传播都是这个结构的重复堆叠,把这个吃透了,再深再复杂的网也只是重复劳动。

先约定符号。输入一个样本x,维度是D;隐藏层有H个神经元;输出层 1 个神经元。各层的参数记作:

  • W1:隐藏层权重,形状(H, D)
  • b1:隐藏层偏置,形状(H,)
  • W2:输出层权重,形状(1, H)
  • b2:输出层偏置,形状(1,)

前向传播的每一步:

z1 = W1 @ x + b1 # 隐藏层线性输出,形状 (H,) a1 = sigmoid(z1) # 隐藏层激活输出,形状 (H,) z2 = W2 @ a1 + b2 # 输出层线性输出,形状 (1,) y_pred = a2 = sigmoid(z2) # 最终预测,形状 (1,) L = -[ y * log(a2) + (1 - y) * log(1 - a2) ] # 二分类交叉熵

sigmoid 函数和它的导数先写在这里,后面推导会反复用:

sigmoid(x) = 1 / (1 + exp(-x)) sigmoid'(x) = sigmoid(x) * (1 - sigmoid(x))

注意一个巧妙的性质:如果令a = sigmoid(z),那么sigmoid'(z) = a * (1 - a)。也就是说,激活函数的导数可以直接用前向传播时已经算好的a来表示,完全不用重新算z。这就是“缓存”思想的第一个体现。

2.2 输出层的梯度:交叉熵与 sigmoid 的神奇抵消

大多数教程一上来直接给结论:交叉熵加 sigmoid 时,∂L/∂z2 = a2 - y。但这个结论怎么来的,很多同学没推过。我们完整推一遍。

先对损失函数求a2的偏导:

∂L/∂a2 = -y/a2 + (1-y)/(1-a2) = (a2 - y) / (a2 * (1 - a2))

这个分母看着有点麻烦,别急,接着对a2z2的偏导:

∂a2/∂z2 = a2 * (1 - a2)

由链式法则:

∂L/∂z2 = (∂L/∂a2) * (∂a2/∂z2) = [ (a2 - y) / (a2 * (1 - a2)) ] * [ a2 * (1 - a2) ] = a2 - y

看到了吗?两个分式完美抵消,结果就是干净利落的a2 - y。这是“交叉熵 + sigmoid”这对组合最迷人的地方:前向时越离谱的预测,反向时回传的误差信号越大。预测值是 0.9,真实标签是 1,误差是 -0.1;预测值是 0.1,真实标签是 1,误差是 -0.9,后者明显会把参数往正确方向推得更狠。这正是我们想要的性质。

有了∂L/∂z2,输出层的参数梯度就顺理成章了:

∂L/∂W2 = (∂L/∂z2) * (∂z2/∂W2) = (a2 - y) * a1ᵀ ∂L/∂b2 = (∂L/∂z2) * (∂z2/∂b2) = a2 - y

这里的a1是隐藏层输出,形状(H,),所以∂L/∂W2的形状是(1, H),正好和W2一致。每次推导完,必须检查梯度形状和参数形状是否一致,这是手推最容易出错的地方,也是后面排查问题的第一道防线。

2.3 隐藏层的梯度:误差回传的关键一步

输出层解决了,隐藏层的梯度才是“反向传播”这个名字的真正体现。我们要求∂L/∂W1,但W1出现在很靠前的位置,中间隔着z1a1z2好几层。这时候链式法则要一路链过去。

第一步,先算 loss 对隐藏层激活输出a1的偏导。a1通过W2影响z2,再由z2影响 loss:

∂L/∂a1 = (∂L/∂z2) * (∂z2/∂a1) = (a2 - y) * W2

注意W2的形状是(1, H)(a2 - y)是标量,所以乘出来形状是(H,),和a1一致。这一步就是“误差回传”的实质:输出层的误差(a2 - y)沿着W2这组连接“分配”回每个隐藏层神经元,权重越大的连接,分到的误差越大。有点像公司里总部根据各区域经理的业绩权重把总亏损分摊下去。

第二步,从a1继续往前推到z1

∂L/∂z1 = (∂L/∂a1) * (∂a1/∂z1) = (a2 - y) * W2 ⊙ sigmoid'(z1) = (a2 - y) * W2 ⊙ a1 * (1 - a1)

这里的表示逐元素相乘。为什么是逐元素乘?因为 sigmoid 是逐元素作用的,每个隐藏神经元各自独立地对自己的z1求导,互不影响。

第三步,终于到达目标参数W1b1

∂L/∂W1 = (∂L/∂z1) * (∂z1/∂W1) = [ (a2 - y) * W2 ⊙ a1 * (1 - a1) ] ⊗ xᵀ ∂L/∂b1 = ∂L/∂z1 = (a2 - y) * W2 ⊙ a1 * (1 - a1)

这里的是外积。如果你把上面的推导过程倒过来看,就会明白为什么叫“反向传播”:我们从输出端拿到误差信号(a2 - y),乘以W2回传到隐藏层,再乘以隐藏层激活函数的局部导数,得到隐藏层的误差信号,最后再乘以输入x得到梯度。每一步都在复用前一步的结果,复杂度跟层数成正比。

2.4 从单个样本到 mini-batch:向量化推导

单个样本推完了,但实际训练都是批量计算,numpy 的优势也在这。向量化的思路是:把N个样本叠成一个矩阵,让整个批次的所有样本共享同一次矩阵运算。

前向传播变成:

Z1 = X @ W1.T + b1 # 形状 (N, H) A1 = sigmoid(Z1) # 形状 (N, H) Z2 = A1 @ W2.T + b2 # 形状 (N, 1) A2 = sigmoid(Z2) # 形状 (N, 1)

这里我用了W1形状为(H, D),所以前向时要转置成(D, H)去乘。也可以反过来定义权重形状为(D, H),那样就不用转置,但反向传播里的转置会转移到另一边。没有哪种定义是绝对正确的,关键在于前后一致。我个人的习惯是权重第一维是输出维度,这样W @ x的写法更符合“输入映射到输出”的直觉。

批量场景下,损失是一个批次里所有样本损失的平均值:

L = (1/N) * Σₙ [ -yₙ * log(a₂ₙ) - (1-yₙ) * log(1-a₂ₙ) ]

因为对 batch 求了平均,反向传播时梯度也要除以N。这是初学者最容易漏掉的地方——单样本推导时没有这个1/N,一上批量就忘了除,结果梯度整体偏大,训练直接发散。

批量的反向传播公式(直接给向量化版本,你可以自己对着单样本推导验证):

dZ2 = A2 - Y # 形状 (N, 1) dW2 = (dZ2.T @ A1) / N # (1, N) @ (N, H) = (1, H) db2 = np.sum(dZ2, axis=0, keepdims=True) / N dA1 = dZ2 @ W2 # (N, 1) @ (1, H) = (N, H) dZ1 = dA1 * (A1 * (1 - A1)) # 逐元素乘,形状 (N, H) dW1 = (dZ1.T @ X) / N # (H, N) @ (N, D) = (H, D) db1 = np.sum(dZ1, axis=0, keepdims=True) / N

注意dbkeepdims=True,如果你丢掉维度,广播机制虽然大概率不报错,但后续更新参数时形状就可能错位,这是个隐藏的坑。我见过太多人在这里栽跟头,报错信息又不直观,查半天才发现是b的形状从(1, H)变成了(H,)

3. numpy 从零实现:每一行代码对应一个公式

3.1 搭建最小环境:绕开 numpy.float 的坑

先解决一个问题:AttributeError: module 'numpy' has no attribute 'float'。这个报错这几年特别常见,原因是 numpy 1.24 起移除了np.floatnp.int这些旧别名。很多老教程里写着np.float,你在新版本 numpy 里一跑就崩。解决方法很简单:用floatnp.float64,或者把 numpy 降到 1.23 以下,但我建议直接改代码,不要降版本。

安装很简单:

pip install numpy matplotlib

Ubuntu 用户如果用的是系统 Python,可能遇到权限问题,建议用虚拟环境或加--user。另外提醒一句:别在 root 下手动装系统级包,后患无穷,虚拟环境才是正解。

我们的实现只要 numpy,matplotlib 用来画损失曲线和决策边界。

3.2 前向传播代码实现

先定义激活函数和它的导数:

import numpy as np def sigmoid(x): return 1 / (1 + np.exp(-x)) def sigmoid_derivative(a): return a * (1 - a)

前向传播函数:

def forward(X, W1, b1, W2, b2): Z1 = X @ W1.T + b1 A1 = sigmoid(Z1) Z2 = A1 @ W2.T + b2 A2 = sigmoid(Z2) cache = (Z1, A1, Z2, A2) return A2, cache

cache这个词是从吴恩达课程里沿用下来的叫法,意思就是“缓存前向传播的中间结果”,反向传播时直接用,不用重新算。为什么必须缓存?你看看反向传播的公式,dZ1里要乘A1 * (1 - A1),这个A1如果不在前向时存下来,反向时就得把Z1重新算一遍 sigmoid,浪费一倍计算量。

3.3 反向传播代码实现

反向传播函数:

def backward(y, A2, cache, W2): Z1, A1, Z2, _ = cache N = X.shape[0] dZ2 = A2 - y dW2 = (dZ2.T @ A1) / N db2 = np.sum(dZ2, axis=0, keepdims=True) / N dA1 = dZ2 @ W2 dZ1 = dA1 * sigmoid_derivative(A1) dW1 = (dZ1.T @ X) / N db1 = np.sum(dZ1, axis=0, keepdims=True) / N return dW1, db1, dW2, db2

你会发现,这段代码和 2.4 节那些公式是逐行对应的。写代码的时候我建议你在每一行边上注释对应的数学表达式,比如:

dZ2 = A2 - y # dL/dz2 = a2 - y(交叉熵+sigmoid抵消后的简写) dW2 = (dZ2.T @ A1) / N # dL/dW2 = (1/N) * dZ2ᵀ @ A1

这样做的好处是,将来代码出了 bug,你能顺着注释回到数学推导里去查,而不是对着变量名瞎猜。

3.4 训练循环:观察损失下降与决策边界

造一个线性不可分的数据集,中心圆点外面套圆环:

def make_circle_data(n_outer=300, n_inner=100, seed=42): rng = np.random.RandomState(seed) theta_outer = rng.uniform(0, 2 * np.pi, n_outer) r_outer = rng.uniform(1.0, 2.0, n_outer) x1_outer = r_outer * np.cos(theta_outer) x2_outer = r_outer * np.sin(theta_outer) theta_inner = rng.uniform(0, 2 * np.pi, n_inner) r_inner = rng.uniform(0, 0.5, n_inner) x1_inner = r_inner * np.cos(theta_inner) x2_inner = r_inner * np.sin(theta_inner) X = np.vstack([ np.c_[x1_outer, x2_outer], np.c_[x1_inner, x2_inner] ]) y = np.vstack([ np.zeros((n_outer, 1)), np.ones((n_inner, 1)) ]) return X, y X, y = make_circle_data()

这个数据集有个好处:单层感知机(没有隐藏层)永远分不开它,必须靠隐藏层做非线性变换,正好用来验证我们的反向传播推得对不对。

初始化参数时有个细节,权重不能全初始化为 0。如果W1全零,隐藏层所有神经元就会完全对称,梯度也对称,模型退化成一个线性模型,隐藏层白设了。随机初始化打散对称性:

D = X.shape[1] H = 10 W1 = np.random.randn(H, D) * 0.5 b1 = np.zeros((1, H)) W2 = np.random.randn(1, H) * 0.5 b2 = np.zeros((1, 1))

* 0.5是把初始权重缩小一点。为什么?sigmoid 在z绝对值很大的地方梯度趋近于 0,如果初始化权重太大,z1很大,一上来就掉进饱和区,梯度消失,训练半天 loss 纹丝不动。权重初始化的尺度问题,是你遇到的第一个跟反向传播密切相关的坑

训练循环:

def binary_cross_entropy(y_true, y_pred, eps=1e-15): y_pred = np.clip(y_pred, eps, 1 - eps) return -np.mean(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)) learning_rate = 0.5 epochs = 3000 loss_history = [] for epoch in range(epochs): A2, cache = forward(X, W1, b1, W2, b2) loss = binary_cross_entropy(y, A2) loss_history.append(loss) dW1, db1, dW2, db2 = backward(y, A2, cache, W2) W1 -= learning_rate * dW1 b1 -= learning_rate * db1 W2 -= learning_rate * dW2 b2 -= learning_rate * db2 if epoch % 500 == 0: print(f"epoch {epoch}, loss = {loss:.6f}")

跑完之后你会看到 loss 一路下降,最后能分得开圆环和中心圆点。我自己第一次在 numpy 里跑通这个的时候,最直观的感受是:原来框架里那个optimizer.step(),本质上就是这里的一行W -= learning_rate * dW

4. 数值梯度检查:怎么证明你推对了

4.1 数值梯度的原理与实现

手推的公式用起来没问题,但怎么证明它没推错?有一个几乎全体深度学习从业者都在用、但很少有人写进博客里的方法:数值梯度检查。原理就是导数的定义:

∂L/∂w ≈ ( L(w + ε) - L(w - ε) ) / (2ε)

这叫中心差分,误差是O(ε²),比单侧差分(L(w+ε) - L(w))/ε精度高得多。取ε = 1e-7时,精度足够验证解析梯度了。

实现也很直接:把每个参数逐个扰动一下,重新算一遍前向和损失,算出数值梯度,再跟反向传播的解析梯度比一比。代码长这样:

def numerical_gradient(loss_fn, params, eps=1e-7): grads = [] for param in params: grad = np.zeros_like(param) it = np.nditer(param, flags=['multi_index']) while not it.finished: idx = it.multi_index old_val = param[idx] param[idx] = old_val + eps loss_plus = loss_fn() param[idx] = old_val - eps loss_minus = loss_fn() param[idx] = old_val grad[idx] = (loss_plus - loss_minus) / (2 * eps) it.iternext() grads.append(grad) return grads

loss_fn是一个闭包,内部用当前参数做前向传播并返回损失:

def make_loss_fn(X, y, W1, b1, W2, b2): def loss_fn(): A2, _ = forward(X, W1, b1, W2, b2) return binary_cross_entropy(y, A2) return loss_fn loss_fn = make_loss_fn(X, y, W1, b1, W2, b2) num_grads = numerical_gradient(loss_fn, [W1, b1, W2, b2]) ana_grads = backward(y, A2, cache, W2) for name, num_g, ana_g in zip(['W1', 'b1', 'W2', 'b2'], num_grads, ana_grads): diff = np.abs(num_g - ana_g).max() print(f"{name}: max abs diff = {diff:.6e}")

判断标准不是看绝对误差,而是看相对误差。一般来说,max(|num - ana|) / max(|num|, |ana|) < 1e-5就说明解析梯度没问题。如果数量级在1e-3左右,说明有轻微 bug;如果完全对不上,那基本是维度、公式或符号的问题。

注意一点:梯度检查要在小模型、小数据上做。因为数值梯度要对每个参数跑一次前向,参数多、数据多时非常慢,调试期先截取 5 个样本、隐藏层设 3 个神经元就够了。

4.2 我踩过的坑:梯度检查为什么不通过

梯度检查不通过,最常见的几个原因,我挨个踩过,列出来供你对照排查。

第一个坑:忘掉损失里的1/N。单个样本推导没有平均项,批量实现时忘了除以样本数,导致解析梯度比数值梯度整体大了 N 倍。症状是比值恒定在 N 左右,非常典型。

第二个坑:np.sum丢掉维度db2如果你不写keepdims=True,形状变成(H,),参数更新时b2 -= learning_rate * db2靠广播勉强能跑,但数值梯度是按(1, H)形状做的,两者比较时形状不匹配,结果一团乱。

第三个坑:ReLU 在 0 点不可导。如果你隐藏层用的是 ReLU,恰好某个神经元的前向输入落在负数区,导数直接是 0,数值梯度在 0 点附近会跟解析梯度差很多。这不是你推错了,而是 ReLU 在 0 点本来就没有导数。调试阶段建议先用 sigmoid 或 tanh,梯度检查通过后再换 ReLU。

第四个坑:eps选得太小。浮点数精度有限,eps = 1e-10loss_plusloss_minus的差会被舍入误差淹没,数值梯度全是噪声。1e-7是个安全值,别去挑战极限。

我自己的调试流程是:先只检查W2b2,通过了再看W1b1;每层单独验证,哪个不对修哪个,别一口气全查。

5. 反向传播的经典误区与实战心得

5.1 反向传播能解决局部最小值问题吗

这是个经常被搜到的问题,直接给答案:不能,反向传播跟局部最小值没有直接关系。反向传播做且只做一件事——高效计算梯度。它解决的是“怎么算梯度”,而局部最小值问题是“算出来梯度之后往哪走、能走到哪”,那是优化器(SGD、Adam、动量)和损失曲面形状的事。

打个比方:反向传播是给你一台高精度体重秤,局部最小值是你减肥过程中可能卡住的平台期。秤再准,也不能保证你一定能突破平台期;但你连秤都没有,就只能靠猜。所以反向传播的意义在于“梯度可靠”,优化能不能收敛到好的解,还得看初始化、学习率、优化器这些因素。实际训练中你遇到的所谓“loss 不降”,大部分根本不是困在局部最小值,而是学习率太大导致震荡、太小导致龟速、特征没做归一化、梯度消失等等。别一遇到不收敛就怪局部最小值,先检查学习率和数据预处理。

5.2 激活函数如何影响梯度流动

回到我们的推导,隐藏层梯度是(a2 - y) * W2 ⊙ a1 * (1 - a1),最后面的a1 * (1 - a1)就是 sigmoid 导数的身影。这个项的最大值是0.25(当a1 = 0.5时),一旦隐藏层输出接近 0 或 1,这个值趋近于 0,梯度就消失了。这就是经典的梯度消失:层数一深,每层都乘一个小于 1 的因子,梯度指数级衰减,前几层几乎学不到东西。

所以现代的隐藏层基本都用 ReLU:它在正半轴的导数是 1,不会把梯度越乘越小,梯度能顺畅地流回浅层。ReLU 的问题是负半轴导数为 0,一旦某个神经元长期输出负值,它就“死”了,再也没梯度。所以很多人会用 Leaky ReLU 或 PReLU 做变体。

但注意,输出层依然推荐 sigmoid(二分类)或 softmax(多分类),并且配交叉熵损失。为什么?因为我们在 2.2 节推过,这对组合的梯度简化为a2 - y,压根不需要乘 sigmoid 的导数,误差信号直接、干净。如果你输出层用 MSE 加 sigmoid,梯度变成(a2 - y) * a2 * (1 - a2),预测极端时又多乘一个接近 0 的项,学习速度会明显变慢,这就是“交叉熵和 sigmoid 是天生一对”的数学解释。

5.3 维度不匹配的排查流程

最后一个实用的技能:反向传播报错或结果不对时,怎么快速定位维度问题。我的流程是固定的。

第一步,把每层的形状写死在注释里。比如X (N, D)W1 (H, D)Z1 (N, H)。写清楚之后,前向和反向的每一行,你都手算一遍结果的形状。

第二步,在关键位置打印形状。numpy 的@运算对形状要求苛刻,矩阵乘法的内维度必须相等,报错时会明确告诉你 shape 信息。逐层打印Z1.shapeA1.shapedZ1.shape这些中间结果,对照你注释里写的期望形状,立马能看出是哪一步算错了。

第三步,检查转置是否成对出现。这是最普遍的坑,我列个对照表:

计算项正确写法常见错误写法
dW2(dZ2.T @ A1) / N(A1.T @ dZ2)(形状和 W2 对不上)
dW1(dZ1.T @ X) / N(X.T @ dZ1)(形状变成 (D,H))
dbnp.sum(dZ, axis=0, keepdims=True) / Nnp.sum(dZ, axis=1)(沿错轴求和)
dA1dZ2 @ W2W2.T @ dZ2(形状对不上)

本质上,任何一层梯度的形状必须能和该层参数形状完美对齐dW2必须和W2同形状,db2必须和b2同形状,这个约束能过滤掉 90% 的实现错误。

还有个小技巧:如果你用的是 PyTorch,可以拿它的autograd结果和你的 numpy 实现对比,误差在1e-6以内就说明你的手工反向传播和自动求导算的是同一件事。我第一次做这个对比时,那种“两个完全不同的实现算出完全一致的梯度”的感觉,比训练一个模型还有成就感。

手推加 numpy 实现这个过程,说到底就是在“把公式变成直觉”。我个人的建议是:推完单隐层之后,再推一个带 ReLU 和 softmax 的多分类版本,或者给网络加一层隐藏层,你会看到误差回传的模式完全一致,只是层数变多了。等到你哪一天不用翻笔记,就能在白板上写出任意一层的dWdb,反向传播这关就算真正过了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询