Pytorch学习: 张量基础操作


Pytorch学习: 张量基础操作

整理内容顺序来自龙龙老师的<深度学习与PyTorch入门实战教程>, 根据个人所需情况进行删减或扩充. 如果想要自己创建新的模块, 这些操作都是基本功, 需要掌握扎实.

张量数据类型

下表摘自Pytorch官方文档, 介绍了现在pytorch中所有涉及到的数据类型.

Data typedtypeCPU tensorGPU tensor
32-bit floating pointtorch.float32 or torch.floattorch.FloatTensortorch.cuda.FloatTensor
64-bit floating pointtorch.float64 or torch.doubletorch.DoubleTensortorch.cuda.DoubleTensor
16-bit floating point 1torch.float16 or torch.halftorch.HalfTensortorch.cuda.HalfTensor
16-bit floating point 2torch.bfloat16torch.BFloat16Tensortorch.cuda.BFloat16Tensor
32-bit complextorch.complex32
64-bit complextorch.complex64
128-bit complextorch.complex128 or torch.cdouble
8-bit integer (unsigned)torch.uint8torch.ByteTensortorch.cuda.ByteTensor
8-bit integer (signed)torch.int8torch.CharTensortorch.cuda.CharTensor
16-bit integer (signed)torch.int16 or torch.shorttorch.ShortTensortorch.cuda.ShortTensor
32-bit integer (signed)torch.int32 or torch.inttorch.IntTensortorch.cuda.IntTensor
64-bit integer (signed)torch.int64 or torch.longtorch.LongTensortorch.cuda.LongTensor
Booleantorch.booltorch.BoolTensortorch.cuda.BoolTensor

一般情况下, 常用的tensor类型只有float, int, bool. 至于使用多少位精度需要结合实际情况而定, 毕竟精度高了训练时间就长了. 其他的类型基本不需要去关心, 需要时再查查文档就好.

在CPU和在GPU上的tensor是完全不同的, 它们甚至不属于同一个类. 在CPU和GPU上训练的两个tensor除非迁移到同一个位置上, 否则不能发生交互.

为什么没有String 类型的tensor?

因为在深度学习中, 文本不会直接输入到框架中. 虽然在pytorch中没有string直接的数据类型, 但是可以根据需要把string做embedding或者one-hot转换成张量输入.

创建张量

大多数张量的基本操作也会穿插在这一节里面.

自动数据类型

创建张量, 使用torch.tensor(). 它可以创建一个标量(dim=0)或者一个张量. 如果传入的对象是一个数, 那么则创建一个标量, 如果传入的对象是一个list, 则创建一个张量. 这种方式是不指定数据类型的, tensor会自动分配数据类型.

使用Tensor.shapeTensor.size()可以查看张量的大小.

import torch
# 创建一个标量
a = torch.tensor(925)
print('a.size():', a.size())
# 创建一个张量
b = torch.tensor([925])
print('b.size()', b.size())
# output:
# a.size():  torch.Size([])
# b.size(): torch.Size([1])

标量和张量是不同的. 对于标量来说, 它本身就是一个单独的数, 没有dim和size这一说. 使用Tensor.dim()len(Tensor.size())是等价的, 对于标量来说, 结果应该是0.

同时, 对于标量来说, 使用Tensor.item()能获取标量的值.

可以通过Tensor.type()查看tensor的类型:

print('a.type():', a.type())
# output:
# a.type(): torch.LongTensor

使用torch.tensor()创建张量会被自动分配数据类型, 对于浮点和整型是不一样的:

c = torch.tensor([925.])
print('c.type():', c.type())
# output:
# c.type(): torch.FloatTensor

在GPU上的tensor和CPU上的tensor完全不是一个类型:

c = c.cuda()
print('c.type():', c.type())
# output:
# c.type(): torch.cuda.FloatTensor

Tensor.cuda()可以返回一个tensor在cuda上的引用, 也就是将tensor移动到GPU上去.

指定类型

torch.tensor()不同的是, 如果向指定数据类型的函数中传入一个数, 不再是创建一个指定类型的标量, 而是创建一个指定数据类型和指定dim和shape的tensor. 例如, torch.FloatTensor(3)是创建一个维度为1, shape为[3]的tensor. tensor中的数据全是随机的.

a = torch.FloatTensor(3)
print('a:', a)
print('a.shape:', a.shape)
"""
output
a: tensor([9.2196e-41, 0.0000e+00, 7.0295e+28])
a.shape: torch.Size([3])
"""
b = torch.FloatTensor(3, 4)
print('b:', b)
print('b.shape:', b.shape)
"""
output:
b: tensor([[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]])
b.shape: torch.Size([3, 4])
"""

