tensorflow2.0基本操作

BG18

Posted by Blue Geek on October 2, 2019

tensorflow2.0基本操作

By 青衣极客 Blue Geek In 2019-10-02

import tensorflow as tf
print('tf.__version__ =', tf.__version__)
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
tf.__version__ = 2.0.0

1. 数据类型

对于一个新的模块,最先需要了解的部分就是数据类型。tensorflow的名字其实就已经阐明了这种最基本的程序结构:数据+操作。TF的数据类型自然如它的名字所说的是Tensor,即张量。所谓张量就是计算机中就是多维数组。

(1) 张量的基础类型

张量是一个复合类型,也是tensorflow中的基本类型,但是张量的元素的类型是更加基本的类型。目前的编程语言大概都是会提供这几种最基本的数据类型,即bool,int,float,double,string。

# 1. bool
print(tf.constant(True))
# 2. int32
print(tf.constant(1))
# 3. float
print(tf.constant(1.0))
# 4. double
print(tf.constant(1.0, dtype=tf.float64))
# 5. string
print(tf.constant('hello'))
# 注意点:与上面的float情况表现不一样
print(tf.constant(np.array(2.0)))
print(tf.constant(np.array(2.0),dtype=tf.float32))
tf.Tensor(True, shape=(), dtype=bool)
tf.Tensor(1, shape=(), dtype=int32)
tf.Tensor(1.0, shape=(), dtype=float32)
tf.Tensor(1.0, shape=(), dtype=float64)
tf.Tensor(b'hello', shape=(), dtype=string)
tf.Tensor(2.0, shape=(), dtype=float64)
tf.Tensor(2.0, shape=(), dtype=float32)

上面的演示中前五项都是很自然的,最后两项由numpy数组变换到tensorflow的张量时需要稍加注意。numpy创建数组时默认的浮点型是double,而tf在创建张量时默认是float32。tf在接收numpy数组时会同时接受其元素的数据类型。tf的使用者可能很多都是需要在GPU上运行的程序,而GPU处理float32比处理double要快很多,所以如果对数据精度要求并没有太高的话,最好还是使用单精度的float32,而不是双精度的double。

(2) Variable是特殊的张量

Variable也是tf的使用者常见的一种复合数据类型,一般用于模型中需要在训练过程中进行调整的参数。

print(tf.Variable(1.0))
print(isinstance(tf.Variable(1.0), tf.Tensor))
print(type(tf.Variable(1.0)))
print(tf.is_tensor(tf.Variable(1.0)))
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=1.0>
False
<class 'tensorflow.python.ops.resource_variable_ops.ResourceVariable'>
True

从打印出的Variable情况来看,它比一般的Tensor要多一些属性,而且有自己的单独类型tf.Variable,看起来并不像是Tensor。但是我们使用tf提供的接口来判断一下发现,Variable也是一种Tensor。

(3) Variable特殊在哪里?

既然Variable也只一种Tensor,那么它跟一般Tensor的区别在哪里呢?答案就在下面的演示中。

a = tf.Variable(1.0)
a.assign(2.0)  # 修改tensor的值
print(a)
print(a.trainable)

b = tf.constant(1.0)
try:
    print(b.trainable)  # 报错,一般的tensor没有trainable属性
    b.assign(2.0)       # 报错,一般的tensor不能被修改
except Exception as e:
    print('error:', e)
<tf.Variable 'Variable:0' shape=() dtype=float32, numpy=2.0>
True
error: 'tensorflow.python.framework.ops.EagerTensor' object has no attribute 'trainable'

Variable在初始化之后还是可以修改其内容的,并且其包含了“trainable”属性,该属性在训练时可以用于判断这个Variable是否需要被调整。而一般的Tensor在初始化之后是不能修改内容的,如果需要修改,就只能生成一个新的Tensor。并且一般的Tensor没有“trainable”属性,说明在训练时,是完全不需要在误差反传中进行调整。

(4) 张量的常用属性

在使用tf的过程中,Tensor的一些属性是经常会用到的,比如形状、维度、元素类型等等。

a = tf.constant([1.0])
print('张量的形状:', a.shape)
print('张量的维度:', a.ndim)
print('张量元素的类型:', a.dtype)
print('张量转换为numpy数组:', a.numpy())
张量的形状: (1,)
张量的维度: 1
张量元素的类型: <dtype: 'float32'>
张量转换为numpy数组: [1.]

除此之外,还有一些,cpu、gpu之类的属性方法,一般不建议使用。因为tf本身就会自动调用所有可用资源来进行计算,如果设置了比较多的GPU相关参数会使得程序在跨平台运行以及代码分享中造成麻烦。

tensorflow的数据类型总体而言跟numpy是非常相似的,有使用numpy经验的朋友完全可以无门槛进入tensorflow的世界。希望本文能为tensorflow2.0的初学者提供一些有效的信息,以节省入门成本。

