TensorFlow 1.x 对比 TensorFlow 2 - 行为和 API

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

就底层而言,TensorFlow 2 遵循的是与 TF1.x 完全不同的编程范式。

本指南将介绍 TF1.x 和 TF2 在行为和 API 方面的根本区别,以及您在迁移之旅中应如何应对这些区别。

主要变更的简略摘要

从根本上讲,TF1.x 和 TF2 围绕执行(TF2 中的 Eager Execution)、变量、控制流、张量形状和张量相等性比较使用了一组不同的运行时行为。要与 TF2 兼容,您的代码必须与全套 TF2 行为兼容。在迁移期间,您可以通过 tf.compat.v1.enable_*tf.compat.v1.disable_* API 单独启用或停用大多数行为。移除集合是一个例外,这是启用/停用 Eager Execution 的副作用。

概括来讲,TensorFlow 2:

以下部分提供了有关 TF1.x 和 TF2 之间差异的更多背景信息。要详细了解 TF2 背后的设计过程,请参阅 RFC设计文档

API 清理

许多 API 在 TF2 中已消失或发生移动。一些重大变更包括移除 tf.apptf.flagstf.logging,转而采用现在开源的 absl-py,重新安置了 tf.contrib 中的项目,并清理了主要的 tf.* 命名空间,将不常用的函数移动到像 tf.math 这样的子软件包中。一些 API 已被替换为 TF2 等效项:tf.summarytf.keras.metricstf.keras.optimizers

tf.compat.v1:旧版和兼容性 API 端点

tf.compattf.compat.v1 命名空间下的符号不被视为 TF2 API。这些命名空间公开了混合的兼容性符号,以及 TF 1.x 中的旧版 API 端点。这些旨在帮助从 TF1.x 迁移到 TF2。但是,这些 compat.v1 API 都不是惯用的 TF2 API,因此不要将它们用于编写全新的 TF2 代码。

单个 tf.compat.v1 符号可能与 TF2 兼容,因为即使启用了 TF2 行为(例如 tf.compat.v1.losses.mean_squared_error),它们也可以继续工作,而其他符号则与 TF2 不兼容(例如 tf.compat.v1.metrics.accuracy)。许多 compat.v1 符号(但非全部)都在其文档中包含了专门的迁移信息,解释了它们与 TF2 行为的兼容性程度,以及如何将它们迁移到 TF2 API。

TF2 升级脚本可以将许多 compat.v1 API 符号映射到等效的 TF2 API,前提是它们共用别名或者具有相同但采用了不同顺序的参数。您还可以使用升级脚本以自动重命名 TF1.x API。

同形异义 API

TF2 tf 命名空间(不在 compat.v1 下)中存在一组“同形异义”符号,它们实际上会在后台忽略 TF2 行为,并且/或者与完整的 TF2 行为集不完全兼容。因此,这些 API 与 TF2 代码一起使用时可能会行为异常,并且可能不会提供警告。

  • tf.estimator.*:Estimator 会在后台创建和使用计算图和会话。因此,这些不应被视为与 TF2 兼容。如果您的代码正在运行 Estimator,它并未使用 TF2 行为。
  • keras.Model.model_to_estimator(...):这会在后台创建一个 Estimator,如上所述,它与 TF2 不兼容。
  • tf.Graph().as_default():这会进入 TF1.x 计算图行为,不遵循标准的 TF2 兼容 tf.function 行为。像这样进入计算图的代码通常会通过会话运行,不应视为与 TF2 兼容。
  • tf.feature_column.*:特征列 API 通常依赖于 TF1 风格的 tf.compat.v1.get_variable 变量创建,并假定将通过全局集合访问创建的变量。由于 TF2 不支持集合,在启用 TF2 行为的情况下运行 API 可能无法正常工作。

其他 API 变更

  • TF2 的特性是对设备放置算法进行了重大改进,这样便不再有必要使用 tf.colocate_with。如果将它移除会导致性能下降,请提交错误

  • tf.v1.ConfigProto 的所有用法替换为 tf.config 中的等效函数。

