《AutoDiff理解》 之第一篇, 自动求导技术在深度学习中的应用

时间:2024-04-13 15:20:19

前言

自动求导技术(AutoDiff) [1] 是在诸多领域有着广泛应用的技术,特别是在深度学习中,其参数更新大多数依赖于梯度的传递,对于自动求导的使用就更加的频繁了。本系列文章将会基于代码[2]去介绍如何基于计算图的方式去反向自动求导,并且通过value feed的方式去lazy run整个计算图和反向梯度计算图,希望能给各位读者一些启示。本文纯属原创,如有谬误请联系指出,不接受转载。

\nabla联系方式:
e-mail: [email protected]
QQ: 973926198
github: https://github.com/FesianXu
code: https://github.com/FesianXu/ToyAutoDiff


什么是自动求导

求导,大家学过多元微积分都知道,指的是求某个函数ff对于其中某个变量xix_i的一个瞬时变化程度大小的一种一阶衡量方式,表达式如下所示:
(fx1x, ,fxNx) (\dfrac{\partial f}{\partial x_1}\mathbf{x}, \cdots, \dfrac{\partial f}{\partial x_N}\mathbf{x})
其中x=(x1, ,xN)\mathbf{x} = (x_1, \cdots, x_N),也就是说这个函数ff是关于NN个变量的多元函数,我们需要对每一个变量都求导,自动求导机制可以根据某些算法自动地求出这个导数,以便于在后续处理中继续使用。

在深度学习模型中,因为存在着数以千计,数以万计的参数需要在训练过程中更新,这个更新又往往是依赖于梯度信息指引的,因此如果将整个深度模型看成是函数ff,这个ff可以看成是关于这些参数的高维函数,其最后的损失函数为L(f,y)L(f, y),其中的yy表示标签信息。

这个损失函数是和深度模型函数关联在一起的,在实践过程中我们就需要求得损失函数关于每个参数的导数
f=(Lw1, ,LwN) \nabla f = (\dfrac{\partial L}{\partial w_1}, \cdots, \dfrac{\partial L}{\partial w_N})
因此不难看出,在深度学习中,自动求导机制的确是核心之一,因为如果没有这个机制,就需要每设计一个模型都去手动去计算其损失函数对于每一个参数的求导表达式,这个工作量是巨大的。

自动求导的实现方式

自动求导一般来说有几种实现方式,以下分别介绍下:

  1. 基于定义进行计算。(有限微分法, Finite Differencing) 一阶导数的定义很简单,如下所示:
    fx=limϵ0f(x+ϵ)f(x)ϵ \dfrac{\partial f}{ \partial x} = \lim_{\epsilon \rightarrow0} \dfrac{f(x+\epsilon)-f(x)}{\epsilon}
    因此,我们可以通过这种方法,近似地对某个参数的导数进行近似,对于一个多元函数来说,可以用下式进行近似:
    fxn(x)f(x1, ,xn+ϵ, ,xN)f(x1, ,xN)ϵ \dfrac{\partial f}{\partial x_n}(x) \approx \dfrac{f(x_1,\cdots,x_n+\epsilon,\cdots,x_N)-f(x_1,\cdots,x_N)}{\epsilon}
    这个是对参数xnx_n的导数进行的近似,由此可看,用这种方式进行近似我们至少需要NN此计算才能得到结果。这种方式虽然很直观,但是确实低效而且低精度的,低效就是因为其需要和参数量成比例的微分近似计算。而且,因为这里的ϵ\epsilon的值很小,很容易在除法过程和减法过程中导致数值问题,影响计算精度。通常来说,这种方式可以验证计算的梯度是否是正确的,这个过程称之为梯度检验(Gradient check),但是因为其计算量巨大,并不适合直接用于计算多元函数的梯度。[3]

  2. 基于符号计算的方式。 符号计算,通俗地说就是用计算机推导数学公式,如对表达式进行因式分解、化简、微分、积分、解代数方程、求解常微分方程等[4]。典型的代表就是SymPyMathematica软件等,而我们熟悉的MATLAB是基于数值计算的。符号计算的方式去求得微分结果,有一个优势就是其代数运算可以简单地应用在输入代数式或者输出代数式中,生成更加高效的或者数值稳定的代码。比如说,log(1+x)\log(1+x)就可以用log1p进行代替[5],使得结果对于xx来说不那么的敏感,因为前者在xx接近于0的时候容易导致数值问题或者下溢。
    然而这些同样可以通过反向传播的方式实现,反向传播我们下面将会继续提到。符号计算有一些难以解决的问题,比如说很多符号计算库是单变量的,这样就很难在深度学习环境下应用了,而且其对于一个复杂表达式,比如存在多种可以分解的子表达式的时候,就不容易进行重复单元的聚合等,导致冗余。

  3. 基于反向梯度传播的方式。 我们曾经在博客[6]中仔细地推导过深度学习中常用的反向梯度传播算法,并且基于这个算法实现过一个简单的三层网络的训练[7],不过那时候我们的梯度是手动推导出来的,这样效率太低了。其实,基于梯度按照计算图[8]反向传播的方法,我们只要定义了每一个节点的正向传播的compute(self, node, input_vals)和反向传播时候的梯度gradient(self, node, output_grad),那么就完全可以自动地进行求导,只要根据链式法则就行了,整个操作的前提很简单,就是我们的节点中涉及到需要梯度的节点都是可导的, 我们将会见到,有些操作比如argmax其实是无法求导的。我们这个系列的代码也就是根据这个反向传播的原理进行编写的,现在大部分的深度学习框架的自动求导机制也是基于此的。我们将会在下一篇博客中介绍我们的代码编写基本思想。

关于计算图

计算图(computation graph)[8],指的是用节点替代整个函数式中涉及到的所有子操作,并且将其组成图(Graph)的形式表达,这个形式很适合计算机的计算。如下图所示:
《AutoDiff理解》 之第一篇, 自动求导技术在深度学习中的应用
这个图就可以表达一个e=(a+b)(b+1)e = (a+b) * (b + 1)的式子。通过这种方式,我们可以把一个很复杂的计算式分解为一系列简单的子操作的图,然后只要每个子操作都定义了求导计算法则和前向计算法则,那么通过图计算的一些特性,我们就可以组合起来,把复杂问题简单化。我们将会在接下来的博客中继续探讨这个问题,有兴趣的读者欢迎提前移步到github中去[2]。

Reference

[1]. Carpenter B, Hoffman M D, Brubaker M, et al. The Stan math library: Reverse-mode automatic differentiation in C++[J]. arXiv preprint arXiv:1509.07164, 2015.
[2]. https://github.com/FesianXu/ToyAutoDiff
[3]. https://blog.****.net/u012328159/article/details/80232585
[4]. https://baike.baidu.com/item/符号计算/5411445?fr=aladdin
[5]. https://docs.scipy.org/doc/numpy-1.15.1/reference/generated/numpy.log1p.html
[6]. https://blog.****.net/LoseInVain/article/details/78092613
[7]. https://blog.****.net/loseinvain/article/details/78239068
[8]. http://colah.github.io/posts/2015-08-Backprop/