Tensorflow2.0学习——计算图机制详解及AutoGraph

思维导图


有哪些计算图

有三种计算图的构建方式:静态计算图动态计算图 以及 AutoGraph
– 静态计算图(tf1.x提出)
静态计算则意味着程序在编译执行时将先生成神经网络的结构,然后再执行相应操作。从理论上讲,静态计算这样的机制允许编译器进行更大程度的优化,但是这也意味着你所期望的程序与编译器实际执行之间存在着更多的代沟。这也意味着,代码中的错误将更加难以发现(比如,如果计算图的结构出现问题,你可能只有在代码执行到相应操作的时候才能发现它)
– 动态计算图(tf2.x提出)
动态计算意味着程序将按照我们编写命令的顺序进行执行。这种机制将使得调试更加容易,并且也使得我们将大脑中的想法转化为实际代码变得更加容易。

AutoGraph(tf2.x提出)

TensorFlow 2.0主要使用的是动态计算图和 Autograph 。
而Autograph 机制可以将 动态图 转换成 静态计算图 ,兼收执行效率和编码效率之利。

AutoGraph在 TensorFlow 2.0 通过 tf.function 实现的。


AutoGraph使用规范

AutoGraph在使用时应该注意以下几条规范。
– 被 tf.function 修饰的函数应尽量使用 TensorFlow 中的函数而不是 Python 中的其他函数。
– 避免在 tf.function 修饰的函数内部定义 tf.Variable。
– 被 tf.function 修饰的函数不可修改该函数外部的 Python 列表或字典等结构类型变量。

1.被 tf.function 修饰的函数应尽量使用 TensorFlow 中的函数而不是 Python 中的其他函数。

import numpy as np
import tensorflow as tf

@tf.function
def np_random():
    a = np.random.randn(3,3)
    tf.print(a)

@tf.function
def tf_random():
    a = tf.random.normal((3,3))
    tf.print(a)
np_random()
np_random()
# 输出结果:
array([[0.64987334, 1.90851605, 0.5053777 ],
       [0.1401809 , 0.11816129, 0.14474688],
       [0.48343842, 1.90316492, 0.92396541]])
array([[0.64987334, 1.90851605, 0.5053777 ],
       [0.1401809 , 0.11816129, 0.14474688],
       [0.48343842, 1.90316492, 0.92396541]])

按理说random函数每次执行出来的结果应该是不一样的,由下面单独运行的np.random.randn()执行出来的结果可以知道。
而我们用上面的np_random()函数(有@tf.function装饰器修饰的)执行出来的结果发现np_random()每次执行都是一样的结果。
而tf_random()不会出现这种问题。
所以,被@tf.function修饰的函数应尽量使用TensorFlow中的函数而不是Python中的其他函数。

np.random.randn(3,3)
np.random.randn(3,3)
# 输出结果:(每次应该都不一样)
array([[ 0.25257702,  0.16708006,  0.79210834],
       [-0.34683175,  0.69328068,  1.31706466],
       [ 0.5580037 , -1.50770656, -0.79776019]])
array([[ 1.55126589, -0.80073444, -0.92080275],
       [-0.50392245, -0.69853033,  0.21980977],
       [-0.1379339 ,  0.89195606,  1.2976263 ]])

而,使用tf_random()不会出现这种问题。

tf_random()
tf_random()
# 输出结果(每次结果都不一样)
[[0.9276793 -0.528112411 1.88184559]
 [-1.20099926 -1.04864311 0.245232761]
 [-1.71584272 -1.03118122 -0.0328678861]]
[[-0.0503324643 -0.459995121 -1.84465837]
 [1.13207293 0.68550396 -0.458074749]
 [1.17981255 1.2004118 -0.352737874]]

2.避免在 tf.function 修饰的函数内部定义 tf.Variable。

x = tf.Variable(1.0,dtype=tf.float32)
@tf.function
def outer_var():
    x.assign_add(1.0)
    tf.print(x)
    return(x)

outer_var() 
outer_var()
# 输出结果
2
3
<tf.Tensor: shape=(), dtype=float32, numpy=3.0>

#报错
@tf.function
def inner_var():
    x = tf.Variable(1.0,dtype = tf.float32)  #避免在@tf.function修饰的函数内部定义tf.Variable.
    x.assign_add(1.0)
    tf.print(x)
    return(x)
inner_var()

3.被 tf.function 修饰的函数不可修改该函数外部的 Python 列表或字典等结构类型变量。

tensor_list = []

#@tf.function #加上这一行切换成Autograph结果将不符合预期!!!
def append_tensor(x):
    tensor_list.append(x)
    return tensor_list

