什么是计算图
什么计算图呢?计算图跟图计算不一样,图计算是对基于图数据的计算的统称。而计算图是对一系列计算和数据流转编排之后形成的有向无环图的描述。搞过大数据的应该都对大数据的调度及依赖任务编排比较熟悉,我们会将前后依赖的任务进配置,设置个任务间的依赖关系,然后形成了一个关于各种数据加工和依赖任务的有向无环图(DAG),在这个有向无环图中,图中的顶点(Vertex、Node)是一个个用于执行某种计算的任务,顶点直接的关系(Relationship也就是边,Edge)有的是数据流转,有的是任务依赖。其实TensorFlow中的计算图也基本上类似与这种有DAG图。
通常一个机器学习任务的核心是定义模型和求解参数,在对模型定义和参数求解过程进行抽象之后,在确定了数据的流转方式、数据的计算方式以及各种计算之间的相互依赖关系之后,就可以确定一个唯一的计算执行逻辑,然后将这个计算执行逻辑用图表示,最后我们称这个有向无环图为计算图。
TensorFlow中的计算图由点(nodes)和边(edges)组成,节点表示算子,边表示算子间的依赖或数据(一般是张量)的传递方向,其中实线表示有数据传递依赖,传递的数据即为张量;而虚线通常表示控制依赖,即执行先后顺序,不存在数据传递依赖。所有的节点都通过边连接,其中入度为0的节点没有前置依赖,可以立即执行;入度大于0的节点,要等待其前置依赖的所有节点执行结束之后才能执行。下图就是TensorFlow中一个简单的计算图示例:
计算图创建好了之后,TensorFlow就会需要启动Session去执行计算图,在TensorFlow中,一个Session可以执行多个计算图,每个计算图之间的执行相互独立。计算图的执行参考了拓扑排序的思想,关于拓扑排序,如果有不清楚的,可以参考我的另外一篇文章——直观理解:拓扑排序。计算图G
的执行大体可以分为如下4个步骤:
- a. 以节点id(node_id)作为
key
、入度(in_degree)作为value
创建哈希表map
,并将计算图G
中的所有节点(nodes)加入map
中。 - b. 为计算图
G
创建一个可执行节点队列queue
,将map
中入度为0的节点加入queue
,并从map
中删除这些节点。 - c. 依次执行
queue
中的每一个节点,执行成功之后将此节点输出指向的节点的入度减1,更新map
中对应节点的入度。 - d. 重复步骤b和c,直至
queue
为空。
TensorFlow在发展过程中一共提供了三种计算图的构建方式,分别是静态计算图、动态计算图和AutoGraph。其中静态计算图的构建是TensorFlow在1.0提供的基础功能,但是原生的静态图构建这个功能在TensorFlow2.0之后被弃用,但为了保持对1.0版本的兼容,TensorFlow2.0在compat
包中提供了兼容1.0版本的静态图构建方式。关于三种计算图的构建方式的优劣及不同,我们在后面按章节进行详细讲解。
静态计算图
TensorFlow1.0是采用静态计算图的方式来构建计算图,需先用TensorFlow中的各种算子创建计算图,然后开启一个Session来显式地执行计算图。TensorFlow2.0为了保证对TensorFlow1.0项目的兼容性,在tf.compat.v1
子模块中保留了对TensorFlow1.0
提供的静态计算图构建方式的支持。但是在TensorFlow2.0中,这种静态图的构建方式已经不被推荐,后面渐渐可能会被舍弃。下面我们以兼容包里的静态计算图构建方式来展示静态计算图的构建方式。代码如下:
import tensorflow as tf
#定义静态计算图g
g = tf.compat.v1.Graph()
with g.as_default():
#placeholder为占位符,会话执行的时候会填充具体的对象内容
x = tf.compat.v1.placeholder(name='x', shape=[], dtype=tf.string)
y = tf.compat.v1.placeholder(name='y', shape=[], dtype=tf.string)
z = tf.strings.join([x, y], name="join", separator=" ")
#开启一个session,执行计算图g
result = None
with tf.compat.v1.Session(graph=g) as sess:
# fetches的结果非常像一个函数的返回值,而feed_dict中的占位符相当于函数的参数序列。
result = sess.run(fetches=z, feed_dict={x:"Hello", y:"World!"})
#打印计算结果
tf.print(result)
结果如下:
b'Hello World!'
动态计算图
动态计算图,也称之为Eager Execution,其和静态计算图最大的区别在于,动态计算图无需显式的定义计算图,然后开启个session来执行,在动态计算图中,默认开启session,所有的算子定义之后立即执行。示例代码如下:
# 动态计算图在每个算子构建后立即执行
x = tf.constant("Hello")
tf.print("x:", x)
y = tf.constant("World!")
tf.print("y:", y)
result = tf.strings.join([x, y], separator=" ")
tf.print("result:", result)
结果如下:
x: "Hello"
y: "World!"
result: "Hello World!"
另外,从模块化和函数化编程的角度出发,也可以将上述的动态计算图进行函数化封装,从而将计算图的输入和输出封装在一个函数里面,示例代码如下:
# 将x,y及result的输入输出关系封装成函数
def str_join(x,y):
tf.print("x:", x)
tf.print("y:", y)
return tf.strings.join([x, y], separator=" ")
result = str_join(tf.constant("Hello"), tf.constant("World!"))
tf.print("result:", result)
结果如下:
x: "Hello"
y: "World!"
result: "Hello World!"
AutoGraph
使用动态计算图(Eager Execution)的好处是方便代码调试,因为所有的中间过程可以在写代码过程中立即执行并显示结果。但是动态计算图的运行效率相对较低,因为Eager Execution会有许多次Python进程和TensorFlow的C++进程之间的通信。而静态计算图构建完成之后几乎全部在TensorFlow内核上使用C++代码执行,无需与Python进程频繁进行交互通信,因而效率更高。此外静态图会对计算步骤进行一定的优化,省略和结果无关的计算步骤。鉴于此,TensorFlow2.0提供了tf.function
让算子从 Eager Execution 切换到静态计算图执行。可以使用@tf.function注解将普通Python函数转换成对应的TensorFlow计算图,而调用该函数就相当于在TensorFlow1.0中开启一个Session执行计算图。这种使用tf.function构建静态图的方式就叫做 Autograph。
在TensorFlow2.0中,如果采用Autograph的方式使用计算图,第一步需要定义函数,第二步调用函数。无需显示定义计算图,然后显式地开启session去执行计算图,执行计算图变得跟Python中函数的定义和调用一样简单。下面用代码来展示采用Autograph的计算图执行方式,代码如下:
import tensorflow as tf
# 使用autograph构建静态图
@tf.function
def str_join(x, y):
tf.print("x:", x)
tf.print("y:", y)
return tf.strings.join([x, y], separator=" ")
# 调用函数@tf.function装饰的函数,执行计算图
result = str_join(tf.constant("Hello"), tf.constant("World!"))
tf.print("result:", result)
结果如下:
hello world
tf.Tensor(b'hello world', shape=(), dtype=string)
三种计算图对比
静态计算图,动态计算图和AutoGraph在执行效率和编程体验两方面各有取舍。原始的使用静态计算图需要严格分两步,第一步定义计算图,第二步在会话中执行计算图。而在TensorFlow2.0中,在兼容原始静态图构建方式的同时,新推出了采用Autograph的方式使用计算图,使得计算图的定义和使用分别变成了定义函数和调用函数,兼顾效率的同时,极大的提升了编程体验和效率。关于三种计算图的比较,分别从定义、执行和效率三个方面做了简单的总结:
- 定义:静态计算图需要严格遵循先定义,后使用的原则,定义过程比较麻烦;而动态计算图和AutoGraph的计算图定义更接近普通的Python任务编程,定义过程比较简单。
- 执行:静态计算图需要显示开启session后执行,比较麻烦;动态计算图无需显示执行,可以即刻执行并查看中间结果;AutoGraph调用函数即可执行,简单易用,调用后会在后台会按照静态图一样的方式执行。
- 效率:静态计算图后台有优化策略,效率最高;AutoGraph装饰后的函数也按静态图的方式去执行;动态计算图由于Eager Execution,C++内核和Python内核需要进行频繁的交互,效率最低。
网友评论