ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

反向传播算法全解:从链式法则到矩阵推导与代码实现

反向传播算法全解:从链式法则到矩阵推导与代码实现

1. 从“黑盒”到“白盒”:为什么我们必须亲手推导反向传播

如果你用过TensorFlow、PyTorch这些框架,搭建一个神经网络可能只需要几行代码,调用一个loss.backward(),梯度就自动算好了。这太方便了,方便到我们常常忘了问:这些梯度到底是怎么来的?模型参数又是如何被精确调整的?

这就是反向传播算法,现代深度学习的基石。但很多人对它的理解,停留在“链式法则”四个字上,或者看过几个示意图,感觉懂了,一动手就懵。更常见的情况是,面试官让你手推一个两层网络的BP,你卡在了矩阵维度对不上,或者符号混乱。

我经历过这个阶段。早期看教程,总觉得反向传播像魔法:前向算出损失,魔法般地,每个参数就知道自己该往哪个方向、移动多少。直到有一次,我需要为一个非标准结构的自定义层手动实现梯度计算,对着公式发呆了半天,才发现之前的“懂”是假的。从那时起,我强迫自己用最原始的方式——纸笔推导——把整个过程走通。走通之后,再看任何复杂的网络结构,心里都有了一张清晰的“计算图”,问题在哪,一目了然。

所以,这篇文章的目的不是让你“知道”反向传播,而是让你能“亲手推导”出它。我们会从一个最简单的两层全连接网络开始,用标量形式一步步算,感受每个导数的物理意义。然后,我们会把它升级到向量和矩阵形式,这才是实际代码中使用的样子。最后,我们会讨论几个推导时最容易卡壳的“魔鬼细节”,比如偏置项的梯度、矩阵求导的布局约定,以及激活函数求导的易错点。我的目标是,看完这一篇,你能关上文章,在白纸上独立地、正确地把整个推导过程写出来。

2. 预备知识:你需要带上的“数学行囊”

推导反向传播不需要高深的数学,但有几样工具必须装进你的背包,并且知道怎么用。如果你对其中任何一项感到模糊,我强烈建议在这里多花几分钟,否则后面会步步维艰。

2.1 核心工具:链式法则的多维面孔

链式法则是反向传播的灵魂。对于标量函数,我们都知道:如果z = f(y),y = g(x),那么dz/dx = (dz/dy) * (dy/dx)

但在神经网络里,我们面对的是多维情况。这里有两种主要的链式法则形式:

  1. 标量对向量的链式法则:这是最常见、最重要的形式。损失函数L是一个标量(一个数字),而我们要对权重向量w或中间变量z求导。假设L = f(a),a = g(z),z = h(w),其中a是标量,z是向量,w是向量。那么梯度∂L/∂w是一个向量,其计算为:∂L/∂w = (∂a/∂w) * (∂L/∂a) = (∂z/∂w) * (∂a/∂z) * (∂L/∂a)注意,∂z/∂w是一个雅可比矩阵(后面会讲),∂a/∂z是一个行向量(梯度),∂L/∂a是一个标量。实际上,更常用的写法是从后往前,利用上游梯度:∂L/∂z = (∂L/∂a) * (∂a/∂z)(这里∂L/∂a是标量,∂a/∂z是行向量,结果∂L/∂z是行向量)∂L/∂w = (∂L/∂z) * (∂z/∂w)(这里∂L/∂z是行向量,∂z/∂w是矩阵,结果∂L/∂w是行向量)

  2. 向量对向量的链式法则(雅可比矩阵):当中间变量也是向量时,链式法则表现为雅可比矩阵的连乘。如果y = f(u),u = g(x),其中y, u, x均为向量,则:∂y/∂x = (∂y/∂u) * (∂u/∂x)这里∂y/∂u∂u/∂x都是雅可比矩阵。在反向传播中,我们更多是计算标量损失对参数的梯度,所以这种形式通常隐含在计算过程中,我们更关注**梯度(标量对向量的导数)**的传递。

关键理解:反向传播中,我们总是在计算标量损失L某个参数或激活值的梯度。这个梯度指明了该参数应该如何微小变化才能降低损失。

2.2 矩阵求导与布局约定:混乱的根源

这是推导中最容易出错的地方。矩阵求导有多种“布局约定”(Layout Convention),主要分为分子布局分母布局。简单说,就是结果矩阵的形状,是由分子(求导对象)的形状决定,还是由分母(被求导变量)的形状决定。

