文 /  李锡涵,Google Developers Expert

本文节选自《简单粗暴 TensorFlow 2.0》

尽管 TensorFlow 2 建议以即时执行模式(Eager Execution)作为主要执行模式,然而,图执行模式(Graph Execution)作为 TensorFlow 2 之前的主要执行模式,依旧对于我们理解 TensorFlow 具有重要意义。尤其是当我们需要使用 tf.function 时,对图执行模式的理解更是不可或缺。

图执行模式在 TensorFlow 1.X 和 2.X 版本中的 API 不同:

  • 在 TensorFlow 1.X 中,图执行模式主要通过 “直接构建计算图 + tf.Session” 进行操作;
  • 在 TensorFlow 2 中,图执行模式主要通过 tf.function 进行操作。

在本章,我们将在 tf.function:图执行模式 一节的基础上,进一步对图执行模式的这两种 API 进行对比说明,以帮助已熟悉 TensorFlow 1.X 的用户过渡到 TensorFlow 2。

提示
TensorFlow 2 依然支持 TensorFlow 1.X 的 API。为了在 TensorFlow 2 中使用 TensorFlow 1.X 的 API ,我们可以使用 import tensorflow.compat.v1 as tf 导入 TensorFlow,并通过 tf.disable_eager_execution() 禁用默认的即时执行模式。

TensorFlow 1+1

TensorFlow 的图执行模式是一个符号式的(基于计算图的)计算框架。简而言之,如果你需要进行一系列计算,则需要依次进行如下两步:

  • 建立一个 “计算图”,这个图描述了如何将输入数据通过一系列计算而得到输出;
  • 建立一个会话,并在会话中与计算图进行交互,即向计算图传入计算所需的数据,并从计算图中获取结果。

使用计算图进行基本运算 

这里以计算 1+1 作为 Hello World 的示例。以下代码通过 TensorFlow 1.X 的图执行模式 API 计算 1+1:

import tensorflow.compat.v1 as tftf.disable_eager_execution()

# 以下三行定义了一个简单的“计算图”a = tf.constant(1)  # 定义一个常量张量(Tensor)b = tf.constant(1)c = a + b           # 等价于 c = tf.add(a, b),c是张量a和张量b通过 tf.add 这一操作(Operation)所形成的新张量# 到此为止,计算图定义完毕,然而程序还没有进行任何实质计算。# 如果此时直接输出张量 c 的值,是无法获得 c = 2 的结果的

sess = tf.Session()     # 实例化一个会话(Session)c_ = sess.run(c)        # 通过会话的 run() 方法对计算图里的节点(张量)进行实际的计算print(c_)

输出:

2

而在 TensorFlow 2 中,我们将计算图的建立步骤封装在一个函数中,并使用 @tf.function修饰符对函数进行修饰。当需要运行此计算图时,只需调用修饰后的函数即可。由此,我们可以将以上代码改写如下:

import tensorflow as tf# 以下被 @tf.function 修饰的函数定义了一个计算图@tf.functiondef graph():    a = tf.constant(1)    b = tf.constant(1)    c = a + breturn c# 到此为止,计算图定义完毕。由于 graph() 是一个函数,在其被调用之前,程序是不会进行任何实质计算的。# 只有调用函数,才能通过函数返回值,获得 c = 2 的结果c_ = graph()print(c_.numpy())

小结

  • 在 TensorFlow 1.X 的 API 中,我们直接在主程序中建立计算图。而在 TensorFlow 2 中,计算图的建立需要被封装在一个被 @tf.function 修饰的函数中;

  • 在 TensorFlow 1.X 的 API 中,我们通过实例化一个 tf.Session ,并使用其 run 方法执行计算图的实际运算。而在 TensorFlow 2 中,我们通过直接调用被 @tf.function 修饰的函数来执行实际运算。

计算图中的占位符与数据输入 

上面这个程序只能计算 1+1,以下代码通过 TensorFlow 1.X 的图执行模式 API 中的 tf.placeholder() (占位符张量)和 sess.run()feed_dict 参数,展示了如何使用 TensorFlow 计算任意两个数的和:

