【TensorFlow2】2.0版本后张量与计算图的构建

问题概述

在初学TensorFlow2构建图和张量时发现网上的很多代码包括教程文档等都无法顺利运行,后来发现是因为网上的资源很多都是基于1.0版本的,而2.0版本的写法都变了,这里就张量与图作出一些总结。


张量简介

如果说程序=数据结构+算法,那么TensorFlow程序=张量数据结构+计算图算法语言,张量和计算图是 TensorFlow的核心概念。
TensorFlow的基本数据结构是张量Tensor。张量即多维数组,Tensorflow的张量和numpy中的array很类似。
从行为特性来看,有两种类型的张量,常量constant和变量Variable。常量的值在计算图中不可以被重新赋值,变量可以在计算图中用assign等算子重新赋值。

常量张量

import numpy as np
import tensorflow as tf

i = tf.constant(1) # tf.int32 类型常量
l = tf.constant(1,dtype = tf.int64) # tf.int64 类型常量
f = tf.constant(1.23) #tf.float32 类型常量
d = tf.constant(3.14, dtype = tf.double) # tf.double 类型常量
s = tf.constant("hello world") # tf.string类型常量
b = tf.constant(True) #tf.bool类型常量

print(tf.int64 == np.int64) # ==> True
print(tf.bool == np.bool) # ==> True
print(tf.double == np.float64) # ==> True
print(tf.string == np.unicode) # ==> False tf.string类型和np.unicode类型不等价

不同类型的数据可以用不同维度(rank)的张量来表示。标量为0维张量,向量为1维张量,矩阵为2维张量;彩色图像有rgb三个通道,可以表示为3维张量;视频还有时间维,可以表示为4维张量。
可以用tf.cast改变张量的数据类型,如:

h = tf.constant([123, 456], dtype = tf.int32)
f = tf.cast(h, tf.float32)
print(h.dtype, f.dtype) # ==> <dtype: 'int32'> <dtype: 'float32'>

可以用numpy方法将tensorflow中的张量转化成numpy中的张量,用shape方法查看张量的尺寸,如:

y = tf.constant([[1.0, 2.0], [3.0, 4.0]])
y_numpy = y.numpy() # 转换成np.array
print(y_numpy ) # ==> [[1. 2.] [3. 4.]] 
print(y.shape) # ==> (2, 2)

u = tf.constant(u"你好 世界")
print(u.numpy())   # ==> b'\xe4\xbd\xa0\xe5\xa5\xbd \xe4\xb8\x96\xe7\x95\x8c'
print(u.numpy().decode("utf-8")) # ==> 你好 世界

常量值不可以改变,常量的重新赋值相当于创造新的内存空间,如:

c = tf.constant([1.0, 2.0])
print(c) # ==> tf.Tensor([1. 2.], shape=(2,), dtype=float32)
print(id(c)) # ==> 5276289568
c = c + tf.constant([1.0, 1.0])
print(c) # ==> tf.Tensor([2. 3.], shape=(2,), dtype=float32)
print(id(c)) # ==> 5276290240

变量张量

模型中需要被训练的参数一般被设置成变量,变量在计算图中需要用assign等算子重新赋值,如:

v = tf.Variable([1.0, 2.0], name = "v")
print(v) # ==> <tf.Variable 'v:0' shape=(2,) dtype=float32, numpy=array([1., 2.], dtype=float32)>
print(id(v)) # ==> 5276259888
v.assign_add([1.0, 1.0]) # 加法
print(v) # ==> <tf.Variable 'v:0' shape=(2,) dtype=float32, numpy=array([2., 3.], dtype=float32)>
print(id(v)) # ==> 5276259888
v.assign_sub([1.0, 1.0]) # 减法
print(v) # ==> <tf.Variable 'v:0' shape=(2,) dtype=float32, numpy=array([0., 1.], dtype=float32)>
print(id(v)) # ==> 5276259888

计算图

