为什么tf.random.uniform在函数内部生成重复项,而在主程序中不生成



我是TensorFlow的新手,我试图了解种子生成是如何工作的(主要是函数),我的困惑是如果我在主程序中运行这段代码:

a = tf.random.uniform([1], seed=1)
b = tf.random.uniform([1], seed=1)
print(a)
print(b)

我得到这个输出

tf.Tensor([0.2390374],shape=(1,),dtype=float32)
tf.Tonsor([0.2267115],shape=(1,,),dtype=floate32)

两个不同的张量(显然),但如果我试图从一个函数调用它:

@tf.function
def foo():
a = tf.random.uniform([1], seed=1)
b = tf.random.uniform([1], seed=1)
print(a)
print(b)
return a, b
foo()

我明白了:
张量("random_uuniform/RandomUniform:0",shape=(1,),dtype=float32
<tf.Tensor:shape=(1,),dtype=float32,numpy=array([0.2390374],dtype=float32)>

有两件事我不明白:

  1. 为什么函数内部的两个打印显示的输出与函数的返回值不同,换句话说,为什么前两行与后两行相比没有显示值(numpy=array…)
  2. 为什么ab相等

让我们回答您的第一个问题

为什么函数内部的两个打印显示的输出与函数的返回值不同,换句话说,为什么前两行与后两行相比没有显示值(numpy=array…)?

热切执行与图形执行

Tensorflow的核心是使用张量运算(ops)。例如,表达式tf.random.uniform([1], seed=1)定义了一个运算,该运算在执行时将生成一个种子为1的单个随机数。

现在,您将需要实际执行这个操作来获得一个具体的随机数。有两种方法

  1. 定义后立即执行此操作权限。这被称为Eager Execution(EE),可以说是执行操作最直观、最灵活的方式。它也是TF 2.x中执行操作的默认方式
  2. 将此操作和可能的其他操作组合成一个静态优化计算图,并在每次执行时使用该优化图。这被称为图形执行(GE)。这是TF1.x中执行操作的默认方式,而在TF2.x中,这是可选的,可以使用@tf.function装饰器启用

在EE上下文中,操作tf.random.uniform([1], seed=1)在定义后立即执行,并立即返回一个具体值-一个EagerTensor对象。

a = tf.random.uniform([1], seed=1)
print(type(a))
[Out]:
tensorflow.python.framework.ops.EagerTensor

而在GE中,op只是优化计算图中节点(确切地说是Tensor对象)的符号引用。无论何时调用函数,操作都会被实际执行,此时它会生成一个具体的值——EagerTensor

@tf.function
def genRandom():
a = tf.random.uniform([1], seed=1)
print(f'a type is {type(a)}')
return a
print(f'output type is {type(genRandom())}')
[Out]:
a type is <class 'tensorflow.python.framework.ops.Tensor'>
output type is <class 'tensorflow.python.framework.ops.EagerTensor'>

GE允许执行各种静态图优化,以提高执行性能。然而,EE更直观,更容易调试,因为操作是动态执行的,而不是转换为神秘的图节点。一个好的做法是在EE中调试函数,并在最终生产中将它们封装在@tf.function中,以利用GE的速度优势。不过要小心,因为某些操作在EE上下文中是允许的,但在GE上下文中是不允许或不可用的

图形执行:跟踪

回到这个例子
@tf.function
def genRandom():
a = tf.random.uniform([1], seed=1)
print(f'a is {a}')
return a
_ = genRandom()
[Out]:
a is Tensor("random_uniform/RandomUniform:0", shape=(1,), dtype=float32)

如果在GE中,op仅在调用函数时执行,此时它会产生具体值,那么为什么函数内部的Pythonprint会显示图形节点ref,而不是a产生的具体值?

这是因为Pythonprint而不是TF操作,也不能转换为TF操作。因此,它不会被包括在计算图中,也不会被执行。然而,在运行该函数时,您确实得到了打印输出,那么这里发生了什么?

事实证明,TF是懒洋洋地构建图的。也就是说,只有当第一次调用用@tf.function修饰的函数时,TF才开始构建其图。这是通过将函数作为普通Python代码执行一次来实现的;记录";函数中的操作(一个称为跟踪的过程),由此构建优化图。之后,调用构建的图(而不是python代码)来生成输出。

换句话说,由于跟踪过程,Python的print(不是操作)将用于第一次调用。然而,一旦完成了跟踪并构建了图,它就不会包含在执行图中,也不会在后续调用中执行:

@tf.function
def genRandom():
a = tf.random.uniform([1], seed=1)
print(f'a is {a}')  # not an op, will not be included in graph and only print during tracing
return a
_ = genRandom()
[Out]:
a is Tensor("random_uniform/RandomUniform:0", shape=(1,), dtype=float32)
_ = genRandom()
[Out]:

要在执行图形时获得实际值,您需要使用tf.print(),这是一个将包含在图形中的有效TF-op。

@tf.function
def genRandom():
a = tf.random.uniform([1], seed=1)
tf.print("a is:", a)  # an op, will be included in graph and always print
return a
_ = genRandom()
[Out]:
a is: [0.239037395]
_ = genRandom()
[Out]:
a is: [0.222671151]

顺便说一句,如果您调用一个已经绘制好图形的函数,其参数类型与该函数上次跟踪时看到的参数类型不同,则可能会发生回溯(因为当前图形可能不适用于这种新类型),在这种情况下,函数内的python代码将再次执行。回溯成本很高,应该避免。


对于第二个问题

为什么在GE中a和b相等,而在EE中却不相等?

tf.random种子

现在你知道通用电气和EE是什么了,看看这个文档。它解释了您希望了解的关于seed参数的作用以及操作内部表示在GE和EE模式中的不同之处。简而言之,tf.random.uniform()操作创建的随机数完全取决于的三个因素

  1. 全局种子(通过tf.random.set_seed设置)
  2. 操作自己的种子(作为seed参数传递给它的构造函数)
  3. 运算的内部计数器_count表示,它计算运算执行的次数。每次调用tf.random.set_seed时,_count重置为0

在EE模式中,相同的操作共享相同的内部表示,因此您的ab共享相同的_count。你得到了两个不同的数字,因为生成它们所涉及的_count的值实际上相差1。

在GE模式中,每个运算都有自己的内部表示,因此ab有自己的_count,这解释了为什么得到相同的数字:生成这两个数字所涉及的一切都是相同的。

相关内容