import tensorflow.compat.v1 as tftf.disable_eager_execution()

a = tf.placeholder(dtype=tf.int32)  # 定义一个占位符Tensorb = tf.placeholder(dtype=tf.int32)c = a + b

a_ = int(input("a = "))  # 从终端读入一个整数并放入变量a_b_ = int(input("b = "))

sess = tf.Session()c_ = sess.run(c, feed_dict={a: a_, b: b_})  # feed_dict参数传入为了计算c所需要的张量的值print("a + b = %d" % c_)

运行程序:

>>> a = 2>>> b = 3a + b = 5

而在 TensorFlow 2 中,我们可以通过为函数指定参数来实现与占位符张量相同的功能。为了在计算图运行时送入占位符数据,只需在调用被修饰后的函数时,将数据作为参数传入即可。由此,我们可以将以上代码改写如下:

import tensorflow as tf

@tf.functiondef graph(a, b):    c = a + breturn c

a_ = int(input("a = "))b_ = int(input("b = "))c_ = graph(a_, b_)print("a + b = %d" % c_)

小结在 TensorFlow 1.X 的 API 中,我们使用 tf.placeholder() 在计算图中声明占位符张量,并通过 sess.run()feed_dict 参数向计算图中的占位符传入实际数据。而在 TensorFlow 2 中,我们使用 tf.function 的函数参数作为占位符张量,通过向被 @tf.function 修饰的函数传递参数,来为计算图中的占位符张量提供实际数据。

计算图中的变量 

变量的声明 

(Variable)是一种特殊类型的张量,使用 tf.get_variable()建立,与编程语言中的变量很相似。使用变量前需要先初始化,变量内存储的值可以在计算图的计算过程中被修改。以下示例代码展示了如何建立一个变量,将其值初始化为 0,并逐次累加 1。

import tensorflow.compat.v1 as tftf.disable_eager_execution()

a = tf.get_variable(name='a', shape=[])initializer = tf.assign(a, 0.0)   # tf.assign(x, y)返回一个“将张量y的值赋给变量x”的操作plus_one_op = tf.assign(a, a + 1.0)

sess = tf.Session()sess.run(initializer)for i in range(5):    sess.run(plus_one_op)       # 对变量a执行加一操作    print(sess.run(a))          # 输出此时变量a在当前会话的计算图中的值

输出:

1.02.03.04.05.0

提示为了初始化变量,也可以在声明变量时指定初始化器(initializer),并通过 tf.global_variables_initializer() 一次性初始化所有变量,在实际工程中更常用:

import tensorflow.compat.v1 as tftf.disable_eager_execution()

a = tf.get_variable(name='a', shape=[],    initializer=tf.zeros_initializer)   # 指定初始化器为全0初始化plus_one_op = tf.assign(a, a + 1.0)

sess = tf.Session()sess.run(tf.global_variables_initializer()) # 初始化所有变量for i in range(5):    sess.run(plus_one_op)    print(sess.run(a)

在 TensorFlow 2 中,我们通过实例化tf.Variable类来声明变量。由此,我们可以将以上代码改写如下:

import tensorflow as tf

a = tf.Variable(0.0)

@tf.functiondef plus_one_op():    a.assign(a + 1.0)return a

for i in range(5):    plus_one_op()    print(a.numpy())

小结
在 TensorFlow 1.X 的 API 中,我们使用 tf.get_variable() 在计算图中声明变量节点。而在 TensorFlow 2 中,我们直接通过 tf.Variable 实例化变量对象,并在计算图中使用这一变量对象。

变量的作用域与重用 在 TensorFlow 1.X 中,我们建立模型时经常需要指定变量的作用域,以及复用变量。此时,TensorFlow 1.X 的图执行模式 API 为我们提供了 tf.variable_scope()reuse 参数来实现变量作用域和复用变量的功能。以下的例子使用了 TensorFlow 1.X 的图执行模式 API 建立了一个三层的全连接神经网络,其中第三层复用了第二层的变量。

import tensorflow.compat.v1 as tfimport numpy as nptf.disable_eager_execution()

