tensorflow 之 张量与变量
TensorFlow 与 MXNet、PyTorch 很相似,上手速度很快(相比于 TensorFlow1.x)。
TensorFlow 拆开来看,就是 Tensor
与 Flow
,即数据流程图,也是有向无环图(DAG)。在 TensorFlow 中以 Tensor
(张量)表示数据,即 DAG 的(实线)边,而虚线的边用来表示流程的控制关系。DAG 的节点表示数学操作符(即数学运算)。
1 张量 tf.Tensor
张量是由 tf.Tensor
对象表示的具有统一类型(称为 dtype
)的多维数组。您可以在 tf.dtypes.DType
中查看所有支持的 dtypes
。就像 Python 数值和字符串一样,所有张量都是不可变的:永远无法更新张量的内容,只能创建新的张量。
编写 TensorFlow 程序时,主要操纵和传递的对象是 tf.Tensor
。tf.Tensor
具有以下属性:
- 单一数据类型 (比如,float32,int32或字符串)
- a shape
TensorFlow 支持 eager 执行和 graph 执行。eager 执行时,将立即执行运算。在 graph 中执行时,构造一个计算图为以后运算。TensorFlow 默认 eager 执行。在以下示例中,立即计算矩阵乘法结果。其中 tf.constant
是张量的一种。
# 使用 Tensor 计算一些值
c = tf.constant([[1.0, 2.0], [3.0, 4.0]]) # 常量
d = tf.constant([[1.0, 1.0], [0.0, 1.0]])
e = tf.matmul(c, d) # 矩阵乘法
print(e)
输出:
tf.Tensor(
[[1. 3.]
[3. 7.]], shape=(2, 2), dtype=float32)
请注意,在 eager 执行过程中,您可能会发现您的 Tensors
实际上是类型为“EagerTensor”。这是内部细节,但确实可以给您访问有用的函数 numpy()
:
在 TensorFlow 中,tf.function
是定义 graph 执行的常用方法。张量的 shape
(即张量的 rank 和每个维度的大小)可能并不总是完全已知的。在 tf.function
定义中,shape 可能仅是部分已知的。
如果也可以完全知道其输入的形状,则大多数操作都会生成形状已知的张量,但是在某些情况下,只有在执行时才能找到张量的形状。
有许多专用的张量:参见 tf.constant
, tf.sparse.SparseTensor
, 和 tf.RaggedTensor
。
1.1 常量 tf.constant
def constant(value, dtype=None, shape=None, name="Const"):
从 tensor-like 的对象创建恒定张量,即常量。
注意:所有 eager 的 tf.Tensor
值都是不可变的(与 tf.Variable
相反)。从 tf.constant
返回的值没有特别常数。此功能与 tf.convert_to_tensor
基本上没有区别。名称 tf.constant
来自符号API(例如 tf.data
或 tf.keras
functional models),其中 value
嵌入在 tf.Graph
的 Const
节点中。tf.constant
可用于断言该值可以通过这种方式嵌入。
我们来创建一些基本张量。
下面是一个“标量”(或称“0 秩”张量)。标量包含单个值,但没有“轴”。
rank_0_tensor = tf.constant(4)
print(rank_0_tensor)
输出:
tf.Tensor(4, shape=(), dtype=int32)
“向量”(或称“1 秩”张量)就像一个值的列表。向量有 1 个轴:
rank_1_tensor = tf.constant([2.0, 3.0, 4.0])
print(rank_1_tensor)
输出:
tf.Tensor([2. 3. 4.], shape=(3,), dtype=float32)
“矩阵”(或称“2 秩”张量)有 2 个轴:
rank_2_tensor = tf.constant([[1, 2],
[3, 4],
[5, 6]], dtype=tf.float16)
print(rank_2_tensor)
输出:
tf.Tensor(
[[1. 2.]
[3. 4.]
[5. 6.]], shape=(3, 2), dtype=float16)
张量的轴可能更多,下面是一个包含 3 个轴的张量:
rank_3_tensor = tf.constant([
[[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]],
[[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]],
[[20, 21, 22, 23, 24],
[25, 26, 27, 28, 29]],])
print(rank_3_tensor)
输出:
tf.Tensor(
[[[ 0 1 2 3 4]
[ 5 6 7 8 9]]
[[10 11 12 13 14]
[15 16 17 18 19]]
[[20 21 22 23 24]
[25 26 27 28 29]]], shape=(3, 2, 5), dtype=int32)
对于包含 2 个以上的轴的张量,您可以通过多种方式将其可视化。
通过使用 np.array
或 tensor.numpy
方法,您可以将张量转换为 NumPy 数组:
张量通常包含浮点型和整型数据,但是还有许多其他数据类型,包括:
- 复杂的数值
- 字符串
tf.Tensor
基类要求张量是“矩形”——也就是说,每个轴上的每一个元素大小相同。但是,张量有可以处理不同形状的特殊类型。
- 不规则张量(参阅 RaggedTensor)
- 稀疏张量(参阅 SparseTensor)
上面并没有指定参数 dtype
,而从 value
的类型中自动推断出类型。
>>> # Constant 1-D Tensor from a python list.
>>> tf.constant([1, 2, 3, 4, 5, 6])
<tf.Tensor: shape=(6,), dtype=int32,
numpy=array([1, 2, 3, 4, 5, 6], dtype=int32)>
>>> # Or a numpy array
>>> a = np.array([[1, 2, 3], [4, 5, 6]])
>>> tf.constant(a)
<tf.Tensor: shape=(2, 3), dtype=int64, numpy=
array([[1, 2, 3],
[4, 5, 6]])>
如果指定了 dtype
,则将结果张量值强制转换为请求的 dtype
。
>>> tf.constant([1, 2, 3, 4, 5, 6], dtype=tf.float64)
<tf.Tensor: shape=(6,), dtype=float64,
numpy=array([1., 2., 3., 4., 5., 6.])>
如果设置了 shape
,则将 value
调整为匹配的形状。标量被扩展以填充 shape
:
>>> tf.constant(0, shape=(2, 3))
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
array([[0, 0, 0],
[0, 0, 0]], dtype=int32)>
>>> tf.constant([1, 2, 3, 4, 5, 6], shape=[2, 3])
<tf.Tensor: shape=(2, 3), dtype=int32, numpy=
array([[1, 2, 3],
[4, 5, 6]], dtype=int32)>
如果将 eager 张量作为 value
传递,tf.constant
无效,它甚至会传输梯度:
v = tf.Variable([0.0])
with tf.GradientTape() as g:
loss = tf.constant(v + v)
g.gradient(loss, v).numpy()
输出:
array([2.], dtype=float32)
但是,由于 tf.constant
将值嵌入到 tf.Graph
中,因此对于符号张量(symbolic tensors)而言失败:
Related Ops:
-
tf.convert_to_tensor
与tf.constant
类似,而不同于:- 无
shape
参数 - Symbolic tensors 被允许传递
- 无
>>> i = tf.keras.layers.Input(shape=[None, None])
>>> t = tf.convert_to_tensor(i)
1.2 张量的简单运算
我们可以对张量执行基本数学运算,包括加法、逐元素乘法和矩阵乘法运算。
a = tf.constant([[1, 2],
[3, 4]])
b = tf.constant([[1, 1],
[1, 1]]) # Could have also said `tf.ones([2,2])`
tf.print(tf.add(a, b), "\n") # 矩阵加法
tf.print(tf.multiply(a, b), "\n") # 逐元素乘法
tf.print(tf.matmul(a, b), "\n") # 矩阵乘法
输出:
[[2 3]
[4 5]]
[[1 2]
[3 4]]
[[3 3]
[7 7]]
其中,tf.print
用于打印信息。
TensorFlow 也重载了运算符:
tf.print(a + b, "\n") # element-wise addition
tf.print(a * b, "\n") # element-wise multiplication
tf.print(a @ b, "\n") # matrix multiplication```
输出:
[[2 3]
[4 5]]
[[1 2]
[3 4]]
[[3 3]
[7 7]]
张量支持其他各种运算 (op) 。
c = tf.constant([[4.0, 5.0], [10.0, 1.0]])
# Find the largest value
tf.print(tf.reduce_max(c), '\n')
# Find the index of the largest value
tf.print(tf.argmax(c), '\n')
# Compute the softmax
tf.print(tf.nn.softmax(c), '\n')
输出:
10
[1 0]
[[0.268941432 0.731058598]
[0.999876618 0.00012339458]]
1.3 形状简介
张量有形状。下面是几个相关术语:
- 形状:张量的每个维度的长度(元素数量)。
- 秩:张量的维度数量。标量的秩为 0,向量的秩为 1,矩阵的秩为 2。
轴或维度:张量的一个特殊维度。 - 大小:张量的总项数,即乘积形状向量
注:虽然您可能会看到“二维张量”之类的表述,但 2 秩张量通常并不是用来描述二维空间。
张量和 tf.TensorShape
对象提供了方便的属性来访问:
虽然通常用索引来指代轴,但是您始终要记住每个轴的含义。轴一般按照从全局到局部的顺序进行排序:首先是批次轴,随后是空间维度,最后是每个位置的特征。这样,在内存中,特征向量就会位于连续的区域。
tf.size(x).numpy()
与 len(x)
等效。
1.4 操作形状
通过重构可以改变张量的形状。重构的速度很快,资源消耗很低,因为不需要复制底层数据。
x = tf.constant([[1], [2], [3]])
reshaped = tf.reshape(x, [1, 3])
print(x.shape)
print(reshaped.shape)
输出:
(3, 1)
(1, 3)
数据在内存中的布局保持不变,同时使用请求的形状创建一个指向同一数据的新张量。TensorFlow 采用 C 样式的“行优先”内存访问顺序,即最右侧的索引值递增对应于内存中的单步位移。
如果您展平张量,则可以看到它在内存中的排列顺序。
一般来说,tf.reshape
唯一合理的用途是用于合并或拆分相邻轴(或添加/移除 1
)。
对于 3x2x5 张量,重构为 (3x2)x5 或 3x(2x5) 都合理,因为切片不会混淆:
重构可以处理总元素个数相同的任何新形状,但是如果不遵从轴的顺序,则不会发挥任何作用。
利用 tf.reshape
无法实现轴的交换,要交换轴,您需要使用 tf.transpose
。
您可能会遇到非完全指定的形状。要么是形状包含 None
维度(维度的长度未知),要么是形状为 None
(张量的秩未知)。
除了 tf.RaggedTensor 外,这种情况只会在 TensorFlow 的符号化计算图构建 API 环境中出现:
1.5 DTypes 详解
使用 Tensor.dtype
属性可以检查 tf.Tensor
的数据类型。
从 Python 对象创建 tf.Tensor
时,您可以选择指定数据类型。
如果不指定,TensorFlow 会选择一个可以表示您的数据的数据类型。TensorFlow 将 Python 整数转换为 tf.int32
,将 Python 浮点数转换为 tf.float32
。另外,当转换为数组时,TensorFlow 会采用与 NumPy 相同的规则。
数据类型可以相互转换。
1.6 广播
广播是从 NumPy 中的等效功能借用的一个概念。简而言之,在一定条件下,对一组张量执行组合运算时,为了适应大张量,会对小张量进行“扩展”。
最简单和最常见的例子是尝试将张量与标量相乘或相加。在这种情况下会对标量进行广播,使其变成与其他参数相同的形状。
同样,可以扩展大小为 1 的维度,使其符合其他参数。在同一个计算中可以同时扩展两个参数。
在本例中,一个 3x1 的矩阵与一个 1x4 进行元素级乘法运算,从而产生一个 3x4 的矩阵。注意前导 1 是可选的:y 的形状是 [4]。
下面是不使用广播的同一运算:
在大多数情况下,广播的时间和空间效率更高,因为广播运算不会在内存中具体化扩展的张量。
使用 tf.broadcast_to
可以了解广播的运算方式。
与数学运算不同,比方说,broadcast_to
并不会节省内存。在这个例子中,张量已经具体化。
这可能会变得更复杂。Jake VanderPlas 的《Python 数据科学手册》一书中的这一节介绍了更多广播技巧(同样使用 NumPy)。
1.7 tf.convert_to_tensor
大部分运算(如 tf.matmul
和 tf.reshape
)会使用 tf.Tensor
类的参数。不过,在上面的示例中,您会发现我们经常传递形状类似于张量的 Python 对象。
大部分(但并非全部)运算会在非张量参数上调用 convert_to_tensor
。我们提供了一个转换注册表,大多数对象类(如 NumPy 的 ndarray
、TensorShape
、Python 列表和 tf.Variable
)都可以自动转换。
有关更多详细信息,请参阅 tf.register_tensor_conversion_function
。如果您有自己的类型,则可能希望自动转换为张量。
1.8 不规则张量
如果张量的某个轴上的元素个数可变,则称为“不规则”张量。对于不规则数据,请使用 tf.ragged.RaggedTensor
。
例如,下面的例子无法用规则张量表示:
应使用 tf.ragged.constant
来创建 tf.RaggedTensor
:
tf.RaggedTensor
的形状包含未知维度:
1.9 字符串张量
tf.string
是一种 dtype
,也就是说,在张量中,我们可以用字符串(可变长度字节数组)来表示数据。
字符串是原子类型,无法像 Python 字符串一样编制索引。字符串的长度并不是张量的一个维度。有关操作字符串的函数,请参阅 tf.strings
。
下面是一个标量字符串张量:
下面是一个字符串向量:
在上面的打印输出中,b
前缀表示 tf.string
dtype 不是 Unicode 字符串,而是字节字符串。有关在 TensorFlow 如何使用 Unicode 文本的详细信息,请参阅 Unicode 教程。
如果传递 Unicode 字符,则会使用 utf-8 编码。
在 tf.strings
中可以找到用于操作字符串的一些基本函数,包括 tf.strings.split
。
以及 tf.string.to_number
:
虽然不能使用 tf.cast
将字符串张量转换为数值,但是可以先将其转换为字节,然后转换为数值。
tf.string
dtype 可用于 TensorFlow 中的所有原始字节数据。tf.io
模块包含在数据与字节类型之间进行相互转换的函数,包括解码图像和解析 csv 的函数。
1.10 稀疏张量
在某些情况下,数据很稀疏,比如说在一个非常宽的嵌入空间中。为了高效存储稀疏数据,TensorFlow 支持 tf.sparse.SparseTensor
和相关运算。
2 变量
TensorFlow 变量是用于表示程序处理的共享持久状态的推荐方法。本指南介绍在 TensorFlow 中如何创建、更新和管理 tf.Variable
的实例。
变量通过 tf.Variable
类进行创建和跟踪。tf.Variable
表示张量,对它执行运算可以改变其值。利用特定运算可以读取和修改此张量的值。更高级的库(如 tf.keras
)使用 tf.Variable
来存储模型参数。
2.1 设置
本笔记本讨论变量布局。如果要查看变量位于哪一个设备上,请取消注释这一行代码。
import tensorflow as tf
# Uncomment to see where your variables get placed (see below)
# tf.debugging.set_log_device_placement(True)
2.2 创建变量
要创建变量,请提供一个初始值。tf.Variable
与初始值的 dtype
相同。
my_tensor = tf.constant([[1.0, 2.0], [3.0, 4.0]])
my_variable = tf.Variable(my_tensor)
# Variables can be all kinds of types, just like tensors
bool_variable = tf.Variable([False, False, False, True])
complex_variable = tf.Variable([5 + 4j, 6 + 1j])
变量与张量的定义方式和操作行为都十分相似,实际上,它们都是 tf.Tensor
支持的一种数据结构。与张量类似,变量也有 dtype
和形状,并且可以导出至 NumPy。
大部分张量运算在变量上也可以按预期运行,不过变量无法重构形状。
如上所述,变量由张量提供支持。您可以使用 tf.Variable.assign
重新分配张量。调用 assign
(通常)不会分配新张量,而会重用现有张量的内存。
如果在运算中像使用张量一样使用变量,那么通常会对支持张量执行运算。
从现有变量创建新变量会复制支持张量。两个变量不能共享同一内存空间。
2.3 生命周期、命名和监视
在基于 Python 的 TensorFlow 中,tf.Variable
实例与其他 Python 对象的生命周期相同。如果没有对变量的引用,则会自动将其解除分配。
为了便于跟踪和调试,您还可以为变量命名。两个变量可以使用相同的名称。
保存和加载模型时会保留变量名。默认情况下,模型中的变量会自动获得唯一变量名,所以除非您希望自行命名,否则不必多此一举。
虽然变量对微分很重要,但某些变量不需要进行微分。在创建时,通过将 trainable 设置为 False 可以关闭梯度。例如,训练计步器就是一个不需要梯度的变量。
step_counter = tf.Variable(1, trainable=False)
2.4 放置变量和张量
为了提高性能,TensorFlow 会尝试将张量和变量放在与其 dtype
兼容的最快设备上。这意味着如果有 GPU,那么大部分变量都会放置在 GPU 上。
不过,我们可以重写变量的位置。在以下代码段中,即使存在可用的 GPU,我们也可以将一个浮点张量和一个变量放置在 CPU 上。通过打开设备分配日志记录(参阅设置),可以查看变量的所在位置。
注:虽然可以手动放置变量,但使用分布策略是一种可优化计算的更便捷且可扩展的方式。
如果在有 GPU 和没有 GPU 的不同后端上运行此笔记本,则会看到不同的记录。请注意,必须在会话开始时打开设备布局记录。
您可以将变量或张量的位置设置在一个设备上,然后在另一个设备上执行计算。但这样会产生延迟,因为需要在两个设备之间复制数据。
不过,如果您有多个 GPU 工作进程,但希望变量只有一个副本,则可以这样做。
注:由于 tf.config.set_soft_device_placement
默认处于打开状态,所以,即使在没有 GPU 的设备上运行此代码,它也会运行,只不过乘法步骤在 CPU 上执行。