红帽白皮书新鲜出炉!点击获取,让你的云战略更胜一筹! 了解详情
写点什么

关于即将发布的 TensorFlow 2.0,你需要知道这几件事

  • 2018-11-12
  • 本文字数:8007 字

    阅读完需:约 26 分钟

关于即将发布的TensorFlow 2.0,你需要知道这几件事

AI 前线导读: 对于最流行的机器学习框架来说,TensorFlow 2.0 将是一个重要的里程碑:大量的更改即将到来,所有的一切都以人人可以使用 ML 为目标。但是,这些更改要求老用户完全重新学习如何使用框架:本文介绍了 1.x 和 2.x 版本之间的所有(已知的)差异,主要是思维方式的改变,并着重介绍了新实现的优缺点。


更多干货内容请关注微信公众号“AI 前线”(ID:ai-front)


对于最流行的机器学习框架来说,TensorFlow 2.0 将是一个重要的里程碑:大量的更改即将到来,所有的一切都以人人可以使用 ML 为目标。但是,这些更改要求老用户完全重新学习如何使用框架:本文介绍了 1.x 和 2.x 版本之间的所有(已知的)差异,主要是思维方式的改变,并着重介绍了新实现的优缺点。


对于新手来说,本文也是一个很好的起点:现在就开始以 TensorFlow 2.0 的方式思考,这样你就不必重新学习一个新的框架(除非 TensorFlow 3.0 发布)。

TensorFlow 2.0:为什么?何时?

TensorFlow 2.0 的核心思想是使 TensorFlow 更易于学习和应用。


公告邮件列表中,谷歌大脑工程师 Martin Wicke 对 TensorFlow 2.0 做了初步介绍。简而言之:


  • Eager Execution 将是 2.0 的核心特性。它将用户对编程模型的期望与 TensorFlow 实践更好地结合起来,使 TensorFlow 更易于学习和应用。

  • 支持更多的平台和语言,通过交换格式的标准化和 API 的对齐,改进这些组件之间的兼容性和对等性。

  • 删除弃用的 API 并减少重复,避免给用户带来混乱。

  • 公开的 2.0 设计过程:社区现在可以与 TensorFlow 开发人员合作,使用TensorFlow讨论组讨论新特性。

  • 兼容性和延续性:提供一个与 TensorFlow 1.x 兼容的模块,这意味着 TensorFlow 2.0 将有一个包含所有 TensorFlow 1.x API 的模块。

  • On-disk 兼容性:TensorFlow 1.x 中导出的模型(检查点和冻结模型)将与 TensorFlow 2.0 兼容,只需要重命名某些变量。

  • tf.contrib:完全删除。大型的维护中的模块将移动到独立的存储库;未使用和未维护的模块将被删除。


实际上,如果你是一个 TensorFlow 新手,那么你很幸运。如果像我一样,从 0.x 版本开始使用 TensorFlow,那么必须重写所有的代码库(与从 0.x 向 1.x 转换不同,更改的地方特别多);不过,TensorFlow 的作者声称,将会发布一个转换工具来帮助转换。然而,转换工具并不完美,需要人工干预。


此外,你必须改变你的思维方式;这很有挑战性,但每个人都喜欢挑战,不是吗?


让我们面对这个挑战,从第一个巨大的差异开始详细查看这次新版本设计的更改:删除tf.get_variablef.variable_scopetf.layers和强制转换为基于 Keras 的方法,使用tf.keras


请注意,发布日期还没有确定。但是,从 TensorFlow 讨论组中,我们知道,2018 年底发布可能会发布一个预览版本,2.0 的正式版本可能会在 2019 年春天发布。


因此,最好是在 RFC 被接受后立即更新所有现有的代码库,以便顺利过渡到这个新的 TensorFlow 版本。

Keras(OOP)与 TensorFlow 1.x 比较

RFC:TensorFlow 2.0中的变量”已经被接受。这个 RFC 可能是对现有代码库影响最大的一个,而且,TensorFlow 的老用户需要换一种新的思维方式。


正如文章“使用Go来理解TensorFlow”中描述的那样,每个变量在计算图中都有一个唯一的名称。


