Tensor

Tensor,又名张量,可以将它简单的认为是一个数组,支持高效的科学计算。它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)或更高维的数组(高阶数据)。Tensor和numpy的array类似,但是Pytorch的tensor支持GPU加速。

基础操作

tensor的接口设计的与numpy类似,以便用户使用。
从接口的角度讲,对tensor的操作可分为两类:
(1)torch.function,如torch.save等
(2)torch.function,如tensor.view等
为方便使用,对tensor的大部分操作同时支持这两类接口。
从存储的角度讲,对tensor的操作可分为两类:
(1)不会修改自身的数据,如a.add(b),加法的结果会返回一个新的tensor。
(2)会修改自身的数据,如a.add_(b),加法的结果仍存储在a中,a被修改了。
函数名以_结尾的都是inplace方式,即会修改调用者自己的数据,在实际应用中需加以区分。

1. 创建Tensor

在Pytorch中新建tensor的方法有很多,下图是常见的创建tensor的方法:

Tensor除法 pytorch pytorch中tensor的含义_数据


其中使用Tensor函数新建tensor是最复杂多变的方式,它既可以接收一个list,并根据list的数据新建tensor,也能根据指定的形状新建tensor,还能传入其他的tensor,下面是一些例子:

from __future__ import print_function
import torch as t
# 指定tensor的形状
a = t.Tensor(2,3)
print(a)  # a的数值取决于内存空间的状态
输出:
tensor([[0., 0., 0.],
        [0., 0., 0.]])

# 用list的数据创建tensor
b = t.Tensor([[1,2,3],[4,5,6]])
print(b)
输出:
tensor([[1., 2., 3.],
        [4., 5., 6.]])

# 把tensor转为list
c = b.tolist()
print(c)
输出:
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]

**tensor.size()**返回torch.Size对象,它是tuple的子类,但其使用方式与tuple略有区别:

# b的形状
b_size = b.size()
print(b_size)
'''
torch.Size([2, 3])
'''
# b中元素个数,2*3=6,等价于b.nelement()
b_num = b.numel()
print(b_num)
'''
6
'''
# 创建一个和b形状一样的tensor
c = t.Tensor(b_size)
d = t.Tensor((2,3))   # 创建一个元素为2和3的tensor
print(c,'\n',d)
'''
tensor([[0.0000e+00, 0.0000e+00, 8.4078e-45],
        [0.0000e+00, 1.4013e-45, 0.0000e+00]]) 
tensor([2., 3.])
'''

除了tensor.size(),还可以利用tensor.shape直接查看tensor的形状,这两个函数等价:

# c的形状
c_size1 = c.shape
c_size2 = c.size()
print(c_size1,'\n',c_size2)
'''
torch.Size([2, 3])
torch.Size([2, 3])
'''

需要注意的是,t.Tensor(*size)创建tensor时,系统不会马上分配空间,只会计算剩余的内存是否足够使用,使用到tensor时才会分配,而其他操作都是在创建完tensor后马上进行空间分配。

其他一些创建tensor的方法:

print(t.ones(2,3))         # 全1
print(t.zeros(2,3))        # 全0
print(t.arange(1,6,2))     # 从1到6,步长为2
print(t.randn(2,3))        # 标准分布
print(t.linspace(2,10,3))  # 从2到10,均匀切分成3份
print(t.randperm(5))       # 随机排列
print(t.eye(2,3))          # 对角线为1,其他为0,不要求行数和列数相等
输出:
tensor([[1., 1., 1.],
        [1., 1., 1.]])
tensor([[0., 0., 0.],
        [0., 0., 0.]])
tensor([1, 3, 5])
tensor([[ 0.7347,  0.5308, -0.5022],
        [ 0.8097,  1.0758,  0.4498]])
tensor([ 2.,  6., 10.])
tensor([3, 0, 2, 4, 1])
tensor([[1., 0., 0.],
        [0., 1., 0.]])

2. Tensor操作

通过tensor.view方法可以调整tensor的形状,但必须保证调整前后元素总数一致。view不会修改自身的数据,返回的新的tensor与原tensor共享内存,即更改其中一个,另一个也会跟着改变。在实际应用中可能经常需要添加或减少某一维度,这时squeeze和unsqueeze两个函数就派上了用场。