2. 创建张量

在前面的文章中了解了tensorflow的数据类型是Tensor之后,就需要掌握常用的张量创建方式。本文介绍在日常开发中使用频率最高的几种创建方式。

按照定义而言,张量应该是维度大于等于3的数组,但是目前广义的张量也包含一维和二维的数组。这里创建张量指的是tf中的Tensor这种数据结构的对象。

(1) 从python基本类型或numpy数组创建

python的基本数据类型或者numpy数组是python开发者比较常用的数据类型,从这些数据转换到tf的Tensor也是使用频率比较高的。

# 从python基本类型创建
print(tf.constant(1))
print(tf.convert_to_tensor(1))
# 从list创建
print(tf.constant([1,2,3]))
print(tf.convert_to_tensor([1,2,3]))
# 从numpy数组创建
print(tf.constant(np.array([1,2,3])))
print(tf.convert_to_tensor(np.array([1,2,3])))
tf.Tensor(1, shape=(), dtype=int32)
tf.Tensor(1, shape=(), dtype=int32)
tf.Tensor([1 2 3], shape=(3,), dtype=int32)
tf.Tensor([1 2 3], shape=(3,), dtype=int32)
tf.Tensor([1 2 3], shape=(3,), dtype=int64)
tf.Tensor([1 2 3], shape=(3,), dtype=int64)

tf.constant和tf.convert_to_tensor都能达到将其他数据类型转换为Tensor的目的,一般而言,用tf.constant更方便一点。

(2) 使用tf中类似numpy的操作创建

熟悉numpy的朋友应该知道numpy中提供了很多创建数组的函数,tf也实现了同样的一组函数,这对于原先的numpy使用者进入tensorflow的世界提供了便利。

# 特殊值数组:ones, zeors, eye
print(tf.ones(shape=(1,3),dtype=tf.float32))
# 线性划分数组
print(tf.linspace(0.0,1.0, 3))
# 同形状填充特殊值:ones_like, zeros_like
print(tf.ones_like(tf.linspace(0.0, 1.0, 4)))
# 通用数值填充
print(tf.fill((1,5), 1.0))
tf.Tensor([[1. 1. 1.]], shape=(1, 3), dtype=float32)
tf.Tensor([0.  0.5 1. ], shape=(3,), dtype=float32)
tf.Tensor([1. 1. 1. 1.], shape=(4,), dtype=float32)
tf.Tensor([[1. 1. 1. 1. 1.]], shape=(1, 5), dtype=float32)

tf提供一组特殊值生成的函数,ones用于生成全为1的张量, zeros用于生成全为0的张量, eye用于生成单位矩阵,ones_like则根据另一个张量的形状生成一个全为1的张量,同样可以知道zeros_like的作用。由于常常需要一组等差数列用于函数自变量,tf提供了linspace来实现这种需求。对于通用的数值填充需求,tf提供了fill函数。

(3) 随机函数创建

在深度学习中,常常需要对网络模型中的参数进行初始化,一般来说,我们会选择随机初始化。正态分布和均匀分布的随机Tensor自然是常用的,不过截断的正态分布对模型训练而言更有好处。

print(tf.random.normal((1,2)))
print(tf.random.truncated_normal((1,2)))
print(tf.random.uniform((1,2), minval=0.0, maxval=1.0))
tf.Tensor([[1.7148873 1.5755692]], shape=(1, 2), dtype=float32)
tf.Tensor([[0.17559904 0.76959455]], shape=(1, 2), dtype=float32)
tf.Tensor([[0.23656762 0.94149077]], shape=(1, 2), dtype=float32)

作为一个通用的机器学习工具,tf当然还提供了更丰富的随机数的产生和控制接口,感兴趣的朋友可以更深入地了解。不过就算是使用tensorflow多年的资深人士,可能日常用到的也就这三种:normal(正态分布), truncated_normal(截断的正态分布) 和 uniform(均匀分布)。

(4) 运算过程中产生

运算过程中产生的也是这种普通的张量,因此运算结果本身是不能直接原地修改的,只能生成新的张量。

a = tf.constant([[1,2], [3,4]])
b = tf.constant([[5,6], [7,8]])
print(a*b)
tf.Tensor(
[[ 5 12]
 [21 32]], shape=(2, 2), dtype=int32)

既然tensorflow是以张量作为基础的数据结构,那么在运算中产生张量也就顺理成章了。

到此,tensorflow2.0中张量的创建方式已经讨论完毕。接下来大家就可以动手尝试一下各种张量的创建方式。

3. 张量索引与切片

实现一组好用的切片操作是数组能够大范围推广的一个原因,当数组维度很高只能依靠抽象的逻辑来进行处理,这时就需要频繁地进行切片。python语言本身就提供了神奇的切片效果,极大方便了对python序列对象的处理。numpy也提供了切片的操作方法,tensorflow张量自然也是可以进行切片的。

