阅读本文大概需要 16 分钟。
前言
在本文中将介绍与我的毕设论文演示案例相关的TensorFlow的一些基础知识,包括张量、计算图、操作、数据类型和维度以及模型的保存,接着在第二部分,本文将介绍演示案例代码中用到的一些TensorFlow 2.0中的高阶API,代码中不会涉及像TensorFlow 1.x版本中的Session等一些较为复杂的东西,所有的代码都是基于高阶API中的tf.keras.models来构建的(具体模型构建使用Sequential按层顺序构建),可以大大的方便读者更好的理解代码。
需要注意的一点,本论文中所实现的两个案例均在本机CPU上进行运算,对于更大数量级的数据训练建议采用添加GPU的方法或者托管在Google cloud、AWS云平台上进行数据的处理。
1.1 基础知识概述
1.1.1 张量
第一次看到TensorFlow这个名词,第一反应是去翻译一下这代表什么意思,通过查阅相关字典可以知道,Tensor被翻译为张量,Flow被翻译为流或者流动,组合起来TensorFlow可以被翻译为张量流。那什么是张量,什么又是流呢?
一般来将,把任意维度的数据称为张量,比如说一维数组(任意一门编程语言里都会学到一维数组的概念)、二维矩阵(我们在线性代数中学过关于矩阵的概念,这里不做赘述)以及N维数据。而流是指让数据在不同的计算设备上进行传输并计算(因为只有Tensor形式的数据可以实现在不同的设备之间进行传递)。
总结起来,我们可以认为TensorFlow的意思就是:让Tensor类型的数据在各个计算设备之间进行流动并完成计算。那为什么要让数据流动起来呢?Tensor类型又具体包括什么呢?接下来先来看一段演示代码:
代码语言:javascript复制# 将通过清华镜像下载的tensorflow包导入
import tensorflow as tf
a = tf.constant([[1.0,-2],[-3,4]])
print(a)
控制台输出结果如下:
代码语言:javascript复制tf.Tensor(
[[ 1. -2.]
[-3. 4.]], shape=(2, 2), dtype=float32)```
在上述代码中规定了一个2*2的矩阵,并将其打印在控制台。通过结果可以发现控制台输出的Tensor里面有三个参数:
- 第一个参数是一个2*2的矩阵,且矩阵中的元素全部为浮点类型。
- 第二个参数是shape,也就是输出矩阵的类型,很明显shape(2,2)表示输出矩阵为一个2*2的矩阵;举个例子,一个二阶张量a=[[1,1,1],[2,2,2]]的形状是两行三列,即shape=(2,3)。
- dtype=float32表示输出矩阵中元素的数据类型为浮点型(32为浮点数)
【注】:在上述对于代码部分的解释中提到一个名词二阶张量,接下来将通过表格的形式来区分一下标量、向量、矩阵的阶数的细微差异:
表1-1 标量向量和矩阵的阶数
rank(阶) | 实例 | 例子 |
---|---|---|
0 | 标量(只有大小) | a=1 |
1 | 向量(有大小和方向) | b=[1,1,1,1] |
2 | 矩阵(数据表) | c=[[1,1],[1,1]] |
3 | 3阶张量(数据立体) | d=[[[1],[1]],[[1],[1]]] |
n | n阶 | n层括号 |
简单解释一下,阶指的就是维度,它与矩阵的阶不同。
举个例子,对于a=[[1,1,1],[2,2,2],[3,3,3]]从矩阵的角度看,这是一个3*3的方阵,也就是说它的阶数为3,而从张量的角度看,它的阶数为2,即维度为2,因为它只有两层中括号。
1.1.2 计算图
首先来看看TensorFlow官网中的这幅图,一方面是帮助我们理解流的概念,另一方面是为我们理解图的概念做下铺垫。
【注】TensorFlow官网中的动图演示请参考如下网址:
http://www.shipudong.com/2020/03/24/bi-ye-she-ji-nei-rong-bu-chong/
图1.1 TensorFlow官网流图演示
将图一般分为两种,包括动态计算图和静态计算图。我以中铁某局修建地铁为例来讲解这两种图的区别:
修建一条地铁需要设计图纸和施工队:
第一种情况,当设计师在设计图纸的时候(包括隧道走向、站点设置等,具体细节不予赘述)施工队什么也不干,必须等到设计工作完成之后,施工队才开始工作,(我们可以把这种情况理解为计算机中的同步方式,把设计工作和施工操作看作两个任务,当前任务未完成之前,不能进行其他操作)也就是说设计工作和具体施工完全分开,这就是所谓的静态计算图,我们称能够支持静态计算图的为静态框架,主要包括TensorFlow、Theano等;
第二种情况,设计工作和施工操作一起进行,设计方要求开凿隧道,施工队立即完成任务,如此下去,一经设计方下达任务,施工队必须立即完成操作,如此良性循环直到项目完成(我们把这种情况理解为计算机中的异步方式),这就是所谓的动态计算图,我们称能够支持动态计算图的为动态框架,主要包括Torch等。
在了解了动态计算图和静态计算图的例子之后,我们很明显的可以看出两种图的差异:静态计算图在未执行之前就必须定义好执行顺序和内存分配,简单来说,在程序未执行之前就知道了所有操作,有助于较快地执行计算操作;相比动态计算图,每次的执行顺序规划和内存分配都是局部的,并非全局最优,虽然灵活性较静态计算图有很大提升,但是代价太高,所以在现在流行的框架中,还是以静态框架为主,比如本论文中的由谷歌公司开源的TensorFlow。
1.1.3 操作
从图1.1可以观察到,数据一经输入(Input),会被进行不同的操作,首先会将数据进行预处理(比如图中的reshape操作),接着给处理好的数据中加入非线性操作(ReLU操作)等,使数据更符合自然界中的普遍关系,然后我们根据输入数据的类型进而采取比较合适的交叉熵函数(Crossentropy),用来衡量真实值与预测值的偏差,最后我们我们将根据项目真实情况选取合适的优化器(图中选用的为sgd,即随机梯度下降法)。图中的一个节点就代表一个操作,我们从计算图中了解到,TensorFlow属于静态计算图,也就是说在未执行前就已经定义好了执行的顺序,简单来讲,图中的各个操作之间是存在执行顺序的,而这些操作之间的依赖就是图中的边。我们以一个非常简单的图示来讲解这个关系,首先来看一段代码:
代码语言:javascript复制# 定义变量a
a = tf.Variable(1.0,name="a")
# 定义操作b为a 1
b = tf.add(a,1,name="b")
# 定义操作c为b 1
c = tf.add(b,1,name="c")
# 定义操作d为b 10
d = tf.add(b,10,name="d")
上述代码认为a、b、c和d均为需要进行的操作,下图中的x表示一个常数,值为1。
图1.2 操作之间的依赖关系
首先定义a=1.0,b=a 1,即b=2.0,以此类推,c=3.0,d=11.0,可以这样理解,操作b的进行需要依赖操作a,操作c的进行需要依赖操作b的完成,操作d的进行需要依赖操作b,且操作c和d之间没有依赖关系。
1.1.4 数据类型和维度
对于任意一门编程语言都会有数据类型,区别就在于每一门编程语言定义不同数据类型的方式不一样,在本章开始的时候了解过,在TensorFlow中,用张量(Tensor)来表示数据结构,接下来我们就将TensorFlow中的的数据类型与Python中的数据类型作以简单的对比,并通过表格的形式清晰的展现出来:
表1-2 TensorFlow和Python中数据类型的对应关系
TensorFlow数据类型 | Python中的表示 | 说明 |
---|---|---|
DT_FLOAT | tf.float32 | 32位浮点数 |
DT_DOUBLE | tf.float64 | 64位浮点数 |
DT_INT8 | tf.int8 | 8位有符号整数 |
DT_INT16 | tf.int16 | 16位有符号整数 |
DT_INT32 | tf.int32 | 32位有符号整数 |
DT_INT64 | tf.int64 | 64位有符号整数 |
DT_UINT8 | tf.uint8 | 8位无符号整数 |
DT_UINT16 | tf.uint16 | 16位无符号整数 |
DT_STRING | tf.string | byte类型数组 |
DT_BOOL | tf.bool | 布尔型 |
DT_COMPLEX64 | tf.complex64 | 复数类型,由32位浮点数的实部和虚部组成 |
DT_COMPLEX128 | tf.complex128 | 复数类型,由64位浮点数的实部和虚部组成 |
DT_QINT8 | tf.qint8 | 量化操作的8位有符号整数 |
DT_QINT32 | tf.quint32 | 量化操作的32位有符号整数 |
DT_QUINT8 | tf.quint8 | 量化操作的8位无符号整数 |
维度的相关概念,在上述文章中的张量部分已经详细讲过,此处不再赘述。一般来说张量的阶数(维度)就是看有几层中括号,接下来看一段代码:
代码语言:javascript复制import tensorflow as tf
value_shape_0 = tf.Variable(1002)
value_shape_1 = tf.Variable([1,2,3])
value_shape_2 = tf.Variable([[1,2,3],[3,4,5],[5,6,7]])
print(value_shape_0.get_shape())
print(value_shape_1.get_shape())
print(value_shape_2.get_shape())
控制台输出结果如下:
代码语言:javascript复制()
(3,)
(3, 3)
(2, 3, 2)
代码解释:
- value_shape_0:定义了一个标量(只有大小),其维度为0;
- value_shape_1:定义了一个一维向量(有大小和方向),其维度为3;
- value_shape_2:定义了一个二维的矩阵,矩阵大小为3*2;
- value_shape_3:定义了一个三维张量,第一维的维度是2,第二维的维度是3,第三维的维度是2,可以简单理解为:这是一个大小为2*3且深度为2的矩阵。
1.1.5 模型保存
当我们完成一个案例之后,我们想要把当前训练好的模型保存下来(保存模型是指把训练的参数保存下来),方便我们之后重新使用。当我们重新使用的时候,我们只需要重新载入模型即可。
首先我们来看一下保存模型的代码:
代码语言:javascript复制# 保存模型
model.save("my_model.h5")
在关于MNIST手写字的例子中将我们训练好的模型保存下来,并命名为my_model.h5,接下来我们看一段载入模型的代码:
代码语言:javascript复制# 加载模型文件
model = tf.keras.models.load_model("my_model.h5")
同样是在MNIST手写字的例子中,我们将保存好的模型导入,并通过matplotlib函数画出模型图,具体模型图我会在本毕设系列推文的案例讲解部分中进行展示。
2. 相关API介绍
一般来讲,TensorFlow共有5个不同的层次结构,从低到高分别是硬件层、内核层、低阶API、中阶API、高阶API,我们对每一层作以简单的介绍:
- 硬件层:我们知道TensorFlow可以支持CPU、GPU、TPU(受限于硬件条件,我们本文中的项目是在本机CPU上运行的)加入计算资源池,作为一种计算设备参与运算;
- 内核层:该层是由C 语言实现的内核,可以支持跨平台的分布运行;
- 低阶API:该层主要提供了由Python实现的一些操作符,并对由内核层实现的一些低阶API进行封装,包括各种Tensor(张量)操作算子、计算图、自动微分等;
- 中阶API:该层是由Python实现的模型组件,并对低阶API进行了函数封装,主要包括各种模型层(tf.keras.layers)、损失函数(tf.keras.losses)、优化器(tf.keras.optimizers)、数据管道(tf.data.Dataset)等;
- 高阶API:该层为由Python实现的模型成品,主要为tf.keras.models提供的模型的类接口,在第四章中实现MNIST手写字识别的例子我们主要使用它。
图2.1 API详解
上述内容是我们对TensorFlow中的API做了宏观的描述,接下来我将着重介绍5个代码案例中较为重要的API:
- tf.keras.models.Sequential:我们可以通过Sequential按层顺序来构建模型,也可以通过add方法一层一层添加模型(不建议使用),以下为代码演示:
model = tf.keras.models.Sequential([
# 里面是添加的模型层,比如说卷积层、池化层等
])
- tf.keras.layers:我们可以通过此API添加我们需要的不同的模型层(卷积层、池化层等),通过查阅TensorFlow官网关于此API的介绍可以知道,读者可以通过此API添加如下模型层:
图2.2 TensorFlow官网tf.keras.layers部分API
以下是代码演示:
代码语言:javascript复制tf.keras.layers.Conv2D(input_shape = (28,28,1),filters = 32,kernel_size = 5,strides = 1,padding = "same",activation = "relu"), # 28*28
- tf.keras.datasets:用如下代码来加载MNIST收据集:
mnist = tf.keras.datasets.mnist
(x_train, y_train),(x_test, y_test) = mnist.load_data()
- model.compile:可以通过此API来编译经Sequential构建好的模型,同时也可以定义优化器、损失函数、如何对网络参数进行优化以及在训练过程中是否要计算准确率等,我们来看看官网中对此API的解释:
图2.3 compile函数官网介绍
具体代码如下:
代码语言:javascript复制tf.keras.layers.Conv2D(input_shape = (28,28,1),filters = 32,kernel_size = 5,strides = 1,padding = "same",activation = "relu"), # 28*28# 编译模型 优化器--adam (sgd--随机梯度下降法)损失函数---均方误差 metrics---训练过程中计算准确率accuracy
model.compile(optimizer='adam', loss='sparse_categorical_crossentropy',metrics=['accuracy'])
- model.fit:通过此API来训练模型,同时可以定义训练的迭代周期以及每次训练获取样本集的数量(一般默认batch_size=32),我们来看看官网对此API的解释:
图2.4 fit函数官网介绍
具体代码如下:
代码语言:javascript复制# 训练模型 epochs --- 迭代周期 batch_size默认为32
model.fit(x_train, y_train, batch_size=32,epochs=5)