作为一个早期的 TensorFlow 用户,我习惯于按照以下模式设计我的计算图:


  1. 哪些操作连接了变量节点?将图定义为多个连接的子图。为了定义不同图的变量,在单独的tf.variable_scope中定义每个子图。在不同的范围内定义子图,可以在Tensorboard中得到一个清晰的图表示。

  2. 在相同的执行步骤中,我是否需要多次使用子图?为了避免创建一个以_n 为前缀的新图,一定要利用tf.variable_scopereuse参数。

  3. 图已经定义了?创建变量初始化 op(看看tf.global_variables_initializer()调用了多少次?)

  4. 把图加载到 Session 中并运行。


在我看来,示例“如何在 TensorFlow 中实现简单的GAN”可以更好地说明这些步骤的合理性。

通过 GAN 了解 TensorFlow 1.x

GAN 判别器 D 必须使用tf.variable_scope reuse参数定义,因为,我们希望首先给 D 提供真样本,然后提供假样本,最后计算 D 相关参数的梯度。


相反,生成网络 G 在一次迭代中从未使用两次,因此,不需要担心其变量重用。


def generator(inputs):    """生成器网络    Args:        inputs: 一个(None, latent_space_size) tf.float32张量    Returns:        G: 生成器输出节点    """a    with tf.variable_scope("generator"):        fc1 = tf.layers.dense(inputs, units=64, activation=tf.nn.elu, name="fc1")        fc2 = tf.layers.dense(fc1, units=64, activation=tf.nn.elu, name="fc2")        G = tf.layers.dense(fc1, units=1, name="G")    return G
def discriminator(inputs, reuse=False): """判别器网络 Args: inputs: 一个(None, 1) tf.float32张量 reuse: Python布尔值, 说明是希望重用(True)还是声明(False) Returns: D: 判别器输出节点 """ with tf.variable_scope("discriminator", reuse=reuse): fc1 = tf.layers.dense(inputs, units=32, activation=tf.nn.elu, name="fc1") D = tf.layers.dense(fc1, units=1, name="D") return D
复制代码


当调用这两个函数时,在默认图中定义了两个不同的子图,每个子图都有自己的作用域(生成器或判别器)。请注意,这个函数返回的是定义子图的输出张量,而不是图本身。


为了共用 D 图,我们定义了 2 个输出(真和假),并定义了训练 G 和 D 所需的损失函数。


# 定义真输入,一组从真实数据的抽样值real_input = tf.placeholder(tf.float32, shape=(None,1))# 定义判别器网络及其参数D_real = discriminator(real_input)
# 任意大小的噪声先验向量latent_space_size = 100# 定义输入噪声,定义生成器input_noise = tf.placeholder(tf.float32, shape=(None,latent_space_size))G = generator(input_noise)
# 现在,我们已经定义了生成器输出G,我们可以把它提供给D的输入# `discriminator`的这个调用不会定义一个新图,但会**重用**之前定义的变量
复制代码


最后要做的只是定义训练 D 和 G 所需的 2 个损失函数和 2 个优化器。


D_loss_real = tf.reduce_mean(    tf.nn.sigmoid_cross_entropy_with_logits(logits=D_real, labels=tf.ones_like(D_real)))
D_loss_fake = tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits(logits=D_fake, labels=tf.zeros_like(D_fake)))
# D_loss:当第一次调用时会使用D_loss_real做一次前向传递# 然后使用D_loss_fake再做一次,共享同样的D参数。D_loss = D_loss_real + D_loss_fake
G_loss = tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits(logits=D_fake, labels=tf.ones_like(D_fake)))
复制代码


损失函数很容易定义。对抗式训练的特点是首先要用真样本和由 G 生成的样本对 D 进行训练,然后用 D 评估的结果作为输入信号对对抗性的 G 进行训练。


对抗性训练的这两个训练步骤需要单独运行,但是,我们在同一个图中定义了模型,我们不想在训练 D 时更新 G 变量,反之亦然。


这样,由于我们在默认图中定义了每个变量,所以每个变量都是全局的,我们必须使用两个不同的列表获得正确的变量,并确保定义了优化器,以便计算梯度,并仅对恰当的子图应用更新。


