【Deep Learning with PyTorch 中文手册】(九)Size,Offset,Strides
Size, storage offset, and strides
为了索引到内存中,张量依赖于一些信息,这些信息明确地定义了:尺寸大小,内存偏移量和单位步长(见下图)。存储尺寸大小信息的变量(NumPy中的术语是形状)是一个元组,表示张量的每个维度上有多少个元素。内存偏移量是内存器中与张量中的第一个元素相对应的索引增量。单位步长是内存中需要跳过的元素数量,以便沿每个维度访问下一个元素。
我们可以通过给定相应的索引来访问张量中的第二个点。
>>> import torch
>>> points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])
>>> second_point = points[1]
>>> second_point.storage_offset()
2
>>> second_point.size()
torch.Size([2])
返回的张量在内存存储中的偏移量为2(因为我们需要跳过第一个点,该点有两个值)。由于该张量是一维的,因此size仅包含一个元素,它是torch.Size类的实例对象。重要说明:此信息与张量的shape属性中包含的信息相同:
>>> second_point.shape
torch.Size([2])
最后,单位步长是一个元组,指示当索引在每个维度上增加1时必须跳过的内存存储中元素的数量。例如,我们访问points张量的stride属性:
>>> points.stride()
(2, 1)
在2D张量中访问位置处于i,j的元素,等价于访问内存存储中的位置处于storage_offset + stride [0] * i + stride [1] * j的元素。storage_offset通常为零,如果此张量是某个更大张量的一部分,则storage_offset可能为正值。
张量和内存存储之间的这种联系使得某些操作(例如张量转置或子张量提取)执行起来很高效,因为程序不会为新的张量重新分配内存;另一方面,分配的新张量又具有不同的尺寸大小,内存偏移量和单位步长。
我们已经了解了如何提取指定点的子张量,并且子张量的storage_offset是个正值。现在我们来看下size和stride:
>>> points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])
>>> second_point = points[1]
>>> second_point.size()
torch.Size([2])
>>> second_point.storage_offset()
2
>>> second_point.stride()
(1,)
最重要的是,second_point子张量的维度降低了,同时仍然和原始的points张量指向相同的内存存储。更改子张量的值也会改变原始张量:
>>> points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])
>>> second_point = points[1]
>>> second_point[0] = 10.0
>>> points
tensor([[ 1., 4.],
[10., 1.],
[ 3., 5.]])
这种副作用可能并不总是令人满意的,但是我们可以将子张量克隆到一个新的张量中,从而避免这种副作用的影响:
>>> points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])
>>> second_point = points[1].clone()
>>> second_point[0] = 10.0
>>> points
tensor([[1., 4.],
[2., 1.],
[3., 5.]])
现在我们进行转置操作。points张量的行存储的是独立的点,列存储的是点的x和y坐标,将其旋转使得张量的列存储的是独立的点:
>>> points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])
>>> points
tensor([[1., 4.],
[2., 1.],
[3., 5.]])
>>> points_t = points.t()
>>> points_t
tensor([[1., 2., 3.],
[4., 1., 5.]])
我们可以很轻松就验证出这两个张量共享同一块内存:
>>> id(points.storage()) == id(points_t.storage())
True
但是它们的形状和单位步长都不一样:
>>> points.stride()
(2, 1)
>>> points_t.stride()
(1, 2)
该结果告诉我们,将points张量中第一个索引值增加1(即将points [0,0]改为points [1,0]),会沿内存存储跳过两个元素。而将points张量中第二个索引值增加1,即将points [0,0]改为点[0,1],会沿内存存储跳过一个元素。换句话说,内存存储中张量的元素是按逐行顺序保存的。
我们可以将points转置为points_t,如下图所示。这改变了stride中元素的顺序。转置后,增加行(张量的第一个索引)会沿着存储跳过1,就像沿着points的列移动时一样,这就是转置的定义。没有分配新的内存:仅通过创建新的张量实例(其stride与原始张量不同)来获得张量的转置。
在PyTorch中进行转置不仅限于矩阵。我们可以通过指定两个维度来转置任意多维张量(例如shape和stride):
>>> some_tensor = torch.ones(3, 4, 5)
>>> some_tensor_t = some_tensor.transpose(0, 2)
>>> some_tensor.shape
torch.Size([3, 4, 5])
>>> some_tensor_t.shape
torch.Size([5, 4, 3])
>>> some_tensor.stride()
(20, 5, 1)
>>> some_tensor_t.stride()
(1, 5, 20)
我们对连续性张量的定义是:值从最右边的维开始存储在内存中(例如,沿着2D张量的行进行存储)。连续性张量很方便,因为我们可以高效且有序地访问它们,而无需在内存存储中跳来跳去。(得益于在现代CPU中的内存访问方式,改善数据访问的局部性可提高性能。)
这个例子中,points是连续性的,但是它的转置不是:
>>> points.is_contiguous()
True
>>> points_t.is_contiguous()
False
我i们可以使用contiguous从非连续性张量中获得新的连续性张量。张量的内容保持不变,但stride和storage发生变化:
>>> points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])
>>> points_t = points.t()
>>> points_t
tensor([[1., 2., 3.],
[4., 1., 5.]])
>>> points_t.storage()
1.0
4.0
2.0
1.0
3.0
5.0
[torch.FloatStorage of size 6]
>>> points_t.stride()
(1, 2)
>>> points_t_cont = points_t.contiguous()
>>> points_t_cont
tensor([[1., 2., 3.],
[4., 1., 5.]])
>>> points_t_cont.stride()
(3, 1)
>>> points_t_cont.storage()
1.0
2.0
3.0
4.0
1.0
5.0
[torch.FloatStorage of size 6]
请注意,已对storage进行了重组,以便在新的内存存储中逐行存储元素。stride也会改变,以反映张量在内存中新的布局。