【ML】TensorFlow 图

  大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流

个人主页-Sonhhxg_柒的博客_CSDN博客 

欢迎各位→点赞 + 收藏⭐️ + 留言​

系列专栏 - 机器学习【ML】 自然语言处理【NLP】  深度学习【DL】

 foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟

TF函数和具体函数

TF 函数是多态的,这意味着它们支持不同类型(和形状)的输入。例如,考虑以下tf_cube()函数:

@tf.function
def tf_cube(x):
    return x ** 3

每次如果您调用具有输入类型或形状的新组合的 TF 函数,它会生成一个新的具体函数,并有自己的图形专门用于该特定组合。这种参数类型和形状的组合称为输入签名。如果您使用之前已经看到的输入签名调用 TF 函数,它将重用它之前生成的具体函数。例如,如果您调用tf_cube(tf.constant(3.0)),则 TF 函数将重用它用于的相同具体函数tf_cube(tf.constant(2.0))(对于 float32 标量张量)。tf_cube(tf.constant([2.0]))但是如果你调用or (对于形状为 [1] 的 float32 张量),它将生成一个新的具体函数tf_cube(tf.constant([3.0])),而另一个对于tf_cube(tf.constant([[1.0, 2.0], [3.0, 4.0]]))(对于形状为 [2, 2] 的 float32 张量)。get_concrete_function()您可以通过调用 TF 函数的方法来获取特定输入组合的具体函数。然后可以像常规函数一样调用它,但它只支持一个输入签名(在本例中,float32 标量张量):

>>> concrete_function = tf_cube.get_concrete_function(tf.constant(2.0))
>>> concrete_function

>>> concrete_function(tf.constant(2.0))

图 G-1显示了我们调用andtf_cube()之后的 TF 函数:生成了两个具体的函数,每个签名一个,每个tf_cube(2)tf_cube(tf.constant(2.0))具有自己优化的函数图FuncGraph) 和自己的函数定义FunctionDef)。函数定义指向对应于函数输入和输出的图形部分。在每个FuncGraph中,节点(椭圆)表示操作(例如,幂、常数或参数的占位符,例如x),而边(操作之间的实线箭头)表示将流过图形的张量。左侧的具体函数专门用于x = 2,因此 TensorFlow 设法将其简化为始终只输出 8 (请注意,函数定义甚至没有输入)。右边的具体函数专门用于 float32 标量张量,无法简化。如果我们打电话tf_cube(tf.constant(5.0)),将调用第二个具体函数,占位符运算x输出5.0,然后幂运算将计算5.0 ** 3,因此输出为125.0。

【ML】TensorFlow 图_第1张图片

图 G-1。tf_cube() TF 函数及其 ConcreteFunctions 及其 FunctionGraphs

这这些图中的张量是符号张量,这意味着它们没有实际值,只有数据类型、形状和名称。一旦将实际值馈送到占位符x并执行图形,它们代表将流过图形的未来张量。符号张量可以提前指定如何连接操作,并且它们还允许 TensorFlow 在给定输入的数据类型和形状的情况下递归推断所有张量的数据类型和形状。

现在让我们继续深入了解,看看如何访问函数定义和函数图,以及如何探索图的操作和张量。

探索函数定义和图表

您可以使用该属性访问具体函数的计算图,并通过调用该图的方法graph获取其操作列表:get_operations()

>>> concrete_function.graph

>>> ops = concrete_function.graph.get_operations()
>>> ops
[,
 ,
 ,
 ]

在这个例子中,第一个运算代表输入参数x(它称为占位符),第二个“运算”代表常数 3,第三个运算代表幂运算(**),最后一个运算代表这个函数的输出(这是一个恒等运算,这意味着它只会复制幂运算的输出1 )。每个操作都有一个输入和输出张量列表,您可以使用操作inputsoutputs属性轻松访问这些张量。例如,让我们获取幂运算的输入和输出列表:

>>> pow_op = ops[2]
>>> list(pow_op.inputs)
[,
 ]
>>> pow_op.outputs
[]

该计算图如图 G-2 所示。

【ML】TensorFlow 图_第2张图片

图 G-2。计算图示例

请注意,每个操作都有一个名称。它默认为操作的名称(例如,"pow"),但您可以在调用操作时手动定义它(例如,tf.pow(x, 3, name="other_name"))。如果名称已经存在,TensorFlow 会自动添加唯一索引(例如"pow_1""pow_2"等)。每个张量也有一个唯一的名称:它总是输出这个张量的操作的名称,加上:0它是操作的第一个输出,还是:1第二个输出,依此类推。get_operation_by_name()您可以使用图形的或get_tensor_by_name()方法按名称获取操作或张量:

>>> concrete_function.graph.get_operation_by_name('x')

>>> concrete_function.graph.get_tensor_by_name('Identity:0')

具体函数还包含函数定义(表示为协议缓冲区2),其中包括函数的签名。这个签名允许具体函数知道哪些占位符输入输入值,以及返回哪些张量:

>>> concrete_function.function_def.signature
name: "__inference_cube_19068241"
input_arg {
  name: "x"
  type: DT_FLOAT
}
output_arg {
  name: "identity"
  type: DT_FLOAT
}

现在让我们更仔细地看一下跟踪。

仔细观察追踪

让我们调整tf_cube()函数以打印其输入:

@tf.function
def tf_cube(x):
    print("x =", x)
    return x ** 3

现在让我们称之为:

>>> result = tf_cube(tf.constant(2.0))
x = Tensor("x:0", shape=(), dtype=float32)
>>> result

result看起来不错,但看看打印的是什么:是x一个符号张量!它具有形状和数据类型,但没有值。此外,它还有一个名称 ( "x:0")。这是因为该print()函数不是 TensorFlow 操作,所以它只会在跟踪 Python 函数时运行,这发生在图形模式下,参数替换为符号张量(相同类型和形状,但没有值)。由于print()函数没有被捕获到图中,下次我们tf_cube()使用 float32 标量张量调用时,不会打印任何内容:

>>> result = tf_cube(tf.constant(3.0))
>>> result = tf_cube(tf.constant(4.0))

但是如果我们调用tf_cube()不同类型或形状的张量,或者使用新的 Python 值,函数将被再次跟踪,因此print()函数将被调用:

>>> result = tf_cube(2) # new Python value: trace!
x = 2
>>> result = tf_cube(3) # new Python value: trace!
x = 3
>>> result = tf_cube(tf.constant([[1., 2.]])) # New shape: trace!
x = Tensor("x:0", shape=(1, 2), dtype=float32)
>>> result = tf_cube(tf.constant([[3., 4.], [5., 6.]])) # New shape: trace!
x = Tensor("x:0", shape=(None, 2), dtype=float32)
>>> result = tf_cube(tf.constant([[7., 8.], [9., 10.]])) # Same shape: no trace

警告

如果您的函数有 Python 副作用(例如,它将一些日志保存到磁盘),请注意此代码只会在跟踪函数时运行(即每次使用新的输入签名调用 TF 函数时)。最好假设在调用 TF 函数时可以跟踪(或不跟踪)该函数。

在某些情况下,您可能希望将 TF 函数限制为特定的输入签名。例如,假设您知道您只会调用具有 28 × 28 像素图像批次的 TF 函数,但这些批次的大小会非常不同。您可能不希望 TensorFlow 为每个批量大小生成不同的具体函数,或者依靠它自行确定何时使用None. 在这种情况下,您可以像这样指定输入签名:

@tf.function(input_signature=[tf.TensorSpec([None, 28, 28], tf.float32)])
def shrink(images):
    return images[:, ::2, ::2] # drop half the rows and columns

这个 TF 函数将接受任何形状为 [*, 28, 28] 的 float32 张量,并且每次都会重用相同的具体函数:

img_batch_1 = tf.random.uniform(shape=[100, 28, 28])
img_batch_2 = tf.random.uniform(shape=[50, 28, 28])
preprocessed_images = shrink(img_batch_1) # Works fine. Traces the function.
preprocessed_images = shrink(img_batch_2) # Works fine. Same concrete function.

但是,如果您尝试使用 Python 值或意外数据类型或形状的张量调用此 TF 函数,则会收到异常:

img_batch_3 = tf.random.uniform(shape=[2, 2, 2])
preprocessed_images = shrink(img_batch_3)  # ValueError! Unexpected signature.

使用 AutoGraph 捕获控制流

如果你的函数包含一个简单的for循环,你期望会发生什么?例如,让我们编写一个函数,将其输入加 10,只需将 1 加 10 次:

@tf.function
def add_10(x):
    for i in range(10):
        x += 1
    return x

它工作得很好,但是当我们查看它的图表时,我们发现它不包含循环:它只包含 10 个加法操作!

>>> add_10(tf.constant(0))

>>> add_10.get_concrete_function(tf.constant(0)).graph.get_operations()
[, [...],
 , [...],
 , [...],
 , [...],
 [...]
 , [...],
 ]

