评价此页

PyTorch 简介#

创建于:2017年4月8日 | 最后更新:2021年6月24日 | 最后验证:2024年11月5日

Torch 张量库简介#

所有的深度学习都是对张量(tensor)的计算,张量是矩阵的推广,可以被2个以上的维度索引。稍后我们将深入探讨这究竟意味着什么。首先,让我们看看我们可以用张量做什么。

# Author: Robert Guthrie

import torch

torch.manual_seed(1)
<torch._C.Generator object at 0x7f9e555586b0>

创建张量#

可以使用 torch.tensor() 函数从 Python 列表创建张量。

# torch.tensor(data) creates a torch.Tensor object with the given data.
V_data = [1., 2., 3.]
V = torch.tensor(V_data)
print(V)

# Creates a matrix
M_data = [[1., 2., 3.], [4., 5., 6]]
M = torch.tensor(M_data)
print(M)

# Create a 3D tensor of size 2x2x2.
T_data = [[[1., 2.], [3., 4.]],
          [[5., 6.], [7., 8.]]]
T = torch.tensor(T_data)
print(T)
tensor([1., 2., 3.])
tensor([[1., 2., 3.],
        [4., 5., 6.]])
tensor([[[1., 2.],
         [3., 4.]],

        [[5., 6.],
         [7., 8.]]])

到底什么是3D张量?可以这样想。如果你有一个向量,对向量进行索引会得到一个标量。如果你有一个矩阵,对矩阵进行索引会得到一个向量。如果你有一个3D张量,那么对张量进行索引会得到一个矩阵!

关于术语的说明:在本教程中,当我提到“张量”时,它指的是任何 torch.Tensor 对象。矩阵和向量是 torch.Tensor 的特例,它们的维度分别为2和1。当我谈论3D张量时,我会明确使用“3D张量”这个术语。

# Index into V and get a scalar (0 dimensional tensor)
print(V[0])
# Get a Python number from it
print(V[0].item())

# Index into M and get a vector
print(M[0])

# Index into T and get a matrix
print(T[0])
tensor(1.)
1.0
tensor([1., 2., 3.])
tensor([[1., 2.],
        [3., 4.]])

你还可以创建其他数据类型的张量。要创建整数类型的张量,请尝试 torch.tensor([[1, 2], [3, 4]])(其中列表中的所有元素都是整数)。你还可以通过传入 dtype=torch.data_type 来指定数据类型。请查看文档以了解更多数据类型,但 Float 和 Long 将是最常见的。

你可以使用 torch.randn() 创建一个具有随机数据和指定维度的张量。

x = torch.randn((3, 4, 5))
print(x)
tensor([[[-1.5256, -0.7502, -0.6540, -1.6095, -0.1002],
         [-0.6092, -0.9798, -1.6091, -0.7121,  0.3037],
         [-0.7773, -0.2515, -0.2223,  1.6871,  0.2284],
         [ 0.4676, -0.6970, -1.1608,  0.6995,  0.1991]],

        [[ 0.8657,  0.2444, -0.6629,  0.8073,  1.1017],
         [-0.1759, -2.2456, -1.4465,  0.0612, -0.6177],
         [-0.7981, -0.1316,  1.8793, -0.0721,  0.1578],
         [-0.7735,  0.1991,  0.0457,  0.1530, -0.4757]],

        [[-0.1110,  0.2927, -0.1578, -0.0288,  0.4533],
         [ 1.1422,  0.2486, -1.7754, -0.0255, -1.0233],
         [-0.5962, -1.0055,  0.4285,  1.4761, -1.7869],
         [ 1.6103, -0.7040, -0.1853, -0.9962, -0.8313]]])

张量运算#

你可以按预期的方式对张量进行操作。

x = torch.tensor([1., 2., 3.])
y = torch.tensor([4., 5., 6.])
z = x + y
print(z)
tensor([5., 7., 9.])

