Backpropagation 课程笔记翻译

Table of Contents:

  • Introduction
  • Simple expressions, interpreting the gradient
  • Compound expressions, chain rule, backpropagation
  • Intuitive understanding of backpropagation
  • Modularity: Sigmoid example
  • Backprop in practice: Staged computation
  • Patterns in backward flow
  • Gradients for vectorized operations
  • Summary

一、Introduction(简介)

动机。这一节中我们将介绍一些更为深入的东西,也就是反向传播,通过这一节争取能够对反向传播有一个直观的印象。反向传播的目标是求表达式梯度,它利用了链式法则,通过递归的调用链式法则得到表达式的梯度【直译:我们用反向传播的直观印象阐述专业知识,反向传播是一种扫过递归调用链式法则计算表达式梯度的方法】。理解其中的机制以及其中的精妙之处至关重要,因为这将决定你将来是否能更有效的设计、开发,以及调试好一个神经网络。

问题描述。在这一节中,我们要研究的核心问题如下:已知函数 ( f ( x ) (f(x) (f(x),其中 x x x是向量,我们希望得到 f f f x x x上的梯度( ∇ f ( x ) \nabla f(x) f(x))。

动机。回忆一下主要原因 在这个问题中我们感兴趣的是 对于神经网络这样一种具体情况而言, f f f对应损失函数( L L L)以及输入 x x x由训练数据以及神经网络权重构成。举个例子,我们用SVM损失函数作为 f f f的表达式,输入部分由训练数据( ( x i , y i ) , i = 1 … N (x_i,y_i), i=1 \ldots N (xi,yi),i=1N),参数(包括权重 W W W和偏置 b b b)这两部分组成。注意,我们假设训练数据是给定的(这点在机器学习中非常常见),权重是我们可以控制变量。因此,即使我们可以用反向传播非常方便计算在输入样本 x i x_i xi上的梯度,但是我们一般不这么做,我们一般是计算在参数(也就是 W , b W,b W,b)上的梯度,因为执行参数更新只需要参数的梯度【原文是因为这样我们就可以执行参数更新了】。但是,在这个课程的后面我们会看到,在 x i x_i xi上的梯度有的时候也很有用,比如用于可视化的时候,或者用于说明什么样神经网络能够起作用的时候。

即使你已经能够熟练的运用链式法则推导出导数,我们依然强烈建议你看一下这一节。本章介绍了一种观点,在这种观点上相关的研究极其地成熟和完善【直译是本章介绍了一种罕见成熟的观点】。如果你能从中获得一些洞见,那么将对你学习这门课是大有裨益。这种观点把损失函数表达式看成一个回路(虽说叫回路,并不是因为存在一个封闭环形的图,其实它的形状是一个树形的,像一个横躺的树,这里需要计算图的概念知识)。之所以叫回路是因为数字的流动是双向的(这里说数字流动,与其叫做流动,更准确的节点上的数据按照层次的顺序一层一层的生成,因此称“传递”更准确一些,但是如果非要拿“回路”这个模型进行比喻的话,那么“流动”这个词确实和“回路”更加配套),既可以正向流动(我们定义从树枝的末端往树枝的根这个方向是正向),这个过程对应就是前向传播(也就是把变量值代入到表达式中计算表达式的值),也可以反向流动,这个就对应梯度从树根处向树枝末端传播,也就是反向传播。反向传播可以看成这个回路中的“逆流”。【为了把这个话说明白,这里做了大量的细节补充,其实就是下面即将要讲的内容,浓缩了一下写在这里】

二、Simple expressions and interpretation of the gradient(简单表达式的梯度以及如何理解这个梯度【原文也可以翻译成:简单表达式与梯度的解释】)

为了能够表达复杂的表达式,我们需要一些做一些约定并引入一些记号,让我们从简单的表达式开始【直译:让我从简单的情况开始,这样我们能够开发一些记号和约定为了更复杂的表达式】。假设有一个简单的函数是由两个数相乘构成的 ( f ( x , y ) = x y (f(x,y) = x y (f(x,y)=xy。求在每个输入上偏导数,这是一个简单的微积分问题。
f ( x , y ) = x y → ∂ f ∂ x = y ∂ f ∂ y = x f(x,y) = x y \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = y \hspace{0.5in} \frac{\partial f}{\partial y} = x f(x,y)=xyxf=yyf=x

解读。要记住导数体现的意义【直译:导数告诉了你什么】:导数体现了函数在某个点周围无限小的一个区域内相对于某个变量的变化率 。
d f ( x ) d x = lim ⁡ h   → 0 f ( x + h ) − f ( x ) h \frac{df(x)}{dx} = \lim_{h\ \to 0} \frac{f(x + h) - f(x)}{h} dxdf(x)=h 0limhf(x+h)f(x)

注意,等式左边看起来像除法的那个记号其实不是除法,而等式右边的除法记号表达的才是除法。这个记号实际上表达这样一个含义:操作符 d d x \frac{d}{dx} dxd作用在函数 f f f上,返回另一个函数(原函数的导数)【instead直译过来是替代,但是这里作为一个逻辑转折,提现了“不是…而是…”的感觉,具体的翻译可以根据语境需要调整】。一种比较好的理解上面表达式的方式是:当 h h h很小时,函数可以被一条直线很好的近似,而导数就是这个直线的斜率【直译:一个好的思考上面表达式的方式是】。从另一个角度说,导数体现了一种“敏感度”,比如说在某一个变量上很微小的变化就能导致整个函数的很大幅度的变化,那么整个函数就对这个变量敏感。函数在各个变量的上的导数(偏导数)体现了函数对这个变量的敏感程度【直译:换句话说,各个变量上导数告诉你整个函数在相应变量上的敏感程度】。比如说:当 x = 4 , y = − 3 x=4,y=-3 x=4,y=3那么 f ( x , y ) = − 12 f(x,y)=-12 f(x,y)=12而在 x x x上的导数是 ∂ f ∂ x = − 3 \frac{\partial f}{\partial x} = -3 xf=3。这个导数体现的意义是这样的:如果我们在 x x x上增加一个很小的值,那么整个表达式将会变小(因为导数是负数),整个表达式减小的量是 x x x上增加的量的3倍【这告诉我们如果我们在这个变量上增加一个很小的值,效果作用于整个表达式将会是减小(因为带负号),而且是3倍于那个值】。通过对上面公式的等价变换,也可以得到同样的结果($ \frac{df(x)}{dx} = \lim_{h\ \to 0} \frac{f(x + h) - f(x)}{h} \rightarrow f(x + h) = f(x) + h \frac{df(x)}{dx} ) 【 直 译 : 这 点 通 过 重 新 排 列 上 面 表 达 式 也 得 到 】 。 类 似 的 , 由 于 )【直译:这点通过重新排列上面表达式也得到】。类似的,由于 \frac{\partial f}{\partial y} = 4 , 对 y 增 加 一 个 很 小 的 量 ,对y增加一个很小的量 yh$会使得整个函数的输出增加(因为导数值是正数),而且增加的幅度是 4 h 4h 4h

函数在各个变量的上的导数体现了函数对这个变量的敏感程度

正如前文所说,梯度 ∇ f \nabla f f是由偏导数构成的向量,因此我们有$\nabla f = [\frac{\partial f}{\partial x}, \frac{\partial f}{\partial y}] = [y, x] 。 ∗ ∗ 虽 然 严 格 上 来 说 梯 度 是 一 个 向 量 , 比 较 准 确 的 表 述 方 法 应 该 是 “ 在 。**虽然严格上来说梯度是一个向量,比较准确的表述方法应该是“在 x 上 的 偏 导 数 ” , 但 是 为 了 方 便 , 我 们 经 常 用 “ 在 上的偏导数”,但是为了方便,我们经常用“在 便x 上 的 梯 度 ” 这 样 的 说 法 来 代 替 之 前 准 确 的 说 法 ∗ ∗ 【 尽 管 在 技 术 上 来 说 梯 度 是 一 个 向 量 , 为 了 简 便 , 我 们 经 常 使 用 “ 在 上的梯度”这样的说法来代替之前准确的说法**【尽管在技术上来说梯度是一个向量,为了简便,我们经常使用“在 便使x 上 的 梯 度 ” 这 样 的 术 语 , 来 代 替 技 术 上 正 确 的 表 述 “ 在 上的梯度”这样的术语,来代替技术上正确的表述“在 x$上的偏导数”】【也就是说这里把梯度和导数混用了,相当于把分量说成向量】。

我们推导出加法操作的导数:
f ( x , y ) = x + y → ∂ f ∂ x = 1 ∂ f ∂ y = 1 f(x,y) = x + y \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = 1 \hspace{0.5in} \frac{\partial f}{\partial y} = 1 f(x,y)=x+yxf=1yf=1
也就是说,不管 x x x y y y是多少,在它们上的导数都为1。这点很合理,因为不管增加 x x x还是 y y y都会增加 f f f的输出,而且增加的比率都与 x x x或者 y y y实际值没有什么关系(和上面的乘法不一样)。另一个我们用的非常多的函数是 max函数:

f ( x , y ) = max ⁡ ( x , y ) → ∂ f ∂ x = 1 ( x > = y ) ∂ f ∂ y = 1 ( y > = x ) f(x,y) = \max(x, y) \hspace{0.5in} \rightarrow \hspace{0.5in} \frac{\partial f}{\partial x} = \mathbb{1}(x >= y) \hspace{0.5in} \frac{\partial f}{\partial y} = \mathbb{1}(y >= x) f(x,y)=max(x,y)xf=1(x>=y)yf=1(y>=x)

也就是,梯度(严格上来说是次梯度)在较大的输入上为1,在另一个输入上为0。直观上,如果输入是 x = 4 , y = 2 x = 4,y = 2 x=4,y=2,则 max是4,而且函数对于 y y y的取值不敏感。也就是说,如果我们在 y y y上增加一个小的变化量 h h h,函数的输出仍然是4,因此梯度是0:对输出的变化没有产生任何效果。当然如果我们在 y y y上施加一个比较大的变化量(比如大于2),那么函数 f f f的值就会发生变化,但是在输入上施加一个比较大的扰动对函数输出值的变化会产生什么样的影响,我们无法从导数中得知【原文直译:导数并没有告诉我们任何在函数输入上较大的改变产生的影响】。因为,如同导数定义中 lim ⁡ h → 0 \lim_{h \rightarrow 0} limh0所表示的那样,导数仅在输入上的一个很小,无线趋近于0的变化幅度上有意义【informative愿意是提供有价值的信息,那么在数学的语境中,这个词的含义应该类似于“xxx有意义”。另外,其实这句话是作者想要解释前一句话中,为什么导数不能告诉你输入上的巨大变化能产生什么影响,因为导数的定义就是在极限中定义的。】【参考注释:在简单的公式上求梯度并从直观的角度阐述梯度的意义

三、Compound expressions with chain rule(在复合表达式上使用链式法则)

让我们开始考虑更复杂的情况,表达式包含多个复合函数,比如 ( f ( x , y , z ) = ( x + y ) z (f(x,y,z) = (x + y) z (f(x,y,z)=(x+y)z。虽然这个表达式依旧简单到可以直接进行微分,但是在这里我们使用一种特殊的方法,这种方法可以让我们对反向传播背后的机制有一个直观的理解。注意到这个表达式可以分成两个表达式: ( q = x + y (q = x + y (q=x+y f = q z f = q z f=qz。而且,我们知道如何求出这两个表达式的导数,之前的章节里提到过。 f f f仅由 q q q z z z这两项相乘得到,所以 ∂ f ∂ q = z , ∂ f ∂ z = q \frac{\partial f}{\partial q} = z, \frac{\partial f}{\partial z} = q qf=z,zf=q q q q x x x y y y相加得到,所以 ∂ q ∂ x = 1 , ∂ q ∂ y = 1 \frac{\partial q}{\partial x} = 1, \frac{\partial q}{\partial y} = 1 xq=1,yq=1。我们没有必要了解中间变量 q q q的梯度, ∂ f ∂ q \frac{\partial f}{\partial q} qf的值在这里没有用。我们最关心的是 f f f在各自的输入 x , y , z x,y,z x,y,z上的梯度。链式法则告诉我们正确将这些梯度表达式“串在一起”的方法是相乘。比如 ∂ f ∂ x = ∂ f ∂ q ∂ q ∂ x \frac{\partial f}{\partial x} = \frac{\partial f}{\partial q} \frac{\partial q}{\partial x} xf=qfxq。在实践当中,这其实就是两个存储着梯度的变量相乘。让我们通过一个例子来展示:

# set some inputs
x = -2; y = 5; z = -4

# perform the forward pass
q = x + y # q becomes 3
f = q * z # f becomes -12

# perform the backward pass (backpropagation) in reverse order:
# first backprop through f = q * z
dfdz = q # df/dz = q, so gradient on z becomes 3
dfdq = z # df/dq = z, so gradient on q becomes -4
# now backprop through q = x + y
dfdx = 1.0 * dfdq # dq/dx = 1. And the multiplication here is the chain rule!
dfdy = 1.0 * dfdq # dq/dy = 1

在最后我们把梯度存入变量[dfdx,dfdy,dfdz]中,这三个变量提现了f在x,y,z的敏感度。这是反向传播最简单的例子。之后,为了省事,我们会把df这部分省略,也就是dfdq我们简写为dq。注意,以后我们默认这个梯度是相对于最后的输出的来说的。

这个计算过程可以用下面的回路图来表示

在这里插入图片描述

左边:传导实数的“回路”展示了计算的过程。向前的传导计算从输入到输出的值(用绿色表示)。向后的传导过程实现了反向传播,它从后面开始一直到回路的输入端这一路上递归的调用链式法则计算梯度(用红色表示)【注:向前——就是从输入端往最终结果的方向。向后就是反过来。这个方向以后就是这样默认的】。梯度可以被想象成回路中朝反方向传导的流。

四、Intuitive understanding of backpropagation(反向传播的直观理解)

注意到反向传播是一个优美的局部过程。当回路中的中的每个门接收到输入后就能立刻计算出两样东西:1.输出值。2.这个门的输出对应这个门的输入的本地梯度【原文是:这个门的输入对应这个门的输出的本地梯度】。注意,门可以完全的独立的完成上面两件事情而不需要知道整个回路中的其他部分是怎么样的。但是,一旦前向传播完毕,每个门将最终学习到整个回路输出相对于这个门的输出的梯度【原文是:这个门的输出相对于整个回路输出的梯度】。链式法则告诉我们门需要接收上游传来的梯度并将其和自己每一个本地梯度相乘【直译是,门要接收那个梯度(指之前那句话的梯度,也就是整个回路的输出相对于这个门的输出的梯度,这里我就说是上游回传来的梯度)并且把它和每一个相对于这门的所有输出通过常规计算得到的梯度相乘。什么叫做“每一个相对于这个门的所有输出通过常规计算得到的梯度”,比如说一个门,它有一个输出,但是不止一个输入,那么本地梯度应该有多个,一个输入对应一个本地梯度,也就是这个门的输出相对于每个输入的梯度】

正是由于链式法则的作用,它通过这样的乘法操作【指上游传来的梯度和本地的梯度相乘】,把一个个独立的门整合成一个互相之间存在联系的网络的一部分。

【原文直译是:这些额外乘法产生的原因是因为链式法则把单一的并且相对无用的门转化为一个复杂回路(像整个神经网络一样复杂)中的一个齿轮。看这句话的直译,我都无语了。太缺少背景信息了,我只能脑补到底怎么样才能把其中提到的每个概念连起来成为一个完整的图片。首先额外的乘法,根据前一段来看应该是回传回来的那个梯度和本地梯度的相乘。链式法则把门转化为齿轮是想说啥呢?我觉得应该是就是想表达这样一个意思——神经网络让原本独立的东西变成一个系统上的一部分,因此局部和局部之间就有了互联互通,梯度得以传递,反向传播成为可能】

我们再用之前提到的那个例子来体会它是如何工作的。加法门收到输入[-2,5]计算出输出3。因为这个门计算的是加法操作,所以这个门【的输出】在每个输入上的局部梯度都是+1。回路的其它部分计算最后的结果,也就是-12。 在反向传播的过程中,链式法则被递归的调用,加法门(其输出是乘法门的其中一个输入)学习到【整个网络的输出在】它的输出的上梯度是-4。如果我们把这回路看成一个人,他有自己的意愿,希望最终的输出尽可能的高,由于在加法门上的梯度是负的(绝对值是4),也就是说加法门的输出要是变大反而会使整个回路的输出变小,那么为了最终输出变高,这个人应该希望加法门的输出变小【直译:如果我们把它拟人化,好像它想要得到更高的输出值一样(这个有助于直觉),我们可以认为回路想要加法门的输出小一些(因为负号),并且强度是4】【参考注释:如何理解文中把计算回路比喻成一个人】。继续递归【的调用链式法则】并且把梯度串起来,加法门把收到的梯度和所有的本地梯度相乘(在输入x和y上的梯度都是 1 * -4 = -4)。注意这有我们所期望的效应:如果我们减小x,y(对应他们的负梯度)那么加法门的输出也会减小,这样会继续导致乘法门的输出增加。

首先我们假定回路有自己的意愿,希望能够输出尽可能大的输出。其次反向传播可以看成门之间通过梯度信号进行通信的过程。通过这些信号,门就可以知道自己是增大还是减小输出,最终使得回路的总输出增大【直译是:不管门想要输出增大还是减小(或者以什么样的强度),反向传播可以看成门之间(通过梯度信号)相互通信,以便使得最终的输出变大】

五、Modularity: Sigmoid example(模块性:以S型函数为例 )

我们在上面介绍的几个门是比较随意的。任何可微的函数都可以作为一个门,我们可以把多个门组合成一个门,也可以把一个函数分解成多个门,如果你觉得这样做方便。我们看这样一个表达式:
f ( w , x ) = 1 1 + e − ( w 0 x 0 + w 1 x 1 + w 2 ) f(w,x) = \frac{1}{1+e^{-(w_0x_0 + w_1x_1 + w_2)}} f(w,x)=1+e(w0x0+w1x1+w2)1
我们在之后的课程会看到,这个表达式描述了一个用S型函数作为激励函数的2维的神经元(输入是x,权重是w)。但是这里我们把它想象成一个简单的函数,从x,w获得输入之后计算出一个标量。这个函数由多个门构成。除了之前已经提到过三种门(加法门、乘法门,max门)之外,这还有另外四种:
f ( x ) = 1 x → d f d x = − 1 / x 2 f c ( x ) = c + x → d f d x = 1 f ( x ) = e x → d f d x = e x f a ( x ) = a x → d f d x = a f(x) = \frac{1}{x} \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = -1/x^2 \\ f_c(x) = c + x \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = 1 \\ f(x) = e^x \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = e^x \\ f_a(x) = ax \hspace{1in} \rightarrow \hspace{1in} \frac{df}{dx} = a f(x)=x1dxdf=1/x2fc(x)=c+xdxdf=1f(x)=exdxdf=exfa(x)=axdxdf=a

其中,函数 f c f_c fc是把输入输入平移了c的距离,函数 f a f_a fa是把输入放大的a倍。严格上来说,这两个都是加法门和乘法门的特殊情况,但是我们把它看成一个新的一元门单元,因为我们确实需要常数c,a的梯度。完整的回路图看起来如下图:
在这里插入图片描述

一个使用了S激活函数的2D神经元的例子。输入是[x0,x1]而且神经元的权重(可被学习)是[w0,w1,w2]。我们在后面就会看到,神经元将输入和权重之间做了点积,之后他们的结果被S型函数轻柔地压缩到了0和1之间。

在上面的例子中,我们看到一个长长的函数应用链条,它作用于w和x之间的点积的结果之上。实现这些操作的函数被称为sigmoid函数。如果我们使用这样用技巧,在分子上先加1再减去1,那么sigmoid函数的求导过程就变得非常简单【直译:S函数对输入的导数变得简单如果在使用分子上加1再减1这样有趣的技巧之后进行推导】。
σ ( x ) = 1 1 + e − x → d σ ( x ) d x = e − x ( 1 + e − x ) 2 = ( 1 + e − x − 1 1 + e − x ) ( 1 1 + e − x ) = ( 1 − σ ( x ) ) σ ( x ) \sigma(x) = \frac{1}{1+e^{-x}} \\ \rightarrow \hspace{0.3in} \frac{d\sigma(x)}{dx} = \frac{e^{-x}}{(1+e^{-x})^2} = \left( \frac{1 + e^{-x} - 1}{1 + e^{-x}} \right) \left( \frac{1}{1+e^{-x}} \right) = \left( 1 - \sigma(x) \right) \sigma(x) σ(x)=1+ex1dxdσ(x)=(1+ex)2ex=(1+ex1+ex1)(1+ex1)=(1σ(x))σ(x)

可以看到,求导的过程变得不可思议的简单。比如,在前向传播中,sigmoid表达式接受输入1.0并且计算出输入0.73。sigmoid的本地梯度用上面得到的求导结果($ \left( 1 - \sigma(x) \right) \sigma(x)$)就能非常简单的得到,也就是(1 - 0.73) * 0.73 ~= 0.2。这个结果和上面计算图得到结果是一样的,但是得到这个过程的所用表达式既简单又高效还不容易出现数值方面的错误。因此在实践应用中,经常会把一些门组合起来变成一个门。我们看这个神经元的反向传播的代码实现:
【这段的直译是:如同我们看到那样,梯度似乎被简化而且变得令人惊讶的简单,比如在前向传播中,sigmoid表达式接受输入1.0并且计算出输入0.73。上面导数表示本地梯度将简单地是(1-0.73)*0.73 ~=0.2,如同上面回路图计算的那样,除了这种方法利用一个单一,简单和有效的表达式(会有更少的数值问题)】

w = [2,-3,-3] # assume some random weights and data
x = [-1, -2]

# forward pass
dot = w[0]*x[0] + w[1]*x[1] + w[2]
f = 1.0 / (1 + math.exp(-dot)) # sigmoid function

# backward pass through the neuron (backpropagation)
ddot = (1 - f) * f # gradient on dot variable, using the sigmoid gradient derivation
dx = [w[0] * ddot, w[1] * ddot] # backprop into x
dw = [x[0] * ddot, x[1] * ddot, 1.0 * ddot] # backprop into w
# we're done! we have the gradients on the inputs to the circuit

【这里有个问题啊,上面的代码,W和X的尺寸不一样,为什么还可以进行点积呢?】

实现提醒:分段反向传播。如同在代码中显示的那样,在实践中,把前向传播分成容易反向通过的多步是很有用的。比如,这里我们创建了一个中间变量dot来存储w和x之间的点积结果。在反向传播的过程,我们沿着反方向依次计算出那些中间变量对应的局部梯度(比如ddot,以及最终的dw,dx)并保存在专门用来存储中间变量对应的梯度的变量中【注释:什么叫做分步的反向传播(staged backpropagation)】。
这一节的要点 1. 了解反向传播运行机制。2. 把函数表达式中的哪些部分看成门,会对后期运算的便利性产生影响,如果选择得当后面的计算会变得非常简便,比如上面的那个sigmoid的例子。第二点可以帮我们注意到表达式中有哪些部分比较容易求出本地梯度,当我们把整个表达式按照这些部分划分之后,整个表达式可以看成由这些部分串在一起组合而成的,这个时候再用代码来实现整个表达式是最方便的,使用的代码量和计算代价最小。

六、Backprop in practice: Staged computation(反向传播实践:分步计算)

我们再看另一个例子。假设函数具有这样的形式:
f ( x , y ) = x + σ ( y ) σ ( x ) + ( x + y ) 2 f(x,y) = \frac{x + \sigma(y)}{\sigma(x) + (x+y)^2} f(x,y)=σ(x)+(x+y)2x+σ(y)
说明一下,这个式子并不是来自一个具有实际意义的案例,纯粹就是为了说明我们这部分要表达的思想。要强调一点是,如果你一上来就直接对x或者y进行微分的话,你将会面对一个计算量巨大的计算过程,而且中间表达式异常复杂。但是这样做完全没有必要,因为我们不需要为了计算出梯度写出任何具体的函数。我们只需要如何计算它【原文就是这样,这句话写得真的超级不好,有点车轱辘话的感觉,我们的目标是为了计算出梯度,通过求导计算出导函数,通过导函数得到梯度这个方法你说不好,那你倒是给一个更好的方法啊,这块知乎专栏上的同学就把这块补充上来了,其实就是用反向传播的方法计算梯度】。这里展示了我们是如何构造这个函数的前向传播的:

x = 3 # example values
y = -4

# forward pass
sigy = 1.0 / (1 + math.exp(-y)) # sigmoid in numerator #(1)
num = x + sigy # numerator #(2)
sigx = 1.0 / (1 + math.exp(-x)) # sigmoid in denominator #(3)
xpy = x + y #(4)
xpysqr = xpy**2 #(5)
den = sigx + xpysqr # denominator #(6)
invden = 1.0 / den #(7)
f = num * invden # done! #(8)

在表达式的最后我们计算出前向传播的结果(长吁一口气)。注意,在代码中,我们使用了很多中间变量,这些中间变量都是一些简单的表达式,我们能够很容易求出这鞋表达式的梯度。在反向传播的过程中,我们会生成一组新的中间变量。这组变量和之前在前向传播中使用的变量(sigy,num,sigx,xpy,xpysqr,den,invden)一一对应,在名称上,就是在之前的变量名上加了一个“d”(dsigy,dnum,dsigx,dxpy,dxpysr,dden,dinvden),在功能上,它们会保存“整个函数相对于前向传播中用到的那些中间变量的”梯度(比如dnum就是f对num的梯度,也就是df/dnum)。
假设有一个函数,函数的输出为f。回路中有一个门,它实现的计算为op1,输出为gate1。它的输出连接着另一个门,这个门实现的操作为op2,它的输出为gate2。如下图:
在这里插入图片描述

那么f对gate1的导数,根据链式法则可以写成 df/dgate1 = df/dgate2 * dgate2/dgage1。也就是函数对某个门输出的梯度等于,“函数对这个门后继的门的输出的梯度”乘以“后继门对这个门的局部梯度”。
注意,如果某一个门有多个输出(多个后继门)的话,那么函数对这个门的导数则是各个后继方向上的梯度的和。
如下图:gate1有两个后继门,gate2,gate3
在这里插入图片描述

那么函数f对gate1的导数就是“f沿着gate2这条路径上产生的导数”与“f沿着gate3这条路径上产生的导数”的和。
df/dgate1 = df/dgate2 * dgate2/dgate1 + df/dgate3* dgate3/dgate1
这个主要依据的是多元复合函数求导的法则。

# backprop f = num * invden
dnum = invden # gradient on numerator #(8)
dinvden = num #(8)
# backprop invden = 1.0 / den 
dden = (-1.0 / (den**2)) * dinvden #(7)
# backprop den = sigx + xpysqr
dsigx = (1) * dden #(6)
dxpysqr = (1) * dden #(6)
# backprop xpysqr = xpy**2
dxpy = (2 * xpy) * dxpysqr #(5)
# backprop xpy = x + y
dx = (1) * dxpy #(4)
dy = (1) * dxpy #(4)
# backprop sigx = 1.0 / (1 + math.exp(-x))
dx += ((1 - sigx) * sigx) * dsigx # Notice += !! See notes below #(3)
# backprop num = x + sigy
dx += (1) * dnum #(2)
dsigy = (1) * dnum #(2)
# backprop sigy = 1.0 / (1 + math.exp(-y))
dy += ((1 - sigy) * sigy) * dsigy #(1)
# done! phew

上面代码所表示的梯度反向传播过程用图形的表示方式如下:
在这里插入图片描述

留意一些事情:
缓存前向传播变量。一些在前向传播过程中使用的变量在计算反向传播中也会用到。如果能够在前向传播的过程中缓存这些变量,则会对计算反向传播有帮助。你可以在代码中实现这些变量的缓存。但是如果实现变量的缓存实在不方便,也没关系,大不了在反向传播中再计算以便(就是有点浪费)。

七、Patterns in backward flow(逆流中的模式)

梯度的反向流动有一种很有意思的直观解释。以神经网络中3个最常用的门(add,mul,max)为例,他们在反向传播中工作的方式都一种直观的解释。假设有这样一个回路:
在这里插入图片描述

这个图展示了反向传播背后这种直观的感觉。加法操作将大小相同的梯度分别传递到各个输入上。取最大值操作将梯度沿着输入值最大的那个输入方向传递过去。乘法门会把它本身的梯度和各个方向上的输入相乘之后再沿着输入的方向返回去,但是不是原路返回,而是交换了方向传播出去(原来从x方向来的输入,沿着y方向传播出去)
从上图中我们能够看到:
加法门总是:从它的输出得到梯度,沿着它各个输入的方向分发这个梯度,并且各个输入方向上分发的梯度和加法门在输出上获得的梯度大小一样。这个符合这样一个事实: 加法操作的本地梯度就是简单的正整数1,因为加法门沿着输入方向传递的门等于加法门输出上的梯度与加法门本地梯度的乘积,所以最后输入方向获得的梯度跟加法门输出上的梯度一样。在上面的图中,注意加法门将大小为2.00的梯度导向它的两个输出,两个输出获得了同样大小的梯度。

max门转发梯度。并不像加法门那样将梯度一点不变的分发到它所有输入方向上去,max门只把梯度分发到一个方向上去,在这个方向上的输入值是最大的。这是因为max门在最大的值上面的本地梯度是1.0,而在其它值上的本地梯度为0.0 。在上面的图中,max门把值为2梯度转发到了变量z上,z的值比w大,w上的梯度仍然是0。

乘法门稍微有以点难理解。它的本地梯度就是输入的值(需要交换一下),在使用链式法则的时候,这个输入值需要和输出的梯度相乘。在上面的图中,x的梯度是-8.00,也就是 -4.00 x 2.00 。

非直观的效应及这些效应导致的影响。注意如果乘法门的一个输入非常的大,一个输入非常的小,那么乘法门就会做一些有点不直观的事情:他会把相对大的梯度分配给小的输入并把小的梯度分配给大的输入。注意在线性分类器中,权重和输入进行点积 w T x i w^Tx_i wTxi,这意味着数据的大小对权重梯度的大小是有影响的。比如,如果你在预处理过程中把所有的数据样本上都乘以1000,那么权重的梯度会放大1000倍,而且由于这个因素的作用你必须降低学习率来补偿。这就是为什么预处理很重要,有的时候需要用特别精巧的方式!对梯度如何流动有一个直观理解对你进行debug特别有帮助。

八、Gradients for vectorized operations(向量化操作中的梯度)

上面的所述的规律是在标量的条件描述的,但是所有的概念都可以直接扩展到矩阵和向量操作。当操作对象是向量或者矩阵的时候,必须对维度和转置操作非常小心。
矩阵与矩阵相乘的梯度。矩阵和矩阵相乘也许是最有技巧的操作了(这个可以泛化到矩阵和向量相乘以及向量和向量相乘):

# forward pass
W = np.random.randn(5, 10)
X = np.random.randn(10, 3)
D = W.dot(X)

# now suppose we had the gradient on D from above in the circuit
dD = np.random.randn(*D.shape) # same shape as D
dW = dD.dot(X.T) #.T gives the transpose of the matrix
dX = W.T.dot(dD)

提示:利用维度分析!注意你不需要记住dW和dX的表达式,因为基于维度,它们很容易被重新推导出来。举个例子,我们知道在权重上的梯度dW和权重矩阵W的大小是相同的,而且dW依赖于X和dD的矩阵乘法(当X和W都是标量而不是矩阵的时候)。这里总是刚好有一种方法能使维度能够刚好匹配。比如,X的大小是[10 x 3] ,dD的大小是[5 x 3],因此如果我们想得到dW,并且W的大小是[5 x 10],那么得到这尺寸的唯一方法就是dD.dot(X.T),如同上面所示的那样。

用小的,具体的例子。对于某些向量化的表达式来说,一上来推导梯度更新表达式非常困难。为了便于直观,我们建议现在纸上写一些小的,具体的向量作为直观的例子,基于这些小的样例推导出梯度,之后再把它泛化到完整的向量形式上,推导出一般形式。
Erik Learned-Miller 写了一篇关于矩阵/向量求导的的文档。看这里

Summary(总结)

  • 在梯度的意义方面,我们给出了一种直观解释,在回路它们是如何流动的,它们如何通知回路的某一部分增大或者减小,增大减小具体多少,来使最终的输出变大的。
  • 我们讨论了在实际的反向传播中分布计算的重要性。你可以把损失函数分成一些模块,这些模块比较容易求出本地梯度,之后再用链式法则把它们链起来。特别要指出的是,不要对完整的表达式进行求导,因为你不必非要得到一个具体的数学表达式才能计算输入变量的梯度。把你的损失函数分解成若干个部分,这样你可以对每个部分独立地进行进行求导(每个部分有可能是矩阵乘法,也有可能是max函数,或者加法操作等等),之后在进行反向传播的让梯度一步一步地从后往前传播,通过各个变量,每通过一个变量就使用链式法则将梯度累积起来,最后传播到输入层【最后一句的原文是:反向传播通过变量一次一步。这里“backprop”作为动词来使用了,相当于“执行反向传播的操作”,这个操作过程是“one step a time”,也就是一步一步的进行的。“through variables”这句话表达的是一个梯度从输出端往前一个一个的流向各个中间变量的过程】。

References(参考文献)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目:使用 JavaScript 编写的杀死幽灵游戏(附源代码) 杀死鬼魂游戏是使用 Vanilla JavaScript、CSS 和 HTML 画布开发的简单项目。这款游戏很有趣。玩家必须触摸/杀死游荡的鬼魂才能得分。您必须将鼠标悬停在鬼魂上 - 尽量得分。鬼魂在眨眼间不断从一个地方移动到另一个地方。您必须在 1 分钟内尽可能多地杀死鬼魂。 游戏制作 这个游戏项目只是用 HTML 画布、CSS 和 JavaScript 编写的。说到这个游戏的特点,用户必须触摸/杀死游荡的幽灵才能得分。游戏会根据你杀死的幽灵数量来记录你的总分。你必须将鼠标悬停在幽灵上——尽量得分。你必须在 1 分钟内尽可能多地杀死幽灵。游戏还会显示最高排名分数,如果你成功击败它,该分数会在游戏结束屏幕上更新。 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox。要玩游戏,首先,单击 index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
javascript 中的 Paint War Game 是使用 HTML、CSS 和 JavaScript 开发的。谈到游戏玩法,这款游戏的主要目标是建造比敌人更多的油漆砖。您所要做的就是使用 WASD 键输入玩家的动作。您可以使用 VS Code 来运行该项目。 关于项目 每次您的玩家走过一块瓷砖时,它都会被涂成您的团队颜色。您必须在同一块瓷砖上走 4 次才能获得更多游戏点数。瓷砖会被您的团队挡住,并且不能再被偷走。如果您走过另一支球队的瓷砖,它会像您第一次走过时一样被涂上颜色。如果您创建一个封闭的被阻挡瓷砖图形,图形内所有未被阻挡的瓷砖都将固定为您的团队颜色。这个游戏充满乐趣,创造和重新即兴发挥会更有趣。 要运行此项目,我们建议您使用现代浏览器,例如 Google Chrome、  Mozilla Firefox。该游戏可能还支持 Explorer/Microsoft Edge。 演示: javascript 中的 Paint War Game 是使用 HTML、CSS 和 JavaScript 开发的。谈到游戏玩法,这款游戏的主要目标是建造比敌人更多的油漆砖。您所要做的就是使用 WASD 键输入玩家的动作。您可以使用 VS Code 来运行该项目。 关于项目 每次您的玩家走过一块瓷砖时,它都会被涂成您的团队颜色。您必须在同一块瓷砖上走 4 次才能获得更多游戏点数。瓷砖会被您的团队挡住,并且不能再被偷走。如果您走过另一支球队的瓷砖,它会像您第一次走过时一样被涂上颜色。如果您创建一个封闭的被阻挡瓷砖图形,图形内所有未被阻挡的瓷砖都将固定为您的团队颜色。这个游戏充满乐趣,创造和重新即兴发挥会更有趣。 要运行此项目,我们建议您使用现代浏览器,例如 Google Chrome、  Mozilla Firefox。该游戏可能还支持 Explorer/Microsoft Edge。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值