(1) 基本索引

基本的索引是操作张量的基本方式,虽然繁琐,也还是值得了解一下。

a = tf.constant([[1,2,3],[4,5,6]])
print(a[0])
print(a[0][0])
tf.Tensor([1 2 3], shape=(3,), dtype=int32)
tf.Tensor(1, shape=(), dtype=int32)

(2) 切片

这种类似与numpy风格的切片的基本语法是:start:end:step。其中start可以省略,默认值是0;end可以省略,默认值是包含最后一个元素,step可以省略,默认值是1,start和end之间的冒号不能省略。还有一个语法是省略号,省略号表示被省略的维度中取所有的shape。

print(a[0,1])        # 取元素
print(a[0:2, 0])     # 取第0列,省略step
print(a[0, 0:3:2])   # 完全形式的切片
print(a[0, ::-1])    # 省略start和end
print(a[0, :-3:-1])  # 省略start
print(a[0, -2::-1])  # 省略end
print(a[0, ...])     # 使用省略号
print(a[..., 1])     # 使用省略号
tf.Tensor(2, shape=(), dtype=int32)
tf.Tensor([1 4], shape=(2,), dtype=int32)
tf.Tensor([1 3], shape=(2,), dtype=int32)
tf.Tensor([3 2 1], shape=(3,), dtype=int32)
tf.Tensor([3 2], shape=(2,), dtype=int32)
tf.Tensor([2 1], shape=(2,), dtype=int32)
tf.Tensor([1 2 3], shape=(3,), dtype=int32)
tf.Tensor([2 5], shape=(2,), dtype=int32)

如果对于python中的切片不是太了解的话,可以阅读我的另一篇文章《python中神奇的切片》。值得一提的是这种省略号切片,省略号会根据当前参数传递的情况自动推导出应当作用在哪些维度上,对于一些维度较多的张量而言,切片时可以减少冒号的书写。

(3) 切片函数

除了以上方便的切片之外,tensorflow还提供了一组函数用于切片。tf.gather函数用于对指定维度进行切片,tf.gather_nd用于对多个维度进行切片,tf.boolean_mask用于根据bool值的蒙版进行切片。有朋友可能会担心,要是元素很多的话设置蒙版的值也太麻烦了。在实际使用过程中基本没有人直接设置蒙版的值,一般都是在程序运行过程中使用条件函数对已有Tensor进行处理生成的蒙版。

b = tf.ones((2,3,4,5,6), dtype=tf.int32)
# 收集指定维度的若干个索引,其他维度保持形状不变
print(tf.gather(b, axis=4, indices=[2]).shape)
# 收集指定的若干个维度的索引
print(tf.gather_nd(b, [[0,0,0], [0,0,1]]).shape)
# 使用bool蒙版
mask = tf.constant([False, False, False, True])
print(tf.boolean_mask(b, mask=mask, axis=2).shape)
(2, 3, 4, 5, 1)
(2, 5, 6)
(2, 3, 1, 5, 6)

切片操作讨论完毕,总体而言,索引操作是中规中矩,跟其他模块也差不多;直接切片操作与python本身的切片以及numpy数组的切片基本一致,上手也很容易。提供的一组用于切片的函数能够增加一些复杂场景的灵活度。

4. 改变张量形状与广播

对于张量的处理中,常常有改变张量形状的操作。这里面包含这三种情况,一种是将一个小张量复制成更高维张量,第二种是不改变元素个数的情况下改变张量的维度,还有一种自动改变维度的方式,即广播。这种机制会根据运算的需要自动对一些张量进行扩展以满足运算的形状要求。

(1) 不改变元素个数和维度

常用的不改变元素的个数和总的维度数的函数有两个,reshape和transpose。其中reshape更加通用一下,而transpose主要用于调换各个维度的顺序。

c = tf.ones((2,3,4), dtype=tf.int32)
print(c.shape)
# 使用reshape
print(tf.reshape(c, (6,4)).shape)
print(tf.reshape(c, (3, -1)).shape)
print(tf.reshape(c, (3, -1, 2)).shape)
# 使用transpose
print(tf.transpose(c).shape)
print(tf.transpose(c, perm=[1,0,2]).shape)
(2, 3, 4)
(6, 4)
(3, 8)
(3, 4, 2)
(4, 3, 2)
(3, 2, 4)

(2) 改变维度,不改变元素个数

改变维度数不改变总的元素个数的办法有两个,expand_dims和squeeze。其中expand_dims用于扩展出一个shape=1的维度,squeeze用于将所有shape=1的维度去掉。