Eager Execution

TF1.x 要求您通过进行 tf.* API 调用手动将抽象语法树(计算图)拼接在一起。随后,它要求用户通过将一组输出张量和输入张量传递给 session.run 调用来手动编译抽象语法树。TF2 会以 Eager 方式执行(像 Python 通常做的那样),使计算图和会话像实现细节一样。

Eager Execution 一个值得注意的地方是不再需要 tf.control_dependencies,因为所有代码行均按顺序执行(在 tf.function 中,带副作用的代码按编写顺序执行)。

没有更多的全局变量

TF1.x 严重依赖隐式全局命名空间。当您调用 tf.Variable 时,它会被放入默认计算图中的集合并保留在其中,即使您已失去指向它的 Python 变量的踪迹。随后,您可以恢复该 tf.Variable,但前提是您知道它在创建时的名称。如果您无法控制变量的创建,这就很难做到。结果,各种机制激增,试图帮助用户再次找到他们的变量,并寻找框架来查找用户创建的变量:变量范围、全局集合、辅助方法(如 tf.get_global_steptf.global_variables_initializer)、隐式计算所有可训练变量梯度的优化器等。TF2 消除了所有这些机制 (Variables 2.0 RFC),转而支持默认机制:跟踪您的变量!如果您失去了 tf.Variable 的踪迹,则会进行垃圾回收。

跟踪变量的要求产生了一些额外的工作,但是借助诸如建模填充码之类的工具以及诸如 tf.Moduletf.keras.layers.Layer 中的隐式面向对象变量集合之类的行为,可以最大限度地减少负担。

函数,而非会话

session.run 调用几乎就像一个函数调用:指定输入和要调用的函数,然后返回一组输出。在 TF2 中,您可以使用 tf.function 来装饰 Python 函数,以将其标记为 JIT 编译,这样 TensorFlow 便可将其作为单个计算图运行 (Functions 2.0 RFC)。这种机制允许 TF2 获得计算图模式的所有好处:

  • 性能:可以优化函数(节点修剪、内核融合等)
  • 可移植性:可以导出/重新导入函数 (SavedModel 2.0 RFC),从而允许您重用和共享模块化 TensorFlow 函数。
# TF1.x
outputs = session.run(f(placeholder), feed_dict={placeholder: input})
# TF2
outputs = f(input)