# 获得D和G变量D_vars = tf.trainable_variables(scope="discriminator")G_vars = tf.trainable_variables(scope="generator")
# 定义优化器和训练操作train_D = tf.train.AdamOptimizer(1e-5).minimize(D_loss, var_list=D_vars)train_G = tf.train.AdamOptimizer(1e-5).minimize(G_loss, var_list=G_vars)
复制代码


好了,我们到了第三步,图定义最后要做的是定义变量初始化 op:


init_op = tf.global_variables_initializer()
复制代码


优点 / 缺点


图已经正确定义,当在训练循环和会话中使用时,它可以正常工作。然而,从软件工程的角度来看,有一些特性值得注意:


  1. 使用上下文管理器tf.variable_scope更改由 tf.layers 定义的变量的(完整)名称:在不同的变量作用域内,对 tf.layers.* 方法的相同调用会在不同的变量作用域内定义一组新的变量。

  2. 布尔标识 reuse 可以完全改变 tf.layers.*方法任何调用的行为(定义或重用)。

  3. 每个变量都是全局的: tf.layers 调用tf.get_variable(在 tf.layers 内部使用)定义的变量可以从任何地方访问:上面使用 tf.trainable_variables(prefix)来获得两个变量列表就是对这种情况的一个很好说明。

  4. 定义子图并不简单:只是调用 discriminator 并不能获得一个新的、独立的判别器。有点违反直觉。

  5. 子图定义的返回值不是其唯一的输出向量,其中也没有包含图的所有信息(虽然可以追溯到输入,但并不简单)。

  6. 定义变量初始化 op 太无趣(但这刚刚通过 tf.train.MonitoredSessiontf.train.MonitoredTrainingSession得到了解决)。


这 6 条大概全是缺点。


我们使用 TensorFlow 1.x 的方式定义了 GAN:下面让我们迁移到 TensorFlow 2.0。

通过 GAN 了解 TensorFlow 2.x

如前一节所述,在 TensorFlow 2.x 中,思维方式改变了。tf.get_variabletf.variable_scopetf.layers被移除并强制转换为基于 Keras 的方法,使用tf.keras会迫使 TensorFlow 开发人员改变其思维方式。


我们必须使用 tf.keras 定义生成器 G 和判别器 D:这将为我们提供变量共享特性,我们曾经使用该特性来定义 D,但是底层实现的方式不同。


请注意:tf.layers 将被移除,因此,请现在就开始使用 tf.keras 定义你的模型,这是为 2.x 做准备所必须的。


def generator(input_shape):    """生成器王国    Args:        input_shape:期望的输入形状(如: (latent_space_size))    Returns:        G:生成器模型    """    inputs = tf.keras.layers.Input(input_shape)    net = tf.keras.layers.Dense(units=64, activation=tf.nn.elu, name="fc1")(inputs)    net = tf.keras.layers.Dense(units=64, activation=tf.nn.elu, name="fc2")(net)    net = tf.keras.layers.Dense(units=1, name="G")(net)    G = tf.keras.Model(inputs=inputs, outputs=net)    return G
def discriminator(input_shape): """判别器网络 Args: input_shape:期望的输入形状(如: (latent_space_size)) Returns: D:判别器模型 """ inputs = tf.keras.layers.Input(input_shape) net = tf.keras.layers.Dense(units=32, activation=tf.nn.elu, name="fc1")(inputs) net = tf.keras.layers.Dense(units=1, name="D")(net) D = tf.keras.Model(inputs=inputs, outputs=net) return D
复制代码


看下该方法的不同:生成器和判别器都返回一个 tf.keras.Model,而不仅仅是一个输出张量。


这意味着,使用 Keras,我们可以实例化我们的模型,并在源代码的不同部分使用相同的模型,我们可以有效地使用模型变量,而无需定义以_n 为前缀的新子图。实际上,和 1.x 版本不同,我们只定义一个 D 模型,但使用了两次。


# 定义真输入,一组从真实数据抽取的值real_input = tf.placeholder(tf.float32, shape=(None,1))
# 定义判别器模型D = discriminator(real_input.shape[1:])
# 设置任意形状的噪声先验向量latent_space_size = 100# 定义输入噪声形状,定义生成器input_noise = tf.placeholder(tf.float32, shape=(None,latent_space_size))G = generator(input_noise.shape[1:])
复制代码