d = tf.ones((2,3,4), dtype=tf.int32)
print(d.shape)
# 在指定维度上扩展出一个shape=1的维度
print(tf.expand_dims(d, axis=0).shape)
e = tf.expand_dims(d, axis=-1)
print(e.shape)
# 移去所有shape=1的维度
print(tf.squeeze(e).shape)
(2, 3, 4)
(1, 2, 3, 4)
(2, 3, 4, 1)
(2, 3, 4)

(3) 改变元素个数

改变元素个数一般来说张量的形状也是会改变的。tile函数用于将已有的张量复制多份,broadcast_to函数用于将一些shape=1的维度扩展到指定的shape。有一些运算操作会自动进行广播,以使得原本不符合运算规则的Tensor可以参加运算。比如加法运算。常用的操作就是tile和这种自动广播的操作。还有一种扩展元素的操作也比较常用,tf.pad。在卷积运算中,如果要保持输出跟输入的形状一致,就需要将输入的形状扩展kernel宽高的一半,也就是大家在卷积层中设置的padding参数的效果。

f = tf.ones((2,3), dtype=tf.int32)
# 复制
print(tf.tile(f, (2,4)).shape)
# 函数广播
print(tf.broadcast_to(tf.ones((2, 1)), (2,4)).shape)
# 自动广播
g = tf.ones((2,1), dtype=tf.int32)
print((f+g).shape)  # 自动广播
# 扩展元素
h =  tf.constant([[1,2],[3,4]])
print(tf.pad(h, [[2,1], [1,2]]))
(4, 12)
(2, 4)
(2, 3)
tf.Tensor(
[[0 0 0 0 0]
 [0 0 0 0 0]
 [0 1 2 0 0]
 [0 3 4 0 0]
 [0 0 0 0 0]], shape=(5, 5), dtype=int32)

改变张量形状的操作讨论完毕,在开发过程中掌握这些操作对于改变张量形状而言基本足够了。至于更多的改变张量形状的方法一般使用较少。

5 张量拼接与拆分

张量的拼接和拆分是计算图中的常用操作。有时需要把多个来源的张量合并成一个统一进行处理,有时也需要将一个来源的Tensor拆分成多个,分作不同的用途。掌握了张量的拼接和拆分事实上也就掌握了复杂计算图的构建方法。在目前的实际任务中,基本很难遇到只搭建一个单一的前向通路就能解决问题的情况。更多的情形是必须综合多个来源的数据,必须将产生的数据拆分给多个任务模块,有时还要对数据加上各种不同的约束。面对这些需求,如果没有好用的拼接和拆分操作那简直生不如死。幸好tensorflow提供了一些简单易用的操作来完成这些任务。

(1) concat和split

concat负责将多个Tensor在指定的坐标轴上拼接成一个,而split正好相反,它负责在指定坐标轴上根据指定参数将该坐标轴的形状切分成多个。

a = tf.constant([[1,2,3,4]], dtype=tf.int32)
b = tf.constant([[5,6,7,8]], dtype=tf.int32)
c = tf.concat((a,b), axis=0)
print(c)
print(tf.split(c, axis=1, num_or_size_splits=[1,1,2]))
tf.Tensor(
[[1 2 3 4]
 [5 6 7 8]], shape=(2, 4), dtype=int32)
[<tf.Tensor: id=1071, shape=(2, 1), dtype=int32, numpy=
array([[1],
       [5]], dtype=int32)>, <tf.Tensor: id=1072, shape=(2, 1), dtype=int32, numpy=
array([[2],
       [6]], dtype=int32)>, <tf.Tensor: id=1073, shape=(2, 2), dtype=int32, numpy=
array([[3, 4],
       [7, 8]], dtype=int32)>]
tf.Tensor(
[[[1 2 3 4]]

 [[5 6 7 8]]], shape=(2, 1, 4), dtype=int32)

从上面的演示可以看出,concat将两个1x4的张量拼接成了一个2x4的张量,而split则将一个2 x4的张量拆分成两个2x1的张量和一个2x2的张量的列表

(2) stack和unstack

拼接多个张量有时也并不需要concat和split那种灵活的操作结构,而stack和unstack就提供了一对更加简单易用的接口。

d = tf.stack((a,b))
print(d)
print(tf.unstack(d))
tf.Tensor(
[[[1 2 3 4]]

 [[5 6 7 8]]], shape=(2, 1, 4), dtype=int32)
[<tf.Tensor: id=1077, shape=(1, 4), dtype=int32, numpy=array([[1, 2, 3, 4]], dtype=int32)>, <tf.Tensor: id=1078, shape=(1, 4), dtype=int32, numpy=array([[5, 6, 7, 8]], dtype=int32)>]

从以上演示可以看出stack先将待拼接的两个张量在axis=0上新扩展一维,然后在拼接到一起。而unstack是在axis=0上进行逐个拆分,然后把axis=0上shape=1的维度收缩掉。

当然也有其他方式进行手动拼接,不过有这四个操作函数基本够用。总体来说想要实现一个稍微复杂一点的图基本上拼接和拆分是必不可少的。本次关于拼接和拆分就讨论到这里。

