PyTorch 2.2 中文官方教程(二)(2)

简介: PyTorch 2.2 中文官方教程(二)

PyTorch 2.2 中文官方教程(二)(1)https://developer.aliyun.com/article/1482481


随机张量和种子

说到随机张量,您是否注意到在其之前立即调用了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]]) 

您应该看到random1random3携带相同的值,random2random4也是如此。手动设置 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([[[ 8.7595e-05,  0.0000e+00,  1.4013e-45],
         [ 0.0000e+00,  7.7463e-37,  0.0000e+00]],
        [[ 0.0000e+00,  0.0000e+00,  8.6286e-05],
         [ 0.0000e+00, -1.7707e+28,  4.5849e-41]]])
torch.Size([2, 2, 3])
tensor([[[ 0.0000e+00,  0.0000e+00,  1.4013e-45],
         [ 0.0000e+00,  7.7463e-37,  0.0000e+00]],
        [[ 0.0000e+00,  0.0000e+00,  8.6408e-05],
         [ 0.0000e+00, -1.7707e+28,  4.5849e-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相同的所有值,但被截断为整数。

可用的数据类型包括:

  • torch.bool
  • torch.int8
  • torch.uint8
  • torch.int16
  • torch.int32
  • torch.int64
  • torch.half
  • torch.float
  • torch.double
  • torch.bfloat

使用 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的那一行一样。

两个张量之间的类似操作也会像您直觉地期望的那样:

powers2 = twos ** torch.tensor([[1, 2], [3, 4]])
print(powers2)
fives = ones + fours
print(fives)
dozens = threes * fours
print(dozens) 
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.cross(v2, v1)) # negative of z unit vector (v1 x v2 == -v2 x v1)
print(m1)
m3 = torch.matmul(m1, m2)
print(m3)                  # 3 times m1
print(torch.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:
/var/lib/jenkins/workspace/beginner_source/introyt/tensors_deeper_tutorial.py:462: UserWarning:
Using torch.cross without specifying the dim arg is deprecated.
Please either pass the dim explicitly or simply use torch.linalg.cross.
The default value of dim will change to agree with that of linalg.cross in a future release. (Triggered internally at ../aten/src/ATen/native/Cross.cpp:63.)
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.svd(
U=tensor([[-0.7889, -0.6145],
        [-0.6145,  0.7889]]),
S=tensor([4.1498, 1.0548]),
V=tensor([[-0.7957,  0.6056],
        [-0.6056, -0.7957]])) 

这只是一小部分操作。有关更多详细信息和数学函数的完整清单,请查看文档

就地更改张量

大多数张量上的二进制操作将返回第三个新张量。当我们说c = a * b(其中ab是张量)时,新张量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]) 

对于算术操作,有类似的函数行为:

a = torch.ones(2, 2)
b = torch.rand(2, 2)
print('Before:')
print(a)
print(b)
print('\nAfter adding:')
print(a.add_(b))
print(a)
print(b)
print('\nAfter multiplying')
print(b.mul_(b))
print(b) 
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 中的任何对象一样,将张量分配给变量会使变量成为张量的标签,而不是复制它。例如:

a = torch.ones(2, 2)
b = a
a[0][1] = 561  # we change a...
print(b)       # ...and b is also altered 
tensor([[  1., 561.],
        [  1.,   1.]]) 

但是如果您想要一个单独的数据副本进行操作呢?clone()方法就是为您准备的:

a = torch.ones(2, 2)
b = a.clone()
assert b is not a      # different objects in memory...
print(torch.eq(a, b))  # ...but still with the same contents!
a[0][1] = 561          # a changes...
print(b)               # ...but b is still all ones 
tensor([[True, True],
        [True, True]])
tensor([[1., 1.],
        [1., 1.]]) 

**在使用clone()时有一件重要的事情需要注意。**如果您的源张量启用了自动求导,那么克隆张量也会启用。**这将在自动求导的视频中更深入地介绍,但如果您想要简要了解详情,请继续阅读。

在许多情况下,这可能是您想要的。例如,如果您的模型在其forward()方法中具有多个计算路径,并且原始张量和其克隆都对模型的输出有贡献,那么为了启用模型学习,您希望为两个张量启用自动求导。如果您的源张量已启用自动求导(如果它是一组学习权重或从涉及权重的计算派生而来,则通常会启用),那么您将获得所需的结果。

另一方面,如果您进行的计算既不需要原始张量也不需要其克隆跟踪梯度,那么只要源张量关闭了自动求导,您就可以继续进行。

然而,还有第三种情况: 想象一下,你正在模型的forward()函数中执行计算,其中默认情况下为所有内容打开梯度,但你想要在中间提取一些值以生成一些指标。在这种情况下,你希望克隆源张量跟踪梯度 - 关闭自动求导历史记录跟踪可以提高性能。为此,你可以在源张量上使用.detach()方法:

a = torch.rand(2, 2, requires_grad=True) # turn on autograd
print(a)
b = a.clone()
print(b)
c = a.detach().clone()
print(c)
print(a) 
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) 

这里发生了什么?

  • 我们使用requires_grad=True创建a我们还没有涵盖这个可选参数,但在自动求导单元中会讨论。
  • 当我们打印a时,它告诉我们属性requires_grad=True - 这意味着自动求导和计算历史跟踪已打开。
  • 我们克隆a并将其标记为b。当我们打印b时,我们可以看到它正在跟踪其计算历史 - 它继承了a的自动求导设置,并添加到了计算历史中。
  • 我们将a克隆到c,但首先调用detach()
  • 打印c,我们看不到计算历史,也没有requires_grad=True

detach()方法将张量与其计算历史分离。它表示,“接下来的操作就好像自动求导已关闭一样。” 它在更改a的情况下执行此操作 - 当我们最后再次打印a时,你会看到它保留了requires_grad=True属性。

转移到 GPU

PyTorch 的一个主要优势是其在 CUDA 兼容的 Nvidia GPU 上的强大加速。 (“CUDA”代表Compute Unified Device Architecture,这是 Nvidia 用于并行计算的平台。)到目前为止,我们所做的一切都是在 CPU 上进行的。我们如何转移到更快的硬件?

首先,我们应该检查 GPU 是否可用,使用is_available()方法。

注意

如果你没有 CUDA 兼容的 GPU 和安装了 CUDA 驱动程序,本节中的可执行单元格将不会执行任何与 GPU 相关的代码。

if torch.cuda.is_available():
    print('We have a GPU!')
else:
    print('Sorry, CPU only.') 
We have a GPU! 

一旦我们确定有一个或多个 GPU 可用,我们需要将数据放在 GPU 可以看到的地方。你的 CPU 在计算时使用计算机 RAM 中的数据。你的 GPU 有专用内存附加在上面。每当你想在设备上执行计算时,你必须将进行该计算所需的所有数据移动到该设备可访问的内存中。 (口语上,“将数据移动到 GPU 可访问的内存”缩写为“将数据移动到 GPU”。)

有多种方法可以将数据放在目标设备上。你可以在创建时执行:

if torch.cuda.is_available():
    gpu_rand = torch.rand(2, 2, device='cuda')
    print(gpu_rand)
else:
    print('Sorry, CPU only.') 
tensor([[0.3344, 0.2640],
        [0.2119, 0.0582]], device='cuda:0') 

默认情况下,新张量是在 CPU 上创建的,因此我们必须在想要使用可选device参数在 GPU 上创建张量时指定。当我们打印新张量时,你可以看到 PyTorch 告诉我们它在哪个设备上(如果不在 CPU 上)。

你可以使用torch.cuda.device_count()查询 GPU 的数量。如果你有多个 GPU,你可以通过索引指定它们:device='cuda:0'device='cuda:1'等。

作为编码实践,在所有地方使用字符串常量指定设备是相当脆弱的。在理想情况下,无论你是在 CPU 还是 GPU 硬件上,你的代码都应该表现稳健。你可以通过创建一个设备句柄来实现这一点,该句柄可以传递给你的张量,而不是一个字符串:

if torch.cuda.is_available():
    my_device = torch.device('cuda')
else:
    my_device = 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) 