同样:不需要像前面那样定义 D_fake,也不需要在定义图时提前考虑变量共享问题。


现在,我们可以继续定义 G 和 D 的损失函数了。


D_real = D(real_input)D_loss_real = tf.reduce_mean(    tf.nn.sigmoid_cross_entropy_with_logits(logits=D_real, labels=tf.ones_like(D_real)))
G_z = G(input_noise)
D_fake = D(G_z)D_loss_fake = tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits(logits=D_fake, labels=tf.zeros_like(D_fake)))
D_loss = D_loss_real + D_loss_fake
G_loss = tf.reduce_mean( tf.nn.sigmoid_cross_entropy_with_logits(logits=D_fake, labels=tf.ones_like(D_fake)))
复制代码


到目前为止还不错。最后要做的是定义两个分别优化 D 和 G 的优化器。因为我们用的是 tf.keras,所以不需要手动创建需要更新的变量列表,因为 tf.keras.Models 对象本身具有这个属性:


# 定义优化器和训练操作train_D = tf.train.AdamOptimizer(1e-5).minimize(D_loss, var_list=D.trainable_variables)train_G = tf.train.AdamOptimizer(1e-5).minimize(G_loss, var_list=G.trainable_variables)
复制代码


我们已经准备好了:我们到达了第 3 步,由于我们仍然在使用静态图模式,我们必须定义变量初始化 op:


init_op = tf.global_variables_initializer()
复制代码


优点/缺点


  • 从 tf.layers 转换到 tf.keras 很简单:所有的 tf.layers 方法都有对等的 tf.keras.layers 方法。

  • f.keras.Model 完全解决了变量重用以及图重定义的困扰。

  • tf.keras.Model 不是一个输出张量,但是一个包含自有变量的完整模型。

  • 我们还是必须初始化所有变量,但就像我们前面说过的那样,tf.train.MonitoredSession 可以帮我们完成。


不管是在 TensorFlow 1.x 中,还是在 2.x 中,GAN 示例都是首先使用“旧”的图定义范式,然后在会话中执行(不管是现在还是将来,这都是一个很好且有效的范式,并且是——个人观点——最好的)。


然而,TensorFlow 2.x 的另一个大变化是让 Eager 模式成为默认执行模式。在 TensorFlow 1.x 中,我们必须显式地启用 Eager Execution,而在 TensorFlow 2.x 中,我们要做相反的事情。

Eager 模式优先

以下是Eager Execution指南的解释:


TensorFlow 的 Eager Execution 是一种必要的编程环境,它可以立即评估操作,而不需要构建图:操作返回具体的值,而不是构建一个计算图并稍后运行。这使得开始 TensorFlow 入门和模型调试变得很容易,同时也减少了模板文件。请按照本指南在交互式 Python 解释器中运行下面的代码示例。


Eager Execution 是一个灵活的机器学习研究和试验平台,提供以下特性:


  • 直观的接口——自然地构造代码并使用 Python 数据结构。快速迭代小模型和小样本。

  • 更易于调试——直接调用 ops 检查正在运行的模型和测试更改。使用标准的 Python 调试工具进行即时错误报告。

  • 自然的控制流——使用 Python 控制流而不是图控制流,简化了动态模型的规范。


简而言之:不需要首先定义图,然后在会话中计算它。在 Eager 模式中使用 TensorFlow 可以混合定义和执行,就像标准的 Python 程序一样。


这与静态图版本并不是一一对应的,因为有些在图中很自然的东西并不存在于这样一个命令式环境中。


这里,最重要的例子是tf.GradientTape上下文管理器,这只存在于 Eager 模式下。


当我们有一个图,我们知道节点是如何连接的,当我们要计算某个函数的梯度时,我们可以从输出回溯到图的输入,计算梯度并得到结果。


在 Eager 模式下,我们不能这样。使用自动微分法计算函数梯度的唯一方法是构建一个图。构建在 tf.GradientTape 上下文管理器中执行的、对一些可观察的元素(比如变量)进行操作的图,然后,可以由 tf.GradientTape 来计算我们需要的梯度。


tf.GradientTape文档页上,我们可以找到例子,清楚地说明如何使用 tf.GradientTape 以及为什么需要它:


x = tf.constant(3.0)with tf.GradientTape() as g:  g.watch(x)  y = x * xdy_dx = g.gradient(y, x) # Will compute to 6.0
复制代码


此外,控制流操作就是 Python 的控制流操作(比如 for loop、if 语句……),与 tf.while_loop、tf.map_fn、tf.cond 不同,那些方法我们必须在静态图版本中使用。


有一个工具,叫做Autograph,它可以帮助你使用普通的 Python 编写复杂的图代码。在后台,AutoGraph 自动将代码转换为等效的 TensorFlow 图代码。


不过,你需要编写的 Python 代码不是纯 Python(例如,如果你要声明一个函数返回一个指定 TensorFlow 数据类型的元素列表,那会用到在标准 Python 函数中不会使用的操作),而其功能至少在本文写作时是有限的。


之所以创建这个工具,是因为图版本有一个很大的优势,即一旦导出,它就成为“单个文件”,而在生产环境中交付经过训练的机器学习模型,使用静态图模式要容易得多。另外,静态图模式更快。


就我个人而言,我不太喜欢 Eager 模式。可能是因为我已经习惯了静态图版本,并且我发现,Eager 模式是 PyTorch 的粗糙模仿。另外,尝试将 GAN 从 PyTorch 实现转成 TensorFlow 2.x 版本,同时使用静态图和 Eager 模式时,我无法让 Eager 模式发挥作用,我还不知道为什么(虽然静态图实现工作得很好)。我在 GitHub 上提交了一个 Bug 报告(当然,这个错误可能是我自己的):TensorFlow Eager版本失败了,而TensorFlow静态图可以正常运行


转换到 TensorFlow 2.x 还需要做其他的修改,我将在下一节“该怎么办?”中总体介绍。

该怎么办?

关于转换到 TensorFlow 2.x,下面是我根据自己的理解整理的 F.A.Q 列表。


如果我的项目使用了****tf.contrib,该怎么办?


所有关于 tf.contrib 内部项目命运的信息可以在这里找到:tf.contrib日落


你可能只需要安装一个新的 Python 包,或者将 tf. instrument .something 重命名为 tf.something。


如果我的项目在 TensorFlow 1.x 中可以运行,而在 2.x 中无法运行了,该怎么办?


不应该出现这种情况:请再次检查转换实现是否正确,如果是,则在 GitHub 上提交一个 Bug 报告。


如果项目在静态图模式下可以运行,而在 Eager 模式下无法运行,该怎么办?


这是我目前遇到的问题,我已经提交了报告:TensorFlow Eager版本失败了,而TensorFlow静态图可以正常运行


现在我还不知道这是我自己的 Bug,还是实际的 TensorFlow Eager 版本有什么问题。但是,由于我习惯于静态图的思考方式,所以我将避免使用 Eager 版本。


如果某个 tf.方法在 2.x 中被删除了,该怎么办?


这个方法很可能只是被移动了。在 TensorFlow 1.x 中,有很多方法的别名。而在 TensorFlow 2.x 中,我们的目标是(如果RFC: TensorFlow名称空间如我所愿被接受的话)删除许多别名,并将方法移动到更好的位置,以提高整体的一致性。


在 RFC 中,你可以找到新提议的名称空间、要删除的名称空间列表以及所有其他为增强框架的一致性(可能)要进行的更改。


另外,即将发布的转换工具可能可以正确地为你应用所有这些更新(这只是我对该转换工具的猜测,但由于这是一项简单的任务,那是很可能会出现的一个特性)。

小结

本文的写作目的是阐明 TensorFlow 2.0 将给框架用户带来的变化和挑战。


TensorFlow 1 中的 GAN 实现以及到 TensorFlow 2.x 的转换应该可以清楚地说明使用新版本所需要的心态改变。


总的来说,我认为,TensorFlow 2.x 将改进框架的质量,标准化并简化它的用法。从未见过静态图方法且习惯于使用命令式语言的新用户可能会发现,Eager 模式是进入 TensorFlow 世界的一个很好的切入点。


