神经网络基础篇:史上最详细_详解计算图(Computation Graph)

发布时间 2023-10-31 10:44:19作者: Oten

计算图

可以说,一个神经网络的计算,都是按照前向或反向传播过程组织的。首先计算出一个新的网络的输出(前向过程),紧接着进行一个反向传输操作。后者用来计算出对应的梯度或导数。计算图解释了为什么用这种方式组织这些计算过程。在这个博客中,将举一个例子说明计算图是什么。让举一个比逻辑回归更加简单的,或者说不那么正式的神经网络的例子。

尝试计算函数\(J\)\(J\)是由三个变量\(a,b,c\)组成的函数,这个函数是\(\text{3(a}+\text{bc)}\) 。计算这个函数实际上有三个不同的步骤,首先是计算 \(b\) 乘以 \(c\),把它储存在变量\(u\)中,因此\({u}={bc}\)
然后计算\(v=a+u\);最后输出\(J=3v\),这就是要计算的函数\(J\)。可以把这三步画成如下的计算图,先在这画三个变量\(a,b,c\),第一步就是计算\(u=bc\),在这周围放个矩形框,它的输入是\(b,c\),接着第二步\(v=a+u\),最后一步\(J=3v\)
举个例子: \(a=5,b=3,c=2\)\(u=bc\)就是6,\(v=a+u\) ,就是5+6=11。\(J\)是3倍的 ,因此。即\(3×(5+3×2)\)。如果把它算出来,实际上得到33就是\(J\)的值。
当有不同的或者一些特殊的输出变量时,例如本例中的\(J\)和逻辑回归中想优化的代价函数\(J\),因此计算图用来处理这些计算会很方便。从这个小例子中可以看出,通过一个从左向右的过程,可以计算出\(J\)的值。为了计算导数,从右到左(红色箭头,和蓝色箭头的过程相反)的过程是用于计算导数最自然的方式。
概括一下:计算图组织计算的形式是用蓝色箭头从左到右的计算,让看看下一个博客中如何进行反向红色箭头(也就是从右到左)的导数计算。

使用计算图求导数(Derivatives with a Computation Graph)

在上一个博客中,看了一个例子使用流程计算图来计算函数J。现在清理一下流程图的描述,看看如何利用它计算出函数\(J\)的导数。

下面用到的公式:

\(\frac{dJ}{du}=\frac{dJ}{dv}\frac{dv}{du}\)\(\frac{dJ}{db}=\frac{dJ}{du}\frac{du}{db}\)\(\frac{dJ}{da}=\frac{dJ}{du}\frac{du}{da}\)

这是一个流程图:

假设要计算\(\frac{{dJ}}{{dv}}\),那要怎么算呢?好,比如说,要把这个\(v\)值拿过来,改变一下,那么\(J\)的值会怎么变呢?

所以定义上\(J = 3v\),现在\(v=11\),所以如果让\(v\)增加一点点,比如到11.001,那么\(J =3v =33.003\),所以这里\(v\)增加了0.001,然后最终结果是\(J\)上升到原来的3倍,所以\(\frac{{dJ}}{{dv}}=3\),因为对于任何 \(v\) 的增量\(J\)都会有3倍增量,而且这类似于在上一个博客中的例子,有\(f(a)=3a\),然后推导出\(\frac{{df}(a)}{{da}}= 3\),所以这里有\(J=3v\),所以\(\frac{{dJ}}{{dv}} =3\),这里\(J\)扮演了\(f\)的角色,在之前的博客里的例子。

在反向传播算法中的术语,看到,如果想计算最后输出变量的导数,使用最关心的变量对\(v\)的导数,那么就做完了一步反向传播,在这个流程图中是一个反向步骤。

来看另一个例子,\(\frac{{dJ}}{da}\)是多少呢?换句话说,如果提高\(a\)的数值,对\(J\)的数值有什么影响?

好,看看这个例子。变量\(a=5\),让它增加到5.001,那么对v的影响就是\(a+u\),之前\(v=11\),现在变成11.001,从上面看到现在\(J\) 就变成33.003了,所以看到的是,如果让\(a\)增加0.001,\(J\)增加0.003。那么增加\(a\),是说如果把这个5换成某个新值,那么\(a\)的改变量就会传播到流程图的最右,所以\(J\)最后是33.003。所以J的增量是3乘以\(a\)的增量,意味着这个导数是3。

要解释这个计算过程,其中一种方式是:如果改变了\(a\),那么也会改变\(v\),通过改变\(v\),也会改变\(J\),所以\(J\)值的净变化量,当提升这个值(0.001),当把\(a\)值提高一点点,这就是\(J\)的变化量(0.003)。

首先a增加了,\(v\)也会增加,\(v\)增加多少呢?这取决于\(\frac{{dv}}{da}\),然后\(v\)的变化导致\(J\)也在增加,所以这在微积分里实际上叫链式法则,如果\(a\)影响到\(v\)\(v\)影响到\(J\),那么当让\(a\)变大时,\(J\)的变化量就是当改变\(a\)时,\(v\)的变化量乘以改变\(v\)\(J\)的变化量,在微积分里这叫链式法则。

从这个计算中看到,如果让\(a\)增加0.001,\(v\)也会变化相同的大小,所以\(\frac{{dv}}{da}= 1\)。事实上,如果代入进去,之前算过\(\frac{{dJ}}{{dv}} =3\)\(\frac{{dv}}{da} =1\),所以这个乘积3×1,实际上就给出了正确答案,\(\frac{{dJ}}{da} = 3\)

这张小图表示了如何计算,\(\frac{{dJ}}{{dv}}\)就是\(J\)对变量\(v\)的导数,它可以帮助计算\(\frac{{dJ}}{da}\),所以这是另一步反向传播计算。