6. 数学运算

使用tensorflow的目的是完成一些计算任务,那数学计算操作就是必不可少的。而且由于计算操作使用频率极高,因此接口设计也应当简洁。如果使用过2.0之前版本的tensorflow估计会对这个问题有相当多的怨言。不过既然有了2.0版本,那就不用再像之前那么痛苦了。除了简洁,数学运算操作还需要满足基本的数学运算习惯。基本提供矩阵或者张量运算的工具都是遵守线性代数的基本约定的。

(1) 加减乘除

加减乘除是最基础的数学运算,但是这个基础的数学运算却并不简单,特别是除法。我们知道除法有一个最基本的约束,那就是除数不为0。这个错误可能大家都是有意识的,但是对于浮点数和整数在除法中的不同表现却是不容易引起注意的。常用的除法是浮点数相除,这个大家都能接受。在计算机中还有一种叫做整数除法,其实就是数学中的“地板除”。从以下的演示中可以看出区别。

a = tf.constant([[1,2],[3,4]], dtype=tf.float32)
b = tf.constant([[5,6],[7,8]], dtype=tf.float32)
print('a+b=', a+b)  # 元素加
print('a-b=', a-b)  # 元素减
print('a*b=', a*b)  # 元素乘
print('a/b=', a/b)  # 元素除
print('a//b', a//b) # 元素地板除
print('b%a=', b%a)  # 元素求余
a+b= tf.Tensor(
[[ 6.  8.]
 [10. 12.]], shape=(2, 2), dtype=float32)
a-b= tf.Tensor(
[[-4. -4.]
 [-4. -4.]], shape=(2, 2), dtype=float32)
a*b= tf.Tensor(
[[ 5. 12.]
 [21. 32.]], shape=(2, 2), dtype=float32)
a/b= tf.Tensor(
[[0.2        0.33333334]
 [0.42857143 0.5       ]], shape=(2, 2), dtype=float32)
a//b tf.Tensor(
[[0. 0.]
 [0. 0.]], shape=(2, 2), dtype=float32)
b%a= tf.Tensor(
[[0. 0.]
 [1. 0.]], shape=(2, 2), dtype=float32)

直接利用运算符对张量进行数值运算都是元素级的运算,即每个元素对应进行运算。如果形状不满足运算规则,则试图利用广播机制进行自动扩展。如果无法扩展则报错。

(2) 幂运算

幂运算使用频率也是比较高,特别是平方和开平方两种幂运算。tensorflow也提供了更加通用的幂运算函数pow,不过一般使用频率并不高。

a = tf.constant([[1,2],[3,4]], dtype=tf.float32)
print(a**2)
print(tf.pow(a, 2.0))
print(tf.square(a))
print(tf.sqrt(a))
tf.Tensor(
[[ 1.  4.]
 [ 9. 16.]], shape=(2, 2), dtype=float32)
tf.Tensor(
[[ 1.  4.]
 [ 9. 16.]], shape=(2, 2), dtype=float32)
tf.Tensor(
[[ 1.  4.]
 [ 9. 16.]], shape=(2, 2), dtype=float32)
tf.Tensor(
[[1.        1.4142135]
 [1.7320508 2.       ]], shape=(2, 2), dtype=float32)

对张量进行幂运算都是元素级的,即对每个元素进行指定的幂运算。从开发角度来说。tf.square和tf.sqrt使用频率比较难高一些。

(3) 指数和对数

指数和对数操作使用的频率也是相当高的。不仅因为很多的公式都跟指数和对数有关,还有一个原因是指数的输出和对数的输入都是有范围的,这对于一些需要限制范围的情况而言是很有用的。

a = tf.constant([[1,2],[3,4]], dtype=tf.float32)
print(tf.exp(a))
print(tf.math.log(a))
tf.Tensor(
[[ 2.7182817  7.389056 ]
 [20.085537  54.59815  ]], shape=(2, 2), dtype=float32)
tf.Tensor(
[[0.        0.6931472]
 [1.0986123 1.3862944]], shape=(2, 2), dtype=float32)

对张量进行指数和对数操作也是元素级的,其中对数操作的接口和指数操作的接口位置有所不同,需要稍微注意一下。

(4) 矩阵乘

矩阵乘法是线性代数所定义的一种乘法,对于实现一些复杂系统而言是必不可少的。比如最常用的全连接层就是通过矩阵乘法来实现的。

a = tf.constant([[1,2],[3,4]], dtype=tf.float32)
b = tf.constant([[5,6],[7,8]], dtype=tf.float32)
print(tf.matmul(a, b))
print(a@b)
tf.Tensor(
[[19. 22.]
 [43. 50.]], shape=(2, 2), dtype=float32)
tf.Tensor(
[[19. 22.]
 [43. 50.]], shape=(2, 2), dtype=float32)

