本节主要介绍深度学习中最基本的数据类型 “张量” 的简单操作。

首先,我们介绍 维数组,也称为张量 tensor。使用过 Python 中 NumPy 计算包的读者会对本部分很熟悉。无论用哪个深度学习框架,它的张量类(在 MXNet 中为 ndarray,在 PyTorch 和 TensorFlow 中为 Tensor 都与 Numpy 的 ndarray 类似。 但深度学习框架又比 Numpy 的 ndarray 多一些重要功能:首先,GPU 很地支持加速计算,而 NumPy 仅支持 CPU 计算; 其次,张量类支持自动微分。这些功能使得张量类更合深度学习。

张量的创建

张量表示一个由数值组成的数组,这个数组可能有多个维度。具有一个轴的张量对应数学上的向量 (vector);具有两个轴的张量对应数学上的矩阵 (matrix);具有两个轴以上的张量没有特殊数学名称。

在 Python 中我们可以通过 Pytorch 包来对张量数据进行操作,首先是创建一个张量。

1
2
3
import torch
x = torch.arange(12)
print(x)

上述代码创建了一个长度为 12,高度为 1 的张量,其中的元素是整数 0 到 1。

1
tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11])

可以通过 shape 属性访问张量的形状 (沿每个轴的长度)。还可以通过 size 属性得到张量的元素总数。

1
print(x.reshape(3,4).shape, x.numel())
1
torch.Size([3,4]), 12

我们可以利用 reshape 操作改变张量的形状,用 zeros 创建值全为零的张量,用 ones 创建值全为 1 的张量。

1
2
3
4
5
6
7
X = x.reshap(3,4)
Y = torch.zeros((2,3,4))
Z = torch.ones((2,3,4))

print(X)
print(Y)
print(Y)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tensor([[ 0,  1,  2,  3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])

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

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 的标准高斯分布(正态分布)中随机采样。

1
print(torch.randn(3, 4))
1
2
3
tensor([[-0.0135,  0.0665,  0.0912,  0.3212],
[ 1.4653, 0.1843, -1.6995, -0.3036],
[ 1.7646, 1.0450, 0.2457, -0.7732]])

当然我们也可以自己指定张量中的值。

1
print(torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]))
1
2
3
tensor([[2, 1, 4, 3],
[1, 2, 3, 4],
[4, 3, 2, 1]])

运算符

对于任意具有相同形状的张量,常见的标准算术运算符 () 都可以被升级为按元素运算。我们可以在同一形状的任意两个张量上调用按元素操作。在下面的例子中,我们使用逗号来表示一个具有 5 个元素的元组,其中每个元素都是按元素操作的结果。

1
2
3
4
5
6
7
8
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])

print(x + y)
print(x - y)
print(x * y)
print(x / y)
print(x ** y) # **运算符是求幂运算
1
2
3
4
5
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.])

“按元素” 方式可以应用更多的计算,包括像求幂这样的一元运算符,下面的代码计算

1
print(torch.exp(x))
1
tensor([2.7183e+00, 7.3891e+00, 5.4598e+01, 2.9810e+03])

我们也可以把多个张量连结 (concatenate) 在一起, 把它们端对端地叠起来形成一个更大的张量。 我们只需要提供张量列表,并给出沿哪个轴连结。下面的例子分别演示了当我们沿行 (轴 - 0,形状的第一个元素) 和按列 (轴 - 1,形状的第二个元素) 连结两个矩阵时,会发生什么情况。 我们可以看到,第一个输出张量的轴 - 0 长度 (6) 是两个输入张量轴 - 0 长度的总和 ();第二个输出张量的轴 - 1 长度 (8) 是两个输入张量轴 - 1 长度的总和 ()。

1
2
3
4
5
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]])

print(X)
print(Y)
1
2
3
4
5
6
7
8
9
10
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.]])

我们还可以根据逻辑运算符来创建张量。

1
print(X == Y)

这样的到的张量是对张量 按元素进行逻辑运算得到的。

1
2
3
tensor([[False,  True, False,  True],
[False, False, False, False],
[False, False, False, False]])

对张量中的元素求和,可以产生一个单元素张量。

1
print(X.sum())
1
tensor(66.)

广播机制

在上面的部分中,我们看到了如何在相同形状的两个张量上执行按元素操作。 在某些情况下,即使形状不同,我们仍然可以通过调用 广播机制 (broadcasting mechanism) 来执行按元素操作。 这种机制的工作方式如下:

  • 通过适当复制元素来扩展一个或两个数组,以便在转换之后,两个张量具有相同的形状;
  • 对生成的数组执行按元素操作。

在大多数情况下,我们将沿着数组中长度为 1 的轴进行广播,如下例子:

1
2
3
4
5
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))

print(a)
print(b)
1
2
3
4
5
tensor([[0],
[1],
[2]])

tensor([[0, 1]])

由于 a 和 b 分别是 矩阵,如果让它们相加,它们的形状不匹配。 我们将两个矩阵广播为一个更大的 矩阵,如下所示:矩阵 a 将复制列,矩阵 b 将复制行,然后再按元素相加。

1
print(a + b)
1
2
3
tensor([[0, 1],
[1, 2],
[2, 3]])

索引和切片

索引

张量中的元素可以通过索引访问,第一个元素的索引是 0,最后一个元素索引是 - 1,也可以指定范围以包含第一个元素和最后一个之前的元素。

如下所示,我们可以用 [-1] 选择最后一个元素,可以用 [1:3] 选择第二个和第三个元素:

1
2
print(X[-1])
print(X[1:3])
1
2
3
4
tensor([ 8.,  9., 10., 11.]),

tensor([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])

除读取外,我们还可以通过指定索引来将元素写入矩阵。

1
2
X[1, 2] = 9
print(X)
1
2
3
tensor([[ 0.,  1.,  2.,  3.],
[ 4., 5., 9., 7.],
[ 8., 9., 10., 11.]])

切片

如果我们想为多个元素赋值相同的值,我们只需要索引所有元素,然后为它们赋值。例如,[0:2, :] 访问第 1 行和第 2 行,其中 : 代表沿轴 1 (列) 的所有元素。虽然我们讨论的是矩阵的索引,但这也适用于向量和超过 2 个维度的张量。

1
2
X[0:2, :] = 12
print(X)
1
2
3
tensor([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[ 8., 9., 10., 11.]])

节省内存

运行一些操作可能会导致为新结果分配内存。例如,如果我们用 Y = X + Y,我们将取消引用 Y 指向的张量,而是指向新分配的内存处的张量。

可以用 X[:] = X + YX += Y 来减少操作的内存开销。

1
2
3
before = id(X)
X += Y
print(id(X) == before)
1
True

转换为其他 Python 对象

将深度学习框架定义的张量转换为 NumPy 张量 ndarray 很容易,反之也同样容易。torch 张量和 numpy 数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。

1
2
3
4
A = X.numpy()
B = torch.tensor(A)
print(type(A))
print(type(B))
1
2
numpy.ndarray
torch.Tensor

要将大小为 1 的张量转换为 Python 标量,我们可以调用 item 函数或 Python 的内置函数。

1
2
a = torch.tensor([3.5])
print(a, a.item(), float(a), int(a))
1
(tensor([3.5000]), 3.5, 3.5, 3)