欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

基于tf2的dl编程学习

程序员文章站 2022-09-17 09:27:00
基于tf2的dl编程学习(一)主要介绍tensorflow的基础部分的核心概念与机制,总结tensorflow作为通用计算框架的一些知识点。主要分为张量,计算图和自动微分机制三个部分。Tensorflow是一个通过计算图的形式来描述计算的编程系统,节点为定义的数学计算,而连接节点的边即为张量。张量在计算图中流动,进出节点完成计算,最终流到终点得到结果。张量张量(Tensor)是tensorflow中基础的数据结构,就像Ndarray之于Numpy一样。二者的行为特性也十分相似,都是描述多维数组,操作,...

主要介绍tensorflow的基础部分的核心概念与机制,总结tensorflow作为通用计算框架的一些知识点。主要分为张量和计算图两个部分。Tensorflow是一个通过计算图的形式来描述计算的编程系统,节点为定义的数学计算,而连接节点的边即为张量。张量在计算图中流动,进出节点完成计算,最终流到终点得到结果。

张量

张量(Tensor)是tensorflow中基础的数据结构,就像Ndarray之于Numpy一样。二者的行为特性也十分相似,都是描述多维数组,操作,类型也都有通用的地方。

常量

常量即值不可改变的张量,在计算图中不可被重新赋值。常量一般用于存储不变的数值如超参数等。初始化方法包括数据转换和直接生成:

a = tf.constant(data)  # 使用constant函数,data可为python基础数据类型和ndarray
a = tf.zeros(shape)
a = tf.zeros(shape)  # 直接初始化为0/1张量
a = tf.eye(x,y)  # 单位矩阵
a = tf.linalg.diag(list)  # 对角矩阵,list为对角线上元素
a = tf.fill(shape, data)  # data为标量,填充shape大小的张量
a = tf.linspace(start, end, n)  # 插值,生成长度为n的均匀分布一维张量
a = tf.range(start, end, step)  # 相当于把range函数结果转换为张量
a = tf.random.uniform(shape, minval=a, maxval=b)  # 均匀分布
a = tf.random.normal(shape, mean=a, stddev=b)  # 正态分布
a = tf.random.shuffle(data)  # 随机打乱

变量

变量即值可变的张量,一般用于存储模型中需要被训练的参数。初始化与改变变量值的方法包括:

a = tf.Variable(data)  # 使用Variable函数,data可为python基础数据类型,ndarray和Tensorflow常量
a.assign_add(data)  # 变量加上data,注意形状应保持一致,除非data为标量
a.assign(data)  # 变量的重新赋值

张量操作

对张量的操作主要分为结构操作和数值运算

结构操作

切片索引

张量的切片索引方式和numpy中ndarray的操作几乎完全一样

t[2,4]  # 索引
t[1:4, :4:2]  # 切片
# tensorflow还支持不规则切片(只抽取元素而不改变原张量)
y = tf.gather(t,[a,b,c], axis=1)  # 按axis抽取第a,b,c个元素组成,其他维度不变
y = tf.gather_nd(t, indices=[indice_1, indice_2, ...])  # 按坐标indice抽取元素
y = tf.boolean_mask(t, boolean)  # boolean为布尔值矩阵,提取出对应位置元素
# 可利用以下函数不规则地修改张量
y = tf.where(boolean)  # 返回满足布尔表达式的元素位置
y = tf.scatter_nd([indice_1, indice_2,...], [value_1, value_2,...], shape)  # 生成一个全0矩阵,指定位置插入指定数值,indice和value列表应长度相等
维度变换

张量的各个元素在内存中是线性存储的,一般同一层级的相邻元素的物理地址也相邻。Tensorflow提供了在不修改张量内部元素的情况下进行张量变形的API:

b = tf.transpose(a, perm=[axis_0, axis_1,...])  # 按照perm交换对应维度,会修改元素相对顺序
# 以下函数返回结果中,元素相对顺序不变
b = tf.reshape(a, shape)  # 使用a中元素生成shape形状的b张量
b = tf.squeeze(a)  # 压缩,消除只有一个元素的维度
b = tf.expand_dims(a, axis=x)  # 在a张量的x维增加一个长度为1的维度
合并分割
tf.concat([tensor_1, tensor_2,...], axis = x)  # 沿axis合并
tf.split(a, n, axis = x)  # 将a沿x维平均分为n份
tf.stack([tensor_1, tensor_2,...])  # 张量堆叠,会增加维度