a = t.arange(0,6)
a1 = a.view(2,3)   # 调整a的形状
print(a)
print(a1)
'''
tensor([0, 1, 2, 3, 4, 5])
tensor([[0, 1, 2],
        [3, 4, 5]])
'''
b = a.view(-1,3)  # 当某一维为-1时,会自动计算它的大小
print(b)
'''
tensor([[0, 1, 2],
        [3, 4, 5]])
'''
b1 = b.unsqueeze(1)  # 在第一维上增加‘1’
print(b1)
b2 = b.unsqueeze(-2) # -2表示倒数第二个维度
print(b2)
'''
tensor([[[0, 1, 2]],

        [[3, 4, 5]]])
tensor([[[0, 1, 2]],

        [[3, 4, 5]]])
'''
c = b.view(1,1,1,2,3)
c1 = c.squeeze(0)  # 压缩第0维的‘1’
c2 = c.squeeze()   # 压缩所有维度为‘1’的
print(c)
print(c1)
print(c2)
'''
tensor([[[[[0, 1, 2],
           [3, 4, 5]]]]])
tensor([[[[0, 1, 2],
          [3, 4, 5]]]])
tensor([[0, 1, 2],
        [3, 4, 5]])
'''

resize是另一种可用来调整size的方法,但与view不同,他可以修改tensor的尺寸。如果新尺寸超过了原尺寸,会自动分配新的内存空间,而如果新尺寸小于原尺寸,则之前的数据依旧会被保存。

d1 = b.resize_(1,3)
print(d1)
d2 = b.resize_(3,3)
print(d2)
输出:
tensor([[0, 1, 2]])
tensor([[                0,                 1,                 2],
        [                3,                 4,                 5],
        [32651548277538908, 27303553780220005, 28992339220037731]])

3. 索引操作

Tensor支持与numpy.ndarray类似的索引操作,语法上也类似。一般情况下索引出来的结果与原tensor共享内存,即修改一个,另一个也会跟着修改。

a = t.randn(3,4)
print(a)
print(a[0])        # 第1行
print(a[:,0])      # 第1列
print(a[0][2])     # 第1行第3个元素
print(a[0,-1])     # 第1行最后一个元素
print(a[:2])       # 前2行
print(a[:2,0:2])   # 前2行,第1,3列
print(a[0:1,:2])   # 第1行,前2列
print(a[0,:2])     # 这两个形状不同
print(a>1)         # 返回一个ByteTensor
print(a[a>1])      # 等价于a.masked_select(a>1),选择结果与原tensor不共享内存空间
print(a[t.LongTensor([0,1])])  # 第1行和第2行

输出:
tensor([[ 1.2822, -1.0321,  0.1433, -0.7840],
        [-0.8364,  1.4116, -1.8359, -0.7511],
        [-1.0043, -0.5043, -0.2761, -0.9449]])
tensor([ 1.2822, -1.0321,  0.1433, -0.7840])
tensor([ 1.2822, -0.8364, -1.0043])
tensor(0.1433)
tensor(-0.7840)
tensor([[ 1.2822, -1.0321,  0.1433, -0.7840],
        [-0.8364,  1.4116, -1.8359, -0.7511]])
tensor([[ 1.2822, -1.0321],
        [-0.8364,  1.4116]])
tensor([[ 1.2822, -1.0321]])
tensor([ 1.2822, -1.0321])
tensor([[ True, False, False, False],
        [False,  True, False, False],
        [False, False, False, False]])
tensor([1.2822, 1.4116])
tensor([[ 1.2822, -1.0321,  0.1433, -0.7840],
        [-0.8364,  1.4116, -1.8359, -0.7511]])

一些常用的选择函数:

Tensor除法 pytorch pytorch中tensor的含义_深度学习_02


gather是一个比较复杂的操作,对一个二维tensor,输出的每个元素如下:

out[i][j] = input[index[i][j][k]]   # 维度=0
out[i][j] = input[i][index[i],[j]]  # 维度=1

三维的tensor的gather操作同理。

