张量

张量是一种特殊的数据结构,可以将其简单地视为数学中的张量。

先载入一些库:

import torch
import numpy as np

张量可以直接从 Python 原生对象中创建:

data = [[1, 2], [3, 4], [5, 6]]
x_data = torch.tensor(data)

或者,从 Numpy 生成张量:

np_array = np.array(data)
x_np = torch.from_numpy(np_array)

或者,借助一些 torch 函数创建:

x_ones = torch.ones_like(x_data) # 全一张量
x_rand = torch.rand_like(x_data, dtype=torch.float) # 随机张量

可以看看张量的样子:

x_data
tensor([[1, 2],
        [3, 4],
        [5, 6]])

通过张量的 shape 属性来访问张量的 形状 (沿每个轴的长度):

x_data.shape
torch.Size([3, 2])

如果想知道张量中元素的总数,即形状的所有元素乘积,可以:

x_data.numel() # 即 3 * 2
6

查看张量的数据类型和其所属设备:

x_data.dtype, x_data.device
(torch.int64, device(type='cpu'))

张量运算

张量有超过 100 的运算,包括算术,线性代数,矩阵操作(转置,索引,切片),采样等(更加详细的介绍见 torch)。

默认情况下,张量是在 CPU 上创建的。如果想将其移到在 GPU 上,需要借助 .to 方法。注意:跨设备复制大型张量在时间和内存方面是昂贵的!

# 查看 GPU 是否可用
if torch.cuda.is_available():
    # 如果可用,则迁移到 GPU,并打印出来
    x_data = x_data.to('cuda')
    print(x_data)
tensor([[1, 2],
        [3, 4],
        [5, 6]], device='cuda:0')

下面简单的列出一些运算:

tensor = torch.ones(4, 3, dtype=torch.float32)
t1 = torch.cat([tensor, tensor], dim=1) # 拼接
print(t1)

y1 = tensor @ tensor.T # 矩阵乘法
print(y1)
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.]])
tensor([[3., 3., 3., 3.],
        [3., 3., 3., 3.],
        [3., 3., 3., 3.],
        [3., 3., 3., 3.]])

将结果存储到操作张量中的操作称为就地操作(in-place)。它们由 _ 后缀表示。例如:x.copy_(y)x.t_(),将会更改 x

tensor = torch.tensor(7)
print(tensor, "\n")
tensor.add_(5)
print(tensor)
tensor(7) 

tensor(12)

重要

CPU 上的张量和 NumPy 数组上可以共享它们的底层内存位置,改变一个就会改变另一个。

比如,

t = torch.ones(5)
print(f"t: {t}")
n = t.numpy() # 张量转换为 NumPy
print(f"n: {n}")
t.add_(1)
print(f"t: {t}")
print(f"n: {n}")
t: tensor([1., 1., 1., 1., 1.])
n: [1. 1. 1. 1. 1.]
t: tensor([2., 2., 2., 2., 2.])
n: [2. 2. 2. 2. 2.]

节省内存

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

在下面的例子中,我们用 Python 的 id() 函数演示了这一点,它给我们提供了内存中引用对象的确切地址。运行 Y = Y + X 后,我们会发现 id(Y) 指向另一个位置。这是因为 Python 首先计算 Y + X,为结果分配新的内存,然后使 Y 指向内存中的这个新位置。

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

before = id(Y)
Y = Y + X
id(Y) == before
False

这样做是不可取的,原因有两个:首先,我们不想总是不必要地分配内存。在机器学习中,我们可能有数百兆的参数,并且在一秒内多次更新所有参数。通常情况下,我们希望原地执行这些更新。其次,我们可能通过多个变量指向相同参数。如果我们不原地更新,其他引用仍然会指向旧的内存位置,这样我们的某些代码可能会无意中引用旧的参数。

可以使用切片表示法将操作的结果分配给先前分配的数组,例如 Y[:] = <expression>。为了说明这一点,我们首先创建一个新的矩阵 Z,其形状与另一个 Y 相同,使用 zeros_like 来分配一个全 0 的块。

Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))
id(Z): 2933842197696
id(Z): 2933842197696

如果在后续计算中没有重复使用 X,我们也可以使用 X[:] = X + YX += Y 来减少操作的内存开销。

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

当然,就地操作也是符合的。