2.5 PyTorch中张量的创建和维度的操作
2.5.1 张量的数据类型
前面已经介绍过,张量的运算是深度学习的基本操作。因此,深度学习框架的重要功能之一就是支持张量的定义和张量的运算。为此,PyTorch提供了专门的torch.Tensor类,在这个类中,根据张量数据的格式和需要使用张量的设备,为张量开辟了不同的存储区域,对张量的元素进行存储。
PyTorch中的张量一共支持9种数据类型,每种数据类型都对应CPU和GPU的两种子类型,整体的类型如表2.1所示(表格来源于PyTorch官网)。表中第1列代表张量实际的数据类型。PyTorch类型代表的是在PyTorch中用什么类型来指代这种数据类型。如果一行中有两个不同名字的PyTorch类型,则一种类型是另外一种类型的别名。举例来说,用户可以通过运行“torch.float32 is torch.float”来判断这两种类型是否指向同一种类型,结果返回True,代表它们指向的是一个类型。除了数据类型,张量数据存储的位置也十分重要,表2.1中第3、4列分别代表张量存储的位置在CPU上和在GPU上时张量的具体数值类型。假如要获得一个张量具体的类型,可以直接访问张量的dtype属性,如果想要进一步获取张量的存储位置和数据类型,可以通过调用张量的type方法来同时获取存储位置和数据类型的值。需要提到的一点是,现阶段PyTorch并不支持复数类型,如果有用到的场合(如使用torch.fft/torch.ifft进行快速傅里叶变换),需要使用张量的两个分量来分别模拟复数的实部和虚部。在PyTorch的不同类型之间,可以通过调用to方法进行转换,该方法传入的参数是转换目标类型(参考代码2.4)。
表2.1 PyTorch支持的张量数据类型
代码2.4 Python列表和Numpy数组转换为PyTorch张量。
2.5.2 张量的创建方式
在PyTorch中创建张量主要有四种方式,分别阐述如下。
1.通过torch.tensor函数创建张量
第一种是通过torch.tensor函数来进行转换。如果预先有数据(包括列表和Numpy数组),可以通过这个方法来进行转换。我们来看一下代码2.4(这里为了方便理解,用#符号加了注释,读者在运行时可以删除#符号及其后面的内容)。在这里,首先导入了torch包和numpy包,然后把Python的列表转换为一个PyTorch张量。接下来通过查看张量数据类型可以看到,这个转换张量的数据类型为int64,即表2.1中的64位带符号整数。在torch.tensor函数中,可以通过传入dtype参数来指定生成的张量的数据类型,函数内部会自动做数据类型的转换。当传入的dtype为torch.float32时,可以看到输出的张量分量多了一个小数点,而且当查看dtype的值时,可以发现这个值变成了torch.float32,即32位单精度浮点数。进一步看,一些能够转换为Python列表的类型也可以转换为PyTorch张量,比如,range函数产生的迭代器可以被转换为Python列表,从而进一步转换为张量。在后面的几个例子里,可以看到PyTorch能转换Numpy数组为张量,而且保证张量的数据类型不发生改变。值得注意的是最后两个例子,当传入Python的浮点数的列表时,PyTorch默认把浮点数转换为单精度浮点数,Numpy默认把浮点数转换为双精度浮点数(Python语言没有单精度浮点数,默认的float类型是双精度浮点数),这样就造成了最后数据类型的差异,即通过Numpy中转后输出的PyTorch张量使用的是双精度浮点数,而直接通过torch.tensor函数转换的张量使用的是单精度浮点数。PyTorch创建张量支持列表的嵌套(即列表的列表),用这个方法可以创建矩阵和其他多维张量(注意子列表的大小要一致,不然会报错)。
2.通过PyTorch内置的函数创建张量
创建张量的第二种方式是使用PyTorch内置的函数创建张量,通过指定张量的形状,返回给定形状的张量。这里介绍几个常用的函数,具体如代码2.5所示。
代码2.5 指定形状生成张量。
代码中,torch.rand函数用于元素生成服从[0,1)上的均匀分布的任意形状的张量,其中张量的形状由输入的参数决定。torch.randn函数能够生成任意形状的张量,张量的形状由函数的传入参数决定,张量的元素服从标准正态分布,即平均值为0、标准差为1的正态分布。torch.zeros生成元素全为0的张量,形状由传入函数的参数决定。torch.ones生成元素全为1的张量,形状由传入函数的参数决定。torch.eye生成单位矩阵,矩阵的大小由输入参数决定。torch.randint生成一定形状的均匀分布的整数张量,整数的上限和下限,以及输出的张量由函数的参数决定。
3.通过已知张量创建形状相同的张量
张量创建的第三种方式是已知某一张量,创建和已知张量相同形状的张量。另一个张量虽然和原始张量的形状相同,但是里面填充的元素可能不同,具体如代码2.6所示。
代码2.6 通过已知张量生成形状相同的张量。
从上述代码可以看到,当给定一个张量以后,可以任意生成一个元素全为0、全为1、元素服从[0,1)上的均匀分布和元素服从标准正态分布的张量,而且新的张量和给定张量的形状相同。
4.通过已知张量创建形状不同但数据类型相同的张量
创建张量的最后一种方法是已知张量的数据类型,创建一个形状不同但数据类型相同的新张量。这种方法一般较少用到,但在一定条件下,比如,写设备无关(Device-agnostic)的代码有一定的作用。相关的应用可以参考代码2.7。
代码2.7 已知张量生成相同类型的张量(接代码2.6)。
其中,第一个方法是new_tensor方法,具体用法和torch.tensor方法类似。我们可以看到,在这里新的张量类型不再是torch.int64,而是和前面创建的张量的类型一样,即torch.float32。和前面一样,可以用new_zeros方法生成和原始张量类型相同且元素全为0的张量,用new_ones方法生成和原始张量类型相同且元素全为1的张量。另外需要注意的一点是没有类似于new_rand和new_randn的函数,所以不能用这种方法生成随机元素填充的张量。
2.5.3 张量的存储设备
前面介绍过,PyTorch张量可以在两种设备上存储,即CPU和GPU。在没有指定设备的时候,PyTorch会默认存储张量到CPU上。如果想转移张量到GPU上,则需要指定张量转移到GPU设备。一般来说,GPU设备在PyTorch上以cuda:0,cuda:1……指定,其中数字代表的是GPU设备的编号,如果一台机器上有N个GPU,则GPU设备的编号分别为0,1,…,N-1。GPU设备的详细信息可以使用nvidia-smi命令查看,如图2.2所示。对于前面的1~3种张量初始化方法,可以在创建张量的函数的参数中指定device参数(如device="cpu",或者device="cuda:0",device="cuda:1",…)来指定张量存储的位置,如代码2.8所示。
图2.2 nvidia-smi命令显示结果(图中是两张Tesla V00显卡,编号分别为0和1)
代码2.8 PyTorch在不同设备上的张量。
通过访问张量的device属性可以获取张量所在的设备。如果想将一个设备上的张量转移到另外一个设备上,有几种方法。首先可以使用cpu方法把张量转移到CPU上,其次可以使用cuda方法把张量转移到GPU上,其中需要传入具体的GPU的设备编号。也可以使用to方法把张量从一个设备转移到另一个设备,该方法的参数是目标设备的名称(可以是字符串名称,也可以是torch.device实例)。需要注意的一点是,两个或多个张量之间的运算只有在相同设备上才能进行(都在CPU上或者在同一个GPU上),否则会报错。
2.5.4 和张量维度相关的方法
在深度学习中经常会用到一些方法来获取张量的维度数目,以及某一维度的具体大小,或者对张量的某些维度进行操作。下面介绍和张量维度相关的方法。
首先介绍获取张量形状的方法,具体如代码2.9所示,主要包含获取张量的某一特定的维度元素的数目和张量包含的所有元素数目的一些方法。
代码2.9 PyTorch张量形状相关的一些函数。
通过调用张量的ndimension方法,可以获取张量的维度的数目;调用张量的nelement方法可以得到张量的元素的数目;调用张量的size方法或者shape属性,可以得到张量的形状(其类型是torch.Size,可通过索引获得某个维度的大小),也可以通过在size方法里传入具体的维度,来获得该维度的大小(可通过传入-1、-2等数字来代表倒数第一个和倒数第二个维度)。
另一个有用的函数是改变张量的形状,这里主要介绍两个方法:view方法和reshape方法。
(1)view方法
view方法作用于原来的张量,传入改变新张量的形状,新张量的总元素数目和原来张量的元素数目相同。另外,假如新的张量有N维,我们可以指定其他N-1维的具体大小,留下一个维度大小指定为-1,PyTorch会自动计算那个维度的大小(注意N-1维的乘积要能被原来张量的元素数目整除,否则会报错)。view方法并不改变张量底层的数据,只是改变维度步长的信息(关于张量的存储形式,可以参考1.3节与张量相关的介绍),如果需要重新生成一个张量(复制底层数据),则需要调用contiguous方法,调用这个方法的时候,如果张量的形状和初始维度形状的信息兼容(兼容的定义是新张量的两个连续维度的乘积等于原来张量的某一维度),则会返回当前张量,否则会根据当前维度形状信息和数据重新生成一个形状信息兼容的张量,并且返回新的张量。为了验证这点,可以使用data_ptr方法来获取当前数据的指针,如果指针发生变化,则张量的数据得到重新分配。可以看到,当张量为长度12的向量时,调用view方法变成3×4和4×3不影响数据的地址。但是,当引入transpose方法的时候(这个方法用于交换两个维度的步长,相当于在这两个维度做了转置,具有同样作用的方法有permute,用于交换多个维度的步长),返回的新张量虽然还是共享底层的数据指针,但是维度和步长不再兼容(即形状信息不再兼容),需要通过调用contiguous方法,才能生成一个新的张量。
(2)reshape方法
大多数情况下,PyTorch的张量在改变形状的时候不需要调用contiguous,在某些情况下,如调用view方法以后再调用transpose方法和一个与初始张量形状不兼容的view方法,会在transpose方法后加contiguous方法的调用。在这种情况下,可以直接调用reshape方法,该方法会在形状信息不兼容的时候自动生成一个新的张量,并自动复制原始张量的数据(相当于连续调用view方法和contiguous方法)。
2.5.5 张量的索引和切片
PyTorch的张量还支持类似Numpy的索引(Indexing)和切片(Slicing)操作,具体如代码2.10所示。
代码2.10 PyTorch张量的切片和索引。
上述操作基本上都基于Python的索引操作符,即[],通过给定不同的参数,实现构造新的张量。和Python一样,PyTorch的编号从0开始,同样可以使用[i:j]的方式来获取张量切片(从i开始,不包含j)。索引和切片后的张量,以及初始的张量共享一个内存区域,如果要在不改变初始张量的情况下改变索引或切片后张量的值,可以使用clone方法得到索引或切片后张量的一份副本,然后进行赋值。索引操作还支持掩码选择,可以传入一个和原来张量形状相同的布尔型张量,返回初始张量对应布尔型张量值为True(或者8位无符号整数1)位置的元素。在这种情况下,索引返回的是一个向量(一维张量),这个张量的每个元素对应的是布尔型张量值为True(或者8位无符号整数1)位置的元素。