注意
转到末尾 下载完整的示例代码。
简介 || **张量** || Autograd || 构建模型 || TensorBoard 支持 || 训练模型 || 模型理解
PyTorch 张量入门#
创建日期: 2021年11月30日 | 最后更新: 2025年1月29日 | 最后验证: 2024年11月05日
请跟随下面的视频或在 YouTube 上观看。
张量是 PyTorch 中的核心数据抽象。本交互式笔记本提供了对 torch.Tensor
类的深入介绍。
首先,让我们导入 PyTorch 模块。我们还将添加 Python 的 math 模块以方便示例。
import torch
import math
创建张量#
创建张量的最简单方法是调用 torch.empty()
x = torch.empty(3, 4)
print(type(x))
print(x)
<class 'torch.Tensor'>
tensor([[3.6009e+24, 4.5688e-41, 9.1349e-38, 4.5689e-41],
[1.9773e+24, 4.5688e-41, 1.9559e+24, 4.5688e-41],
[2.8522e+24, 4.5688e-41, 1.8730e+24, 4.5688e-41]])
让我们来看看我们刚才做了什么
我们使用 `torch` 模块附带的众多工厂方法之一创建了一个张量。
张量本身是 2 维的,有 3 行 4 列。
返回对象的类型是
torch.Tensor
,它是torch.FloatTensor
的别名;默认情况下,PyTorch 张量用 32 位浮点数填充。(更多关于数据类型的信息如下。)打印张量时,您可能会看到一些随机值。
torch.empty()
调用为张量分配内存,但不会用任何值初始化它——所以您看到的是分配时内存中的任何内容。
关于张量及其维数以及术语的简要说明
您有时会看到一维张量被称为 *向量*。
同样,二维张量通常被称为 *矩阵*。
任何超过两个维度的都通常只称为张量。
大多数情况下,您会希望用某个值初始化张量。常见的情况是全零、全一或随机值,并且 `torch` 模块为所有这些情况提供了工厂方法。
zeros = torch.zeros(2, 3)
print(zeros)
ones = torch.ones(2, 3)
print(ones)
torch.manual_seed(1729)
random = torch.rand(2, 3)
print(random)
tensor([[0., 0., 0.],
[0., 0., 0.]])
tensor([[1., 1., 1.],
[1., 1., 1.]])
tensor([[0.3126, 0.3791, 0.3087],
[0.0736, 0.4216, 0.0691]])
工厂方法的作用正如您所料——我们有一个全零的张量,一个全一的张量,还有一个在 0 到 1 之间的随机值张量。
随机张量和种子#
说到随机张量,您是否注意到它前面有 `torch.manual_seed()` 的调用?用随机值初始化张量,例如模型的学习权重,是很常见的,但在某些情况下——尤其是在研究环境中——您会希望确保结果的可复现性。手动设置随机数生成器的种子是做到这一点的方法。让我们更仔细地看看。
torch.manual_seed(1729)
random1 = torch.rand(2, 3)
print(random1)
random2 = torch.rand(2, 3)
print(random2)
torch.manual_seed(1729)
random3 = torch.rand(2, 3)
print(random3)
random4 = torch.rand(2, 3)
print(random4)
tensor([[0.3126, 0.3791, 0.3087],
[0.0736, 0.4216, 0.0691]])
tensor([[0.2332, 0.4047, 0.2162],
[0.9927, 0.4128, 0.5938]])
tensor([[0.3126, 0.3791, 0.3087],
[0.0736, 0.4216, 0.0691]])
tensor([[0.2332, 0.4047, 0.2162],
[0.9927, 0.4128, 0.5938]])
上面您应该看到 `random1` 和 `random3` 的值相同,`random2` 和 `random4` 也相同。手动设置 RNG 的种子会重置它,因此在大多数情况下,依赖随机数的相同计算应该会提供相同的结果。
有关更多信息,请参阅 PyTorch 关于可复现性的文档。
张量形状#
通常,当您对两个或多个张量执行操作时,它们需要具有相同的 *形状*——也就是说,具有相同的维数并且在每个维度的单元数相同。为此,我们有 `torch.*_like()` 方法。
x = torch.empty(2, 2, 3)
print(x.shape)
print(x)
empty_like_x = torch.empty_like(x)
print(empty_like_x.shape)
print(empty_like_x)
zeros_like_x = torch.zeros_like(x)
print(zeros_like_x.shape)
print(zeros_like_x)
ones_like_x = torch.ones_like(x)
print(ones_like_x.shape)
print(ones_like_x)
rand_like_x = torch.rand_like(x)
print(rand_like_x.shape)
print(rand_like_x)
torch.Size([2, 2, 3])
tensor([[[-1.2732e-15, 4.5689e-41, 3.6110e-20],
[ 3.0750e-41, 1.4013e-45, 0.0000e+00]],
[[ 1.4013e-45, 0.0000e+00, 1.4013e-45],
[ 0.0000e+00, 1.4013e-45, 0.0000e+00]]])
torch.Size([2, 2, 3])
tensor([[[1.6556e+03, 3.0757e-41, 0.0000e+00],
[1.5046e-36, 6.2161e-25, 3.0750e-41]],
[[1.0842e-19, 0.0000e+00, 3.9714e-20],
[3.0750e-41, 4.3029e-20, 3.0750e-41]]])
torch.Size([2, 2, 3])
tensor([[[0., 0., 0.],
[0., 0., 0.]],
[[0., 0., 0.],
[0., 0., 0.]]])
torch.Size([2, 2, 3])
tensor([[[1., 1., 1.],
[1., 1., 1.]],
[[1., 1., 1.],
[1., 1., 1.]]])
torch.Size([2, 2, 3])
tensor([[[0.6128, 0.1519, 0.0453],
[0.5035, 0.9978, 0.3884]],
[[0.6929, 0.1703, 0.1384],
[0.4759, 0.7481, 0.0361]]])
代码单元格中的第一个新内容是张量上 `.shape` 属性的使用。此属性包含张量每个维度的范围列表——在我们的例子中,`x` 是一个三维张量,形状为 2 x 2 x 3。
在此之下,我们调用了 `.empty_like()`、`.zeros_like()`、`.ones_like()` 和 `.rand_like()` 方法。使用 `.shape` 属性,我们可以验证每个方法都返回具有相同维数和范围的张量。
我们将介绍的最后一种创建张量的方法是从 PyTorch 集合直接指定其数据。
some_constants = torch.tensor([[3.1415926, 2.71828], [1.61803, 0.0072897]])
print(some_constants)
some_integers = torch.tensor((2, 3, 5, 7, 11, 13, 17, 19))
print(some_integers)
more_integers = torch.tensor(((2, 4, 6), [3, 6, 9]))
print(more_integers)
tensor([[3.1416, 2.7183],
[1.6180, 0.0073]])
tensor([ 2, 3, 5, 7, 11, 13, 17, 19])
tensor([[2, 4, 6],
[3, 6, 9]])
如果您已经有了 Python 元组或列表中的数据,使用 `torch.tensor()` 是创建张量最直接的方法。如上所示,嵌套集合将产生多维张量。
注意
`torch.tensor()` 创建数据的副本。
张量数据类型#
张量的数据类型可以通过几种方式设置。
a = torch.ones((2, 3), dtype=torch.int16)
print(a)
b = torch.rand((2, 3), dtype=torch.float64) * 20.
print(b)
c = b.to(torch.int32)
print(c)
tensor([[1, 1, 1],
[1, 1, 1]], dtype=torch.int16)
tensor([[ 0.9956, 1.4148, 5.8364],
[11.2406, 11.2083, 11.6692]], dtype=torch.float64)
tensor([[ 0, 1, 5],
[11, 11, 11]], dtype=torch.int32)
设置张量底层数据类型的最简单方法是在创建时使用可选参数。在上面的单元格的第一行,我们为张量 `a` 设置了 `dtype=torch.int16`。当我们打印 `a` 时,我们可以看到它充满了 `1` 而不是 `1.`——这是 Python 细微的提示,表明这是一个整数类型而不是浮点数。
关于打印 `a` 的另一件事是,与我们默认将 `dtype` 设置为(32 位浮点数)不同,打印张量时还会指定其 `dtype`。
您可能还注意到,我们从将张量的形状指定为一系列整数参数,改为将这些参数分组到一个元组中。这并非严格必要——PyTorch 会将一系列初始的、未标记的整数参数视为张量形状——但添加可选参数时,可以使您的意图更具可读性。
另一种设置数据类型的方法是使用 `.to()` 方法。在上面的单元格中,我们以通常的方式创建一个随机浮点张量 `b`。之后,我们通过使用 `.to()` 方法将 `b` 转换为 32 位整数来创建 `c`。请注意,`c` 包含与 `b` 相同的值,但截断为整数。
有关更多信息,请参阅数据类型文档。
PyTorch 张量的数学和逻辑运算#
既然您知道了一些创建张量的方法……您可以用它们做什么?
首先,让我们看看基本算术以及张量如何与简单标量进行交互。
ones = torch.zeros(2, 2) + 1
twos = torch.ones(2, 2) * 2
threes = (torch.ones(2, 2) * 7 - 1) / 2
fours = twos ** 2
sqrt2s = twos ** 0.5
print(ones)
print(twos)
print(threes)
print(fours)
print(sqrt2s)
tensor([[1., 1.],
[1., 1.]])
tensor([[2., 2.],
[2., 2.]])
tensor([[3., 3.],
[3., 3.]])
tensor([[4., 4.],
[4., 4.]])
tensor([[1.4142, 1.4142],
[1.4142, 1.4142]])
如上所示,张量和标量之间的算术运算,例如加法、减法、乘法、除法和指数运算,会分布到张量的每个元素上。因为此类运算的输出将是一个张量,所以您可以按照通常的运算符优先级将它们链接在一起,就像我们在创建 `threes` 的那一行中一样。
两个张量之间的类似操作也如您直观期望的那样工作。
tensor([[ 2., 4.],
[ 8., 16.]])
tensor([[5., 5.],
[5., 5.]])
tensor([[12., 12.],
[12., 12.]])
在此需要注意的是,前一个代码单元中的所有张量形状都相同。当我们在形状不同的张量上尝试执行二元运算时会发生什么?
注意
下面的单元格会引发运行时错误。这是故意的。
a = torch.rand(2, 3)
b = torch.rand(3, 2)
print(a * b)
在一般情况下,您不能这样对不同形状的张量进行运算,即使在上面的单元格中,张量具有相同的元素数量也是如此。
简而言之:张量广播#
注意
如果您熟悉 NumPy ndarrays 中的广播语义,您会发现这里的规则是相同的。
形状相同的规则的例外是 *张量广播*。这是一个例子。
rand = torch.rand(2, 4)
doubled = rand * (torch.ones(1, 4) * 2)
print(rand)
print(doubled)
tensor([[0.6146, 0.5999, 0.5013, 0.9397],
[0.8656, 0.5207, 0.6865, 0.3614]])
tensor([[1.2291, 1.1998, 1.0026, 1.8793],
[1.7312, 1.0413, 1.3730, 0.7228]])
这里的诀窍是什么?我们如何将一个 2x4 的张量乘以一个 1x4 的张量?
广播是一种在形状相似的张量之间执行操作的方法。在上例中,一行的四列张量乘以两行四列张量的 *两行*。
这是深度学习中的一项重要操作。常见的例子是将权重张量乘以输入张量的 *批次*,将操作分别应用于批次中的每个实例,并返回相同形状的张量——就像我们上面 (2, 4) * (1, 4) 的例子返回一个形状为 (2, 4) 的张量一样。
广播的规则是:
每个张量必须至少有一个维度——没有空张量。
比较两个张量的维度大小,*从最后一个维度到第一个维度*:
每个维度必须相等,*或者*
其中一个维度的大小必须为 1,*或者*
维度在其中一个张量中不存在
相同形状的张量当然可以被“广播”,正如您之前所见。
以下是一些遵循上述规则并允许广播的情况示例。
a = torch.ones(4, 3, 2)
b = a * torch.rand( 3, 2) # 3rd & 2nd dims identical to a, dim 1 absent
print(b)
c = a * torch.rand( 3, 1) # 3rd dim = 1, 2nd dim identical to a
print(c)
d = a * torch.rand( 1, 2) # 3rd dim identical to a, 2nd dim = 1
print(d)
tensor([[[0.6493, 0.2633],
[0.4762, 0.0548],
[0.2024, 0.5731]],
[[0.6493, 0.2633],
[0.4762, 0.0548],
[0.2024, 0.5731]],
[[0.6493, 0.2633],
[0.4762, 0.0548],
[0.2024, 0.5731]],
[[0.6493, 0.2633],
[0.4762, 0.0548],
[0.2024, 0.5731]]])
tensor([[[0.7191, 0.7191],
[0.4067, 0.4067],
[0.7301, 0.7301]],
[[0.7191, 0.7191],
[0.4067, 0.4067],
[0.7301, 0.7301]],
[[0.7191, 0.7191],
[0.4067, 0.4067],
[0.7301, 0.7301]],
[[0.7191, 0.7191],
[0.4067, 0.4067],
[0.7301, 0.7301]]])
tensor([[[0.6276, 0.7357],
[0.6276, 0.7357],
[0.6276, 0.7357]],
[[0.6276, 0.7357],
[0.6276, 0.7357],
[0.6276, 0.7357]],
[[0.6276, 0.7357],
[0.6276, 0.7357],
[0.6276, 0.7357]],
[[0.6276, 0.7357],
[0.6276, 0.7357],
[0.6276, 0.7357]]])
仔细查看上面每个张量的值。
创建 `b` 的乘法运算被广播到了 `a` 的每个“层”上。
对于 `c`,运算被广播到了 `a` 的每个层和行上——每 3 个元素的列都相同。
对于 `d`,我们进行了切换——现在每 *行* 都相同,跨越层和列。
有关广播的更多信息,请参阅 PyTorch 关于该主题的文档。
以下是一些广播失败的尝试示例。
注意
下面的单元格会引发运行时错误。这是故意的。
a = torch.ones(4, 3, 2)
b = a * torch.rand(4, 3) # dimensions must match last-to-first
c = a * torch.rand( 2, 3) # both 3rd & 2nd dims different
d = a * torch.rand((0, )) # can't broadcast with an empty tensor
张量的更多数学运算#
PyTorch 张量有超过三百种可以对其执行的操作。
以下是主要操作类别中的一小部分示例。
# common functions
a = torch.rand(2, 4) * 2 - 1
print('Common functions:')
print(torch.abs(a))
print(torch.ceil(a))
print(torch.floor(a))
print(torch.clamp(a, -0.5, 0.5))
# trigonometric functions and their inverses
angles = torch.tensor([0, math.pi / 4, math.pi / 2, 3 * math.pi / 4])
sines = torch.sin(angles)
inverses = torch.asin(sines)
print('\nSine and arcsine:')
print(angles)
print(sines)
print(inverses)
# bitwise operations
print('\nBitwise XOR:')
b = torch.tensor([1, 5, 11])
c = torch.tensor([2, 7, 10])
print(torch.bitwise_xor(b, c))
# comparisons:
print('\nBroadcasted, element-wise equality comparison:')
d = torch.tensor([[1., 2.], [3., 4.]])
e = torch.ones(1, 2) # many comparison ops support broadcasting!
print(torch.eq(d, e)) # returns a tensor of type bool
# reductions:
print('\nReduction ops:')
print(torch.max(d)) # returns a single-element tensor
print(torch.max(d).item()) # extracts the value from the returned tensor
print(torch.mean(d)) # average
print(torch.std(d)) # standard deviation
print(torch.prod(d)) # product of all numbers
print(torch.unique(torch.tensor([1, 2, 1, 2, 1, 2]))) # filter unique elements
# vector and linear algebra operations
v1 = torch.tensor([1., 0., 0.]) # x unit vector
v2 = torch.tensor([0., 1., 0.]) # y unit vector
m1 = torch.rand(2, 2) # random matrix
m2 = torch.tensor([[3., 0.], [0., 3.]]) # three times identity matrix
print('\nVectors & Matrices:')
print(torch.linalg.cross(v2, v1)) # negative of z unit vector (v1 x v2 == -v2 x v1)
print(m1)
m3 = torch.linalg.matmul(m1, m2)
print(m3) # 3 times m1
print(torch.linalg.svd(m3)) # singular value decomposition
Common functions:
tensor([[0.9238, 0.5724, 0.0791, 0.2629],
[0.1986, 0.4439, 0.6434, 0.4776]])
tensor([[-0., -0., 1., -0.],
[-0., 1., 1., -0.]])
tensor([[-1., -1., 0., -1.],
[-1., 0., 0., -1.]])
tensor([[-0.5000, -0.5000, 0.0791, -0.2629],
[-0.1986, 0.4439, 0.5000, -0.4776]])
Sine and arcsine:
tensor([0.0000, 0.7854, 1.5708, 2.3562])
tensor([0.0000, 0.7071, 1.0000, 0.7071])
tensor([0.0000, 0.7854, 1.5708, 0.7854])
Bitwise XOR:
tensor([3, 2, 1])
Broadcasted, element-wise equality comparison:
tensor([[ True, False],
[False, False]])
Reduction ops:
tensor(4.)
4.0
tensor(2.5000)
tensor(1.2910)
tensor(24.)
tensor([1, 2])
Vectors & Matrices:
tensor([ 0., 0., -1.])
tensor([[0.7375, 0.8328],
[0.8444, 0.2941]])
tensor([[2.2125, 2.4985],
[2.5332, 0.8822]])
torch.return_types.linalg_svd(
U=tensor([[-0.7889, -0.6145],
[-0.6145, 0.7889]]),
S=tensor([4.1498, 1.0548]),
Vh=tensor([[-0.7957, -0.6056],
[ 0.6056, -0.7957]]))
这是一个操作的小样本。有关更多详细信息和完整的数学函数列表,请查看文档。有关更多详细信息和完整的线性代数运算列表,请查看此文档。
原地修改张量#
大多数张量的二元运算将返回第三个新张量。当我们说 `c = a * b`(其中 `a` 和 `b` 是张量)时,新张量 `c` 将占据一个独立于其他张量的内存区域。
但是,有时您可能希望就地修改张量——例如,如果您正在进行逐元素计算,并且可以丢弃中间值。为此,大多数数学函数都有一个带有附加下划线 (`_`) 的版本,它会就地修改张量。
例如
a = torch.tensor([0, math.pi / 4, math.pi / 2, 3 * math.pi / 4])
print('a:')
print(a)
print(torch.sin(a)) # this operation creates a new tensor in memory
print(a) # a has not changed
b = torch.tensor([0, math.pi / 4, math.pi / 2, 3 * math.pi / 4])
print('\nb:')
print(b)
print(torch.sin_(b)) # note the underscore
print(b) # b has changed
a:
tensor([0.0000, 0.7854, 1.5708, 2.3562])
tensor([0.0000, 0.7071, 1.0000, 0.7071])
tensor([0.0000, 0.7854, 1.5708, 2.3562])
b:
tensor([0.0000, 0.7854, 1.5708, 2.3562])
tensor([0.0000, 0.7071, 1.0000, 0.7071])
tensor([0.0000, 0.7071, 1.0000, 0.7071])
对于算术运算,有一些函数具有类似的功能。
Before:
tensor([[1., 1.],
[1., 1.]])
tensor([[0.3788, 0.4567],
[0.0649, 0.6677]])
After adding:
tensor([[1.3788, 1.4567],
[1.0649, 1.6677]])
tensor([[1.3788, 1.4567],
[1.0649, 1.6677]])
tensor([[0.3788, 0.4567],
[0.0649, 0.6677]])
After multiplying
tensor([[0.1435, 0.2086],
[0.0042, 0.4459]])
tensor([[0.1435, 0.2086],
[0.0042, 0.4459]])
请注意,这些就地算术函数是 `torch.Tensor` 对象上的方法,而不是像许多其他函数(例如 `torch.sin()`)那样附加到 `torch` 模块。从 `a.add_(b)` 中可以看到,*调用张量是就地更改的那个*。
还有一个用于将计算结果放置在现有已分配张量中的选项。我们迄今为止看到的许多方法和函数——包括创建方法!——都有一个 `out` 参数,允许您指定一个接收输出的张量。如果 `out` 张量具有正确的形状和 `dtype`,则可以实现这一点,而无需进行新的内存分配。
a = torch.rand(2, 2)
b = torch.rand(2, 2)
c = torch.zeros(2, 2)
old_id = id(c)
print(c)
d = torch.matmul(a, b, out=c)
print(c) # contents of c have changed
assert c is d # test c & d are same object, not just containing equal values
assert id(c) == old_id # make sure that our new c is the same object as the old one
torch.rand(2, 2, out=c) # works for creation too!
print(c) # c has changed again
assert id(c) == old_id # still the same object!
tensor([[0., 0.],
[0., 0.]])
tensor([[0.3653, 0.8699],
[0.2364, 0.3604]])
tensor([[0.0776, 0.4004],
[0.9877, 0.0352]])
复制张量#
与 Python 中的任何对象一样,将张量分配给变量会使该变量成为张量的 *标签*,而不是复制它。例如。
tensor([[ 1., 561.],
[ 1., 1.]])
但是,如果您想拥有一个单独的数据副本进行处理,该怎么办?`clone()` 方法可供您使用。
tensor([[True, True],
[True, True]])
tensor([[1., 1.],
[1., 1.]])
**在使用 ``clone()`` 时有一件重要的事情需要注意。** 如果您的源张量启用了 autograd,那么克隆的张量也会启用。**这将在关于 autograd 的视频中更深入地介绍,** 但如果您想要细节的简化版本,请继续。
在许多情况下,这正是您想要的。 例如,如果您的模型在其 `forward()` 方法中有多个计算路径,并且 *原始张量及其克隆都* 对模型的输出做出贡献,那么为了启用模型学习,您希望 autograd 在两个张量上都打开。如果您的源张量启用了 autograd(如果它是一组学习权重或派生自涉及权重的计算,通常就是这种情况),那么您将得到想要的结果。
另一方面,如果您正在进行一项计算,其中 *原始张量或其克隆都不需要跟踪梯度*,那么只要源张量的 autograd 已关闭,您就可以继续了。
还有第三种情况: 想象一下您正在模型 forward()
函数中执行一个计算,其中默认情况下所有内容的梯度都已打开,但您想在中途提取一些值来生成一些指标。在这种情况下,您 *不* 希望克隆的源张量副本跟踪梯度——通过关闭 autograd 的历史跟踪可以提高性能。为此,您可以对源张量使用 `.detach()` 方法。
tensor([[0.0905, 0.4485],
[0.8740, 0.2526]], requires_grad=True)
tensor([[0.0905, 0.4485],
[0.8740, 0.2526]], grad_fn=<CloneBackward0>)
tensor([[0.0905, 0.4485],
[0.8740, 0.2526]])
tensor([[0.0905, 0.4485],
[0.8740, 0.2526]], requires_grad=True)
这里发生了什么?
我们创建了 `a`,并将 `requires_grad=True` 打开。**我们尚未涵盖此可选参数,但将在关于 autograd 的单元中介绍。**
当我们打印 `a` 时,它会告知我们属性 `requires_grad=True`——这意味着 autograd 和计算历史跟踪已打开。
我们将 `a` 克隆并标记为 `b`。当我们打印 `b` 时,我们可以看到它正在跟踪其计算历史——它已继承 `a` 的 autograd 设置,并添加到了计算历史中。
我们将 `a` 克隆到 `c`,但首先调用了 `detach()`。
打印 `c` 时,我们看不到计算历史,也没有 `requires_grad=True`。
`.detach()` 方法*将张量与计算历史分离*。它表示,“像 autograd 关闭一样继续进行下一步操作。”它这样做*不会改变* `a`——您可以看到,当我们最后再次打印 `a` 时,它保留了其 `requires_grad=True` 属性。
移动到加速器#
PyTorch 的主要优点之一是它在 CUDA、MPS、MTIA 或 XPU 等加速器上的强大加速能力。到目前为止,我们所做的一切都是在 CPU 上进行的。我们如何迁移到更快的硬件?
首先,我们应该使用 `is_available()` 方法检查加速器是否可用。
注意
如果您没有加速器,本节中的可执行单元将不会执行任何与加速器相关的代码。
if torch.accelerator.is_available():
print('We have an accelerator!')
else:
print('Sorry, CPU only.')
We have an accelerator!
一旦我们确定一个或多个加速器可用,我们就需要将数据放在加速器可以看到的地方。您的 CPU 在您计算机的 RAM 中进行数据计算。您的加速器具有专用的内存。每当您想在设备上执行计算时,都必须将该计算所需的所有数据移动到该设备可访问的内存中。(俗称“将数据移动到 GPU 可访问的内存”简称为“将数据移动到 GPU”。)
有多种方法可以将数据放到目标设备上。您可以在创建时执行此操作。
if torch.accelerator.is_available():
gpu_rand = torch.rand(2, 2, device=torch.accelerator.current_accelerator())
print(gpu_rand)
else:
print('Sorry, CPU only.')
tensor([[0.3344, 0.2640],
[0.2119, 0.0582]], device='cuda:0')
默认情况下,新张量在 CPU 上创建,因此我们需要在创建张量时指定加速器,使用可选的 `device` 参数。您可以看到,当我们打印新张量时,PyTorch 会告知我们它在哪一个设备上(如果它不在 CPU 上)。
您可以使用 `torch.accelerator.device_count()` 查询加速器的数量。如果您有多个加速器,可以通过索引指定它们,以 CUDA 为例:`device='cuda:0'`、`device='cuda:1'` 等。
将我们的设备处处指定为字符串常量是一种相当脆弱的编码实践。理想情况下,您的代码无论是在 CPU 还是加速器硬件上都能稳健运行。您可以通过创建一个设备句柄来完成此操作,该句柄可以传递给您的张量,而不是字符串。
my_device = torch.accelerator.current_accelerator() if torch.accelerator.is_available() else torch.device('cpu')
print('Device: {}'.format(my_device))
x = torch.rand(2, 2, device=my_device)
print(x)
Device: cuda
tensor([[0.0024, 0.6778],
[0.2441, 0.6812]], device='cuda:0')
如果您有一个现有张量位于某个设备上,您可以使用 `.to()` 方法将其移动到另一个设备。下面的代码行在 CPU 上创建了一个张量,并将其移动到您在上一个单元中获取的任何设备句柄。
y = torch.rand(2, 2)
y = y.to(my_device)
重要的是要知道,为了进行涉及两个或多个张量的计算,*所有张量必须位于同一设备上*。下面的代码将引发运行时错误,无论您是否可用加速器设备,以 CUDA 为例。
x = torch.rand(2, 2)
y = torch.rand(2, 2, device='cuda')
z = x + y # exception will be thrown
操作张量形状#
有时,您需要更改张量的形状。下面,我们将介绍一些常见情况以及如何处理它们。
更改维数#
您可能需要更改维数的一种情况是向模型传递单个输入实例。PyTorch 模型通常期望输入 *批次*。
例如,假设有一个模型可以处理 3 x 226 x 226 的图像——一个 226 像素的正方形,带有 3 个颜色通道。当您加载和转换它时,您将得到一个形状为 (3, 226, 226)
的张量。然而,您的模型期望输入形状为 (N, 3, 226, 226)
,其中 N
是批次中的图像数量。那么如何创建大小为 1 的批次呢?
torch.Size([3, 226, 226])
torch.Size([1, 3, 226, 226])
`.unsqueeze()` 方法添加了一个大小为 1 的维度。`unsqueeze(0)` 在其作为新的第零维添加——现在您有了一个大小为 1 的批次!
那么,如果这是 *取消* 挤压?我们所说的挤压是什么意思?我们利用了大小为 1 的任何维度*不会改变张量中元素数量*的事实。
c = torch.rand(1, 1, 1, 1, 1)
print(c)
tensor([[[[[0.2347]]]]])
继续上面的例子,假设模型的输出是一个 20 元素向量,对应于每个输入。那么您期望输出的形状为 (N, 20)
,其中 N
是输入批次中的实例数量。这意味着对于我们只有一个输入的批次,我们将得到形状为 (1, 20)
的输出。
如果您想对该输出执行一些*非批处理*计算——一些只需要 20 元素向量的东西呢?
torch.Size([1, 20])
tensor([[0.1899, 0.4067, 0.1519, 0.1506, 0.9585, 0.7756, 0.8973, 0.4929, 0.2367,
0.8194, 0.4509, 0.2690, 0.8381, 0.8207, 0.6818, 0.5057, 0.9335, 0.9769,
0.2792, 0.3277]])
torch.Size([20])
tensor([0.1899, 0.4067, 0.1519, 0.1506, 0.9585, 0.7756, 0.8973, 0.4929, 0.2367,
0.8194, 0.4509, 0.2690, 0.8381, 0.8207, 0.6818, 0.5057, 0.9335, 0.9769,
0.2792, 0.3277])
torch.Size([2, 2])
torch.Size([2, 2])
您可以从形状看出,我们现在是二维张量变成了一维,如果您仔细查看上面单元格的输出,您会看到打印 `a` 时由于多了一个维度,显示了“额外”的一对方括号 `[]`。
您只能 squeeze()
大小为 1 的维度。参见上面我们在 `c` 中尝试挤压大小为 2 的维度,并得到了与我们开始时相同的形状。调用 `squeeze()` 和 `unsqueeze()` 只能作用于大小为 1 的维度,因为否则会改变张量中的元素数量。
您可能还会在 `unsqueeze()` 中使用它来简化广播。回想一下我们上面有以下代码的例子。
a = torch.ones(4, 3, 2)
c = a * torch.rand( 3, 1) # 3rd dim = 1, 2nd dim identical to a
print(c)
其净效应是将操作广播到维度 0 和 2 上,导致随机的 3 x 1 张量与 `a` 中每个 3 元素列进行逐元素相乘。
如果随机向量只是一个 3 元素向量呢?我们将失去进行广播的能力,因为最终维度将不符合广播规则。`unsqueeze()` 派上用场。
a = torch.ones(4, 3, 2)
b = torch.rand( 3) # trying to multiply a * b will give a runtime error
c = b.unsqueeze(1) # change to a 2-dimensional tensor, adding new dim at the end
print(c.shape)
print(a * c) # broadcasting works again!
torch.Size([3, 1])
tensor([[[0.1891, 0.1891],
[0.3952, 0.3952],
[0.9176, 0.9176]],
[[0.1891, 0.1891],
[0.3952, 0.3952],
[0.9176, 0.9176]],
[[0.1891, 0.1891],
[0.3952, 0.3952],
[0.9176, 0.9176]],
[[0.1891, 0.1891],
[0.3952, 0.3952],
[0.9176, 0.9176]]])
`.squeeze()` 和 `.unsqueeze()` 方法也有就地版本,`squeeze_()` 和 `unsqueeze_()`。
batch_me = torch.rand(3, 226, 226)
print(batch_me.shape)
batch_me.unsqueeze_(0)
print(batch_me.shape)
torch.Size([3, 226, 226])
torch.Size([1, 3, 226, 226])
有时您会希望更彻底地更改张量的形状,同时仍然保留元素数量及其内容。一种情况发生在模型的卷积层与模型的线性层之间的接口处——这在图像分类模型中很常见。卷积核将产生形状为 *特征 x 宽度 x 高度* 的输出张量,但下面的线性层期望一维输入。`reshape()` 将为您完成此操作,前提是您请求的维度产生的元素数量与输入张量相同。
output3d = torch.rand(6, 20, 20)
print(output3d.shape)
input1d = output3d.reshape(6 * 20 * 20)
print(input1d.shape)
# can also call it as a method on the torch module:
print(torch.reshape(output3d, (6 * 20 * 20,)).shape)
torch.Size([6, 20, 20])
torch.Size([2400])
torch.Size([2400])
注意
单元格最后一行的 `(6 * 20 * 20,)` 参数是因为 PyTorch 在指定张量形状时需要一个**元组**——但当形状是方法的第一个参数时,它允许我们通过使用一系列整数来作弊。在这里,我们必须添加括号和逗号来让方法相信这真的是一个单元素元组。
当它能够做到时,`reshape()` 将返回被更改张量的*视图*——也就是说,一个查看相同底层内存区域的独立张量对象。*这一点很重要:* 这意味着对源张量所做的任何更改都将反映在对该张量的视图上,除非您克隆它。
存在一些超出本介绍范围的条件,在这种情况下,reshape()
必须返回一个承载数据副本的张量。更多信息,请参阅 文档。
NumPy 桥接#
在上面关于广播的部分,我们提到 PyTorch 的广播语义与 NumPy 的兼容——但 PyTorch 和 NumPy 之间的亲缘关系比这更深远。
如果您有现有的机器学习或科学代码,其中数据存储在 NumPy ndarrays 中,您可能希望将相同的数据表示为 PyTorch 张量,无论是为了利用 PyTorch 的 GPU 加速,还是利用其用于构建 ML 模型的有效抽象。在 ndarrays 和 PyTorch 张量之间切换很容易
import numpy as np
numpy_array = np.ones((2, 3))
print(numpy_array)
pytorch_tensor = torch.from_numpy(numpy_array)
print(pytorch_tensor)
[[1. 1. 1.]
[1. 1. 1.]]
tensor([[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
PyTorch 会创建一个与 NumPy 数组形状相同且包含相同数据的张量,甚至保留 NumPy 的默认 64 位浮点数据类型。
转换也可以轻松地反向进行
pytorch_rand = torch.rand(2, 3)
print(pytorch_rand)
numpy_rand = pytorch_rand.numpy()
print(numpy_rand)
tensor([[0.8716, 0.2459, 0.3499],
[0.2853, 0.9091, 0.5695]])
[[0.87163675 0.2458961 0.34993553]
[0.2853077 0.90905803 0.5695162 ]]
重要的是要知道,这些转换后的对象使用与其源对象相同的底层内存,这意味着对一个的更改会反映在另一个上
numpy_array[1, 1] = 23
print(pytorch_tensor)
pytorch_rand[1, 1] = 17
print(numpy_rand)
tensor([[ 1., 1., 1.],
[ 1., 23., 1.]], dtype=torch.float64)
[[ 0.87163675 0.2458961 0.34993553]
[ 0.2853077 17. 0.5695162 ]]
脚本总运行时间: (0 分 0.422 秒)