欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Pytorch学习(1) —— Tensor基础

程序员文章站 2022-06-12 10:05:41
...

学习时候作为Pytorch的学习笔记,对一些关键函数进行整理,方便后续学习时候查询用。

许多教程基于老版本Pytorch,本博客基于1.*系列的版本

本文章还在更新中,目前仅更新常用的一些方法,内容参考了官方文档和书籍《Pytorch物体检测实战》

1 Tensor的创建

注意区分: torch.Tensor是定义一个类,表示Tensor数据类型,而torch.tensor是一个函数,返回值类型为Tensor。使用torch.tensor比用Tensor似乎更好用些。

1.1 使用torch.tensor创建

torch.tensor(data, dtype, device, requires_grad)

这个是最基础的函数调用,data可以是一个list,也可以是一个numpy矩阵,dtype表示的是数据类型,device表示这个数据存在CPU还是GPU中,requires_grad用于自动求导时记录导数信息,默认为False。除了data必须,其余均可省略。

官方给出的dtype包含的类型如下所示,如果想设置32位浮点类型,那么令dtype=torch.float32即可,其余类似。

类型 32位浮点 64位浮点 16位浮点 8位整数 16位整数 32位整数 64位整数 布尔变量
dtype float32 或 float float64 或 double float16 或 half uint8 和 int8 int16 和 short int32 和 int int64 和 long bool

device指定数据存储设备,可以用torch.device('cpu')torch.device('cuda')指定CPU还是GPU,此外可以通过torch.device('cuda', 0)指定设备再哪个显卡上。

下面给一个定义示例,使用torchdata.dtypetorchdata.device可以查看数据类型和数据所在的设备。

npdata = np.ones((2,2))
torchdata = torch.tensor(npdata, dtype=torch.float16,device=torch.device('cuda',0))

注意:torch.tensor()会完整拷贝data,不会共享数据,下面给出几个可以共享数据的方法。

d2 = d1.requires_grad_(True) # 改变requires_grad,d1内部属性也会被改变,返回值也是个Tensor
d2 = d1.detach() # d2不会被操作,仅用于查看,在后面网络传导中常见,到时候在更新这部分
d2 = torch.as_tensor(np1) # 与numpy数据类型共享内存,当然这种都在CPU中。

1.2 其他创建操作(比如全0,全1,随机等等)

下面给出一些特殊的创建操作。

# 全是0的一个Tensor,size为尺度,比如2*3矩阵就为[2,3],out跟返回值一样,layout内存布局,不常用。
torch.zeros(size, out, dtype, layout, device, requires_grad) # 除size必须,其余均可省略

# 全是1的一个Tensor,定义用法与zeros一样
torch.ones(size, out, dtype, layout, device, requires_grad) 

其他类似的还有,eye, randn, randperm等等,用法类似,后续在对这些小函数进行细致的研究。

1.3 Tensor矩阵的元素访问,维度相关

为了方便说明,先定义一个Tensor矩阵A。

Tensor的元素访问与一般的矩阵访问差别不大,访问第i行第j列的元素可以使用A[i,j]A[i][j],返回值仍然是一个Tensor,只不过这个Tensor只有一个数。如果想将这个数转为一般的Python数字,可以使用A[i][j].item(),这样返回的值就是一般的数值,与Tensor无关。

对于Tensor的维度, 可使用A.shape或者A.size()函数维度, 两者等价,值得注意的是,返回维度显示是torch.Size([m,n]),实际上torch.Size是继承tuple的一个类,所以就当正常矩阵用就行,不用考虑torch转换的问题,简单来说,可以直接np.array(A.size())

如果想查看总的元素个数,使用A.numel()A.nelement()即可返回总元素个数,两者等价。

2 Tensor的组合与分块

2.1 组合

组合操作是指将不同的Tensor叠加起来, 主要有torch.cat()torch.stack() 两个函数。 cat即concatenate的意思, 是指沿着已有的数据的某一维度进行拼接, 操作后数据的总维数不变, 在进行拼接时, 除了拼接的维度之外, 其他维度必须相同。 而torch.stack()函数指新增维度, 并按照指定的维度进行叠加, 具体示例如下:

a = torch.tensor([[1,2],[3,4]])
b = torch.tensor([[5,6], [7,8]])
print(torch.cat([a,b],0))
print(torch.cat([a,b],1))

输出结果如下所示,清晰易懂,注意拼接对应的维度一定要相等。

tensor([[1, 2],
        [3, 4],
        [5, 6],
        [7, 8]])
tensor([[1, 2, 5, 6],
        [3, 4, 7, 8]])

关于堆叠,按照[a,b,a]以第0,1,2维进行堆叠,则得到的矩阵维度分别为:3*2*2,2*3*2,2*2*3。堆叠原理请从结果分析即可。

print(torch.stack([a,b,a], 0))   print(torch.stack([a,b,a], 1))  print(torch.stack([a,b,a], 2))

