序言
反向传播(Backpropagation,简称backprop)是神经网络训练过程中最关键的技术之一,尤其在多层神经网络中广泛应用。它是一种与优化方法(如梯度下降法)结合使用的算法,用于计算网络中各参数的梯度,进而通过调整这些参数来最小化损失函数,从而提高模型的预测准确性和泛化能力。微分算法在机器学习中占据核心地位,主要用于计算复杂函数的梯度。反向传播作为其中的一种,特别适用于神经网络中的梯度计算。其基本原理是利用链式法则,通过计算图中每个节点的梯度来逐步反向传播误差信号,从而实现对网络参数的优化。
反向传播和其他的微分算法
-
当我们使用前馈神经网络接收输入 x \boldsymbol{x} x并产生输出 y ^ \hat{\boldsymbol{y}} y^时,信息通过网络向前流动。
-
前向传播(forward propagation)
- 输入 x \boldsymbol{x} x提供初始信息,然后传播到每一层的隐藏单元,最终产生输出 y ^ \hat{\boldsymbol{y}} y^
-
反向传播(back propagation)
- 在训练过程中,前向传播可以持续向前直到它产生一个标量代价函数 J ( θ ) J(\boldsymbol{\theta}) J(θ)。反向传播(back propagation),经常简称为backprop,允许来自代价函数的信息通过网络向后流动,以便计算梯度。
- 计算梯度的解析表达式是很直观的,但是数值化地求解这样的表达式在计算上可能是昂贵的。反向传播算法使用简单和廉价的程序来实现这个目标。
- 反向传播这个术语经常被误解为用于多层神经网络的整个学习算法。实际上,反向传播仅指用于计算梯度的方法,而另一种算法,例如随机梯度下降,使用该梯度来进行学习。
- 此外,反向传播仅适用于多层神经网络,但原则上它可以计算任何函数的导数(对于一些函数,正确的响应是报告函数的导数是未定义的)。
- 特别地,我们会描述如何计算一个任意函数
f
f
f的梯度
∇
x
f
(
x
,
y
)
\nabla_x f(\boldsymbol{x,y})
∇xf(x,y)
- 其中 x \boldsymbol{x} x是一组变量,我们需要它们的导数
- 而 y \boldsymbol{y} y是另外一组函数的输入变量,但我们并不需要它们的导数。
- 在学习算法中,我们最常需要的梯度是成本函数关于参数的梯度,即 ∇ θ J ( θ ) \nablaθJ(θ) ∇θJ(θ)。许多机器学习任务涉及计算其他导数,作为学习过程的一部分,或者用来分析学习的模型。反向传播算法也适用于这些任务,并且不限于计算成本函数关于参数的梯度。通过网络传播信息来计算导数的想法是非常通用的,并且可以用于计算诸如具有多个输出的函数 f f f的 Jacobi \text{Jacobi} Jacobi矩阵的值。我们这里描述的是最常用的情况, f f f只有单个输出。
-
算法3
典型深度神经网络中的前向传播和代价函数的计算。
损失函数 L ( y ^ , y ) L(\boldsymbol{\hat{y}},\boldsymbol{y}) L(y^,y)取决于输出 y ^ \boldsymbol{\hat{y}} y^和目标 y \boldsymbol{y} y(参见:基于梯度的学习篇中损失函数的示例)。
为了获得总代价 J J J,损失函数可以加上正则项 Ω ( θ ) \Omega(\theta) Ω(θ),其中 θ \theta θ包含所有参数(权重和偏置)。
算法4说明了如何计算 J J J对参数 W \boldsymbol{W} W和 b \boldsymbol{b} b的梯度。
为简单起见,该演示仅使用单个输入样例 x \boldsymbol{x} x。实际应用应该使用minibatch。请参见用于MLP训练的反向传播算法以获得更加真实的演示。
伪代码:
R e q u i r e \bold{Require} Require: l l l, Network depth
R e q u i r e \bold{Require} Require: W ( i ) \boldsymbol{W}^{(i)} W(i), i ∈ { 1 , … , l } i\in\{1,\dots,l\} i∈{1,…,l}, the weight matrices of the model
R e q u i r e \bold{Require} Require: b ( i ) \boldsymbol{b}^{(i)} b(i), i ∈ { 1 , … , l } i\in\{1,\dots,l\} i∈{1,…,l}, the bias parameters of the model
R e q u i r e \bold{Require} Require: x \boldsymbol{x} x, the input to process
R e q u i r e \bold{Require} Require: y \boldsymbol{y} y, the target output
h ( 0 ) = x \boldsymbol{h}^{(0)} = x h(0)=x
f o r \bold{for} for k k k = 1 , … , l 1,\dots,l 1,…,l d o \bold{do} do
\quad a ( k ) = b ( k ) + W ( k ) h ( k − 1 ) \boldsymbol{a}^{(k)} = \boldsymbol{b}^{(k)} + \boldsymbol{W}^{(k)}\boldsymbol{h}^{(k-1)} a(k)=b(k)+W(k)h(k−1)
\quad h ( k ) = f ( a ( k ) ) \boldsymbol{h}^{(k)} = f(\boldsymbol{a}^{(k)}) h(k)=f(a(k))
e n d \bold{end} end f o r \bold{for} for
y ^ = h ( l ) \boldsymbol{\hat{y}} = \boldsymbol{h}^{(l)} y^=h(l)
J = L ( y ^ , y ) + λ Ω ( θ ) J = L(\boldsymbol{\hat{y}},\boldsymbol{y}) + \lambda\Omega(\theta) J=L(y^,y)+λΩ(θ)
-
算法4
深度神经网络中算法3的反向计算,它使用了不止输入 x \boldsymbol{x} x和目标 y \boldsymbol{y} y。
该计算对于每一层 k k k都产生了对激活 a ( k ) \boldsymbol{a}^{(k)} a(k)的梯度,从输出层开始向后计算一直到第一个隐藏层。
这些梯度可以看作是对每层的输出应如何调整以减小误差的指导,根据这些梯度可以获得对每层参数的梯度。
权重和偏置上的梯度可以立即用作随机梯度更新的一部分(梯度算出后即可执行更新),或者与其他基于梯度的优化方法一起使用。
伪代码:
After the forward computation, compute the gradient on the output layer:
g ← ∇ y ^ J = ∇ y ^ L ( y ^ , y ) \boldsymbol{g} \gets \nabla_{\boldsymbol{\hat{y}}}J= \nabla_{\boldsymbol{\hat{y}}}L(\boldsymbol{\hat{y}},\boldsymbol{y}) g←∇y^J=∇y^L(y^,y)
f o r \bold{for} for k k k = l , l − 1 , … , 1 l,l-1,\dots,1 l,l−1,…,1 d o \bold{do} do
\quad Convert the gradient on the layer’s output into a gradient into the prenonlinearity activation (element-wise multiplication if f is element-wise):
\quad g ← ∇ a ( k ) J = g ⊙ f ′ ( a ( k ) ) \boldsymbol{g} \gets \nabla_{\boldsymbol{a}^{(k)}}J=\boldsymbol{g}\odot f'(\boldsymbol{a}^{(k)}) g←∇a(k)J=g⊙f′(a(k))
\quad Compute gradients on weights and biases (including the regularization term,
where needed):
\quad ∇ b ( k ) J = g + λ ∇ b ( k ) Ω ( θ ) \nabla_{\boldsymbol{b}^{(k)}}J=\boldsymbol{g}+\lambda\nabla_{\boldsymbol{b}^{(k)}}\Omega(\theta) ∇b(k)J=g+λ∇b(k)Ω(θ)
\quad ∇ W ( k ) J = g h ( k − 1 ) ⊤ + λ ∇ W ( k ) Ω ( θ ) \nabla_{\boldsymbol{W}^{(k)}}J=\boldsymbol{g}\boldsymbol{h}^{(k-1)\top}+\lambda\nabla_{\boldsymbol{W}^{(k)}}\Omega(\theta) ∇W(k)J=gh(k−1)⊤+λ∇W(k)Ω(θ)
\quad Propagate the gradients w.r.t. the next lower-level hidden layer’s activations:
\quad g ← ∇ h ( k − 1 ) J = W ( k ) ⊤ g \boldsymbol{g} \gets \nabla_{\boldsymbol{h}^{(k-1)}}J=\boldsymbol{W}^{(k)\top}\boldsymbol{g} g←∇h(k−1)J=W(k)⊤g
e n d \bold{end} end f o r \bold{for} for
一般化的反向传播
-
反向传播算法非常简单。
-
为了计算某个标量 z z z对图中它的一个祖先 x \boldsymbol{x} x的梯度,我们首先观察到对 z z z的梯度由 ∂ z ∂ z = 1 \frac{\partial z}{\partial z}=1 ∂z∂z=1给出。我们然后可以计算对图中 z z z的操作的 Jacobi \text{Jacobi} Jacobi矩阵。我们继续乘以 Jacobi \text{Jacobi} Jacobi矩阵,以这种方式向后穿过图,直到我们到达 x \boldsymbol{x} x。对于从 z z z触发可以经过两个或更多路径向后行进而到达的任意节点,我们简单的对该节点来自不同路径上的梯度进行求和。
-
更正式地,图 G \mathcal{G} G中的每个节点对应着一个变量。为了实现最大的一般化,我们将这个变量描述为一个张量 V \bold{V} V。张量通常可以具有任意维度,并且包含标量、向量和矩阵。
-
我们假设每个变量 V \bold{V} V与下列子程序相关联:
- get_operation(
V
\bold{V}
V):
- 它返回用于计算 V \bold{V} V的操作,代表了在计算图中流入 V \bold{V} V的边。
- 例如,可能有一个Python或者C++的类表示矩阵乘法操作,以及get_operation函数。
- 假设我们的一个变量是由矩阵乘法产生的, C = A B C=AB C=AB。
- 那么,get_operation( V \bold{V} V)返回一个指向相应C++类的实力的指针。
- get_consumers(
V
,
G
\bold{V},\mathcal{G}
V,G):
- 它返回一组变量,是计算图 G \mathcal{G} G中 V \bold{V} V的孩子节点。
- get_inputs(
V
,
G
\bold{V},\mathcal{G}
V,G):
- 它返回一组变量,是计算图 G \mathcal{G} G中 V \bold{V} V的父节点。
- get_operation(
V
\bold{V}
V):
-
重要:每个操作op也与bprop操作相关联。该bprop操作可以计算如公式: ∇ X z = ∑ j ( ∇ X Y j ) ∂ z ∂ Y j \nabla_{\bold{X}}z=\sum\limits_j(\nabla_{\bold{X}}\bold{Y}_j)\displaystyle\frac{\partial z}{\partial \bold{Y}_j} ∇Xz=j∑(∇XYj)∂Yj∂z描述的 Jacobi \text{Jacobi} Jacobi向量积。这是反向传播算法能够实现很大通用性的原因。
-
每个操作负责了解如何通过它参与的图中的边来反向传播。例如,我们可以使用矩阵乘法操作来产生变量 C = A B C = AB C=AB。
- 假设标量 z z z关于 C C C的梯度是 G G G。
- 矩阵乘法操作负责定义两个反向传播规则,每个规则对应于一个输入变量。
- 如果我们调用bprop方法来请求 A A A 的梯度,在给定输出的梯度为 G G G的情况下,那么矩阵乘法操作的bprop方法必须说明对 A A A的梯度是 G B ⊤ GB^\top GB⊤。
- 类似的,如果我们调用bprop方法来请求 B B B的梯度,那么矩阵操作负责实现bprop方法并指定期望的梯度是 A ⊤ G A^\top G A⊤G。
- 反向传播算法本身并不需知道任何微分法则。它只需要使用正确的参数调用每个操作的bprop 方法即可。正式地, op.bprop(inputs, X , G \bold{X},G X,G)必须返回: ∑ i ( ∇ X op . f ( inputs ) i ) G i \sum\limits_i(\nabla_{\bold{X}} \text{op}.f(\text{inputs})_i)G_i i∑(∇Xop.f(inputs)i)Gi
- 这只是如公式:
∇
X
z
=
∑
j
(
∇
X
Y
j
)
∂
z
∂
Y
j
\nabla_{\bold{X}}z=\sum\limits_j(\nabla_{\bold{X}}\bold{Y}_j)\displaystyle\frac{\partial z}{\partial \bold{Y}_j}
∇Xz=j∑(∇XYj)∂Yj∂z所表达的链式法则的实现。
- 说明:
- inputs是提供给操作的一组输入
- op.f是操作实现的数学函数
- X \bold{X} X是输入,我们想要计算关于它的梯度
- G G G是操作对输出的梯度
- 说明:
- op.bprop方法应该总是假装它的所有输入彼此不同,即使它们不是。
- 例如,如果mul操作传递两个 x x x来计算 x 2 x^2 x2,op.bprop方法应该仍然返回 x x x作为对于两个输入的导数。
- 反向传播算法后面会将这些变量加起来获得 2 x 2x 2x,这是 x x x上总的正确导数。
-
反向传播算法的软件实现通常提供操作和其bprop 两种方法,所以深度学习软件库的用户能够对使用诸如矩阵乘法、指数运算、对数运算等等常用操作构建的图进行反向传播。构建反向传播新实现的软件工程师或者需要向现有库添加自己的操作的高级用户通常必须手动为新操作推导op.bprop方法。
-
反向传播算法的正式描述参见算法5
-
算法5
反向传播算法最外围的骨架。这部分做简单的设置和清理工作。大多数重要的工作在发生在算法6的子程序build_grad中。
伪代码:
R e q u i r e \bold{Require} Require: T \mathbb{T} T, 计算梯度的变量的目标集
R e q u i r e \bold{Require} Require: G \mathbb{G} G, 计算图
R e q u i r e \bold{Require} Require: z z z, 要求导的变量
\quad Let G ′ \mathcal{G}' G′ be G \mathcal{G} G pruned to contain only nodes that are ancestors of z z z and descendents of nodes in T \mathbb{T} T.
\quad Initialize grad_table, a data structure associating tensors to their gradients
\quad grad_table[ z z z] ← \gets ← 1
\quad f o r \bold{for} for V \bold{V} V in T \mathbb{T} T d o \bold{do} do
\quad\quad build_grad( V \bold{V} V, G \mathcal{G} G, G ′ \mathcal{G}' G′, grad_table)
\quad e n d \bold{end} end f o r \bold{for} for
\quad Return grad_table restricted to T \mathbb{T} T
-
算法6
反向传播算法的内循环子程序build_grad( V \bold{V} V, G \mathcal{G} G, G ′ \mathcal{G}' G′, grad_table),被在算法5中定义的反向传播算法调用。
伪代码:
R e q u i r e \bold{Require} Require: V \bold{V} V, the variable whose gradient should be added to G \mathcal{G} G and grad_table.
R e q u i r e \bold{Require} Require: G \mathcal{G} G, the graph to modify.
R e q u i r e \bold{Require} Require: G ′ \mathcal{G}' G′, the restriction of G \mathcal{G} G to nodes that participate in the gradient.
R e q u i r e \bold{Require} Require: grad_table, a data structure mapping nodes to their gradients
\quad i f \bold{if} if V \mathbb{V} V is in grad_table t h e n \bold{then} then
\quad\quad Return grad_table[ V \bold{V} V]
e n d \quad\bold{end} end i f \bold{if} if
i ← 1 \quad i\gets1 i←1
\quad f o r \bold{for} for C \bold{C} C in get_consumers( V \bold{V} V, G ′ \mathcal{G}' G′) d o \bold{do} do
\quad \quad op get_operation( C \bold{C} C)
\quad D \bold{D} D build_grad( C \bold{C} C, G \mathcal{G} G, G ′ \mathcal{G}' G′, grad_table)
\quad \quad G ( i ) \bold{G}^{(i)} G(i) ← \gets ← op:bprop(get_inputs( C \bold{C} C, G ′ \mathcal{G}' G′), V \bold{V} V, D \bold{D} D)
\quad i ← i + 1 \quad i \gets i + 1 i←i+1
\quad e n d \bold{end} end f o r \bold{for} for
\quad G ← ∑ i G ( i ) \bold{G} \gets \sum_i \bold{G}^{(i)} G←∑iG(i)
\quad grad_table[ V \bold{V} V] = G \bold{G} G
\quad Insert G \bold{G} G and the operations creating it into G \mathcal{G} G
\quad Return G \bold{G} G
-
在微积分中的链式法则中,我们使用反向传播作为一种策略来避免多次计算链式法则中的相同子表达式。
-
由于这些重复子表达式的存在,简单的算法可能具有指数运行时间。现在我们已经详细说明了反向传播算法,我们可以去理解它的计算成本。
-
如果我们假设每个操作的执行都有大致相同的开销,那么我们可以依据执行操作的数量来分析计算成本。注意这里我们将一个操作记为计算图的基本单位,它实际可能包含许多算术运算(例如,我们可能将矩阵乘法视为单个操作)。
-
在具有 n n n个节点的图中计算梯度,将永远不会执行超过 O ( n 2 ) \Omicron(n^2) O(n2)个操作,或者存储超过 O ( n 2 ) \Omicron(n^2) O(n2)个操作的输出。这里我们是对计算图中的操作进行计数,而不是由底层硬件执行的单独操作,所以重要的是要记住每个操作的运行时间可能是高度可变的。
-
例如,两个矩阵相乘可能对应着图中的一个单独的操作,但这两个矩阵可能每个都包含数百万个元素。我们可以看到,计算梯度至多需要 O ( n 2 ) \Omicron(n^2) O(n2)的操作,因为在最坏的情况下,前向传播的步骤将执行原始图的全部 n n n个节点(取决于我们想要计算的值,我们可能不需要执行整个图)。
-
反向传播算法在原始图的每条边添加一个 Jacobi \text{Jacobi} Jacobi向量积,可以用 O ( 1 ) \Omicron(1) O(1)个节点来表达。因为计算图是有向无环图,它至多有 O ( n 2 ) \Omicron(n^2) O(n2)条边。对于实践中常用的图的类型,情况会更好。
-
大多数神经网络的代价函数大致是链式结构的,使得反向传播只有 O ( n ) \Omicron(n) O(n)的成本。这远远胜过简单的方法,简单方法可能需要执行指数级的节点。
-
这种潜在的指数级代价可以通过非递归地扩展和重写递归链式法则(公式: ∂ u ( n ) ∂ u ( j ) = ∑ i : j ∈ P a ( u ( i ) ) ∂ u ( n ) ∂ u ( i ) ∂ u ( i ) ∂ u ( j ) \displaystyle\frac{\partial u^{(n)}}{\partial u^{(j)}}=\sum\limits_{i:j\in Pa(u^{(i)})}\frac{\partial u^{(n)}}{\partial u^{(i)}} \frac{\partial u^{(i)}}{\partial u^{(j)}} ∂u(j)∂u(n)=i:j∈Pa(u(i))∑∂u(i)∂u(n)∂u(j)∂u(i))来看出:
∂ u ( n ) ∂ u ( j ) = ∑ path ( u ( π 1 ) , u ( π 2 ) , … , u ( π t ) ) , from π i = j t o π t = n ∏ k = 2 t ∂ u ( π k ) ∂ u ( π k − 1 ) \frac{\partial u^{(n)}}{\partial u^{(j)}}=\sum\limits_{\text{path}(u^{(\pi_1)},u^{(\pi_2)},\dots,u^{(\pi_t)}), \hspace{2pt}\text{from}\hspace{2pt}\pi_i=j\hspace{2pt}to\hspace{2pt}\pi_t=n} \prod\limits_{k=2}^t \frac{\partial u^{(\pi_k)}}{\partial u^{(\pi_{k-1})}} ∂u(j)∂u(n)=path(u(π1),u(π2),…,u(πt)),fromπi=jtoπt=n∑k=2∏t∂u(πk−1)∂u(πk)- 由于节点 j j j到节点 n n n的路径数目可以在这些路径的长度上指数地增长,所以上述求和符号中的项数(这些路径的数目),可能以前向传播图的深度的指数级增长。
- 会产生如此大的成本是因为对于 ∂ u ( i ) ∂ u ( j ) \frac{\partial u^{(i)}}{\partial u^{(j)}} ∂u(j)∂u(i),相同的计算会重复进行很多次。
- 为了避免这种重新计算,我们可以将反向传播看作一种表填充算法,利用存储的中间结果 ∂ u ( n ) ∂ u ( i ) \frac{\partial u^{(n)}}{\partial u^{(i)}} ∂u(i)∂u(n)来对表进行填充。
- 图中的每个节点对应着表中的一个位置,这个位置存储对该节点的梯度。
- 通过顺序填充这些表的条目,反向传播算法避免了重复计算许多公共子表达式。
- 这种表填充策略有时被称为动态规划(dynamic programming)。
用于MLP训练的反向传播算法实例
- 作为一个例子,我们利用反向传播算法来训练多层感知器(MLP)。
- 这里,我们考虑具有单个隐藏层的一个非常简单的多层感知机。为了训练这个模型,我们将使用小批量(minibatch)随机梯度下降算法。反向传播算法用于计算单个minibatch上的代价的梯度。
-
具体来说,我们使用训练集上的一组minibatch实例,将其规范化为一个设计矩阵 X \boldsymbol{X} X以及相关联的类标签的向量 y \boldsymbol{y} y。
-
网络计算隐藏特征层: H = max { 0 , X W ( 1 ) } \boldsymbol{H}=\max\{0,\boldsymbol{XW^{(1)}}\} H=max{0,XW(1)}。为了简化表示,我们在这个模型中不使用偏置。
-
假设我们的图语言包含relu操作,该操作可以对 max { 0 , Z } \max\{0,\boldsymbol{Z}\} max{0,Z}表达式的每个元素分别进行计算。
-
类的非归一化对数概率的预测将随后由 H W ( 2 ) \boldsymbol{HW^{(2)}} HW(2) 给出。假设我们的图语言包含cross_entropy操作,用以计算目标 y \boldsymbol{y} y和由这些未归一化对数概率定义的概率分布间的交叉熵。所得到的交叉熵定义了代价函数 J MLE J_{\text{MLE}} JMLE。
-
最小化这个交叉熵将执行对分类器的最大似然估计。然而,为了使得这个例子更加真实,我们也包含一个正则项。总的代价函数为:
J = J MLE + λ ( ∑ i , j ( W i , j ( 1 ) ) 2 + ∑ i , j ( W i , j ( 2 ) ) 2 ) J=J_{\text{MLE}}+\lambda \left(\sum\limits_{i,j}(W_{i,j}^{(1)})^2+\sum\limits_{i,j}(W_{i,j}^{(2)})^2 \right) J=JMLE+λ(i,j∑(Wi,j(1))2+i,j∑(Wi,j(2))2) -
包含了交叉熵和系数为 λ \lambda λ的权重衰减项。它的计算图在下图中给出。
-
例如,用于计算代价函数的计算图,这个代价函数是使用交叉熵损失以及权重衰减训练我们的单层MLP示例所产生的。
-
说明:
- 这个示例的梯度计算图实在太大,以至于绘制或者阅读都将是乏味的。这显示出了反向传播算法的优点之一,即它可以自动生成梯度,而这种计算对于软件工程师来说需要进行直观但冗长的手动推导.
- 我们可以通过观察图中的正向传播图来粗略地描述反向传播算法的行为。为了训练,我们希望计算 ∇ W ( 1 ) J \nabla_{\boldsymbol{W}^{(1)}}J ∇W(1)J和 ∇ W ( 2 ) J \nabla_{\boldsymbol{W}^{(2)}}J ∇W(2)J。
- 有两种不同的路径从
J
J
J后退到权重:
- 一条通过交叉熵成本
- 另一条通过权重衰减成本
- 权重衰减成本相对简单,它总是对 W ( i ) \boldsymbol{W}^{(i)} W(i)上的梯度贡献 2 λ W ( i ) 2\lambda\boldsymbol{W}^{(i)} 2λW(i)。
- 另一条通过交叉熵成本的路径稍微复杂一些。
- 令 G \boldsymbol{G} G是cross_entropy操作提供的对未归一化对数概率 U ( 2 ) \boldsymbol{U}^{(2)} U(2)的梯度。
- 反向传播算法现在需要探索两个不同的分支。
- 在较短的分支上,它使用对矩阵乘法的第二个变量的反向传播规则,将 H ⊤ G \boldsymbol{H}^\top\boldsymbol{G} H⊤G加到 W ( 2 ) \boldsymbol{W}^{(2)} W(2)的梯度上。
- 另一条更长些的路径沿着网络逐步下降。
- 首先,反向传播算法使用对矩阵乘法的第一个变量的反向传播规则,计算 ∇ H J = G W ( 2 ) ⊤ \nabla_{\boldsymbol{H}}J=\boldsymbol{GW^{(2)\top}} ∇HJ=GW(2)⊤
- 接下来,relu操作使用期反向传播规则来对关于 U ( 1 ) \boldsymbol{U}^{(1)} U(1)的梯度中小于 0 0 0的部分清零。
- 记上述结果为 G ′ \boldsymbol{G}' G′。反向传播算法的最后一步是使用对matmul操作的第二个变量的反向传播规则,将 X ⊤ G ′ \boldsymbol{X}^\top\boldsymbol{G}' X⊤G′加到 W ( 1 ) \boldsymbol{W}^{(1)} W(1)的梯度上。
- 在计算了这些梯度以后,梯度下降算法或者其他的优化算法的责任就是使用这些梯度来更新参数。
-
- 对于MLP,计算成本主要来源于矩阵乘法matmul操作。
- 在前向传播阶段,我们乘以每个权重矩阵,得到了 O ( ω ) \Omicron(\omega) O(ω)数量的乘-加,其中 ω \omega ω是权重的数量。
- 在反向传播阶段,我们乘以每个权重矩阵的转置,具有相同的计算成本。
- 算法主要的存储成本是我们需要将输入存储到隐藏层的非线性中去。这些值从被计算时开始存储,直到反向过程回到了同一点。
- 因此存储成本是 O ( m n h ) \Omicron(mn_h) O(mnh),其中 m m m是minibatch中样例的数目, n h n_h nh是隐藏单元的数量。
复杂化
- 我们这里描述的反向传播算法要比现实中实际使用的实现要简单。
- 正如前面提到的,我们将操作的定义限制为返回单个张量的函数。大多数软件实现需要支持可以返回多个张量的操作。例如,如果我们希望计算张量中的最大值和该值的索引,则最好在单次运算中计算两者,因此将该过程实现为具有两个输出的操作效率更高。
- 我们还没有描述如何控制反向传播的内存消耗。反向传播经常涉及将许多张量加在一起。在朴素方法中,将分别计算这些张量中的每一个,然后在第二步中对所有这些张量求和。朴素方法具有过高的存储瓶颈,可以通过保持一个缓冲器,并且在计算时将每个值加到该缓冲器中来避免该瓶颈。
- 反向传播的现实实现还需要处理各种数据类型,例如,32位浮点数,64位浮点数和整型。处理这些类型的策略需要特别的设计考虑。
- 一些操作具有未定义的梯度,并且重要的是跟踪这些情况并且确定用户请求的梯度是否是未定义的。
- 各种其他技术的特性使现实世界的微分更加复杂。这些技术性并不是不可逾越的,本章已经描述了计算微分所需的关键智力工具,但重要的是要知道还有许多的精妙之处存在。
深度学习界以外的微分(注:晦涩难懂则可暂且略过)
- 深度学习界在某种程度上已经与更广泛的计算机科学界隔离开来,并且在很大程度上发展了自己关于如何进行微分的文化态度。
- 更一般地, 自动微分(automatic differentiation) 领域关心如何以算法方式计算导数。这里描述的反向传播算法只是自动微分的一种方法。它是一种称为反向模式累加 (reverse mode accumulation) 的更广泛类型的技术的特殊情况。其他方法以不同的顺序来计算链式法则的子表达式。一般来说,确定一种计算的顺序使得计算开销最小,是困难的问题。找到计算梯度的最优操作序列是 NP 完全问题 (Naumann, 2008),在这种意义上,它可能需要将代数表达式简化为它们最廉价的形式。
- 例如,假设我们有变量
p
1
,
p
2
,
…
,
p
n
p_1,p_2,\dots,p_n
p1,p2,…,pn表示概率,和变量
z
i
,
z
2
,
…
,
z
n
z_i,z_2,\dots,z_n
zi,z2,…,zn表示未归一化的对数概率。假设我们定义:
q
i
=
e
z
i
∑
i
e
z
i
q_i=\frac{e^{z_i}}{\sum_i e^{z_i}}
qi=∑ieziezi。
- 其中我们通过指数化、求和与除法运算构建 softmax \text{softmax} softmax函数,并构造交叉熵损失函数 J = − ∑ i p i log q i J=-\sum_i p_i\log q_i J=−∑ipilogqi。
- 人类数学家可以观察到 J J J对 z i z_i zi的导数采用了非常简单的形式: p i q i − p i p_iq_i-p_i piqi−pi。
- 反向传播算法不能够以这种方式来简化梯度,而是会通过原始图中的所有对数和指数操作显示地传播梯度。一些软件库如 Theano(Bergstra et al., 2010b;Bastien et al., 2012b) 能够执行某些种类的代数替换来改进由纯反向传播算法提出的图。
- 当前向图 G \mathcal{G} G具有单个输出节点,并且每个偏导数 ∂ u ( i ) ∂ u ( j ) \frac{\partial u^{(i)}}{\partial u^{(j)}} ∂u(j)∂u(i)都可以用恒定的计算量来计算时,反向传播保证梯度计算的计算数目和前向计算的计算数目是同一个量级:这可以在算法2中看出,因为每个局部偏导数 ∂ u ( i ) ∂ u ( j ) \frac{\partial u^{(i)}}{\partial u^{(j)}} ∂u(j)∂u(i)以及递归链式公式(公式 ∂ u ( n ) ∂ u ( j ) = ∑ i : j ∈ P a ( u ( i ) ) ∂ u ( n ) ∂ u ( i ) ∂ u ( i ) ∂ u ( j ) \displaystyle\frac{\partial u^{(n)}}{\partial u^{(j)}}=\sum\limits_{i:j\in Pa(u^{(i)})}\frac{\partial u^{(n)}}{\partial u^{(i)}} \frac{\partial u^{(i)}}{\partial u^{(j)}} ∂u(j)∂u(n)=i:j∈Pa(u(i))∑∂u(i)∂u(n)∂u(j)∂u(i))中相关的乘和加都只需计算一次。因此,总的计算量是 O ( #edges ) \Omicron(\text{\#edges}) O(#edges)。然而,可能通过对反向传播算法构建的计算图进行简化来减少这些计算量,并且这是NP完全问题。诸如 Theano \text{Theano} Theano和 TensorFlow \text{TensorFlow} TensorFlow的实现使用基于匹配已知简化模式的试探法,以便重复地尝试去简化图。我们定义反向传播仅用于计算标量输出的梯度,但是反向传播可以扩展到计算 Jacobi \text{Jacobi} Jacobi 矩阵(该 Jacobi \text{Jacobi} Jacobi矩阵或者来源于图中的 k k k个不同标量节点,或者来源于包含 k k k个值的张量值节点)。朴素的实现可能需要 k k k倍的计算:对于原始前向图中的每个内部标量节点,朴素的实现计算 k k k个梯度而不是单个梯度。当图的输出数目大于输入的数目时,有时更偏向于使用另外一种形式的自动微分,称为前向模式累加 (forward mode accumulation)。前向模式计算已经被提出用于循环神经网络梯度的实时计算,例如 (Williams and Zipser, 1989)。
- 这也避免了存储整个图的值和梯度的需要,是计算效率和内存使用的折中。前向模式和后向模式的关系类似于左乘和右乘一系列矩阵之间的关系,例如 A B C D \boldsymbol{ABCD} ABCD,其中的矩阵可以认为是 Jacobi \text{Jacobi} Jacobi矩阵。例如,如果 D \boldsymbol{D} D是列向量,而 A \boldsymbol{A} A有很多行,那么这对应于一幅具有单个输出和多个输入的图,并且从最后开始乘,反向进行,只需要矩阵-向量的乘积。这对应着反向模式。相反,从左边开始乘将涉及一系列的矩阵-矩阵乘积,这使得总的计算变得更加昂贵。然而,如果 A \boldsymbol{A} A的行数小于 D \boldsymbol{D} D的列数,则从左到右乘更为便宜,这对应着前向模式。
- 在机器学习以外的许多社区中,更常见的是使用传统的编程语言来直接实现微分软件,例如用 Python \text{Python} Python或者 C \text{C} C来编程,并且自动生成使用这些语言编写的不同函数的程序。在深度学习界中,计算图通常使用由专用库创建的明确的数据结构表示。专用方法的缺点是需要库开发人员为每个操作定义bprop方法,并且限制了库的用户仅使用定义好的那些操作。然而,专用方法也允许定制每个操作的反向传播规则,允许开发者以非显而易见的方式提高速度或稳定性,对于这种方式自动的过程可能不能复制。
- 因此,反向传播不是计算梯度的唯一方式或最佳方式,但它是一个非常实用的方法,继续为深度学习社区服务。在未来,深度网络的微分技术可能会提高,因为深度学习的从业者更加懂得了更广泛的自动微分领域的进步。
高阶微分
- 一些软件框架支持使用高阶导数。在深度学习软件框架中,这至少包括 Theano \text{Theano} Theano和 TensorFlow \text{TensorFlow} TensorFlow。这些库使用一种数据结构来描述要被微分的原始函数,它们使用相同类型的数据结构来描述这个函数的导数表达式。这意味着符号微分机制可以应用于导数(从而产生高阶导数)。
- 在深度学习的相关领域,很少会计算标量函数的单个二阶导数。相反,我们通常对 Hessian \text{Hessian} Hessian矩阵的性质比较感兴趣。如果我们有函数 f : R n → R f:\mathbb{R}^n\rightarrow\mathbb{R} f:Rn→R,那么 Hessian \text{Hessian} Hessian矩阵的大小是 n × n n\times n n×n。在典型的深度学习应用中, n n n将是模型的参数数量,可能很容易达到数十亿。完整的 Hessian \text{Hessian} Hessian矩阵因此甚至不能表示。
- 代替明确地计算 Hessian \text{Hessian} Hessian矩阵,典型的深度学习方法是使用Krylov方法(Krylov method)。Krylov方法是用于执行各种操作的一组迭代技术,这些操作包括像近似求解矩阵的逆、或者近似矩阵的特征值或特征向量等,而不使用矩阵-向量乘法以外的任何操作。
- 为了在 Hessian \text{Hessian} Hessian矩阵上使用Krylov方法,我们只需要能够计算 Hessian \text{Hessian} Hessian矩阵 H \boldsymbol{H} H和一个任意向量 v \boldsymbol{v} v间的乘积即可。实现这一目标的一种直观方法 (Christianson,1992)是: H v = ∇ x [ ( ∇ x f ( x ) ) ⊤ v ] \boldsymbol{Hv}=\nabla_{\boldsymbol{x}}\left[(\nabla_{\boldsymbol{x}}f(x))^\top\boldsymbol{v}\right] Hv=∇x[(∇xf(x))⊤v]
- 该表达式中两个梯度的计算都可以由适当的软件库自动完成。注意,外部梯度表达式是内部梯度表达式的函数的梯度。
- 如果 v \boldsymbol{v} v本身是由计算图产生的一个向量,那么重要的是指定自动微分软件不要对产生 v \boldsymbol{v} v的图进行微分。
- 虽然计算 Hessian \text{Hessian} Hessian通常是不可取的,但是可以使用 Hessian \text{Hessian} Hessian向量积。可以对所有的 i = 1 , … , n i = 1,\dots,n i=1,…,n简单地计算 H e ( i ) \boldsymbol{H}_{e}^{(i)} He(i),其中 e ( i ) e^{(i)} e(i)是 e i ( i ) = 1 e_i^{(i)}=1 ei(i)=1并且其他元素都为 0 0 0的one-hot向量。
总结
反向传播算法和自动微分技术是神经网络训练过程中不可或缺的组成部分。它们通过高效地计算梯度,使得神经网络的参数优化成为可能。反向传播算法利用链式法则,通过反向传播误差信号来逐层调整网络参数,而自动微分技术则通过构建计算图来自动完成这一复杂的计算过程。这些技术的结合,极大地简化了神经网络的训练过程,提高了模型的训练效率和性能。
本章涉及知识点参考往期内容
应用数学与机器学习基础 - 线性代数篇
深度网络现代实践 - 深度前馈网络之基于梯度的学习篇
深度网络现代实践 - 深度前馈网络之反向传播和其他的微分算法篇