备注
点击 此处 下载完整示例代码
介绍 || 张量 || 自动求导 || 构建模型 || TensorBoard支持 || 训练模型 || 模型理解
PyTorch张量介绍¶
Created On: Nov 30, 2021 | Last Updated: Jan 29, 2025 | Last Verified: Nov 05, 2024
请跟随以下视频或在`youtube <https://www.youtube.com/watch?v=r7QDUPb2dCM>`__观看。
张量是PyTorch中的核心数据抽象。此交互式notebook提供对``torch.Tensor``类的深入介绍。
首先,让我们导入PyTorch模块。我们还会添加Python的数学模块以支持一些操作示例。
import torch
import math
创建张量¶
创建张量最简单的方法是使用 torch.empty()
调用:
x = torch.empty(3, 4)
print(type(x))
print(x)
<class 'torch.Tensor'>
tensor([[ nan, 4.5567e-41, -5.5229e+25, 3.0700e-41],
[ nan, 4.5567e-41, -6.5534e+25, 3.0700e-41],
[ 4.6182e+19, 7.1441e+31, 7.5876e+31, 5.1480e-11]])
让我们解读一下刚才完成的操作:
我们使用绑定到
torch
模块的众多工厂方法之一创建了一个张量。张量本身是二维的,有3行4列。
返回对象的类型是
torch.Tensor
,这是torch.FloatTensor
的别名;默认情况下,PyTorch张量会用32位浮点数填充。(稍后会讨论更多关于数据类型的内容。)当打印你的张量时,你可能会看到一些看似随机的值。
torch.empty()
调用为张量分配了内存,但并没有用任何值进行初始化——所以你看到的是分配时内存中的原始内容。
关于张量及其维度和术语的简短说明:
有时,你会看到一维张量被称为*向量(vector)*。
同样,二维张量通常被称为*矩阵(matrix)*。
具有超过二维的张量通常只是简单地称为张量。
通常情况下,你可能需要用一些值来初始化张量。常见的情况是全零、全一或随机值,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
一样。手动设置随机数生成器的种子会重置它,因此依赖随机数的相同计算在大多数情况下会产生相同的结果。
更多详细信息,请参阅 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([[[-6.9715e+25, 3.0700e-41, -9.4640e+25],
[ 3.0700e-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([[[-9.4800e-08, 3.0695e-41, 2.0000e+00],
[ 1.7023e+00, -0.0000e+00, 1.5912e+00]],
[[ 3.6893e+19, 1.8732e+00, -2.0000e+00],
[ 1.7064e+00, 1.0842e-19, 1.7735e+00]]])
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.
这表明它是整数类型而不是浮点数。
另一个在打印 a
时值得注意的点是,与默认的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中的广播语义,你会发现这里的规则相同。
同形规则的例外是*张量广播(tensor broadcasting)*。以下是一个例子:
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的张量?
广播是一种处理具有形状相似性的张量之间操作的方法。上面的例子中,1行4列的张量与两行4列张量的*每一行*相乘。
这是深度学习中的一个重要操作。常见的例子是将一个学习权重的张量与一个输入张量*批量(batch)*相乘,对批中的每个实例分别应用操作,并返回一个形状相同的张量——就像我们上面的 (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``模块(例如,torch.sin()
)。正如你从``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()``时需要注意一个重要事项。 如果你的源张量启用了自动求导,克隆的张量也会启用。**这将在自动求导的视频中更深入地介绍,**但如果你想要了解简单版的细节,请继续阅读。
在很多情况下,这将是您想要的。比如,如果您的模型在其``forward()``方法中有多个计算路径,并且原始张量及其克隆都对模型的输出有贡献,那么为了使模型学习生效,您希望两个张量都启用自动梯度计算。如果源张量启用了自动梯度计算(通常情况下,如果它是学习权重的一部分或者是由涉及这些权重的计算派生出来的,那么它就会启用自动梯度计算),那么您将获得您想要的结果。
另一方面,如果您进行的是一种计算,其中既不需要原始张量也不需要克隆张量跟踪梯度,那么只要源张量关闭了自动梯度计算,您就没问题了。
不过,还有一种第三种情况:假设您正在模型的``forward()``函数中执行计算,其中默认情况下所有内容都启用了梯度跟踪,但您希望中途提取一些值以生成一些指标。在这种情况下,您不希望源张量的克隆副本跟踪梯度——关闭自动梯度的历史跟踪会提高性能。为此,您可以对源张量使用``.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``。我们尚未介绍该可选参数,但会在有关自动梯度计算的章节中详细讨论。
当我们打印``a``时,它告诉我们属性``requires_grad=True``——这意味着自动梯度计算和计算历史跟踪已开启。
我们克隆了``a``并将其标记为``b``。当我们打印``b``时,我们可以看到它正在跟踪其计算历史——它继承了``a``的自动梯度设置,并添加了计算历史。
我们将``a``克隆为``c``,但首先调用了``detach()``。
打印``c``时,我们发现没有计算历史,也没有``requires_grad=True``。
``detach()``方法将张量从其计算历史中分离出来。它表示,“之后执行的任何操作都像关闭了自动梯度一样。”它在不改变``a``的情况下完成此操作——您可以看到,当我们最后再次打印``a``时,它仍然具有其``requires_grad=True``属性。
转到`加速器 <https://pytorch.org/docs/stable/torch.html#accelerators>`__¶
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``是批中的图像数量。那么如何创建一个单图像的批呢?
torch.Size([3, 226, 226])
torch.Size([1, 3, 226, 226])
``unsqueeze()``方法添加一个维度范围为1。``unsqueeze(0)``将其作为新的第零维度添加——现在您有一个单输入的批了!
如果这就是*扩展*,我们说的压缩是什么?我们利用了这样一个事实,即任何维度范围为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``显示了一个“额外”的方括号``[]``,这是因为有一个额外维度。
您只能对范围为1的维度使用``squeeze()``。查看上面的地方,我们尝试对``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``中的每个三元素列。
如果随机向量仅是一个三元素向量呢?由于广播规则的最终维度不匹配,我们将失去广播能力。``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()``会返回一个张管视图——即一个单独的张量对象,查看同一底层的内存区域。*这一点很重要:*这意味着对源张量进行的任何修改都会反映在该张管视图上,除非您将其``clone()
。
有一些超出本介绍范围的情况,其中``reshape()``不得不返回一个包含数据副本的张量。有关更多信息,请参阅`文档 <https://pytorch.org/docs/stable/torch.html#torch.reshape>`__。
NumPy 桥接¶
在以上关于广播的部分中提到,PyTorch 的广播语义与 NumPy 的兼容性——但 PyTorch 与 NumPy 的关系实远不止此。
如果您有现有的机器学习或科学代码,其数据存储在 NumPy 的 ndarrays 中,您可能希望将这些数据表示为 PyTorch 张量,无论是为了利用 PyTorch 的 GPU 加速,还是因其构建机器学习模型的高效抽象。切换 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 ]]