当然, 也可以传入list直接对tensor初始化:

a = torch.FloatTensor([1, 2, 3])
print('a:', a)
print('a.type():', a.type())
"""
output:
a: tensor([1., 2., 3.])
a.type(): torch.FloatTensor
"""

通过nump创建

也可以通过numpy创建tensor.

a = np.ones([3, 4])
print('numpy.a:', a)
a = torch.from_numpy(a)
print('torch.a:', a)
"""
numpy.a: [[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]
torch.a: tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]], dtype=torch.float64)
"""

当然创建好了以后是一个float64的tensor, 从numpy导入的float其实是double类型.

其他创建方法

大多函数与numpy相同, 有numpy基础的建议直接跳过.

基本创建方法

# 均匀分布初始化
a = torch.rand(2, 3)
print('rand:', a)
# 空张量(全是随机数)
a = torch.empty(2, 3)
print('empty:', a)
# 随机整数
a = torch.randint(0, 10, [2, 2])
print('ranint:', a)
"""
output:
rand: tensor([[0.5267, 0.8344, 0.6042],
        [0.1859, 0.3207, 0.1803]])
empty: tensor([[1.0561e-38, 1.0653e-38, 1.4013e-45],
        [0.0000e+00, 1.4013e-45, 0.0000e+00]])
ranint: tensor([[8, 7],
        [1, 8]])
"""

torch.randint(low, high, size)是遵循切片规则的, 即生成的随机整数包含左侧不包含右侧.

xx_like

和numpy一样, 也有xx_like()这个函数, 能按传入的tensor的shape创建tensor:

a = torch.empty(2, 3)
b = torch.ones_like(a)
print('b:', b)
# output:
# b: tensor([[1., 1., 1.],
#        [1., 1., 1.]])

xx可以替换成前面所说的任意初始化的函数名称.

arange / range

numpy老朋友了. torch.arange()生成遵循切片规则[min, max)的tensor, 支持步长. torch.range()与前者功能相同, 因为完全可以代替, 可能会被移除, 不建议使用.

print('[0, 6):', torch.arange(0, 6))
print('[0, 6), 步长为2:', torch.arange(0, 6, 2))
# ouput:
# [0, 6): tensor([0, 1, 2, 3, 4, 5])
# [0, 6), 步长为2: tensor([0, 2, 4])

full

使用torch.full()创建一个指定shape的tensor并填满某个值.

print('填满6:', torch.full([2, 3], 6))
print('创建值为6的标量:', torch.full([], 6))
# output:
# 填满6: tensor([[6., 6., 6.],
#         [6., 6., 6.]])
# 创建值为6的标量: tensor(6.)

randn

使用torch.randn()按照(0, 1)初始化, 使用torch.normal()按照指定均值和方差进行初始化.

# 0, 1初始化
a = torch.randn(2, 3)
print('randn:', a)
# 指定均值和标准差
a = torch.normal(mean=torch.full([10], 0), std=torch.arange(1, 0, -0.1))
print('normal:', a)

"""
randn: tensor([[-1.1174,  0.8060,  0.1918],
        [-0.1511,  0.3734, -0.6192]])
normal: tensor([-2.1135e+00,  4.9261e-01, -9.9956e-01,  3.7895e-01, -4.1920e-01,
        -1.6493e-01,  3.6504e-01, -1.1884e-01, -1.1261e-03, -4.7203e-02])
"""

linspace / logspace

torch.linspace()torch.arange()非常相似, 只不过给出的是范围和所需的元素个数.

torch.logspace()torch.linspace()的对数版本.

print('[0, 10), 2:', torch.linspace(0, 10, 2))
print('[0, 10), 3:', torch.linspace(0, 10, 3))
print('log[0, 1), 3:', torch.logspace(0, 1, 3))
"""
[0, 10), 2: tensor([ 0., 10.])
[0, 10), 3: tensor([ 0.,  5., 10.])
log[0, 1), 3: tensor([ 1.0000,  3.1623, 10.0000])
"""

ones / zeros / eye

print('0阵:\n', torch.zeros(2, 3))
print('1阵:\n', torch.ones(2, 3))
print('单位阵:\n', torch.eye(2, 3))
"""
0阵:
 tensor([[0., 0., 0.],
        [0., 0., 0.]])
1阵:
 tensor([[1., 1., 1.],
        [1., 1., 1.]])
单位阵:
 tensor([[1., 0., 0.],
        [0., 1., 0.]])
"""