在神经网络和深度学习框架中,通常采用一种混合但一致的约定,我称之为“梯度约定”:

  • 标量y对向量x的导数∂y/∂x:结果是一个行向量,形状与x的转置(1, n)相同。这是因为在梯度下降更新w = w - η * ∂L/∂w时,我们希望∂L/∂w的形状和w完全一致,方便做减法。
  • 向量y对向量x的导数∂y/∂x:结果是雅可比矩阵,其中∂y_i/∂x_j位于第i行第j列。即y的元素变化对x的元素变化的敏感度矩阵。

一个必须记住的黄金法则:在反向传播的每一步,你都要非常清楚当前正在计算的梯度∂L/∂A形状。它的形状必须与A本身完全相同。如果形状对不上,99% 是因为链式法则相乘时顺序或转置搞错了。

2.3 激活函数求导:非线性之钥

激活函数(如Sigmoid, Tanh, ReLU)的导数,是反向传播路径上的“收费站”。每个神经元在反向传播时,都需要乘上其激活函数的导数。

  • Sigmoid:σ(z) = 1 / (1 + e^{-z})其导数非常优美:σ'(z) = σ(z) * (1 - σ(z))注意:在前向传播时我们已经计算了a = σ(z),因此反向传播时可以直接用a来计算导数a * (1 - a),无需重新计算σ(z),这是一个重要的优化技巧。
  • Tanh:tanh(z) = (e^z - e^{-z}) / (e^z + e^{-z})导数:tanh'(z) = 1 - tanh^2(z)。同样,可以利用前向传播的结果。
  • ReLU:ReLU(z) = max(0, z)导数:ReLU'(z) = 1 if z > 0 else 0。这是一个分段函数,实现时通常用一个掩码(mask)记录前向传播中哪些神经元被激活(z>0)。

一个实战心得:在推导时,把激活函数看作一个独立的层。它的反向传播规则就是:上游传来的梯度∂L/∂a,乘以该层激活函数关于其输入z的局部导数∂a/∂z,得到传递给更前一层的梯度∂L/∂z。即:∂L/∂z = (∂L/∂a) ⊙ σ'(z),其中表示逐元素相乘(Hadamard product)。

3. 从零开始:一个两层网络的标量推导

让我们搭建一个最简单的全连接神经网络,并用标量形式推导。这是理解本质的最佳方式。

  • 网络结构:输入层(1个神经元x),隐藏层(1个神经元),输出层(1个神经元ŷ)。为简化,省略偏置b
  • 前向传播
    1. 隐藏层输入:z1 = w1 * x
    2. 隐藏层输出:a1 = σ(z1),其中σ是Sigmoid函数。
    3. 输出层输入:z2 = w2 * a1
    4. 输出层输出(即预测值):ŷ = a2 = σ(z2)。这里我们让输出层也用Sigmoid,假设是做二分类。
  • 损失函数:采用均方误差(MSE),L = 1/2 * (y - ŷ)^2,其中y是真实标签。

我们的目标是求损失L对两个权重w1w2的梯度∂L/∂w1∂L/∂w2

反向传播推导(标量版)

我们从损失函数开始,一步步往回(反向)计算梯度。

第1步:计算损失对输出ŷ的梯度∂L/∂ŷ = ∂[1/2*(y-ŷ)^2]/∂ŷ = -(y - ŷ)这个很好理解,预测值ŷ离真实值y越远,损失对它的梯度绝对值越大,且方向是使ŷy靠近。

第2步:计算∂L/∂w2w2只通过z2影响ŷ,再影响L。根据链式法则:∂L/∂w2 = (∂L/∂ŷ) * (∂ŷ/∂z2) * (∂z2/∂w2)

  1. ∂L/∂ŷ = -(y - ŷ)(上一步已求)
  2. ∂ŷ/∂z2 = σ'(z2) = ŷ * (1 - ŷ)(Sigmoid导数公式,用前向结果ŷ计算)
  3. ∂z2/∂w2 = a1(因为z2 = w2 * a1,对w2求导得a1) 所以,∂L/∂w2 = -(y - ŷ) * ŷ * (1 - ŷ) * a1

第3步:计算∂L/∂a1a1影响z2,进而影响ŷL。我们需要这个梯度,因为它要继续向前传播。∂L/∂a1 = (∂L/∂ŷ) * (∂ŷ/∂z2) * (∂z2/∂a1)

  1. ∂L/∂ŷ∂ŷ/∂z2同上。
  2. ∂z2/∂a1 = w2(因为z2 = w2 * a1,对a1求导得w2) 所以,∂L/∂a1 = -(y - ŷ) * ŷ * (1 - ŷ) * w2注意观察:计算∂L/∂w2时我们乘了a1,计算∂L/∂a1时我们乘了w2。这体现了权值和激活值在梯度传播中的对称性。