凭借自由穿插 Python 和 TensorFlow 代码的能力,您能够充分利用 Python 的表现力。但是,可移植的 TensorFlow 可以在没有 Python 解释器(如移动、C++ 和 JavaScript)的情况下执行。为帮助您避免在添加 tf.function 时重写代码,AutoGraph 会将 Python 构造的一个子集转换成其 TensorFlow 等效项:

  • for/while -> tf.while_loop(支持 breakcontinue
  • if -> tf.cond
  • for _ in dataset -> dataset.reduce

AutoGraph 支持控制流的任意嵌套,这样便有可能高效而简洁地实现许多复杂的 ML 程序,例如序贯模型、强化学习、自定义训练循环等。

适应 TF 2.x 行为变更

迁移到全套 TF2 行为后,您向 TF2 的迁移才算完成。可以通过 tf.compat.v1.enable_v2_behaviorstf.compat.v1.disable_v2_behaviors 来启用或停用全套行为。以下部分详细讨论了各项主要行为变更。

使用 tf.function

在迁移期间,您的程序的最大变化可能源于基本编程模型范式从计算图和会话转变为 Eager Execution 和 tf.function。请参阅 TF2 迁移指南以详细了解如何从与 Eager Execution 和 tf.function 不兼容的 API 迁移到与其兼容的 API。

注:在迁移期间,您可以选择使用 tf.compat.v1.enable_eager_executiontf.compat.v1.disable_eager_execution 来直接启用和停用 Eager Execution,但这在程序的生命周期内只能执行一次。

以下是一些常见程序模式,它们不涉及从 tf.Graphtf.compat.v1.Session 切换到 Eager Execution 和 tf.function 时可能会导致问题的 API。

模式 1:多次运行计划仅进行一次的 Python 对象操纵和变量创建

在依赖计算图和会话的 TF1.x 程序中,通常会期望程序中的所有 Python 逻辑只运行一次。但是,使用 Eager Execution 和 tf.function 时,可以合理期望您的 Python 逻辑会至少运行一次,也可能会运行更多次(以 Eager 方式多次运行,或在不同的 tf.function 跟踪记录之间运行多次)。有时,tf.function 甚至会在同一输入上跟踪两次,从而导致意外行为(请参见示例 1 和示例 2)。请参阅 tf.function 指南,了解详细信息。

注:这种模式通常会导致您的代码在不使用 tf.function 的情况下以 Eager 方式执行时无提示地出现异常行为,但在尝试将有问题的代码包装在 tf.function 内时通常会引发 InaccessibleTensorErrorValueError。要发现和调试此问题,建议尽早使用 tf.function 包装您的代码,并使用 pdb 或交互式调试来识别 InaccessibleTensorError 的来源。

示例 1:变量创建

请思考下面的示例,该函数在调用时会创建一个变量:

def f():
  v = tf.Variable(1.0)
  return v

with tf.Graph().as_default():
  with tf.compat.v1.Session() as sess:
    res = f()
    sess.run(tf.compat.v1.global_variables_initializer())
    sess.run(res)

但是,不允许单纯地使用 tf.function 来包装以上包含变量创建的函数。tf.function 仅支持第一次调用时的单例变量创建。为了强制执行这一点,当 tf.function 在第一次调用中检测到变量创建时,它将尝试再次跟踪并在第二次跟踪中发现变量创建时引发错误。

@tf.function
def f():
  print("trace") # This will print twice because the python body is run twice
  v = tf.Variable(1.0)
  return v

try:
  f()
except ValueError as e:
  print(e)

一种变通方法是在第一次调用中创建变量后对其进行缓存和重用。

class Model(tf.Module):
  def __init__(self):
    self.v = None

  @tf.function
  def __call__(self):
    print("trace") # This will print twice because the python body is run twice
    if self.v is None:
      self.v = tf.Variable(0)
    return self.v

m = Model()
m()

示例 2:因 tf.function 回溯而导致张量超出范围

如示例 1 所示,tf.function 将在第一次调用中检测到变量创建时进行回溯。这可能会进一步造成混乱,因为两次跟踪将创建两个计算图。当回溯创建的第二个计算图尝试访问第一次跟踪期间生成的计算图中的张量时,Tensorflow 将引发提示张量超出范围的错误。为了演示这一场景,下面的代码在第一次调用 tf.function 的基础上创建了一个数据集。这将按预期运行。

class Model(tf.Module):
  def __init__(self):
    self.dataset = None

  @tf.function
  def __call__(self):
    print("trace") # This will print once: only traced once
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    it = iter(self.dataset)
    return next(it)

m = Model()
m()

但是,如果我们还尝试在第一次调用 tf.function 时创建变量,代码将引发提示数据集超出范围的错误。这是因为数据集位于第一个计算图中,而第二个计算图也在尝试访问它。

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  @tf.function
  def __call__(self):
    print("trace") # This will print twice because the python body is run twice
    if self.v is None:
      self.v = tf.Variable(0)
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
try:
  m()
except TypeError as e:
  print(e) # <tf.Tensor ...> is out of scope and cannot be used here.

最直接的解决方案是确保变量创建和数据集创建均位于 tf.funciton 调用之外。例如:

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  def initialize(self):
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])
    if self.v is None:
      self.v = tf.Variable(0)

  @tf.function
  def __call__(self):
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
m.initialize()
m()

但是,有时在 tf.function 中创建变量是不可避免的(例如某些 TF Keras 优化器中的槽位变量)。不过,我们可以简单地将数据集创建移到 tf.function 调用之外。我们可以依赖这种方式的原因是 tf.function 将以隐式输入的形式接收数据集,并且两个计算图都可以正确访问它。