tf.matmul是按照矩阵乘法的规则运行的,如果维度大于3,则以最后两个维度进行矩阵操作,前面的形状保持不变。这里还提供了一种简化的写法,即使用”@”。

到此基本的数学运算讨论完毕,初学者看到这里应该就已经可以利用tensorflow搭建起一个普通的前向数学运算模型了。即使对于一些不研究深度学习的朋友而言,tensorflow也是一个很好的数学计算的工具,特别是还带有自动多核调用和GPU加速的功能。

7. 统计操作

现在深度学习训练一般都是使用一个batch的数据作为输入的,这样在计算损失函数的时候就需要使用统计的方法来进行一些汇聚形成最终的损失数值,这样才能用于误差反传。损失函数常常是针对高维数据来进行描述的,通常使用L1范数、L2范数、交叉熵或者简单损失函数的组合等等。因此损失函数本身也是在进行一些统计的计算。在计算图运行的过程中,还需要对一些中间结果进行监控,而这些中间结果大多都是高维张量,监控所有数值是不利于分析也没有必要的,因此也需要对中间结果进行一些统计操作。

(1) 范数

关于范数的定义是各种各样,不过在日常任务中常用基本范数的就是两种:L1范数和L2范数。L1是将输入的所有元素去绝对值然后求和,L2是将所有元素的平方计算均值然后开平方。在张量中计算方式稍微复杂一点,需要分清是指定维度上进行操作还是对所有元素进行操作。

a = tf.constant([[1,2],[3,4]], dtype=tf.float32)
print(tf.norm(a))                 # 对所有元素计算2范数
print(tf.norm(a, axis=1))         # 对axis=1计算2范数
print(tf.norm(a, ord=1))          # 对所有元素计算1范数
print(tf.norm(a, ord=1, axis=1))  # 对axis=1计算1范数
tf.Tensor(5.477226, shape=(), dtype=float32)
tf.Tensor([2.236068 5.      ], shape=(2,), dtype=float32)
tf.Tensor(10.0, shape=(), dtype=float32)
tf.Tensor([3. 7.], shape=(2,), dtype=float32)

(2) 汇聚

reduce操作是一种汇聚归纳的操作,对于张量这种比较庞大的数据结构而言是必须要的。在海量数据处理中,我们常常关心的并不是每一个数据数值的大小,而只关心其统计指标。对于机器学习而言更是如此,因为只有单一指标的系统是可以方便地使用误差反传来进行参数调整的,如果是多目标系统也需要映射成单目标。因此,关于汇聚的操作是构建一个可分析系统的关键。

a = tf.constant([[1,2],[3,4]], dtype=tf.float32)
print(tf.reduce_max(a, axis=1))  # 在指定维度上计算最大值
print(tf.reduce_min(a, axis=0))  # 在指定维度上计算最小值
print(tf.reduce_mean(a))         # 在指定维度上计算均值,默认全维度
print(tf.argmax(a, axis=0))      # 在指定维度上计算最大值索引
print(tf.argmin(a, axis=1))      # 在指定维度上计算最小值索引

# unique只能作用于一维
b = tf.constant([1,1,1,4], dtype=tf.float32)
print(tf.unique(b))              # 获取不重复数值和其索引
tf.Tensor([2. 4.], shape=(2,), dtype=float32)
tf.Tensor([1. 2.], shape=(2,), dtype=float32)
tf.Tensor(2.5, shape=(), dtype=float32)
tf.Tensor([1 1], shape=(2,), dtype=int64)
tf.Tensor([0 0], shape=(2,), dtype=int64)
Unique(y=<tf.Tensor: id=1277, shape=(2,), dtype=float32, numpy=array([1., 4.], dtype=float32)>, idx=<tf.Tensor: id=1278, shape=(4,), dtype=int32, numpy=array([0, 0, 0, 1], dtype=int32)>)

(3) 条件蒙版

在分析数据时,我们常常只对满足某种条件的数据感兴趣,对于其他数据完全可以忽略。这是条件蒙版操作就可以帮助我们达到目的。而tensorflow2.0提供了很简洁的条件蒙版生成接口。而不用像之前的版本中那样费尽心机调用各种丑陋的接口进行组合。

a = tf.constant([[1,2],[3,4]], dtype=tf.float32)
b = tf.constant([[5,6],[7,8]], dtype=tf.float32)
print(tf.equal(a, b))
print(a**2>b)
tf.Tensor(
[[False False]
 [False False]], shape=(2, 2), dtype=bool)
tf.Tensor(
[[False False]
 [ True  True]], shape=(2, 2), dtype=bool)

(4) 排序

只关心在某种指标下排序比较靠前或者比较靠后的数据应该是屡见不鲜的。tensorflow提供了一系列这种排序类型的函数。除了普通的升序和降序排列,还有取top的若干个数据,或者不关心数据的具体内容,而对数据的范围进行控制,等等这些操作为我们灵活分析需要排序的数据提供了便利。

