PyTorch 深度学习(GPT 重译)(一)(2)https://developer.aliyun.com/article/1485199
3.2 张量:多维数组
我们已经学到了张量是 PyTorch 中的基本数据结构。张量是一个数组:即,一种数据结构,用于存储一组可以通过索引单独访问的数字,并且可以用多个索引进行索引。
3.2.1 从 Python 列表到 PyTorch 张量
让我们看看list
索引是如何工作的,这样我们就可以将其与张量索引进行比较。在 Python 中,取一个包含三个数字的列表(.code/p1ch3/1_tensors.ipynb):
# In[1]: a = [1.0, 2.0, 1.0]
我们可以使用相应的从零开始的索引来访问列表的第一个元素:
# In[2]: a[0] # Out[2]: 1.0 # In[3]: a[2] = 3.0 a # Out[3]: [1.0, 2.0, 3.0]
对于处理数字向量的简单 Python 程序,比如 2D 线的坐标,使用 Python 列表来存储向量并不罕见。正如我们将在接下来的章节中看到的,使用更高效的张量数据结构,可以表示许多类型的数据–从图像到时间序列,甚至句子。通过定义张量上的操作,其中一些我们将在本章中探讨,我们可以高效地切片和操作数据,即使是从一个高级(并不特别快速)语言如 Python。
3.2.2 构建我们的第一个张量
让我们构建我们的第一个 PyTorch 张量并看看它是什么样子。暂时它不会是一个特别有意义的张量,只是一个列中的三个 1:
# In[4]: import torch # ❶ a = torch.ones(3) # ❷ a # Out[4]: tensor([1., 1., 1.]) # In[5]: a[1] # Out[5]: tensor(1.) # In[6]: float(a[1]) # Out[6]: 1.0 # In[7]: a[2] = 2.0 a # Out[7]: tensor([1., 1., 2.])
❶ 导入 torch 模块
❷ 创建一个大小为 3、填充为 1 的一维张量
导入 torch
模块后,我们调用一个函数,创建一个大小为 3、填充值为 1.0
的(一维)张量。我们可以使用基于零的索引访问元素或为其分配新值。尽管表面上这个例子与数字对象列表没有太大区别,但在底层情况完全不同。
3.2.3 张量的本质
Python 列表或数字元组是单独分配在内存中的 Python 对象的集合,如图 3.3 左侧所示。另一方面,PyTorch 张量或 NumPy 数组是对(通常)包含未装箱的 C 数值类型而不是 Python 对象的连续内存块的视图。在这种情况下,每个元素是一个 32 位(4 字节)的 float
,正如我们在图 3.3 右侧所看到的。这意味着存储 1,000,000 个浮点数的 1D 张量将需要确切的 4,000,000 个连续字节,再加上一些小的开销用于元数据(如维度和数值类型)。
图 3.3 Python 对象(带框)数值值与张量(未带框数组)数值值
假设我们有一个坐标列表,我们想用它来表示一个几何对象:也许是一个顶点坐标为 (4, 1), (5, 3) 和 (2, 1) 的 2D 三角形。这个例子与深度学习无关,但很容易理解。与之前将坐标作为 Python 列表中的数字不同,我们可以使用一维张量,将X存储在偶数索引中,Y存储在奇数索引中,如下所示:
# In[8]: points = torch.zeros(6) # ❶ points[0] = 4.0 # ❷ points[1] = 1.0 points[2] = 5.0 points[3] = 3.0 points[4] = 2.0 points[5] = 1.0
❶ 使用 .zeros 只是获取一个适当大小的数组的一种方式。
❷ 我们用我们实际想要的值覆盖了那些零值。
我们也可以将 Python 列表传递给构造函数,效果相同:
# In[9]: points = torch.tensor([4.0, 1.0, 5.0, 3.0, 2.0, 1.0]) points # Out[9]: tensor([4., 1., 5., 3., 2., 1.])
要获取第一个点的坐标,我们执行以下操作:
# In[10]: float(points[0]), float(points[1]) # Out[10]: (4.0, 1.0)
这是可以的,尽管将第一个索引指向单独的 2D 点而不是点坐标会更实用。为此,我们可以使用一个 2D 张量:
# In[11]: points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]]) points # Out[11]: tensor([[4., 1.], [5., 3.], [2., 1.]])
在这里,我们将一个列表的列表传递给构造函数。我们可以询问张量的形状:
# In[12]: points.shape # Out[12]: torch.Size([3, 2])
这告诉我们张量沿每个维度的大小。我们也可以使用 zeros
或 ones
来初始化张量,提供大小作为一个元组:
# In[13]: points = torch.zeros(3, 2) points # Out[13]: tensor([[0., 0.], [0., 0.], [0., 0.]])
现在我们可以使用两个索引访问张量中的单个元素:
# In[14]: points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]]) points # Out[14]: tensor([[4., 1.], [5., 3.], [2., 1.]]) # In[15]: points[0, 1] # Out[15]: tensor(1.)
这返回我们数据集中第零个点的Y坐标。我们也可以像之前那样访问张量中的第一个元素,以获取第一个点的 2D 坐标:
# In[16]: points[0] # Out[16]: tensor([4., 1.])
输出是另一个张量,它呈现了相同基础数据的不同视图。新张量是一个大小为 2 的 1D 张量,引用了 points
张量中第一行的值。这是否意味着分配了一个新的内存块,将值复制到其中,并返回了包装在新张量对象中的新内存?不,因为那样会非常低效,特别是如果我们有数百万个点。当我们在本章后面讨论张量视图时,我们将重新讨论张量是如何存储的。
3.3 张量索引
如果我们需要获取一个不包含第一个点的张量,那很容易使用范围索引表示法,这也适用于标准 Python 列表。这里是一个提醒:
# In[53]: some_list = list(range(6)) some_list[:] # ❶ some_list[1:4] # ❷ some_list[1:] # ❸ some_list[:4] # ❹ some_list[:-1] # ❺ some_list[1:4:2] # ❻
❶ 列表中的所有元素
❷ 从第 1 个元素(包括)到第 4 个元素(不包括)
❸ 从第 1 个元素(包括)到列表末尾
❹ 从列表开头到第 4 个元素(不包括)
❺ 从列表开头到倒数第二个元素之前
❻ 从第 1 个元素(包括)到第 4 个元素(不包括),步长为 2
为了实现我们的目标,我们可以使用与 PyTorch 张量相同的符号表示法,其中的额外好处是,就像在 NumPy 和其他 Python 科学库中一样,我们可以为张量的每个维度使用范围索引:
# In[54]: points[1:] # ❶ points[1:, :] # ❷ points[1:, 0] # ❸ points[None] # ❹
❶ 第一个之后的所有行;隐式地所有列
❷ 第一个之后的所有行;所有列
❸ 第一个之后的所有行;第一列
❹ 添加一个大小为 1 的维度,就像 unsqueeze 一样
除了使用范围,PyTorch 还具有一种强大的索引形式,称为高级索引,我们将在下一章中看到。
3.4 命名张量
我们的张量的维度(或轴)通常索引像素位置或颜色通道之类的内容。这意味着当我们想要索引张量时,我们需要记住维度的顺序,并相应地编写我们的索引。随着数据通过多个张量进行转换,跟踪哪个维度包含什么数据可能会出错。
为了使事情具体化,想象我们有一个三维张量 img_t
,来自第 2.1.4 节(这里为简单起见使用虚拟数据),我们想将其转换为灰度。我们查找了颜色的典型权重,以得出单个亮度值:¹
# In[2]: img_t = torch.randn(3, 5, 5) # shape [channels, rows, columns] weights = torch.tensor([0.2126, 0.7152, 0.0722])
我们经常希望我们的代码能够泛化–例如,从表示为具有高度和宽度维度的 2D 张量的灰度图像到添加第三个通道维度的彩色图像(如 RGB),或者从单个图像到一批图像。在第 2.1.4 节中,我们引入了一个额外的批处理维度 batch_t
;这里我们假装有一个批处理为 2 的批次:
# In[3]: batch_t = torch.randn(2, 3, 5, 5) # shape [batch, channels, rows, columns]
有时 RGB 通道在维度 0 中,有时它们在维度 1 中。但我们可以通过从末尾计数来概括:它们总是在维度-3 中,距离末尾的第三个。因此,懒惰的、无权重的平均值可以写成如下形式:
# In[4]: img_gray_naive = img_t.mean(-3) batch_gray_naive = batch_t.mean(-3) img_gray_naive.shape, batch_gray_naive.shape # Out[4]: (torch.Size([5, 5]), torch.Size([2, 5, 5]))
但现在我们也有了权重。PyTorch 将允许我们将形状相同的东西相乘,以及其中一个操作数在给定维度上的大小为 1。它还会自动附加大小为 1 的前导维度。这是一个称为广播的特性。形状为 (2, 3, 5, 5) 的 batch_t
乘以形状为 (3, 1, 1) 的 unsqueezed_weights
,得到形状为 (2, 3, 5, 5) 的张量,然后我们可以对末尾的第三个维度求和(三个通道):
# In[5]: unsqueezed_weights = weights.unsqueeze(-1).unsqueeze_(-1) img_weights = (img_t * unsqueezed_weights) batch_weights = (batch_t * unsqueezed_weights) img_gray_weighted = img_weights.sum(-3) batch_gray_weighted = batch_weights.sum(-3) batch_weights.shape, batch_t.shape, unsqueezed_weights.shape # Out[5]: (torch.Size([2, 3, 5, 5]), torch.Size([2, 3, 5, 5]), torch.Size([3, 1, 1]))
因为这很快变得混乱–出于效率考虑–PyTorch 函数 einsum
(改编自 NumPy)指定了一个索引迷你语言²,为这些乘积的和给出维度的索引名称。就像在 Python 中经常一样,广播–一种总结未命名事物的形式–使用三个点 '...'
完成;但不要太担心 einsum
,因为我们接下来不会使用它:
# In[6]: img_gray_weighted_fancy = torch.einsum('...chw,c->...hw', img_t, weights) batch_gray_weighted_fancy = torch.einsum('...chw,c->...hw', batch_t, weights) batch_gray_weighted_fancy.shape # Out[6]: torch.Size([2, 5, 5])
正如我们所看到的,涉及到相当多的簿记工作。这是容易出错的,特别是当张量的创建和使用位置在我们的代码中相距很远时。这引起了从业者的注意,因此有人建议³给维度赋予一个名称。
PyTorch 1.3 添加了命名张量作为一个实验性功能(参见pytorch.org/tutorials/intermediate/named_tensor_tutorial.html
和 pytorch.org/docs/stable/named_tensor.html
)。张量工厂函数如 tensor
和 rand
接受一个 names
参数。这些名称应该是一个字符串序列:
# In[7]: weights_named = torch.tensor([0.2126, 0.7152, 0.0722], names=['channels']) weights_named # Out[7]: tensor([0.2126, 0.7152, 0.0722], names=('channels',))
当我们已经有一个张量并想要添加名称(但不更改现有名称)时,我们可以在其上调用方法 refine_names
。类似于索引,省略号 (...
) 允许您省略任意数量的维度。使用 rename
兄弟方法,您还可以覆盖或删除(通过传入 None
)现有名称:
# In[8]: img_named = img_t.refine_names(..., 'channels', 'rows', 'columns') batch_named = batch_t.refine_names(..., 'channels', 'rows', 'columns') print("img named:", img_named.shape, img_named.names) print("batch named:", batch_named.shape, batch_named.names) # Out[8]: img named: torch.Size([3, 5, 5]) ('channels', 'rows', 'columns') batch named: torch.Size([2, 3, 5, 5]) (None, 'channels', 'rows', 'columns')
对于具有两个输入的操作,除了通常的维度检查–大小是否相同,或者一个是否为 1 且可以广播到另一个–PyTorch 现在将为我们检查名称。到目前为止,它不会自动对齐维度,因此我们需要明确地执行此操作。方法 align_as
返回一个具有缺失维度的张量,并将现有维度排列到正确的顺序:
# In[9]: weights_aligned = weights_named.align_as(img_named) weights_aligned.shape, weights_aligned.names # Out[9]: (torch.Size([3, 1, 1]), ('channels', 'rows', 'columns'))
接受维度参数的函数,如 sum
,也接受命名维度:
# In[10]: gray_named = (img_named * weights_aligned).sum('channels') gray_named.shape, gray_named.names # Out[10]: (torch.Size([5, 5]), ('rows', 'columns'))
如果我们尝试结合具有不同名称的维度,我们会收到一个错误:
gray_named = (img_named[..., :3] * weights_named).sum('channels') attempting to broadcast dims ['channels', 'rows', 'columns'] and dims ['channels']: dim 'columns' and dim 'channels' are at the same position from the right but do not match.
如果我们想在不操作命名张量的函数之外使用张量,我们需要通过将它们重命名为 None
来删除名称。以下操作使我们回到无名称维度的世界:
# In[12]: gray_plain = gray_named.rename(None) gray_plain.shape, gray_plain.names # Out[12]: (torch.Size([5, 5]), (None, None))
鉴于在撰写时此功能的实验性质,并为避免处理索引和对齐,我们将在本书的其余部分坚持使用无名称。命名张量有潜力消除许多对齐错误的来源,这些错误——如果以 PyTorch 论坛为例——可能是头痛的根源。看到它们将被广泛采用将是很有趣的。
3.5 张量元素类型
到目前为止,我们已经介绍了张量如何工作的基础知识,但我们还没有涉及可以存储在 Tensor
中的数值类型。正如我们在第 3.2 节中暗示的,使用标准的 Python 数值类型可能不是最佳选择,原因有几个:
- Python 中的数字是对象。 虽然浮点数可能只需要,例如,32 位来在计算机上表示,但 Python 会将其转换为一个完整的 Python 对象,带有引用计数等等。这个操作,称为装箱,如果我们需要存储少量数字,那么这并不是问题,但分配数百万个数字会变得非常低效。
- Python 中的列表用于对象的顺序集合。 没有为例如高效地计算两个向量的点积或将向量相加等操作定义。此外,Python 列表无法优化其内容在内存中的布局,因为它们是指向 Python 对象(任何类型,不仅仅是数字)的可索引指针集合。最后,Python 列表是一维的,虽然我们可以创建列表的列表,但这同样非常低效。
- 与优化的编译代码相比,Python 解释器速度较慢。 在大量数值数据上执行数学运算时,使用在编译、低级语言如 C 中编写的优化代码可以更快地完成。
出于这些原因,数据科学库依赖于 NumPy 或引入专用数据结构如 PyTorch 张量,它们提供了高效的低级数值数据结构实现以及相关操作,并包装在方便的高级 API 中。为了实现这一点,张量中的对象必须都是相同类型的数字,并且 PyTorch 必须跟踪这种数值类型。
3.5.1 使用 dtype 指定数值类型
张量构造函数(如 tensor
、zeros
和 ones
)的 dtype
参数指定了张量中将包含的数值数据类型。数据类型指定了张量可以保存的可能值(整数与浮点数)以及每个值的字节数。dtype
参数故意与同名的标准 NumPy 参数相似。以下是 dtype
参数可能的值列表:
torch.float32
或torch.float
:32 位浮点数torch.float64
或torch.double
:64 位,双精度浮点数torch.float16
或torch.half
:16 位,半精度浮点数torch.int8
:有符号 8 位整数torch.uint8
:无符号 8 位整数torch.int16
或torch.short
:有符号 16 位整数torch.int32
或torch.int
:有符号 32 位整数torch.int64
或torch.long
:有符号 64 位整数torch.bool
:布尔值
张量的默认数据类型是 32 位浮点数。
3.5.2 每个场合的 dtype
正如我们将在未来的章节中看到的,神经网络中发生的计算通常以 32 位浮点精度执行。更高的精度,如 64 位,不会提高模型的准确性,并且会消耗更多的内存和计算时间。16 位浮点、半精度数据类型在标准 CPU 上并不存在,但在现代 GPU 上提供。如果需要,可以切换到半精度以减少神经网络模型的占用空间,对准确性的影响很小。
张量可以用作其他张量的索引。在这种情况下,PyTorch 期望索引张量具有 64 位整数数据类型。使用整数作为参数创建张量,例如使用 torch.tensor([2, 2])
,将默认创建一个 64 位整数张量。因此,我们将大部分时间处理 float32
和 int64
。
最后,关于张量的谓词,如 points > 1.0
,会产生 bool
张量,指示每个单独元素是否满足条件。这就是数值类型的要点。
3.5.3 管理张量的 dtype 属性
为了分配正确数值类型的张量,我们可以将适当的 dtype
作为构造函数的参数指定。例如:
# In[47]: double_points = torch.ones(10, 2, dtype=torch.double) short_points = torch.tensor([[1, 2], [3, 4]], dtype=torch.short)
通过访问相应的属性,我们可以了解张量的 dtype
:
# In[48]: short_points.dtype # Out[48]: torch.int16
我们还可以使用相应的转换方法将张量创建函数的输出转换为正确的类型,例如
# In[49]: double_points = torch.zeros(10, 2).double() short_points = torch.ones(10, 2).short()
或更方便的 to
方法:
# In[50]: double_points = torch.zeros(10, 2).to(torch.double) short_points = torch.ones(10, 2).to(dtype=torch.short)
在幕后,to
检查转换是否必要,并在必要时执行。像 float
这样以 dtype
命名的转换方法是 to
的简写,但 to
方法可以接受我们将在第 3.9 节讨论的其他参数。
在操作中混合输入类型时,输入会自动转换为较大的类型。因此,如果我们想要 32 位计算,我们需要确保所有输入都是(最多)32 位:
# In[51]: points_64 = torch.rand(5, dtype=torch.double) # ❶ points_short = points_64.to(torch.short) points_64 * points_short # works from PyTorch 1.3 onwards # Out[51]: tensor([0., 0., 0., 0., 0.], dtype=torch.float64)
❶ rand 将张量元素初始化为介于 0 和 1 之间的随机数。
3.6 张量 API
到目前为止,我们知道 PyTorch 张量是什么,以及它们在幕后是如何工作的。在我们结束之前,值得看一看 PyTorch 提供的张量操作。在这里列出它们都没有太大用处。相反,我们将对 API 有一个大致了解,并在在线文档 pytorch.org/docs
中确定一些查找内容的方向。
首先,大多数张量上的操作都可以在 torch
模块中找到,并且也可以作为张量对象的方法调用。例如,我们之前遇到的 transpose
函数可以从 torch
模块中使用
# In[71]: a = torch.ones(3, 2) a_t = torch.transpose(a, 0, 1) a.shape, a_t.shape # Out[71]: (torch.Size([3, 2]), torch.Size([2, 3]))
或作为 a
张量的方法:
# In[72]: a = torch.ones(3, 2) a_t = a.transpose(0, 1) a.shape, a_t.shape # Out[72]: (torch.Size([3, 2]), torch.Size([2, 3]))
这两种形式之间没有区别;它们可以互换使用。
我们 之前提到过在线文档 (pytorch.org/docs
)。它们非常详尽且组织良好,将张量操作分成了不同的组:
创建操作 --用于构建张量的函数,如 ones
和 from_numpy
索引、切片、连接、变异操作 --用于改变张量形状、步幅或内容的函数,如 transpose
数学操作 --通过计算来操作张量内容的函数
- 逐点操作 --通过独立地对每个元素应用函数来获取新张量的函数,如
abs
和cos
- 缩减操作 --通过迭代张量计算聚合值的函数,如
mean
、std
和norm
- 比较操作 --用于在张量上评估数值谓词的函数,如
equal
和max
- 频谱操作 --用于在频域中进行转换和操作的函数,如
stft
和hamming_window
- 其他操作 --在向量上操作的特殊函数,如
cross
,或在矩阵上操作的函数,如trace
- BLAS 和 LAPACK 操作 --遵循基本线性代数子程序(BLAS)规范的函数,用于标量、向量-向量、矩阵-向量和矩阵-矩阵操作
随机抽样 --通过从概率分布中随机抽取值生成值的函数,如randn
和normal
序列化 --用于保存和加载张量的函数,如load
和save
并行性 --用于控制并行 CPU 执行线程数的函数,如set_num_threads
花些时间玩玩通用张量 API。本章提供了进行这种交互式探索所需的所有先决条件。随着我们继续阅读本书,我们还将遇到几个张量操作,从下一章开始。
3.7 张量:存储的景观
是时候更仔细地查看底层实现了。张量中的值是由torch.Storage
实例管理的连续内存块分配的。存储是一个一维数值数据数组:即,包含给定类型数字的连续内存块,例如float
(表示浮点数的 32 位)或int64
(表示整数的 64 位)。PyTorch 的Tensor
实例是这样一个Storage
实例的视图,能够使用偏移量和每维步长索引到该存储中。⁵
图 3.4 张量是Storage
实例的视图。
即使多个张量以不同方式索引数据,它们可以索引相同的存储。我们可以在图 3.4 中看到这种情况。实际上,在我们在第 3.2 节请求points[0]
时,我们得到的是另一个索引与points
张量相同存储的张量–只是不是全部,并且具有不同的维度(1D 与 2D)。然而,底层内存只分配一次,因此可以快速创建数据的备用张量视图,而不管Storage
实例管理的数据大小如何。
3.7.1 存储索引
让我们看看如何在实践中使用我们的二维点进行存储索引。给定张量的存储可以通过.storage
属性访问:
# In[17]: points = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]]) points.storage() # Out[17]: 4.0 1.0 5.0 3.0 2.0 1.0 [torch.FloatStorage of size 6]
尽管张量报告自身具有三行和两列,但底层存储是一个大小为 6 的连续数组。在这种意义上,张量只知道如何将一对索引转换为存储中的位置。
我们也可以手动索引到存储中。例如:
# In[18]: points_storage = points.storage() points_storage[0] # Out[18]: 4.0 # In[19]: points.storage()[1] # Out[19]: 1.0
PyTorch 深度学习(GPT 重译)(一)(4)https://developer.aliyun.com/article/1485201