深度强化学习实践(原书第2版)
上QQ阅读APP看书,第一时间看更新

3.1 张量

张量是所有DL工具包的基本组成部分。名字听起来很神秘,但本质上张量就是一个多维数组。用数学知识来类比,单个数字就像点,是零维的,向量就像线段,是一维的,矩阵则是二维的对象。三维数字的集合可以用平行六面体表示,但它们不像矩阵一样有特定的名称。我们可以用“张量”来表示高维集合,如图3.1所示。

054-01

图3.1 从一个数字到n维张量

需要注意的是,在DL中所使用的张量和在张量演算或张量代数中所用的张量相比,只在部分层面上相关。在DL中,张量是任意多维的数组,但在数学中,张量是向量空间之间的映射,在某些情况下可以表示为多维数组,但背后具有更多的语义信息。数学家通常会对任何使用公认数学术语来命名不同事物的人皱眉,因此要当心!

3.1.1 创建张量

如果你熟悉NumPy库,那你应该知道其主要目的是以通用的方式处理多维数组。在NumPy中,这样的数组没被称为张量,但事实上,它们就是张量。张量在科学计算中被广泛用作数据的通用存储方式。例如,彩图会被编码成具有宽度、高度和色值的三维张量。

除了维度之外,元素类型也是张量的特征之一。PyTorch支持八种类型,包括三种浮点类型(16位、32位和64位)和五种整数类型(有符号8位、无符号8位、16位、32位和64位)。不同类型的张量用不同的类表示,其中最常用的是torch.FloatTensor(对应32位浮点类型)、torch.ByteTensor(无符号8位整数)、torch.LongTensor(有符号64位整数)。其余的可以在PyTorch的文档中查到。

有三种方法可以在PyTorch中创建张量:

  • 通过调用所需类型的构造函数。
  • 通过将NumPy数组或Python列表转换为张量。在这种情况下,类型将从数组的类型中获取。
  • 通过要求PyTorch创建带有特定数据的张量。例如,可以使用torch.zeros()函数创建一个全为零的张量。

下面是上述方法的实现示例:

054-02

在这里,我们导入了PyTorch和NumPy,并创建未初始化的3×2的张量。默认情况下,PyTorch会为张量分配内存,但不进行初始化。要清除张量的内容,需要使用以下操作:

055-01

张量有两种类型的操作:inplace和functional。inplace操作需在函数名称后附加一个下划线,作用于张量的内容。然后,会返回对象本身。functional操作是创建一个张量的副本,对其副本进行修改,而原始张量保持不变。从性能和内存角度来看,inplace方式通常更为高效。

通过其构造函数创建张量的另一种方法是提供Python可迭代对象(例如列表或元组),它将被用作新创建张量的内容:

055-02

下面的代码用NumPy创建了一个全零张量:

055-03

torch.tensor方法接受NumPy数组作为参数,并创建一个适当形状的张量。在前面的示例中,我们创建了一个由零初始化的NumPy数组,默认情况下,该数组创建一个double(64位浮点数)数组。因此,生成的张量具有DoubleTensor类型(在前面的示例中使用dtype值显示)。在DL中,通常不需要双精度,因为使用双精度会增加内存和性能开销。通常的做法是使用32位浮点类型,甚至使用16位浮点类型,这已经能够满足需求了。要创建这样的张量,需要明确指定NumPy数组的类型:

055-04

作为可选项,可以在dtype参数中将所需张量的类型提供给torch.tensor函数。但是,请小心,因为此参数期望传入PyTorch类型规范,而不是NumPy类型规范。PyTorch类型保存在torch包中,例如torch.float32torch.uint8

055-05

007-03兼容性说明

0.4.0版本中添加了torch.tensor()方法和显式PyTorch类型规范,这是朝着简化张量创建迈出的一步。在以前的版本中,推荐使用torch.from_numpy()函数来转换NumPy数组,但是在处理Python列表和NumPy数组的组合时遇到了问题。from_numpy()函数仍可实现后向兼容性,但不推荐使用此函数,而推荐使用更灵活的torch.tensor()方法。

3.1.2 零维张量

从0.4.0版本开始,PyTorch已经支持了与标量相对应的零维张量(在图3.1的左侧)。这种张量可能是某些操作(例如对张量中的所有值求和)的结果。这种情况以前是通过创建维度为1的张量(向量)来处理的。

该解决方案是有效的,但它并不简单,因为需要额外的索引才能访问值。现在,已经支持零维张量并由合适的函数返回,并且可以通过torch.tensor()函数创建。为了访问这种张量的实际Python值,可以使用特殊的item()方法:

056-01

3.1.3 张量操作

你可以对张量执行很多操作,因为操作太多,无法一一列出。通常,在PyTorch的官方文档(http://pytorch.org/docs/)中搜索就足够使用了。需要指出的是,除了前文所讨论的inplace和functional(即带有下划线的函数和不带下划线的函数,例如abs()abs_()),还有两个地方可以查找操作:torch包和张量类。在第一种情况下,函数通常接受张量作为参数。在第二个中,函数作用于张量上。

在大多数情况下,张量操作试图和NumPy中相应的操作等效,因此,如果NumPy中存在一些不是非常专有的函数,那么PyTorch中也可能会有,例如torch.stack()torch.transpose()torch.cat()

3.1.4 GPU张量

PyTorch透明地支持CUDA GPU,这意味着所有操作都有两个版本(CPU和GPU)供自动选择。使用哪个版本操作根据所操作的张量类型决定。

我提到的每种张量类型都是针对CPU的,并且具有与之对应的GPU类型。唯一的区别是GPU张量在torch.cuda包中,而不是在torch中。例如,torch.FloatTensor是在CPU内存中的32位浮点张量,而torch.cuda.FloatTensor是在GPU中的32位浮点张量。

为了从CPU转换到GPU,有一个to(device)的张量方法,可以创建张量的副本到指定设备(可以是CPU或GPU)。如果张量已经在相应的设备上,那么什么也不会发生,并且返回原始张量。可以用不同的方式指定设备类型。首先,可以仅传递设备的字符串名称,对于CPU内存为“cpu”,对于GPU可用“cuda”。GPU设备可以在冒号之后指定一个可选设备的索引。例如,系统中的第二个GPU可以用“cuda:1”寻址(索引从零开始)。

to()方法中指定设备的另一种更为有效的方法是使用torch.device类,该类接受设备名称和可选索引。它有device属性,所以可以访问张量当前所在的设备。

057-01

上面的代码在CPU上创建了一个张量,然后将其复制到GPU中。两个副本均可用于计算,并且所有GPU特有的机制对用户都是透明的:

057-02

007-03兼容性说明

to()方法和torch.device类是在0.4.0中引入的。在以前的版本中,CPU和GPU之间的相互复制是分开的,分别通过单独的方法CPU()cuda()来执行,需要添加额外的代码行来显式地将张量转换成它们的CUDA版本。在最新的版本中,你可以在程序开始时创建一个想要的torch.device对象,然后在创建的每一个张量上使用to(device)。张量中的旧方法,cpu()cuda()仍然存在,如果想确保一个张量在CPU或GPU中,而不管它的原始位置,使用旧方法可能会更方便。