TensorFlow中的Eager Execution和自动微分
在传统的TensorFlow开发中,我们需要首先通过变量和Placeholder来定义一个计算图,然后启动一个Session,通过TensorFlow引擎来执行这个计算图,最后给出我们需要的结果。相信大家在入门阶段,最困惑的莫过于想要打印某些向量或张量的值,在Session之外或未执行时,其值不可打印的问题。TensorFlow采用这种反人性的设计方式,主要是为了生成基于符号的计算图,然后通过C++的计算图执行引擎,进行各种性能优化、分布式处理,从而获取优秀的运行时性能。与此形成对照的是以PyTorch为代表的动态图方式,其不用生成基于符号表示的计算图,直接计算结果,与我们平常编程的处理方式类似,无疑这种方式学习曲线会低很多。TensorFlow实际上也注意到了这一问题,在2017年11月,推出的Eager Execution就是这种动态图机制在TensorFlow中的实现。目前虽然Eager Execution在性能上还没有达到静态计算图的效率,但是由于其编程调试的方便性,会在实际应用中得到越来越广泛的应用。
大家还记得深度学习大佬Yann Lecun在前一段时间说的“深度学习已死,可微编程永生”的话吧?实际上Yann Lecun所说的可微编程(Differentiable Programming)还处在非常早期的阶段,Lecun的意思是说这种编程范式具有比深度学习更好的灵活性,有希望解决更为复杂的问题,提醒大家重点关注这一领域。而可微编程这种范式,在TensorFlow的Eager Execution下是可以比较方便实现的。
- 矩阵乘法TensorFlow传统实现方式
- 我们先来以矩阵相乘这一简单的功能为例,来看一下使用TensorFlow传统方法和Eager Execution方法的实现方式。
- 我们首先来看TensorFlow传统实现方式:
def tfsess1(args={}): v1 = tf.constant([[1, 2], [3, 4]]) v2 = tf.constant([[5, 6], [7, 8]]) v3 = tf.matmul(v1, v2) with tf.Session() as sess: rst = sess.run([v3]) print('v3:{0}'.format(rst)) 1 2 3 4 5 6 7
运行结果如下所示:
由上面的代码可以看出,只有启动Session后,我们才能看到矩阵乘法的结果。正是这一点,是许多初学者非常不适应的地方。
- Eager Execution模式
- 如果采用Eager Execution模式,程序如下所示:
def tfee1(args={}): tf.enable_eager_execution() v1 = [[1, 2],[3, 4]] v2 = [[5, 6],[7, 8]] v3 = tf.matmul(v1, v2) print('v3={0}'.format(v3)) print('v3 type:{0}'.format(type(v3))) print('v3 shape:{0}'.format(v3.shape)) print('v3 dtype:{0}'.format(v3.dtype)) print('v3=>ndarray:{0}'.format(v3.numpy())) 1 2 3 4 5 6 7 8 9 10
运行结果如下所示:
由上面的代码可以看出,我们直接采用普通的程序形式,就可以求出矩阵乘法的结果,而且TensorFlow中的Tensor和numpy中的ndarray可以互相无缝转换,非常方便使用。更加有用的是,我们还可以利用使用机器中的GPU。
- 自动微分
- 根据高等数学的知识可知,我们求一个函数的极值,我们可以求函数对自变量的一阶导数,然后令导数为零,此时解出的自变量的取值,就是对应的极值点,我们可以根据极值点邻域值的特点,确定该极值点是极大值还是极小值。当然在有些情况下,我们可以求出二阶导数,根据二阶导数在极值点的符号来判断是极大值点还是极小值点。我们知道,在机器学习和深度学习中,有很大一部分问题都是求极值问题,而非机器学习和深度学习领域,也有大量的求极值问题,因此Yann Lecun才会说“深度学习已死,可微编程永生”的话,就是可微编程比深度学习和机器学习的应用范围要广得多,具有更大的发展前景。而微分编程的基础就是自动微分,我们在这一部分中,将向大家展示一下TensorFlow下怎样实现自动微分。
- 在这里我们先来看一个最简单的例子,我们要求导数的函数为:
- y=f(x)=x
- 2
- y=f(x)=x2
- 对于这个式子,我们只要略懂高等数学,我们不难看出其导数为:
- dy
- dx
- =2x
- dydx=2x
- 但是如果使用计算图来计算导数,那么就需要有一定技巧才能求出这一正确的导数形式了。上式所对应的计算图如下图所示:
我们将x
2
x2 视为两个独立的变量x相乘。我们先来看正向传播,如下图所示:
我们将x
1
=3.0
x1=3.0 和x
2
=3.0
x2=3.0 代入左边两个节点,然后根据计算图到达右边的y节点,执行乘法操作y=x
1
∗x
2
y=x1∗x2,最后得到最终的计算结果,与函数x
2
x2 的计算值相同。
我们接下来看求导的反向传播,如下图所示:
因为在我们的计算图中的公式为y=x
1
∗x
2
y=x1∗x2,所以dy
dx
1
=x
2
=x
dydx1=x2=x,同理dy
dx
2
=x
1
=x
dydx2=x1=x,我们将导数结果写在对应的边上。根据计算图求导规则,对从最终节点到输入节点的全部路径,每条路径上边的导数值相乘,再将所有路径的导数值相加,根据这个计算图,可以得到如下算式:
dy
dx
=dy
dx
1
+dy
dx
2
=x
2
+x
1
=2x=2×3.0=6.0
dydx=dydx1+dydx2=x2+x1=2x=2×3.0=6.0
由此可以看出,这与我们通过高等数学求导公式算出的结果相同。
有了这些知识,我们再来看深度学习框架中静态图和动态图的争论,我们可以有一个更清晰的认识。对于动态图来说,就是每次正向传播还是反向传输,均通过图的遍历方式进行。而静态图则通过对计算的编译,将正向传播和反向传播变为公式,这样就不用再来遍历计算图了,效率因此会提高不少。静态图虽然有很多优点,但是计算图不能动态调整结构,要学习一种元语言才能在计算图中引入条件和循环结构,在模型理解和编程复杂性方面,存在很大的问题。
有了上面这些知识,我们就可以来看一下,怎样通过TensorFlow Eager Execution技术来实现求导问题了。我们在这里假设想求出y=x
2
y=x2的极值,代码如下所示:def f1(x): return x**2 def ad1(args={}): tf.enable_eager_execution() tfe = tf.contrib.eager grad_f1 = tfe.gradients_function(f1) threshold = 0.001 delta_x = 0.0001 x = 3.0 done = False while not done: rst = grad_f1(x) dval = rst[0] if dval > threshold: x -= delta_x elif dval < -threshold: x += delta_x else: done = True y = f1(x) rst = grad_f1(x) print('x={0}, y={1}; d={2}!'.format(x, y, rst[0])) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
运行结果如下所示:
需要指出的是,我们这里给出的代码实现,绝对是效率相当低的一种,如果我们真的想求极值,我们应该采用牛顿法等,收敛速度会成数量级的提高