tf.function和Autograph使用指南-Part 2

在第1部分中,我们已经知道了如何将TF 1.x代码转换为其eager的代码,然后又将eager的代码通过tf.function转换为图表示代码,并遇到了在该函数中创建状态(tf.Variable)时由于eager和图表示的差异而导致的问题。

在本文中,我们将要分析用tf.Tensor和python对象作为被tf.function装饰的函数的输入时的异同. 那么是否在被装饰函数中的所有逻辑都将转换为符合期望的图表示代码呢?

tf.function调用了AutoGraph

首先我们来看下tf.function的所有输入标志(input_signature):

def function(func=None,
             input_signature=None,
             autograph=True,
             experimental_autograph_options=None)

参数autograph的默认值为True, 因此之前我们用@tf.function装饰的函数是使用了autograph了的. 我们可以看文档中对该参数的描述:

  • 当该值为True时, 所有依赖于tensor值的python操作都会被加入到TensorFlow图中
  • 当该值为False时, 该函数的操作会被加入到Tensorflow图中, 但是python的逻辑不会被AutoGraph转化

因此, tf.function默认会调用AutoGraph. 我们接下来看看如果我们改变输入参数类型和函数结构的话会发生什么.

改变tf.Tensor的数据类型

我们先构思一个用来测试的函数, 函数输入参数的数据类型是很重要的, 因为输入参数会被用来构造图, 而且这个图是静态类型的, 并且会有它的一个独特的ID.(请参见第一部分)

@tf.function
def f(x):
    print("Python execution: ", x)
    tf.print("Graph execution: ", x)
    return x

注意两个print是不同的. 接着我们执行以下测试:

print("##### float32 test #####")
a = tf.constant(1, dtype=tf.float32)
print("first call")
f(a)
a = tf.constant(1.1, dtype=tf.float32)
print("second call")
f(a)

print("##### uint8 test #####")

b = tf.constant(2, dtype=tf.uint8)
print("first call")
f(b)
b = tf.constant(3, dtype=tf.uint8)
print("second call")
f(b)

我们得到以下结果:

##### float32 test #####
first call
Python execution:  Tensor("x:0", shape=(), dtype=float32)
Graph execution:  1
second call
Graph execution:  1.1
##### uint8 test #####
first call
Python execution:  Tensor("x:0", shape=(), dtype=uint8)
Graph execution:  2
second call
Graph execution:  3

我们可以看到传入不同数据类型的tensor时, 图会重新被构建一次(注意: print并没有被转成tf.print). 我们可以用tf.autograph.to_code(f.python_function)来看看生成的图表示代码:

def tf__f(x):
  try:
    with ag__.function_scope('f'):
      do_return = False
      retval_ = None
      with ag__.utils.control_dependency_on_returns(ag__.converted_call(print, None, ag__.ConversionOptions(recursive=True, force_conversion=False, optional_features=ag__.Feature.ALL, internal_convert_user_code=True), ('Python execution: ', x), {})):
        tf_1, x_1 = ag__.utils.alias_tensors(tf, x)
        with ag__.utils.control_dependency_on_returns(ag__.converted_call('print', tf_1, ag__.ConversionOptions(recursive=True, force_conversion=False, optional_features=ag__.Feature.ALL, internal_convert_user_code=True), ('Graph execution: ', x_1), {})):
          x_2 = ag__.utils.alias_tensors(x_1)
          do_return = True
          retval_ = x_1
          return retval_
  except:
    ag__.rewrite_graph_construction_error(ag_source_map__)

其中, 有些python代码只会在构建图的时候被执行:

with ag__.utils.control_dependency_on_returns(
        ag__.converted_call(
            print, None, ag__.ConversionOptions(
                recursive=True,
                force_conversion=False,
                optional_features=ag__.Feature.ALL,
                internal_convert_user_code=True),
            ('Python execution: ', x), {})
        ):

我们可以看到ag__.utils.control_dependency_on_returns会在ag__.converted_call返回结果时建立一个tf.control_dependencies的依赖, 这样能确保图是按照python代码的顺序执行的.

converted_call会打包函数的执行. 它的输入包含了可能需要的转换和执行该函数需要的所有输入参数:

  • f: 函数本身, 这里是print.
  • owner: 函数所在模块, 由于print是python内置函数, 因此为None. tf.print的owner就是tf_1, tf的别名
  • options: 转换选项, 也就是ag__.ConversionOptions
  • args, kwargs: 函数f(print)的参数

那么问题来了:

为什么在追踪函数执行的时候要确保这些python代码只被执行一次呢?

猜测:

我的猜测是没有直接的办法知道该python代码有没有可能会让图效果改变的副作用, 因此在函数第一次执行的时候就直接追踪并加入到图中了.