append_tensor(tf.constant(5.0))
append_tensor(tf.constant(6.0))
print(tensor_list)

#输出结果:
[<tf.Tensor: shape=(), dtype=float32, numpy=5.0>, <tf.Tensor: shape=(), dtype=float32, numpy=6.0>]
# *************************************************

tensor_list = []

@tf.function #加上这一行切换成Autograph结果将不符合预期!!!
def append_tensor(x):
    tensor_list.append(x)
    return tensor_list

append_tensor(tf.constant(5.0))
append_tensor(tf.constant(6.0))
print(tensor_list)

# 输出结果:
[<tf.Tensor 'x:0' shape=() dtype=float32>]

AutoGraph机制原理

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
<tf.Tensor: shape=(), dtype=string, numpy=b'helloworld'>

发生了2件事情:

  • 第一件事情是创建计算图。
  • 第二件事情是执行计算图。

因此我们先看到的是第一个步骤的结果:即Python调用标准输出流打印”tracing”语句。
然后看到第二个步骤的结果:TensorFlow调用标准输出流打印1,2,3。

当我们再次用相同的输入参数类型调用这个被@tf.fuanction装饰的函数时,后面到底发生了什么?

myadd(tf.constant("good"),tf.constant("morning"))
#输出结果
0
1
2
<tf.Tensor: shape=(), dtype=string, numpy=b'goodmorning'>

只会发生一件事情,那就是上面步骤的第二步,执行计算图。 所以这一次我们没有看到打印”tracing”的结果。

当我们再次用不同的的输入参数类型调用这个被@tf.function装饰的函数时,后面到底发生了什么?

myadd(tf.constant(1),tf.constant(2))

# 输出结果
tracing
0
1
2
<tf.Tensor: shape=(), dtype=int32, numpy=3>

由于输入参数的类型已经发生变化,已经创建的计算图不能够再次使用。
需要重新做2件事情:创建新的计算图、执行计算图。
所以我们又会先看到的是第一个步骤的结果:即Python调用标准输出流打印”tracing”语句。
然后再看到第二个步骤的结果:TensorFlow调用标准输出流打印1,2,3。

需要注意的是,如果调用被@tf.function装饰的函数时输入的参数不是Tensor类型,则每次都会重新创建计算图。
例如我们写下如下代码。两次都会重新创建计算图。因此,一般建议调用@tf.function时应传入Tensor类型。

myadd("hello","world")
myadd("good","morning")
#输出结果
tracing
0
1
2
tracing
0
1
2
<tf.Tensor: shape=(), dtype=string, numpy=b'goodmorning'>

重新理解Autograph的编码规范
了解了以上Autograph的机制原理,我们也就能够理解Autograph编码规范的3条建议了。

1,被@tf.function修饰的函数应尽量使用TensorFlow中的函数而不是Python中的其他函数。例如使用tf.print而不是print.

解释:Python中的函数仅仅会在跟踪执行函数以创建静态图的阶段使用,普通Python函数是无法嵌入到静态计算图中的,所以 在计算图构建好之后再次调用的时候,这些Python函数并没有被计算,而TensorFlow中的函数则可以嵌入到计算图中。使用普通的Python函数会导致 被@tf.function修饰前【eager执行】和被@tf.function修饰后【静态图执行】的输出不一致。

2,避免在@tf.function修饰的函数内部定义tf.Variable.

解释:如果函数内部定义了tf.Variable,那么在【eager执行】时,这种创建tf.Variable的行为在每次函数调用时候都会发生。但是在【静态图执行】时,这种创建tf.Variable的行为只会发生在第一步跟踪Python代码逻辑创建计算图时,这会导致被@tf.function修饰前【eager执行】和被@tf.function修饰后【静态图执行】的输出不一致。实际上,TensorFlow在这种情况下一般会报错。

3,被@tf.function修饰的函数不可修改该函数外部的Python列表或字典等数据结构变量。

解释:静态计算图是被编译成C++代码在TensorFlow内核中执行的。Python中的列表和字典等数据结构变量是无法嵌入到计算图中,它们仅仅能够在创建计算图时被读取,在执行计算图时是无法修改Python中的列表或字典这样的数据结构变量的。


AutoGraph的使用方法

前面在介绍Autograph 的编码规范时提到构建 Autograph 时应该避免在tf.function 修饰的函数内部定义 tf.Variable。
但是如果在函数外部定义tf.Variable 的话,又会显得这个函数有外部变量依赖,封装不够完美。
怎么解决呢?

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.Variable 创建放在类的初始化方法中。而将函数的逻辑放在其他方法中。
TensorFlow提供了一个基类 tf.Module ,通过继承它构建子类,我们不仅可以获得以上的函数逻辑,而且可以非常方便地管理变量,还可以非常方便地管理它引用的其它 Module 。最重要的是,我们能够利用 tf.saved_model保存模型 并实现跨平台部署使用。