randperm

这个函数说一下功能就懂了, 是用来做shuffle的.

a = torch.rand(3, 4)
print('a:', a)
index = torch.randperm(2)
print('index:', index)
print('a[index]:', a[index])
"""
a: tensor([[0.7391, 0.1663, 0.1362, 0.9353],
        [0.2951, 0.9289, 0.3369, 0.8836],
        [0.2730, 0.8966, 0.7737, 0.5760]])
index: tensor([0, 1])
a[index]: tensor([[0.7391, 0.1663, 0.1362, 0.9353],
        [0.2951, 0.9289, 0.3369, 0.8836]])
"""

顺带一提, Tensor.numel()能得到tensor中的所有参数个数.

a = torch.empty(2, 3)
print('numel:', a.numel())
# numel: 6

默认数据类型

在使用torch.tensor()创建的数据类型默认是torch.FloatTensor, 可以使用torch.set_default_tensor_type(tensor_data_type)来设置默认创建的tensor类型.

索引和切片

索引访问与python基本一致, python的list用的比较熟的建议跳过索引访问. 多了一些其他的新东西.

索引访问

就是python中list的访问方法, 完全一致.

a = torch.randn(4, 3, 28, 28)
print('a.shape:', a.shape)
print('a[0].shape:', a[0].shape)
print('a[0][0].shape:', a[0][0].shape)
print('a[0][0][23][24].shape:', a[0][0][23][24].shape)
print('a[0][0][23][24]:', a[0][0][23][24])
"""
a.shape: torch.Size([4, 3, 28, 28])
a[0].shape: torch.Size([3, 28, 28])
a[0][0].shape: torch.Size([28, 28])
a[0][0][23][24].shape: torch.Size([])
a[0][0][23][24]: tensor(0.4280)
"""

通过切片访问多个元素:

a = torch.randn(4, 3, 28, 28)
print('a.shape:', a.shape)
print('a[:2].shape:', a[:2].shape)
print('a[:2, :1, :, :].shape:', a[:2, :1, :, :].shape)
print('a[:2, 1:, :, :].shape:', a[:2, 1:, :, :].shape)
print('a[:2, -1:, :, :].shape:', a[:2, -1:, :, :].shape)
"""
a.shape: torch.Size([4, 3, 28, 28])
a[:2].shape: torch.Size([2, 3, 28, 28])
a[:2, :1, :, :].shape: torch.Size([2, 1, 28, 28])
a[:2, 1:, :, :].shape: torch.Size([2, 2, 28, 28])
a[:2, -1:, :, :].shape: torch.Size([2, 1, 28, 28])
"""

通过步长:

print('a[:, :, 0:28:2, 0:28:2].shape:', a[:, :, 0:28:2, 0:28:2].shape)
print('a[:, :, ::2, ::2].shape:', a[:, :, ::2, ::2].shape)
# a[:, :, 0:28:2, 0:28:2].shape: torch.Size([4, 3, 14, 14])
# a[:, :, ::2, ::2].shape: torch.Size([4, 3, 14, 14])

特殊用法

index_select

多了一个Tensor.index_select(dim, index)函数, 像是对切片的封装, 不知道速度有没有提升, 在python中函数好像比切片要快一些, 记不太清了. 反正这个函数用起来是比较麻烦, index还必须是tensor类型的.

print(a.index_select(0, torch.tensor([1, 2])).shape)
# torch.Size([2, 3, 28, 28])

auto filled

...代表了任意多的维度, 能根据其他维度自动填充, 当维度能够根据其他值推断出来的时候特别方便.

print('a.shape:', a.shape)
print('a[...].shape:', a[...].shape)
print('a[0, ...].shape:', a[0, ...].shape)
print('a[:, 1, ...].shape:', a[:, 1, ...].shape)
print('a[..., :2].shape:', a[..., :2].shape)
"""
a.shape: torch.Size([4, 3, 28, 28])
a[...].shape: torch.Size([4, 3, 28, 28])
a[0, ...].shape: torch.Size([3, 28, 28])
a[:, 1, ...].shape: torch.Size([4, 28, 28])
a[..., :2].shape: torch.Size([4, 3, 28, 2])
"""

masked select

通过torch.masked_select(x, mask)能用Mask来筛选元素.

x = torch.randn(2, 3)
print('x:', x)
mask = x.ge(.5)
print('mask:', mask)
print('masked_select:', torch.masked_select(x, mask))
"""
x: tensor([[-0.7403, -1.3733,  0.8203],
        [-0.0259,  0.4284,  0.7480]])
mask: tensor([[0, 0, 1],
        [0, 0, 1]], dtype=torch.uint8)
masked_select: tensor([0.8203, 0.7480])
"""

