TensorFlow 基本概念及构建
总体介绍
目前深度学习异常的火热,而深度学习模型的搭建需要依赖于深度学习框架,TensorFlow 就是其中的一种非常流行的深度学习框架。因此,想要学习深度学习算法,学习 TensorFlow 十分必要。而本次主要介绍 TensorFlow 的基本概念以及基本使用方法。
知识点
- TensorFlow
- 张量 Tensor
- 计算图 Graph
- 线性回归实现
- 模型保存 Save
TensorFlow 介绍
TensorFlow 是目前最强大的深度学习框架之一,由 Google 团队主导开发,并在 2015 年进行开源。因此,TensorFlow 拥有非常活跃的社区。这意味着当你在使用 TensorFlow 遇到问题时,往往在许多搜索引擎中搜索相关的报错信息就能找到答案。
经过几年的不断优化和发展,TensorFlow 目前的代码量大约在 40 万行左右。因此,本系列不可能涵盖 TensorFlow 所有的内容,仅介绍其常用的操作或 API,具体如下:
简介 - 介绍了如何使用高阶 API 之外的低阶 TensorFlow API 的基础知识。
张量 - 介绍了如何创建、操作和访问张量(TensorFlow 中的基本对象)。
变量 - 详细介绍了如何在程序中表示共享持久状态。
图和会话
- 介绍了以下内容:
- 数据流图:这是 TensorFlow 将计算表示为操作之间的依赖关系的一种表示法。
- 会话:TensorFlow 跨一个或多个本地或远程设备运行数据流图的机制。如果您使用低阶 TensorFlow API 编程,请务必阅读并理解本单元的内容。如果你使用高阶 TensorFlow API(例如 Estimator 或 Keras)编程,则高阶 API 会为你创建和管理图和会话,但是理解图和会话依然对你有所帮助。
- 保存和恢复 - 介绍了如何保存和恢复变量及模型。
虽然使用 TensorFlow 的高阶 API 来搭建模型会更简单。但是由于其是低阶 API 的高层封装,所以往往更难调试。所以本次实验主要讲解低阶 API。
数据流图
与其他科学计算库不一样,在 TensorFlow 中,每个运算操作都可以看做是一个计算图,如下图所示。
TensorFlow 使用数据流图将计算表示为独立的指令之间的依赖关系。这可生成低级别的编程模型,在该模型中,首先定义数据流图,然后创建 TensorFlow 会话,以便在一组本地和远程设备上运行所构建计算图的各个部分。
数据流 是一种用于并行计算的常用编程模型。在数据流图中,节点表示计算单元,边表示计算使用或产生的数据。例如,在 TensorFlow 图中,tf.matmul
操作对应于单个节点,该节点具有两个传入边(要相乘的矩阵)和一个传出边(乘法结果)。
在执行程序时,数据流可以为 TensorFlow 提供多项优势:
- 并行处理。 通过使用明确的边来表示操作之间的依赖关系,系统可以轻松识别能够并行执行的操作。
- 分布式执行。 通过使用明确的边来表示操作之间流动的值,TensorFlow 可以将程序划分到连接至不同机器的多台设备上(CPU、GPU 和 TPU)。TensorFlow 将在这些设备之间进行必要的通信和协调。
- 编译。 TensorFlow 的 XLA 编译器 可以使用数据流图中的信息生成更快的代码,例如将相邻的操作融合到一起。
- 可移植性。 数据流图是一种不依赖于语言的模型代码表示法。你可以使用 Python 构建数据流图,将其存储在 SavedModel 中,并使用 C++ 程序进行恢复,从而实现低延迟的推理。
在 TensorFlow 中,数据流图是一个 tf.Graph
对象,tf.Graph
包含两类相关信息:
- 图结构: 图的节点和边,表示各个操作组合在一起的方式,但不规定它们的使用方式。图结构与汇编代码类似:检查图结构可以传达一些有用的信息,但它不包含源代码传达的所有实用上下文信息。
- 图集合: TensorFlow 提供了一种在
tf.Graph
中存储元数据集合的通用机制。tf.add_to_collection
函数允许将对象列表与一个键关联(其中tf.GraphKeys
定义了部分标准键),tf.get_collection
允许查询与某个键关联的所有对象。TensorFlow 库的许多部分会使用此设施资源:例如,当创建tf.Variable
时,系统会默认将其添加到表示 “全局变量” 和 “可训练变量” 的集合中。当后续创建tf.train.Saver
或tf.train.Optimizer
时,这些集合中的变量将用作默认参数。
构建 tf.Graph
大多数 TensorFlow 程序都以数据流图构建阶段开始。在 TensorFlow 中,我们可以使用 tf.Graph
来创建一个图。例如下面代码:
1 | import tensorflow as tf |
在上面的代码中,我们创建了一个图 g_1
,并在该图中添加两个节点 a
和 b
。然后对两者进行相加得到 c
。如果使用 TensorBoard 可以将上面所构建的图打印出来,如下图所示.因 TensorBoard 在没有展示代码,如果你感兴趣可以在现在运行 官方文档 提供的案例。
现在我们打印出这三个节点。
1 | print(a) |
从上面的输出结果可以看到,我们的输出结果为三个形如 Tensor("a:0", shape=(), dtype=float32)
的 Tensor 对象,其中 "a:0"
表示节点名称,shape=()
表示节点的形状,dtype=float32
为节点的数据类型。
一般情况下 TensorFlow 提供了一个默认图,而且大多数程序仅依赖于默认图。所以如果你的代码中只创建一个运算图,则不需要自己手动创建。
命名空间
tf.Graph
对象会定义一个命名空间(为其包含的 tf.Operation
对象)。TensorFlow 会自动为数据流图中的每个指令选择一个唯一名称,也可以指定描述性名称,使程序阅读和调试起来更轻松。TensorFlow API 提供两种方法来覆盖操作名称:
- 如果 API 函数会创建新的
tf.Operation
或返回新的tf.Tensor
,则会接受可选name
参数。例如,tf.constant(42.0, name="answer")
会创建一个新的tf.Operation
(名为"answer"
)并返回一个tf.Tensor
(名为"answer:0"
)。如果默认图已包含名为"answer"
的操作,则 TensorFlow 会在名称上附加"_1"
、"_2"
等字符,以便让名称具有唯一性。 - 借助
tf.name_scope
函数,可以向在特定上下文中创建的所有操作添加名称作用域前缀。当前名称作用域前缀是一个用"/"
分隔的名称列表,其中包含所有活跃tf.name_scope
上下文管理器的名称。如果某个名称作用域已在当前上下文中被占用,TensorFlow 将在该作用域上附加"_1"
、"_2"
等字符。例如:
1 | e_0 = tf.constant(0, name="e") |
会话
我们现在再来看上面所述的加法运算例子。
1 | a = tf.constant(3.0, name='a') # 创建一个常量 a |
上面我们说到 a,b,c 只是我们在数据流图中所构建的节点而已,所以我们直接对其进行打印,并不能直接打印出其值。在 TensorFlow 中,需要创建会话才能进行运算,并打印出结果。在TensorFlow 中,创建会话的语句为 tf.Session
,使用会话执行数据流图的计算为 tf.Session.run
。下面我们创建一个会话。
1 | sess = tf.Session() |
由上面的结果可知,输出的结果与我们预想的一致。由于 tf.Session
拥有物理资源(例如 GPU 和网络连接),因此通常(在 with
代码块中)用作上下文管理器,并在你退出代码块时自动关闭会话。当然,你也可以在不使用 with
代码块的情况下创建会话,但应在完成会话时明确调用 tf.Session.close
以便释放资源。
1 | with tf.Session() as sess: |
在实际使用中,tf.Session.run
也可以选择接受 feed 字典,该字典是从 tf.Tensor
对象(通常是 tf.placeholder
张量)到在执行时会替换这些张量的值,通常是 Python 标量、列表或 NumPy 数组的映射。例如:
1 | x = tf.placeholder(tf.int32, shape=[3]) # 创建一个占位符 |
这里需要注意的是使用 x = tf.placeholder(tf.int32, shape=[ 3])
创建占位符表示的是在创建计算图时,x 没有被赋予实际的值,而在 tf.Session.close
运行计算图时需要对其传入数据,传入数据的方法采用上面所述的字典形式。
张量 Tensor
在 TensorFlow 中,其基本的数据结构是张量(Tensor),其是对矢量和矩阵向潜在的更高维度的泛化。TensorFlow 在内部将张量表示为基本数据类型的 n 维数组,即我们通常所说的多维数组。
在 TensorFlow 中,张量被操作和传递的主要对象是 tf.Tensor
,其具有以下属性:
- 数据类型(dtype):指的是张量的数据类型,例如
float32
、int32
或string
等; - 形状(shape):指的是张量的维度以及每个维度的大小,例如三维的张量:(4,2,6)。
- 名字(name):指的是张量在计算图中的命名。
在 TensorFlow 中常用的主要有四种类型的张量,分别如下:
tf.Variable
变量,其值可以在训练中被改变tf.constant
常量,其值可以在训练中不可改变tf.placeholder
占位符常量,在运行会话时,其值需要给定tf.SparseTensor
常量,稀疏张量
上面所列的四种类型的张量中,只有 tf.Variable 是可变张量,其他张量均不可改变。
张量的秩
tf.Tensor
对象的阶是它本身的维数。阶的同义词包括:秩、等级或 n 维。请注意,TensorFlow 中的阶与数学中矩阵的阶并不是同一个概念。如下表所示,TensorFlow 中的每个阶都对应一个不同的数学实例:
阶 | 数学实例 |
---|---|
0 | 标量(只有大小) |
1 | 矢量(大小和方向) |
2 | 矩阵(数据表) |
3 | 3 阶张量(数据立体) |
n | n 阶张量(自行想象) |
以下演示了创建 0 阶变量的过程:
1 | mammal = tf.Variable("Elephant", tf.string) |
输出为:
1 | <tf.Variable 'Variable:0' shape=() dtype=string_ref> |
要创建 1 阶 tf.Tensor
对象,可以传递一个项目列表作为初始值。例如:
1 | cool_numbers = tf.Variable([3.14159, 2.71828], tf.float32) |
输出为:
1 | <tf.Variable 'Variable_1:0' shape=(2,) dtype=float32_ref> |
2 阶 tf.Tensor
对象至少包含一行和一列:
1 | squarish_squares = tf.Variable([[4, 9], [16, 25]], tf.int32) |
输出为:
1 | <tf.Variable 'Variable_2:0' shape=(2, 2) dtype=int32_ref> |
同样,更高阶的张量由一个 n 维数组组成。例如,在图像处理过程中,会使用许多 4 阶张量,维度对应批次大小、图像宽度、图像高度和颜色通道。
1 | my_image = tf.zeros([10, 299, 299, 3]) |
输出为:
1 | <tf.Tensor 'zeros:0' shape=(10, 299, 299, 3) dtype=float32> |
要确定 tf.Tensor
对象的阶,需调用 tf.rank
方法。例如:
1 | r = tf.rank(my_image) |
输出为:
1 | <tf.Tensor 'Rank:0' shape=() dtype=int32> |
同样使用会话运行 r 才能得到其值。
1 | with tf.Session() as sess: |
张量的切片
由于 tf.Tensor
是 n 维单元数组,因此要访问 tf.Tensor
中的某一单元或元素,需要指定 n 个索引,这与 NumPy 是一致的。对于 2 阶 tf.Tensor
,传递两个数字会如预期般返回一个标量:
1 | my_matrix = tf.constant([[4, 9], [16, 25]], tf.int32) |
从上面运行的结果可以知道,TensorFlow 中张量的切片与 NumPy 中数组的类似。只不过在 TensorFlow 中,任何运算都会看成是一个计算图,所以需要建立会话运行计算图,从而得到计算结果。
张量的形状
张量的形状是每个维度中元素的数量。TensorFlow 在图的构建过程中自动推理形状。这些推理的形状可能具有已知或未知的阶。如果阶已知,则每个维度的大小可能已知或未知。
TensorFlow 文件编制中通过三种符号约定来描述张量维度:阶,形状和维数。下表阐述了三者如何相互关联:
阶 | 形状 | 维数 | 示例 |
---|---|---|---|
0 | [] | 0-D | 0 维张量。标量。 |
1 | [D0] | 1-D | 形状为 [5] 的 1 维张量。 |
2 | [D0, D1] | 2-D | 形状为 [3, 4] 的 2 维张量。 |
3 | [D0, D1, D2] | 3-D | 形状为 [1, 4, 3] 的 3 维张量。 |
n | [D0, D1, … Dn-1] | n 维 | 形状为 [D0, D1, … Dn-1] 的张量。 |
可以通过两种方法获取 tf.Tensor
的形状。在构建图的时候,询问有关张量形状的已知信息通常很有帮助。可以通过查看 shape
属性(属于 tf.Tensor
对象)获取这些信息。该方法会返回一个 TensorShape
对象,这样可以方便地表示部分指定的形状,因为在构建图的时候,并不是所有形状都完全已知。
也可以获取一个将在运行时表示另一个 tf.Tensor
的完全指定形状的 tf.Tensor
。为此,可以调用 tf.shape
操作。如此一来,可以构建一个图,通过构建其他取决于输入 tf.Tensor
的动态形状的张量来控制张量的形状。例如,以下代码展示了如何创建大小与给定矩阵中的列数相同的零矢量:
1 | my_matrix = tf.constant([[4, 9], [16, 25]], tf.int32) # 创建一个常量 |
张量的元素数量是其所有形状大小的乘积。由于通常有许多不同的形状具有相同数量的元素,因此如果能够改变 tf.Tensor
的形状并使其元素固定不变通常会很方便。为此,可以使用 tf.reshape
。以下示例演示如何重构张量:
1 | rank_three_tensor = tf.ones([3, 4, 5]) # 创建一个全为 1 的矩阵常量 |
张量的数据类型
除维度外,张量还具有数据类型。如需数据类型的完整列表,请参阅 tf.DType
页面。一个 tf.Tensor
只能有一种数据类型。但是,可以将 tf.Tensor
从一种数据类型转型为另一种,这需要通过 tf.cast
函数来执行,例如下面例子:
1 | list0 = tf.constant([1, 2, 3], dtype=tf.int32) |
要检查 tf.Tensor
的数据类型,可以使用 Tensor.dtype
属性。用 Python 对象创建 tf.Tensor
时,可以选择指定数据类型。如果不指定数据类型,TensorFlow 会自动选择一个合适的数据类型。TensorFlow 会将 Python 整数转型为 tf.int32
,并将 Python 浮点数转型为 tf.float32
。此外,TensorFlow 使用 Numpy 在转换至数组时使用的相同规则。
数据集
占位符适用于简单的实验,而 数据集 是将数据流传输到模型的首选方法。要从数据集中获取可运行的 tf.Tensor
,必须先将其转换成 tf.data.Iterator
,然后调用迭代器的 get_next
方法。创建迭代器的最简单的方式是采用 make_one_shot_iterator
方法。例如,在下面的代码中,next_item
张量将在每次 run
调用时从 my_data
阵列返回一行:
1 | my_data = [ # 初始化一个二维数组 |
到达数据流末端时,Dataset
会抛出 OutOfRangeError
。例如,下面的代码会一直读取 next_item
,直到没有数据可读:
1 | with tf.Session() as sess: |
如果 Dataset 依赖于有状态操作,即每个批次处理都依赖之前的批次数据或者中间结果来计算当前批次的数据。因此,需要在使用迭代器之前先初始化它,如下所示:
1 | r = tf.random_normal([10, 3]) |
要详细了解数据集和迭代器,请参阅 导入数据。
网络层
TensorFlow 主要是用来搭建深度学习模型的,因此其通过各种各样的 层 来创建神经网络的每一层。
层将变量和作用于它们的操作打包在一起。例如, 密集连接层 会对每个输出对应的所有输入执行加权和,并应用 激活函数 (可选)。连接权重和偏差由层对象管理。
创建层
下面的代码会创建一个 Dense
层,该层会接受一批输入矢量,并为每个矢量生成一个输出值。要将层应用于输入值,请将该层当做函数来调用。例如:
1 | x = tf.placeholder(tf.float32, shape=[None, 3]) # 创建一个占位符 |
层会检查其输入数据,以确定其内部变量的大小。因此,必须在这里设置 x 占位符的形状,以便层构建正确大小的权重矩阵。现在已经定义了输出值 y 的计算,在运行计算之前,还需要处理一个细节。
初始化层
层包含的变量必须先初始化,然后才能使用。尽管可以单独初始化各个变量,但也可以轻松地初始化一个 TensorFlow 图中的所有变量,如下:
1 | init = tf.global_variables_initializer() # 定义一个全局初始化操作 |
执行层
我们现在已经完成了层的初始化,可以像处理任何其他张量一样评估 linear_model 的输出张量了。例如,下面的代码:
1 | with tf.Session() as sess: |
层函数的快捷方式
对于每个层类(如 tf.layers.Dense
),TensorFlow 还提供了一个快捷函数(如 tf.layers.dense
)。两者唯一的区别是快捷函数版本是在单次调用中创建和运行层。例如:
1 | x = tf.placeholder(tf.float32, shape=[None, 3]) # 创建一个占位符 x |
尽管这种方法很方便,但无法访问 tf.layers.Layer
对象。这会让自省和调试变得更加困难,并且无法重复使用相应的层。
回归模型
现在已经了解 TensorFlow 核心部分的基础知识了,我们来手动训练一个小型回归模型吧。
我们首先来定义一些输入值 x,以及每个输入值对应的真实输出值 y_true:
1 | x = tf.constant([[1], [2], [3], [4]], dtype=tf.float32) |
接下来,建立一个简单的线性模型,其输出值只有 1 个:
1 | linear_model = tf.layers.Dense(units=1) |
你可以如下评估预测值:
1 | init = tf.global_variables_initializer() |
该模型尚未接受训练,因此四个 “预测” 值并不理想。因为网络层的权重值是随机初始化的,所以多次运行的输出应该有所不同:
要优化模型,首先需要定义损失函数。我们将使用均方误差,这是回归问题的标准损失。
虽然你可以使用较低级别的数学运算手动定义,但 tf.losses
模块提供了一系列常用的损失函数。你可以使用它来计算均方误差,具体操作如下所示:
1 | loss = tf.losses.mean_squared_error(labels=y_true, predictions=y_pred) |
TensorFlow 提供了执行标准优化算法的 优化器 。这些优化器被实现为 tf.train.Optimizer
的子类。它们会逐渐改变每个变量,以便将损失最小化。最简单的优化算法是 梯度下降法 ,由 tf.train.GradientDescentOptimizer
实现。它会根据损失相对于变量的导数大小来修改各个变量。例如:
1 | optimizer = tf.train.GradientDescentOptimizer(0.01) # 创建优化器 |
该代码构建了优化所需的所有图组件,并返回一个训练指令。该训练指令在运行时会更新图中的变量。你可以按以下方式运行该指令:
1 | with tf.Session() as sess: |
由上面的输出结果可以知道,随着迭代次数的增加,模型的损失函数值在不断的下降。
模型的保存与恢复
一般情况下,当我们训练完模型之后,需要把训练结果保存下来,以便测试的时候使用。在 TensorFlow 中,通过 tf.train.Saver()
来保存图模型。
创建 Saver
来管理模型中的所有变量。例如,以下代码段展示了如何调用 tf.train.Saver.save
方法以将变量保存到检查点文件中。因为前面所构建的变量都存于一个图中,这里为了防止变量冲突,新建另一个图来运行。
1 | g_2 = tf.Graph() # 重新定义一个图 |
从上面的运行结果可以看出,我们已经成功保存了模型,我们可以通过下面命令来查看所保存的模型。
1 | !tree |
输出为:
1 | . |
可以看到我们所保存的模型在文件夹 temp 下方,总共含有四个文件。.meta 文件表示模型的图结构,.data-00000-of-00001 和 .index 文件表示模型的权重文件;checkpoint 表示检查点文件。
现在来将模型读取出来。这里需要注意的是 tf.train.Saver
对象不仅将变量保存到检查点文件中,还将恢复变量。当恢复变量时,不必事先将其初始化。例如,以下代码段展示了如何调用 tf.train.Saver.restore
方法以从检查点文件中恢复变量:
1 | g_2 = tf.Graph() |
总结
通过以上学习,我们主要了解了 TensorFlow 的基本概念,如数据量流图,会话,张量等,并动手使用 TensorFlow 来实现一个简单的线性回归例子。此外还讲解了模型的保存与恢复。相信你此时已经对 TensorFlow 有一个初步的了解。
原文作者: 贺同学
原文链接: http://clarkhedi.github.io/2020/04/09/tensorflow-ji-ben-gai-nian-ji-gou-jian/
版权声明: 转载请注明出处(必须保留原文作者署名原文链接)