TensorFlow 实战(一)(1)https://developer.aliyun.com/article/1522664
2.1.1 TensorFlow 在底层是如何运行的?
在典型的 TensorFlow 程序中,有两个主要步骤:
- 定义一个涵盖输入、操作和输出的数据流图。在我们的练习中,数据流图将表示 x、w1、b1、w2、b2、h 和 y 之间的关系。
- 通过为输入提供值并计算输出来执行图形。例如,如果我们需要计算 h,则将一个值(例如 NumPy 数组)馈送到 x 并获取 h 的值。
TensorFlow 2 使用一种称为命令式执行的执行样式。在命令式执行中,声明(定义图形)和执行同时发生。这也被称为急切执行代码。
您可能想知道数据流图是什么样的。这是 TensorFlow 用来描述您定义的计算流程的术语,并表示为有向无环图(DAG):箭头表示数据,节点表示操作。换句话说,tf.Variable 和 tf.Tensor 对象表示图中的边,而操作(例如 tf.matmul)表示节点。例如,对于
h = x W[1] + b[1]
将看起来像图 2.4。然后,在运行时,您可以通过向 x 提供值来获取 y 的值,因为 y 依赖于输入 x。
图 2.4 一个示例计算图。这里的各个元素将在 2.2 节中更详细地讨论。
TensorFlow 如何知道创建数据流图?您可能已经注意到以@符号开头的行悬挂在 forward(…) 函数的顶部。这在 Python 语言中称为装饰器。@tf.function 装饰器接受执行各种 TensorFlow 操作的函数,跟踪所有步骤,并将其转换为数据流图。这是多么酷?这鼓励用户编写模块化代码,同时实现数据流图的计算优势。TensorFlow 2 中这个功能被称为 AutoGraph(www.tensorflow.org/guide/function
)。
什么是装饰器?
装饰器通过包装函数来修改函数的行为,这发生在函数被调用之前/之后。一个很好的装饰器示例是在每次调用函数时记录输入和输出。下面是如何使用装饰器的示例:
def log_io(func): def wrapper(*args, **kwargs): print("args: ", args) print(“kwargs: “, kwargs) out = func(*args, **kwargs) print("return: ", out) return wrapper @log_io def easy_math(x, y): return x + y + ( x * y) res = easy_math(2,3)
这将输出
args: (2, 3) kwargs: {} return: 11
预期的。因此,当您添加 @tf.function 装饰器时,它实际上修改了调用函数的行为,通过构建给定函数内发生的计算的计算图。
图 2.5 中的图解描述了 TensorFlow 2 程序的执行路径。第一次调用函数 a(…) 和 b(…) 时,将创建数据流图。然后,将输入传递给函数,以将输入传递给图并获取您感兴趣的输出。
图 2.5 TensorFlow 2 程序的典型执行。在第一次运行时,TensorFlow 会跟踪所有使用 @tf.function 注释的函数,并构建数据流图。在后续运行中,根据函数调用传递相应的值给图,并检索结果。
AutoGraph
AutoGraph 是 TensorFlow 中的一个很棒的功能,通过在幕后努力工作,减轻了开发者的工作量。要真正欣赏这个功能,请阅读更多内容请访问www.tensorflow.org/guide/function
。虽然它相当令人惊叹,但 AutoGraph 不是万能药。因此,了解其优点以及限制和注意事项非常重要:
- 如果您的代码包含大量重复操作(例如,多次迭代训练神经网络),AutoGraph 将提供性能提升。
- 如果您运行多个仅运行一次的不同操作,则 AutoGraph 可能会减慢您的速度;因为您仅运行一次操作,构建图仅是一种开销。
- 要注意将什么包含在您向 AutoGraph 公开的函数内。例如
- NumPy 数组和 Python 列表将被转换为 tf.constant 对象。
- 在函数跟踪期间将展开 for 循环,这可能导致大型图最终耗尽内存。
TensorFlow 1,TensorFlow 2 的前身,使用了一种称为声明式基于图的执行的执行风格,它包含两个步骤:
- 明确定义一个数据流图,使用各种符号元素(例如占位符输入、变量和操作),以实现你所需的功能。与 TensorFlow 2 不同,这些在声明时不会保存值。
- 明确编写代码来运行定义的图,并获取或评估结果。您可以在运行时向先前定义的符号元素提供实际值,并执行图。
这与 TensorFlow 2 非常不同,后者隐藏了数据流图的所有复杂性,通过自动在后台构建它。在 TensorFlow 1 中,您必须显式构建图,然后执行它,导致代码更加复杂且难以阅读。表 2.2 总结了 TensorFlow 1 和 TensorFlow 2 之间的区别。
表 2.2 TensorFlow 1 和 TensorFlow 2 之间的区别
TensorFlow 1 | TensorFlow 2 |
默认情况下不使用急切执行 | 默认情况下使用急切执行 |
使用符号占位符表示图形的输入 | 直接将实际数据(例如,NumPy 数组)提供给数据流图 |
由于结果不是按命令式评估,因此难以调试 | 由于操作是按命令式评估的,因此易于调试 |
需要显式手动创建数据流图 | 具有 AutoGraph 功能,可以跟踪 TensorFlow 操作并自动创建图形 |
不鼓励面向对象编程,因为它强制您提前定义计算图 | 鼓励面向对象编程 |
由于具有单独的图形定义和运行时代码,代码的可读性较差 | 具有更好的代码可读性 |
在下一节中,我们将讨论 TensorFlow 的基本构建模块,为编写 TensorFlow 程序奠定基础。
练习 1
给定以下代码,
# A import tensorflow as tf # B def f1(x, y, z): return tf.math.add(tf.matmul(x, y) , z) #C w = f1(x, y, z)
tf.function 装饰器应该放在哪里?
- A
- B
- C
- 以上任何一项
2.2 TensorFlow 构建模块
我们已经看到了 TensorFlow 1 和 TensorFlow 2 之间的核心差异。在此过程中,您接触到了 TensorFlow API 公开的各种数据结构(例如,tf.Variable)和操作(例如,tf.matmul)。现在让我们看看在哪里以及如何使用这些数据结构和操作。
在 TensorFlow 2 中,我们需要了解三个主要的基本元素:
- tf.Variable
- tf.Tensor
- tf.Operation
你已经看到所有这些被使用了。例如,从前面的 MLP 示例中,我们有这些元素,如表 2.3 所示。了解这些基本组件有助于理解更抽象的组件,例如 Keras 层和模型对象,稍后将进行讨论。
表 2.3 MLP 示例中的 tf.Variable、tf.Tensor 和 tf.Operation 实体
元素 | 示例 |
tf.Variable | w1*,* b1*,* w2 和 b2 |
tf.Tensor | h 和 y |
tf.Operation | tf.matmul |
牢牢掌握 TensorFlow 的这些基本元素非常重要,原因有几个。主要原因是,从现在开始,您在本书中看到的所有内容都是基于这些元素构建的。例如,如果您使用像 Keras 这样的高级 API 构建模型,它仍然使用 tf.Variable、tf.Tensor 和 tf.Operation 实体来进行计算。因此,了解如何使用这些元素以及您可以实现什么和不能实现什么非常重要。另一个好处是,TensorFlow 返回的错误通常使用这些元素呈现给您。因此,这些知识还将帮助我们理解错误并在开发更复杂的模型时迅速解决它们。
2.2.1 理解 tf.Variable
构建典型的机器学习模型时,您有两种类型的数据:
- 模型参数随时间变化(可变),因为模型针对所选损失函数进行了优化。
- 模型输出是给定数据和模型参数的静态值(不可变)
tf.Variable 是定义模型参数的理想选择,因为它们被初始化为某个值,并且可以随着时间改变其值。一个 TensorFlow 变量必须具有以下内容:
- 形状(变量的每个维度的大小)
- 初始值(例如,从正态分布中抽样的随机初始化)
- 数据类型(例如 int32、float32)
你可以如下定义一个 TensorFlow 变量
tf.Variable(initial_value=None, trainable=None, dtype=None)
其中
- 初始值包含提供给模型的初始值。通常使用 tf.keras.initializers 子模块中提供的变量初始化器提供(完整的初始化器列表可以在
mng.bz/M2Nm
找到)。例如,如果你想使用均匀分布随机初始化一个包含四行三列的二维矩阵的变量,你可以传递 tf.keras.initializers.RandomUniform()([4,3])。你必须为 initial_value 参数提供一个值。 - trainable 参数接受布尔值(即 True 或 False)作为输入。将 trainable 参数设置为 True 允许通过梯度下降更改模型参数。将 trainable 参数设置为 False 将冻结层,以使值不能使用梯度下降进行更改。
- dtype 指定变量中包含的数据的数据类型。如果未指定,这将默认为提供给 initial_value 参数的数据类型(通常为 float32)。
让我们看看如何定义 TensorFlow 变量。首先,请确保已导入以下库:
import tensorflow as tf import numpy as np
你可以如下定义一个大小为 4 的一维 TensorFlow 变量,其常量值为 2:
v1 = tf.Variable(tf.constant(2.0, shape=[4]), dtype='float32') print(v1) >>> <tf.Variable 'Variable:0' shape=(4,) dtype=float32, numpy=array([2., 2., 2., 2.], dtype=float32)>
在这里,tf.constant(2.0, shape=[4]) 生成一个有四个元素且值为 2.0 的向量,然后将其用作 tf.Variable 的初始值。你也可以使用 NumPy 数组定义一个 TensorFlow 变量:
v2 = tf.Variable(np.ones(shape=[4,3]), dtype='float32') print(v2) >>> <tf.Variable 'Variable:0' shape=(4, 3) dtype=float32, numpy= array([[1., 1., 1.], [1., 1., 1.], [1., 1., 1.], [1., 1., 1.]], dtype=float32)>
在这里,np.ones(shape=[4,3]) 生成一个形状为 [4,3] 的矩阵,所有元素的值都为 1。下一个代码片段定义了一个具有随机正态初始化的三维(3×4×5) TensorFlow 变量:
v3 = tf.Variable(tf.keras.initializers.RandomNormal()(shape=[3,4,5]), dtype='float32') print(v3) >>> <tf.Variable 'Variable:0' shape=(3, 4, 5) dtype=float32, numpy= array([[[-0.00599647, -0.04389469, -0.03364765, -0.0044175 , 0.01199682], [ 0.05423453, -0.02812728, -0.00572744, -0.08236874, -0.07564012], [ 0.0283042 , -0.05198685, 0.04385028, 0.02636188, 0.02409425], [-0.04051876, 0.03284673, -0.00593955, 0.04204708, -0.05000611]], ... [[-0.00781542, -0.03068716, 0.04313354, -0.08717368, 0.07951441], [ 0.00467467, 0.00154883, -0.03209472, -0.00158945, 0.03176221], [ 0.0317267 , 0.00167555, 0.02544901, -0.06183815, 0.01649506], [ 0.06924769, 0.02057942, 0.01060928, -0.00929202, 0.04461157]]], dtype=float32)>
在这里,你可以看到如果我们打印一个 tf.Variable,可以看到它的属性,如下所示:
- 变量的名称
- 变量的形状
- 变量的数据类型
- 变量的初始值
你还可以使用一行代码将你的 tf.Variable 转换为 NumPy 数组
arr = v1.numpy()
然后,你可以通过打印 Python 变量 arr 来验证结果
print(arr)
将返回
>>> [2\. 2\. 2\. 2.]
tf.Variable 的一个关键特点是,即使在初始化后,你也可以根据需要更改其元素的值。例如,要操作 tf.Variable 的单个元素或片段,你可以使用 assign() 操作如下。
为了本练习的目的,让我们假设以下 TensorFlow 变量,它是一个由零初始化的矩阵,有四行三列:
v = tf.Variable(np.zeros(shape=[4,3]), dtype='float32')
你可以如下更改第一行(即索引为 0)和第三列(即索引为 2)中的元素:
v = v[0,2].assign(1)
这会产生下列数组:
>>> [[0\. 0\. 1.] [0\. 0\. 0.] [0\. 0\. 0.] [0\. 0\. 0.]]
请注意,Python 使用以零为基数的索引。这意味着索引从零开始(而不是从一开始)。例如,如果你要获取向量 vec 的第二个元素,你应该使用 vec[1]。
你也可以使用切片改变数值。例如,下面我们就将最后两行和前两列的数值改为另外一些数:
v = v[2:, :2].assign([[3,3],[3,3]])
结果如下:
>>> [[0\. 0\. 1.] [0\. 0\. 0.] [3\. 3\. 0.] [3\. 3\. 0.]]
练习 2
请编写代码创建一个 tf.Variable,其数值为下面的数值,并且类型为 int16。你可以使用 np.array() 来完成该任务。
1 2 3 4 3 2
2.2.2 理解 tf.Tensor
正如我们所见,tf.Tensor 是对某些数据进行 TensorFlow 操作后得到的输出(例如,对 tf.Variable 或者 tf.Tensor 进行操作)。在定义机器学习模型时,tf.Tensor 对象广泛应用于存储输入、层的中间输出、以及模型的最终输出。到目前为止,我们主要看了向量(一维)和矩阵(二维)。但是,我们也可以创建 n 维数据结构。这样的一个 n 维数据结构被称为一个 张量。表 2.4 展示了一些张量的示例。
表 2.4 张量的示例
描述 | 示例 |
一个 2 × 4 的二维张量 |
[ [1,3,5,7], [2,4,6,8] ]
|
一个大小为 2 × 3 × 2 × 1 的四维张量 |
[ [ [[1],[2]], [[2],[3]], [[3],[4]] ], [ [[1],[2]], [[2],[3]], [[3],[4]] ] ]
|
张量也有轴,张量的每个维度都被认为是一个轴。图 2.6 描述了一个 3D 张量的轴。
图 2.6 一个 2 × 4 × 3 张量,包含三个轴。第一个轴(axis 0)是行维度,第二个轴(axis 1)是列维度,最后一个轴(axis 2)是深度维度。
严格来说,张量也可以只有一个维度(即向量)或者只是一个标量。但是需要区分 tensor 和 tf.Tensor。在讨论模型的数学方面时我们会使用 tensor/vector/scalar,而我们在提到 TensorFlow 代码所输出的任何数据相关输出时都会使用 tf.Tensor。
下面我们将讨论一些会产生 tf.Tensor 的情况。例如,你可以通过一个 tf.Variable 和一个常数相乘来产生一个 tf.Tensor:
v = tf.Variable(np.ones(shape=[4,3]), dtype='float32') b = v * 3.0
如果你使用 print(type(b).name) 分析前面操作生成的对象类型,你会看到下面的输出:
>>> EagerTensor
EagerTensor 是从 tf.Tensor 继承而来的一个类。它是一种特殊类型的 tf.Tensor,其值在定义后会立即得到计算。你可以通过执行下列命令验证 EagerTensor 实际上是 tf.Tensor:
assert isinstance(b, tf.Tensor)
也可以通过将一个 tf.Tensor 加上另一个 tf.Tensor 来创建一个 tf.Tensor。
a = tf.constant(2, shape=[4], dtype='float32') b = tf.constant(3, shape=[4], dtype='float32') c = tf.add(a,b)
print© 将打印出下列结果:
>>> [5\. 5\. 5\. 5]
在这个例子中,tf.constant() 用于创建 tf.Tensor 对象 a 和 b。通过将 a 和 b 加在一起,你将得到一个类型为 tf.Tensor 的张量 c。如之前所述,可以通过运行如下代码验证该张量:
assert isinstance(c, tf.Tensor)
tf.Variable 和 tf.Tensor 之间的关键区别在于,tf.Variable 允许其值在变量初始化后发生更改(称为可变结构)。然而,一旦您初始化了一个 tf.Tensor,在执行的生命周期中您就无法更改它(称为不可变数据结构)。tf.Variable 是一种可变数据结构,而 tf.Tensor 是一种不可变数据结构。
让我们看看如果尝试在初始化后更改 tf.Tensor 的值会发生什么:
a = tf.constant(2, shape=[4], dtype='float32') a = a[0].assign(2.0)
您将收到以下错误:
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-19-6e4e6e519741> in <module>() 1 a = tf.constant(2, shape=[4], dtype='float32') ----> 2 a = a[0].assign(2.0) AttributeError: 'tensorflow.python.framework.ops.EagerTensor' object has no attribute 'assign'
显然,TensorFlow 对我们尝试修改 tf.Tensor 对象的叛逆行为并不感兴趣。
张量动物园
TensorFlow 有各种不同的张量类型,用于解决各种问题。以下是 TensorFlow 中可用的一些不同的张量类型:
RaggedTensor——一种用于不能有效表示为矩阵的可变序列长度数据集的数据类型
TensorArray——一种动态大小的数据结构,可以从小开始,并随着添加更多数据而伸展(类似于 Python 列表)
SparseTensor——一种用于表示稀疏数据的数据类型(例如,用户-电影评分矩阵)
在下一小节中,我们将讨论一些流行的 TensorFlow 操作。
练习 3
你能写出创建初始化为从正态分布中抽样的值并且形状为 4 × 1 × 5 的 tf.Tensor 的代码吗?您可以使用 np.random.normal()来实现这个目的。
2.2.3 理解 tf.Operation
TensorFlow 的骨干是操作,它允许您对数据进行有用的操作。例如,深度网络中的核心操作之一是矩阵乘法,这使得 TensorFlow 成为实现核心操作的强大工具。就像矩阵乘法一样,TensorFlow 提供了许多低级操作,可用于 TensorFlow。可以在TensorFlow API中找到可用操作的完整列表。
让我们讨论一些您可以使用的流行算术操作。首先,您有基本的算术操作,如加法、减法、乘法和除法。您可以像对待普通 Python 变量一样执行这些操作。为了演示这一点,让我们假设以下向量:
import tensorflow as tf import numpy as np a = tf.constant(4, shape=[4], dtype='float32') b = tf.constant(2, shape=[4], dtype='float32')
我们可以通过执行以下操作来查看 a 和 b 的样子
print(a) print(b)
这给出
>>> tf.Tensor([4\. 4\. 4\. 4.], shape=(4,), dtype=float32) >>> tf.Tensor([2\. 2\. 2\. 2.], shape=(4,), dtype=float32)
对 a 和 b 执行加法
c = a+b print(c)
提供
>>> tf.Tensor([6\. 6\. 6\. 6.], shape=(4,), dtype=float32)
对 a 和 b 执行乘法
e = a*b print(e)
提供
>>> tf.Tensor([8\. 8\. 8\. 8.], shape=(4,), dtype=float32)
您还可以在张量之间进行逻辑比较。假设
a = tf.constant([[1,2,3],[4,5,6]]) b = tf.constant([[5,4,3],[3,2,1]])
并检查逐元素相等性
equal_check = (a==b) print(equal_check)
提供
>>> tf.Tensor( [[False False True] [False False False]], shape=(2, 3), dtype=bool)
检查小于或等于元素
leq_check = (a<=b) print(leq_check)
提供
>>> tf.Tensor( [[ True True True] [False False False]], shape=(2, 3), dtype=bool)
接下来,您有减少运算符,允许您在特定轴或所有轴上减少张量(例如,最小值/最大值/和/乘积):
a = tf.constant(np.random.normal(size=[5,4,3]), dtype='float32')
这里,a 是一个看起来像这样的 tf.Tensor:
>>> tf.Tensor( [[[-0.7665215 0.9611947 1.456347 ] [-0.52979267 -0.2647674 -0.57217133] [-0.7511135 2.2282166 0.6573406 ] [-1.1323775 0.3301812 0.1310132 ]] ... [[ 0.42760614 0.17308706 -0.90879506] [ 0.5347165 2.569637 1.3013649 ] [ 0.95198756 -0.74183583 -1.2316796 ] [-0.03830088 1.1367576 -1.2704859 ]]], shape=(5, 4, 3), dtype=float32)
让我们首先获取此张量的所有元素的总和。换句话说,在所有轴上减少张量:
red_a1 = tf.reduce_sum(a)
这产生
>>> -4.504758
接下来,让我们在轴 0 上获取产品(即,对 a 的每一行进行逐元素乘积):
red_a2 = tf.reduce_prod(a, axis=0)
这产生
>>> [[-0.04612858 0.45068324 0.02033644] [-0.27674386 -0.03757533 -0.33719817] [-1.4913832 -2.1016302 -0.39335614] [-0.00213956 0.14960718 0.01671476]]
现在我们将在多个轴(即 0 和 1)上获取最小值:
red_a3 = tf.reduce_min(a, axis=[0,1])
这产生
>>> [-1.6531237 -1.6245098 -1.4723392]
你可以看到,无论何时在某个维度上执行缩减操作,你都会失去该维度。例如,如果你有一个大小为[6,4,2]的张量,并且在轴 1 上缩减该张量(即第二个轴),你将得到一个大小为[6,2]的张量。在某些情况下,你需要在缩减张量的同时保留该维度(导致一个[6,1,2]形状的张量)。一个这样的情况是使你的张量广播兼容另一个张量(mng.bz/g4Zn
)。广播是一个术语,用来描述科学计算工具(例如 NumPy/TensorFlow)在算术操作期间如何处理张量。在这种情况下,你可以将 keepdims 参数设置为 True(默认为 False)。你可以看到最终输出的形状的差异
# Reducing with keepdims=False red_a1 = tf.reduce_min(a, axis=1) print(red_a1.shape)
这产生
>>> [5,3] # Reducing with keepdims=True red_a2 = tf.reduce_min(a, axis=1, keepdims=True) print(red_a2.shape)
这产生
>>> red_a2.shape = [5,1,3]
表 2.5 中概述了其他几个重要的函数。
表 2.5 TensorFlow 提供的数学函数
tf.argmax | 描述 | 计算给定轴上最大值的索引。例如,以下示例显示了如何在轴 0 上计算 tf.argmax。 |
用法 | d = tf.constant([[1,2,3],[3,4,5],[6,5,4]])d_max1 = tf.argmax(d, axis=0) | |
结果 | tf.Tensor ([2,2,0]) | |
tf.argmin | 描述 | 计算给定轴上最小值的索引。例如,以下示例显示了如何在轴 1 上计算 tf.argmin。 |
用法 | d = tf.constant([[1,2,3],[3,4,5],[6,5,4]])d_min1 = tf.argmin(d, axis=1) | |
结果 | tf.Tensor([[0],[0],[0]]) | |
tf.cumsum | 描述 | 计算给定轴上向量或张量的累积和 |
用法 | e = tf.constant([1,2,3,4,5])e_cumsum = tf.cumsum(e) | |
结果 | tf.Tensor([1,3,6,10,15]) |
我们在这里结束了对 TensorFlow 基本原语的讨论。接下来我们将讨论在神经网络模型中常用的一些计算。
练习 4
还有另一个计算平均值的函数叫做 tf.reduce_mean()。给定包含以下值的 tf.Tensor 对象 a,你能计算每列的平均值吗?
0.5 0.2 0.7 0.2 0.3 0.4 0.9 0.1 0.1
TensorFlow 实战(一)(3)https://developer.aliyun.com/article/1522666