有三种计算图的构建方式:静态计算图,动态计算图,以及Autograph。
在TensorFlow1.0时代,采用的是静态计算图,需要先使用TensorFlow的各种算子创建计算图,然后再开启一个会话Session,显式执行计算图。
而在TensorFlow2.0时代,采用的是动态计算图,即每使用一个算子后,该算子会被动态加入到隐含的默认计算图中立即执行得到结果,而无需开启Session。
使用动态计算图即Eager Excution的好处是方便调试程序,它会让TensorFlow代码的表现和Python原生代码的表现一样,写起来就像写numpy一样,各种日志打印,控制流全部都是可以使用的。
使用动态计算图的缺点是运行效率相对会低一些。因为使用动态图会有许多次Python进程和TensorFlow的C++进程之间的通信。而静态计算图构建完成之后几乎全部在TensorFlow内核上使用C++代码执行,效率更高。此外静态图会对计算步骤进行一定的优化,剪去和结果无关的计算步骤。
如果需要在TensorFlow2.0中使用静态图,可以使用@tf.function装饰器将普通Python函数转换成对应的TensorFlow计算图构建代码。运行该函数就相当于在TensorFlow1.0中用Session执行代码。使用tf.function构建静态图的方式叫做 Autograph。
这也是无法按照网上代码顺利构建图的原因。

静态计算图

在TensorFlow1.0中,使用静态计算图分两步,第一步定义计算图,第二步在会话中执行计算图,如:

import tensorflow as tf

# 定义计算图
g = tf.Graph()
# 作为默认图
with g.as_default():
    #placeholder为占位符,执行会话时候指定填充对象
    x = tf.placeholder(name='x', shape=[], dtype=tf.string)  
    y = tf.placeholder(name='y', shape=[], dtype=tf.string)
    z = tf.string_join([x, y],name = 'join', separator=' ')
    
# 新建会话,执行计算图
with tf.Session(graph = g) as sess:
    print(sess.run(fetches = z, feed_dict = {x:"hello", y:"world"})) # ⇒ hello world

而在2.0版本以后使用动态计算图,不再区分这两步。

动态计算图

在TensorFlow2.0中,使用的是动态计算图和Autograph。动态计算图已经不区分计算图的定义和执行了,而是定义后立即执行。因此称之为 Eager Excution。Eager这个英文单词的原意是”迫不及待的”,也就是立即执行的意思。

# 动态计算图在每个算子处都进行构建,构建后立即执行
x = tf.constant("hello")
y = tf.constant("world")
z = tf.strings.join([x, y], separator=" ")
tf.print(z) # ⇒ hello world

也可以将动态计算图代码的输入和输出关系封装成函数,如:

def strjoin(x, y):
    z =  tf.strings.join([x, y], separator = " ")
    tf.print(z)
    return z # ⇒ hello world
    
result = strjoin(tf.constant("hello"),tf.constant("world"))
print(result) # ⇒ tf.Tensor(b'hello world', shape=(), dtype=string)

如果硬是想在2.0版本中使用1.0版本的计算图,可是可以的。2.0版本提供了tf.compat.v1模块来兼容低版本代码,如:

import tensorflow as tf

g = tf.compat.v1.Graph()
with g.as_default():
    x = tf.compat.v1.placeholder(name='x', shape=[], dtype=tf.string)
    y = tf.compat.v1.placeholder(name='y', shape=[], dtype=tf.string)
    z = tf.strings.join([x, y], name = "join", separator = " ")
    
with tf.compat.v1.Session(graph = g) as sess:
    # fetches的结果非常像一个函数的返回值,而feed_dict中的占位符相当于函数的参数序列。
    result = sess.run(fetches = z, feed_dict = {x:"hello", y:"world"})
    print(result) # ⇒ b'hello world'

Autograph

动态计算图运行效率相对较低,但是可以用@tf.function装饰器将普通Python函数转换成和TensorFlow1.0对应的静态计算图构建代码。
在TensorFlow1.0中,使用计算图分两步,第一步定义计算图,第二步在会话中执行计算图。
在TensorFlow2.0中,如果采用Autograph的方式使用计算图,第一步定义计算图变成了定义函数,第二步执行计算图变成了调用函数。不需要使用会话了,一些都像原始的Python语法一样自然。
实践中,我们一般会先用动态计算图调试代码,然后在需要提高性能的的地方利用@tf.function切换成Autograph获得更高的效率。

import tensorflow as tf

# 使用autograph构建静态图
@tf.function
def strjoin(x, y):
    z =  tf.strings.join([x,y], separator = " ")
    tf.print(z)
    return z # ⇒ hello world
    
result = strjoin(tf.constant("hello"), tf.constant("world"))
print(result) # ⇒ tf.Tensor(b'hello world', shape=(), dtype=string)
上一篇:TensorFlow2 入门指南 | 08 认识与搭建全连接层神经网络


下一篇:深度残差网络(ResNet)原理与实现(tensorflow2.x)