TensorFlow2.0的动态图和静态图切换 part 1

TensorFlow2.0的动态图和静态图切换

tf.function介绍

动态图是tf2.0最引人注目的特征,大部分其他改动都是为了适应动态图。它允许将一部分python语法转换为可移植、高性能、语言无关的TensorFlow1.x语法,通过这种方式完成tf1.x静态库和tf2.0动态图的切换。

尽管听起来很美好,但是tf.function还是有一些不为人知的细节值得我们注意,本文就是通过错误驱动的方式来探索tf.funcion的使用技巧。

tf1.x的Session执行

首先我们来回顾一下tf1.x中利用Session来完成图计算的过程:

  1. 创建一个tf.Graph对象,并设置为当前范围的默认图
  2. 通过TensorFlow API描述这个图的计算过程
  3. 提前想好参数共享并定义好各个参数的使用范围
  4. 创建并配置好tf.Session
  5. 编译静态图,并加载到Session中
  6. 初始化参数
  7. 使用tf.Session.run法来启动这个计算过程。执行过程将触发一个回溯机制,从选定的节点一直回溯到各层的输入,来决定每个节点的依赖以及计算最终结果。

上面这些过程可以通过以下代码了解:

g = tf.Graph()
with g.as_default():
    a = tf.constant([[10,10],[11.,1.]])
    x = tf.constant([[1.,0.],[0.,1.]])
    b = tf.Variable(12.)
    y = tf.matmul(a, x) + b
    init_op = tf.global_variables_initializer()

with tf.Session() as sess:
    sess.run(init_op)
    print(sess.run(y))

tf2.0默认的是直接运行(eager execution)模式,和1.x不一样,2.0的运行过程完全和人的想法是一致的。

  • 去掉了图的定义
  • 去掉了Session的执行
  • 去掉了变量的初始化
  • 去掉了参数的范围限制
  • 去掉了tf.control_dependencies,可以直接运行一个没有依赖关系的操作序列

代码示例如下:

a = tf.constant([[10,10],[11.,1.]])
x = tf.constant([[1.,0.],[0.,1.]])
b = tf.Variable(12.)
y = tf.matmul(a, x) + b
print(y.numpy())

相比于tf1.x,直接执行的缺点就是运行速度慢,因为它依赖python的解释器来运行所有的计算过程,而这是很慢的,且有一些优化仅仅只能用在静态库上。简而言之,开发效率和运行效率不可兼得。

如果我们鱼和熊掌都想要呢?tf2.0 提供了tf.function来兼容静态图和动态图两种模式。

使用tf.function 而不是 tf.Session

tf2.0最大的变化之一就是去除了Session类。这个改变强制开发者使用python函数和一些装饰语句来构造计算图,不能再使用Session来定义了。也就是说我们可以通过tf.function装饰器来加速python函数。

注意:静态图相对动态图的加速是不一定的。对于一些简单的过程,不太值得我们去定义静态图,比如简单的矩阵乘法操作等;但是对于深度神经网络的计算,转换到静态图是可以有比较明显的性能提升的。

从python函数到它的图表示的转换称为AutoGraph。

在tf2.0中,python函数会自动转换到它的图表式,只需要使用@tf.function装饰,这个装饰语句会自动生成python函数的可调用的静态图。

tf.funcion : 简单的解释

在第一次调用使用tf.function装饰的python函数时:

  • 函数被执行且被追踪。在这个环境下,eager execution被禁止了,就像tf1.x一样,每个tf的方法都定义了一个tf.Operation,有输入和输出的Tensor对象。
  • AutoGraph可以用来检测可以被转换的python的结构体,并转换为相应的图表示(while -> tf.while, for -> tf.while, if -> tf.cond, assert -> tf.assert …)
  • 通过函数的追踪+AutoGraph,函数的图表示就有了。为了保持图的计算顺序,tf.control_dependencies会被自动添加到每个执行语句,来定义i+1行和第i行的依赖条件。
  • tf.Graph对象被编译
  • 一个唯一的ID会给到这个构建好的图,这个ID是根据函数名字和输入参数生成的。这个图会被放到一个map中去,map[id]=graph
  • 如果key能够匹配成功,每次函数调用都是在重复使用已经构建好的图

转换到eager execution

为了使用tf.function,我们首先要做的就是重构tf1.x的代码,这个过程是比较容易的,将Session中运行的函数部分提取出来携程python函数:

def f():
    a = tf.constant([[10,10],[11.,1.]])
    x = tf.constant([[1.,0.],[0.,1.]])
    b = tf.Variable(12.)
    y = tf.matmul(a, x) + b
    return y

如果我们执行f(),会发生什么? 什么都不会发生!因为tf2.0默认的eager execution模式会把f当做一个标准的python函数来执行。

从eager到tf.function:需要做的准备