class Model(tf.Module):
  def __init__(self):
    self.v = None
    self.dataset = None

  def initialize(self):
    if self.dataset is None:
      self.dataset = tf.data.Dataset.from_tensors([1, 2, 3])

  @tf.function
  def __call__(self):
    if self.v is None:
      self.v = tf.Variable(0)
    it = iter(self.dataset)
    return [self.v, next(it)]

m = Model()
m.initialize()
m()

示例 3:因使用字典而导致意外重新创建 TensorFlow 对象

tf.function 对 Python 副作用的支持(例如附加到列表或检查/添加到字典)非常差。使用 tf.function 提升性能中提供了更多详细信息。在下面的示例中,代码使用字典来缓存数据集和迭代器。对于相同的键,对模型的每次调用都将返回数据集的相同迭代器。

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  def __call__(self, key):
    if key not in self.datasets:
      self.datasets[key] = tf.compat.v1.data.Dataset.from_tensor_slices([1, 2, 3])
      self.iterators[key] = self.datasets[key].make_initializable_iterator()
    return self.iterators[key]

with tf.Graph().as_default():
  with tf.compat.v1.Session() as sess:
    m = Model()
    it = m('a')
    sess.run(it.initializer)
    for _ in range(3):
      print(sess.run(it.get_next())) # prints 1, 2, 3

但是,上面的模式在 tf.function 中不会以预期方式工作。在跟踪期间,tf.function 将忽略添加到字典的 Python 副作用。相反,它只会记住新数据集和迭代器的创建。因此,对模型的每次调用将始终返回一个新的迭代器。除非数值结果或性能足够显著,否则将很难注意到这个问题。因此,我们建议用户在将 tf.function 单纯地包装到 Python 代码之前仔细思考代码。

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  @tf.function
  def __call__(self, key):
    if key not in self.datasets:
      self.datasets[key] = tf.data.Dataset.from_tensor_slices([1, 2, 3])
      self.iterators[key] = iter(self.datasets[key])
    return self.iterators[key]

m = Model()
for _ in range(3):
  print(next(m('a'))) # prints 1, 1, 1

我们可以使用 tf.init_scope 将数据集和迭代器创建提至计算图之外,以实现预期的行为:

class Model(tf.Module):
  def __init__(self):
    self.datasets = {}
    self.iterators = {}

  @tf.function
  def __call__(self, key):
    if key not in self.datasets:
      # Lifts ops out of function-building graphs
      with tf.init_scope():
        self.datasets[key] = tf.data.Dataset.from_tensor_slices([1, 2, 3])
        self.iterators[key] = iter(self.datasets[key])
    return self.iterators[key]

m = Model()
for _ in range(3):
  print(next(m('a'))) # prints 1, 2, 3

一般来说,您应避免在逻辑中依赖 Python 副作用,而应仅将其用于调试您的跟踪。

示例 4:操纵全局 Python 列表

以下 TF1.x 代码使用了全局损失列表,仅用于维护当前训练步骤生成的损失列表。请注意,无论会话运行多少个训练步骤,将损失附加到列表的 Python 逻辑都只会被调用一次。

all_losses = []

class Model():
  def __call__(...):
    ...
    all_losses.append(regularization_loss)
    all_losses.append(label_loss_a)
    all_losses.append(label_loss_b)
    ...

g = tf.Graph()
with g.as_default():
  ...
  # initialize all objects
  model = Model()
  optimizer = ...
  ...
  # train step
  model(...)
  total_loss = tf.reduce_sum(all_losses)
  optimizer.minimize(total_loss)
  ...
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

但是,如果将此 Python 逻辑单纯地映射到采用 Eager Execution 的 TF2,则全局损失列表在每个训练步骤中都将附加新值。这意味着之前期望列表仅包含当前训练步骤内损失的训练步骤代码现在实际上看到的是迄今运行的所有训练步骤的损失列表。这是一种意外的行为变更,需要在每个步骤开始时对该列表进行清理,或者将其设置为训练步骤的局部列表。