现在想介绍一个新的符号约定,当编程实现反向传播时,通常会有一个最终输出值是要关心的,最终的输出变量,真正想要关心或者说优化的。在这种情况下最终的输出变量是J,就是流程图里最后一个符号,所以有很多计算尝试计算输出变量的导数,所以输出变量对某个变量的导数,就用\(dvar\)命名,所以在很多计算中需要计算最终输出结果的导数,在这个例子里是\(J\),还有各种中间变量,比如\(a、b、c、u、v\),当在软件里实现的时候,变量名叫什么?可以做的一件事是,在python中,可以写一个很长的变量名,比如\({dFinalOutputvar}\_{dvar}\),但这个变量名有点长,就用\(dJ\_dvar\),但因为一直对\(dJ\)求导,对这个最终输出变量求导。这里要介绍一个新符号,在程序里,当编程的时候,在代码里,就使用变量名\(dvar\),来表示那个量。

好,所以在程序里是\(dvar\)表示导数,关心的最终变量\(J\)的导数,有时最后是\(L\),对代码中各种中间量的导数,所以代码里这个东西,用\(dv\)表示这个值,所以\(dv=3\),的代码表示就是\(da=3\)

好,所以通过这个流程图完成部分的后向传播算法。

清理出一张新的流程图,回顾一下,到目前为止,一直在往回传播,并计算\(dv=3\),再次,\(dv\)是代码里的变量名,其真正的定义是\(\frac{{dJ}}{{dv}}\)。发现\(da=3\),再次,\(da\)是代码里的变量名,其实代表\(\frac{{dJ}}{da}\)的值。

大概手算了一下,两条直线怎么计算反向传播。

好,继续计算导数,看看这个值\(u\),那么\(\frac{dJ}{du}\)是多少呢?通过和之前类似的计算,现在从\(u=6\)出发,如果令\(u\)增加到6.001,那么\(v\)之前是11,现在变成11.001了,\(J\) 就从33变成33.003,所以\(J\) 增量是3倍,所以\(\frac{{dJ}}{du}= 3\)。对\(u\)的分析很类似对a的分析,实际上这计算起来就是\(\frac{{dJ}}{dv}\cdot \frac{{dv}}{du}\),有了这个,可以算出\(\frac{{dJ}}{dv} =3\)\(\frac{{dv}}{du} = 1\),最终算出结果是\(3×1=3\)

所以还有一步反向传播,最终计算出\(du=3\),这里的\(du\)当然了,就是\(\frac{{dJ}}{du}\)

现在,仔细看看最后一个例子,那么\(\frac{{dJ}}{db}\)呢?想象一下,如果改变了\(b\)的值,想要然后变化一点,让\(J\) 值到达最大或最小,那么导数是什么呢?这个\(J\)函数的斜率,当稍微改变\(b\)值之后。事实上,使用微积分链式法则,这可以写成两者的乘积,就是\(\frac{{dJ}}{du}\cdot \frac{{du}}{db}\),理由是,如果改变\(b\)一点点,所以\(b\)变化比如说3.001,它影响J的方式是,首先会影响\(u\),它对\(u\)的影响有多大?好,\(u\)的定义是\(b\cdot c\),所以\(b=3\)时这是6,现在就变成6.002了,对吧,因为在的例子中\(c=2\),所以这告诉\(\frac{{du}}{db}= 2\)当让\(b\)增加0.001时,\(u\)就增加两倍。所以\(\frac{{du}}{db} =2\),现在想\(u\)的增加量已经是\(b\)的两倍,那么\(\frac{{dJ}}{du}\)是多少呢?已经弄清楚了,这等于3,所以让这两部分相乘,发现\(\frac{{dJ}}{db}= 6\)

好,这就是第二部分的推导,其中想知道 \(u\) 增加0.002,会对\(J\) 有什么影响。实际上\(\frac{{dJ}}{du}=3\),这告诉u增加0.002之后,\(J\)上升了3倍,那么\(J\) 应该上升0.006,对吧。这可以从\(\frac{{dJ}}{du}= 3\)推导出来。

如果仔细看看这些数学内容,会发现,如果\(b\)变成3.001,那么\(u\)就变成6.002,\(v\)变成11.002,然后\(J=3v=33.006\),对吧?这就是如何得到\(\frac{{dJ}}{db}= 6\)

为了填进去,如果反向走的话,\(db=6\),而\(db\)其实是Python代码中的变量名,表示\(\frac{{dJ}}{db}\)

不会很详细地介绍最后一个例子,但事实上,如果计算\(\frac{{dJ}}{dc} =\frac{{dJ}}{du}\cdot \frac{{du}}{dc} = 3 \times 3\),这个结果是9。

不会详细说明这个例子,在最后一步,可以推出\(dc=9\)

所以这个博客的要点是,对于那个例子,当计算所有这些导数时,最有效率的办法是从右到左计算,跟着这个红色箭头走。特别是当第一次计算对\(v\)的导数时,之后在计算对\(a\)导数就可以用到。然后对\(u\)的导数,比如说这个项和这里这个项:

可以帮助计算对\(b\)的导数,然后对\(c\)的导数。

所以这是一个计算流程图,就是正向或者说从左到右的计算来计算成本函数J,可能需要优化的函数,然后反向从右到左计算导数。如果不熟悉微积分或链式法则,知道这里有些细节讲的很快,但如果没有跟上所有细节,也不用怕。在下一个博客中,会再过一遍。在逻辑回归的背景下过一遍,并给介绍需要做什么才能编写代码,实现逻辑回归模型中的导数计算。