输出:
tensor([[[1, 2],                 tensor([[[1, 2],                tensor([[[1, 5, 1],
         [3, 4]],                         [5, 6],                         [2, 6, 2]],
                                          [1, 2]],
        [[5, 6],                                                         [[3, 7, 3],
         [7, 8]],                         [[3, 4],                        [4, 8, 4]]])
                                           [7, 8],
        [[1, 2],                           [3, 4]]])
         [3, 4]]])

2.2 分块

分块则是与组合相反的操作, 指将Tensor分割成不同的子Tensor, 主要有torch.chunk()torch.split() 两个函数, 前者需要指定分块的数量, 而后者则需要指定每一块的大小, 以整型或者list来表示。 具体示例如下:

先把矩阵按照第0维分割,很显然矩阵按照行被分割维两个矩阵。

a = torch.tensor([[1,2,3],[4,5,6]])
print(torch.chunk(a, 2, 0))

输出:
(tensor([[1, 2, 3]]), tensor([[4, 5, 6]]))

那么如果我们按照第1维分割维两块呢,明显3不能被2除,那么会发生什么,第一个分为2*2,第二个为2*1,当不能整除时, 最后一个的维数会小于前面。

a = torch.tensor([[1,2,3],[4,5,6]])
print(torch.chunk(a, 2, 1))

输出:
(tensor([[1, 2],
        [4, 5]]), tensor([[3],
        [6]]))

下面使用split进行分块,返回的是一个list,分割类似chunk,只不过一个是按照块,一个是按照维度而已。

a = torch.tensor([[1,2,3],[4,5,6],[7,8,9]])
print(torch.split(a, 2, 0))
输出
(tensor([[1, 2, 3],
        [4, 5, 6]]), tensor([[7, 8, 9]]))

3 Tensor的索引与变形

3.1 索引操作

索引用法与nunpy太相似了,我们先定义好一个tensor矩阵A,注意,下面说的这些操作返回值都是Tensor类型。

  • 取第i行:A[i]
  • 取第i行第j列值: A[i][j]
  • A>0:选择A中大于0的元素, 返回和A相同大小的Tensor, 符合条件的置1, 否则置0
  • A[A>0]:选择符合条件的元素并返回, 等价于torch.masked_select(A, A>0)
  • torch.nonzero(A):选择非0元素的坐标, 并返回

关于较抽象的函数,torch.where(condition, x, y), 满足condition的位置输出x, 否则输出y。对Tensor元素进行限制可以使用clamp()函数,具体示例如下所示。

a = torch.tensor([[0, 1], [2, 3]])
print(torch.where(a>1, -a, a))
print(a.clamp(1,2))

输出:
tensor([[ 0,  1],
        [-2, -3]])
tensor([[1, 1],
        [2, 2]])

3.2 变形操作

变形操作则是指改变Tensor的维度, 以适应在深度学习的计算中, 数据维度经常变换的需求, 是一种十分重要的操作。 在PyTorch中主要有4类不同的变形方法, 如下表所示。

变形操作 功能
view()和reshape() 调整Tensor形状,元素总数相同
transpose()和permute() 各维度之间的变换
squeeze()和unsqueeze() 处理size为1的维度
expand()和expand_as() 复制元素来扩展维度

3.2.1 view()和reshape()函数

view()和reshape()函数可以在不改变Tensor数据的前提下任意改变Tensor的形状, 必须保证调整前后的元素总数相同, 并且调整前后共享内存, 作用基本相同。

a = torch.tensor([1,2,3,4])
b = a.view(2,2)
c = a.reshape(4,1)

每个变量输出为:
tensor([1, 2, 3, 4])
tensor([[1, 2],
        [3, 4]])
tensor([[1],
        [2],
        [3],
        [4]])

如果想要直接改变Tensor的尺寸, 可以使用resize_()的原地操作函数。 在resize_()函数中, 如果超过了原Tensor的大小则重新分配内存, 多出部分置0, 如果小于原Tensor大小则剩余的部分仍然会隐藏保留。注意,resize之后,a也变了

b = a.resize_(2,3)
print(b)

输出:
tensor([[1, 2, 3],
        [4, 0, 0]])

3.2.2 transpose()和permute()函数

transpose()函数可以将指定的两个维度的元素进行转置, 而permute()函数则可以按照给定的维度进行维度变换。

a = torch.arange(1,25).resize_(2,3,4)
print(a)
print(a.transpose(0,1))
print(a.transpose(0,1).size())
print(a.permute(1,2,0))
print(a.permute(1,2,0).size())                 