all_losses = []

class Model():
  def __call__(...):
    ...
    all_losses.append(regularization_loss)
    all_losses.append(label_loss_a)
    all_losses.append(label_loss_b)
    ...

# initialize all objects
model = Model()
optimizer = ...

def train_step(...)
  ...
  model(...)
  total_loss = tf.reduce_sum(all_losses) # global list is never cleared,
  # Accidentally accumulates sum loss across all training steps
  optimizer.minimize(total_loss)
  ...

模式 2:本应在 TF1.x 中每一步都重新计算的符号张量在切换到 Eager 时意外缓存了初始值。

这种模式通常会导致您的代码在 tf.function 外部以 Eager 方式执行时无提示的出现异常行为,但如果初始值缓存发生在 tf.function 内部,则会引发 InaccessibleTensorError。但请注意,您通常会为了避免上述模式 1 而无意中以这样的方式构建代码,使初始值缓存发生在任何可能引发错误的 tf.function 之外。因此,如果您知道自己的程序可能容易受到这种模式的影响,请格外小心。

这种模式的一般解决方案是重组代码或在必要时使用 Python 可调用对象,以确保值每次都重新计算,而非意外缓存。

示例 1:学习率/超参数等。取决于全局步骤的调度

在下面的代码段中,期望的模式是在每次运行会话时都读取最新的 global_step 值并计算新的学习率。

g = tf.Graph()
with g.as_default():
  ...
  global_step = tf.Variable(0)
  learning_rate = 1.0 / global_step
  opt = tf.compat.v1.train.GradientDescentOptimizer(learning_rate)
  ...
  global_step.assign_add(1)
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

但是,当尝试切换到 Eager 时,请注意学习率最终只计算一次并被重用,而未遵循预期调度:

global_step = tf.Variable(0)
learning_rate = 1.0 / global_step # Wrong! Only computed once!
opt = tf.keras.optimizers.SGD(learning_rate)

def train_step(...):
  ...
  opt.apply_gradients(...)
  global_step.assign_add(1)
  ...

这个特定示例是一种常见模式,优化器应只初始化一次,而非在每个训练步骤都初始化,因此 TF2 优化器支持 tf.keras.optimizers.schedules.LearningRateSchedule 调度或 Python 可调用对象作为学习率和其他超参数的参数。

示例 2:分配为对象特性然后通过指针重用的符号随机数初始化在切换到 Eager 时被意外缓存

请思考以下 NoiseAdder 模块:

class NoiseAdder(tf.Module):
  def __init__(shape, mean):
    self.noise_distribution = tf.random.normal(shape=shape, mean=mean)
    self.trainable_scale = tf.Variable(1.0, trainable=True)

  def add_noise(input):
    return (self.noise_distribution + input) * self.trainable_scale

在 TF1.x 中如下使用会在每次运行会话时计算新的随机噪声张量:

g = tf.Graph()
with g.as_default():
  ...
  # initialize all variable-containing objects
  noise_adder = NoiseAdder(shape, mean)
  ...
  # computation pass
  x_with_noise = noise_adder.add_noise(x)
  ...
...
sess = tf.compat.v1.Session(graph=g)
sess.run(...)

但在 TF2 中,在开始时初始化 noise_adder 将导致 noise_distribution 只计算一次并在所有训练步骤中冻结:

...
# initialize all variable-containing objects
noise_adder = NoiseAdder(shape, mean) # Freezes `self.noise_distribution`!
...
# computation pass
x_with_noise = noise_adder.add_noise(x)
...

要解决此问题,请重构 NoiseAdder 以在每次需要新的随机张量时均调用 tf.random.normal,而非每次都引用同一个张量对象。