注意, 选择后的结果是Flatten的, 而非保持原来的shape.

take

用的不是很多, 通过torch.take(src, index_tensor)能按照打平后的index进行访问.

x = torch.arange(0, 6).view(2, 3)
print('x:', x)
print('第2和第4个元素:', torch.take(x, torch.tensor([2, 4])))
"""
x: tensor([[0, 1, 2],
        [3, 4, 5]])
第2和第4个元素: tensor([2, 4])
"""

维度变换

很多函数也和numpy类似.

view / reshape

Tensor.view(size)能将tensor变形, 和reshape一样, 只要保证数据总数不变就能够进行shape变化. 它可以理解为将某个tensor中的元素按行依次取出, 再根据指定的size按行依次填充进去.

a = torch.rand(4, 1, 28, 28)
print(a.shape)
a = a.view(4, 28, 28)
print(a.shape)
a = a.view(4, 784)
print(a.shape)
"""
torch.Size([4, 1, 28, 28])
torch.Size([4, 28, 28])
torch.Size([4, 784])
"""

时刻注意每个dim所对应的含义, 否则在恢复时数据会失去原来的意义, 全部打乱掉. 在初学期, 最好要对数据的维度和意义进行追踪.

Tensor.view(size)Tensor.reshape(size)的差别不是很大, 但仍有差别, 在后面说transpose的时候会提到.

squeeze / unsqueeze

这一对函数主要是对维度进行提升或压缩.

Tensor.unsqueeze():

x = torch.rand(4, 1, 28, 28)
print(x.shape)
a = x.unsqueeze(0)
print(a.shape)
a = x.unsqueeze(-1)
print(a.shape)
a = x.unsqueeze(4)
print(a.shape)
a = x.unsqueeze(-4)
print(a.shape)
a = x.unsqueeze(-5)
print(a.shape)
# wrong
# a = x.unsqueeze(5)
"""
torch.Size([4, 1, 28, 28])
torch.Size([1, 4, 1, 28, 28])
torch.Size([4, 1, 28, 28, 1])
torch.Size([4, 1, 28, 28, 1])
torch.Size([4, 1, 1, 28, 28])
torch.Size([1, 4, 1, 28, 28])
"""

提升的维度也是遵循切片规则的, 区间为[-dim - 1, dim + 1).

Tensor.unsqueeze()相反, Tensor.squeeze用于无用维度压缩.

a = torch.randn(1, 32, 1, 1)
print(a.shape)
print(a.squeeze().shape)
print(a.squeeze(0).shape)
print(a.squeeze(-1).shape)
print(a.squeeze(1).shape)
print(a.squeeze(-4).shape)
"""
torch.Size([1, 32, 1, 1])
torch.Size([32])
torch.Size([32, 1, 1])
torch.Size([1, 32, 1])
torch.Size([1, 32, 1, 1])
torch.Size([32, 1, 1])
"""

expand / repeat

这两个函数从最终效果来说完全等价, 但过程不同.

Tensor.expand(size)实际上是在做BroadCasting, 被动复制数据, 只有在需要时候才复制. 而Tensor.repeat(copy_times)是直接复制. 建议使用前者减小内存压力.

对broadcast不理解的可以查看NumPy 广播(Broadcast), 这是一个很重要的机制, 广播能减少内存消耗或减少我们的操作.

Tensor.expand(size):

a = torch.randn(4, 32, 14, 14)
b = torch.randn(1, 32, 1, 1)
print(a.shape, b.shape)
print(b.expand(a.shape).shape)
print(b.expand(-1, 32, -1, -1).shape)
"""
torch.Size([4, 32, 14, 14]) torch.Size([1, 32, 1, 1])
torch.Size([4, 32, 14, 14])
torch.Size([1, 32, 1, 1])
"""

此处填入-1代表维度保持不变.

Tensor.repeat(copy_times)传入的是在每个dim上复制的次数:

a = torch.randn(1, 32, 1, 1)
print(a.shape)
print(a.repeat(4, 32, 1, 1).shape)
print(a.repeat(4, 1, 1, 1).shape)
print(a.repeat(4, 1, 32, 32).shape)
"""
torch.Size([1, 32, 1, 1])
torch.Size([4, 1024, 1, 1])
torch.Size([4, 32, 1, 1])
torch.Size([4, 32, 32, 32])
"""