def dense(inputs, num_units):    weight = tf.get_variable(name='weight', shape=[inputs.shape[1], num_units])    bias = tf.get_variable(name='bias', shape=[num_units])return tf.nn.relu(tf.matmul(inputs, weight) + bias)

def model(inputs):with tf.variable_scope('dense1'):   # 限定变量的作用域为 dense1        x = dense(inputs, 10)           # 声明了 dense1/weight 和 dense1/bias 两个变量with tf.variable_scope('dense2'):   # 限定变量的作用域为 dense2        x = dense(x, 10)                # 声明了 dense2/weight 和 dense2/bias 两个变量with tf.variable_scope('dense2', reuse=True):   # 第三层复用第二层的变量        x = dense(x, 10)return x

inputs = tf.placeholder(shape=[10, 32], dtype=tf.float32)outputs = model(inputs)print(tf.global_variables())    # 输出当前计算图中的所有变量节点sess = tf.Session()sess.run(tf.global_variables_initializer())outputs_ = sess.run(outputs, feed_dict={inputs: np.random.rand(10, 32)})print(outputs_)

在上例中,计算图的所有变量节点为:

['dense1/weight:0' shape=(32, 10) dtype=float32>,'dense1/bias:0' shape=(10,) dtype=float32>,'dense2/weight:0' shape=(10, 10) dtype=float32>,'dense2/bias:0' shape=(10,) dtype=float32>]

可见,tf.variable_scope() 为在其上下文中的,以 tf.get_variable 建立的变量的名称添加了 “前缀” 或 “作用域”,使得变量在计算图中的层次结构更为清晰,不同 “作用域” 下的同名变量各司其职,不会冲突。同时,虽然我们在上例中调用了 3 次 dense 函数,即调用了 6 次 tf.get_variable 函数,但实际建立的变量节点只有 4 个。这即是 tf.variable_scope()reuse 参数所起到的作用。当 reuse=True 时, tf.get_variable 遇到重名变量时将会自动获取先前建立的同名变量,而不会新建变量,从而达到了变量重用的目的。

而在 TensorFlow 2 的图执行模式 API 中,不再鼓励使用 tf.variable_scope() ,而应当使用 tf.keras.layers.Layertf.keras.Model 来封装代码和指定作用域,具体可参考 本手册第三章。上面的例子与下面基于 tf.kerastf.function 的代码等价。

import tensorflow as tfimport numpy as np

class Dense(tf.keras.layers.Layer):def __init__(self, num_units, **kwargs):        super().__init__(**kwargs)        self.num_units = num_units

def build(self, input_shape):        self.weight = self.add_variable(name='weight', shape=[input_shape[-1], self.num_units])        self.bias = self.add_variable(name='bias', shape=[self.num_units])

def call(self, inputs):        y_pred = tf.matmul(inputs, self.weight) + self.biasreturn y_pred

class Model(tf.keras.Model):def __init__(self):        super().__init__()        self.dense1 = Dense(num_units=10, name='dense1')        self.dense2 = Dense(num_units=10, name='dense2')

    @tf.functiondef call(self, inputs):        x = self.dense1(inputs)        x = self.dense2(inputs)        x = self.dense2(inputs)return x

model = Model()print(model(np.random.rand(10, 32)))

我们可以注意到,在 TensorFlow 2 中,变量的作用域以及复用变量的问题自然地淡化了。基于 Python 类的模型建立方式自然地为变量指定了作用域,而变量的重用也可以通过简单地多次调用同一个层来实现。

为了详细了解上面的代码对变量作用域的处理方式,我们使用 get_concrete_function导出计算图,并输出计算图中的所有变量节点:

graph = model.call.get_concrete_function(np.random.rand(10, 32))print(graph.variables)

输出如下:

('dense1/weight:0' shape=(32, 10) dtype=float32, numpy=...>,'dense1/bias:0' shape=(10,) dtype=float32, numpy=...>,'dense2/weight:0' shape=(32, 10) dtype=float32, numpy=...>,'dense2/bias:0' shape=(10,) dtype=float32, numpy=...)

