本文内容整理自书籍《Deep Learning with PyTorch》 https://pytorch.org/deep-learning-with-pytorch-thank-you
本章涵盖
import numpy as np
import torch
points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])
points
tensor([[1., 4.], [2., 1.], [3., 5.]])
points.shape
torch.Size([3, 2])
points[0]
tensor([1., 4.])
points[0, 1], points[0][1]
(tensor(4.), tensor(4.))
尽管张量报告自己有三行两列,但它的底层是一个大小为6的连续数组。从这个意义上说,张量知道如何把一对指标转换成存储中的一个位置。
points.storage()
1.0 4.0 2.0 1.0 3.0 5.0 [torch.FloatStorage of size 6]
你也可以手动索引到一个存储:
points.storage()[0]
2.0
你不能用两个指标来索引一个二维张量的存储。存储的布局总是一维的,而与可能涉及到它的任何张量的维数无关。
在这一点上,改变存储的值就会改变其引用张量的内容,这并不奇怪:
points = torch.tensor([[1.0, 4.0], [2.0, 1.0], [3.0, 5.0]])
points_storage = points.storage()
points_storage[0] = 2.0
tensor([[2., 4.], [2., 1.], [3., 5.]])
points.shape, points[0].shape, points[0][0].shape
(torch.Size([3, 2]), torch.Size([2]), torch.Size([]))
points.size(), points[0].size(), points[0][0].size()
(torch.Size([3, 2]), torch.Size([2]), torch.Size([]))
points.stride(), points[0].stride(), points[0][0].stride()
((2, 1), (1,), ())
points, points[0], points[0][0]
(tensor([[2., 4.], [2., 1.], [3., 5.]]), tensor([2., 4.]), tensor(2.))
这种张量和存储之间的间接性导致了一些操作,比如转置一个张量或者提取一个次张量,这些操作是便宜的,因为它们不会导致内存的重新分配;而是,它们包括分配一个新的张量对象,这个张量对象的形状、存储偏移量或步长有不同的值。
points.storage_offset(), points[0].storage_offset(), points[0][0].storage_offset()
(0, 0, 0)
points[1], points[1][0], points[1][1]
(tensor([2., 1.]), tensor(2.), tensor(1.))
points[1].storage_offset(), points[1][0].storage_offset(), points[1][1].storage_offset()
(2, 2, 3)
在二维张量中访问元素i, j的结果是访问存储中的storage_offset + stride[0] * i + stride[1] * j元素。
second_point = points[1]
second_point[0] = 10.0
points
tensor([[ 2., 4.], [10., 1.], [ 3., 5.]])
这种效果可能并不总是可取的,所以你最终可以把次张量克隆成一个新的张量:
second_point = points[1].clone()
second_point[0] = 20.0
points
tensor([[ 2., 4.], [10., 1.], [ 3., 5.]])
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.storage()
1.0 4.0 2.0 1.0 3.0 5.0 [torch.FloatStorage of size 6]
它们只是在形状和步幅上不同:
points.stride()
(2, 1)
points_t.stride()
(1, 2)
points[0, 1].storage_offset()
1
points_t[0, 1].storage_offset()
2
在pytorch中,只有很少几个操作是不改变tensor的内容本身,而只是重新定义下标与元素的对应关系的。换句话说,这种操作不进行数据拷贝和数据的改变,变的是元数据。
这些操作是:narrow(),view(),expand()和transpose()
举个例子,在使用transpose()进行转置操作时,pytorch并不会创建新的、转置后的tensor,而是修改了tensor中的一些属性(也就是元数据),使得此时的offset和stride是与转置tensor相对应的。转置的tensor和原tensor的内存是共享的!
tensor_A = torch.tensor([
[[ 0, 6, 12, 18],
[ 2, 8, 14, 20],
[ 4, 10, 16, 22]],
[[ 1, 7, 13, 19],
[ 3, 9, 15, 21],
[ 5, 11, 17, 23]]])
tensor_A
tensor([[[ 0, 6, 12, 18], [ 2, 8, 14, 20], [ 4, 10, 16, 22]], [[ 1, 7, 13, 19], [ 3, 9, 15, 21], [ 5, 11, 17, 23]]])
在存储数据时,内存并不支持这个维度层级概念,只能以平铺方式按序写入内存,因此这 种层级关系需要人为管理,也就是说,每个张量的存储顺序需要人为跟踪。为了方便表达,我们把张量 shape 中相对靠左侧的维度叫做大维度,shape 中相对靠右侧的维度叫做小维度,比如[2, 3, 4]的张量中,图片数量维度与通道数量相比,图片数量叫做大维度,通道 数叫做小维度。在优先写入小维度的设定下,形状(2, 3, 4)张量的内存布局为:
tensor_A.storage()
0 6 12 18 2 8 14 20 4 10 16 22 1 7 13 19 3 9 15 21 5 11 17 23 [torch.LongStorage of size 24]
tensor_A.stride()
(12, 4, 1)
tensor_B = torch.tensor(np.reshape(np.arange(2*3*4), (4, 3, 2)))
tensor_B
tensor([[[ 0, 1], [ 2, 3], [ 4, 5]], [[ 6, 7], [ 8, 9], [10, 11]], [[12, 13], [14, 15], [16, 17]], [[18, 19], [20, 21], [22, 23]]])
tensor_B.storage()
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [torch.LongStorage of size 24]
tensor_B.stride()
(6, 2, 1)
这种张量和存储之间的间接性导致了一些操作,比如转置一个张量或者提取一个次张量,这些操作是便宜的,因为它们不会导致内存的重新分配;而是,它们包括分配一个新的张量对象,这个张量对象的形状、存储偏移量或步长有不同的值。
tensor_B_transpose = tensor_B.transpose(0, 2)
tensor_B_transpose
tensor([[[ 0, 6, 12, 18], [ 2, 8, 14, 20], [ 4, 10, 16, 22]], [[ 1, 7, 13, 19], [ 3, 9, 15, 21], [ 5, 11, 17, 23]]])
tensor_B_transpose.storage() # 与 tensor_B.storage() 相同
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [torch.LongStorage of size 24]
tensor_B_transpose.stride() # 与 tensor_B 不同
(1, 2, 6)
经过上述transpose操作后得到的tensor_B_transpose,它内部数据的布局方式和从头开始创建一个这样的常规的tensor的布局方式是不一样的!于是,这就有contiguous()的用武之地了。在上面的例子中,tensor_B是contiguous的,但tensor_B_transpose不是(因为内部数据不是通常的布局方式)。注意不要被contiguous的字面意思“连续的”误解,tensor中数据还是在内存中一块区域里,只是布局的问题!当调用contiguous()时,会强制拷贝一份tensor,让它的布局和从头创建的一样。
tensor_B_transpose_contiguous = tensor_B_transpose.contiguous()
tensor_B_transpose_contiguous
tensor([[[ 0, 6, 12, 18], [ 2, 8, 14, 20], [ 4, 10, 16, 22]], [[ 1, 7, 13, 19], [ 3, 9, 15, 21], [ 5, 11, 17, 23]]])
tensor_B_transpose_contiguous.storage() # 与 tensor_A.storage() 一样
0 6 12 18 2 8 14 20 4 10 16 22 1 7 13 19 3 9 15 21 5 11 17 23 [torch.LongStorage of size 24]
tensor_B_transpose_contiguous.stride()
(12, 4, 1)
联系
对于形状 shape 为(d1, d2,.., dn)的张量的视图中的元素E(e1, e2,...,en),如果该张量的存储的步长为 stride 为 (s1, s2,...,sn) 、存储偏移量storage offset 为 s_o,那么元素E的存储位置index是: $$index((e1, e2,...,en)) = s\_o + s1 * e1 + s2 * e2 + ... + sn *en$$
区别
总结:张量的视图与存储通过索引来建立关系,它们之间没有必然性,即相同存储可以有不同的视图,相同的视图可以有不同的存储。
要分配一个正确的数字类型的张量,你可以指定正确的dtype作为构造函数的参数,如下所示:
double_points = torch.ones(10, 2, dtype=torch.double)
short_points = torch.tensor([[1, 2], [3, 4]], dtype=torch.short)
你可以通过访问相应的属性来找到一个张量的d类型:
short_points.dtype
torch.int16
您还可以通过使用相应的转换方法(例如,转换),将张量创建函数的输出转换为正确的类型
double_points = torch.zeros(10, 2).double()
short_points = torch.ones(10, 2).short()
或者更方便的方法:
double_points = torch.zeros(10, 2).to(torch.double)
short_points = torch.ones(10, 2).to(dtype=torch.short)
在底层,type和to执行相同的类型检查和转换(如果需要的话)操作,但是to方法可以接受额外的参数。 你总是可以用type方法把一种类型的张量转换成另一种类型的张量:
points = torch.randn(10, 2)
short_points = points.type(torch.short)
points = torch.tensor([[1.0, 4.0, 5.1], [2.0, 1.0, 3.2], [3.0, 5.0, 1.7]])
points
tensor([[1.0000, 4.0000, 5.1000], [2.0000, 1.0000, 3.2000], [3.0000, 5.0000, 1.7000]])
points[0:3:2]
tensor([[1.0000, 4.0000, 5.1000], [3.0000, 5.0000, 1.7000]])
points[1:, :]
tensor([[2.0000, 1.0000, 3.2000], [3.0000, 5.0000, 1.7000]])
利用了Python的Buffer Protocol(https://docs.python.org/3/c-api/buffer.html%EF%BC%89%EF%BC%8C 所以tensor 与 numpy 具有零拷贝的互操作性。
points = torch.ones(2, 3)
points_np = points.numpy()
points_np
array([[1., 1., 1.], [1., 1., 1.]], dtype=float32)
points = torch.from_numpy(points_np)
points
tensor([[1., 1., 1.], [1., 1., 1.]])
PyTorch在底层使用pickle来序列化张量对象,以及专门用于存储的序列化代码。这种技术允许您快速保存张量,以便您只想用PyTorch加载它们,但是文件格式本身不能互操作。除了PyTorch,你不能用其他软件读取张量。
import os
if not os.path.exists("./PyTorch_learn/data/"):
os.makedirs("./PyTorch_learn/data/")
torch.save(points, './PyTorch_learn/data/ourpoints.t')
points = torch.load('./PyTorch_learn/data/ourpoints.t')
points
tensor([[1., 1., 1.], [1., 1., 1.]])
HDF5是一种可移植的、广泛支持的表示序列化多格式的格式维数组,组织在嵌套的键值字典中。Python通过h5py library支持HDF5,它以NumPy的形式接受和返回数据数组。
import h5py
f = h5py.File("./PyTorch_learn/data/ourpoints.hdf5", 'w')
dset = f.create_dataset('coords', data=points.numpy())
f.close()
f = h5py.File('./PyTorch_learn/data/ourpoints.hdf5', 'r')
dset = f['coords']
last_points = dset[1:]
last_points
array([[1., 1., 1.]], dtype=float32)
last_points = torch.from_numpy(dset[1:])
f.close()
last_points
tensor([[1., 1., 1.]])
在线文档 https://pytorch.org/docs/stable/index.html 它是详尽无遗的,并且组织合理,将张量操作分为几组。
a = torch.tensor(list(range(9)))
a
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8])
a.size(), a.storage_offset(), a.stride()
(torch.Size([9]), 0, (1,))
b = a.view(3, 3)
b, b[1, 1]
(tensor([[0, 1, 2], [3, 4, 5], [6, 7, 8]]), tensor(4))
b.size(), b.storage_offset(), b.stride()
(torch.Size([3, 3]), 0, (3, 1))
c = b[1:, 1:]
c
tensor([[4, 5], [7, 8]])
c.size(), c.storage_offset(), c.stride()
(torch.Size([2, 2]), 4, (3, 1))
c = c.type(torch.float32)
c
tensor([[4., 5.], [7., 8.]])
c.cos()
tensor([[-0.6536, 0.2837], [ 0.7539, -0.1455]])
c.sqrt()
tensor([[2.0000, 2.2361], [2.6458, 2.8284]])
c.cos_()
tensor([[-0.6536, 0.2837], [ 0.7539, -0.1455]])
c
tensor([[-0.6536, 0.2837], [ 0.7539, -0.1455]])