第4步:计算∂L/∂w1现在我们已经有了∂L/∂a1,可以继续向前求∂L/∂w1∂L/∂w1 = (∂L/∂a1) * (∂a1/∂z1) * (∂z1/∂w1)

  1. ∂L/∂a1上一步已求。
  2. ∂a1/∂z1 = σ'(z1) = a1 * (1 - a1)(隐藏层Sigmoid的导数)
  3. ∂z1/∂w1 = x(因为z1 = w1 * x) 所以,∂L/∂w1 = [-(y - ŷ) * ŷ * (1 - ŷ) * w2] * [a1 * (1 - a1)] * x

推导完成!我们得到了:

  • ∂L/∂w2 = -(y - ŷ) * ŷ * (1 - ŷ) * a1
  • ∂L/∂w1 = -(y - ŷ) * ŷ * (1 - ŷ) * w2 * a1 * (1 - a1) * x

这个标量推导清晰地展示了梯度是如何从损失函数L一层层“反向传播”到每一个权重w的。每一步都是简单的链式法则应用。你会发现,在反向传播路径上,每个节点(如a1,z2)都需要计算一个局部梯度∂L/∂[该节点],并将其传递给前驱节点∂L/∂w1的表达式比∂L/∂w2更长,因为它离损失函数更远,需要经过更多的链式法则。

4. 升级到实战:向量化与矩阵形式推导

标量推导帮助我们理解了原理,但实际的神经网络处理的是批量数据,神经元也是多个。我们必须使用向量和矩阵,利用线性代数的并行计算能力。现在,我们将网络扩展为更通用的形态:

  • 网络结构:输入层x(形状n_in x 1),隐藏层有n_hid个神经元,输出层有n_out个神经元。
  • 参数W1(形状n_hid x n_in),b1(形状n_hid x 1),W2(形状n_out x n_hid),b2(形状n_out x 1)。
  • 前向传播(单个样本)
    1. z1 = W1 * x + b1(形状n_hid x 1)
    2. a1 = σ(z1)(形状n_hid x 1)
    3. z2 = W2 * a1 + b2(形状n_out x 1)
    4. ŷ = a2 = σ(z2)(形状n_out x 1)
  • 损失函数:为了一般性,我们使用交叉熵损失(用于分类),但推导逻辑与MSE相似。L是标量。

我们的目标是求∂L/∂W2,∂L/∂b2,∂L/∂W1,∂L/∂b1。这里的关键是牢记我们的“梯度约定”:梯度∂L/∂W的形状必须与W相同。

反向传播推导(矩阵版)

我们采用从后往前的顺序,并定义每个节点的“误差信号”或“上游梯度”。

第1步:计算输出层的误差信号δ2定义δ2 = ∂L/∂z2。这是损失对输出层加权输入z2的梯度。为什么对z2求导而不是直接对W2求导?因为z2是激活函数的输入,从这里开始计算更规整。δ2 = ∂L/∂z2 = (∂L/∂a2) ⊙ (∂a2/∂z2)

  1. ∂L/∂a2:取决于损失函数。对于MSE,∂L/∂a2 = -(y - a2)。对于交叉熵+Sigmoid,有一个非常简洁的结果:∂L/∂a2 = a2 - y(这是一个很重要的结论,可以单独推导,这里先接受它)。其形状为(n_out x 1)
  2. ∂a2/∂z2:这是激活函数σ的导数,是对z2的每个元素分别求导,所以结果是一个对角矩阵吗?不,在向量化计算中,我们通常进行逐元素乘法。因为a2 = σ(z2)是逐元素的,所以∂a2/∂z2是一个由σ'(z2)组成的向量,记作σ'(z2)。形状也是(n_out x 1)。 因此,δ2 = (a2 - y) ⊙ σ'(z2)。这里是逐元素乘。δ2的形状是(n_out x 1)

