用梯度下降算法训练神经网络的时候,求导过程是其中的关键计算之一。使用Tensorflow的用户会发现,神经网络的反向传播计算是用户不用考虑的,在给足便捷性的同时也抑制了用户对反向传播的探索心态(博主深受其害)。Tensorflow同时也激起了一个思考:一定存在某种求导的通用方法。
这篇文章主要探索编程求导的通法。
- 文章代码实现均为Python3.6.4
- 使用模块numpy,sympy
方法1: 定义导函数:
对于函数:y=x2
y
=
x
2
其导数为:y′=2∗x
y
′
=
2
∗
x
对于函数:y=sin(x)
y
=
s
i
n
(
x
)
其导数为:y′=cos(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=sin(x)+x2
y
=
s
i
n
(
x
)
+
x
2
拆分成两个基本函数y1
y
1
=sin(x)
s
i
n
(
x
)
和y2=
y
2
=
x2
x
2
。事实上,这种做法也很难,分解复杂函数是最大的难点。比如y=sin(x2/x)
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=sin(x2/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=x2
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
2
∗
x
可见,这种方法无法把数组当作输入。只能把数组的每一个元素抽出来作为单独的数输入到导函数中求解,这种操作非常慢(矩阵计算可以用GPU并行执行,而转换成单独的数字就不能利用GPU的并行性了)。
方法3. 通过导数的数学定义求导
方法2可以让高数初学者验证自己求导是否正确。方法3可以在实际过程中解决问题:计算速度和精度都达到不错的程度。这就是所谓的“无敌”方法。导数定义如下:
f′(x)=limΔx→0[f(x+Δx)−f(x)]/Δx f ′ ( x ) = lim Δ x → 0 [ f ( x + Δ x ) − f ( x ) ] / Δ x
直接用一个很小的数来代替
Δx
Δ
x
就可以在计算机上实现这个求导公式了。
实现代码如下:
def derive(f):
dx = 0.0000000001
return lambda x: (f(x + dx) - f(x)) / dx
对,这种方法并不能给你返回一个导函数公式,你输入x2
x
2
并不能返回2∗x
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处不可导,那么我们需要对它人工分段,不然可能会有尖刺。虽然对最后结果影响几乎没有,不过注意一下还是好的。