有三种计算图的构建方式:静态图,动态计算图,以及AutoGraph;
静态计算图:静态计算则意味着程序在编译执行时将先生成神经网络的结构,然后再执行相应操作。从理论上讲,静态计算这样的机制允许编译器进行更大程度的迭代,但是这也意味着我们所期望的程序与编译器实际执行之间存在着更多的代沟。这也意味着,代码中的错误将更加难以发现(比如,如果计算图的结构出现问题,我们可能只有在代码执行到相应操作的时候才能发现它)动态计算图:动态计算意味着程序将按照我们编写命令的顺序进行执行,这种机制将使得调试更加容易,并且也使得我们将大脑中的想法转化为实际代码变得更加容易;TF2.0 主要使用的是动态计算图和Autograph,而Autograph机制可以将动态图转换成静态计算图,兼收执行效率和编码效率之利;
动态计算图易于调试,编码效率高,但执行效率偏低;静态计算图执行效率很高,但较难调试;
AutoGraph在TensorFlow 2.0通过@tf.function实现的;
当我们使用@tf.function装饰一个函数的时候,后面到底发生了什么呢?
例如我们写下如下代码。
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 myadd(tf.constant("hello"),tf.constant("world"))上述代码的输出结果如下:
tracing 0 1 2代码中发生了2件事情:
第一件事情是创建计算图;第二件事情是执行计算图;因此我们先看到的是第一个步骤的结果:即Python调用标准输出流打印"tracing"语句。
然后看到第二个步骤的结果:TensorFlow调用标准输出流打印1,2,3。
当我们再次用相同的输入参数类型调用这个被@tf.function装饰的函数时,代码并不会输出"tracing",因为计算图已经创建好,所以只会输出"1,2,3"。
当我们再次用不同的的输入参数类型调用这个被@tf.function装饰的函数时,则又会输出"tracing"和"1,2,3",如下所示:
myadd(tf.constant(1),tf.constant(2))由于输入参数的类型已经发生变化,已经创建的计算图不能够再次使用。程序需要重新做2件事情:创建新的计算图、执行计算图。所以我们又会先看到的是第一个步骤的结果:即Python调用标准输出流打印"tracing"语句。然后再看到第二个步骤的结果:TensorFlow调用标准输出流打印1,2,3。
需要注意的是,如果调用被@tf.function装饰的函数时输入的参数不是Tensor类型,则每次都会重新创建计算图。
例如我们写下如下代码。两次都会重新创建计算图。因此,一般建议调用@tf.function时应传入Tensor类型。
myadd("hello","world") myadd("good","morning")被@tf.function修饰的函数应尽量使用TensorFlow中的函数而不是Python中的其他函数。例如使用tf.print而不是print.
解释:Python中的函数仅仅会在跟踪执行函数以创建静态图的阶段使用,普通Python函数是无法嵌入到静态计算图中的,所以 在计算图构建好之后再次调用的时候,这些Python函数并没有被计算,而TensorFlow中的函数则可以嵌入到计算图中。使用普通的Python函数会导致 被@tf.function修饰前【eager执行】和被@tf.function修饰后【静态图执行】的输出不一致。
避免在@tf.function修饰的函数内部定义tf.Variable.
解释:如果函数内部定义了tf.Variable,那么在【eager执行】时,这种创建tf.Variable的行为在每次函数调用时候都会发生。但是在【静态图执行】时,这种创建tf.Variable的行为只会发生在第一步跟踪Python代码逻辑创建计算图时,这会导致被@tf.function修饰前【eager执行】和被@tf.function修饰后【静态图执行】的输出不一致。实际上,TensorFlow在这种情况下一般会报错。
被@tf.function修饰的函数不可修改该函数外部的Python列表或字典等数据结构变量。
解释:静态计算图是被编译成C++代码在TensorFlow内核中执行的。Python中的列表和字典等数据结构变量是无法嵌入到计算图中,它们仅仅能够在创建计算图时被读取,在执行计算图时是无法修改Python中的列表或字典这样的数据结构变量的。
在前面介绍Autograph的编码规范时提到构建Autograph时应避免在@tf.function修饰的函数内部定义tf.Variable;
但是如果在函数外部定义tf.Variable的话,又会显得这个函数有外部变量的依赖,封装不够完美;
一种简单的思路是定义一个类,并将相关的tf.Variable创建放在类的初始化方法中,而将函数的逻辑放在其它方法中;
Tensorflow提供了一个基类tf.Module,通过继承它构建子类,我们不仅可以获得以上的函数逻辑,而且可以非常方便地管理变量,还可以非常方便地管理它引用的其它Module。最重要的是,我们能够利用tf.save_model保存模型并实现跨平台部署使用;
举个例子
先定义一个简单的实例: import tensorflow as tf x = tf.Variable(1.0,dtype=tf.float32) #在tf.function中用input_signature限定输入张量的签名类型:shape和dtype @tf.function(input_signature=[tf.TensorSpec(shape = [], dtype = tf.float32)]) def add_print(a): x.assign_add(a) tf.print(x) return(x) add_print(tf.constant(3.0)) #add_print(tf.constant(3)) #输入不符合张量签名的参数将报错 下面利用tf.Module的子类化将其封装一下: class DemoModule(tf.Module): def __init__(self,init_value = tf.constant(0.0),name=None): super(DemoModule, self).__init__(name=name) with self.name_scope: #相当于with tf.name_scope("demo_module") self.x = tf.Variable(init_value,dtype = tf.float32,trainable=True) @tf.function(input_signature=[tf.TensorSpec(shape = [], dtype = tf.float32)]) def addprint(self,a): with self.name_scope: self.x.assign_add(a) tf.print(self.x) return(self.x) #执行 demo = DemoModule(init_value = tf.constant(1.0)) result = demo.addprint(tf.constant(5.0)) #查看模块中的全部变量和全部可训练变量 print(demo.variables) print(demo.trainable_variables) #查看模块中的全部子模块 demo.submodules #使用tf.saved_model 保存模型,并指定需要跨平台部署的方法 tf.saved_model.save(demo,"./data/",signatures = {"serving_default":demo.addprint}) #加载模型 demo2 = tf.saved_model.load("./data/") demo2.addprint(tf.constant(5.0)) # 查看模型文件相关信息,红框标出来的输出信息在模型部署和跨平台使用时有可能会用到 !saved_model_cli show --dir ./data/ --all