请参阅文档以获取可用的海量操作的完整列表。它们不仅仅局限于数学运算。

我们稍后将使用的一个有用的操作是连接(concatenation)。

# By default, it concatenates along the first axis (concatenates rows)
x_1 = torch.randn(2, 5)
y_1 = torch.randn(3, 5)
z_1 = torch.cat([x_1, y_1])
print(z_1)

# Concatenate columns:
x_2 = torch.randn(2, 3)
y_2 = torch.randn(2, 5)
# second arg specifies which axis to concat along
z_2 = torch.cat([x_2, y_2], 1)
print(z_2)

# If your tensors are not compatible, torch will complain.  Uncomment to see the error
# torch.cat([x_1, x_2])
tensor([[-0.8029,  0.2366,  0.2857,  0.6898, -0.6331],
        [ 0.8795, -0.6842,  0.4533,  0.2912, -0.8317],
        [-0.5525,  0.6355, -0.3968, -0.6571, -1.6428],
        [ 0.9803, -0.0421, -0.8206,  0.3133, -1.1352],
        [ 0.3773, -0.2824, -2.5667, -1.4303,  0.5009]])
tensor([[ 0.5438, -0.4057,  1.1341, -0.1473,  0.6272,  1.0935,  0.0939,  1.2381],
        [-1.1115,  0.3501, -0.7703, -1.3459,  0.5119, -0.6933, -0.1668, -0.9999]])

重塑张量#

使用 .view() 方法来重塑张量。这个方法被大量使用,因为许多神经网络组件期望它们的输入具有特定的形状。通常,在将数据传递给组件之前,你需要进行重塑。

x = torch.randn(2, 3, 4)
print(x)
print(x.view(2, 12))  # Reshape to 2 rows, 12 columns
# Same as above.  If one of the dimensions is -1, its size can be inferred
print(x.view(2, -1))
tensor([[[ 0.4175, -0.2127, -0.8400, -0.4200],
         [-0.6240, -0.9773,  0.8748,  0.9873],
         [-0.0594, -2.4919,  0.2423,  0.2883]],

        [[-0.1095,  0.3126,  1.5038,  0.5038],
         [ 0.6223, -0.4481, -0.2856,  0.3880],
         [-1.1435, -0.6512, -0.1032,  0.6937]]])
tensor([[ 0.4175, -0.2127, -0.8400, -0.4200, -0.6240, -0.9773,  0.8748,  0.9873,
         -0.0594, -2.4919,  0.2423,  0.2883],
        [-0.1095,  0.3126,  1.5038,  0.5038,  0.6223, -0.4481, -0.2856,  0.3880,
         -1.1435, -0.6512, -0.1032,  0.6937]])
tensor([[ 0.4175, -0.2127, -0.8400, -0.4200, -0.6240, -0.9773,  0.8748,  0.9873,
         -0.0594, -2.4919,  0.2423,  0.2883],
        [-0.1095,  0.3126,  1.5038,  0.5038,  0.6223, -0.4481, -0.2856,  0.3880,
         -1.1435, -0.6512, -0.1032,  0.6937]])

计算图和自动微分#

计算图的概念对于高效的深度学习编程至关重要,因为它使你无需自己编写反向传播的梯度。计算图只是一个关于你的数据如何组合以得到输出的规范。由于该图完全指定了哪些参数参与了哪些运算,它包含了足够的信息来计算导数。这可能听起来很模糊,所以让我们使用基本标志 requires_grad 来看看到底发生了什么。

首先,从程序员的角度思考。我们上面创建的 torch.Tensor 对象中存储了什么?显然是数据和形状,可能还有其他一些东西。但是当我们将两个张量相加时,我们得到了一个输出张量。这个输出张量只知道它的数据和形状。它不知道自己是另外两个张量的和(它可能是从文件中读取的,也可能是其他一些运算的结果,等等)。