TensorFlow中的name_scope函数的作用是创建一个参数名称空间,这个空间里包括许多参数,每个参数有不同的名字,这样可以更好的管理参数空间,防止变量命名时产生冲突。

对于被tf.function修饰过的函数都有get_concrete_function的属性,可以使用@tf.function()对函数添加函数签名,从而获取特定追踪。通过增加函数签名之后才能够将模型保存

下面利用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))
# 输出
6

#查看模块中的全部变量和全部可训练变量
print(demo.variables)
print(demo.trainable_variables)
# 输出
(<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=6.0>,)
(<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=6.0>,)

#查看模块中的全部子模块
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))
# 输出
11
<tf.Tensor: shape=(), dtype=float32, numpy=11.0>

# 查看模型文件相关信息,红框标出来的输出信息在模型部署和跨平台使用时有可能会用到
!saved_model_cli show --dir ./data/ --all

# 输出
MetaGraphDef with tag-set: 'serve' contains the following SignatureDefs:

signature_def['__saved_model_init_op']:
  The given SavedModel SignatureDef contains the following input(s):
  The given SavedModel SignatureDef contains the following output(s):
    outputs['__saved_model_init_op'] tensor_info:
        dtype: DT_INVALID
        shape: unknown_rank
        name: NoOp
  Method name is: 

signature_def['serving_default']:
  The given SavedModel SignatureDef contains the following input(s):
    inputs['a'] tensor_info:
        dtype: DT_FLOAT
        shape: ()
        name: serving_default_a:0
  The given SavedModel SignatureDef contains the following output(s):
    outputs['output_0'] tensor_info:
        dtype: DT_FLOAT
        shape: ()
        name: StatefulPartitionedCall:0
  Method name is: tensorflow/serving/predict

Defined Functions:
  Function Name: 'addprint'
    Option #1
      Callable with:
        Argument #1
          a: TensorSpec(shape=(), dtype=tf.float32, name='a')
【红框内容】
2021-04-30 14:23:38.097461: W tensorflow/stream_executor/platform/default/dso_loader.cc:60] Could not load dynamic library 'cudart64_110.dll'; dlerror: cudart64_110.dll not found
2021-04-30 14:23:38.097504: I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine.

完整代码

import numpy as np
class MyModel(tf.keras.Model):

    def __init__(self, num_classes=10):
        super(MyModel, self).__init__(name='my_model')
        self.num_classes = num_classes
        # 定义自己需要的层
        self.dense_1 = tf.keras.layers.Dense(32, activation='relu')
        self.dense_2 = tf.keras.layers.Dense(num_classes)

    @tf.function(input_signature=[tf.TensorSpec([None,32], tf.float32)])  
    #定义输入数据的类型和shape,如果不是这样的类型和shape就会报错
    #并且这里采用@tf.function()来修饰,会将计算图转化为静态图
    def call(self, inputs):
        #定义前向传播
        # 使用在 (in `__init__`)定义的层
        x = self.dense_1(inputs)
        return self.dense_2(x)

data = np.random.random((1000, 32))
labels = np.random.random((1000, 10))


# Instantiate an optimizer.
optimizer = tf.keras.optimizers.SGD(learning_rate=1e-3)
# Instantiate a loss function.
loss_fn = tf.keras.losses.CategoricalCrossentropy()

# Prepare the training dataset.
batch_size = 64
train_dataset = tf.data.Dataset.from_tensor_slices((data, labels))
train_dataset = train_dataset.shuffle(buffer_size=1024).batch(batch_size)

model = MyModel(num_classes=10)
epochs = 3
for epoch in range(epochs):
    print('Start of epoch %d' % (epoch,))

    # 遍历数据集的batch_size
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):
        with tf.GradientTape() as tape:
            logits = model(x_batch_train)
            loss_value = loss_fn(y_batch_train, logits)
        grads = tape.gradient(loss_value, model.trainable_weights)
        optimizer.apply_gradients(zip(grads, model.trainable_weights))

        # 每200 batches打印一次.
        if step % 200 == 0:
            print('Training loss (for one batch) at step %s: %s' % (step, float(loss_value)))
            print('Seen so far: %s samples' % ((step + 1) * 64))

tf.saved_model.save(model,'my_saved_model')

模型保存的具体内容,会专门写一篇博客!

发表评论

您的电子邮箱地址不会被公开。