在正式介绍完整的算法之前,让我们先看看如何为一个非常简单的网络手动计算单个梯度。这将帮助我们理解为什么链式法则如此重要。对于本节内容,如果您暂时无法理解也请不必担心。单个梯度计算让我们以一个具有输入 $x$、权重 $w$、偏置 $b$ 和激活函数 $\sigma$ 的单个神经元为例。损失为 $L$。计算过程是:1. 预激活:$z = wx + b$ 2. 激活:$a = \sigma(z)$ 3. 损失:$L = \text{Loss}(a, y)$为了找到损失相对于权重 $\frac{\partial L}{\partial w}$ 的梯度,我们需要看 $w$ 的微小变化如何影响损失 $L$。链式法则告诉我们可以通过将每一步的变化率相乘来找到: $$\frac{\partial L}{\partial w} = \frac{\partial L}{\partial a} \times \frac{\partial a}{\partial z} \times \frac{\partial z}{\partial w}$$ 让我们分解每一项: - $\frac{\partial L}{\partial a}$:这是神经元输出激活 $a$ 变化时,损失如何变化。这是我们第一个需要计算的东西。 - $\frac{\partial a}{\partial z}$:这是激活函数的导数,$\sigma'(z)$。它告诉我们激活值如何根据预激活值变化。 - $\frac{\partial z}{\partial w}$:这是 $wx + b$ 相对于 $w$ 的导数,它就是 $x$。因此,为了找到权重 $w$ 的梯度,我们需要计算所有这些部分并将它们相乘。对于一个多层网络,这个导数链会更长,将网络深处的参数一直连接到最终的损失。反向传播算法只是一种结构化且高效的方法,可以同时为所有参数执行这个长链式法则计算。链式法则的威力本质上,神经网络是一系列嵌套函数。一层的输出成为下一层的输入。为了找出网络内部深处的参数的变化如何影响最终损失,我们需要重复应用链式法则。考虑一个非常简单的网络计算:1. 输入 $x$ 2. 隐藏层预激活:$z^{(1)} = w^{(1)}x + b^{(1)}$ 3. 隐藏层激活:$a^{(1)} = \sigma(z^{(1)})$ (其中 $\sigma$ 是激活函数) 4. 输出层预激活:$z^{(2)} = w^{(2)}a^{(1)} + b^{(2)}$ 5. 输出层激活(预测):$a^{(2)} = \sigma(z^{(2)})$ 6. 损失:$L = \text{Loss}(a^{(2)}, y)$ (其中 $y$ 是真实标签)如果我们想找到损失 $L$ 相对于第一层中的一个权重(例如 $w^{(1)}$)的梯度,我们需要追溯其对损失的影响路径:$w^{(1)}$ 影响 $z^{(1)}$,进而影响 $a^{(1)}$,再影响 $z^{(2)}$,接着影响 $a^{(2)}$,最终影响 $L$。链式法则允许我们分解这个计算:$$\frac{\partial L}{\partial w^{(1)}} = \frac{\partial L}{\partial a^{(2)}} \times \frac{\partial a^{(2)}}{\partial z^{(2)}} \times \frac{\partial z^{(2)}}{\partial a^{(1)}} \times \frac{\partial a^{(1)}}{\partial z^{(1)}} \times \frac{\partial z^{(1)}}{\partial w^{(1)}}$$反向传播有效地组织了这一计算。它首先计算像 $\frac{\partial L}{\partial a^{(2)}}$ 和 $\frac{\partial L}{\partial z^{(2)}}$ 这样的项,然后复用它们来计算前几层的梯度。反向传播过程解释该算法分两个主要阶段工作:1. 前向传播: 输入数据逐层输入网络,每一步计算预激活值 ($z^{(l)}$) 和激活值 ($a^{(l)}$),最终得到最终输出预测 $a^{(L)}$。在此过程中,我们存储中间值(所有层 $l$ 的 $z^{(l)}$ 和 $a^{(l)}$),因为反向传播过程将需要它们。最终损失 $L$ 使用预测 $a^{(L)}$ 和真实目标 $y$ 计算。2. 反向传播: 该过程计算梯度。 * 输出层: 计算损失相对于输出层激活 $\frac{\partial L}{\partial a^{(L)}}$ 的梯度。然后,使用链式法则,计算损失相对于输出层预激活 $z^{(L)}$ 的梯度: $$\delta^{(L)} = \frac{\partial L}{\partial z^{(L)}} = \frac{\partial L}{\partial a^{(L)}} \frac{\partial a^{(L)}}{\partial z^{(L)}} = \frac{\partial L}{\partial a^{(L)}} \sigma'(z^{(L)})$$ 其中,$\sigma'(z^{(L)})$ 是输出层使用的激活函数在 $z^{(L)}$ 处的导数。这一项 $\delta^{(L)}$ 表示输出层预激活处的“误差信号”。 现在,计算输出层权重 $W^{(L)}$ 和偏置 $b^{(L)}$ 的梯度: $$\frac{\partial L}{\partial W^{(L)}} = \frac{\partial L}{\partial z^{(L)}} \frac{\partial z^{(L)}}{\partial W^{(L)}} = \delta^{(L)} (a^{(L-1)})^T$$ $$\frac{\partial L}{\partial b^{(L)}} = \frac{\partial L}{\partial z^{(L)}} \frac{\partial z^{(L)}}{\partial b^{(L)}} = \delta^{(L)}$$ (注意:具体形式取决于我们是否使用向量/矩阵记号。$a^{(L-1)}$ 表示传入输出层的上一层激活。转置是为了确保矩阵乘法的维度匹配)。 * 隐藏层(从 $l = L-1$ 逆向到 $l=1$): 对于每个隐藏层 $l$,根据 下一层(更靠近输出层的那一层)的误差信号 $\delta^{(l+1)}$ 来计算其误差信号 $\delta^{(l)}$: $$\delta^{(l)} = \frac{\partial L}{\partial z^{(l)}} = \left( (W^{(l+1)})^T \delta^{(l+1)} \right) \odot \sigma'(z^{(l)})$$ 其中,$(W^{(l+1)})^T$ 是连接层 $l$ 和层 $l+1$ 的权重。这一步有效地将误差梯度通过网络权重反向传播。项 $\sigma'(z^{(l)})$ 是当前隐藏层 $l$ 的激活函数的导数。符号 $\odot$ 表示逐元素乘法(哈达玛积)。 一旦 $\delta^{(l)}$ 已知,就可以像输出层一样计算层 $l$ 的权重 $W^{(l)}$ 和偏置 $b^{(l)}$ 的梯度: $$\frac{\partial L}{\partial W^{(l)}} = \delta^{(l)} (a^{(l-1)})^T$$ $$\frac{\partial L}{\partial b^{(l)}} = \delta^{(l)}$$ 其中 $a^{(l-1)}$ 是传入层 $l$ 的那一层的激活(对于第一个隐藏层,这将是输入数据 $x$)。这个过程继续逆向进行,直到所有参数的梯度都已计算完毕。流程可视化我们可以将计算看作是向前流动以得到损失,然后向后流动以得到梯度。digraph G { rankdir=LR; node [shape=circle, style=filled, color="#ced4da"]; edge [color="#868e96"]; subgraph cluster_input { label = "输入层 (l=0)"; style=dashed; color="#adb5bd"; x [label="x", color="#a5d8ff"]; } subgraph cluster_hidden { label = "隐藏层 (l=1)"; style=dashed; color="#adb5bd"; z1 [label="z(1)", color="#b2f2bb"]; a1 [label="a(1)", color="#8ce99a"]; W1 [label="W(1), b(1)", shape=box, style=filled, color="#e9ecef", fixedsize=true, width=1, height=0.5]; z1 -> a1 [label=" σ(.)", color="#69db7c"]; } subgraph cluster_output { label = "输出层 (l=2)"; style=dashed; color="#adb5bd"; z2 [label="z(2)", color="#b2f2bb"]; a2 [label="a(2)", color="#8ce99a"]; W2 [label="W(2), b(2)", shape=box, style=filled, color="#e9ecef", fixedsize=true, width=1, height=0.5]; z2 -> a2 [label=" σ(.)", color="#69db7c"]; } subgraph cluster_loss { label = "损失"; style=dashed; color="#adb5bd"; L [label="L", color="#ffc9c9", shape=diamond]; } // 前向传播 x -> z1 [label=" W(1)x+b(1) ", arrowhead=vee, color="#1c7ed6"]; a1 -> z2 [label=" W(2)a(1)+b(2) ", arrowhead=vee, color="#1c7ed6"]; a2 -> L [label=" Loss(a(2), y) ", arrowhead=vee, color="#1c7ed6"]; // 参数连接 W1 -> z1 [style=invis]; W2 -> z2 [style=invis]; // 反向传播 edge [arrowhead=curve, color="#f03e3e", constraint=false]; L -> a2 [label=" ∂L/∂a(2) "]; a2 -> z2 [label=" ∂a(2)/∂z(2)=σ'(z(2)) "]; z2 -> a1 [label=" ∂z(2)/∂a(1)=W(2) "]; z2 -> W2 [label=" ∂L/∂W(2), ∂L/∂b(2) ", style=dashed]; a1 -> z1 [label=" ∂a(1)/∂z(1)=σ'(z(1)) "]; z1 -> x [label=" ∂z(1)/∂x=W(1) ", style=invis]; // 通常不需要用于参数 z1 -> W1 [label=" ∂L/∂W(1), ∂L/∂b(1) ", style=dashed]; {rank=same; x} {rank=same; W1; z1; a1} {rank=same; W2; z2; a2} {rank=same; L} }前向(蓝色箭头)和反向(红色箭头)传播过程的简化视图。前向传播计算激活值和损失。反向传播计算梯度,从损失开始并逆向传播误差信号(如 $\delta^{(l)} = \frac{\partial L}{\partial z^{(l)}}$),复用前向传播过程中计算的值($a^{(l)}$、$z^{(l)}$)和网络权重($W^{(l)}$)。参数($W, b$)的梯度则从这些误差信号中得出。为何高效?反向传播的效率主要来自两个方面:1. 计算复用: 在前向传播过程中计算的中间值($z^{(l)}$、$a^{(l)}$)会被存储并在反向传播过程中复用。更重要的是,为层 $l+1$ 计算的误差信号($\delta^{(l+1)}$)直接用于计算层 $l$ 的误差信号($\delta^{(l)}$)。这避免了冗余计算。 2. 动态规划: 它本质上使用了动态规划。通过从后向前逐层计算梯度,它构建出整个网络梯度的解,而无需独立地重新计算每个参数的影响路径。与数值估计梯度相比(数值估计梯度对于 每个参数 至少需要一次额外的前向传播),反向传播以大致相当于单次前向传播的计算时间,计算出 所有 梯度。对于拥有数百万参数的网络来说,这种差异非常巨大,使得训练深度网络变得可行。通过反向传播计算出梯度后,我们现在有了更新权重和偏置所需的方向,这将在以下部分详细说明。