笔者自从接触四、五月份接触深度学习框架以来,一直有个疑问:为什么Tensorflow、theano等框架需要tf.plactholder(…)、T.matrix()等张量。之前的java、python定义变量然后计算不是也可以吗?这个问题一直没有花太多时间去深究,最近由于学习基于python语言的深度学习框架Theano,再一次发现里面好多东西和Python不一样,忍不了这个疑惑然后搜了资料终于发现了一点眉目。
要解决上述问题,首先要弄清楚一个概念:编程范式。范式译自英文的paradigm,也有译作典范、范型、范例的。所谓编程范式(programming paradigm),指的是计算机编程的基本风格或典范模式。借用哲学的术语,如果说每个编程者都在创造虚拟世界,那么编程范式就是他们置身其中自觉不自觉采用的世界观和方法论。我们知道,编程是为了解决问题,而解决问题可以有多种视角和思路,其中普适且行之有效的模式被归结为范式。比如我们常用的“面向对象编程”就是一种范式。
由于着眼点和思维方式的不同,相应的范式自然各有侧重和倾向,因此一些范式常用‘oriented’来描述。换言之,每种范式都引导人们带着某种的倾向去分析问题、解决问题,这不就是“导向”吗?编程范式是抽象的,必须通过具体的编程语言来体现。它代表的世界观往往体现在语言的核心概念中,代表的方法论往往体现在语言的表达机制中。一种范式可以在不同的语言中实现,一种语言也可以同时支持多种范式。比如,java、python可以面向过程编程,也可以面向对象编程。任何语言在设计时都会倾向某些范式,同时回避某些范式,由此形成了不同的语法特征和语言风格。
如果说怎么用简单的话解释什么是编程范式?那就是变成范式就是一种思想,如果说具体的语言(如python、java)中的API是是武功的招式,那编程范式就是武功心法。
目前编程范式比较流行的有命令式编程、符号式编程、声明式编程、函数式编程等范式。回到本文开始提出的问题,平时python、java代码中用到的是命令式编程,具体告诉计算机每一步做什么,而且每一步都要执行,每一步都需要有反馈。Tensorflow、Theano等深度学习框架使用的是符号式编程,开始用张量符号和计算符号构成一个计算图,通过给计算图喂入具体的输入数据,最终得出输出。
下面通过两个具体例子说明命令式编程和符号式编程的区别。
首先关于命令式(imperative style)编程举个例子,代码如下所示:
import numpy as np
a = np.ones(10)
b = np.ones(10) * 2
c = b * a
d = c + 1
当程序执行到 c=b∗a时,代码开始做对应的数值计算。
符号式编程(symbolic style programs)与此不同。它需要先给出一个函数的定义(可能十分复杂)。当我们定义这个函数时,并不会做真正的数值计算。这类函数的定义中使用张量占位符。当给定真正的输入后,才会对这个函数进行编译计算,代码如下例所示:
A = Variable('A')
B = Variable('B')
C = B * A
D = C + Constant(1)
# compiles the function
f = compile(D)
d = f(A=np.ones(10), B=np.ones(10)*2)
上述代码中,语句C=B∗A并不会触发真正的数值计算,但会生成一个计算图(也称symbolic graph)描述这个计算。计算D的计算图如下图所示:
大部分符号式编程都显性或隐性的包含一个编译的步骤,把计算转换成可以调用的函数。上面的例子中,数值计算仅仅在代码最后一行进行。符号式编程一个重要特点是其明确有构建计算图和生成可执行代码两个步骤。对于神经网络,一般会用一个就算图描述整个模型。
到目前为止大概了解了命令式编程、符号式编程。下面就二者的优缺点讲一讲。
命令式编程更加灵活:
用python调用imperative-style库十分简单,编写方式和普通的python代码一样,在合适的位置调用库的代码实现加速。如果用python调用symbolic-style库,代码结构将出现一些变化,比如iteration可能无法使用。尝试把下面的例子转换成symbolic-style。
a = 2
b = a + 1
d = np.zeros(10)
for i in range(d):
d += np.zeros(10)
如果symblic-style API不支持for循环,转换就没那个直接。不能用python的编码思路调用symblic-style库。需要利用symblic API定义的domain-specific-language(DSL)。深度学习框架会提供功能强大的DSL,把神经网络转化成可被调用的计算图。感觉上命令式编程更加符合习惯,使用更加简单。例如可以在任何位置打印出变量的值,轻松使用符合习惯的流程控制语句和循环语句。
符号式编程更加高效:
既然imperative pragrams更加灵活,和计算机原生语言更加贴合,那么为什么很多深度学习框架使用symbolic风格? 最主要的原因式效率,内存效率和计算效率都很高。比如下面的例子:
import numpy as np
a = np.ones(10)
b = np.ones(10) * 2
c = b * a
d = c + 1
...
如果数组每个元素内存中占据8字节,在python中需要多少内存?
对于命令式编程s中,需要在每一行上都分配必要的内存。一共4个数组,每个数组10个元素,一共4∗10∗8=320字节。如果事先知道只有d是需要的结果,构造计算图时可以重复利用一些中间变量的空间。比如利用原址计算,我们可以把b的内存借给c使用,同样c的内存可以给d用,如此可以节省一半内存,仅仅需要2∗10∗8=160字节。符号式编程s限制更多。因为只需要d,构建计算图后,一些中间量,比如c的值将无法看到。通过符号式编程,使用原址计算可以安全的重用内存。但牺牲了对c的访问可能。命令式编程可以处理各种访问可能。如果在python执行上述例子,任何中间量都可以方便访问。
符号式编程还可以通过操作整合(operation folding)优化计算。在上述的例子中,乘法和加法可以展成一个操作,如下图所示。
如果在GPU上运算,计算图只需要一个核心,节省了一个核心。在很多优化库,比如tensorflow/theano,人工编码进行此类优化操作。 操作整合(operation folding)可以提高计算效率。 命令式编程中不能自动操作整合(operation folding),因为不知道中间变量是否会被访问到。符号式编程中可以做操作整合(operation folding),因为获得了完整的计算图,而且明确哪些量以后会被访问,哪些量以后都不会被访问。
总结
命令式编程就是从局部的角度去执行任务,计算机一行一行的执行代码进行反馈。符号式编程更像是从全局的角度去执行任务,先记录下所有操作细节,然后编译。构建一个整体的计算图。因为是从全局的角度看问题,因此有更多空间优化计算,不仅减少了函数调用,还节省了内存,将不同的操作整合成相同的操作,不仅节省资源,并且有增加并行提高效率的可能。
参考
Deep Learning Programming Style
http://www.nowamagic.net/academy/detail/1220210
https://www.cnblogs.com/anliven/p/10349356.html
https://blog.csdn.net/z0n1l2/article/details/80873608