让我们在f前面增加一个@tf.function语句,为了清晰的说明(也为了使用传统打印的方式来debug),我们还在函数体内增加print和tf.print函数调用。

@tf.function
def f():
    a = tf.constant([[10,10],[11.,1.]])
    x = tf.constant([[1.,0.],[0.,1.]])
    b = tf.Variable(12.)
    y = tf.matmul(a, x) + b
    print("PRINT: ", y)
    tf.print("TF-PRINT: ", y)
    return y

f()

会发生什么❓

  1. @tf.function会将f函数包装成一个tensorflow.python.eager.def_function.Function对象,而这个python函数f,是这个Function对象的.python_function成员变量。
  2. 直到这个对象( f( ) )被调用,什么都不会发生
  3. 当f()被调用时,图的编译就开始了。此时,只有python代码会被执行且追踪,来获取足够的数据构建图。tf.print不会被执行,因为它会被编译到图中,因此输出是:

PRINT: Tensor(“add:0”, shape=(2, 2), dtype=float32)

  1. FAIL:函数第一次执行的时候,会报以下错误,@tf.function编译失败:

ValueError: tf.function-decorated function tried to create variables on non-first call.

原因是:

State (like tf.Variable objects) are only created the first time the function f is called.

第一次调用

f()

因为是第一次调用,所以f被执行,且图被定义。
** 其他调用 **

f() #again

报错:

ValueError: tf.function-decorated function tried to create variables on non-first call.

这个原因是很好解释的,之所以报错是因为函数定义中包含了一个tf.Variable定义。实际上,在eager模式中,这个对象就是一个普通的python对象,在定义范围之外会被自动回收,然后下次运行会重新定义,因此不会有错误。然而tf.Variable定义了一个持久的对象,如果函数被@tf.function修饰,eager execution被禁止,tf.Variable定义的实际上是图中的一个节点,而这个节点不会被自动回收,且图一旦编译成功,不能再创建变量。

因此,同一个函数在eager 模式下完美运行,但是使用tf.function修饰之后运行报错。这就是我们踩得第一个坑:

要将一个函数从eager 模式转换为它的图表示,我们需要好好想想这个图的编译和运行过程,即便我们当前是在eager模式中。

那么我们怎么避免这个问题呢? 有三种方法:

  1. 定义f() 接受一个输入,这个输入可以是tf.Variable或者其他类型
  2. 定义一个全局变量b,在f()中判断b是否是None,如果不是,则不再重复定义
  3. 将所有代码包装到一个类中,将b作为一个类成员变量,如果变量已经被初始化,则不再重复定义

丑陋的全局变量

b = None

@tf.function
def f():
    a = tf.constant([[10, 10], [11., 1.]])
    x = tf.constant([[1., 0.], [0., 1.]])
    global b
    if b is None:
        b = tf.Variable(12.)
    y = tf.matmul(a, x) + b
    print("PRINT: ", y)
    tf.print("TF-PRINT: ", y)
    return y

f()

包装成类

class F():
    def __init__(self):
        self._b = None

    @tf.function
    def __call__(self):
        a = tf.constant([[10, 10], [11., 1.]])
        x = tf.constant([[1., 0.], [0., 1.]])
        if self._b is None:
            self._b = tf.Variable(12.)
        y = tf.matmul(a, x) + self._b
        print("PRINT: ", y)
        tf.print("TF-PRINT: ", y)
        return y

f = F()
f()

包装成类的方式更好:没有全局变量,这个F()类可以随时被实例化且完全不用担心变量b是否会被重复定义。这是我们踩得第二个坑:

当我们尝试将一个函数转换成它对应的加速的图,我们要思考这个图的构建过程。eager模式的python函数和tf.function的函数并不是1:1对应的,由于AutoGraph的存在,我们不需要关注执行顺序,但是我们还是需要关注那些可以创建状态的对象(tf.Variable)。

传参数的方法

@tf.function
def f(b):
    a = tf.constant([[10,10],[11.,1.]])
    x = tf.constant([[1.,0.],[0.,1.]])
    y = tf.matmul(a, x) + b
    print("PRINT: ", y)
    tf.print("TF-PRINT: ", y)
    return y

b = tf.Variable(12.)
f(b)

和之前的两种方法一样,传参的方法也可以正常运行,且由于传入的是状态的引用,因此参数在函数内部被修改之后,外部调用时结果也会被修改。

a = tf.Variable(0)

@tf.function
def g(x):
    x.assign_add(1)
    return x

print(g(a))
print(g(a))
print(g(a))

这个会输出

1
2
3

结论

  1. tf.function可以作为tf1.x和tf2.0的桥梁,切换静态图和动态图
  2. 一般来说,不能直接使用tf.function来修饰一个python函数,我们需要做一些重构来满足条件
  3. tf.function修饰的函数不能定义tf.Variable,可以通过封装成类、传参等方法来解决这个问题
  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
