gitbook/PyTorch深度学习实战/docs/427460.md
2022-09-03 22:05:03 +08:00

15 KiB
Raw Permalink Blame History

04 | TensorPyTorch中最基础的计算单元

在上节课中我们一起学习了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)]等。

为了帮助你更好理解标量、向量和矩阵,我特意准备了一张示意图,你可以结合图片理解。

不难发现,几种数据表示其实都是有着联系的,标量可以组合成向量,向量可以组合成矩阵。那么,我们可否将它们看作是一种数据形式呢?

答案是可以的这种统一的数据形式在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支持的数据类型有很多种这里列举较为常用的几种格式

图片

一般来说torch.float32、torch.float64、torch.uint8和torch.int64用得相对较多一些但是也不是绝对还是要根据实际情况进行选择。这里你有个印象就行后面课程用到时我还会进一步讲解。

Tensor的创建

PyTorch对于Tensor的操作已经非常友好了你可以通过多种不同的方式创建一个任意形状的Tensor而且每种方式都很简便我们一起来看一下。

直接创建

首先来看直接创建的方法这也是最简单创建的方法。我们需要用到下面的torch.tensor函数直接创建。

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的语句

torch.from_numpy(ndarry)

有时候我们在开发模型的过程中需要用到一些特定形式的矩阵Tensor比如全是0的或者全是1的。这时我们就可以用这个方法创建比如说先生成一个全是0的NumPy数组然后转换成Tensor。但是这样也挺麻烦的因为这意味着你要引入更多的包NumPy也会使用更多的代码这会增加出错的可能性。
不过你别担心PyTorch内部已经提供了更为简便的方法我们接着往下看。

创建特殊形式的Tensor

我们一块来看一下后面的几个常用函数它们都是在PyTorch模型内部使用的。

  • 创建零矩阵Tensor零矩阵顾名思义就是所有的元素都为0的矩阵。
torch.zeros(*size, dtype=None...)

其中我们用得比较多的就是size参数和dtype参数。size定义输出张量形状的整数序列。
这里你可能注意到了在函数参数列表中我加入了省略号这意味着torch.zeros的参数有很多。不过。咱们现在是介绍零矩阵的概念形状相对来说更重要。其他的参数比如前面提到的requires_grad参数与此无关现阶段我们暂时不关注。

  • 创建单位矩阵Tensor单位矩阵是指主对角线上的元素都为1的矩阵。
torch.eye(size, dtype=None...)

  • 创建全一矩阵Tensor全一矩阵顾名思义就是所有的元素都为1的矩阵。
torch.ones(size, dtype=None...)

  • 创建随机矩阵Tensor在PyTorch中有几种较为经常使用的随机矩阵创建方式分别如下。
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的转换
a = torch.tensor(1)
b = a.item()

我们通过torch.Tensor将一个数字或者标量转换为Tensor又通过item()函数将Tensor转换为数字标量item()函数的作用就是将Tensor转换为一个python number。

  • list与tensor的转换
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之间的转换
CPU->GPU: data.cuda()
GPU->CPU: data.cpu()

Tensor的常用操作

刚才我们一起了解了Tensor的类型如何创建Tensor以及如何实现Tensor和一些常见的数据类型之间的相互转换。其实Tensor还有一些比较常用的功能比如获取形状、维度转换、形状变换以及增减维度接下来我们一起来看看这些功能。

获取形状

在深度学习网络的设计中我们需要时刻对Tensor的情况做到了如指掌其中就包括获取Tensor的形式、形状等。

为了得到Tensor的形状我们可以使用shape或size来获取。两者的不同之处在于shape是torch.tensor的一个属性而size()则是一个torch.tensor拥有的方法。

>>> a=torch.zeros(2, 3, 5)
>>> a.shape
torch.Size([2, 3, 5])
>>> a.size()
torch.Size([2, 3, 5])

图片

知道了Tensor的形状我们就能知道这个Tensor所包含的元素的数量了。具体的计算方法就是直接将所有维度的大小相乘比如上面的Tensor a所含有的元素的个数为2_3_5=30个。这样似乎有点麻烦我们在PyTorch中可以使用numel()函数直接统计元素数量。

>>> a.numel()
30

矩阵转秩(维度转换)

在PyTorch中有两个函数分别是permute()和transpose()可以用来实现矩阵的转秩或者说交换不同维度的数据。比如在调整卷积层的尺寸、修改channel的顺序、变换全连接层的大小的时候我们就要用到它们。

其中用permute函数可以对任意高维矩阵进行转置但只有 tensor.permute() 这个调用方式,我们先看一下代码:

>>> 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])

图片

有没有发现原来的Tensor的形状是[2,3,5]我们在permute中分别写入原来索引位置的新位置x.permute(2,1,0)2表示原来第二个维度现在放在了第零个维度同理1表示原来第一个维度仍旧在第一个维度0表示原来第0个维度放在了现在的第2个维度形状就变成了[5,3,2]

而另外一个函数transpose不同于permute它每次只能转换两个维度或者说交换两个维度的数据。我们还是来看一下代码

>>> 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。

>>> 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再进行一步操作代码如下

>>> x = x.permute(1,0)
>>> x.shape
torch.Size([8, 2])
>>> x.view(4, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
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

>>> x = x.reshape(4, 4)
>>> x.shape
torch.Size([4, 4])

这样问题就迎刃而解了。其实reshape相当于进行了两步操作先把Tensor在内存中捋顺了然后再进行view操作。

增减维度

有时候我们需要对Tensor增加或者删除某些维度比如删除或者增加图片的几个通道。PyTorch提供了squeeze()和unsqueeze()函数解决这个问题。

我们先来看squeeze()。如果dim指定的维度的值为1则将该维度删除若指定的维度值不为1则返回原来的Tensor。为了方便你理解我还是结合例子来讲解。

>>> 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维度的数据删除得到ysqueeze执行成功是因为第1维度的大小为1。然而在y上我们打算进一步删除第1维度的时候就会发现删除失败了这是因为y此刻的第1维度的大小为3suqeeze不能删除。
unsqueeze()这个函数主要是对数据维度进行扩充。给指定位置加上维数为1的维度我们同样结合代码例子来看看。

>>> 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()两种函数,它们的区别是什么呢?

欢迎你在留言区和我交流,也推荐你把今天的内容分享给更多同事和朋友。