a = t.arange(0,16).view(4,4)
print(a)
# 选取对角线的元素
index1 = t.LongTensor([[0,1,2,3]])
print(a.gather(0,index1))
# 选取反对角线上的元素
index2 = t.LongTensor([[3,2,1,0]]).t()
print(a.gather(1,index2))
# 选取反对角线上的元素,与上面的不同
index3 = t.LongTensor([[3,2,1,0]])
print(a.gather(0,index3))
# 选取两个对角线上的元素
index4 = t.LongTensor([[0,1,2,3],[3,2,1,0]]).t()
print(a.gather(1,index4))

输出:
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11],
        [12, 13, 14, 15]])
tensor([[ 0,  5, 10, 15]])
tensor([[ 3],
        [ 6],
        [ 9],
        [12]])
tensor([[12,  9,  6,  3]])
tensor([[ 0,  3],
        [ 5,  6],
        [10,  9],
        [15, 12]])

与gather相对应的逆操作是scatter_,gather把数据从input中按index取出,而scatter_是把取出的数据再放回去。注意scatter_函数是inplace操作。

# 把两个对角线元素放回到指定位置
a = t.arange(0,16).view(4,4).float()
index = t.LongTensor([[0,1,2,3],[3,2,1,0]]).t()
b = a.gather(1,index)
c = t.zeros(4,4)
d = c.scatter_(1,index,b)
print(d)
输出:
tensor([[ 0.,  0.,  0.,  3.],
        [ 0.,  5.,  6.,  0.],
        [ 0.,  9., 10.,  0.],
        [12.,  0.,  0., 15.]])

高级索引
Pytorch的高级索引目前已经支持绝大多数numpy风格的高级索引。高级索引可以看做是普通索引操作的扩展,但是高级索引操作的结果一般不和原始的tensor共享内存。

x = t.arange(0,27).view(3,3,3)
输出:
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]]])
print(x[[1,2],[1,2],[2,0]])  # x[1,1,2] and x[2,2,0]
输出:
tensor([14, 24])
print(x[[2,1,0],[0],[1]])  # x[2,0,1] and x[1,0,1] and x[0,0,1]
输出:
tensor([19, 10,  1])
print(x[[0,2],...])  # x[0] and x[2]
输出:
tensor([[[ 0,  1,  2],
         [ 3,  4,  5],
         [ 6,  7,  8]],

        [[18, 19, 20],
         [21, 22, 23],
         [24, 25, 26]]])

4. Tensor类型

Tensor有很多数据类型,每个类型分别对应CPU和GPU版本(HalfTensor除外):

Tensor除法 pytorch pytorch中tensor的含义_深度学习_03


默认的tensor是FloatTensor类型的。可以通过t.set_default_tensor_type修改默认tensor类型。

Tensor除法 pytorch pytorch中tensor的含义_Tensor除法 pytorch_04


各类型之间可以相互转换,一般使用type(new_type) 的方式,同时还有float、long、half等快捷方式。CPU tensor与GPU tensor之间的转换通过tensor.cudatensor.cpu的方式。Tensor还有一个new方法,用法与t.Tensor一样,会调用该tensor对应类型的构造函数,生成与当前tensor类型一致的tensor。

5. 操作方式

(1)逐元素操作

这样会对tensor的每一个元素进行操作,此类操作的输入与输出形状一致。常用的该类操作如下:

Tensor除法 pytorch pytorch中tensor的含义_pytorch_05


Tensor除法 pytorch pytorch中tensor的含义_深度学习_06


clamp常用在某些需要比较大小的地方,如取一个tensor的每个元素与另一个数的较大值:

a = t.arange(0,6).view(2,3)
print(a)
print(t.clamp(a,min=3))  # a中的每一个元素都与3相比,取较大的一个
输出:
tensor([[0, 1, 2],
        [3, 4, 5]])
tensor([[3, 3, 3],
        [3, 4, 5]])

对于很多操作,例如div、mul、pow、fmod等,PyTorch都实现了运算符重载,所以可以直接使用运算符。例如:a**2 等价于 torch.pow(a,2)。

(2)归并操作

此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。如加法sum,既可以计算整个tensor的和,也可以计算tensor中每一行或每一列的和,常用的归并操作如下:

Tensor除法 pytorch pytorch中tensor的含义_Tensor除法 pytorch_07


以上大多数函数都有一个参数dim,用来指定这些操作是哪个维度上执行的。关于dim:

Tensor除法 pytorch pytorch中tensor的含义_数据_08


(3)比较

比较函数中有一些是逐元素操作,还有一些则类似于归并操作,常用的如下:

Tensor除法 pytorch pytorch中tensor的含义_Tensor除法 pytorch_09


表中第一行已经实现运算符重载,因此可以直接用运算符,返回结果是一个ByteTensor,可用来选取元素。

max和min比较特殊,以max为例:

Tensor除法 pytorch pytorch中tensor的含义_深度学习_10

a = t.linspace(0,15,6).view(2,3)
b = t.linspace(15,0,6).view(2,3)
print(a)
print(b)
print(a>b) 
print(a[a>b])  # a中大于b的元素
print(t.max(b,dim=1))
print(t.max(a,b))
输出:
tensor([[ 0.,  3.,  6.],
        [ 9., 12., 15.]])
tensor([[15., 12.,  9.],
        [ 6.,  3.,  0.]])
tensor([[False, False, False],
        [ True,  True,  True]])
tensor([ 9., 12., 15.])
torch.return_types.max(values=tensor([15.,  6.]),indices=tensor([0, 0]))
第一个返回值的15和6分别表示第0行和第1行最大的元素;
第二个返回值的0和0表示上述最大的数是该行第0个元素
tensor([[15., 12.,  9.],
        [ 9., 12., 15.]])

(4)线性代数

Pytorch的线性函数主要封装了Blas和Lapack,其用法和接口都与之类似。常用的线性函数如下所示:

Tensor除法 pytorch pytorch中tensor的含义_Tensor除法 pytorch_11


需要注意的是,矩阵的转置会导致存储空间不连续,需调用它的.contiguous方法将其转为连续。

6. Tensor和Numpy

Tensor和Numpy数组之间具有很高的相似性,彼此之间互操作简单高效,且内存共享。一些Tensor不支持的操作,可以先转为Numpy数组,处理后再转回来,转换开销很小。

(1)当输入数组的某个维度长度为1时,计算时沿此维度复制扩充成一样的形状。

Pytorch当前已经支持自动广播法则,但是一般建议用以下方法,更直观不易出错:

Tensor除法 pytorch pytorch中tensor的含义_深度学习_12

a = t.ones(3,2)
b = t.zeros(2,3,1)
print(a+b)
# 手动广播法则
print(a.unsqueeze(0).expand(2,3,2)+b.expand(2,3,2))
print(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([[[1., 1.],
         [1., 1.],
         [1., 1.]],

        [[1., 1.],
         [1., 1.],
         [1., 1.]]])
tensor([[[1., 1.],
         [1., 1.],
         [1., 1.]],

        [[1., 1.],
         [1., 1.],
         [1., 1.]]])

广播法则是科学计算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。

Numpy的广播法则如下:

Tensor除法 pytorch pytorch中tensor的含义_Tensor除法 pytorch_13

7. 持久化

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 = t.load('a.pth')  # 加载为b,存储于GPU1上(因为保存时tensor就在GPU1上)
    c = t.load('a.pth',map_location = lambda storage,loc:storage)  # 加载为c,存储于CPU
    d = t.load('a.pth',map_location = {'cuda:1':'cuda:0'})    # 加载为d,存储于GPU0上

8. 向量化

向量化计算是一种特殊的并行计算方式,一般程序在同一时间只执行一个操作的方式,它可在同一时间执行多个操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量上。向量化可极大地提高科学计算的效率。所以在科学计算程序中尽量避免使用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 10 for_loop_add(x,y)
%timeit -n 10 x+y
输出:
10 loops, best of 3: 1.15 ms per loop
10 loops, best of 3: 11.6 µs per loop

9. 其他

t.function都有一个参数out,这时产生的结果将保存在out指定的tensor之中。
t.set_printoptions可以用来设置打印tensor时的数值精度和格式。

a = t.randn(2,3)
print(a)
t.set_printoptions(precision=10)
print(a)
输出:
tensor([[-0.8406,  0.0474,  0.5966],
        [-0.0563,  0.5799, -0.0103]])
tensor([[-0.8405807018,  0.0474130958,  0.5966325402],
        [-0.0562929250,  0.5798707008, -0.0103018042]])