class NoiseAdder(tf.Module):
  def __init__(shape, mean):
    self.noise_distribution = lambda: tf.random.normal(shape=shape, mean=mean)
    self.trainable_scale = tf.Variable(1.0, trainable=True)

  def add_noise(input):
    return (self.noise_distribution() + input) * self.trainable_scale

模式 3:TF1.x 代码直接依赖张量并按名称查找张量

TF1.x 代码测试通常会依赖于检查计算图中存在哪些张量或运算。在极少数情况下,建模代码也会依赖于这些按名称查找。

tf.function 之外以 Eager 方式执行时根本不会生成张量名称,因此 tf.Tensor.name 的所有用法都必须发生在 tf.function 内部。请记住,即使在同一个 tf.function 中,TF1.x 与 TF2 之间实际的生成名称也很可能不同,并且 API 保证不能确保生成名称在各个 TF 版本之间的稳定性。

注:即使是在 tf.function 之外仍会生成变量名称,也不能保证其名称在 TF1.x 与 TF2 之间匹配,除非遵循模型映射指南中的相关部分。

模式 4:TF1.x 会话选择性地仅运行生成计算图的一部分

在 TF1.x 中,您可以构造计算图,然后通过选择一组不需要运行计算图中每个运算的输入和输出,选择仅在会话中选择性地运行其中的一个子集。

例如,您在单个计算图中可能同时具有生成器和鉴别器,并使用单独的 tf.compat.v1.Session.run 调用在仅训练鉴别器或仅训练生成器之间交替。

在 TF2 中,由于 tf.function 中的自动控制依赖项以及 Eager Execution,不会对 tf.function 跟踪进行选择性剪枝。例如,即使只有鉴别器或生成器的输出是输出自 tf.function,也会运行包含所有变量更新的完整计算图。

因此,您需要使用包含程序不同部分的多个 tf.function,或者为您分支的 tf.function 提供一个条件参数,以便仅执行您实际想要运行的部分。

集合移除

启用 Eager Execution 后,与计算图集合相关的 compat.v1 API(包括那些在后台读取或写入集合的 API,例如 tf.compat.v1.trainable_variables)将不再可用。有些可能会引发 ValueError,有些可能会静默地返回空列表。

在 TF1.x 中,集合最标准的用法是维护初始化器、全局步骤、权重、正则化损失、模型输出损失和需要运行的变量更新(例如从 BatchNormalization 层)。

处理上述各项标准用法:

  1. 初始化器 - 请忽略。启用 Eager Execution 的情况下不需要手动变量初始化。
  2. 全局步骤 - 有关迁移说明,请参阅 tf.compat.v1.train.get_or_create_global_step 的文档。
  3. 权重 - 请按照模型映射指南中的指导将您的模型映射到 tf.Module/tf.keras.layers.Layer/tf.keras.Model,然后使用它们各自的权重跟踪机制,例如 tf.module.trainable_variables
  4. 正则化损失 - 请按照模型映射指南中的指导将您的模型映射到 tf.Module/tf.keras.layers.Layer/tf.keras.Model,然后使用 tf.keras.losses。或者,您也可以手动跟踪您的正则化损失。
  5. 模型输出损失 - 请使用 tf.keras.Model 损失管理机制,或在不使用集合的情况下单独跟踪您的损失。
  6. 权重更新 - 请忽略此集合。Eager Execution 和 tf.function(带有 AutoGraph 和自动控制流依赖项)意味着所有变量更新都将自动运行。因此,您不必在最后显式运行所有权重更新,但请注意,这意味着权重更新的发生时间可能与在 TF1.x 代码中不同,具体取决于您使用控制依赖项的方式。
  7. 摘要 - 请参阅迁移摘要 API 指南

对于更为复杂的集合用法(例如使用自定义集合),您可能需要重构代码以维护自己的全局存储,或者使其完全不依赖于全局存储。

ResourceVariables 而非 ReferenceVariables

ResourceVariablesReferenceVariables 相比具有更强的读写一致性保证。这样一来,在使用变量时,有关能否观察先前写入的结果的语义将更加可预测、更容易推理。此变更导致现有代码引发错误或静默中断的可能性极低。

