Pytorch学习: 张量基础操作
整理内容顺序来自龙龙老师的<深度学习与PyTorch入门实战教程>
, 根据个人所需情况进行删减或扩充. 如果想要自己创建新的模块, 这些操作都是基本功, 需要掌握扎实.
张量数据类型
下表摘自Pytorch官方文档, 介绍了现在pytorch中所有涉及到的数据类型.
Data type | dtype | CPU tensor | GPU tensor |
---|---|---|---|
32-bit floating point | torch.float32 or torch.float | torch.FloatTensor | torch.cuda.FloatTensor |
64-bit floating point | torch.float64 or torch.double | torch.DoubleTensor | torch.cuda.DoubleTensor |
16-bit floating point 1 | torch.float16 or torch.half | torch.HalfTensor | torch.cuda.HalfTensor |
16-bit floating point 2 | torch.bfloat16 | torch.BFloat16Tensor | torch.cuda.BFloat16Tensor |
32-bit complex | torch.complex32 | ||
64-bit complex | torch.complex64 | ||
128-bit complex | torch.complex128 or torch.cdouble | ||
8-bit integer (unsigned) | torch.uint8 | torch.ByteTensor | torch.cuda.ByteTensor |
8-bit integer (signed) | torch.int8 | torch.CharTensor | torch.cuda.CharTensor |
16-bit integer (signed) | torch.int16 or torch.short | torch.ShortTensor | torch.cuda.ShortTensor |
32-bit integer (signed) | torch.int32 or torch.int | torch.IntTensor | torch.cuda.IntTensor |
64-bit integer (signed) | torch.int64 or torch.long | torch.LongTensor | torch.cuda.LongTensor |
Boolean | torch.bool | torch.BoolTensor | torch.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.shape
或Tensor.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])
"""
但是请注意, 在使用transpose
和permute
后, 只是改变了访问的方式(数组的访问步长), 而不会改变底层数组的存储方式, 这也就是所谓的”不连续“.
而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])
"""
也同时要注意, 使用后也是不连续的.