可见,TensorFlow 2 的图执行模式在变量的作用域上与 TensorFlow 1.X 实际保持了一致。我们通过name参数为每个层指定的名称将成为层内变量的作用域。

小结
在 TensorFlow 1.X 的 API 中,使用 tf.variable_scope()reuse 参数来实现变量作用域和复用变量的功能。在 TensorFlow 2 中,使用 tf.keras.layers.Layertf.keras.Model 来封装代码和指定作用域,从而使变量的作用域以及复用变量的问题自然淡化。两者的实质是一样的。

自动求导机制与优化器 

在本节中,我们对 TensorFlow 1.X 和 TensorFlow 2 在图执行模式下的自动求导机制进行较深入的比较说明。

自动求导机制 我们首先回顾 TensorFlow 1.X 中的自动求导机制。在 TensorFlow 1.X 的图执行模式 API 中,可以使用 tf.gradients(y, x) 计算计算图中的张量节点 y 相对于变量 x 的导数。以下示例展示了在 TensorFlow 1.X 的图执行模式 API 中计算时的导数。

x = tf.get_variable('x', dtype=tf.float32, shape=[], initializer=tf.constant_initializer(3.))y = tf.square(x)    # y = x ^ 2y_grad = tf.gradients(y, x)

以上代码中,计算图中的节点 y_grad 即为 y 相对于 x 的导数。

而在 TensorFlow 2 的图执行模式 API 中,我们使用 tf.GradientTape 这一上下文管理器封装需要求导的计算步骤,并使用其 gradient 方法求导,代码示例如下:

x = tf.Variable(3.)@tf.functiondef grad():with tf.GradientTape() as tape:        y = tf.square(x)    y_grad = tape.gradient(y, x)return y_grad

小结
在 TensorFlow 1.X 中,我们使用 tf.gradients() 求导。而在 TensorFlow 2 中,我们使用使用 tf.GradientTape 这一上下文管理器封装需要求导的计算步骤,并使用其 gradient 方法求导。

优化器

由于机器学习中的求导往往伴随着优化,所以 TensorFlow 中更常用的是优化器(Optimizer)。在 TensorFlow 1.X 的图执行模式 API 中,我们往往使用tf.train中的各种优化器,将求导和调整变量值的步骤合二为一。例如,以下代码片段在计算图构建过程中,使用 tf.train.GradientDescentOptimizer这一梯度下降优化器优化损失函数 loss

y_pred = model(data_placeholder)    # 模型构建loss = ...                          # 计算模型的损失函数 lossoptimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)train_one_step = optimizer.minimize(loss)# 上面一步也可拆分为# grad = optimizer.compute_gradients(loss)# train_one_step = optimizer.apply_gradients(grad)

以上代码中, train_one_step 即为一个将求导和变量值更新合二为一的计算图节点(操作),也就是训练过程中的 “一步”。特别需要注意的是,对于优化器的 minimize 方法而言,只需要指定待优化的损失函数张量节点 loss 即可,求导的变量可以自动从计算图中获得(即 tf.trainable_variables )。在计算图构建完成后,只需启动会话,使用 sess.run 方法运行 train_one_step 这一计算图节点,并通过 feed_dict 参数送入训练数据,即可完成一步训练。代码片段如下:

for data in dataset:    data_dict = ... # 将训练所需数据放入字典 data 内    sess.run(train_one_step, feed_dict=data_dict)

而在 TensorFlow 2 的 API 中,无论是图执行模式还是即时执行模式,均先使用 tf.GradientTape 进行求导操作,然后再使用优化器的 apply_gradients 方法应用已求得的导数,进行变量值的更新。也就是说,和 TensorFlow 1.X 中优化器的 compute_gradients + apply_gradients 十分类似。同时,在 TensorFlow 2 中,无论是求导还是使用导数更新变量值,都需要显式地指定变量。计算图的构建代码结构如下:

optimizer = tf.keras.optimizer.SGD(learning_rate=...)