# 生成一个张量,然后随机排序一下
a = tf.random.shuffle(tf.range(8))
print('a = ', a)
# 降序排序
print('降序排序:', tf.sort(a, direction='DESCENDING'))
# 获取降序排序的索引
print('降序排序在原张量中的索引:', tf.argsort(a, direction='DESCENDING'))
# 利用降序排序的所有生成降序结果
print('tf.gather = ', tf.gather(a, tf.argsort(a, direction='DESCENDING')))
# 获取前若干个数值,返回前几个数值和索引
print(tf.math.top_k(a, 3))
# 设置所有值不小于4
print(tf.maximum(a, 4))
# 设置所有值不大于4
print(tf.minimum(a, 4))
# 设置所有值在一个区间内
print(tf.clip_by_value(a, 2, 6))
a =  tf.Tensor([6 5 0 4 7 2 1 3], shape=(8,), dtype=int32)
降序排序: tf.Tensor([7 6 5 4 3 2 1 0], shape=(8,), dtype=int32)
降序排序在原张量中的索引: tf.Tensor([4 0 1 3 7 5 6 2], shape=(8,), dtype=int32)
tf.gather =  tf.Tensor([7 6 5 4 3 2 1 0], shape=(8,), dtype=int32)
TopKV2(values=<tf.Tensor: id=1701, shape=(3,), dtype=int32, numpy=array([7, 6, 5], dtype=int32)>, indices=<tf.Tensor: id=1702, shape=(3,), dtype=int32, numpy=array([4, 0, 1], dtype=int32)>)
tf.Tensor([6 5 4 4 7 4 4 4], shape=(8,), dtype=int32)
tf.Tensor([4 4 0 4 4 2 1 3], shape=(8,), dtype=int32)
tf.Tensor([6 5 2 4 6 2 2 3], shape=(8,), dtype=int32)

到此关于统计操作就讨论完毕。掌握了统计相关的操作之后,对于计算图的搭建就更加得心应手,对于数据分析,特别是模型中间结果的分析更有把握一些。

8. 保存和加载参数

有很多原因使得我们需要将计算图的一些数据保存到文件中,比如分享模型或变量、硬件故障导致训练停机需要重启接着训练或者只是单纯查看模型参数等等。tensorflow既提供了灵活的接口,也提供了简单易用的接口。接下来我们就来了解一下tf2.0中有哪些接口可以将参数保存到文件和加载到内存。

(1) 保存和加载变量

最简单的一种是直接保存变量,这当然是最基础的方式,自然就可以满足一些灵活的任务。首先定义一个tf变量,然后将该变量传给Checkpoint,由Checkpoint对象负责将该变量存入文件中。

x = tf.Variable(1.0)
ckpt = tf.train.Checkpoint(x=x)
ckpt.save('../../output/tf-save-01/model.ckpt')
print('保存的x:', x)
!ls ../../output/tf-save-01
del x
del ckpt
保存的x: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=1.0>
checkpoint                       model.ckpt-1.index
model.ckpt-1.data-00000-of-00001
x = tf.Variable(0.0)
ckpt = tf.train.Checkpoint(x=x)
ckpt.restore(tf.train.latest_checkpoint('../../output/tf-save-01/'))
print('加载的x:', x)
加载的x: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=1.0>

从以上的演示中可以发现,Checkpoint对象将保存的变量值1.0恢复到了新建的变量中。这种操作虽然是灵活,但却很少用到,原因大家应该也能猜到,就是太麻烦了。通常在一个机器学习的模型中,参数的数量是极多的,要想这样逐个指定来进行保存实在是难以想象。因此这种方式只供了解,除非实现一些非常基础的功能,并且没有简便方式的时候才考虑采用这种方式。

(2) 保存和加载模型

直接保存一整个个模型的参数,这种情况是我们日常开发中使用得更频繁的方式。还是使用Checkpoint对象,只不过需要传入tf.keras.Model及其子类的对象。下面演示一个极其简单的例子只为说明问题,事实上还可以指定更多的参数来完整保存模型的训练过程中的参数。

class Model(tf.keras.Model):
    def __init__(self):
        super(Model, self).__init__()
        self.x = tf.Variable(0.0)
        
    def __call__(self, y):
        self.x.assign(y)
model = Model()
model(100.0)
print('保存的模型变量x:', model.x)
ckpt = tf.train.Checkpoint(net=model)
ckpt.save('../../output/tf-save-02/')
del model
保存的模型变量x: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=100.0>
model = Model()
ckpt = tf.train.Checkpoint(net=model)
print('加载前的模型变量x:', model.x )
ckpt.restore(tf.train.latest_checkpoint('../../output/tf-save-02/'))
print('加载的模型变量x:', model.x )
tf.train.list_variables(
    tf.train.latest_checkpoint('../../output/tf-save-02/'))