第2步:计算∂L/∂W2∂L/∂b2现在我们有δ2,可以计算对第二层参数的梯度了。

  • ∂L/∂W2:根据链式法则,∂L/∂W2 = δ2 * (∂z2/∂W2)。但z2 = W2 * a1 + b2W2是一个矩阵,∂z2/∂W2是一个三维张量?直接求导很麻烦。这里有一个技巧:考虑W2的单个元素W2_{ij},它只影响z2的第i个元素。所以∂L/∂W2_{ij} = δ2_i * a1_j。把所有的i, j组合起来,你会发现:∂L/∂W2 = δ2 * a1^T请仔细验证维度:δ2(n_out x 1)a1^T(1 x n_hid),它们的矩阵乘法结果是(n_out x n_hid),与W2的形状完全一致!这就是矩阵求导在深度学习中的常用结论:参数矩阵W的梯度,等于下一层的误差信号δ乘以前一层激活值a的转置。
  • ∂L/∂b2:同理,b2的每个元素b2_i只影响z2_i。所以∂L/∂b2_i = δ2_i。因此:∂L/∂b2 = δ2其形状为(n_out x 1),与b2一致。偏置项的梯度就是其所在层的误差信号δ

第3步:反向传播误差信号到隐藏层δ1现在我们需要计算隐藏层的误差信号δ1 = ∂L/∂z1δ1 = ∂L/∂z1 = (∂L/∂a1) ⊙ (∂a1/∂z1)

  1. 先求∂L/∂a1a1影响z2,而z2 = W2 * a1 + b2。所以∂L/∂a1 = W2^T * δ2。维度检查:W2^T(n_hid x n_out)δ2(n_out x 1),结果∂L/∂a1(n_hid x 1)这揭示了反向传播的另一个关键:前一层的梯度,是后一层权重矩阵的转置乘以后一层的误差信号。你可以把它理解为误差信号沿着权重连接“反向流动”。
  2. ∂a1/∂z1 = σ'(z1),形状(n_hid x 1)。 因此,δ1 = (W2^T * δ2) ⊙ σ'(z1)。形状为(n_hid x 1)

第4步:计算∂L/∂W1∂L/∂b1有了δ1,计算第一层参数的梯度和第二层如出一辙。

  • ∂L/∂W1 = δ1 * x^T。维度:δ1(n_hid x 1)x^T(1 x n_in),结果是(n_hid x n_in),与W1一致。
  • ∂L/∂b1 = δ1。形状(n_hid x 1),与b1一致。

矩阵形式总结: 对于一个L层的全连接网络(记输入为a0),其通用反向传播公式如下(对于第l层):

  1. 前向:z_l = W_l * a_{l-1} + b_la_l = σ(z_l)
  2. 反向:
    • 输出层Lδ_L = (∂L/∂a_L) ⊙ σ'(z_L)
    • 对于l = L-1, ..., 1δ_l = (W_{l+1}^T * δ_{l+1}) ⊙ σ'(z_l)
    • 参数梯度:∂L/∂W_l = δ_l * a_{l-1}^T∂L/∂b_l = δ_l

