欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > 名人名企 > PyTorch深度学习实战(5)—— Tensor的命名张量和基本结构

PyTorch深度学习实战(5)—— Tensor的命名张量和基本结构

2024/10/25 11:27:35 来源:https://blog.csdn.net/shangjg03/article/details/140946034  浏览:    关键词:PyTorch深度学习实战(5)—— Tensor的命名张量和基本结构

1. 命名张量

命名张量(Named Tensors)允许用户将显式名称与Tensor的维度关联起来,便于对Tensor进行其他操作。笔者推荐使用维度的名称进行维度操作,这样可以避免重复计算Tensor每个维度的位置。支持命名张量的工厂函数(factory functions)有tensoremptyoneszerosrandn等。

下面举例说明命名张量的使用,其中N代表batch_size,C代表通道数,H代表高度,W代表宽度。

In: # 命名张量API在后续还有可能还有变化,系统会提示warning,在此忽略import warnings
    warnings.filterwarnings("ignore")# 直接使用names参数创建命名张量
    imgs = t.randn(1, 2, 2, 3, names=('N', 'C', 'H', 'W'))
    imgs.namesOut:('N', 'C', 'H', 'W')In: # 查看旋转操作造成的维度变换
    imgs_rotate = imgs.transpose(2, 3)
    imgs_rotate.namesOut:('N', 'C', 'W', 'H')In: # 通过refine_names对未命名的张量命名,不需要名字的维度可以用None表示
    another_imgs = t.rand(1, 3, 2, 2)
    another_imgs = another_imgs.refine_names('N', None, 'H', 'W')
    another_imgs.names
 Out:('N', None, 'H', 'W')In: # 修改部分维度的名称
    renamed_imgs = imgs.rename(H='height', W='width')
    renamed_imgs.namesOut:('N', 'C', 'height', 'width')In: # 通过维度的名称做维度转换
    convert_imgs = renamed_imgs.align_to('N', 'height', 'width','C')
    convert_imgs.namesOut:('N', 'height', 'width', 'C')

在进行张量的运算时,命名张量可以提供更高的安全性。例如,在进行Tensor的加法时,如果两个Tensor的维度名称没有对齐,那么即使它们的维度相同也无法进行计算。

In: a = t.randn(1, 2, 2, 3, names=('N', 'C', 'H', 'W'))
    b = t.randn(1, 2, 2, 3, names=('N', 'H', 'C', 'W'))# a + b # 报错,RuntimeError: Error when attempting to broadcast dims ['N', 'C', 'H', 'W'] and dims ['N', 'H', 'C', 'W']: dim 'H' and dim 'C' are at the same position from the right but do not match.

2. Tensor与NumPy

Tensor在底层设计上参考了NumPy数组,它们彼此之间的相互转换非常简单高效。因为NumPy中已经封装了常用操作,同时与Tensor之间在某些情况下共享内存,所以当遇到CPU Tensor不支持的操作时,可以先将其转成NumPy数组,完成相应处理后再转回Tensor。由于这样的操作开销很小,所以在实际应用中经常进行二者的相互转换,下面举例说明:

In: import numpy as np
    a = np.ones([2, 3], dtype=np.float32)
    aOut:array([[1., 1., 1.],[1., 1., 1.]], dtype=float32)In: # 从NumPy数组转化为Tensor,由于dtype为float32,所以a和b共享内存
    b = t.from_numpy(a)# 该种情况下,使用t.Tensor创建的Tensor与NumPy数组仍然共享内存# b = t.Tensor(a)
    bOut:tensor([[1., 1., 1.],[1., 1., 1.]])In: # 此时,NumPy数组和Tensor是共享内存的
    a[0, 1] = -1
    b # 修改a的值,b的值也会被修改Out:tensor([[ 1., -1.,  1.],[ 1.,  1.,  1.]])

注意:使用torch.Tensor()创建的张量默认dtype为float32,如果NumPy的数据类型与默认类型不一致,那么数据仅会被复制,不会共享内存。