这实际上是有道理的:当函数被跟踪时,循环运行了 10 次,因此该x += 1操作运行了 10 次,并且由于它处于图形模式,因此它在图形中记录了该操作 10 次。您可以将此for循环视为在创建图形时展开的“静态”循环。

如果您希望图形包含一个“动态”循环(即,在图形执行时运行的循环),您可以使用该tf.while_loop()操作手动创建一个,但它不是很直观(请参阅“使用 AutoGraph 捕获控制以第 12 章笔记本的 Flow”部分为例)。相反,使用第 12 章讨论的TensorFlow 的AutoGraph功能要简单得多。AutoGraph 实际上是默认激活的(如果你需要关闭它,你可以传递给)。那么如果它打开了,为什么它没有捕获函数中的循环呢?好吧,它只捕获迭代的循环,而不是. 这是给你的选择:autograph=Falsetf.function()foradd_10()fortf.range()range()

  • 如果使用range(),则for循环将是静态的,这意味着它只会在跟踪函数时执行。正如我们所见,循环将被“展开”成每次迭代的一组操作。

  • 如果使用tf.range(),则循环将是动态的,这意味着它将包含在图形本身中(但在跟踪期间不会运行)。

让我们看看如果您只是在函数中替换range()为生成的图形:tf.range()add_10()

>>> add_10.get_concrete_function(tf.constant(0)).graph.get_operations()
[, [...],
 , [...],
 , [...],
 ]

如您所见,该图现在包含一个While循环操作,就像您调用了该tf.while_loop()函数一样。

在 TF 函数中处理变量和其他资源

在 TensorFlow 中,变量和其他有状态的对象,例如队列或数据集,称为资源。TF 函数对它们特别小心:任何读取或更新资源的操作都被认为是有状态的,并且 TF 函数确保有状态的操作按照它们出现的顺序执行(与可能并行运行的无状态操作相反,因此它们的不保证执行顺序)。此外,当您将资源作为参数传递给 TF 函数时,它会通过引用传递,因此函数可能会修改它。例如:

counter = tf.Variable(0)

@tf.function
def increment(counter, c=1):
    return counter.assign_add(c)

increment(counter) # counter is now equal to 1
increment(counter) # counter is now equal to 2

如果您查看函数定义,第一个参数被标记为资源:

>>> function_def = increment.get_concrete_function(counter).function_def
>>> function_def.signature.input_arg[0]
name: "counter"
type: DT_RESOURCE

也可以tf.Variable在函数外部使用定义,而无需将其作为参数显式传递:

counter = tf.Variable(0)

@tf.function
def increment(c=1):
    return counter.assign_add(c)

TF 函数会将其视为隐式的第一个参数,因此它实际上会以相同的签名结束(参数名称除外)。但是,使用全局变量很快就会变得混乱,因此您通常应该将变量(和其他资源)包装在类中。好消息也@tf.function适用于方法:

class Counter:
    def __init__(self):
        self.counter = tf.Variable(0)

    @tf.function
    def increment(self, c=1):
        return self.counter.assign_add(c)

警告

不要对 TF 变量使用=+=-=或任何其他 Python 赋值运算符。相反,您必须使用assign()assign_add()assign_sub()方法。如果您尝试使用 Python 赋值运算符,则在调用该方法时会出现异常。

这种面向对象方法的一个很好的例子当然是 tf.keras。让我们看看如何在 tf.keras 中使用 TF 函数。

将 TF 函数与 tf.keras 一起使用(或不使用)

默认情况下,您与 tf.keras 一起使用的任何自定义函数、层或模型都将自动转换为 TF 函数;你根本不需要做任何事情!但是,在某些情况下,您可能希望禁用这种自动转换——例如,如果您的自定义代码无法转换为 TF 函数,或者您只想调试代码,这在 Eager 模式下要容易得多。为此,您可以dynamic=True在创建模型或其任何层时简单地传递:

model = MyModel(dynamic=True)

如果您的自定义模型或层将始终是动态的,您可以改为调用基类的构造函数dynamic=True

class MyLayer(keras.layers.Layer):
    def __init__(self, units, **kwargs):
        super().__init__(dynamic=True, **kwargs)
        [...]

run_eagerly=True或者,您可以在调用方法时传递compile()

model.compile(loss=my_mse, optimizer="nadam", metrics=[my_mae],
              run_eagerly=True)

现在您知道了 TF Functions 如何处理多态性(具有多个具体函数),如何使用 AutoGraph 和跟踪自动生成图形,图形是什么样的,如何探索它们的符号操作和张量,如何处理变量和资源,以及如何使用带有 tf.keras 的 TF 函数。

你可能感兴趣的