这个公式非常优美且统一,是任何深度学习框架实现自动微分的核心。它告诉我们,反向传播本质上就是误差信号δ的层层反向传递,每传递到一层,就利用该层的本地信息(前向传播时保存的激活值a和激活函数导数σ'(z))以及连接权重W,计算出该层参数的梯度和传递给前一层的误差信号。

5. 推导中的“魔鬼细节”与避坑指南

理论公式看起来干净,但自己动手推导时,总会遇到一些让人抓狂的细节。下面是我在多次推导和教学中总结的几个最容易出错的地方。

5.1 维度对齐:永远的检查点

这是新手推导时出错的重灾区。记住一个铁律:每次进行矩阵乘法或计算梯度后,立即检查结果的维度是否与预期一致。

  • ∂L/∂W的维度:必须等于W的维度。公式δ * a_{prev}^T保证了这一点。如果你写成了a_{prev} * δ^T,维度就反了。
  • δ的维度δ_l必须等于z_l的维度,也就是该层的神经元个数。
  • 链式法则中的乘法顺序:在δ_l = (W_{l+1}^T * δ_{l+1}) ⊙ σ'(z_l)中,必须是W_{l+1}^T左乘δ_{l+1}。如果顺序反了,维度不匹配,物理意义也不对(误差是从后往前传的)。

一个实用的检查方法:为每个变量假设一个具体的维度。例如,设输入x(100, 1)W1(50, 100),那么z1就是(50, 1)。然后一步步推导下去,确保所有中间变量的维度都合理。

5.2 偏置项b的梯度:为什么是δ本身?

很多人在推导时会对∂L/∂b感到困惑。从公式z = W*a + b看,b是直接加到z上的。所以对于b的每一个元素b_i,有∂z_i/∂b_i = 1,且∂z_j/∂b_i = 0 (j≠i)。因此,∂L/∂b_i = (∂L/∂z_i) * (∂z_i/∂b_i) = δ_i * 1 = δ_i。整个向量b的梯度就是向量δ

一个常见的错误:试图对b求导时考虑它对其他z_j的影响,或者错误地认为∂z/∂b是一个单位矩阵。记住,b的每个元素是独立地加到对应的z元素上的,所以其梯度就是对应的误差信号。

5.3 激活函数求导的实践技巧

前向传播时,我们计算了a = σ(z)并保存了za。反向传播时,我们需要σ'(z)

  • Sigmoid/Tanh:最好保存前向传播的激活值a。因为σ'(z) = a * (1 - a)tanh'(z) = 1 - a^2。这样只需要一次计算,避免了重复计算指数函数,既快又准。
  • ReLU:保存一个掩码(mask)mask = (z > 0)。反向传播时,ReLU'(z) = mask。在Python中,这通常是一个布尔型或0/1的数组。
  • Softmax(配合交叉熵损失):这是一个特例,但极其重要。在分类任务中,输出层常用Softmax,损失用交叉熵。它们组合在一起时,导数有非常简洁的形式。推导稍复杂,但结论是:∂L/∂z_L = a_L - y(其中y是one-hot编码的真实标签)。这是一个需要记住的结论,它比分别求Softmax导数和交叉熵导数再相乘要简单得多。如果你在推导多分类网络时,输出层的δ_L变得非常复杂,很可能是因为你没有利用这个组合求导的简化结果。

5.4 批量处理(Batch)下的梯度计算

上面推导的是单个样本。实际训练中,我们使用一个批次(Batch)的N个样本。

  • 前向传播:输入X的形状变为(n_in, N)。每一列是一个样本。Z1 = W1 * X + b1(这里b1会通过广播加到每一列)。A1 = σ(Z1),形状(n_hid, N)。以此类推。
  • 反向传播:损失L现在是N个样本损失的平均值。所以我们的δ也是针对每个样本的。δ2的形状是(n_out, N)
  • 参数梯度计算∂L/∂W2应该如何计算?对于单个样本,我们有∂L/∂W2 = δ2 * a1^T。对于批次数据,我们需要对所有样本的梯度求平均(或求和,取决于损失函数的定义,通常是平均)。因此:∂L/∂W2 = (1/N) * (δ2 * A1^T)这里δ2(n_out, N)A1^T(N, n_hid),相乘得到(n_out, n_hid),再除以N得到平均梯度。∂L/∂b2则需要沿着样本维度(axis=1)对δ2求平均:∂L/∂b2 = (1/N) * np.sum(δ2, axis=1, keepdims=True),以保持(n_out, 1)的形状。

一个易错点:在代码实现时,如果忘记了求平均,会导致梯度是单样本情况的N倍,如果学习率不变,更新步长会巨大,很可能导致训练瞬间发散。所以,在推导和实现批量梯度时,务必明确梯度是平均梯度

6. 从公式到代码:实现一个简单的Numpy版BP

理解了所有原理和细节后,最好的巩固方式就是实现它。下面我用Python和Numpy实现一个两层网络的前向和反向传播,并加上详细的注释。

import numpy as np class TwoLayerNet: def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01): # 初始化参数 self.params = {} self.params['W1'] = weight_init_std * np.random.randn(hidden_size, input_size) self.params['b1'] = np.zeros((hidden_size, 1)) self.params['W2'] = weight_init_std * np.random.randn(output_size, hidden_size) self.params['b2'] = np.zeros((output_size, 1)) def sigmoid(self, x): return 1 / (1 + np.exp(-x)) def sigmoid_grad(self, x): # 这里输入x可以是z,也可以是a。我们约定传入前向传播计算出的a,因为a = sigmoid(z) # 所以梯度 = a * (1 - a) return x * (1 - x) def forward(self, x): # x shape: (input_size, batch_size) W1, b1, W2, b2 = self.params['W1'], self.params['b1'], self.params['W2'], self.params['b2'] # 第一层 z1 = np.dot(W1, x) + b1 # (hidden_size, batch_size) a1 = self.sigmoid(z1) # (hidden_size, batch_size) # 第二层 z2 = np.dot(W2, a1) + b2 # (output_size, batch_size) a2 = self.sigmoid(z2) # (output_size, batch_size) # 缓存中间变量,反向传播需要 self.cache = {'x': x, 'z1': z1, 'a1': a1, 'z2': z2, 'a2': a2} return a2 def loss(self, y_pred, y_true): # 使用均方误差损失,y_true是one-hot或实数标签 batch_size = y_true.shape[1] loss = 0.5 * np.sum((y_pred - y_true) ** 2) / batch_size # 平均损失 return loss def backward(self, y_true): # 从缓存中取出前向传播的结果 x, z1, a1, z2, a2 = self.cache['x'], self.cache['z1'], self.cache['a1'], self.cache['z2'], self.cache['a2'] batch_size = x.shape[1] W1, W2 = self.params['W1'], self.params['W2'] grads = {} # 用于保存梯度 # 1. 输出层误差信号 δ2 # 对于MSE损失 + Sigmoid输出:∂L/∂a2 = -(y_true - a2) = a2 - y_true # 这里我们直接用这个结果。如果是交叉熵,公式不同。 d_a2 = a2 - y_true # (output_size, batch_size) # σ'(z2) = a2 * (1 - a2) sigma_prime_z2 = self.sigmoid_grad(a2) # (output_size, batch_size) delta2 = d_a2 * sigma_prime_z2 # (output_size, batch_size),逐元素乘 # 2. 计算第二层参数的梯度 # ∂L/∂W2 = (1/batch_size) * δ2 * a1^T grads['W2'] = np.dot(delta2, a1.T) / batch_size # (output_size, hidden_size) # ∂L/∂b2 = (1/batch_size) * sum(δ2, axis=1, keepdims=True) grads['b2'] = np.sum(delta2, axis=1, keepdims=True) / batch_size # (output_size, 1) # 3. 反向传播误差信号到第一层 δ1 # ∂L/∂a1 = W2^T * δ2 d_a1 = np.dot(W2.T, delta2) # (hidden_size, batch_size) # σ'(z1) = a1 * (1 - a1) sigma_prime_z1 = self.sigmoid_grad(a1) # (hidden_size, batch_size) delta1 = d_a1 * sigma_prime_z1 # (hidden_size, batch_size),逐元素乘 # 4. 计算第一层参数的梯度 # ∂L/∂W1 = (1/batch_size) * δ1 * x^T grads['W1'] = np.dot(delta1, x.T) / batch_size # (hidden_size, input_size) # ∂L/∂b1 = (1/batch_size) * sum(δ1, axis=1, keepdims=True) grads['b1'] = np.sum(delta1, axis=1, keepdims=True) / batch_size # (hidden_size, 1) return grads def update_params(self, grads, learning_rate=0.01): # 简单的梯度下降更新 for key in self.params: self.params[key] -= learning_rate * grads[key] # 一个简单的测试 if __name__ == '__main__': np.random.seed(42) # 网络结构 net = TwoLayerNet(input_size=2, hidden_size=4, output_size=1) # 伪造一个批次数据 (2个特征,3个样本) X = np.random.randn(2, 3) Y = np.random.randn(1, 3) # 回归任务,真实值 # 前向传播 Y_pred = net.forward(X) print(f"Prediction shape: {Y_pred.shape}") loss = net.loss(Y_pred, Y) print(f"Initial loss: {loss:.4f}") # 反向传播 grads = net.backward(Y) print("Gradients keys:", grads.keys()) print(f"Grad W1 shape: {grads['W1'].shape}, should be (4, 2)") print(f"Grad b1 shape: {grads['b1'].shape}, should be (4, 1)") # 更新参数 net.update_params(grads, learning_rate=0.1) # 再次前向,损失应该下降(对于随机数据,不一定,但流程正确) Y_pred_new = net.forward(X) loss_new = net.loss(Y_pred_new, Y) print(f"Loss after one update: {loss_new:.4f}")

这段代码严格遵循了我们推导出的矩阵公式。注意几个关键点:

  1. backward函数中,所有梯度都除以了batch_size,计算的是平均梯度。
  2. 我们缓存了前向传播的za,用于反向传播计算激活函数导数和a_{prev}
  3. 参数更新是朴素的梯度下降W = W - η * ∂L/∂W

运行这段代码,如果维度没有报错,并且梯度计算正确,就证明你真正掌握了反向传播的矩阵推导。你可以尝试修改网络层数、激活函数(如换成ReLU)、损失函数(如交叉熵),来挑战更复杂的推导和实现。这才是从“看懂”到“会做”的关键一步。

返回列表