In: a = np.ones([2, 3])# 注意和上面的a的区别(dtype不是float32)
    a.dtypeOut:dtype('float64')In: b = t.Tensor(a) # 此处进行拷贝,不共享内存
    b.dtypeOut:torch.float32In: c = t.from_numpy(a) # 注意c的类型(DoubleTensor)
    cOut:tensor([[1., 1., 1.],[1., 1., 1.]], dtype=torch.float64)In: a[0, 1] = -1print(b) # b与a不共享内存,所以即使a改变了,b也不变print(c) # c与a共享内存Out:tensor([[1., 1., 1.],[1., 1., 1.]])
    tensor([[ 1., -1.,  1.],[ 1.,  1.,  1.]], dtype=torch.float64)

注意:无论输入类型是什么,torch.tensor()都只进行进行数据拷贝,不会共享内存。读者需要注意torch.Tensor()torch.from_numpy()torch.tensor()在内存共享方面的区别。

In: a_tensor = t.tensor(a)
    a_tensor[0, 1] = 1
    a # a和a_tensor不共享内存Out:array([[ 1., -1.,  1.],[ 1.,  1.,  1.]])

除了使用上述操作完成NumPy和Tensor之间的数据转换,PyTorch还构建了torch.utils.dlpack模块。该模块可以实现PyTorch张量和DLPack内存张量结构之间的相互转换,因此,用户可以轻松实现不同深度学习框架的张量数据的交换。注意:转换后的DLPack张量与原PyTorch张量仍然是共享内存的。

3. Tensor的基本结构

Tensor的数据结构如图3-1所示。Tensor分为头信息区(Tensor)和存储区(Storage),头信息区主要保存Tensor的形状(size)、步长(stride)、数据类型(type)等信息,真正的数据在存储区保存成连续数组。头信息区元素占用内存较少,主要内存占用取决于Tensor中元素的数目,即存储区的大小。

一般来说,一个Tensor有与之对应的Storage,Storage是在data之上封装的接口。Tensor的内存地址指向Tensor的头(head),不同Tensor的头信息一般不同,但可能使用相同的Storage。关于Tensor的很多操作虽然创建了一个新的head,但是它们仍共享同一个Storage,下面举例说明。

In: a = t.arange(0, 6).float()
    b = a.view(2, 3)# Storage的内存地址一样,即它们是同一个Storage
    a.storage().data_ptr() == b.storage().data_ptr()Out:TrueIn: # a改变,b也随之改变,因为它们共享Storage
    a[1] = 100
    bOut:tensor([[  0., 100.,   2.],[  3.,   4.,   5.]])In: # 对a进行索引操作,只改变了head信息,Storage相同
    c = a[2:] 
    a.storage().data_ptr() == c.storage().data_ptr()Out:TrueIn: c.data_ptr(), a.data_ptr() # data_ptr返回Tensor首元素的内存地址# 可以看出两个内存地址相差8,这是因为2×4=8:相差两个元素,每个元素占4个字节(float)# 如果差值不是8,如16,那么可以用a.type()查看一下数据类型是不是torch.FloatTensor
 Out:(94880397551496, 94880397551488)In: c[0] = -100 # c[0]的内存地址对应a[2]的内存地址
    aOut:tensor([   0.,  100., -100.,    3.,    4.,    5.])In: d = t.Tensor(c.storage()) # d和c仍然共享内存
    d[0] = 6666
    bOut:tensor([[ 6.6660e+03,  1.0000e+02, -1.0000e+02],[ 3.0000e+00,  4.0000e+00,  5.0000e+00]])In: # 下面四个Tensor共享Storage
    a.storage().data_ptr() == b.storage().data_ptr() == c.storage().data_ptr() == d.storage().data_ptr()Out:TrueIn: # c取得a的部分索引,改变了偏移量
    a.storage_offset(), c.storage_offset(), d.storage_offset()Out:(0, 2, 0)In: e = b[::2, ::2] # 隔2行/列取一个元素print(a.storage().data_ptr() == e.storage().data_ptr()) # 共享内存print(e.is_contiguous()) # e的存储空间是不连续的
 Out:TrueFalse

由此可见,绝大多数操作不是修改Tensor的Storage,而是修改了Tensor的头信息。这种做法更节省内存,同时提升了处理速度。此外,有些操作会导致Tensor不连续,这时需要调用tensor.contiguous()方法将它们变成连续的数据。该方法会复制数据到新的内存,不再与原来的数据共享Storage。

读者可以思考一个问题,高级索引一般不共享Storage,而基本索引共享Storage,这是为什么呢?(提示:基本索引可以通过修改Tensor的offset、stride和size实现,不用修改Storage的数据,高级索引则不行。)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com