【Deep Learning with PyTorch 中文手册】(七) Tensor fundamentals

Tensor fundamentals

读者已经了解张量是PyTorch中的基本数据结构。所谓的张量就是数组,即一种存储数字集合的数据结构,这些数字可通过单个或多个索引访问。请读者观察一下list中的索引,以便将其与张量中的索引进行比较。以下展示了一个包含三个数字的list。

>>> a = [1.0, 2.0, 1.0]

读者可以使用对应的从0开始的索引来访问列表的第一个元素:

>>> a[0]1.0>>> a[2] = 3.0>>> a[1.0, 2.0, 3.0]

对于简单的Python程序来说,使用Python list存储并处理矢量(例如2D直线的坐标)是很常见的。但是,由于以下几个原因,这种做法可能不是最佳选择:

  • Python中的数值是成熟的对象。浮点数可能只需要32位就可以在计算机上表示,而Python将它们封装在功能齐全的Python对象中,并带有引用计数等功能。如果读者仅需要存储少量的数值那问题不大,但是分配数百万个这样的数值就会效率低下。

  • Python中的lists适用于对象的有序集合。没有定义用于有效地获取两个向量的点积或向量求和的操作。此外,Python lists无法优化其在内存中的布局,因为它们是指向Python对象(任何类型,而不仅仅是数值)的可索引指针集合。最后,Python lists是一维的,尽管读者可以创建包含lists对象的lists,但是这种做法效率低下。

  • 与经过优化的编译代码相比,Python解释器速度较慢。使用编译型低级语言(例如C)编写的经过优化后的代码,能够更快地对大量数值执行数学运算。

由于这些原因,数据科学库依赖于NumPy或引入专用数据结构(例如PyTorch张量),这些数据结构提供了数值数据结构及其相关操作的高效率低级别实现并将其封装在高级API里。

张量可以表示许多类型的数据,包括图像、时间序列、音频甚至句子。通过定义张量上的操作(本章中将探讨其中的一些操作),即使使用高级(不是特别快速)的语言(例如Python),也可以同时高效地切片和操作数据。

现在读者可以构建第一个PyTorch张量,具体地看看它的结构。这个张量有一列三行:

>>> import torch>>> a = torch.ones(3)>>> atensor([1., 1., 1.])>>> a[1]tensor(1.)>>> float(a[1])1.0>>> a[2] = 2.0>>> atensor([1., 1., 2.])

现在看看我们都执行了什么操作。导入torch模块后调用了一个函数,该函数创建size为3的(一维)张量,并填充值为1.0。我们可以使用基于0的索引来访问元素,也可以为其分配新的值。

尽管从表面上看,此示例与list并没有太大区别,但实际上情况完全不同。Python list或tuple中的数值是在内存中单独分配的Python对象的集合,如下图左侧所示。与之不同的是,PyTorch张量或NumPy数组是(通常)占用连续的内存块,包含未封装的C数值类型而不是Python对象。在这种情况下,32位浮点数(4字节)存储方式如下图右侧所示。因此,具有一百万个浮点数的一维张量需要存储400万个连续字节,再加上少量的元数据开销(维度,数据类型等)。

假设我们要管理一个包含2D坐标的list,以表示一个几何对象,例如三角形。该示例与深度学习相关不大,但易于理解。我们可以使用一维张量,将xs存储在偶数索引中并且将ys存储在奇数索引中,而不是在Python list中将坐标作为数值来使用,就像这样:

>>> points = torch.zeros(6) #torch.zeros是一种定义相同尺寸数组的方法>>> points[0] = 1.0         #重新赋值>>> points[1] = 4.0>>> points[2] = 2.0>>> points[3] = 1.0>>> points[4] = 3.0>>> points[5] = 5.0

我们还可以传入一个Python list来构造对象:

>>> points = torch.tensor([1.0, 4.0, 2.0, 1.0, 3.0, 5.0])>>> pointstensor([1., 4., 2., 1., 3., 5.])

获取第一个点的坐标:

>>> float(points[0]), float(points[1])(1.0, 4.0)

这项技术只能说合格,尽管让第一个索引引用单个2D点而不是点坐标的做法是可行的。为了改进,我们可以使用2D张量:

>>> points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])>>> pointstensor([[1., 4.], [2., 1.], [3., 5.]])

我们传入一个包含list对象的list来构造张量,现在可以获取这个张量的形状:

>>> points.shapetorch.Size([3, 2])

它会告知我们张量每个维度的大小。我们还可以使用0或1来初始化张量,仅传入一个包含维度信息的tuple:

>>> points = torch.zeros(3, 2)>>> pointstensor([[0., 0.], [0., 0.], [0., 0.]])

现在我们可以使用两个索引来访问单个元素:

>>> points = torch.FloatTensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])>>> pointstensor([[1., 4.], [2., 1.], [3., 5.]])>>> points[0, 1]tensor(4.)

这段代码返回数据集中第1个点的y坐标。我们也可以按照之前的做法来获取第一个点的2D坐标:

>>> points[0]tensor([1., 4.])

请注意,输出结果是另一个张量。它是尺寸大小为2的一维张量,包含points张量第一行中的值。此输出是否意味着分配了新的内存块,将值复制到其中并封装进一个新的张量,将新的内存返回了?答案是No,因为该过程效率不高,尤其是处理数百万个点的时候。相反,我们获得的新张量和points共享同一块内存,当然仅限于第一行(因为我们调用的是第一行)。

(0)

相关推荐