数值运算

标量运算

张量的标量运算和矩阵运算基本相同。基本的数学运算符(+,-,*,/,**,//,%)都表示张量对应位置元素的逐元素运算,张量形状应一致。值得注意的是,张量参与的布尔表达式会生成相同形状的布尔值张量,结果为对原张量各元素逐一应用布尔表达式的结果。

向量运算

向量运算只在特定的维度上进行,将一个向量映射到一个标量或另一个向量:

# 对张量a的x维进行相应运算,默认的keepdims=False会返回一维向量或标量,True会保留维度
tf.reduce_sum(a, axis = x, keepdims=True)
tf.reduce_mean(a, axis = x, keepdims=True)
tf.reduce_max(a, axis = x, keepdims=True)
tf.reduce_min(a, axis = x, keepdims=True)
tf.reduce_prod(a, axis = x, keepdims=True)
# 对布尔值张量的reduce
tf.reduce_all(a)  # 所有元素与
tf.reduce_any(a)  # 所有元素或
# 获取元素索引
tf.argmax(a)  # 最大值索引
tf.argmin(a)  # 最小值索引
矩阵运算

矩阵特指二维张量,除基本操作外,大部分函数都在tf.linalg包中:

tf.matmul(a,b)  # 矩阵乘法
tf.transpose(a)  # 矩阵转置
tf.linalg.inv(a)  # 矩阵求逆,数据类型必须为tf.float32或tf.double
tf.linalg.det(a)  # 矩阵求行列式
tf.linalg.eigvalsh(a)  # 矩阵求特征值
tf.linalg.norm(a)  # 矩阵求范数
q,r = tf.linalg.qr(a)  # 矩阵求qr分解
v,s,d = tf.linalg.svd(a)  # 矩阵求svd分解

计算图

计算图是用图的方式来表示数据的运算。经过若干函数复合映射的过程可以清晰地对应到图的边与节点的关系中。边表示依赖关系,数据依赖即流动的张量,控制依赖即执行顺序;节点即为定义的操作,或称之为算子。Tensorflow有三种图的构建方式:
静态计算图,动态计算图和Autograph。
Tensorflow1.x使用的是静态计算图的方式,需要先创建计算图,再开启Session执行计算。好处是运行效率高。
Tensroflow2.x使用的是动态计算图的方式,算子创建即加入到隐含的默认计算图中,立即执行。好处是方便编写和调试。
可利用@tf.function装饰器,将Python函数转换成Tensorflow计算图,相当于在Tensorflow1.x中用Session执行代码,即可在Tensorflow2.x中使用静态图,这样的方式叫做Autograph。

静态计算图

使用静态计算图分为两步,定义计算图和在会话中执行计算图。

# 定义计算图
g = tf.Graph()
with g.as_default():
	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:
	sess.run(fetches=z, feed_dict={x:"hello", y:"world"})

动态计算图

与静态计算图对比,定义后直接执行,就像命令行中的输入python代码一样。

# 动态计算图的输入输出可封装为函数
def strjoin(x,y):
	z = tf.strings.join([x,y],separator = " ")
	tf.print(z)
	return z
# 直接调用函数执行得到结果
result = strjoin(tf.constant("hello"),tf.constant("world"))

Autograph

Autograph是在Tensorflow2.x中使用静态计算图的方法。可以使用动态计算图的形式,得到静态计算图的效率。在实践中,一般先用动态计算图调试代码,然后在需要提高性能的地方使用@tf.function切换成Autograph。
知识补充:
Python装饰器是调用函数包装的一种简写形式,将被装饰的函数作为参数传到装饰器函数,对其进行处理并返回,是一种函数的复合。@tf.function接受被装饰的函数,将其内定义的算子全部转换为静态计算图。

编码规范

Autograph能够兼具执行效率和编码效率,但也有相应的约束,即要遵守的编码规范,否则可能转换失败或出错。这些规范总结为:

  • 被@tf.function修饰的函数应尽可能使用tensorflow中定义的函数和数据,而不是具有相似形式的numpy,python函数或数据。
  • 避免在@tf.function修饰的函数中定义tf.Variable
  • 被@tf.function修饰的函数不可修改函数外部的Python数据结构变量

机制原理

调用被@tf.function修饰的函数时,会先创建计算图,再运行计算图。创建计算图时,跟踪执行一遍函数体内的Python代码,读取并确定各个变量的Tensor类型,将语句转换为对应算子,按执行顺序加入到计算图中。主要将if语句转换为tf.cond算子,while和for循环语句转换为tf.while_loop算子,并在必要的时候添加tf.control_dependencies指定执行顺序依赖关系。
由此机制原理可以理解之前的编码规范:

  • 普通的Python函数无法嵌入静态计算图中,会导致@tf.function装饰函数的输出结果与动态计算图的输出结果不一致;
  • 在函数内部定义tf.Variable在静态图执行时,只会发生一次;而在动态图中每次都会发生;
  • 静态计算图是被编译成C++代码在Tensorflow内核中执行的,Python的数据结构变量仅能在创建计算图时被读取,执行计算图时无法修改。

使用一个实验来理解调用被@tf.function修饰的函数所发生的事情:

@tf.function
def myadd(a,b):
	for i in tf.range(3):
		tf.print(i)
	c = a+b
	print("tracing")
	return c
myadd(tf.constant("hello"),tf.constant("world"))
# 输出:
tracing
0
1
2

可以发现,打印出来的结果并不是如正常的Python执行顺序“0 1 2 3 tracing”。被装饰的函数被转换成了以下类似的代码:

# 计算图的创建
g = tf.Graph()
with g.as_default():
	a = tf.placeholder(shape=[],dtype=tf.string)
	b = tf.placeholder(shape=[],dtype=tf.string)
	cond = lambda i: i<tf.constant(3)
	def body(i):
		tf.print(i)
		return(i+1)
	loop = tf.while_loop(cond,body,loop_vars=[0])  # 被加入图,需要Session执行
	loop
	with tf.control_dependencies(loop):
		c = tf.strings.join([a,b])
	print("tracing")  # 立刻执行
# 然后运行计算图
with tf.Session(graph=g) as sess: 
	sess.run(c,feed_dict={a:tf.constant("hello"),b:tf.constant("world")})

可以看到,print语句因没有加入到计算图中,在创建计算图时已经输出;而for语句被转换为tf算子加入计算图,然后计算图运行,输出“1 2 3”。
需要注意的是,重复调用相同的函数不会重复创建相同的计算图;但如果传入的参数类型发生变化,计算图发生了改变,会创建新的计算图。

tf.Module

由于Autograph编码规范的限制,会使得封装不够完美,代码不够自然。Tensorflow提供了一个基类tf.Module,通过继承它构建子类,可非常方便地管理变量,引用的其他Module,并利用tf.saved_model保存模型并实现跨平台部署使用。

# 在初始化方法内定义tf.Variable
# with self.name_scope定义一个命名空间,目的是方便区分对象和对变量的使用没有影响
class DemoModule(tf.Module):
	def __init__(self,init_value = tf.constant(0.0),name=None):
		super(DemoModule, self).__init__(name=name)
		with self.name_scope: #相当于with tf.name_scope("demo_module")
			self.x = tf.Variable(init_value,dtype = tf.float32,trainable=True)
	
	# 限制输入张量的形状和类型
	@tf.function(input_signature=[tf.TensorSpec(shape = [], dtype = tf.float32)])
	def addprint(self,a):
		with self.name_scope:
			self.x.assign_add(a)
			tf.print(self.x)
			return self.x

tf.Module的常用属性与函数:

# demo为tf.Module实例
demo.variables  # 全部变量
demo.trainable_variables  # 可训练变量
demo.submodules  # 全部子模块
tf.saved_model.save(demo, path, signatures={"serving_default":demo.addprint})  # 保存模型,并指定需要跨平台部署的方法
tf.saved_model.load(path)

本文地址:https://blog.csdn.net/shiningsword/article/details/109606027