Pytorch入門第二課之Tensor

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函數新建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]])

一些常用的選擇函數:
在這裏插入圖片描述
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是FloatTensor類型的。可以通過t.set_default_tensor_type修改默認tensor類型。
在這裏插入圖片描述
各類型之間可以相互轉換,一般使用type(new_type) 的方式,同時還有float、long、half等快捷方式。CPU tensor與GPU tensor之間的轉換通過tensor.cudatensor.cpu的方式。Tensor還有一個new方法,用法與t.Tensor一樣,會調用該tensor對應類型的構造函數,生成與當前tensor類型一致的tensor。

5. 操作方式

(1)逐元素操作
這樣會對tensor的每一個元素進行操作,此類操作的輸入與輸出形狀一致。常用的該類操作如下:
在這裏插入圖片描述
在這裏插入圖片描述
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中每一行或每一列的和,常用的歸併操作如下:
在這裏插入圖片描述
以上大多數函數都有一個參數dim,用來指定這些操作是哪個維度上執行的。關於dim:
在這裏插入圖片描述
(3)比較
比較函數中有一些是逐元素操作,還有一些則類似於歸併操作,常用的如下:
在這裏插入圖片描述
表中第一行已經實現運算符重載,因此可以直接用運算符,返回結果是一個ByteTensor,可用來選取元素。
max和min比較特殊,以max爲例:
在這裏插入圖片描述

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]))
第一個返回值的156分別表示第0行和第1行最大的元素;
第二個返回值的00表示上述最大的數是該行第0個元素
tensor([[15., 12.,  9.],
        [ 9., 12., 15.]])

(4)線性代數
Pytorch的線性函數主要封裝了Blas和Lapack,其用法和接口都與之類似。常用的線性函數如下所示:
在這裏插入圖片描述
需要注意的是,矩陣的轉置會導致存儲空間不連續,需調用它的.contiguous方法將其轉爲連續。

6. Tensor和Numpy

Tensor和Numpy數組之間具有很高的相似性,彼此之間互操作簡單高效,且內存共享。一些Tensor不支持的操作,可以先轉爲Numpy數組,處理後再轉回來,轉換開銷很小。
(1)當輸入數組的某個維度長度爲1時,計算時沿此維度複製擴充成一樣的形狀。
Pytorch當前已經支持自動廣播法則,但是一般建議用以下方法,更直觀不易出錯:
在這裏插入圖片描述

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的廣播法則如下:
在這裏插入圖片描述

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]])
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章