不过,更新中有些部分我不喜欢(这只是我个人的观点):


  • 把重点放在 Eager Execution 上,并使之成为默认模式:在我看来,这似乎是一种营销手段。TensorFlow 似乎是想要追赶 PyTorch(默认是 Eager);

  • 静态图和 Eager(以及混合它们的可能性)不是 1:1 兼容性的,在我看来,这可能会在大型项目中造成混乱,使得项目难以维护;

  • 切换到基于 Keras 的方法是一项很好的举措,但它使图在 Tensorboard 中的可视化变得非常难看。事实上,变量和图的定义是全局的,在 TensorFlow 图中创建新“块”的 tf.named_scope(为了共享变量更容易,每次调用 Keras Model 时都会调用)被图隔开,它是内部使用的,它的输入节点列表中包含所有的模型变量——这使得 Tensorboard 中图的可视化变得几乎没有用处,对于这样一个好工具,这真是个遗憾。


如果你喜欢这篇文章,请分享;如果文章中有什么问题/可以改进的地方,请随时告诉我。


感谢你的阅读!


查看英文原文:TensorFlow 2.0: models migration and new design


公众号推荐:

2024 年 1 月,InfoQ 研究中心重磅发布《大语言模型综合能力测评报告 2024》,揭示了 10 个大模型在语义理解、文学创作、知识问答等领域的卓越表现。ChatGPT-4、文心一言等领先模型在编程、逻辑推理等方面展现出惊人的进步,预示着大模型将在 2024 年迎来更广泛的应用和创新。关注公众号「AI 前线」,回复「大模型报告」免费获取电子版研究报告。

AI 前线公众号
2018-11-12 17:583902
用户头像

发布了 1008 篇内容, 共 372.1 次阅读, 收获喜欢 340 次。

关注

评论

发布
暂无评论
发现更多内容

数据库连接池 -Druid 源码学习(七)

wjchenge

Druid 数据库连接池

druid 源码阅读 7——keepAlive实现方式

张大彪

Flutter StatefulBuilder 示例

坚果

5月月更

如何给 kitex 贡献单元测试

baiyutang

开源 字节跳动 微服务 5月月更 kitex

springboot启动失败的问题('hibernate.dialect' not set)

程序员欣宸

Java 5月月更

☕️从 Java8 到 Java17 的新特性(五):Java12 的新特性

看山

Java Java12

☕️从 Java8 到 Java17 的新特性(七):Java14 的新特性

看山

Java java14

☕️从 Java8 到 Java17 的新特性(八):Java15 的新特性

看山

Java java15

贝叶斯简介

5月月更

5.17电信日 | 融云积极消除数字鸿沟 助力打造数字化新基建

融云 RongCloud

druid源码学习七

Nick

☕️从 Java8 到 Java17 的新特性(二):Java9 的新特性

看山

Java Java9

☕️从 Java8 到 Java17 的新特性(三):Java10 的新特性

看山

Java Java10

Go 语言入门很简单:AES加密和解密

宇宙之一粟

AES Go 语言 5月月更

架构实战营模块一作业

融冰

架构训练营第一次作业

地下地上

☕️从 Java8 到 Java17 的新特性(九):Java16 的新特性

看山

Java java16

C++中字符三兄弟(WCHAR、CHAR、TCHAR)

liuzhen007

c c++ 5月月更

加入MOVE,一起体验Move2Earn的运动乐趣

股市老人

ffmpeg封装格式转换

Loken

音视频 5月月更

☕️从 Java8 到 Java17 的新特性(六):Java13 的新特性

看山

Java Java13

架构实战营-第一周

Asura

【愚公系列】2022年05月 二十三种设计模式(十六)-迭代器模式(Iterator Pattern)

愚公搬代码

5月月更

设计模式之观察者模式

乌龟哥哥

5月月更

Druid连接池源码阅读07

石小天

融云 x Zervo:打造欧美 Z 世代社交的「主题幻想世界」

融云 RongCloud

☕️从 Java8 到 Java17 的新特性(四):Java11 的新特性

看山

Java Java11 LTS

使用 nginx 配置项的好习惯

liuzhen007

nginx 5月月更

FFmpeg安装

Loken

音视频

二分查找树

工程师日月

二叉树 5月月更

☕️从 Java8 到 Java17 的新特性(十):Java17 的新特性

看山

Java java17

关于即将发布的TensorFlow 2.0,你需要知道这几件事_AI&大模型_Rita Lia_InfoQ精选文章