# 04 | Tensor:PyTorch中最基础的计算单元 在上节课中,我们一起学习了NumPy的主要使用方法和技巧,有了NumPy我们可以很好地处理各种类型的数据。而在深度学习中,数据的组织则更进一步,从数据的组织,到模型内部的参数,都是通过一种叫做**张量**的数据结构进行表示和处理。 今天我们就来一块儿了解一下张量(Tensor),学习一下Tensor的常用操作。 ## 什么是Tensor Tensor是深度学习框架中极为基础的概念,也是PyTroch、TensorFlow中最重要的知识点之一,它是一种数据的存储和处理结构。 回忆一下我们目前知道的几种数据表示: 1. 标量,也称Scalar,是一个只有大小,没有方向的量,比如1.8、e、10等。 2. 向量,也称Vector,是一个有大小也有方向的量,比如(1,2,3,4)等。 3. 矩阵,也称Matrix,是多个向量合并在一起得到的量,比如\[(1,2,3),(4,5,6)\]等。 为了帮助你更好理解标量、向量和矩阵,我特意准备了一张示意图,你可以结合图片理解。 ![](https://static001.geekbang.org/resource/image/a8/85/a85883cc14171ff5361346dd65776085.jpg?wh=1920x1090) 不难发现,几种数据表示其实都是有着联系的,标量可以组合成向量,向量可以组合成矩阵。那么,我们可否将它们看作是一种数据形式呢? 答案是可以的,这种统一的数据形式,在PyTorch中我们称之为**张量(Tensor)**。从标量、向量和矩阵的关系来看,你可能会觉得它们就是不同**“维度”**的Tensor,这个说法对,也不全对。 说它不全对是因为在Tensor的概念中,我们更愿意使用Rank(秩)来表示这种**“维度”**,比如标量,就是Rank为0阶的Tensor;向量就是Rank为1阶的Tensor;矩阵就是Rank为2阶的Tensor。也有Rank大于2的Tensor。当然啦,你如果说维度其实也没什么错误,平时很多人也都这么叫。 说完Tensor的含义,我们一起看一下Tensor的类型,以及如何创建Tensor。 ## Tensor的类型、创建及转换 在不同的深度学习框架下,Tensor呈现的特点大同小异,我们使用它的方法也差不多。这节课我们就以PyTorch中的使用方法为例进行学习。 ### Tensor的类型 在PyTorch中,Tensor支持的数据类型有很多种,这里列举较为常用的几种格式: ![图片](https://static001.geekbang.org/resource/image/e6/08/e6af6a3b2172ee08db8c564146ae2108.jpg?wh=1680x933) 一般来说,torch.float32、torch.float64、torch.uint8和torch.int64用得相对较多一些,但是也不是绝对,还是要根据实际情况进行选择。这里你有个印象就行,后面课程用到时我还会进一步讲解。 ### Tensor的创建 PyTorch对于Tensor的操作已经非常友好了,你可以通过多种不同的方式创建一个任意形状的Tensor,而且每种方式都很简便,我们一起来看一下。 #### 直接创建 首先来看直接创建的方法,这也是最简单创建的方法。我们需要用到下面的torch.tensor函数直接创建。 ```python torch.tensor(data, dtype=None, device=None,requires_grad=False) ``` 结合代码,我们看看其中的参数是什么含义。 我们从左往右依次来看,首先是data,也就是我们要传入模型的数据。PyTorch支持通过list、 tuple、numpy array、scalar等多种类型进行数据传入,并转换为tensor。 接着是dtype,它声明了你需要返回一个怎样类型的Tensor,具体类型可以参考前面表格里列举的Tensor的8种类型。 然后是device,这个参数指定了数据要返回到的设备,目前暂时不需要关注,缺省即可。 最后一个参数是requires\_grad,用于说明当前量是否需要在计算中保留对应的梯度信息。在PyTorch中,只有当一个Tensor设置requires\_grad为True的情况下,才会对这个Tensor以及由这个Tensor计算出来的其他Tensor进行求导,然后将导数值存在Tensor的grad属性中,便于优化器来更新参数。 所以,你需要注意的是,把requires\_grad设置成true或者false要灵活处理。**如果是训练过程就要设置为true,目的是方便求导、更新参数。而到了验证或者测试过程,我们的目的是检查当前模型的泛化能力,那就要把requires\_grad设置成Fasle,避免这个参数根据loss自动更新**。 #### 从NumPy中创建 还记得之前的课程中,我们一同学习了NumPy的使用,在实际应用中,我们在处理数据的阶段多使用的是NumPy,而数据处理好之后想要传入PyTorch的深度学习模型中,则需要借助Tensor,所以PyTorch提供了一个从NumPy转到Tensor的语句: ```python torch.from_numpy(ndarry) ``` 有时候我们在开发模型的过程中,需要用到一些特定形式的矩阵Tensor,比如全是0的,或者全是1的。这时我们就可以用这个方法创建,比如说,先生成一个全是0的NumPy数组,然后转换成Tensor。但是这样也挺麻烦的,因为这意味着你要引入更多的包(NumPy),也会使用更多的代码,这会增加出错的可能性。 不过你别担心,PyTorch内部已经提供了更为简便的方法,我们接着往下看。 #### 创建特殊形式的Tensor 我们一块来看一下后面的几个常用函数,它们都是在PyTorch模型内部使用的。 * 创建零矩阵Tensor:零矩阵顾名思义,就是所有的元素都为0的矩阵。 ```python torch.zeros(*size, dtype=None...) ``` 其中,我们用得比较多的就是size参数和dtype参数。size定义输出张量形状的整数序列。 这里你可能注意到了,在函数参数列表中我加入了省略号,这意味着torch.zeros的参数有很多。不过。咱们现在是介绍零矩阵的概念,形状相对来说更重要。其他的参数(比如前面提到的requires\_grad参数)与此无关,现阶段我们暂时不关注。 * 创建单位矩阵Tensor:单位矩阵是指主对角线上的元素都为1的矩阵。 ```python torch.eye(size, dtype=None...) ``` * 创建全一矩阵Tensor:全一矩阵顾名思义,就是所有的元素都为1的矩阵。 ```python torch.ones(size, dtype=None...) ``` * 创建随机矩阵Tensor:在PyTorch中有几种较为经常使用的随机矩阵创建方式,分别如下。 ```python torch.rand(size) torch.randn(size) torch.normal(mean, std, size) torch.randint(low, high, size) ``` 这些方式各自有不同的用法,你可以根据自己的需要灵活使用。 * torch.rand用于生成数据类型为浮点型且维度指定的随机Tensor,随机生成的浮点数据在 **0~1 区间均匀分布**。 * torch.randn用于生成数据类型为浮点型且维度指定的随机Tensor,随机生成的浮点数的取值满足**均值为 0、方差为 1 的标准正态分布**。 * torch.normal用于生成数据类型为浮点型且维度指定的随机Tensor,**可以指定均值和标准差**。 * torch.randint用于生成随机整数的Tensor,其内部填充的是在\[low,high)均匀生成的随机整数。 ### Tensor的转换 在实际项目中,我们接触到的数据类型有很多,比如Int、list、NumPy等。为了让数据在各个阶段畅通无阻,不同数据类型与Tensor之间的转换就非常重要了。接下来我们一起来看看int、list、NumPy是如何与Tensor互相转换的。 * Int与Tensor的转换: ```python a = torch.tensor(1) b = a.item() ``` 我们通过torch.Tensor将一个数字(或者标量)转换为Tensor,又通过item()函数,将Tensor转换为数字(标量),item()函数的作用就是将Tensor转换为一个python number。 * list与tensor的转换: ```python a = [1, 2, 3] b = torch.tensor(a) c = b.numpy().tolist() ``` 在这里对于一个list a,我们仍旧直接使用torch.Tensor,就可以将其转换为Tensor了。而还原回来的过程要多一步,需要我们先将Tensor转为NumPy结构,之后再使用tolist()函数得到list。 * NumPy与Tensor的转换: 有了前面两个例子,你是否能想到NumPy怎么转换为Tensor么?对,我们仍旧torch.Tensor即可,是不是特别方便。 * CPU与GPU的Tensor之间的转换: ```python CPU->GPU: data.cuda() GPU->CPU: data.cpu() ``` ## Tensor的常用操作 好,刚才我们一起了解了Tensor的类型,如何创建Tensor,以及如何实现Tensor和一些常见的数据类型之间的相互转换。其实Tensor还有一些比较常用的功能,比如获取形状、维度转换、形状变换以及增减维度,接下来我们一起来看看这些功能。 ### 获取形状 在深度学习网络的设计中,我们需要时刻对Tensor的情况做到了如指掌,其中就包括获取Tensor的形式、形状等。 为了得到Tensor的形状,我们可以使用shape或size来获取。两者的不同之处在于,shape是torch.tensor的一个属性,而size()则是一个torch.tensor拥有的方法。 ```python >>> a=torch.zeros(2, 3, 5) >>> a.shape torch.Size([2, 3, 5]) >>> a.size() torch.Size([2, 3, 5]) ``` ![图片](https://static001.geekbang.org/resource/image/8d/24/8d5a9954126f8a737e58ba2f8afdc624.jpg?wh=1920x1080) 知道了Tensor的形状,我们就能知道这个Tensor所包含的元素的数量了。具体的计算方法就是直接将所有维度的大小相乘,比如上面的Tensor a所含有的元素的个数为2_3_5=30个。这样似乎有点麻烦,我们在PyTorch中可以使用numel()函数直接统计元素数量。 ```python >>> a.numel() 30 ``` ### 矩阵转秩(维度转换) 在PyTorch中有两个函数,分别是permute()和transpose()可以用来实现矩阵的转秩,或者说交换不同维度的数据。比如在调整卷积层的尺寸、修改channel的顺序、变换全连接层的大小的时候,我们就要用到它们。 其中,用permute函数可以对任意高维矩阵进行转置,但只有 tensor.permute() 这个调用方式,我们先看一下代码: ```python >>> x = torch.rand(2,3,5) >>> x.shape torch.Size([2, 3, 5]) >>> x = x.permute(2,1,0) >>> x.shape torch.Size([5, 3, 2]) ``` ![图片](https://static001.geekbang.org/resource/image/02/84/025985c8635f3896d45d15e1ea381c84.jpg?wh=1920x1080) 有没有发现,原来的Tensor的形状是\[2,3,5\],我们在permute中分别写入原来索引位置的新位置,x.permute(2,1,0),2表示原来第二个维度现在放在了第零个维度;同理1表示原来第一个维度仍旧在第一个维度;0表示原来第0个维度放在了现在的第2个维度,形状就变成了\[5,3,2\] 而另外一个函数transpose,不同于permute,它每次只能转换两个维度,或者说交换两个维度的数据。我们还是来看一下代码: ```python >>> x.shape torch.Size([2, 3, 4]) >>> x = x.transpose(1,0) >>> x.shape torch.Size([3, 2, 4]) ``` 需要注意的是,经过了transpose或者permute处理之后的数据,变得不再连续了,什么意思呢? 还是接着刚才的例子说,我们使用torch.rand(2,3,4)得到的tensor,在内存中是连续的,但是经过transpose或者permute之后呢,比如transpose(1,0),内存虽然没有变化,但是我们得到的数据“看上去”是第0和第1维的数据发生了交换,现在的第0维是原来的第1维,所以Tensor都会变得不再连续。 那你可能会问了,不连续就不连续呗,好像也没啥影响吧?这么想你就草率了,我们继续来看看Tensor的形状变换,学完以后你就知道Tensor不连续的后果了。 ### 形状变换 在PyTorch中有两种常用的改变形状的函数,分别是view和reshape。我们先来看一下view。 ```python >>> x = torch.randn(4, 4) >>> x.shape torch.Size([4, 4]) >>> x = x.view(2,8) >>> x.shape torch.Size([2, 8]) ``` 我们先声明了一个\[4, 4\]大小的Tensor,然后通过view函数,将其修改为\[2, 8\]形状的Tensor。我们还是继续刚才的x,再进行一步操作,代码如下: ```python >>> x = x.permute(1,0) >>> x.shape torch.Size([8, 2]) >>> x.view(4, 4) Traceback (most recent call last):   File "", line 1, in RuntimeError: view size is not compatible with input tensor's size and stride (at least one dimension spans across two contiguous subspaces). Use .reshape(...) instead. ``` 结合代码可以看到,利用permute,我们将第0和第1维度的数据进行了变换,得到了\[8, 2\]形状的Tensor,在这个新Tensor上进行view操作,忽然就报错了,为什么呢?其实就是因为view不能处理内存不连续Tensor的结构。 那这时候要怎么办呢?我们可以使用另一个函数,reshape: ```python >>> x = x.reshape(4, 4) >>> x.shape torch.Size([4, 4]) ``` 这样问题就迎刃而解了。其实reshape相当于进行了两步操作,先把Tensor在内存中捋顺了,然后再进行view操作。 ### 增减维度 有时候我们需要对Tensor增加或者删除某些维度,比如删除或者增加图片的几个通道。PyTorch提供了squeeze()和unsqueeze()函数解决这个问题。 我们先来看squeeze()。如果dim指定的维度的值为1,则将该维度删除,若指定的维度值不为1,则返回原来的Tensor。为了方便你理解,我还是结合例子来讲解。 ```python >>> x = torch.rand(2,1,3) >>> x.shape torch.Size([2, 1, 3]) >>> y = x.squeeze(1) >>> y.shape torch.Size([2, 3]) >>> z = y.squeeze(1) >>> z.shape torch.Size([2, 3]) ``` 结合代码我们可以看到,我们新建了一个维度为\[2, 1, 3\]的Tensor,然后将第1维度的数据删除,得到y,squeeze执行成功是因为第1维度的大小为1。然而在y上我们打算进一步删除第1维度的时候,就会发现删除失败了,这是因为y此刻的第1维度的大小为3,suqeeze不能删除。 unsqueeze():这个函数主要是对数据维度进行扩充。给指定位置加上维数为1的维度,我们同样结合代码例子来看看。 ```python >>> x = torch.rand(2,1,3) >>> y = x.unsqueeze(2) >>> y.shape torch.Size([2, 1, 1, 3]) ``` 这里我们新建了一个维度为\[2, 1, 3\]的Tensor,然后在第2维度插入一个维度,这样就得到了一个\[2,1,1,3\]大小的tensor。 ## 小结 之前我们学习了NumPy相关的操作,如果把NumPy和Tensor做对比,就不难发现它们之间有很多共通的内容,共性就是两者都是数据的表示形式,都可以看作是科学计算的通用工具。但是NumPy和Tensor的用途是不一样的,NumPy不能用于GPU加速,Tensor则可以。 这节课我们一同学习了Tensor的创建、类型、转换、变换等常用功能,通过这几个功能,我们就可以对Tensor进行最基本也是最常用的操作,这些都是必须要牢记的内容。 此外,在实际上,真正的项目实战中还有个非常多的操作种类,其中较为重要的是**数学计算操作**,比如加减乘除、合并、连接等。但是这些操作如果一个一个列举出来,数量极其繁多,你也会感觉很枯燥,所以在后续的课程中,咱们会在具体的实战环节来学习相关的数学操作。 下一节课的内容,咱们会对Tensor的变形、切分等高级操作进行学习,这是一个很好玩儿的内容,敬请期待。 ## 每课一练 在PyTorch中,有torch.Tensor()和torch.tensor()两种函数,它们的区别是什么呢? 欢迎你在留言区和我交流,也推荐你把今天的内容分享给更多同事和朋友。