输出:
tensor([[[ 1,  2,  3,  4],    tensor([[[ 1,  2,  3,  4],    tensor([[[ 1, 13],
         [ 5,  6,  7,  8],             [13, 14, 15, 16]],            [ 2, 14],
         [ 9, 10, 11, 12]],                                          [ 3, 15],
                                      [[ 5,  6,  7,  8],             [ 4, 16]],
        [[13, 14, 15, 16],             [17, 18, 19, 20]],           [[ 5, 17],
         [17, 18, 19, 20],                                           [ 6, 18],
         [21, 22, 23, 24]]])           [[ 9, 10, 11, 12],            [ 7, 19],
                                        [21, 22, 23, 24]]])          [ 8, 20]],
                               torch.Size([3, 2, 4])                [[ 9, 21],
                                                                     [10, 22],
                                                                     [11, 23],      
                                                                     [12, 24]]])           
                                                             torch.Size([3, 4, 2]) 

3.2.3 squeeze()和unsqueeze()函数

在实际的应用中, 经常需要增加或减少Tensor的维度, 尤其是维度为1的情况, 这时候可以使用squeeze()与unsqueeze()函数, 前者用于去除size为1的维度, 而后者则是将指定的维度的size变为1。

a = torch.arange(1,4)
print(a.shape)
# 将第0维变为1, 因此总的维度为1、 3
print(a.unsqueeze(0).shape)
# 第0维如果是1, 则去掉该维度, 如果不是1则不起任何作用
print(a.unsqueeze(0).squeeze(0).shape)

输出:
torch.Size([3])
torch.Size([1, 3])
torch.Size([3])

3.2.4 expand()和expand_as()函数

有时需要采用复制元素的形式来扩展Tensor的维度, 这时expand就派上用场了。 expand()函数将size为1的维度复制扩展为指定大小,也可以使用expand_as()函数指定为示例Tensor的维度。

a=torch.randn(2,2,1)
print(a)
# 将第2维的维度由1变为3, 则复制该维的元素, 并扩展为3
print(a.expand(2,2,3))

输出:
tensor([[[-0.1415],
         [ 0.6401]],

        [[ 0.0036],
         [ 1.8994]]])
tensor([[[-0.1415, -0.1415, -0.1415],
         [ 0.6401,  0.6401,  0.6401]],

        [[ 0.0036,  0.0036,  0.0036],
         [ 1.8994,  1.8994,  1.8994]]])

注意: 在进行Tensor操作时, 有些操作如transpose()、permute()等可能会把Tensor在内存中变得不连续, 而有些操作如view()等是需要Tensor内存连续的, 这种情况下需要使用contiguous()操作先将内存变为连续的。 reshape()操作, 可以看做是Tensor.contiguous().view()。

4 Tensor的排序与取极值

比较重要的是排序函数sort(), 选择沿着指定维度进行排序, 返回排序后的Tensor及对应的索引位置。 max()与min()函数则是沿着指定维度选择最大与最小元素, 返回该元素及对应的索引位置。

a = torch.randn(3,3)
# 按照第0维即按行排序, 每一列进行比较, True代表降序, False代表升序
b = a.sort(0, True)
# 按照第0维即按行选取最大值, 返回最大值,和对应位置
c = a.max(0)

对于Tensor的单元素数学运算, 如abs()、 sqrt()、 log()、 pow()和三角函数等, 都是逐元素操(element-wise) , 输出的Tensor形状与原始Tensor形状一致。

对于类似求和、 求均值、 求方差、 求距离等需要多个元素完成的操作, 往往需要沿着某个维度进行计算, 在Tensor中属于归并操作,输出形状小于输入形状。 由于比较简单且与NumPy极为相似, 在此就不详细展开

5 Tensor的内存共享

为了实现高效计算, PyTorch提供了一些原地操作运算, 即inplace operation, 不经过复制, 直接在原来的内存上进行计算。 对于内存的共享, 主要有如下3种情况。

5.1 通过Tensor初始化Tensor

直接通过Tensor来初始化另一个Tensor, 或者通过Tensor的组合、 分块、 索引、 变形操作来初始化另一个Tensor, 则这两个Tensor共享内存。

下面这个例子,abc三个变量内存共享。

a=torch.randn(2,2)
b = a
c = a.view(4)

5.2 原地操作符

PyTorch对于一些操作通过加后缀“”实现了原地操作, 如add() 和resize_()等, 这种操作只要被执行, 本身的Tensor则会被改变。

下面这个例子,abc三个变量共享内存,原始的a已经发生变化

a = torch.tensor([[1,2],[3,4]])
b = a.add_(a)
c = a.resize_(4)

5.3 Tensor与NumPy转换

Tensor与NumPy可以进行转换, 并且转换前后的变量共享内存。 在进行PyTorch不支持的操作时, 可以将Tensor转换为NumPy类型, 操作后再转为Tensor。

a = torch.randn(1,2)
b = a.numpy()
c = torch.from_numpy(b)
d = a.tolist()

总结

本部分就是对Tensor常用的一些功能进行总结,在后续的开发中,肯定会涉及到大量没用过的函数,到时候遇到没有提到的,再补充,大规模更新版本后,这个也会同步更新。