重要的是要知道,为了进行涉及两个或更多张量的计算,所有张量必须在同一设备上。无论你是否有 GPU 设备可用,以下代码都会抛出运行时错误:

x = torch.rand(2, 2)
y = torch.rand(2, 2, device='gpu')
z = x + y  # exception will be thrown 

操作张量形状

有时,你需要改变张量的形状。下面,我们将看几种常见情况以及如何处理它们。

改变维度数量

有一种情况可能需要改变维度的数量,就是向模型传递单个输入实例。PyTorch 模型通常期望输入的批量

例如,想象一个模型处理 3 x 226 x 226 的图像 - 一个有 3 个颜色通道的 226 像素正方形。当你加载和转换它时,你会得到一个形状为(3, 226, 226)的张量。然而,你的模型期望的输入形状是(N, 3, 226, 226),其中N是批量中图像的数量。那么如何制作一个批量为 1 的批次?

a = torch.rand(3, 226, 226)
b = a.unsqueeze(0)
print(a.shape)
print(b.shape) 
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 元素的向量,怎么办?

a = torch.rand(1, 20)
print(a.shape)
print(a)
b = a.squeeze(0)
print(b.shape)
print(b)
c = torch.rand(2, 2)
print(c.shape)
d = c.squeeze(0)
print(d.shape) 
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()会返回一个视图,即一个查看相同底层内存区域的独立张量对象以进行更改。*这很重要:*这意味着对源张量进行的任何更改都会反映在该张量的视图中,除非你使用clone()