但是,这些更强大的一致性保证有可能(尽管可能性很低)增加特定程序的内存使用量。如果您遇到这种情况,请提交议题。此外,如果您的单元测试依赖于与计算图中变量读取对应的运算符名称的精确字符串比较,请注意启用资源变量可能会稍微更改这些运算符的名称。

为了隔离此行为变更对您的代码产生的影响,如果停用了 Eager Execution,可以使用 tf.compat.v1.disable_resource_variables()tf.compat.v1.enable_resource_variables() 来全局停用或启用此行为变更。如果启用了 Eager Execution,将始终使用 ResourceVariables

Control Flow v2

在 TF1.x 中,控制流运算(例如 tf.condtf.while_loop)会内嵌低级运算(例如 SwitchMerge 等)。TF2 提供了改进的函数式控制流运算,可以通过单独的 tf.function 跟踪记录对每个分支实现并支持更高阶的微分。

为了隔离此行为变更对您的代码产生的影响,如果停用了 Eager Execution,您可以使用 tf.compat.v1.disable_control_flow_v2()tf.compat.v1.enable_control_flow_v2() 来全局停用或启用此行为变更。但是,如果还停用了 Eager Execution,则只能停用 Control Flow v2。如果启用了 Eager Execution,将始终使用 Control Flow v2。

这种行为变更可以极大地改变使用控制流的生成 TF 程序的结构,因为它们将包含多个嵌套函数跟踪记录,而非一个平面计算图。因此,任何高度依赖于所生成跟踪记录的确切语义的代码都可能需要进行一些修改。这包括:

  • 依赖于运算符和张量名称的代码
  • 从 TensorFlow 控制流分支外部引用在该分支内创建的张量的代码。这很可能会产生 InaccessibleTensorError

此行为变更旨在保持或提高性能,但如果您遇到 Control Flow v2 性能不及 TF1.x 控制流性能的问题,请提交议题并说明重现步骤。

TensorShape API 行为变更

TensorShape 类已经过简化,可以保存 int(而非 tf.compat.v1.Dimension)对象。因此,无需调用 .value 来获取 int

仍然可以从 tf.TensorShape.dims 访问各个 tf.compat.v1.Dimension 对象。

要隔离此行为变更对您的代码产生的影响,您可以使用 tf.compat.v1.disable_v2_tensorshape()tf.compat.v1.enable_v2_tensorshape() 来全局停用或启用此行为变更。

以下代码演示了 TF1.x 与 TF2 之间的区别。

import tensorflow as tf
2023-11-07 19:17:24.640547: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2023-11-07 19:17:24.640591: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2023-11-07 19:17:24.642139: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
# Create a shape and choose an index
i = 0
shape = tf.TensorShape([16, None, 256])
shape
TensorShape([16, None, 256])

如果您在 TF1.x 中使用此代码:

value = shape[i].value

在 TF2 中则使用:

value = shape[i]
value
16

如果您在 TF1.x 中使用此代码:

for dim in shape:
    value = dim.value
    print(value)

在 TF2 中则使用:

for value in shape:
  print(value)
16
None
256

如果您在 TF1.x 中使用此代码(或使用任何其他维度方法):

dim = shape[i]
dim.assert_is_compatible_with(other_dim)

在 TF2 中则使用:

other_dim = 16
Dimension = tf.compat.v1.Dimension

if shape.rank is None:
  dim = Dimension(None)
else:
  dim = shape.dims[i]
dim.is_compatible_with(other_dim) # or any other dimension method
True
shape = tf.TensorShape(None)

if shape:
  dim = shape.dims[i]
  dim.is_compatible_with(other_dim) # or any other dimension method

如果秩已知,tf.TensorShape 的布尔值将为 True,否则为 False