如果在第一次执行的时候监测到副作用, 那么图会被更新, 不然的话, 就像在这个例子中, python函数print会被tf.no_op取代.

由于这只是我的猜测, 我在这里提问了, 如果你也对这个问题感兴趣可以留意下.

使用python原生类型

我们可以不止用tf.Tensor作为输入, AutoGraph能够根据输入类型自动转化成新的图, 接下来我们会测试一下python原生类型和tf.Tensor作为输入时的异同.

由于python有三种数值类型: 整数, 浮点数, 和复数, 我们逐一对此进行测试:

def printinfo(x):
  print("Type: ", type(x), " value: ", x)

print("##### int test #####")
print("first call")
a = 1
printinfo(a)
f(a)
print("second call")
b = 2
printinfo(b)
f(b)

print("##### float test #####")
print("first call")
a = 1.0
printinfo(a)
f(a)
print("second call")
b = 2.0
printinfo(b)
f(b)

print("##### complex test #####")
print("first call")
a = complex(1.0, 2.0)
printinfo(a)
f(a)
print("second call")
b = complex(2.0, 1.0)
printinfo(b)
f(b)

输出有点不太符合预期了:

##### int test #####
first call
Type:  <class 'int'>  value:  1
Python execution:  1
Graph execution:  1

second call
Type:  <class 'int'>  value:  2
Python execution:  2
Graph execution:  2

##### float test #####
first call
Type:  <class 'float'>  value:  1.0
Graph execution:  1
second call
Type:  <class 'float'>  value:  2.0
Graph execution:  2

##### complex test #####
first call
Type:  <class 'complex'>  value:  (1+2j)
Python execution:  (1+2j)
Graph execution:  (1+2j)
second call
Type:  <class 'complex'>  value:  (2+1j)
Python execution:  (2+1j)
Graph execution:  (2+1j)

这意味着对于每一个不同的数值, 都有一个独立的图! 就是说:

  • f(1)构造了图, f(1.0)重复使用了这个图
  • f(2)构造了图, f(2.0)重复使用了这个图
  • f(1+2j)f(2+1j)都分别构造了图

这就很奇怪了.

我们可以通过调用函数f(1.0)看返回值的类型来看看其是否调用了输入整数的图:

ret = f(1.0)
if tf.float32 == ret.dtype:
    print("f(1.0) returns float")
else:
    print("f(1.0) return ", ret)

结果:

Graph execution:  1
f(1.0) return  tf.Tensor(1, shape=(), dtype=int32)

因此, 在输入参数是python原生类型的时候, 与ID相关联的是参数的(1.0==1)而不是类型.

警告: 由于每次不同的python值都会生成一个图, 因此对于每个可能的值, 都会执行一次python代码和图构造, 从而极大地降低效率.

(官方文档: Therefore, python numerical inputs should be restricted to arguments that will have few distinct values, such as hyperparameters like the number of layers in a neural network. This allows TensorFlow to optimize each variant of the neural network.)

效率测量

我们用以下代码来验证:

@tf.function
def g(x):
  return x

start = time.time()
for i in tf.range(1000):
  g(i)
end = time.time()

print("tf.Tensor time elapsed: ", (end-start))

start = time.time()
for i in range(1000):
  g(i)
end = time.time()

print("Native type time elapsed: ", (end-start))

按照上面的理论, 第一个循环只会执行一次python代码和构建一个图, 而第二次循环则会执行1000次python代码和构建1000个图.

结果是符合预期的:

tf.Tensor time elapsed:  0.41594886779785156
Native type time elapsed:  5.189513444900513

结论: 在每个地方都用tf.Tensor.

(译者注: 这并不准确, 我们应该理解成: 用python原生类型来控制构造图, 用tf.Tensor做一切实际运算. 比如我们应该用python的整数来控制隐层数量, 用tf.Tensor来传入训练数据, 而不应该用 tf.Tensor来控制隐层数量, numpy array来传入训练数据)

tf.function真的是在用AutoGraph吗?

这个部分暂且省略, 因为在这个帖子里面的开发者说, 他们之后会让tf.function和AutoGraph的行为一致.

结论

在本文中我们分析了tf.function在启用AutoGraph的情况下的行为:

  • 在用tf.Tensor的时候, 所有东西都在预期之中
  • 如果是在用python原生类型的时候, 每个不同的值都会建立一个不同的图, 相同的值的话会被重复
  • python的函数代码只会在构建图的时候被执行一次

在第三部分的文章中, 我们会探寻比print更加复杂的函数, 来看看各种操作重载等python操作的行为.

声明: 本文翻译自Paolo Galeone的博客, 已取得作者的同意, 如需转载本文请联系本人

Disclaimer: This is a translation of the article Analyzing tf.function to discover AutoGraph strengths and subtleties by Paolo Galeone.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容