1. 计算图
图是 TensorFlow 最基本的结构,一切计算都是基于图结构运行的,图结构包含张量(Tensor)和操作(Operation)。TensorFlow 1.x 采用的是静态计算图,需要先创建计算图,然后再开启会话 Session,显式执行计算图。在 TensorFlow 2.0 中,采用的是动态计算图,即每使用一个算子后,该算子会被动态加入到默认计算图中即执行,无需开启 Session。
使用动态计算图即 Eager Excution 的优势是方便程序调试。使用动态计算图的缺点是运行效率相对会低一些。因为使用动态图会有许多次 Python 进程和 TensorFlow 的 C++ 进程之间的通信。而静态计算图构建完成之后几乎全部在 TensorFlow 内核上使用 C++ 代码执行,效率更高。此外静态图会对计算步骤进行一定的优化,剪去与结果无关的计算步骤。
如果需要在 TensorFlow 2.0 中使用静态图,可以使用 @tf.function 装饰器将普通 Python 函数转换成对应的 TensorFlow 计算图构建代码。运行该函数就相当于在 TensorFlow 1.x 中使用 Session 执行代码。使用 tf.function 构建静态图的方式叫做 Autograph。
计算图由节点(nodes)和线(edges)组成。节点表示操作符 Operator,线表示计算间的依赖关系。实线表示有数据传递依赖,传递的数据即张量。虚线通常可以表示控制依赖,即执行先后顺序。
2. 静态计算图
静态计算图:静态计算则意味着程序在编译执行时将先生成神经网络的结构,然后再执行相 应操作。从理论上讲,静态计算这样的机制允许编译器进行更大程度的优化,但是这也意味 着你所期望的程序与编译器实际执行之间存在着更多的代沟。这也意味着,代码中的错误将 更加难以发现(比如,如果计算图的结构出现问题,你可能只有在代码执行到相应操作的时 候才能发现它)。
import tensorflow as tf
g = tf.compat.v1.Graph()
with g.as_default():
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=" ")
with tf.compat.v1.Session(graph=g) as sess:
# fetches的结果非常像一个函数的返回值,而feed_dict中的占位符相当于函数的参数序列。
result = sess.run(fetches = z,feed_dict = {x:"hello",y:"world"})
print(result)
b'hello world'
3. 动态计算图
动态计算图:动态计算意味着程序将按照我们编写命令的顺序进行执行。这种机制将使得调试更加容易,并且也使得我们将大脑中的想法转化为实际代码变得更加容易。
# 动态计算图在每个算子处都进行构建,构建后立即执行
x = tf.constant("hello")
y = tf.constant("world")
z = tf.strings.join([x,y], separator=" ")
tf.print(z)
hello world
4. AutoGraph
TensorFlow 2.0主要使用的是动态计算图和Autograph。而Autograph机制可以将动态图转换成静态计算图,兼收执行效率和编码效率之利。动态计算图易于调试,编码效率较高,但执行效率偏低。静态计算图执行效率很高,但较难调试。而Autograph机制通过@tf.function装饰器,可以将动态图转换成静态计算图,达到兼顾执行效率和编码效率的目的。AutoGraph将Python控制流转换为TensorFlow表达式,允许用户在装饰有tf.function的函数中编写常规Python,例如while,if,break,continue和return,支持嵌套。这意味着可以在while和if语句的条件下使用Tensor表达式,或者在for循环中迭代Tensor。当然Autograph机制能够转换的代码并不是没有任何约束的,有一些编码规范需要遵循,否则可能会转换失败或者不符合预期,在使用过程中有以下三个注意事项:
- 使用tf内部函数,避免直接使用python函数,因为无法嵌入进计算图;
- 避免定义 tf.Variable, 以为它是动态的,每次迭代都会更新;
- 不可以修改列表字典等数据结构
import tensorflow as tf
import numpy as np
@tf.function(autograph=True)
def myadd(a,b):
for i in tf.range(3):
tf.print(i)
c = a+b
print("tracing")
return c
程序运行会经历两个步骤:
第一步,会创建一个静态计算图,跟踪执行一遍函数体中的Python代码,确定各个变量的Tensor类型,并根据执行顺序将算子添加到计算图中。 在这个过程中,如果开启了autograph=True(默认开启),会将Python控制流转换成TensorFlow图内控制流。主要是将if语句转换成tf.cond算子表达,将while和for循环语句转换成tf.while_loop算子表达,并在必要的时候添加tf.control_dependencies指定执行顺序依赖关系。
第二步,执行计算图。
# 第一次调用函数
myadd(tf.constant("hello"),tf.constant("world"))
tracing
0
1
2
<tf.Tensor: shape=(), dtype=string, numpy=b'helloworld'>
# 第二次调用函数,只进行第二步,执行计算图。因此打印结果中没有"tracing"。
myadd(tf.constant("hello"),tf.constant("world"))
0
1
2
<tf.Tensor: shape=(), dtype=string, numpy=b'helloworld'>
# 第三次调用函数,由于输入参数的类型已经发生变化,已经创建的计算图不能够再次使用。需要重新创建新的计算图、执行计算图。
myadd(tf.constant(1),tf.constant(2))
tracing
0
1
2
<tf.Tensor: shape=(), dtype=int32, numpy=3>
# 如果调用被@tf.function装饰的函数时输入的参数不是Tensor类型,则每次都会重新创建计算图。
myadd("hello","world")
tracing
0
1
2
<tf.Tensor: shape=(), dtype=string, numpy=b'helloworld'>