在学习过repeat后, 顺带复习一下view的含义, 注意下面操作为什么有些是不等价的:

a = torch.arange(3)
b = a.repeat(2)
c = a.unsqueeze(1).repeat(1, 2).view(-1)
d = a.unsqueeze(0).repeat(2, 1).view(-1)
print(a)
print(b.size(), b)
print(c.size(), c)
print(d.size(), d)
"""
tensor([0, 1, 2])
torch.Size([6]), tensor([0, 1, 2, 0, 1, 2])
torch.Size([6]), tensor([0, 0, 1, 1, 2, 2])
torch.Size([6]), tensor([0, 1, 2, 0, 1, 2])
"""

虽然它们大小均相同, 同样都使用了repeat, 但结果却并不一致.

transpose / t / permute

这三个使用频率非常高.

Tensor.t()就是转置, 只能对2d-tensor使用, 否则会报错:

a = torch.rand(2, 3)
print(a.shape)
print(a.t().shape)
"""
torch.Size([2, 3])
torch.Size([3, 2])
"""

Tensor.transpose()能任意交换2个维度之间的数据, 只进行一次交换时候建议使用它:

a = torch.rand(4, 3, 32, 32)
print('a.shape:', a.shape)
a.transpose(1, 3)
print('交换1, 3:', a.transpose(1, 3).shape)
"""
a.shape: torch.Size([4, 3, 32, 32])
交换1, 3: torch.Size([4, 32, 32, 3])
"""

但是请注意, 在使用transposepermute后, 只是改变了访问的方式(数组的访问步长), 而不会改变底层数组的存储方式, 这也就是所谓的”不连续“.

Tensor.view()要求tensor必须是连续的, 所以在view前必须使用contiguous()让tensor变连续, 新版中直接使用reshape函数更方便, 它等价于前面的操作.

关于连续与否更详细的解释可以看PyTorch中的contiguous.

a = torch.randn(4, 3, 32, 32)
# 错误做法
a1 = a.transpose(1, 3).contiguous().view(4, 3 * 32 * 32).view(4, 3, 32, 32)
# 正确做法
a2 = a.transpose(1, 3).contiguous().view(4, 3 * 32 * 32).view(4, 32, 32, 3).transpose(1, 3)
# 用reshape
a3 = a.transpose(1, 3).reshape(4, 3 * 32 * 32).reshape(4, 32, 32, 3).transpose(1, 3)
print('a.shape:', a.shape)
print('a1.shape:', a1.shape)
print('a2.shape:', a2.shape)
print('a3.shape:', a3.shape)
print('a == a1?:', torch.all(torch.eq(a, a1)))
print('a == a2?:', torch.all(torch.eq(a, a2)))
print('a == a3?:', torch.all(torch.eq(a, a3)))
"""
a.shape: torch.Size([4, 3, 32, 32])
a1.shape: torch.Size([4, 3, 32, 32])
a2.shape: torch.Size([4, 3, 32, 32])
a3.shape: torch.Size([4, 3, 32, 32])
a == a1?: tensor(0, dtype=torch.uint8)
a == a2?: tensor(1, dtype=torch.uint8)
a == a3?: tensor(1, dtype=torch.uint8)
"""

一定要先view交换后的shape, 再transpose回来.

Tensor.permute()更加灵活, 能随意交换所有dim之间的位置, 如果交换多个维度最好使用这个函数:

a = torch.rand(4, 3, 28, 32)
print(a.shape)
print(a.permute(0, 2, 3, 1).shape)
print(a.permute(0, 2, 1, 3).shape)
print(a.transpose(1, 2).shape)
"""
torch.Size([4, 3, 28, 32])
torch.Size([4, 28, 32, 3])
torch.Size([4, 28, 3, 32])
torch.Size([4, 28, 3, 32])
"""

也同时要注意, 使用后也是不连续的.


文章作者: DaNing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 DaNing !
评论
 上一篇
Pytorch学习: 张量进阶操作 Pytorch学习: 张量进阶操作
2020.10.03: 因torch版本更新, 对gather描述进行了修正. 2021.03.11: 更新了对gather的描述. Pytorch学习: 张量进阶操作整理内容顺序来自龙龙老师的<深度学习与PyTorch入门实战教
2020-10-03
下一篇 
指针网络家族 指针网络家族
本文介绍了Pointer Network, CopyNet, Pointer-Generator Network以及Coverage机制在文本摘要与对话系统中的应用, 既可以作为知识点介绍, 也可以作为论文阅读笔记. 此外, 该部分内容为外
2020-09-28
  目录