@tf.functiondef train_one_step(data):with tf.GradientTape() as tape:        y_pred = model(data)    # 模型构建        loss = ...              # 计算模型的损失函数 loss    grad = tape.gradient(loss, model.variables)    optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))

在计算图构建完成后,我们直接调用 train_one_step函数并送入训练数据即可:

for data in dataset:    train_one_step(data)

小结
在 TensorFlow 1.X 中,我们多使用优化器的 minimize 方法,将求导和变量值更新合二为一。而在 TensorFlow 2 中,我们需要先使用 tf.GradientTape 进行求导操作,然后再使用优化器的 apply_gradients 方法应用已求得的导数,进行变量值的更新。而且在这两步中,都需要显式指定待求导和待更新的变量。

自动求导机制的计算图对比 *

在本节,为了帮助读者更深刻地理解 TensorFlow 的自动求导机制,我们以前节的 “计算  在 时的导数” 为例,展示 TensorFlow 1.X 和 TensorFlow 2 在图执行模式下,为这一求导过程所建立的计算图,并进行详细讲解。

在 TensorFlow 1.X 的图执行模式 API 中,将生成的计算图使用 TensorBoard 进行展示:

在计算图中,灰色的块为节点的命名空间(Namespace,后文简称 “块”),椭圆形代表操作节点(OpNode),圆形代表常量,灰色的箭头代表数据流。为了弄清计算图节点 xyy_grad 与计算图中节点的对应关系,我们将这些变量节点输出,可见:

  • x :
  • y : Tensor("Square:0", shape=(), dtype=float32)
  • y_grad : []

在 TensorBoard 中,我们也可以通过点击节点获得节点名称。通过比较我们可以得知,变量 x 对应计算图最下方的 x,节点 y 对应计算图 “Square” 块的 “ (Square) ”,节点 y_grad 对应计算图上方 “Square_grad” 的 Mul_1 节点。同时我们还可以通过点击节点发现,“Square_grad” 块里的 const 节点值为 2,“gradients” 块里的 grad_ys_0 值为 1, Shape 值为空,以及 “x” 块的 const 节点值为 3。

接下来,我们开始具体分析这个计算图的结构。我们可以注意到,这个计算图的结构是比较清晰的,“x” 块负责变量的读取和初始化,“Square” 块负责求平方 y = x ^ 2 ,而 “gradients” 块则负责对 “Square” 块的操作求导,即计算 y_grad = 2 * x。由此我们可以看出, tf.gradients 是一个相对比较 “庞大” 的操作,并非如一般的操作一样往计算图中添加了一个或几个节点,而是建立了一个庞大的子图,以应用链式法则求计算图中特定节点的导数。

在 TensorFlow 2 的图执行模式 API 中,将生成的计算图使用 TensorBoard 进行展示:

我们可以注意到,除了求导过程没有封装在 “gradients” 块内,以及变量的处理简化以外,其他的区别并不大。由此,我们可以看出,在图执行模式下, tf.GradientTape这一上下文管理器的 gradient 方法和 TensorFlow 1.X 的 tf.gradients 是基本等价的。

小结
TensorFlow 1.X 中的 tf.gradients 和 TensorFlow 2 图执行模式下的 tf.GradientTape 上下文管理器尽管在 API 层面的调用方法略有不同,但最终生成的计算图是基本一致的。

“哪吒头”—玩转小潮流

