计算损失函数相对于多层神经网络中每个权重的梯度,这似乎是一项艰巨的任务。如果我们试图单独推导每个权重的导数,表达式会很快变得极其复杂且难以处理。幸好,我们可以通过计算图系统地处理这个问题。计算图是一种将任何数学表达式(例如神经网络正向传播中涉及的表达式)表示为有向图的方法。在此类图中:节点: 表示输入变量、常量或数学运算(如加法、乘法、激活函数)。边: 表示数据流。一条边从提供输出的节点指向将该输出作为输入的节点。可以将其理解为将复杂的计算分解为一系列简单、基本的操作步骤。计算的可视化一个与神经网络无关的简单例子是:表达式 $f(x, y, z) = (x + y) * z$ 可以表示为图:digraph G { rankdir=LR; node [shape=circle, style=filled, fontname="Helvetica", margin=0.1]; edge [fontname="Helvetica"]; // 输入节点 x [label="x", fillcolor="#a5d8ff"]; y [label="y", fillcolor="#a5d8ff"]; z [label="z", fillcolor="#a5d8ff"]; // 运算节点 plus_op [label="+", shape=square, fillcolor="#ffe066"]; mul_op [label="*", shape=square, fillcolor="#ffe066"]; // 中间/输出节点 q [label="q", fillcolor="#b2f2bb"]; f [label="f", fillcolor="#ffc9c9"]; // 边 x -> plus_op [label=" "]; y -> plus_op [label=" "]; plus_op -> q [label=" "]; q -> mul_op [label=" "]; z -> mul_op [label=" "]; mul_op -> f [label=" "]; }表达式 $f = (x + y) * z$ 的计算图。圆形表示变量/数值,方形表示运算。箭头指示计算的流向。这里,$x$、$y$ 和 $z$ 是输入节点。+ 节点将 $x$ 和 $y$ 作为输入,输出中间值 $q = x + y$。* 节点将 $q$ 和 $z$ 作为输入,产生最终输出 $f$。正向传播与反向传播计算图阐明了两个不同的阶段:正向传播: 我们从输入值开始,沿着边的方向在图中前进,在每个节点执行运算。这会逐步计算表达式的值,最终得到最终输出值。对于我们的例子,如果 $x=2, y=3, z=4$,正向传播将计算 $q = 2 + 3 = 5$,然后 $f = 5 * 4 = 20$。反向传播: 这是梯度计算的精妙之处。从最终输出节点开始,我们沿着与边方向相反的方向在图中向后移动。在每个节点,我们使用消耗其输出的节点计算出的梯度,来计算最终输出相对于该节点输出值的梯度。此过程主要基于微积分的链式法则。让我们追踪简单例子 $f = q * z$(其中 $q = x + y$)的反向传播过程。我们想求 $\frac{\partial f}{\partial x}$、$\frac{\partial f}{\partial y}$ 和 $\frac{\partial f}{\partial z}$。从终点开始: $f$ 相对于自身的梯度是显而易见的:$\frac{\partial f}{\partial f} = 1$。移至 * 节点: 此节点计算 $f = q * z$。我们需要它相对于输入 $q$ 和 $z$ 的梯度。局部梯度 $\frac{\partial f}{\partial q} = z$。局部梯度 $\frac{\partial f}{\partial z} = q$。 利用链式法则,流回 $q$ 的梯度是 $\frac{\partial f}{\partial f} \times \frac{\partial f}{\partial q} = 1 \times z = z$。 流回 $z$ 的梯度是 $\frac{\partial f}{\partial f} \times \frac{\partial f}{\partial z} = 1 \times q = q$。(如果 $z$ 是由其他节点计算的,此梯度会进一步回传。)移至 q 节点: 此节点从 * 节点接收到梯度 $\frac{\partial f}{\partial q} = z$。移至 + 节点: 此节点计算 $q = x + y$。我们需要局部梯度:$\frac{\partial q}{\partial x} = 1$。$\frac{\partial q}{\partial y} = 1$。 再次利用链式法则,我们将传入梯度 ($\frac{\partial f}{\partial q}$) 乘以局部梯度:流回 $x$ 的梯度是 $\frac{\partial f}{\partial q} \times \frac{\partial q}{\partial x} = z \times 1 = z$。这就是 $\frac{\partial f}{\partial x}$。流回 $y$ 的梯度是 $\frac{\partial f}{\partial q} \times \frac{\partial q}{\partial y} = z \times 1 = z$。这就是 $\frac{\partial f}{\partial y}$。所以,我们系统地得出:$\frac{\partial f}{\partial x} = z$、$\frac{\partial f}{\partial y} = z$ 和 $\frac{\partial f}{\partial z} = q = x + y$。请注意,链式法则如何使我们能够将整体梯度计算分解为每个运算节点的局部梯度。神经网络的计算图现在,设想一个神经网络。计算输出的整个过程(正向传播)可以表示为一个可能非常庞大的计算图。每个神经元的加权和、每个激活函数的应用以及最终的损失计算,都是此图中的节点。digraph G { rankdir=LR; splines=true; node [shape=circle, style=filled, fontname="Helvetica", margin=0.1]; edge [fontname="Helvetica"]; // 输入节点 x1 [label="x₁", fillcolor="#a5d8ff"]; w1 [label="w₁", fillcolor="#ced4da"]; x2 [label="x₂", fillcolor="#a5d8ff"]; w2 [label="w₂", fillcolor="#ced4da"]; b [label="b", fillcolor="#ced4da"]; // 运算节点 mul1_op [label="*", shape=square, fillcolor="#ffe066"]; mul2_op [label="*", shape=square, fillcolor="#ffe066"]; add1_op [label="+", shape=square, fillcolor="#ffe066"]; add2_op [label="+", shape=square, fillcolor="#ffe066"]; sigma_op [label="σ", shape=square, fillcolor="#fcc2d7"]; // 示例激活函数 // 中间/输出节点 p1 [label="p₁", fillcolor="#b2f2bb"]; p2 [label="p₂", fillcolor="#b2f2bb"]; s [label="s", fillcolor="#b2f2bb"]; z [label="z", fillcolor="#b2f2bb"]; a [label="a", fillcolor="#ffc9c9"]; // 神经元输出 // 边 x1 -> mul1_op; w1 -> mul1_op; x2 -> mul2_op; w2 -> mul2_op; mul1_op -> p1; mul2_op -> p2; p1 -> add1_op; p2 -> add1_op; add1_op -> s; s -> add2_op; b -> add2_op; add2_op -> z; z -> sigma_op; sigma_op -> a; }一个表示单个神经元计算的计算图:$a = \sigma(w_1x_1 + w_2x_2 + b)$。图中显示了输入变量($x_1, x_2$)、参数($w_1, w_2, b$)、中间值($p_1, p_2, s, z$)以及最终输出($a$)和运算(*、+、σ)。反向传播算法(我们将在接下来详细介绍)本质上是在此神经网络计算图上执行反向传播过程。它从最终损失值开始,通过在图结构中递归应用链式法则,高效地计算损失相对于网络中每一个参数(权重和偏置)的梯度。现代深度学习框架,如 PyTorch 和 TensorFlow,很大程度上基于此原理。它们自动构建计算图(通常在正向传播期间动态进行),然后执行反向传播以计算梯度。此过程常被称为自动微分(autodiff),它使我们不必手动推导和实现这些复杂的梯度计算。理解计算图有助于洞悉这些强大工具的内部运作方式。