Pytorch学习(1) —— Tensor基础
学习时候作为Pytorch的学习笔记,对一些关键函数进行整理,方便后续学习时候查询用。
许多教程基于老版本Pytorch,本博客基于1.*系列的版本
本文章还在更新中,目前仅更新常用的一些方法,内容参考了官方文档和书籍《Pytorch物体检测实战》
1 Tensor的创建
注意区分: torch.Tensor是定义一个类,表示Tensor数据类型,而torch.tensor是一个函数,返回值类型为Tensor。使用torch.tensor比用Tensor似乎更好用些。
1.1 使用torch.tensor创建
torch.tensor(data, dtype, device, requires_grad)
这个是最基础的函数调用,data可以是一个list,也可以是一个numpy矩阵,dtype表示的是数据类型,device表示这个数据存在CPU还是GPU中,requires_grad用于自动求导时记录导数信息,默认为False。除了data必须,其余均可省略。
官方给出的dtype包含的类型如下所示,如果想设置32位浮点类型,那么令dtype=torch.float32
即可,其余类似。
类型 | 32位浮点 | 64位浮点 | 16位浮点 | 8位整数 | 16位整数 | 32位整数 | 64位整数 | 布尔变量 |
---|---|---|---|---|---|---|---|---|
dtype | float32 或 float | float64 或 double | float16 或 half | uint8 和 int8 | int16 和 short | int32 和 int | int64 和 long | bool |
device指定数据存储设备,可以用torch.device('cpu')
或torch.device('cuda')
指定CPU还是GPU,此外可以通过torch.device('cuda', 0)
指定设备再哪个显卡上。
下面给一个定义示例,使用torchdata.dtype
和torchdata.device
可以查看数据类型和数据所在的设备。
npdata = np.ones((2,2))
torchdata = torch.tensor(npdata, dtype=torch.float16,device=torch.device('cuda',0))
注意:torch.tensor()
会完整拷贝data,不会共享数据,下面给出几个可以共享数据的方法。
d2 = d1.requires_grad_(True) # 改变requires_grad,d1内部属性也会被改变,返回值也是个Tensor
d2 = d1.detach() # d2不会被操作,仅用于查看,在后面网络传导中常见,到时候在更新这部分
d2 = torch.as_tensor(np1) # 与numpy数据类型共享内存,当然这种都在CPU中。
1.2 其他创建操作(比如全0,全1,随机等等)
下面给出一些特殊的创建操作。
# 全是0的一个Tensor,size为尺度,比如2*3矩阵就为[2,3],out跟返回值一样,layout内存布局,不常用。
torch.zeros(size, out, dtype, layout, device, requires_grad) # 除size必须,其余均可省略
# 全是1的一个Tensor,定义用法与zeros一样
torch.ones(size, out, dtype, layout, device, requires_grad)
其他类似的还有,eye, randn, randperm等等,用法类似,后续在对这些小函数进行细致的研究。
1.3 Tensor矩阵的元素访问,维度相关
为了方便说明,先定义一个Tensor矩阵A。
Tensor的元素访问与一般的矩阵访问差别不大,访问第i行第j列的元素可以使用A[i,j]
或A[i][j]
,返回值仍然是一个Tensor,只不过这个Tensor只有一个数。如果想将这个数转为一般的Python数字,可以使用A[i][j].item()
,这样返回的值就是一般的数值,与Tensor无关。
对于Tensor的维度, 可使用A.shape
或者A.size()
函数维度, 两者等价,值得注意的是,返回维度显示是torch.Size([m,n]),实际上torch.Size是继承tuple的一个类,所以就当正常矩阵用就行,不用考虑torch转换的问题,简单来说,可以直接np.array(A.size())。
如果想查看总的元素个数,使用A.numel()
或A.nelement()
即可返回总元素个数,两者等价。
2 Tensor的组合与分块
2.1 组合
组合操作是指将不同的Tensor叠加起来, 主要有torch.cat() 和 torch.stack() 两个函数。 cat即concatenate的意思, 是指沿着已有的数据的某一维度进行拼接, 操作后数据的总维数不变, 在进行拼接时, 除了拼接的维度之外, 其他维度必须相同。 而torch.stack()函数指新增维度, 并按照指定的维度进行叠加, 具体示例如下:
a = torch.tensor([[1,2],[3,4]])
b = torch.tensor([[5,6], [7,8]])
print(torch.cat([a,b],0))
print(torch.cat([a,b],1))
输出结果如下所示,清晰易懂,注意拼接对应的维度一定要相等。
tensor([[1, 2],
[3, 4],
[5, 6],
[7, 8]])
tensor([[1, 2, 5, 6],
[3, 4, 7, 8]])
关于堆叠,按照[a,b,a]以第0,1,2维进行堆叠,则得到的矩阵维度分别为:3*2*2,2*3*2,2*2*3。堆叠原理请从结果分析即可。
print(torch.stack([a,b,a], 0)) print(torch.stack([a,b,a], 1)) print(torch.stack([a,b,a], 2))
输出:
tensor([[[1, 2], tensor([[[1, 2], tensor([[[1, 5, 1],
[3, 4]], [5, 6], [2, 6, 2]],
[1, 2]],
[[5, 6], [[3, 7, 3],
[7, 8]], [[3, 4], [4, 8, 4]]])
[7, 8],
[[1, 2], [3, 4]]])
[3, 4]]])
2.2 分块
分块则是与组合相反的操作, 指将Tensor分割成不同的子Tensor, 主要有torch.chunk() 与torch.split() 两个函数, 前者需要指定分块的数量, 而后者则需要指定每一块的大小, 以整型或者list来表示。 具体示例如下:
先把矩阵按照第0维分割,很显然矩阵按照行被分割维两个矩阵。
a = torch.tensor([[1,2,3],[4,5,6]])
print(torch.chunk(a, 2, 0))
输出:
(tensor([[1, 2, 3]]), tensor([[4, 5, 6]]))
那么如果我们按照第1维分割维两块呢,明显3不能被2除,那么会发生什么,第一个分为2*2,第二个为2*1,当不能整除时, 最后一个的维数会小于前面。
a = torch.tensor([[1,2,3],[4,5,6]])
print(torch.chunk(a, 2, 1))
输出:
(tensor([[1, 2],
[4, 5]]), tensor([[3],
[6]]))
下面使用split进行分块,返回的是一个list,分割类似chunk,只不过一个是按照块,一个是按照维度而已。
a = torch.tensor([[1,2,3],[4,5,6],[7,8,9]])
print(torch.split(a, 2, 0))
输出
(tensor([[1, 2, 3],
[4, 5, 6]]), tensor([[7, 8, 9]]))
3 Tensor的索引与变形
3.1 索引操作
索引用法与nunpy太相似了,我们先定义好一个tensor矩阵A,注意,下面说的这些操作返回值都是Tensor类型。
- 取第i行:A[i]
- 取第i行第j列值: A[i][j]
- A>0:选择A中大于0的元素, 返回和A相同大小的Tensor, 符合条件的置1, 否则置0
- A[A>0]:选择符合条件的元素并返回, 等价于torch.masked_select(A, A>0)
- torch.nonzero(A):选择非0元素的坐标, 并返回
关于较抽象的函数,torch.where(condition, x, y), 满足condition的位置输出x, 否则输出y。对Tensor元素进行限制可以使用clamp()函数,具体示例如下所示。
a = torch.tensor([[0, 1], [2, 3]])
print(torch.where(a>1, -a, a))
print(a.clamp(1,2))
输出:
tensor([[ 0, 1],
[-2, -3]])
tensor([[1, 1],
[2, 2]])
3.2 变形操作
变形操作则是指改变Tensor的维度, 以适应在深度学习的计算中, 数据维度经常变换的需求, 是一种十分重要的操作。 在PyTorch中主要有4类不同的变形方法, 如下表所示。
变形操作 | 功能 |
---|---|
view()和reshape() | 调整Tensor形状,元素总数相同 |
transpose()和permute() | 各维度之间的变换 |
squeeze()和unsqueeze() | 处理size为1的维度 |
expand()和expand_as() | 复制元素来扩展维度 |
3.2.1 view()和reshape()函数
view()和reshape()函数可以在不改变Tensor数据的前提下任意改变Tensor的形状, 必须保证调整前后的元素总数相同, 并且调整前后共享内存, 作用基本相同。
a = torch.tensor([1,2,3,4])
b = a.view(2,2)
c = a.reshape(4,1)
每个变量输出为:
tensor([1, 2, 3, 4])
tensor([[1, 2],
[3, 4]])
tensor([[1],
[2],
[3],
[4]])
如果想要直接改变Tensor的尺寸, 可以使用resize_()的原地操作函数。 在resize_()函数中, 如果超过了原Tensor的大小则重新分配内存, 多出部分置0, 如果小于原Tensor大小则剩余的部分仍然会隐藏保留。注意,resize之后,a也变了
b = a.resize_(2,3)
print(b)
输出:
tensor([[1, 2, 3],
[4, 0, 0]])
3.2.2 transpose()和permute()函数
transpose()函数可以将指定的两个维度的元素进行转置, 而permute()函数则可以按照给定的维度进行维度变换。
a = torch.arange(1,25).resize_(2,3,4)
print(a)
print(a.transpose(0,1))
print(a.transpose(0,1).size())
print(a.permute(1,2,0))
print(a.permute(1,2,0).size())
输出:
tensor([[[ 1, 2, 3, 4], tensor([[[ 1, 2, 3, 4], tensor([[[ 1, 13],
[ 5, 6, 7, 8], [13, 14, 15, 16]], [ 2, 14],
[ 9, 10, 11, 12]], [ 3, 15],
[[ 5, 6, 7, 8], [ 4, 16]],
[[13, 14, 15, 16], [17, 18, 19, 20]], [[ 5, 17],
[17, 18, 19, 20], [ 6, 18],
[21, 22, 23, 24]]]) [[ 9, 10, 11, 12], [ 7, 19],
[21, 22, 23, 24]]]) [ 8, 20]],
torch.Size([3, 2, 4]) [[ 9, 21],
[10, 22],
[11, 23],
[12, 24]]])
torch.Size([3, 4, 2])
3.2.3 squeeze()和unsqueeze()函数
在实际的应用中, 经常需要增加或减少Tensor的维度, 尤其是维度为1的情况, 这时候可以使用squeeze()与unsqueeze()函数, 前者用于去除size为1的维度, 而后者则是将指定的维度的size变为1。
a = torch.arange(1,4)
print(a.shape)
# 将第0维变为1, 因此总的维度为1、 3
print(a.unsqueeze(0).shape)
# 第0维如果是1, 则去掉该维度, 如果不是1则不起任何作用
print(a.unsqueeze(0).squeeze(0).shape)
输出:
torch.Size([3])
torch.Size([1, 3])
torch.Size([3])
3.2.4 expand()和expand_as()函数
有时需要采用复制元素的形式来扩展Tensor的维度, 这时expand就派上用场了。 expand()函数将size为1的维度复制扩展为指定大小,也可以使用expand_as()函数指定为示例Tensor的维度。
a=torch.randn(2,2,1)
print(a)
# 将第2维的维度由1变为3, 则复制该维的元素, 并扩展为3
print(a.expand(2,2,3))
输出:
tensor([[[-0.1415],
[ 0.6401]],
[[ 0.0036],
[ 1.8994]]])
tensor([[[-0.1415, -0.1415, -0.1415],
[ 0.6401, 0.6401, 0.6401]],
[[ 0.0036, 0.0036, 0.0036],
[ 1.8994, 1.8994, 1.8994]]])
注意: 在进行Tensor操作时, 有些操作如transpose()、permute()等可能会把Tensor在内存中变得不连续, 而有些操作如view()等是需要Tensor内存连续的, 这种情况下需要使用contiguous()操作先将内存变为连续的。 reshape()操作, 可以看做是Tensor.contiguous().view()。
4 Tensor的排序与取极值
比较重要的是排序函数sort(), 选择沿着指定维度进行排序, 返回排序后的Tensor及对应的索引位置。 max()与min()函数则是沿着指定维度选择最大与最小元素, 返回该元素及对应的索引位置。
a = torch.randn(3,3)
# 按照第0维即按行排序, 每一列进行比较, True代表降序, False代表升序
b = a.sort(0, True)
# 按照第0维即按行选取最大值, 返回最大值,和对应位置
c = a.max(0)
对于Tensor的单元素数学运算, 如abs()、 sqrt()、 log()、 pow()和三角函数等, 都是逐元素操(element-wise) , 输出的Tensor形状与原始Tensor形状一致。
对于类似求和、 求均值、 求方差、 求距离等需要多个元素完成的操作, 往往需要沿着某个维度进行计算, 在Tensor中属于归并操作,输出形状小于输入形状。 由于比较简单且与NumPy极为相似, 在此就不详细展开
5 Tensor的内存共享
为了实现高效计算, PyTorch提供了一些原地操作运算, 即inplace operation, 不经过复制, 直接在原来的内存上进行计算。 对于内存的共享, 主要有如下3种情况。
5.1 通过Tensor初始化Tensor
直接通过Tensor来初始化另一个Tensor, 或者通过Tensor的组合、 分块、 索引、 变形操作来初始化另一个Tensor, 则这两个Tensor共享内存。
下面这个例子,abc三个变量内存共享。
a=torch.randn(2,2)
b = a
c = a.view(4)
5.2 原地操作符
PyTorch对于一些操作通过加后缀“”实现了原地操作, 如add() 和resize_()等, 这种操作只要被执行, 本身的Tensor则会被改变。
下面这个例子,abc三个变量共享内存,原始的a已经发生变化
a = torch.tensor([[1,2],[3,4]])
b = a.add_(a)
c = a.resize_(4)
5.3 Tensor与NumPy转换
Tensor与NumPy可以进行转换, 并且转换前后的变量共享内存。 在进行PyTorch不支持的操作时, 可以将Tensor转换为NumPy类型, 操作后再转为Tensor。
a = torch.randn(1,2)
b = a.numpy()
c = torch.from_numpy(b)
d = a.tolist()
总结
本部分就是对Tensor常用的一些功能进行总结,在后续的开发中,肯定会涉及到大量没用过的函数,到时候遇到没有提到的,再补充,大规模更新版本后,这个也会同步更新。