导入包
代码语言:javascript复制import torch
虽然被称为Pytorch
,但是代码中使用torch
张量
张量表示由一个数值组成的数组,这个数组可能有多个维度。具有一个轴的张量对应数学上的向量(vector);具有两个轴的张量对应数学上的矩阵(matrix);具有两个轴以上的张量没有特殊的数学名称。
可以使用 arange
创建一个行向量 x
。这个行向量包含以0开始的前12个整数,它们默认创建为整数。也可指定创建类型为浮点数。张量中的每个值都称为张量的 元素(element)。例如,张量 x
中有 12 个元素。除非额外指定,新的张量将存储在内存中,并采用基于CPU的计算。
x = torch.arange(12)
x
# tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
# 可以通过张量的shape属性来访问张量(沿每个轴的长度)的形状
x.shape
# torch.Size([12])
x.numel()
# 12
要想改变一个张量的形状而不改变元素数量和元素值,可以调用reshape函数。
我们不需要通过手动指定每个维度来改变形状。 也就是说,如果我们的目标形状是(高度,宽度), 那么在知道宽度后,高度会被自动计算得出,不必我们自己做除法。 在上面的例子中,为了获得一个3行的矩阵,我们手动指定了它有3行和4列。 幸运的是,我们可以通过-1来调用此自动计算出维度的功能。 即我们可以用x.reshape(-1,4)或x.reshape(3,-1)来取代x.reshape(3,4)。
代码语言:javascript复制X = x.reshape(3, 4)
X
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
X.reshape(2,-1)
# tensor([[ 0, 1, 2, 3, 4, 5],
# [ 6, 7, 8, 9, 10, 11]])
有时,我们希望使用全0、全1、其他常量,或者从特定分布中随机采样的数字来初始化矩阵。
代码语言:javascript复制# 创建一个形状为(2,3,4)的张量,其中所有元素都设置为0
torch.zeros((2, 3, 4))
# tensor([[[0., 0., 0., 0.],
# [0., 0., 0., 0.],
# [0., 0., 0., 0.]],
#
# [[0., 0., 0., 0.],
# [0., 0., 0., 0.],
# [0., 0., 0., 0.]]])
# 同理,所有元素设置为1
torch.ones((2, 3, 4))
# tensor([[[1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.]],
#
# [[1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.]]])
# 创建一个形状为(3,4)的张量。 其中的每个元素都从均值为0、标准差为1的标准高斯分布(正态分布)中随机采样。
torch.randn(3, 4)
# tensor([[ 1.1704, -0.4649, -1.1481, -2.0655],
# [-0.1964, 0.3888, 0.2516, -0.5367],
# [-0.3829, -0.0578, 0.8739, -1.4293]])
还可以用Python的嵌套数组来为所需张量中每个元素赋予确定值
代码语言:javascript复制torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
# tensor([[2, 1, 4, 3],
# [1, 2, 3, 4],
# [4, 3, 2, 1]])
运算符
代码语言:javascript复制x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x y, x - y, x * y, x / y, x ** y # **运算符是求幂运算
# (tensor([ 3., 4., 6., 10.]),
# tensor([-1., 0., 2., 6.]),
# tensor([ 2., 4., 8., 16.]),
# tensor([0.5000, 1.0000, 2.0000, 4.0000]),
# tensor([ 1., 4., 16., 64.]))
torch.exp(x)
# tensor([2.7183e 00, 7.3891e 00, 5.4598e 01, 2.9810e 03])
也可以把多个张量连结(concatenate)在一起,把它们端对端地叠起来形成一个更大的张量。只需要提供张量列表,并给出沿哪个轴连结。
下面的例子分别演示了当沿行(轴-0,形状的第一个元素)和按列(轴-1,形状的第二个元素)连结两个矩阵时,会发生什么情况。可以看到,第一个输出张量的轴-0长度(6)是两个输入张量轴-0长度的总和(3 3);第二个输出张量的轴-1长度(8)是两个输入张量轴-1长度的总和(4 4)。
代码语言:javascript复制X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
#(tensor([[ 0., 1., 2., 3.],
# [ 4., 5., 6., 7.],
# [ 8., 9., 10., 11.],
# [ 2., 1., 4., 3.],
# [ 1., 2., 3., 4.],
# [ 4., 3., 2., 1.]]),
# tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.],
# [ 4., 5., 6., 7., 1., 2., 3., 4.],
# [ 8., 9., 10., 11., 4., 3., 2., 1.]]))
简单地说,前者是上下堆叠,后者是左右堆叠。
广播机制
在某些情况下,即使形状不同,我们仍然可以通过调用广播机制(broadcasting mechanism)来执行按元素操作。这种机制的工作方式如下:首先,通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状。其次,对生成的数组执行按元素操作。在大多数情况下,我们将沿着数组中长度为1的轴进行广播,如下例子:
代码语言:javascript复制a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b
#(tensor([[0],
# [1],
# [2]]),
# tensor([[0, 1]]))
a b
# 此处体现了广播机制
# tensor([[0, 1],
# [1, 2],
# [2, 3]])
由于a和b分别是3times1和1times2矩阵,如果让它们相加,它们的形状不匹配。广播机制将两个矩阵广播为一个更大的3times2矩阵,矩阵a将复制列,矩阵b将复制行,然后再按元素相加。
索引和切片
索引和切片操作与Python
和pandas
中的数组操作基本一致。张量中的元素可以通过索引访问,第一个元素的索引是0,最后一个元素索引是-1;可以指定范围以包含第一个元素和最后一个之前的元素。
X[-1], X[1:3]
# (tensor([ 8., 9., 10., 11.]),
# tensor([[ 4., 5., 6., 7.],
# [ 8., 9., 10., 11.]]))
除了读取,还可以进行写入操作
代码语言:javascript复制# 单个赋值
X[1, 2] = 9
X
# tensor([[ 0., 1., 2., 3.],
# [ 4., 5., 9., 7.],
# [ 8., 9., 10., 11.]])
# 多个赋值
X[0:2, :] = 12
X
# tensor([[12., 12., 12., 12.],
# [12., 12., 12., 12.],
# [ 8., 9., 10., 11.]])
节省内存
运行一些操作可能会导致为新结果分配内存。
例如,如果用Y = X Y
,将取消引用Y
指向的张量,而是指向新分配的内存处的张量。
在下面的例子中,用Python的id()
函数演示了这一点,它给我们提供了内存中引用对象的确切地址。运行Y = Y X
后,我们会发现id(Y)
指向另一个位置。这是因为Python首先计算Y X
,为结果分配新的内存,然后使Y
指向内存中的这个新位置。
before = id(Y)
Y = Y X
id(Y) == before
# False
这可能是不可取的,原因有两个:首先,我们不想总是不必要地分配内存。 在机器学习中,我们可能有数百兆的参数,并且在一秒内多次更新所有参数。 通常情况下,我们希望原地执行这些更新。 其次,如果我们不原地更新,其他引用仍然会指向旧的内存位置, 这样我们的某些代码可能会无意中引用旧的参数。
幸运的是,执行原地操作非常简单。我们可以使用切片表示法将操作的结果分配给先前分配的数组,例如Y[:] = <expression>
。
为了说明这一点,首先创建一个新的矩阵Z,其形状与另一个Y相同,使用zeros_like来分配一个全0的块。
代码语言:javascript复制Z = torch._like(Y)
print('id(Z):', id(Z))
Z[:] = X Y
print('id(Z):', id(Z))
# id(Z): 1822658475968
# id(Z): 1822658475968
如果在后续计算中没有重复使用X
,也可以使用X[:] = X Y
或X = Y
来减少操作的内存开销。
before = id(X)
X = Y
id(X) == before
# True
转换为其他Python对象
代码语言:javascript复制A = X.numpy()
B = torch.tensor(A)
type(A), type(B)
# (numpy.ndarray, torch.Tensor)
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
# (tensor([3.5000]), 3.5, 3.5, 3)
小结
深度学习中存储和操作数据的主要接口是张量(n维数组),Pytorch中张量的基本操作与Python数组、Numpy中基本一致,但要特别注意Pytorch中的广播机制。