05_pytorch的Tensor操作
- 2021 年 4 月 20 日
- 笔记
05_pytorch的Tensor操作
pytorch完整教程目录://www.cnblogs.com/nickchen121/p/14662511.html
一、引言
上一篇文章我们利用手写数字分类这个问题讲解了深度网络模型的架构以及来源,简单点说,深度网络模型就是多个分类的模型叠加在一起,而分类模型就是在回归模型上加了一个激活函数。
本次分享更多的是想让我们更好的利用torch帮我们解决更多的实际问题,而为了解决我们这第一个手写数字分类问题,首先让我们来先了解下torch的一些基础语法和基础方法。不得不再次申明,由于torch的数据类型和numpy的数据类型有异曲同工之妙,所以此处默认你有numpy的基础,如果你不是很了解numpy的用法,可以查看我的这篇博客//www.cnblogs.com/nickchen121/p/10807564.html
二、tensor的基础操作
tensor,也可以叫做张量,学过线性代数的你,其实早就解除了张量,只不过我们一直把它叫做向量和矩阵,而向量就是一维张量、矩阵是二维张量,只不过张量还可以是三维的、四维的,只是维数高了之后,我们难以理解,因此统一把它都叫做张量。
在torch中,张量是一个数据类型,也就是tensor,它和numpy中的ndarray这个数据类型很像,以及和它的操作方法也很类似,其实你可以发现,ndarray不就是一维和二维张量吗?
如果你看过我的Python博客,可以发现我把python的所有的基础类型和其对应的操作方法都讲到了,那是因为任何框架的基础都是python,python的所有操作方法都学习全面了,你自己也可以造框架。而对于框架的各种操作方法,底层无非就是一堆python代码的堆叠,也就是有些操作方法你不学,你也可以自己造出来,所以对于torch的很多不常用的内容我们可能会一笔概之或者直接不讲。
而对于tensor的基础操作,我们可以从两个方面来讲。
如果从接口的角度,对tensor的操作可以分为两类:
torch.function
,如torch.save
tensor.function
,如tensor.view
注:对于这两种接口方法,大多数时候都是等价的,如torch.sum(a,b)
和a.sum(b)
如果从存储的角度讲,对tensor的操作也可以分为两类:
a.add(b)
,不会修改a自身的数据,加法的结果会返回一个新的tensora.add_(b)
,会修改a自身的数据,也就是说加法的结果存在a中
注:函数名以_结尾的都是修改调用者自身的数据。
2.1 创建tensor
此处我只列出表格,不给出详细介绍和代码打印结果,只给出一些细节上需要注意的东西,因为它除了支持多维,其他和numpy简直一模一样
函数 | 功能 |
---|---|
Tensor(*size) |
基础构造函数 |
ones(*sizes) |
全1Tensor |
zeros(*sizes) |
全0Tensor |
eye(*sizes) |
对角矩阵(对角线为1,其他为0,不要求行列一致) |
arrange(s,e,step) |
从s到e,步长为step |
linspace(s,e,steps) |
从s到e,均匀分成steps份 |
rand/randn(*sizes) |
均匀/标准分布 |
normal(mean,std)/uniform(from,tor) |
正态分布/均匀分布 |
randperm(m) |
随机排列 |
import torch as t
如果*size
为列表,则按照列表的形状生成张量,否则传入的参数看作是张量的形状
a = t.Tensor(2, 3) # 指定形状构建2*3维的张量
a
tensor([[ 0.0000e+00, -2.5244e-29, 0.0000e+00],
[-2.5244e-29, 6.7294e+22, 1.8037e+28]])
b = t.tensor([[1, 2, 3], [2, 3, 4]]) # 通过传入列表构建2*3维的张量
b
tensor([[1, 2, 3],
[2, 3, 4]])
b.tolist() # 把b转化为列表,但是b的实际数据类型仍是tensor
[[1, 2, 3], [2, 3, 4]]
print(f'type(b): {type(b)}')
type(b): <class 'torch.Tensor'>
b.size() # 返回b的大小,等价于b.shape()
torch.Size([2, 3])
b.numel() # 计算b中的元素个数,等价于b.nelement()
6
c = t.Tensor(b.size()) # 创建一个和b一样形状的张量
c
tensor([[0.0000e+00, 3.6013e-43, 1.8754e+28],
[2.0592e+23, 1.3003e+22, 1.0072e-11]])
注:t.Tensor(*size)创建tensor时,系统不会马上分配空间,只有使用到tensor时才会分配内存,而其他操作都是在创建tensor后马上进行空间分配
2.2 常用tensor操作
2.2.1 调整tensor的形状
view()
方法调整tensor的形状,但是必须得保证调整前后元素个数一致,但是view方法不会修改原tensor的形状和数据
a = t.arange(0, 6)
a
tensor([0, 1, 2, 3, 4, 5])
b = a.view(2, 3)
print(f'a: {a}\n\n b:{b}')
a: tensor([0, 1, 2, 3, 4, 5])
b:tensor([[0, 1, 2],
[3, 4, 5]])
c = a.view(-1, 3) # -1会自动计算大小。注:我已经知道你在想什么了,两个-1你就上天吧,鬼知道你想改成什么形状的
print(f'a: {a}\n\n b:{c}')
a: tensor([0, 1, 2, 3, 4, 5])
b:tensor([[0, 1, 2],
[3, 4, 5]])
a[1] = 0 # view方法返回的tensor和原tensor共享内存,修改一个,另外一个也会修改
print(f'a: {a}\n\n b:{b}')
a: tensor([0, 0, 2, 3, 4, 5])
b:tensor([[0, 0, 2],
[3, 4, 5]])
resize()
是另一种用来调整size的方法,但是它相比较view,可以修改tensor的尺寸,如果尺寸超过了原尺寸,则会自动分配新的内存,反之,则会保留老数据
b.resize_(1, 3)
tensor([[0, 0, 2]])
b.resize_(3, 3)
tensor([[0, 0, 2],
[3, 4, 5],
[0, 0, 0]])
b.resize_(2, 3)
tensor([[0, 0, 2],
[3, 4, 5]])
2.2.2 添加或压缩tensor维度
unsqueeze()
可以增加tensor的维度;squeeze()
可以压缩tensor的维度
# 过于抽象,无法理解就跳过。
d = b.unsqueeze(
1) # 在第1维上增加“1”,也就是2*3的形状变成2*1*3。如果是b.unsqueeze(0)就是在第0维上增加1,形状变成1*2*3。
d, d.size()
(tensor([[[0, 0, 2]],
[[3, 4, 5]]]), torch.Size([2, 1, 3]))
b.unsqueeze(-1) # 在倒数第1维上增加“1”,也就是2*3的形状变成2*3*1。
tensor([[[0],
[0],
[2]],
[[3],
[4],
[5]]])
e = b.view(1, 1, 2, 1, 3)
f = e.squeeze(0) # 压缩第0维的“1”,某一维度为“1”才能压缩,如果第0维的维度是“2”如(2,1,1,1,3)则无法亚索第0维
f, f.size()
(tensor([[[[0, 0, 2]],
[[3, 4, 5]]]]), torch.Size([1, 2, 1, 3]))
e.squeeze() # 把所有维度为“1”的压缩。
tensor([[0, 0, 2],
[3, 4, 5]])
2.3 索引操作
tensor的索引操作和ndarray的索引操作类似,并且索引出来的结果与原tensor共享内存。因此在这里普通的切片操作我们就不多介绍,我们只讲解tensor一些特有的选择函数。
函数 | 功能 |
---|---|
index_select(input,dim,index) |
在指定维度dim上选取,例如选取某些行、某些列 |
masked_select(inpu,mask) |
a[a>1] 等价于a.masked_select(a>1) |
non_zero(input) |
获取非0元素的下标 |
gather(input,dim,index) |
根据index,在dim维度上选取数据,输出的size与index一样 |
import torch as t
对于上述选择函数,我们讲解一下比较难的gather函数,对于一个二维tensor,gather的输出如下所示:
out[i][j] = input[index[i][j]][j] # dim=0
out[i][j] = input[i][index[i][j]] # dim=1
a = t.arange(0, 16).view(4, 4)
a
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
# 选取对角线的元素
index = t.LongTensor([[0, 1, 2, 3]])
print(f'index: {index}')
a.gather(0, index) # dim=0
index: tensor([[0, 1, 2, 3]])
tensor([[ 0, 5, 10, 15]])
对于上述实例,可以做出如下解释:
i=0,j=0 -> index[0,0]=0 -> input[index[0,0]][0]=input[0][0] = 0
i=0,j=1 -> index[0,1]=1 -> input[index[0,1]][1]=input[1][1] = 5
i=0,j=2 -> index[0,2]=2 -> input[index[0,2]][2]=input[2][2] = 10
i=0,j=3 -> index[0,3]=3 -> input[index[0,3]][3]=input[3][3] = 15
下述实例,自行判断。
# 选取反对角线上的元素
index = t.LongTensor([[3, 2, 1, 0]]).t() # .t()是转置
print(f'index: {index}')
a.gather(1, index)
index: tensor([[3],
[2],
[1],
[0]])
tensor([[ 3],
[ 6],
[ 9],
[12]])
# 选取反对角线上的元素
index = t.LongTensor([[3, 2, 1, 0]]) # .t()是转置
a.gather(0, index)
tensor([[12, 9, 6, 3]])
# 选取两个对角线上的元素
index = t.LongTensor([[0, 1, 2, 3], [3, 2, 1, 0]]).t() # .t()是转置
print(f'index: {index}')
b = a.gather(1, index)
b
index: tensor([[0, 3],
[1, 2],
[2, 1],
[3, 0]])
tensor([[ 0, 3],
[ 5, 6],
[10, 9],
[15, 12]])
与gather函数相应的逆操作则是scatter_,scatter_可以把gather取出的元素放回去。
out = input.gather(dim, index)
out = Tensor()
out.scatter_(dim, index)
# 把两个对角线元素放回到指定位置里
c = t.zeros(4, 4, dtype=t.int64)
c.scatter_(1, index, b)
tensor([[ 0, 0, 0, 3],
[ 0, 5, 6, 0],
[ 0, 9, 10, 0],
[12, 0, 0, 15]])
2.4 高级索引
torch的高级索引和numpy的高级索引也很类似,因此照例,只讲一些复杂的高级索引方法。
注:高级索引操作的结果和原tensor不共享内存
x = t.arange(0, 27).view(3, 3, 3)
x
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],
[24, 25, 26]]])
x[[1, 2], [1, 2], [2, 0]] # x[1,1,2] 和 x[2,2,0]
tensor([14, 24])
x[[2, 1, 0], [0], [1]] # x[2,0,1],x[1,0,1],x[0,0,1]
tensor([19, 10, 1])
x[[0, 2], ...] # x[0] 和 x[2]
tensor([[[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8]],
[[18, 19, 20],
[21, 22, 23],
[24, 25, 26]]])
可以从上述三个例子看出高级索引的本质就是先循环第一个列表中的元素,然后与后面列表的元素配对,配对满足维数要求则停止,否则继续往后搜索。
对于第一个例子:
- 先从
[1,2]
中取出1 - 1和第二个列表
[1,2]
配对,满足三维要求,即[1,1,2]
,停止配对,循环步骤一取出2 - 2和第三个列表
[2,0]
配对,满足三维要求,即[2,2,0]
,停止配对
对于第二例子:
- 先从
[2,1,0]
中取出2 - 2和第二个列表
[0]
配对,不满足三维要求,继续往后搜索,和第三个列表[1]
配对,满足三维要求,即[2,0,1]
- ……
2.5 Tensor类型
2.5.1 Tensor数据类型
数据类型 | CPU tensor | GPU tensor |
---|---|---|
32bit浮点 | torch.FloatTensor | torch.cuda.FloatTensor |
64bit浮点 | torch.DoubleTensor | torch.cuda.DoubleTensor |
16bit半精度浮点 | torch.HalfTensor | torch.cuda.HalfTensor |
8bit无符号整型(0~255) | torch.ByteTensor | torch.cuda.ByteTensor |
8bit有符号整型(-128~127) | torch.CharTensor | torch.cuda.CharTensor |
16bit有符号整型 | torch.ShortTensor | torch.cuda.ShortTensor |
32bit有符号整型 | torch.IntTensor | torch.cuda.IntTensor |
64bit有符号整型 | torch.LongTensor | torch.cuda.LongTensor |
上表中只有HalfTensor值得一提,它是gpu独有的数据类型,使用该数据类型,gpu在存储该类型数据时,内存占用会减少一半,可以解决gpu显存不足的问题,但是由于它所能表示的数值大小和精度有限,所以可能存在溢出问题。
2.5.2 数据类型转换
# 设置默认tensor,系统默认tensor是FloatTensor,也仅支持浮点数类型为默认数据类型,设置成IntTensor会报错
t.set_default_tensor_type('torch.DoubleTensor')
a = t.Tensor(2, 3)
a, a.type() # a现在是DoubleTensor
(tensor([[0., 0., 0.],
[0., 0., 0.]]), 'torch.DoubleTensor')
b = a.int() # 可通过`float(), int(), double(), char(), long(), int()`更换数据类型
b.type()
'torch.IntTensor'
c = a.type_as(b) # 对a进行数据类型转换
c, c.type()
(tensor([[0, 0, 0],
[0, 0, 0]], dtype=torch.int32), 'torch.IntTensor')
d = a.new(2, 3) # 生成与a数据类型一致的tensor
d, d.type()
(tensor([[ 2.0000e+00, 2.0000e+00, 3.9525e-323],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00]]), 'torch.DoubleTensor')
a.new?? # 查看new的源码
t.set_default_tensor_type('torch.FloatTensor') # 恢复之前的默认设置
2.5.3 cpu和gpu间数据类型转换
cpu和gpu的数据类型通常有tensor.cpu()
和tensor.gpu()
互相转换,由于我的电脑没有gpu,从网上摘抄一段供大家参考:
In [115]: a = t.ones(2,3)
In [116]: a.type()
Out[116]: 'torch.FloatTensor'
In [117]: a
Out[117]:
tensor([[1., 1., 1.],
[1., 1., 1.]])
In [118]: a.cuda()
Out[118]:
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:0')
In [119]: b = a.cuda()
In [120]: b
Out[120]:
tensor([[1., 1., 1.],
[1., 1., 1.]], device='cuda:0')
In [121]: b.type()
Out[121]: 'torch.cuda.FloatTensor'
In [122]: b.cpu()
Out[122]:
tensor([[1., 1., 1.],
[1., 1., 1.]])
In [123]: b.cpu().type()
Out[123]: 'torch.FloatTensor'
2.6 逐元素操作
通俗点讲,就是对tensor进行数学操作,只不过是对tensor的每个元素都进行相对应的操作,因此叫做逐元素操作,也因此该类操作的输出形状与原tensor形状一致。常见的逐元素操作如下表:
函数 | 功能 |
---|---|
mul/abs/sqrt/exp/fmod/log/pow…… | 乘法(*)/绝对值/平方根/除法(/)/指数/求余(%)/求幂(**) |
cos/sin/asin/atan2/cosh | 三角函数 |
ceil/round/floor/trunc | 上取整/四舍五入/下去整/只保留整数部分 |
clamp(input,min,max) |
超过min和max部分截断 |
sigmod/tanh/… | 激活函数 |
针对上述一些运算符,torch实现了运算符重载,例如a**2
等价于torch.pow(a,2)
。
针对clamp函数,它的输出满足下述公式:
\begin{cases}
& min,\quad\text{if x_i < min} \\
& x_i,\quad\quad\text{if min}\leq\text{x_i}\leq\text{max}\\
& max,\quad\text{if x_i > max}
\end{cases}
\]
a = t.arange(0, 6).view(2, 3)
a
tensor([[0, 1, 2],
[3, 4, 5]])
a.clamp(min=3)
tensor([[3, 3, 3],
[3, 4, 5]])
2.7 归并操作
该类操作可以沿着某一维度进行指定操作,因此它们的输出形状一般小于元tensor形状。如加法sum,可以计算正整个tensor的和,也可以计算某一行或某一列的和。常用的归并操作如下表所示:
函数 | 功能 |
---|---|
mean/sum/median/mode | 均值/和/中位数/众数 |
norm/dist | 范数/距离 |
std/var | 标准差/方差 |
cunsum/cumprod | 累加/累乘 |
以上函数大多都有一个dim参数(对应numpy中的axis参数),它的使用如下所示(假设输入的形状是(a,b,c)):
- 如果指定dim=0,输出形状是(1,b,c)或(b,c)
- 如果指定dim=1,输出形状是(a,1,c)或(a,c)
- 如果指定dim=2,输出形状是(a,b,1)或(a,b)
对于上述操作是否保留输出形状中的“1”,取决于参数keepdim,如果keepdim=True
则保留,反之不保留。但是从torch0.2.0版本开始,统一不保留。虽然以上总结适用于大多数函数,但是对于cumsum函数,则不适用该规则。
b = t.ones(2, 3)
b.sum(dim=0), b.sum(dim=0, keepdim=True) # 前者输出形状是(3),后者输出形状是(1,3)
(tensor([2., 2., 2.]), tensor([[2., 2., 2.]]))
a = t.arange(0, 6).view(2, 3)
a
tensor([[0, 1, 2],
[3, 4, 5]])
a.cumsum(dim=1) # 对第二个维度行的元素按照索引顺序进行累加
tensor([[ 0, 1, 3],
[ 3, 7, 12]])
2.8 比较
对于比较函数中,有些函数逐元素操作,有些函数则类似于归并不逐元素操作。常用的比较函数有:
函数 | 功能 |
---|---|
gt/lt/le/eq/ne | 大于(>)/小于(<)/大于等于(>=)/小于等于(<=)/等于)(=)/不等(!=) |
topk | 最大的k个数 |
sort | 排序 |
max/min | 比较两个tensor的最大值和最小值 |
其中max和min两个函数有点特殊,它们有以下三种情况:
t.max(tensor)
:返回tensor中最大的一个数t.max(tensor,dim)
:指定维上最带的数,返回tensor和下标t.max(tensor1,tensor2)
:比较两个tensor相比较大的元素
a = t.linspace(0, 15, 6).view(2, 3)
a
tensor([[ 0., 3., 6.],
[ 9., 12., 15.]])
b = t.linspace(15, 0, 6).view(2, 3)
b
tensor([[15., 12., 9.],
[ 6., 3., 0.]])
t.max(a)
tensor(15.)
t.max(a, 1) # 返回第0行和第1行的最大的元素
torch.return_types.max(
values=tensor([ 6., 15.]),
indices=tensor([2, 2]))
t.max(a, b)
tensor([[15., 12., 9.],
[ 9., 12., 15.]])
2.9 线性代数
常用的线性代数函数如下表所示:
函数 | 功能 |
---|---|
trace | 对角线元素之和(矩阵的迹) |
diag | 对角线元素 |
triu/tril | 矩阵的上三角/下三角,可指定偏移量 |
mm/bmm | 矩阵的乘法/batch的矩阵乘法 |
addmm/addbmm/addmv | 矩阵运算 |
t | 转置 |
dot/cross | 内积/外积 |
inverse | 求逆矩阵 |
svd | 奇异值分解 |
其中矩阵的转置会导致存储空间不连续,需调用它的.contiguous方法让它连续
b = a.t()
b, b.is_contiguous()
(tensor([[ 0., 9.],
[ 3., 12.],
[ 6., 15.]]), False)
b = b.contiguous()
b, b.is_contiguous()
(tensor([[ 0., 9.],
[ 3., 12.],
[ 6., 15.]]), True)
三、Tensor和Numpy
由于tensor和ndarray具有很高的相似性,并且两者相互转化需要的开销很小。但是由于ndarray出现时间较早,相比较tensor有更多更简便的方法,因此在某些时候tensor无法实现某些功能,可以把tensor转换为ndarray格式进行处理后再转换为tensor格式。
3.1 tensor数据和ndarray数据相互转换
import numpy as np
a = np.ones([2, 3], dtype=np.float32)
a
array([[1., 1., 1.],
[1., 1., 1.]], dtype=float32)
b = t.from_numpy(a) # 把ndarray数据转换为tensor数据
b
tensor([[1., 1., 1.],
[1., 1., 1.]])
b = t.Tensor(a) # 把ndarray数据转换为tensor数据
b
tensor([[1., 1., 1.],
[1., 1., 1.]])
a[0, 1] = 100
b
tensor([[ 1., 100., 1.],
[ 1., 1., 1.]])
c = b.numpy() # 把tensor数据转换为ndarray数据
c
array([[ 1., 100., 1.],
[ 1., 1., 1.]], dtype=float32)
3.2 广播法则
广播法则来源于numpy,它的定义如下:
- 让所有输入数组都向其中shape最长的数组看齐,shape中不足部分通过在前面加1补齐
- 两个数组要么在某一个维度的长度一致,要么其中一个为1,否则不能计算
- 当输入数组的某个维度的长度为1时,计算时沿此维度复制扩充×一样的形状
torch当前支持自动广播法则,但更推荐使用以下两个方法进行手动广播,这样更直观,更不容出错:
- unsqueeze或view:为数据某一维的形状补1
- expand或expand_as:重复数组,实现当输入的数组的某个维度的长度为1时,计算时沿此维度复制扩充成一样的形状
注:repeat与expand功能相似,但是repeat会把相同数据复制多份,而expand不会占用额外空间,只会在需要的时候才扩充,可以极大地节省内存。
a = t.ones(3, 2)
b = t.zeros(2, 3, 1)
自动广播法则:
- a是二维,b是三维,所在现在较小的a前面补1(等价于
a.unsqueeze(0)
,a的形状变成(0,2,3)) - 由于a和b在第一维和第三维的形状不一样,利用广播法则,两个形状都变成了(2,3,2)
a + b
tensor([[[1., 1.],
[1., 1.],
[1., 1.]],
[[1., 1.],
[1., 1.],
[1., 1.]]])
对上述自动广播可以通过以下方法实现手动广播
a.unsqueeze(0).expand(2, 3, 2) + b.expand(
2, 3, 2) # 等价于a.view(1,3,2).expand(2,3,2) + b.expand(2,3,2)
tensor([[[1., 1.],
[1., 1.],
[1., 1.]],
[[1., 1.],
[1., 1.],
[1., 1.]]])
四、Tensor内部存储结构
tensor的数据存储结构如上图所示,它分为信息区(Tensor)和存储区(Storage),信息区主要保存tensor的形状、数据类型等信息;而真正的数据则保存成连续数组存放在存储区。
一个tensor有着一个与之对应的storage,storage是在data之上封装的接口,便于使用。不同的tensor的头信息一般不同,但却有可能使用相同的storage。
a = t.Tensor([0, 1, 2, 3, 4, 5])
b = a.view(2, 3)
id(a.storage()), id(b.storage()), id(a.storage()) == id(b.storage())
(140397108640200, 140397108640200, True)
a[1] = 100 # a改变,b进而随之改变,因为它们共享内存
b
tensor([[ 0., 100., 2.],
[ 3., 4., 5.]])
c = a[2:]
# data_ptr返回tensor首元素的地址
c.data_ptr() - a.data_ptr() # 相差16,这是因为2*8=16相差两个元素,每个元素占8个字节
8
c[0] = -100 # c和a共享内存
a
tensor([ 0., 100., -100., 3., 4., 5.])
c.storage()
0.0
100.0
-100.0
3.0
4.0
5.0
[torch.FloatStorage of size 6]
d = t.Tensor(c.storage()) # 使用a的存储数据建立d
d[0] = 666
a
tensor([ 666., 100., -100., 3., 4., 5.])
id(a.storage()) == id(b.storage()) == id(c.storage()) == id(d.storage())
True
# storage_offset是数据在storage中的索引,a和d从sotrage的第一个元素开始找,c是从第三个元素开始查找
a.storage_offset(), c.storage_offset(), d.storage_offset()
(0, 2, 0)
e = b[::2, ::2] # 从0开始,每隔2行/列取一个元素
e
tensor([[ 666., -100.]])
b
tensor([[ 666., 100., -100.],
[ 3., 4., 5.]])
e.storage()
666.0
100.0
-100.0
3.0
4.0
5.0
[torch.FloatStorage of size 6]
# stride是storage中对应于tensor的相邻维度间第一个索引的跨度
# 对于b,第一行第一个元素到第二行第一个元素的索引差距为3,第一列第一个元素到到第二列第一个元素的索引差距为1
# 对于e,第一行第一个元素到第二行第一个元素(空)的索引差距为6,第一列第一个元素到到第二列第一个元素的索引差距为2
b.stride(), e.stride()
((3, 1), (6, 2))
e.is_contiguous()
False
id(d.storage()), id(e.storage())
(140397108641736, 140397108641736)
e.contiguous()
id(e.storage())
140397108699912
从上可见大多数操作并不会修改tensor的数据,只是修改tensor的头信息,这种做法减少了内存的占用,并且更加节省了时间。但是有时候这种操作会导致tensor不连续,此时可以通过contiguous方法让其连续,但是这种方法会复制数据到新的内存空间,不再和原来的数据共享内存。
五、其他
5.1 持久化
和sklearn中的持久化一样,保存一个模型或者特有的数据为pkl数据。但是tensor在加载数据的时候还可以把gpu tensor映射到cpu上或者其他gpu上。
5.1.1 保存模型
if t.cuda.is_available():
a = a.cuda(1) # 把a转为gpu1上的tensor
t.save(a, 'a.pkl')
5.1.2 加载模型
# 加载为b,存储于gpu1上(因为保存时tensor就在gpu1上)
b = t.load('a.pkl')
# 加载为c,存储于cpu
c = t.load('a.pkl', map_location=lambda storage, loc: storage)
# 加载为d,存储于gpu0上
d = t.load('a.pkl', map_location={'cuda:1': 'cuda:0'})
5.2 向量化
向量化计算是一种特殊的并行计算方法,通常是对不同的数据执行同样的一个或一批指令。由于Python原生的for循环效率低下,因此可以尽可能的使用向量化的数值计算。
def for_loop_add(x, y):
result = []
for i, j in zip(x, y):
result.append(i + j)
return t.Tensor(result)
x = t.zeros(100)
y = t.ones(100)
%timeit -n 100 for_loop_add(x,y)
%timeit -n 100 x+y
566 µs ± 100 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.25 µs ± 1.63 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
从上面可以看见,如果自己写一个方法实现内建函数,运行时间相差200倍,因为内建函数底层大多由c/c++实现,能通过执行底层优化实现高效计算。所以平时在写代码时,应该养成向量化的思维习惯。
5.3 注意事项
除了上述讲的大多数内容,最后还有以下三点需要注意:
- 大多数
t.function
都有一个参数out,可以将其产生的结果保存在out指定的tensor之中 t.set_num_threads
可以设置torch进行cpu多线程并行计算时所占用的线程数,用来限制torch所占用的cpu数目t.set_printoptions
可以用来设置打印tensor时的数值精度和格式
b = t.FloatTensor()
t.randn(2, 3, out=b)
b
tensor([[ 1.4754, -0.7392, -0.1900],
[-0.8091, 0.2227, 0.8951]])
t.set_printoptions(precision=10)
b
tensor([[ 1.4753551483, -0.7392477989, -0.1899909824],
[-0.8091416359, 0.2227495164, 0.8951155543]])
六、总结
这一篇章幅度较大,对于熟悉numpy的同学可能得心应手很多,如果对numpy不是特别熟悉的同学,建议先按照上述所给的教程学一遍numpy,再过来学习tensor这个数据类型,从一二维过渡到高维,也将更容易上手。
这篇文章内容虽多,但从实用的角度来说,相对而言也比较全面,其中内容不需要全部熟稔于心,但至少得对每个方法都大概有个印象,知道有这个东西,这个东西能干啥!