介绍 || 张量 || 自动求导 || 构建模型 || TensorBoard 支持 || 训练模型 || 模型理解
PyTorch 张量简介
请跟随以下视频或前往 YouTube 观看。
张量是 PyTorch 中的核心数据结构。本交互式笔记本将深入介绍 torch.Tensor
类。
首先,让我们导入 PyTorch 模块。为了便于演示,我们还将引入 Python 的数学模块。
importtorch
importmath
创建张量
创建张量的最简单方法是使用 torch.empty()
调用:
x = torch.empty(3, 4)
print(type(x))
print(x)
<class 'torch.Tensor'>
tensor([[ 6.2754e+13, 7.0065e-45, 1.4013e-45, 0.0000e+00],
[-9.3681e+11, 3.0634e-41, 0.0000e+00, 0.0000e+00],
[ 1.5457e+03, 3.0635e-41, 2.5256e-12, 4.5895e-41]])
让我们来分解一下刚才的操作:
-
我们使用
torch
模块提供的众多工厂方法之一创建了一个张量。 -
该张量本身是二维的,包含 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([[[5.5989e+07, 3.0635e-41, 3.4774e-15],
[3.0635e-41, 8.9683e-44, 0.0000e+00]],
[[1.5695e-43, 0.0000e+00, nan],
[3.0631e-41, 0.0000e+00, 1.4013e-45]]])
torch.Size([2, 2, 3])
tensor([[[ 1.5582e+03, 3.0635e-41, 4.0625e+04],
[ 3.0635e-41, 1.1210e-43, 0.0000e+00]],
[[ 8.9683e-44, 0.0000e+00, 2.2417e+24],
[ 3.0631e-41, -9.3681e+11, 3.0634e-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]])
使用 torch.tensor()
是创建张量最直接的方式,如果您已经有一个 Python 元组或列表中的数据。如上所示,嵌套集合将生成一个多维张量。
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 Tensors 进行数学与逻辑运算
既然您已经了解了一些创建张量的方法……那么您可以用它们做些什么呢?
让我们首先看看基本的算术运算,以及张量与简单标量之间的交互:
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 ndarray 中的广播语义,您会发现同样的规则在这里也适用。
相同形状规则的例外是张量广播。以下是一个示例:
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])
对于算术操作,有一些功能类似的函数:
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()`` 时,有一点需要注意。 如果您的源张量启用了自动求导(autograd),那么克隆的张量也会启用自动求导。这一点将在关于自动求导的视频中深入讨论, 但如果您想了解简要的细节,请继续阅读。
在许多情况下,这正是您所需要的。 例如,如果您的模型在其 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)
这里发生了什么?
-
我们在创建
a
时开启了requires_grad=True
。我们尚未介绍这个可选参数,但会在自动求导的单元中详细讲解。 -
当我们打印
a
时,它会提示我们requires_grad=True
属性已开启——这意味着自动求导和计算历史跟踪功能已启用。 -
我们克隆了
a
并将其标记为b
。当我们打印b
时,可以看到它正在跟踪其计算历史——它继承了a
的自动求导设置,并添加到了计算历史中。 -
我们将
a
克隆到c
,但在克隆之前调用了detach()
。 -
打印
c
时,我们发现它没有计算历史,也没有requires_grad=True
属性。
detach()
方法将张量从其计算历史中分离出来。 它表示“接下来执行的操作就像关闭了 autograd 一样。” 它这样做不会改变 a
—— 你可以在最后再次打印 a
时看到,它仍然保留着 requires_grad=True
的属性。
迁移至 Accelerator
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 的图像——即具有 3 个颜色通道的 226 像素正方形图像。当您加载并转换该图像时,会得到一个形状为 (3, 226, 226)
的张量。然而,您的模型期望的输入形状是 (N, 3, 226, 226)
,其中 N
是批次中的图像数量。那么,如何创建一个包含单张图像的批次呢?
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)
将其作为新的第0维度添加——现在您就有了一个大小为1的批次。
那么,既然这是 unsqueezing(取消压缩)?我们所说的 squeezing(压缩)又是什么意思呢?我们利用了这样一个事实:任何大小为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
时由于多了一个维度,会显示“额外”的一对方括号 []
。
你只能对大小为 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
中的每个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])
有时,您可能希望更彻底地改变张量的形状,同时仍然保留元素的数量及其内容。这种情况常见于模型的卷积层和线性层之间的接口——这在图像分类模型中很常见。卷积核会生成形状为 features x width x height 的输出张量,但接下来的线性层期望一个一维输入。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 的关系远不止于此。
如果您现有的机器学习或科学计算代码中数据存储在 NumPy 的 ndarrays 中,您可能希望将这些数据表示为 PyTorch 张量,无论是为了利用 PyTorch 的 GPU 加速,还是其用于构建机器学习模型的高效抽象。在 ndarrays 和 PyTorch 张量之间切换非常简单:
importnumpyasnp
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 ]]