timertask run函数未执行_图执行模式下的 TensorFlow 2相关推荐

  1. timertask run函数未执行_函数的防抖和节流是个啥???

    内容来源:SegmentFault社区 作者:nero 整理编辑:SegmentFault 曾经面试时候被问到过这个,年少的我一脸无知... 后来工作中遇到了一个场景:输入名称的同时去服务器校验名称是 ...

  2. connect函数在阻塞和非阻塞模式下的行为

    connect函数在阻塞和非阻塞模式下的行为 当socket使用阻塞模式时,connect函数会阻塞到有明确结果才会返回,如果网络环境较差,可能要等一会,影响体验, 为了解决这个问题,我们使用异步co ...

  3. socket的阻塞模式和非阻塞模式(send和recv函数在阻塞和非阻塞模式下的表现)

    socket的阻塞模式和非阻塞模式 无论是Windows还是Linux,默认创建socket都是阻塞模式的 在Linux中,可以再创建socket是直接将它设置为非阻塞模式 int socket (i ...

  4. 细说——命令执行_代码执行

    目录 原理 命令执行漏洞原理 代码执行漏洞原理 命令执行与代码执行漏洞区别 命令执行&代码执行漏洞危害 命令执行无回显 代码执行函数 1- eval() 2- assert()-最好不要加上分 ...

  5. 不是管理员计划任务不执行_为执行任务而不是老板

    不是管理员计划任务不执行 在2016年Slush新加坡大会上,我有一次绝妙的机会采访蜡笔创始人Suresh V. Shankar . 在会议上,他谈到了自己作为企业家的经历以及所面临的困难. 他还谈到 ...

  6. mysql脚本报错继续执行_解决执行sql脚本报错:没有足够的内存继续执行程序。...

    AngularJS 模块& 表单 模块定义了一个应用程序. 模块是应用程序中不同部分的容器. 模块是应用控制器的容器. 控制器通常属于一个模块. 应用("myApp" ...

  7. win7装mysql一直未响应_求助啊 WIN7下安装mysql出问题 老是说未响应~!!

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 # # The following options will be read by the MySQL Server. Make sure that # ...

  8. tensorflow环境下的识别食物_在win10环境下进行tensorflow物体识别(ObjectDetection)训练...

    安装ObjectDetection,CPU和GPU都需要 解压module.rar放到C:\TFWS\models目录 地址:https://github.com/tensorflow/models ...

  9. 大数据之-Hadoop完全分布式_完全分布式模式下的集群配置---大数据之hadoop工作笔记0034

    然后前面我们准备好了,完全分布式下用的集群环境,下面我们,开始配置hadoop集群 我们这里用hadoop102,hadoop103,hadoop104 这3台机器. 需要hadoop102,103, ...

最新文章

  1. 综合评价模型的缺点_浅谈交通影响评价中不同交通预测方法的特性
  2. spring-data-redis 使用过程中需要注意的地方
  3. Serverless应用场景
  4. 朱宁:70%散户跑不赢大盘?因为机构利用了他们的错误
  5. TypeScript学习笔记1:变量赋值及书写方式
  6. Java 线程池相关问题
  7. 堂妹问的一道暑假作业题,难住985的家长
  8. 【项目管理】项目管理计划
  9. 暑期训练日志----2018.8.25
  10. Visual Studio 编写纯C语言程序
  11. outset边框html,CSS3 border-image-outset属性怎么用?
  12. 中标麒麟系统u盘安装_中标麒麟u盘安装电脑系统方法
  13. 数据分析的 5 种归纳方法
  14. c语言scanf不用取地址符,关于c中printf和scanf函数是否使用取地址符的疑问
  15. html_css_尺寸调整/调整图片样式(img/max-widht/object-fit)
  16. 后端接口如何提高性能?
  17. 高校企业双向赋能,首届飞桨启航菁英计划圆满结束
  18. 卷王指南,大学计算机专业,面临分专业,计科,软工,大数据,物联网,网络工程,该选什么?
  19. CentOS 7使用ttyd搭建一个WEB共享终端(WebSSH)
  20. 第三次作业(蒋鑫和贺俊朋组)

热门文章

  1. 最近做了一个安装包的安装流程图
  2. java定时执行一次_java Timer(定时调用、实现固定时间执行)
  3. java system.setproperties_在JAVA中 System.getProperty 和 System.setProperty 方法.
  4. pwd命令是什么的缩写_手机学编程(2)目录管理命令
  5. 道闸系统服务器价格,小区道闸系统价格服务客户
  6. html php获取post数据格式,html - php文件无法得到POST过来的数据
  7. java.io.IOException: Broken pipe 的异常处理
  8. Java中return的两种用法
  9. 后端技术:Spring Boot 项目优化和 JVM 调优,真实有效。
  10. 7个CSS你可能不知道的技巧