如果 requires_grad=True,Tensor 对象会跟踪它是如何被创建的。让我们看看实际效果。

# Tensor factory methods have a ``requires_grad`` flag
x = torch.tensor([1., 2., 3], requires_grad=True)

# With requires_grad=True, you can still do all the operations you previously
# could
y = torch.tensor([4., 5., 6], requires_grad=True)
z = x + y
print(z)

# BUT z knows something extra.
print(z.grad_fn)
tensor([5., 7., 9.], grad_fn=<AddBackward0>)
<AddBackward0 object at 0x7f9e15020e50>

所以张量知道是什么创建了它们。z 知道它不是从文件中读入的,它不是乘法、指数或其他任何运算的结果。如果你一直跟踪 z.grad_fn,你会发现自己最终会回到 x 和 y。

但这如何帮助我们计算梯度呢?

# Let's sum up all the entries in z
s = z.sum()
print(s)
print(s.grad_fn)
tensor(21., grad_fn=<SumBackward0>)
<SumBackward0 object at 0x7f9e150207f0>

那么现在,这个和关于 x 的第一个分量的导数是什么?在数学上,我们想要

\[\frac{\partial s}{\partial x_0}\]

嗯,s 知道它是作为张量 z 的和创建的。z 知道它是 x + y 的和。所以

\[s = \overbrace{x_0 + y_0}^\text{$z_0$} + \overbrace{x_1 + y_1}^\text{$z_1$} + \overbrace{x_2 + y_2}^\text{$z_2$} \]

因此 s 包含了足够的信息来确定我们想要的导数是 1!

当然,这忽略了如何实际计算该导数的挑战。这里的重点是 s 携带了足够的信息,使得计算它成为可能。实际上,Pytorch 的开发者对 sum() 和 + 操作进行了编程,使其知道如何计算它们的梯度,并运行反向传播算法。对该算法的深入讨论超出了本教程的范围。

让我们让 Pytorch 计算梯度,看看我们是否正确:(注意,如果你多次运行此代码块,梯度将会累加。这是因为 Pytorch 将梯度*累积*到 .grad 属性中,因为对于许多模型来说,这样做非常方便。)

# calling .backward() on any variable will run backprop, starting from it.
s.backward()
print(x.grad)
tensor([1., 1., 1.])

理解下面代码块中发生的事情对于成为一名成功的深度学习程序员至关重要。

x = torch.randn(2, 2)
y = torch.randn(2, 2)
# By default, user created Tensors have ``requires_grad=False``
print(x.requires_grad, y.requires_grad)
z = x + y
# So you can't backprop through z
print(z.grad_fn)

# ``.requires_grad_( ... )`` changes an existing Tensor's ``requires_grad``
# flag in-place. The input flag defaults to ``True`` if not given.
x = x.requires_grad_()
y = y.requires_grad_()
# z contains enough information to compute gradients, as we saw above
z = x + y
print(z.grad_fn)
# If any input to an operation has ``requires_grad=True``, so will the output
print(z.requires_grad)

# Now z has the computation history that relates itself to x and y
# Can we just take its values, and **detach** it from its history?
new_z = z.detach()

# ... does new_z have information to backprop to x and y?
# NO!
print(new_z.grad_fn)
# And how could it? ``z.detach()`` returns a tensor that shares the same storage
# as ``z``, but with the computation history forgotten. It doesn't know anything
# about how it was computed.
# In essence, we have broken the Tensor away from its past history
False False
None
<AddBackward0 object at 0x7f9e15021270>
True
None

你还可以通过将代码块包装在 with torch.no_grad(): 中,来阻止 autograd 跟踪带有 .requires_grad=True 的张量的历史记录。

print(x.requires_grad)
print((x ** 2).requires_grad)

with torch.no_grad():
    print((x ** 2).requires_grad)
True
True
False

脚本总运行时间: (0 分 0.123 秒)