【AI数学原理】神经网络反向传播的函数求导(精髓篇)

用梯度下降算法训练神经网络的时候,求导过程是其中的关键计算之一。使用Tensorflow的用户会发现,神经网络的反向传播计算是用户不用考虑的,在给足便捷性的同时也抑制了用户对反向传播的探索心态(博主深受其害)。Tensorflow同时也激起了一个思考:一定存在某种求导的通用方法。


这篇文章主要探索编程求导的通法

  • 文章代码实现均为Python3.6.4
  • 使用模块numpy,sympy

方法1: 定义导函数:

对于函数: y = x 2
其导数为: y = 2 x
对于函数: y = s i n ( x )
其导数为: y = c o s ( x )
这里的导函数都需要开发者自己用笔算出来,然后用代码表示出来。为什么可以这么做? 因为神经网络里用到的函数种类有限,我们可以把所有的函数导函数都预先定义出来。如下:

# 目标函数:y=x^2
def func(x):
    return x**2

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
    return 2*x

来看这篇文章的,估计不是为了得到这个答案,为的是解决一般情况的求导公式。而且对于千变万化的loss function,每次都自己手动求导就很麻烦了。
      CSDN上有不少讲解求导的文章,有码友说通过把各种常见基本函数的导数定义好,然后对任何复杂函数进行分解,再查表调用基本函数的导函数,例如 y = s i n ( x ) + x 2 拆分成两个基本函数 y 1 = s i n ( x ) y 2 = x 2 。事实上,这种做法也很难,分解复杂函数是最大的难点。比如 y = s i n ( x 2 / x ) ,这要怎么通过一般式分解呢,当然可以分解,不过分复杂。博主认为,这种思路明显把这个问题弄复杂了。
这种方法,归根结底还是属于查表法

是否认真想过,求导计算能不能用非查表法解决?


方法2:使用专门求导模块

python提供了一个十分好用的求导模块sympy,当然并不是唯一能用来求导的模块。
这个求导方法如下:

from sympy import *
x = Symbol("x") # 把"x"设置为自变量
fu = diff(x ** 2, x) # 求导x^2
print(fu)

输出:

2*x

可以试着把原函数写复杂点: y = s i n ( x 2 / x )
改为fu = diff(sin(x**2)/x, x)
输出为:

2*cos(x**2) - sin(x**2)/x**2

可以发现:这个方法可以求解复杂函数的导数
其实这个方法还有若干缺陷:输入的x只能是数,不能是numpy数组。我们知道,当进行反向传播的时候,输入往往都是数组。此外,很多函数并不是用函数表达式来表示的(下面会举例),没有确切的函数表达式是无法使用这个方法来求导的。

# 目标函数:y=x^2
def func(x):
    return np.square(x)

如上,这个函数没有确切的函数表达式(虽然知道是 y = x 2 ,但无法用这种办法求导)
如果向导函数输入数字:

from sympy import *
x = Symbol("x")
fu = diff(sin(x**2) / x, x)
print(fu.subs('x', 3))

输出: 6
如果向导函数输入数组:

from sympy import *
a = np.array([[1,3,5,7],[7,7,0,0]])
x = Symbol("x")
fu = diff(sin(x**2) / x, x)
print(fu.subs('x', a))

输出: 2 x
可见,这种方法无法把数组当作输入。只能把数组的每一个元素抽出来作为单独的数输入到导函数中求解,这种操作非常慢(矩阵计算可以用GPU并行执行,而转换成单独的数字就不能利用GPU的并行性了)。


方法3. 通过导数的数学定义求导

方法2可以让高数初学者验证自己求导是否正确。方法3可以在实际过程中解决问题:计算速度和精度都达到不错的程度。这就是所谓的“无敌”方法。导数定义如下:

f ( x ) = lim Δ x 0 [ f ( x + Δ x ) f ( x ) ] / Δ x

直接用一个很小的数来代替 Δ x 就可以在计算机上实现这个求导公式了。
实现代码如下:

def derive(f):
    dx = 0.0000000001
    return lambda x: (f(x + dx) - f(x)) / dx

对,这种方法并不能给你返回一个导函数公式,你输入 x 2 并不能返回 2 x 。但在计算机系统中,求导计算并不需要你准确的导函数公式,只要导函数函数值的计算精度高便可以使用。这种方法,尤其在神经网络的方向传播中十分有用!
具体怎么使用呢,我们可以改造一下上面的例子:

# 目标函数:y=x^2
def func(x):
    return np.square(x)

# 目标函数一阶导数:dy/dx=2*x
def dfunc(x):
    return derive(func)(x)

def derive(f):
    dx = 0.0000000001
    return lambda x: (f(x + dx) - f(x)) / dx

可以看出这里的derive方法可以通用。但是呢,这种方法有点点缺陷说明一下:满足数学意义上具有可导性的函数才能用这种方法求解,比如Relu(x)函数,它在x=0处不可导,那么我们需要对它人工分段,不然可能会有尖刺。虽然对最后结果影响几乎没有,不过注意一下还是好的。

猜你喜欢

转载自blog.csdn.net/leviopku/article/details/80434692
今日推荐