### 回答1: TensorFlow 2.0和1.0是两个不同版本的TensorFlow深度学习框架。 TensorFlow 1.0是早期版本的TensorFlow框架,它主要使用静态计算来定义和运行模型。静态计算需要在模型运行之前先定义完整的计算,并将数据传递到该中。这种方式需要更多的代码和操作,因此比较繁琐。 TensorFlow 2.0则是一个更新版本的框架,它主要使用动态计算来定义和运行模型。动态计算可以动态地构建计算,并将数据传递到该中,因此比较方便。TensorFlow 2.0还增加了一些新特性,如eager execution(即时执行)、Keras API(易用性更高的高级API)和更好的分布式训练支持等。 总体而言,TensorFlow 2.0比1.0更易于使用和理解,并且有更好的性能和新功能。如果您是新手或想要更快地构建模型,则TensorFlow 2.0是更好的选择。但如果您使用的是早期版本的TensorFlow代码,或者您正在使用不支持TensorFlow 2.0的库,则需要使用TensorFlow 1.0。 ### 回答2: 随着深度学习开发的普及,TensorFlow作为一种广泛使用的框架,有着许多改进和更新。TensorFlow 2.0版本是其最新版本,相对于之前的版本,它有着许多显著的改进和更新。下面是TensorFlow 2.0与1.0之间的区别。 1. 简化API TensorFlow 2.0通过Keras API在处理神经网络的创建和训练上进行大量改进。这个新的API提供了一种更为直观和简洁的方式,使得开发者能够更加轻松地构建和训练网络模型,而不需要过多的复杂代码或拼写错误。 2. 动态图TensorFlow 1.0中,所有的计算都需要在编译时创建,这种静态的方式有着许多限制。而在TensorFlow 2.0中,使用Eager Execution的方式,可以将TensorFlow变成一种动态图,并且可以直接按照Python程序一样输出结果并进行修改。 3. 支持多种数据类型 TensorFlow 2.0从基于张量的计算框架扩展为基于哈希表、稀疏张量和其他非张量数据类型来处理各种类型的数据。这意味着,TensorFlow 2.0不再局限于基本数据类型,它可以更加灵活地处理各种数据类型的问题。 4. 更好的性能和分布式训练 TensorFlow 2.0引入了XLA编译器和TensorFlow Lite,这些工具可以优化深度学习模型的性能,并提高模型预测的速度。另外,TensorFlow 2.0支持分布式训练,可以在多个GPU和多台机器上分布式训练模型,极大提高训练速度。 总的来说,TensorFlow 2.0相对于1.0版本有着更高的性能和更灵活的功能以及更为简化的API。这些改进和更新都可以帮助深度学习开发者更快的开发出高质量的模型,提高模型的准确率和效率。 ### 回答3: TensorFlow(中文名:张量流)是谷歌基于DistBelief进行研发的一款开源机器学习框架,可以通过数据流来进行数值计算,广泛应用于机器学习、深度学习和科学计算等领域。而在TensorFlow的发展历程中,2.0版本和1.0版本是相较而言最具代表性的两个版本。以下是他们的区别: 1)API的变化。TensorFlow 2.0针对初学者对API更加友好,简化和统一了API,包括资源管理器、数据集、损失函数等,并且采用eager execution方式,即时执行每个操作,并在具有交互性的shell中进行了一些增强。相比TensorFlow 1.0,算法实现更加简洁,代码更易读。 2)keras的集成。Tensorflow 2.0把keras作为默认的高级API,它为用户提供了一种更加方便的方式来构建深度学习模型。keras是一个高度模块化的深度学习API,可以构建各种类型的深度学习模型,最近它已经成为深度学习领域的一种标准方式,而Tensorflow 2.0是使用这种方式构建深度学习模型的工具之一。 3)动态图静态分离。在TensorFlow 1.0中,模型只能在Graph(静态)中构建并且修改,然后输入数据以进行模型训练。这种方式非常适合高性能的GPU处理,但是会限制开发人员的灵活性,使调试和错误检查变得更加困难。而Tensorflow 2.0将会提供一种全新的动态图静态分离的方式去实现,使得高级API和低级API一起工作更简单、更自然,能够通过eager execution模式检查和优化模型程序。 4)性能的提升。TensorFlow 2.0在性能方面有所提高。在TensorFlow 2.0中,只加载正在使用的部分,降低了环境和变量的使用,提高了性能。在训练时间上也有一定的提升,就像keras的CPU和GPU支持一样,TensorFlow 2.0改进了TensorFlow1.0的GPU支持。同时,它还支持多GPU训练,让模型训练更加高效。 综上所述,TensorFlow 2.0比TensorFlow 1.0更加用户友好,集成了更多实用的模型和工具,API更加简洁,且提高了性能和支持。不管对于新用户还是TensorFlow 1.0的老用户,TensorFlow 2.0都应该是一种值得尝试的体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值