导入包

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

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、其他常量,或者从特定分布中随机采样的数字来初始化矩阵。

# 创建一个形状为(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的嵌套数组来为所需张量中每个元素赋予确定值

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

运算符

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$)。

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的轴进行广播,如下例子:

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

由于ab分别是$3\times1$和$1\times2$矩阵,如果让它们相加,它们的形状不匹配。广播机制将两个矩阵广播为一个更大的$3\times2$矩阵,矩阵a将复制列,矩阵b将复制行,然后再按元素相加。

索引和切片

索引和切片操作与Pythonpandas中的数组操作基本一致。张量中的元素可以通过索引访问,第一个元素的索引是0,最后一个元素索引是-1;可以指定范围以包含第一个元素和最后一个之前的元素。

X[-1], X[1:3]
# (tensor([ 8.,  9., 10., 11.]),
# tensor([[ 4.,  5.,  6.,  7.],
#         [ 8.,  9., 10., 11.]]))

除了读取,还可以进行写入操作

# 单个赋值
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$的块。

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 + YX += Y来减少操作的内存开销。

before = id(X)
X += Y
id(X) == before
# True

转换为其他Python对象

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中的广播机制。

最后修改:2022 年 04 月 24 日
如果觉得我的文章对你有用,请随意赞赏