本文章是对 Gradient Notes 的整理与简单实现。
数学部分生成:Gemini-2.5-pro, 代码部分+整理:fyerfyer
神经网络梯度计算 1. 向量化梯度 虽然计算神经网络相对于单个参数的梯度是一个很好的练习,但在实践中,这样做往往相当缓慢。相反,将所有内容保持为矩阵/向量形式会更有效率。向量化梯度的基本构建模块是雅可比矩阵 。
假设我们有一个函数 f : R n → R m f: \mathbb{R}^n \rightarrow \mathbb{R}^m f : R n → R m ,它将一个长度为 n n n 的向量映射到一个长度为 m m m 的向量:f ( x ) = [ f 1 ( x 1 , . . . , x n ) , f 2 ( x 1 , . . . , x n ) , . . . , f m ( x 1 , . . . , x n ) ] f(x) = [f_1(x_1, ..., x_n), f_2(x_1, ..., x_n), ..., f_m(x_1, ..., x_n)] f ( x ) = [ f 1 ( x 1 , ... , x n ) , f 2 ( x 1 , ... , x n ) , ... , f m ( x 1 , ... , x n )] 。那么,它的雅可比矩阵是一个 m × n m \times n m × n 的矩阵,定义如下:
∂ f ∂ x = [ ∂ f 1 ∂ x 1 ⋯ ∂ f 1 ∂ x n ⋮ ⋱ ⋮ ∂ f m ∂ x 1 ⋯ ∂ f m ∂ x n ] \frac{\partial f}{\partial x} = \begin{bmatrix} \frac{\partial f_1}{\partial x_1} & \cdots & \frac{\partial f_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial f_m}{\partial x_1} & \cdots & \frac{\partial f_m}{\partial x_n} \end{bmatrix} ∂ x ∂ f = ∂ x 1 ∂ f 1 ⋮ ∂ x 1 ∂ f m ⋯ ⋱ ⋯ ∂ x n ∂ f 1 ⋮ ∂ x n ∂ f m 也就是说,( ∂ f ∂ x ) i j = ∂ f i ∂ x j (\frac{\partial f}{\partial x})_{ij} = \frac{\partial f_i}{\partial x_j} ( ∂ x ∂ f ) ij = ∂ x j ∂ f i (这只是一个标准的非向量导数)。雅可比矩阵对我们很有用,因为我们只需通过乘以雅可比矩阵,就可以将链式法则应用于向量值函数 。
举个例子,假设我们有一个函数 f ( x ) = [ f 1 ( x ) , f 2 ( x ) ] f(x) = [f_1(x), f_2(x)] f ( x ) = [ f 1 ( x ) , f 2 ( x )] ,它将一个标量映射到一个大小为 2 的向量;还有一个函数 g ( y ) = [ g 1 ( y 1 , y 2 ) , g 2 ( y 1 , y 2 ) ] g(y) = [g_1(y_1, y_2), g_2(y_1, y_2)] g ( y ) = [ g 1 ( y 1 , y 2 ) , g 2 ( y 1 , y 2 )] ,它将一个大小为 2 的向量映射到一个大小为 2 的向量。现在,我们将它们复合起来得到 g ( f ( x ) ) = [ g 1 ( f 1 ( x ) , f 2 ( x ) ) , g 2 ( f 1 ( x ) , f 2 ( x ) ) ] g(f(x)) = [g_1(f_1(x), f_2(x)), g_2(f_1(x), f_2(x))] g ( f ( x )) = [ g 1 ( f 1 ( x ) , f 2 ( x )) , g 2 ( f 1 ( x ) , f 2 ( x ))] 。使用常规的链式法则,我们可以计算 g g g 的导数(即雅可比矩阵):
∂ g ∂ x = [ ∂ g 1 ∂ f 1 ∂ f 1 ∂ x + ∂ g 1 ∂ f 2 ∂ f 2 ∂ x ∂ g 2 ∂ f 1 ∂ f 1 ∂ x + ∂ g 2 ∂ f 2 ∂ f 2 ∂ x ] \frac{\partial g}{\partial x} = \begin{bmatrix} \frac{\partial g_1}{\partial f_1} \frac{\partial f_1}{\partial x} + \frac{\partial g_1}{\partial f_2} \frac{\partial f_2}{\partial x} \\ \frac{\partial g_2}{\partial f_1} \frac{\partial f_1}{\partial x} + \frac{\partial g_2}{\partial f_2} \frac{\partial f_2}{\partial x} \end{bmatrix} ∂ x ∂ g = [ ∂ f 1 ∂ g 1 ∂ x ∂ f 1 + ∂ f 2 ∂ g 1 ∂ x ∂ f 2 ∂ f 1 ∂ g 2 ∂ x ∂ f 1 + ∂ f 2 ∂ g 2 ∂ x ∂ f 2 ] 我们可以看到,这与乘以两个雅可比矩阵的结果是相同的 :
∂ g ∂ x = ∂ g ∂ f ∂ f ∂ x = [ ∂ g 1 ∂ f 1 ∂ g 1 ∂ f 2 ∂ g 2 ∂ f 1 ∂ g 2 ∂ f 2 ] [ ∂ f 1 ∂ x ∂ f 2 ∂ x ] \frac{\partial g}{\partial x} = \frac{\partial g}{\partial f} \frac{\partial f}{\partial x} = \begin{bmatrix} \frac{\partial g_1}{\partial f_1} & \frac{\partial g_1}{\partial f_2} \\ \frac{\partial g_2}{\partial f_1} & \frac{\partial g_2}{\partial f_2} \end{bmatrix} \begin{bmatrix} \frac{\partial f_1}{\partial x} \\ \frac{\partial f_2}{\partial x} \end{bmatrix} ∂ x ∂ g = ∂ f ∂ g ∂ x ∂ f = [ ∂ f 1 ∂ g 1 ∂ f 1 ∂ g 2 ∂ f 2 ∂ g 1 ∂ f 2 ∂ g 2 ] [ ∂ x ∂ f 1 ∂ x ∂ f 2 ] 2. 一些恒等式的计算 (1) 矩阵乘以列向量对列向量的导数 (z = W x z = Wx z = W x ,求 ∂ z ∂ x \frac{\partial z}{\partial x} ∂ x ∂ z ?)
假设 W ∈ R n × m W \in \mathbb{R}^{n \times m} W ∈ R n × m 。我们可以将 z z z 看作一个函数,它将一个 m m m 维向量映射到一个 n n n 维向量。所以它的雅可比矩阵将是 n × m n \times m n × m 。注意:
z i = ∑ k = 1 m W i k x k z_i = \sum_{k=1}^{m} W_{ik} x_k z i = ∑ k = 1 m W ik x k
所以,雅可比矩阵的一个元素 ( ∂ z ∂ x ) i j (\frac{\partial z}{\partial x})_{ij} ( ∂ x ∂ z ) ij 将是:
( ∂ z ∂ x ) i j = ∂ z i ∂ x j = ∂ ∂ x j ∑ k = 1 m W i k x k = ∑ k = 1 m W i k ∂ x k ∂ x j = W i j (\frac{\partial z}{\partial x})_{ij} = \frac{\partial z_i}{\partial x_j} = \frac{\partial}{\partial x_j} \sum_{k=1}^{m} W_{ik} x_k = \sum_{k=1}^{m} W_{ik} \frac{\partial x_k}{\partial x_j} = W_{ij} ( ∂ x ∂ z ) ij = ∂ x j ∂ z i = ∂ x j ∂ ∑ k = 1 m W ik x k = ∑ k = 1 m W ik ∂ x j ∂ x k = W ij
因为当 k = j k=j k = j 时 ∂ x k ∂ x j = 1 \frac{\partial x_k}{\partial x_j} = 1 ∂ x j ∂ x k = 1 ,否则为 0。所以我们看到:
∂ z ∂ x = W \frac{\partial z}{\partial x} = W ∂ x ∂ z = W
(2) 行向量乘以矩阵对行向量的导数 (z = x W z = xW z = x W ,求 ∂ z ∂ x \frac{\partial z}{\partial x} ∂ x ∂ z ?)
与 (1) 类似的计算表明:
∂ z ∂ x = W T \frac{\partial z}{\partial x} = W^T ∂ x ∂ z = W T
(3) 向量与自身的导数 (z = x z = x z = x ,求 ∂ z ∂ x \frac{\partial z}{\partial x} ∂ x ∂ z ?)
我们有 z i = x i z_i = x_i z i = x i 。所以:
( ∂ z ∂ x ) i j = ∂ z i ∂ x j = ∂ x i ∂ x j = { 1 if i = j 0 otherwise (\frac{\partial z}{\partial x})_{ij} = \frac{\partial z_i}{\partial x_j} = \frac{\partial x_i}{\partial x_j} = \begin{cases} 1 & \text{if } i=j \\ 0 & \text{otherwise} \end{cases} ( ∂ x ∂ z ) ij = ∂ x j ∂ z i = ∂ x j ∂ x i = { 1 0 if i = j otherwise
所以我们看到雅可比矩阵 ∂ z ∂ x \frac{\partial z}{\partial x} ∂ x ∂ z 是一个对角矩阵,其中 ( i , i ) (i, i) ( i , i ) 处的元素为 1。这正是单位矩阵:
∂ z ∂ x = I \frac{\partial z}{\partial x} = I ∂ x ∂ z = I
在应用链式法则时,这一项会消失,因为任何矩阵或向量乘以单位矩阵都不会改变。
(4) 逐元素函数应用于向量 (z = f ( x ) z = f(x) z = f ( x ) ,求 ∂ z ∂ x \frac{\partial z}{\partial x} ∂ x ∂ z ?)
由于 f f f 是逐元素应用的,我们有 z i = f ( x i ) z_i = f(x_i) z i = f ( x i ) 。所以:
( ∂ z ∂ x ) i j = ∂ z i ∂ x j = ∂ f ( x i ) ∂ x j = { f ′ ( x i ) if i = j 0 otherwise (\frac{\partial z}{\partial x})_{ij} = \frac{\partial z_i}{\partial x_j} = \frac{\partial f(x_i)}{\partial x_j} = \begin{cases} f'(x_i) & \text{if } i=j \\ 0 & \text{otherwise} \end{cases} ( ∂ x ∂ z ) ij = ∂ x j ∂ z i = ∂ x j ∂ f ( x i ) = { f ′ ( x i ) 0 if i = j otherwise
所以我们看到雅可比矩阵 ∂ z ∂ x \frac{\partial z}{\partial x} ∂ x ∂ z 是一个对角矩阵,其中 ( i , i ) (i, i) ( i , i ) 处的元素是应用于 x i x_i x i 的 f f f 的导数。我们可以写成:
∂ z ∂ x = diag ( f ′ ( x ) ) \frac{\partial z}{\partial x} = \text{diag}(f'(x)) ∂ x ∂ z = diag ( f ′ ( x ))
由于乘以一个对角矩阵等同于逐元素乘以其对角线,我们也可以在应用链式法则时写作 ∘ f ′ ( x ) \circ f'(x) ∘ f ′ ( x ) 。
(5) 矩阵乘以列向量对矩阵的导数 (z = W x , δ = ∂ J ∂ z z = Wx, \delta = \frac{\partial J}{\partial z} z = W x , δ = ∂ z ∂ J ,求 ∂ J ∂ W = ∂ J ∂ z ∂ z ∂ W \frac{\partial J}{\partial W} = \frac{\partial J}{\partial z} \frac{\partial z}{\partial W} ∂ W ∂ J = ∂ z ∂ J ∂ W ∂ z ?)
这比其他恒等式要复杂一些。在上面的问题表述中包含 δ = ∂ J ∂ z \delta = \frac{\partial J}{\partial z} δ = ∂ z ∂ J 的原因稍后会变得清晰。
首先,假设我们有一个损失函数 J J J (一个标量),并且我们正在计算它关于矩阵 W ∈ R n × m W \in \mathbb{R}^{n \times m} W ∈ R n × m 的梯度。我们可以将 J J J 看作是 W W W 的一个函数,它接受 n m nm nm 个输入(W W W 的元素)到一个输出(J J J )。这意味着雅可比矩阵 ∂ J ∂ W \frac{\partial J}{\partial W} ∂ W ∂ J 将是一个 1 × n m 1 \times nm 1 × nm 的向量。但在实践中,这种排列梯度的方式不是很有用。如果导数能在一个像下面这样的 n × m n \times m n × m 矩阵中会好得多:
∂ J ∂ W = [ ∂ J ∂ W 11 ⋯ ∂ J ∂ W 1 m ⋮ ⋱ ⋮ ∂ J ∂ W n 1 ⋯ ∂ J ∂ W n m ] \frac{\partial J}{\partial W} = \begin{bmatrix} \frac{\partial J}{\partial W_{11}} & \cdots & \frac{\partial J}{\partial W_{1m}} \\ \vdots & \ddots & \vdots \\ \frac{\partial J}{\partial W_{n1}} & \cdots & \frac{\partial J}{\partial W_{nm}} \end{bmatrix} ∂ W ∂ J = ∂ W 11 ∂ J ⋮ ∂ W n 1 ∂ J ⋯ ⋱ ⋯ ∂ W 1 m ∂ J ⋮ ∂ W nm ∂ J 由于这个矩阵的形状与 W W W 相同,我们在进行梯度下降时,只需从 W W W 中减去它(乘以学习率)。因此(在稍微滥用符号的情况下),让我们找到这个矩阵作为 ∂ J ∂ W \frac{\partial J}{\partial W} ∂ W ∂ J 。
当计算 ∂ z ∂ W \frac{\partial z}{\partial W} ∂ W ∂ z 时,这种排列梯度的方式会变得复杂。与 J J J 不同,z z z 是一个向量。因此,如果我们试图将梯度排列得像 ∂ J ∂ W \frac{\partial J}{\partial W} ∂ W ∂ J 那样,∂ z ∂ W \frac{\partial z}{\partial W} ∂ W ∂ z 将会是一个 n × m × n n \times m \times n n × m × n 的张量!幸运的是,我们可以通过转而计算关于单个权重 W i j W_{ij} W ij 的梯度来避免这个问题。∂ z ∂ W i j \frac{\partial z}{\partial W_{ij}} ∂ W ij ∂ z 只是一个向量,处理起来容易得多。我们有:
z k = ∑ l = 1 m W k l x l z_k = \sum_{l=1}^{m} W_{kl} x_l z k = ∑ l = 1 m W k l x l ∂ z k ∂ W i j = ∂ ∂ W i j ∑ l = 1 m W k l x l \frac{\partial z_k}{\partial W_{ij}} = \frac{\partial}{\partial W_{ij}} \sum_{l=1}^{m} W_{kl} x_l ∂ W ij ∂ z k = ∂ W ij ∂ ∑ l = 1 m W k l x l
注意 ∂ W k l ∂ W i j = 1 \frac{\partial W_{kl}}{\partial W_{ij}} = 1 ∂ W ij ∂ W k l = 1 如果 k = i k=i k = i 且 l = j l=j l = j ,否则为 0。因此,如果 k ≠ i k \neq i k = i ,和中的所有项都为零,梯度也为零。否则,和中唯一非零的元素是当 l = j l=j l = j 时,我们得到 x j x_j x j 。因此我们发现 ∂ z k ∂ W i j = x j \frac{\partial z_k}{\partial W_{ij}} = x_j ∂ W ij ∂ z k = x j 如果 k = i k=i k = i ,否则为 0。另一种写法是:
∂ z ∂ W i j = [ 0 ⋮ x j ⋮ 0 ] ← i -th element \frac{\partial z}{\partial W_{ij}} = \begin{bmatrix} 0 \\ \vdots \\ x_j \\ \vdots \\ 0 \end{bmatrix} \leftarrow i \text{-th element} ∂ W ij ∂ z = 0 ⋮ x j ⋮ 0 ← i -th element 现在让我们计算 ∂ J ∂ W i j \frac{\partial J}{\partial W_{ij}} ∂ W ij ∂ J :
∂ J ∂ W i j = ∂ J ∂ z ∂ z ∂ W i j = δ ∂ z ∂ W i j = ∑ k = 1 m δ k ∂ z k ∂ W i j = δ i x j \frac{\partial J}{\partial W_{ij}} = \frac{\partial J}{\partial z} \frac{\partial z}{\partial W_{ij}} = \delta \frac{\partial z}{\partial W_{ij}} = \sum_{k=1}^{m} \delta_k \frac{\partial z_k}{\partial W_{ij}} = \delta_i x_j ∂ W ij ∂ J = ∂ z ∂ J ∂ W ij ∂ z = δ ∂ W ij ∂ z = ∑ k = 1 m δ k ∂ W ij ∂ z k = δ i x j
(和中唯一的非零项是 δ i ∂ z i ∂ W i j \delta_i \frac{\partial z_i}{\partial W_{ij}} δ i ∂ W ij ∂ z i )。为了得到 ∂ J ∂ W \frac{\partial J}{\partial W} ∂ W ∂ J ,我们需要一个矩阵,其中 ( i , j ) (i, j) ( i , j ) 处的元素是 δ i x j \delta_i x_j δ i x j 。这个矩阵等于外积:
∂ J ∂ W = δ x T \frac{\partial J}{\partial W} = \delta x^T ∂ W ∂ J = δ x T
(6) 行向量乘以矩阵对矩阵的导数 (z = x W , δ = ∂ J ∂ z z = xW, \delta = \frac{\partial J}{\partial z} z = x W , δ = ∂ z ∂ J ,求 ∂ J ∂ W = δ ∂ z ∂ W \frac{\partial J}{\partial W} = \delta \frac{\partial z}{\partial W} ∂ W ∂ J = δ ∂ W ∂ z ?)
与 (5) 类似的计算表明:
∂ J ∂ W = x T δ \frac{\partial J}{\partial W} = x^T \delta ∂ W ∂ J = x T δ
(7) 交叉熵损失对 logits 的导数 (y ^ = softmax ( θ ) , J = CE ( y , y ^ ) \hat{y} = \text{softmax}(\theta), J = \text{CE}(y, \hat{y}) y ^ = softmax ( θ ) , J = CE ( y , y ^ ) ,求 ∂ J ∂ θ \frac{\partial J}{\partial \theta} ∂ θ ∂ J ?)
梯度是:
∂ J ∂ θ = y ^ − y \frac{\partial J}{\partial \theta} = \hat{y} - y ∂ θ ∂ J = y ^ − y
(如果 y y y 是列向量,则为 ( y ^ − y ) T (\hat{y} - y)^T ( y ^ − y ) T )。
3. 神经网络示例 我们计算一个使用交叉熵损失训练的单层神经网络的梯度。模型的前向传播过程如下:
x = input x = \text{input} x = input z = W x + b 1 z = Wx + b_1 z = W x + b 1 h = ReLU ( z ) h = \text{ReLU}(z) h = ReLU ( z ) θ = U h + b 2 \theta = Uh + b_2 θ = U h + b 2 y ^ = softmax ( θ ) \hat{y} = \text{softmax}(\theta) y ^ = softmax ( θ ) J = CE ( y , y ^ ) J = \text{CE}(y, \hat{y}) J = CE ( y , y ^ ) a . a. a . 数学推导将模型分解为可能的最简单的部分是有帮助的,所以请注意我们定义了 z z z 和 θ \theta θ 来将网络层中的线性变换与激活函数分开。模型参数的维度是:
x ∈ R D x × 1 b 1 ∈ R D h × 1 W ∈ R D h × D x b 2 ∈ R N c × 1 U ∈ R N c × D h x \in \mathbb{R}^{D_x \times 1} \quad b_1 \in \mathbb{R}^{D_h \times 1} \quad W \in \mathbb{R}^{D_h \times D_x} \quad b_2 \in \mathbb{R}^{N_c \times 1} \quad U \in \mathbb{R}^{N_c \times D_h} x ∈ R D x × 1 b 1 ∈ R D h × 1 W ∈ R D h × D x b 2 ∈ R N c × 1 U ∈ R N c × D h
其中 D x D_x D x 是我们输入的大小,D h D_h D h 是我们隐藏层的大小,N c N_c N c 是类的数量。
在这个例子中,我们将计算网络的所有梯度:
∂ J ∂ U , ∂ J ∂ b 2 , ∂ J ∂ W , ∂ J ∂ b 1 , ∂ J ∂ x \frac{\partial J}{\partial U}, \frac{\partial J}{\partial b_2}, \frac{\partial J}{\partial W}, \frac{\partial J}{\partial b_1}, \frac{\partial J}{\partial x} ∂ U ∂ J , ∂ b 2 ∂ J , ∂ W ∂ J , ∂ b 1 ∂ J , ∂ x ∂ J
首先, ReLU ( x ) = max ( x , 0 ) \text{ReLU}(x) = \max(x, 0) ReLU ( x ) = max ( x , 0 ) 。这意味着:
ReLU ′ ( x ) = { 1 if x > 0 0 otherwise = sgn ( ReLU ( x ) ) \text{ReLU}'(x) = \begin{cases} 1 & \text{if } x > 0 \\ 0 & \text{otherwise} \end{cases} = \text{sgn}(\text{ReLU}(x)) ReLU ′ ( x ) = { 1 0 if x > 0 otherwise = sgn ( ReLU ( x )) 其中 sgn 是符号函数。注意,我们能够用激活函数本身来表示激活函数的导数。
现在让我们写出 ∂ J ∂ U \frac{\partial J}{\partial U} ∂ U ∂ J 和 ∂ J ∂ b 2 \frac{\partial J}{\partial b_2} ∂ b 2 ∂ J 的链式法则:
∂ J ∂ U = ∂ J ∂ y ^ ∂ y ^ ∂ θ ∂ θ ∂ U \frac{\partial J}{\partial U} = \frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial \theta} \frac{\partial \theta}{\partial U} ∂ U ∂ J = ∂ y ^ ∂ J ∂ θ ∂ y ^ ∂ U ∂ θ ∂ J ∂ b 2 = ∂ J ∂ y ^ ∂ y ^ ∂ θ ∂ θ ∂ b 2 \frac{\partial J}{\partial b_2} = \frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial \theta} \frac{\partial \theta}{\partial b_2} ∂ b 2 ∂ J = ∂ y ^ ∂ J ∂ θ ∂ y ^ ∂ b 2 ∂ θ
注意 ∂ J ∂ y ^ ∂ y ^ ∂ θ = ∂ J ∂ θ \frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial \theta}=\frac{\partial J}{\partial \theta} ∂ y ^ ∂ J ∂ θ ∂ y ^ = ∂ θ ∂ J 在两个梯度中都存在。这使得数学计算有点繁琐。更糟糕的是,如果我们不使用自动微分来实现模型,计算 ∂ y ^ ∂ θ \frac{\partial \hat{y}}{\partial \theta} ∂ θ ∂ y ^ 两次会很低效。因此,定义一些变量来表示中间导数会很有帮助 :
δ 1 = ∂ J ∂ θ δ 2 = ∂ J ∂ z \delta_1 = \frac{\partial J}{\partial \theta} \quad \quad \delta_2 = \frac{\partial J}{\partial z} δ 1 = ∂ θ ∂ J δ 2 = ∂ z ∂ J
这些可以被认为是反向传播时传递给 θ \theta θ 和 z z z 的误差信号。我们可以如下计算它们:
δ 1 = ∂ J ∂ θ = ( y ^ − y ) T \delta_1 = \frac{\partial J}{\partial \theta} = (\hat{y} - y)^T δ 1 = ∂ θ ∂ J = ( y ^ − y ) T δ 2 = ∂ J ∂ z = ∂ J ∂ θ ∂ θ ∂ h ∂ h ∂ z \delta_2 = \frac{\partial J}{\partial z} = \frac{\partial J}{\partial \theta} \frac{\partial \theta}{\partial h} \frac{\partial h}{\partial z} δ 2 = ∂ z ∂ J = ∂ θ ∂ J ∂ h ∂ θ ∂ z ∂ h δ 2 = δ 1 ∂ θ ∂ h ∂ h ∂ z \delta_2 = \delta_1 \frac{\partial \theta}{\partial h} \frac{\partial h}{\partial z} δ 2 = δ 1 ∂ h ∂ θ ∂ z ∂ h δ 2 = δ 1 U ∂ h ∂ z \delta_2 = \delta_1 U \frac{\partial h}{\partial z} δ 2 = δ 1 U ∂ z ∂ h δ 2 = δ 1 U ∘ ReLU ′ ( z ) \delta_2 = \delta_1 U \circ \text{ReLU}'(z) δ 2 = δ 1 U ∘ ReLU ′ ( z ) δ 2 = δ 1 U ∘ sgn ( h ) \delta_2 = \delta_1 U \circ \text{sgn}(h) δ 2 = δ 1 U ∘ sgn ( h ) 现在我们可以使用误差项来计算我们的梯度。注意,当为列向量项计算梯度时,我们转置我们的答案以遵循形状约定。
∂ J ∂ U = ∂ J ∂ θ ∂ θ ∂ U = δ 1 T h T \frac{\partial J}{\partial U} = \frac{\partial J}{\partial \theta} \frac{\partial \theta}{\partial U} = \delta_1^T h^T ∂ U ∂ J = ∂ θ ∂ J ∂ U ∂ θ = δ 1 T h T ∂ J ∂ b 2 = ∂ J ∂ θ ∂ θ ∂ b 2 = δ 1 T \frac{\partial J}{\partial b_2} = \frac{\partial J}{\partial \theta} \frac{\partial \theta}{\partial b_2} = \delta_1^T ∂ b 2 ∂ J = ∂ θ ∂ J ∂ b 2 ∂ θ = δ 1 T ∂ J ∂ W = ∂ J ∂ z ∂ z ∂ W = δ 2 T x T \frac{\partial J}{\partial W} = \frac{\partial J}{\partial z} \frac{\partial z}{\partial W} = \delta_2^T x^T ∂ W ∂ J = ∂ z ∂ J ∂ W ∂ z = δ 2 T x T ∂ J ∂ b 1 = ∂ J ∂ z ∂ z ∂ b 1 = δ 2 T \frac{\partial J}{\partial b_1} = \frac{\partial J}{\partial z} \frac{\partial z}{\partial b_1} = \delta_2^T ∂ b 1 ∂ J = ∂ z ∂ J ∂ b 1 ∂ z = δ 2 T ∂ J ∂ x = ∂ J ∂ z ∂ z ∂ x = ( δ 2 W ) T \frac{\partial J}{\partial x} = \frac{\partial J}{\partial z} \frac{\partial z}{\partial x} = (\delta_2 W)^T ∂ x ∂ J = ∂ z ∂ J ∂ x ∂ z = ( δ 2 W ) T b . b. b . 代码实现整个过程可以被拆分成下面的层:
LinearReLULayer
: 处理 z = W x + b 1 z = Wx + b_1 z = W x + b 1 和 h = ReLU ( z ) h = \text{ReLU}(z) h = ReLU ( z ) LinearLayer
: 处理 θ = U h + b 2 \theta = Uh + b_2 θ = U h + b 2 SoftmaxCrossEntropyLoss
: 处理 y ^ = s o f t m a x ( θ ) \hat{y} = softmax(\theta) y ^ = so f t ma x ( θ ) 和 J = C E ( y , y ^ ) J = CE(y, \hat{y}) J = CE ( y , y ^ ) i . i. i . LinearReLULayer
层这个类合并了两个数学步骤:
z = W x + b 1 z = Wx + b_1 z = W x + b 1 (线性变换)h = ReLU ( z ) h = \text{ReLU}(z) h = ReLU ( z ) (ReLU激活)前向传播
def forward (self, x, W, b ):
z = W @ x + b
h = np.maximum(0 , z)
self .cache['x' ] = x
self .cache['W' ] = W
self .cache['z' ] = z
return h
反向传播
反向传播的目标是计算损失 J J J 对 W W W 和 b 1 b1 b 1 的梯度 d W dW d W 和 d b 1 db1 d b 1 ,以及将梯度传导到上一层(计算 d x dx d x )。它接收一个参数 d h dh d h ,这是后一层传过来的梯度。
def backward (self, dh ):
z = self .cache['z' ]
x = self .cache['x' ]
W = self .cache['W' ]
drelu = np.where(z > 0 , 1 , 0 )
dz = dh * drelu
dW = dz @ x.T
db = dz
dx = W.T @ dz
return dx, dW, db
i i . ii. ii . LinearLayer
层这个类只实现了一个数学步骤:θ = U h + b 2 θ = Uh + b_2 θ = U h + b 2 。它的 forward
和 backward
方法与 LinearReLULayer
中的线性部分原理完全相同,只是没有 ReLU 激活。
class LinearLayer :
"""A linear output layer"""
def __init__ (self ):
self .cache = {}
def forward (self, h, U, b ):
"""h: (H, 1), U: (C, H), b: (C, 1) -> o: (C, 1)"""
o = U @ h + b
self .cache['h' ] = h
self .cache['U' ] = U
return o
def backward (self, do ):
"""do: (C, 1) -> dh: (H, 1), dU: (C, H), db: (C, 1)"""
h = self .cache['h' ]
U = self .cache['U' ]
dU = do @ h.T
db = do
dh = U.T @ do
return dh, dU, db
i i i . iii. iii . SoftmaxCrossEntropyLoss
层这个类合并了最后两个数学步骤:y ^ = softmax ( θ ) \hat{y} = \text{softmax}(\theta) y ^ = softmax ( θ ) 和 J = CE ( y , y ^ ) J = \text{CE}(y, \hat{y}) J = CE ( y , y ^ )
前向传播
def forward (self, o, y ):
o_stable = o - np.max (o)
exp_o = np.exp(o_stable)
y_hat = exp_o / np.sum (exp_o)
loss = -np.sum (y * np.log(y_hat + 1e-9 ))
self .cache['y_hat' ] = y_hat
self .cache['y' ] = y
return loss
反向传播
反向传播中,我们使用先前的公式 d θ = y ^ − y d\theta=\hat{y}-y d θ = y ^ − y 来计算出 d θ d\theta d θ ,d θ d\theta d θ 会传给 LinearLayer
的后向传播方法,作为它的输入 d θ d\theta d θ 。
def backward (self ):
y_hat = self .cache['y_hat' ]
y = self .cache['y' ]
do = y_hat - y
return do
i v . iv. i v . 训练流程整合整个神经网络的训练过程如下:
初始化: 创建 W1
, b1
, W2
, b2
这些需要学习的参数。 实例化层: layer1
, layer2
, loss_fn
。 训练循环: 前向传播 :
h = layer1.forward(x_train, W1, b1)
o = layer2.forward(h, W2, b2)
loss = loss_fn.forward(o, y_train)
这里完全按照数学公式的顺序,将数据从头到尾计算一遍,得到最终的损失。
反向传播 :
这是最关键的一步。梯度从后往前 传播。loss_fn
首先计算出初始梯度 do
,然后把它传给 layer2
。layer2
利用 do
计算出自己内部参数的梯度 dW2
, db2
,同时计算出需要传给前一层的梯度 dh
。layer1
再利用 dh
计算出自己参数的梯度 dW1
, db1
。
do = loss_fn.backward()
dh, dW2, db2 = layer2.backward(do)
dx, dW1, db1 = layer1.backward(dh)
参数更新 :
我们用计算出的梯度来更新参数。我们希望损失 J
变小,所以参数要向着梯度的反方向 移动一小步(步长由 learning_rate
控制)。
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
完整程序如下:
if __name__ == '__main__' :
D_in = 10
H = 64
D_out = 5
learning_rate = 0.01
epochs = 200
W1 = np.random.randn(H, D_in) * 0.1
b1 = np.zeros((H, 1 ))
W2 = np.random.randn(D_out, H) * 0.1
b2 = np.zeros((D_out, 1 ))
layer1 = LinearReLULayer()
layer2 = LinearLayer()
loss_fn = SoftmaxCrossEntropyLoss()
x_train = np.random.randn(D_in, 1 )
y_true_index = np.random.randint(0 , D_out)
y_train = np.zeros((D_out, 1 ))
y_train[y_true_index] = 1
print (f"Input dimension: {D_in} , Hidden layer dimension: {H} , Number of output classes: {D_out} " )
print (f"Randomly generated true class index: {y_true_index} " )
print (f"Training started, total {epochs} epochs..." )
for epoch in range (epochs):
h = layer1.forward(x_train, W1, b1)
o = layer2.forward(h, W2, b2)
loss = loss_fn.forward(o, y_train)
if epoch % 20 == 0 :
print (f"Epoch {epoch} , Loss: {loss:.6 f} " )
do = loss_fn.backward()
dh, dW2, db2 = layer2.backward(do)
dx, dW1, db1 = layer1.backward(dh)
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
print ("\nTraining complete!" )
final_h = layer1.forward(x_train, W1, b1)
final_o = layer2.forward(final_h, W2, b2)
final_loss = loss_fn.forward(final_o, y_train)
final_prediction = np.argmax(final_o)
4. 通过维度匹配推导梯度 在进行复杂的梯度计算时,一个非常强大且实用的技巧是检查并匹配维度 。这个方法不仅可以作为复杂推导的健全性检查,甚至可以在不进行详细的逐元素推导的情况下,帮助我们直接得出正确的梯度表达式。
其核心思想非常简单:损失函数 J J J (一个标量)对某个参数矩阵 W W W 的梯度 ∂ J ∂ W \frac{\partial J}{\partial W} ∂ W ∂ J ,其维度必须与 W W W 本身的维度完全相同 。这是梯度下降更新规则 W n e w = W o l d − α ∂ J ∂ W W_{new} = W_{old} - \alpha \frac{\partial J}{\partial W} W n e w = W o l d − α ∂ W ∂ J 能够成立的基础。
让我们重新审视前面的一些例子,看看如何应用这个技巧。
a . a. a . 重访恒等式 z = W x z = Wx z = W x 目标 : 计算 ∂ J ∂ W \frac{\partial J}{\partial W} ∂ W ∂ J 。已知维度 :W : D h × D x W: D_h \times D_x W : D h × D x x : D x × 1 x: D_x \times 1 x : D x × 1 z : D h × 1 z: D_h \times 1 z : D h × 1 梯度维度 :根据维度匹配原则,∂ J ∂ W \frac{\partial J}{\partial W} ∂ W ∂ J 的维度必须是 D h × D x D_h \times D_x D h × D x 。 反向传播传来的上游梯度是 δ 2 = ∂ J ∂ z \delta _2 = \frac{\partial J}{\partial z} δ 2 = ∂ z ∂ J 。在代码实现中,这通常是一个维度为 D h × 1 D_h \times 1 D h × 1 的列向量。 推导 :我们需要组合上游梯度 δ 2 \delta _2 δ 2 (维度 D h × 1 D_h \times 1 D h × 1 ) 和前向传播中的输入 x x x (维度 D x × 1 D_x \times 1 D x × 1 ),来得到一个维度为 D h × D x D_h \times D_x D h × D x 的矩阵。 唯一能实现这个维度转换的运算是外积:δ 2 x T \delta _2 x^T δ 2 x T 。 检查维度:( D h × 1 ) ( 1 × D x ) → ( D h × D x ) (D_h \times 1) (1 \times D_x) \rightarrow (D_h \times D_x) ( D h × 1 ) ( 1 × D x ) → ( D h × D x ) 。 这与我们代码实现中的 dW = dz @ x.T
完全吻合,也与原始推导 δ 2 T x T \delta _2^T x^T δ 2 T x T (假设 δ 2 \delta _2 δ 2 是 1 × D h 1 \times D_h 1 × D h 的行向量)的结果在维度上是一致的。 b. 重访神经网络示例中的 ∂ J ∂ U \frac{\partial J}{\partial U} ∂ U ∂ J 目标 : 计算 ∂ J ∂ U \frac{\partial J}{\partial U} ∂ U ∂ J 。已知维度 :U : N c × D h U: N_c \times D_h U : N c × D h h : D h × 1 h: D_h \times 1 h : D h × 1 t h e t a : N c × 1 theta: N_c \times 1 t h e t a : N c × 1 梯度维度 :∂ J ∂ U \frac{\partial J}{\partial U} ∂ U ∂ J 的维度必须是 N c × D h N_c \times D_h N c × D h 。上游梯度是 δ 1 = ∂ J ∂ θ \delta_1 = \frac{\partial J}{\partial \theta} δ 1 = ∂ θ ∂ J ,其维度为 N c × 1 N_c \times 1 N c × 1 。 推导 :我们需要组合 δ 1 \delta_1 δ 1 (维度 N c × 1 N_c \times 1 N c × 1 ) 和 h h h (维度 D h × 1 D_h \times 1 D h × 1 ),得到一个 N c × D h N_c \times D_h N c × D h 的矩阵。 同样,唯一可行的方式是外积:δ 1 h . T \delta _1 h.T δ 1 h . T 。 检查维度:( N c × 1 ) ( 1 × D h ) → ( N c ′ × D h ) (N_c \times 1) (1 \times D_h) \rightarrow (N_c '\times D_h) ( N c × 1 ) ( 1 × D h ) → ( N c ′ × D h ) 。 这也与我们代码实现中的 dU = do @ h.T
完全一致。 c . c. c . 总结当对梯度计算公式感到困惑时,不妨退后一步,检查你所拥有的各个变量(通常是上游梯度和前向传播的输入)的维度。然后,思考如何通过合法的矩阵运算(矩阵乘法、转置、外积等)将它们组合起来,以得到一个与要求解的参数梯度维度相匹配的结果。在绝大多数情况下,只有一种组合方式是正确的。这个简单的技巧可以帮我们避免许多常见的错误。