加载前的模型变量x: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>
加载的模型变量x: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=100.0>





[('_CHECKPOINTABLE_OBJECT_GRAPH', []),
 ('net/x/.ATTRIBUTES/VARIABLE_VALUE', []),
 ('save_counter/.ATTRIBUTES/VARIABLE_VALUE', [])]

(3) 列表和词典追踪

有这样一种情形:保存A变量,但是想把A变量恢复到另一个名字的B变量上。遇到这种需求该怎么处理呢?这个就需要用到tensorflow中Checkpoint提供的列表和词典的追踪功能,即用列表存储变量的数据,使用词典指定变量的名字,然后在载入时将这个名字的数据载入到另一个新建的变量中。说起来总是比较绕,还是代码演示更清晰一些。

ckpt = tf.train.Checkpoint()
ckpt.listed = [tf.Variable(1.0), tf.Variable(2.0)]
ckpt.mapped = {'x1':ckpt.listed[0], 'x2':ckpt.listed[1]}
ckpt.save('../../output/tf-save-03/')
tf.train.list_variables(
    tf.train.latest_checkpoint('../../output/tf-save-03/'))
[('_CHECKPOINTABLE_OBJECT_GRAPH', []),
 ('listed/0/.ATTRIBUTES/VARIABLE_VALUE', []),
 ('listed/1/.ATTRIBUTES/VARIABLE_VALUE', []),
 ('save_counter/.ATTRIBUTES/VARIABLE_VALUE', [])]
ckpt = tf.train.Checkpoint()
y1, y2 = tf.Variable(0.0), tf.Variable(0.0)
ckpt.mapped = {'x1':y1, 'x2':y2}
ckpt.restore(tf.train.latest_checkpoint('../../output/tf-save-03/'))
print('y1 =', y1)
print('y2 =', y2)

y1 = <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=1.0>
y2 = <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=2.0>

这种方式却是很具有灵活性,不过说实话,日常开发中用到的可能性也不大。所以,如果对tensorflow的这一套机制感兴趣的朋友可以仔细了解一下,如果只是想应用那就完全可以忽略这种操作。

(4) keras便捷接口

keras是tensorflow内嵌的一种高层语言,所以提供了一些简单易用的接口,在模型参数保存这方面的便捷性就更加突出。对于只保存模型变量参数的情况,可以直接使用tf.keras.Model对象的save_weights方法。

class Model(tf.keras.Model):
    def __init__(self):
        super(Model, self).__init__()
        self.x = tf.Variable(0.0)
        
    def __call__(self, y):
        self.x.assign(y)
model = Model()
model(100.0)
model.save_weights('../../output/tf-keras-04')
del model
model = Model()
print('加载之前的模型x:', model.x)
model.load_weights('../../output/tf-keras-04')
print('加载之后的模型x:', model.x)
加载之前的模型x: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=0.0>
加载之后的模型x: <tf.Variable 'Variable:0' shape=() dtype=float32, numpy=100.0>

save_weights和load_weights这一组参数虽然方便,但是有一个问题:每次载入之前需要先产生网络结构。在一般的开发中,这或许不是个问题,但是对于一些想要保护模型构建代码或者模型产品部署而言是不方便的。因此,keras模块还提供了一组函数,save函数将模型的所有参数,包括连接关系都存储到文件中, tf.keras.models.load_model函数可以直接根据模型文件恢复模型的状态。也就是说,在产品部署时,只需要将模型文件交给部署人员即可,完全不需要将训练模型的完整代码公开。不过,在使用save函数时有一些要求,即要求模型的输入形状是已知。这对于训练模型的人来说应该不是问题,毕竟是模型能够正常开始训练才有保存的必要。因此,只要是一个能正常训练的模型都能调用save函数,过程比较简单,这里就不演示了。

到此,关于tensorflow2.0中参数的保存和加载已经讨论完毕。根据个人的开发经验,我的建议是一般情况日常开发直接使用Checkpoint保存完整模型是比较方便的,也是很多人都采用的,交流比较方便;如果需要在不同的keras后端之间切换,可以采用save_weights保存模型变量参数;如果需要产品部署,再将其他形式的模型重新调用save函数即可。

xx. 自动微分

a = tf.Variable(2.0)
b = tf.Variable(3.0)
with tf.GradientTape() as t:
    c = 2*(a**2) + b
da, db = t.gradient(c, [a,b])
print(da)  # da=4a
print(db)  # db=1
tf.Tensor(8.0, shape=(), dtype=float32)
tf.Tensor(1.0, shape=(), dtype=float32)

【青衣极客】公众号



COMMENT

博客评论区功能由Github Issue提供,提交Issue时请以本文标题为话题

"BG18-tensorflow2.0基本操作"