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

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

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

答案是可以的,这种统一的数据形式,在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所含有的元素的个数为235=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维度的数据删除,得到y,squeeze执行成功是因为第1维度的大小为1。然而在y上我们打算进一步删除第1维度的时候,就会发现删除失败了,这是因为y此刻的第1维度的大小为3,suqeeze不能删除。
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()两种函数,它们的区别是什么呢?

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

精选留言

  • 徐洲更

    2021-12-08 18:37:35

    通过查阅文档,torch.Tensor是默认tensor类型的torch.FloatTensor别名, 可以直接从给定数据中创建出FloatTensor的tensor, 而torch.tensor是创建会根据输入的数据类型判断。 也就是说,如果我传入的是int类型,那么torch.Tensor输出的是FloatTensor数据格式,而torch.tensor输出的torch.int32
    作者回复

    👍🏻👍🏻👍🏻👍🏻👍🏻👍🏻

    2021-12-09 10:25:18

  • vcjmhg

    2021-10-18 15:37:14

    torch.Tensor() 是Tensor类的构造方法,通过构造方法创建Tensor对象的实例;torch.tensor()则是Tensor类内部的一个方法,方法的返回值是Tensor类型
    作者回复

    你好,vcjmhg,感谢你的留言。回答的很赞👍。torch.tensor是函数,调用之后会对参数中的数据做拷贝,生成相应的张量并返回这个张量。

    2021-10-19 10:46:12

  • Sam Wang

    2021-10-20 10:42:15

    Squeeze 和unsqueeze我的理解是他们只是减少或增加了1维,但其实并未增加或者减少数据。因为数据数量没变 (多乘1而已,数量不变)
    作者回复

    你好,Geek_60b6fb,感谢你的留言。数据是没有发生变化的,只是维度变化了。
    但是你说的多乘1,我没太理解是什么意思。可以留言,或者在微信群里咱们继续交流。^^

    2021-10-20 16:59:24

  • Geek_86b454

    2021-12-13 19:59:21

    新创建出来的tensor是CPU tensor还是GPU tensor呢
    作者回复

    你好,感谢你的留言。
    由创建tensor中的device参数决定是在CPU还是GPU。

    2021-12-14 08:51:21

  • celerychen

    2022-07-28 14:40:08

    x.view(4, 4) 元素个数仍然是16个,和之前的一样,内存怎么就变成不连续的呢,能否解释一下?谢谢老师!
    作者回复

    celerychen,你好,感谢你的留言。
    view操作只作用在连续内存上,仅仅按照行重新排列下标,不改变数据的内存分布。也就是说,数据本身没有变化,下标发生了变化,如果从下标顺序的角度来看,内存就是不连续的了。

    2022-07-29 08:56:15

  • 步比天下

    2021-10-19 15:19:19

    老师您好,感觉最后一个例子有些不太理想,有些同学可能会搞不清新的一维是在哪个位置插入的,能不能再换个例子啊
    作者回复

    你好,步比天下。感谢你的留言。这个例子其实还是比较好理解的,unsqueeze函数例子中是在第2维度进行插入的,也就是说,新的维度在第二维,新tensor的shape为2,1,1,3,其中第二个1就是新插入的。

    2021-10-20 08:45:01

  • Dreamboat

    2025-04-05 15:10:36

    老师,关于内存连续问题,在实际的深度学习过程中,模型对于数值所在的位置信息是很敏感的。我是不是可以理解为,view不会改变数值的位置信息,transpose 或者 permute会导致位置信息发生变化?
    作者回复

    对,是的。
    view不改变连续性。transpose与permute都会改变改变连续性

    2025-05-21 21:36:33

  • 纳兰容若

    2025-03-21 15:50:33

    老师您好 为什么requires_grad = true在训练的时候比False时慢很多,感谢老师
    作者回复

    应为会涉及到计算图构建、梯度计算 和 内存计算等开销啊

    2025-05-21 21:39:52

  • 梦之吃吃

    2023-12-13 00:03:47

    调用torch.Tensor构造的tensor默认类型是单精度浮点数类型,调用torch.tensor构造的tensor根据原始数据类型生成对应的tensor数据类型
    作者回复

    👍🏻

    2023-12-25 15:29:09

  • Geek_1509a8

    2023-11-24 22:24:37

    torch.Tensor()通过data创建张量的实例,此时没有对data做拷贝,更改数据同时会更改张量;而torch.tensor()是一个创建张量的函数,会对数据做拷贝,更改数据不会对张量有影响。
  • ifelse

    2023-11-22 12:19:42

    学习打卡
  • 胖胖虎

    2023-04-18 10:22:00

    PyTorch's tensor (lowercase "t") and Tensor (capital "T") both refer to the same fundamental data structure in PyTorch, which is a multi-dimensional array. The difference between the two is that tensor is a function used to create a new tensor from an existing data source, while Tensor is a class used to represent a tensor object.

    The tensor function is used to create a new tensor from an existing data source such as a list or a NumPy array. For example, the following code creates a tensor using the tensor function:
    ```python
    import torch

    data = [1, 2, 3, 4, 5]
    tensor = torch.tensor(data)
    ```
    On the other hand, the Tensor class is used to create a new tensor object with specified properties such as size and data type. For example, the following code creates a tensor using the Tensor class:
    ```python
    import torch

    tensor = torch.Tensor(2, 3)
    ```
    In general, it is recommended to use the tensor function to create new tensors from existing data sources, and to use the Tensor class to create new empty tensors with specific properties. However, both tensor and Tensor can be used interchangeably depending on the use case.
  • mtfelix

    2022-08-25 11:58:10

    NumPy 不能用于 GPU 加速,Tensor 则可以。

    ---- 老师能给详细说说嘛?
  • 站着睡觉的树

    2022-03-28 10:34:27

    老师,请问 获取形状的代码中,a=torch.zeros(2, 3, 5) ,这是一个2行,3列,深度为5的张量吗?我看您的配图,表示的是2列,3行,深度为5的张量。 也就是说,张量的0维,指的是有多少列,张量的1维指的是有多少行,张量的2维指的是有层(深度),我的理解对吗?
    作者回复

    你好,站着睡觉的树,感谢你的留言。
    其实,没有行、列、深度的概念的。只不过3维是我们可以用图画描述出来的,如果是4维、5维更高维度的话,那该怎么办呢?
    a=torch.zeros(2, 3, 5)
    只能说张量a的shape的第一个维度是2,第二个维度是3,第三个维度是5

    2022-03-29 15:46:11

  • Chloe

    2022-01-26 23:10:20

    请问老师能不能讲讲为什么squeeze 只能增减维度的大小为 1 的维度呢?
    作者回复

    squeeze只能减不能增啊。

    2022-01-27 16:51:38

  • 马克图布

    2021-11-05 22:16:22

    老师,关于 Tensor 内存不连续的问题,使用 contiguous() 函数也可以解决吧?还是说使用 reshape() 有些什么其他优势呢?
    作者回复

    你好。马克图布,感谢你的留言。
    View本身不支持内存不连续的Tensor结构,所以你先用contiguous处理一下是可以的。但是View方法本身共享底层数据内存,返回的Tensor不会使用新的内存。所以如果你想在view中使用contiguous,就有可能会使用新的内存,破坏了代码的连贯与兼容性。

    2021-11-08 13:30:41

  • AstrHan

    2021-10-26 15:28:56

    为什么我运行a = torch.Tensor(1);b = a.item()的时候,b的数值不是1,而且每次运行的结果都不相同。会得到1.0或者0.0或者很接近0的数或者一个随机的浮点数。
  • 李雄

    2021-10-23 15:04:47

    这里我们新建了一个维度为[2, 1, 3]的 Tensor,然后在第 2 维度插入一个数据,这样就得到了一个[2,1,1,3]大小的 tensor。
    应该是在第2维度上插入一个维度,这样说是不是更准确些。因为x.numel() == y.numel()的返回值是True
    作者回复

    你好,李雄。感谢你的留言,也谢谢你的纠正。👍🏻👍🏻👍🏻👍🏻维度更准确,我修改一下。^^

    2021-10-24 20:46:49

  • IUniverse

    2021-10-18 20:28:20

    两者都可以用于生成张量,但一个是类(torch.FloatTensor()的别名),而另外一个是函数。
    作者回复

    你好,IUniverse。感谢你的留言。为你的回答点赞👍^^。

    2021-10-19 10:19:28

  • Joe Black

    2021-10-18 08:08:48

    torch.Tensor()是Python类,是默认张量类型torch.FloatTensor()的别名,而torch.tensor()是Python函数。
    作者回复

    你好,Joe Black,感谢你的留言。👍你的回答是正确的。不过是PyTorch,不是Python。^^

    2021-10-18 16:59:23