在这个介绍范围之外,reshape()有时必须返回一个携带数据副本的张量。更多信息请参阅文档

NumPy 桥接

在上面关于广播的部分中提到,PyTorch 的广播语义与 NumPy 的兼容 - 但 PyTorch 和 NumPy 之间的关系甚至比这更深。

如果您有现有的 ML 或科学代码,并且数据存储在 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.294 秒)

下载 Python 源代码:tensors_deeper_tutorial.py

下载 Jupyter 笔记本:tensors_deeper_tutorial.ipynb

由 Sphinx-Gallery 生成的图库


PyTorch 2.2 中文官方教程(二)(3)https://developer.aliyun.com/article/1482483

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
6天前
|
PyTorch 算法框架/工具 异构计算
PyTorch 2.2 中文官方教程(二十)(4)
PyTorch 2.2 中文官方教程(二十)
31 0
PyTorch 2.2 中文官方教程(二十)(4)
|
6天前
|
PyTorch 算法框架/工具 并行计算
PyTorch 2.2 中文官方教程(二十)(3)
PyTorch 2.2 中文官方教程(二十)
49 0
|
6天前
|
Android开发 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(二十)(2)
PyTorch 2.2 中文官方教程(二十)
49 0
PyTorch 2.2 中文官方教程(二十)(2)
|
6天前
|
iOS开发 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(二十)(1)
PyTorch 2.2 中文官方教程(二十)
51 0
PyTorch 2.2 中文官方教程(二十)(1)
|
6天前
|
PyTorch 算法框架/工具 并行计算
PyTorch 2.2 中文官方教程(十九)(4)
PyTorch 2.2 中文官方教程(十九)
32 0
|
6天前
|
PyTorch 算法框架/工具 异构计算
PyTorch 2.2 中文官方教程(十九)(3)
PyTorch 2.2 中文官方教程(十九)
30 0
PyTorch 2.2 中文官方教程(十九)(3)
|
6天前
|
异构计算 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(十九)(2)
PyTorch 2.2 中文官方教程(十九)
62 0
PyTorch 2.2 中文官方教程(十九)(2)
|
6天前
|
PyTorch 算法框架/工具 异构计算
PyTorch 2.2 中文官方教程(十九)(1)
PyTorch 2.2 中文官方教程(十九)
81 1
PyTorch 2.2 中文官方教程(十九)(1)
|
6天前
|
机器学习/深度学习 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(十八)(4)
PyTorch 2.2 中文官方教程(十八)
55 1
|
6天前
|
机器学习/深度学习 PyTorch 算法框架/工具
PyTorch 2.2 中文官方教程(十八)(3)
PyTorch 2.2 中文官方教程(十八)
36 1
PyTorch 2.2 中文官方教程(十八)(3)

相关实验场景

更多