ensor的数据结构如下图所示。tensor分为头信息区(Tensor)和存储区(Storage),信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息,而真正的数据则保存成连续数组。由于数据动辄成千上万,因此信息区元素占用内存较少,主要内存占用则取决于tensor中元素的数目,也即存储区的大小。
一般来说一个tensor有着与之相对应的storage, storage是在data之上封装的接口,便于使用,而不同tensor的头信息一般不同,但却可能使用相同的数据。下面看两个例子。
a = t.arange(0, 6) #a为一维tensor
print(a.storage())
"""
0
1
2
3
4
5
[torch.LongStorage of size 6]
"""
b = a.view(2, 3)
print(b.storage())
"""
0
1
2
3
4
5
[torch.LongStorage of size 6]
"""
# 一个对象的id值可以看作它在内存中的地址
# storage的内存地址一样,即是同一个storage
print(id(b.storage()) == id(a.storage()))
"""
True
"""
# a改变,b也随之改变,因为他们共享storage
a[1] = 100
print(b)
"""
tensor([[ 0, 100, 2],
[ 3, 4, 5]])
"""
c = a[2:]
print(c.storage())
"""
0
100
2
3
4
5
[torch.LongStorage of size 6]
"""
print(c.data_ptr(), a.data_ptr()) # data_ptr返回tensor首元素的内存地址
# 可以看出相差16,这是因为4*4=16--相差四个元素,每个元素占4个字节(float)
"""
1736685874768 1736685874752
"""
c[0] = -100 # c[0]的内存地址对应a[2]的内存地址
print(a) #a为一维tensor
"""
tensor([ 0, 100, -100, 3, 4, 5])
"""
d = t.LongTensor(c.storage())
d[0] = 6666
print(b)
"""
tensor([[6666, 100, -100],
[ 3, 4, 5]])
"""
# 下面4个tensor共享storage
print(id(a.storage()) == id(b.storage()) == id(c.storage()) == id(d.storage()))
"""
True
"""
#storage_offset():偏移量 以储存元素的个数的形式返回tensor在原内存中的偏移量。
print(a.storage_offset(), c.storage_offset(), d.storage_offset())
"""
0 2 0
"""
e = b[::2, ::2] # 隔2行/列取一个元素
id(e.storage()) == id(a.storage())
"""
tensor([[6666, -100]])
True
"""
#stride():步长
print(a)
print(b)
print(e)
print(b.stride(), e.stride())
"""
(3, 1) (6, 2)
"""
print(e.is_contiguous())#判断内存是否连续
"""
False
"""
可见绝大多数操作并不修改tensor的数据,而只是修改了tensor的头信息。这种做法更节省内存,同时提升了处理速度。在使用中需要注意。 此外有些操作会导致tensor不连续,这时需调用tensor.contiguous方法将它们变成连续的数据,该方法会使数据复制一份,不再与原来的数据共享storage。
其它关于Tensor的话题
GPU/CPU
tensor可以很随意的在gpu/cpu上传输。使用tensor.cuda(device_id)或者tensor.cpu()。另外一个更通用的方法是tensor.to(device)。
a = t.randn(3, 4)
print(a.device)
"""
cpu
"""
if t.cuda.is_available():
a = t.randn(3,4, device=t.device('cuda:1'))
# 等价于
# a.t.randn(3,4).cuda(1)
# 但是前者更快
print(a.device)
device = t.device('cpu')
print(a.to(device))
"""
tensor([[ 0.9213, -1.1403, -0.9568, 2.2102],
[ 1.1778, -0.8786, 0.2658, 1.8155],
[ 0.3244, 0.7009, -1.0741, -0.7994]])
"""
注意
- 尽量使用tensor.to(device), 将device设为一个可配置的参数,这样可以很轻松的使程序同时兼容GPU和CPU
- 数据在GPU之中传输的速度要远快于内存(CPU)到显存(GPU), 所以尽量避免频繁的在内存和显存中传输数据。
持久化
Tensor的保存和加载十分的简单,使用t.save和t.load即可完成相应的功能。在save/load时可指定使用的pickle模块,在load时还可将GPU tensor映射到CPU或其它GPU上。
if t.cuda.is_available():
a = a.cuda(1) # 把a转为GPU1上的tensor,
t.save(a,'a.pth')
# 加载为b, 存储于GPU1上(因为保存时tensor就在GPU1上)
b = t.load('a.pth')
# 加载为c, 存储于CPU
c = t.load('a.pth', map_location=lambda storage, loc: storage)
# 加载为d, 存储于GPU0上
d = t.load('a.pth', map_location={'cuda:1':'cuda:0'})
向量化
向量化计算是一种特殊的并行计算方式,相对于一般程序在同一时间只执行一个操作的方式,它可在同一时间执行多个操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量上。向量化可极大提高科学运算的效率,Python本身是一门高级语言,使用很方便,但这也意味着很多操作很低效,尤其是for循环。在科学计算程序中应当极力避免使用Python原生的for循环。
在实际使用中应尽量调用内建函数(buildin-function),这些函数底层由C/C++实现,能通过执行底层优化实现高效计算。因此在平时写代码时,就应养成向量化的思维习惯,千万避免对较大的tensor进行逐元素遍历。
注意:
- 大多数t.function都有一个参数out,这时候产生的结果将保存在out指定tensor之中。
- t.set_num_threads可以设置PyTorch进行CPU多线程并行计算时候所占用的线程数,这个可以用来限制PyTorch所占用的CPU数目。
- t.set_printoptions可以用来设置打印tensor时的数值精度和格式。
a = t.arange(0, 20000000)
print(a[-1], a[-2]) # 32bit的IntTensor精度有限导致溢出
b = t.LongTensor()
t.arange(0, 20000000, out=b) # 64bit的LongTensor不会溢出
print(b[-1])
print(b[-2])
"""
tensor(19999999) tensor(19999998)
tensor(19999999)
tensor(19999998)
"""
a = t.randn(2,3)
print(a)
"""
tensor([[-0.3922, 0.6426, 1.3085],
[ 0.8493, -2.3117, 0.7676]])
"""
t.set_printoptions(precision=10)
print(a)
"""
tensor([[-1.2462767363, 0.2002745867, 0.5890192389],
[ 0.3281148374, -1.7425490618, 1.7160825729]])
"""