print(bool(tf.TensorShape([])))      # Scalar
print(bool(tf.TensorShape([0])))     # 0-length vector
print(bool(tf.TensorShape([1])))     # 1-length vector
print(bool(tf.TensorShape([None])))  # Unknown-length vector
print(bool(tf.TensorShape([1, 10, 100])))       # 3D tensor
print(bool(tf.TensorShape([None, None, None]))) # 3D tensor with no known dimensions
print()
print(bool(tf.TensorShape(None)))  # A tensor with unknown rank.
True
True
True
True
True
True

False

因 TensorShape 变更而导致的潜在错误

TensorShape 行为变更不太可能会静默地破坏您的代码。但是,您可能会看到与形状相关的代码开始引发 AttributeError,因为 intNone 不具有与 tf.compat.v1.Dimension 相同的特性。以下是这些 AttributeError 的一些示例:

try:
  # Create a shape and choose an index
  shape = tf.TensorShape([16, None, 256])
  value = shape[0].value
except AttributeError as e:
  # 'int' object has no attribute 'value'
  print(e)
'int' object has no attribute 'value'
try:
  # Create a shape and choose an index
  shape = tf.TensorShape([16, None, 256])
  dim = shape[1]
  other_dim = shape[2]
  dim.assert_is_compatible_with(other_dim)
except AttributeError as e:
  # 'NoneType' object has no attribute 'assert_is_compatible_with'
  print(e)
'NoneType' object has no attribute 'assert_is_compatible_with'

按值比较张量相等性

变量和张量上的二元 ==!= 运算符在 TF2 中已变更为按值进行比较,而不是像在 TF1.x 中那样按对象引用进行比较。此外,张量和变量不再具有直接可哈希性,也不能在集合或字典键中使用,因为可能无法按值对其进行哈希。相反,它们公开了一个 .ref() 方法,您可以使用该方法获取对张量或变量的可哈希引用。

要隔离此行为变更产生的影响,您可以使用 tf.compat.v1.disable_tensor_equality()tf.compat.v1.enable_tensor_equality() 来全局停用或启用此行为变更。

例如,在 TF1.x 中,当您使用 == 运算符时,两个具有相同值的变量将返回 false:

tf.compat.v1.disable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x == y
False

而在启用了张量相等性检查的 TF2 中,x == y 则将返回 True

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x == y
<tf.Tensor: shape=(), dtype=bool, numpy=True>

因此,在 TF2 中,如果您需要按对象引用进行比较,请确保使用 isis not

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)
y = tf.Variable(0.0)

x is y
False

哈希张量和变量

对于 TF1.x 行为,您过去可以直接将变量和张量添加到需要哈希的数据结构中,例如 setdict 键。

x = tf.Variable(0.0)
set([x, tf.constant(2.0)])

但是,在启用了张量相等性的 TF2 中,由于 ==!= 运算符语义更改为值相等性检查,张量和变量变为不可哈希。

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)

try:
  set([x, tf.constant(2.0)])
except TypeError as e:
  # TypeError: Variable is unhashable. Instead, use tensor.ref() as the key.
  print(e)
Variable is unhashable. Instead, use variable.ref() as the key. (Variable: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>)

因此,在 TF2 中,如果您需要使用张量或变量对象作为键或 set 内容,可以使用 tensor.ref() 来获取可用作键的可哈希引用:

tf.compat.v1.enable_tensor_equality()
x = tf.Variable(0.0)

tensor_set = set([x.ref(), tf.constant(2.0).ref()])
assert x.ref() in tensor_set

tensor_set
{<Reference wrapping <tf.Tensor: shape=(), dtype=float32, numpy=2.0>>,
 <Reference wrapping <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>>}

如果需要,您还可以使用 reference.deref() 以从引用中获取张量或变量:

referenced_var = x.ref().deref()
assert referenced_var is x
referenced_var
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>

资源和延伸阅读

  • 请访问迁移到 TF2 部分,详细了解如何从 TF1.x 迁移到 TF2。
  • 阅读模型映射指南,详细了解如何映射 TF1.x 模型以直接在 TF2 中使用。