超全總結!玩轉Pytorch張量(Tensor)!
一、什么是張量(Tensor)?
在深度學習領域,PyTorch是一個廣泛應用的開源庫,Tensor之于PyTorch就好比是array之于Numpy或者DataFrame之于Pandas,都是構建了整個框架中最為底層的核心數(shù)據(jù)結構。Pytorch中的所有操作都是在張量的基礎上進行的。
PyTorch官網(wǎng)對其的定義如下:
也就是說,一個Tensor是一個包含單一數(shù)據(jù)類型的多維矩陣。通常,其多維特性用三維及以上的矩陣來描述,例如下圖所示:單個元素為標量(scalar),一個序列為向量(vector),多個序列組成的平面為矩陣(matrix),多個平面組成的立方體為張量(tensor)。
當然,張量也無需嚴格限制在三維及以上才叫張量,就像矩陣也有一維、二維矩陣乃至多維矩陣之分一樣。
「在深度學習的范疇內,標量、向量和矩陣也可分為稱為零維張量、一維張量、二維張量?!?/p>
二、為什么深度學習要搞出Tensor?
熟悉機器學習的小伙伴們應該都知道,有監(jiān)督機器學習模型的輸入X通常是多個特征列組成的二維矩陣,輸出y是單個特征列組成的標簽向量或多個特征列組成的二維矩陣。那么深度學習中,為何要定義多維矩陣Tensor呢?
深度學習當前最成熟的兩大應用方向莫過于CV和NLP,其中CV面向圖像和視頻,NLP面向語音和文本,二者分別以卷積神經(jīng)網(wǎng)絡和循環(huán)神經(jīng)網(wǎng)絡作為核心基礎模塊,且標準輸入數(shù)據(jù)集都是至少三維以上。其中,
- 圖像數(shù)據(jù)集:至少包含三個維度(樣本數(shù)Nx圖像高度Hx圖像寬度W);如果是彩色圖像,則還需增加一個通道C,包含四個維度(NxHxWxC);如果是視頻幀,可能還需要增加一個維度T,表示將視頻劃分為T個等時長的片段。
- 文本數(shù)據(jù)集:包含三個維度(樣本數(shù)N×序列長度L×特征數(shù)H)。
因此,輸入學習模型的輸入數(shù)據(jù)結構通常都要三維以上,這也就促使了Tensor的誕生。
三、Tensor創(chuàng)建
Pytorch可基于給定數(shù)據(jù)手動創(chuàng)建Tensor,并提供了多種方式:
1.使用torch.tensor()函數(shù)直接創(chuàng)建
在PyTorch中,torch.tensor()函數(shù)用于直接從Python的數(shù)據(jù)結構(如列表、元組或NumPy數(shù)組)中創(chuàng)建一個新的張量。
"""
data:數(shù)據(jù),可以是list,numpy
dtype:數(shù)據(jù)類型,默認與data對應
device:張量所在的設備(cuda或cpu)
requires_grad:是否需要梯度
pin_memory:是否存于鎖存內存
"""
torch.tensor(data,dtype=None,device=None,requires_grad=False,pin_memory=False)
pin_memor用于實現(xiàn)鎖頁內存,創(chuàng)建DataLoader時,設置pin_memory=True,則意味著生成的Tensor數(shù)據(jù)最開始是屬于內存中的鎖頁內存,這樣將內存的Tensor轉義到GPU的顯存就會更快一些。
以下是使用torch.tensor()創(chuàng)建張量的基本示例:
import numpy as np
import torch
arr = np.ones((3, 3))
'''
[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
'''
print(arr)
# ndarray的數(shù)據(jù)類型:float64
print("ndarray的數(shù)據(jù)類型:", arr.dtype)
t= torch.tensor(arr)
'''
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
'''
print(t)
如需創(chuàng)建一個放在GPU的數(shù)據(jù),則可做如下修改,運行結果同上。
import numpy as np
import torch
device= torch.device("cuda" if torch.cuda.is_available() else 'cpu')
arr = np.ones((3, 3))
print("ndarray的數(shù)據(jù)類型:", arr.dtype)
t = torch.tensor(arr, device=device)
print(t)
2.從numpy創(chuàng)建Tensor
torch.from_numpy(ndarray)
利用該方法創(chuàng)建的tensor與原ndarray共享內存,當修改其中一個數(shù)據(jù),另外一個也會被更新。
import numpy as np
import torch
# 創(chuàng)建一個numpy數(shù)組
numpy_array = np.array([[1, 2, 3], [4, 5, 6]])
# 從numpy數(shù)組創(chuàng)建一個Tensor,并保持數(shù)據(jù)共享(更改Tensor內容會同時改變numpy數(shù)組)
tensor_from_numpy = torch.from_numpy(numpy_array)
print(tensor_from_numpy)
# 輸出:
# tensor([[1, 2, 3],
# [4, 5, 6]], dtype=torch.int32)
# 修改tensor,array也會被修改
print("# -----------修改tensor--------------*")
t[0, 0] = -1
print("numpy array: ", arr)
# 輸出:
# numpy array: [[-1 2 3]
# [ 4 5 6]]
print("tensor : ", t)
# 輸出:
# tensor([[-1, 2, 3],
# [4, 5, 6]], dtype=torch.int32)
3.根據(jù)數(shù)值創(chuàng)建張量
(1) torch.zeros():根據(jù)size創(chuàng)建全0張量
'''
size:張量的形狀
out:輸出的張量,如果指定了out,torch.zeros()返回的張量則會和out共享同一個內存地址
layout:內存中的布局方式,有strided,sparse_coo等。如果是稀疏矩陣,則可以設置為sparse_coo以減少內存占用
device:張量所在的設備(cuda或cpu)
requires_grad:是否需要梯度
'''
torch.zeros(*size, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
import torch
out_t = torch.tensor([1])
print(f"out_t初始值:{out_t}")
#指定out
t = torch.zeros((3, 3), out=out_t)
print(f"t:\n{t}")
print(f"out_t更新值:\n{out_t}")
# id是取內存地址,t和out_t是同一個內存地址
print(id(t), id(out_t), id(t) == id(out_t))
運行結果如下,由此可見,和out_t最終共享同一個內存地址。
out_t初始值:tensor([1])
t:
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
out_t更新值:
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
2083081770704 2083081770704 True
(2) torch.zeros_like:根據(jù)input形狀創(chuàng)建全0張量
torch.zeros_like(input, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format)
同理還有全1張量的創(chuàng)建:torch.ones(),torch.ones_like()
(3) torch.full() & torch.full_like():創(chuàng)建自定義某一數(shù)值的張量。
'''
size:張量的形狀,例如(3,3)
fill_value:張量中每一個元素的值。
'''
torch.full(size, fill_value, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
例如:
import torch
t = torch.full((4, 4), 10)
print(t)
'''
tensor([[10, 10, 10, 10],
[10, 10, 10, 10],
[10, 10, 10, 10],
[10, 10, 10, 10]])
'''
(4) 創(chuàng)建等差的一維張量。
?注意區(qū)間為:[start,end)。
'''
start:數(shù)列起始值,默認為0
end:數(shù)列結束值,開區(qū)間,取不到結束值
step:數(shù)列公差,默認為1
'''
torch.arange(start=0, end, step=1, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
例如:
import torch
t = torch.arange(2, 10, 2)
print(t) # tensor([2, 4, 6, 8])
(5) torch.linspace():創(chuàng)建均分的一維張量
?注意區(qū)間為:[start,end]。
'''
step:數(shù)列長度(元素個數(shù))
'''
torch.linspace(start, end, steps=100, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
import torch
t = torch.linspace(2, 10, 3)
print(t) # tensor([ 2., 6., 10.])
(6) torch.logspace():創(chuàng)建對數(shù)均分的一維張量
?注意區(qū)間為:[start,end]。
'''
step:數(shù)列長度(元素個數(shù))
base:對數(shù)函數(shù)的底,默認為 10
'''
torch.logspace(start, end, steps, base=10, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
import torch
t = torch.logspace(2, 4, 3)
print(t) # tensor([100., 1000., 10000.])
(7) torch.eye():創(chuàng)建單位對角矩陣(2維張量)
?默認輸出方陣。
'''
n: 矩陣行數(shù)。因為是方陣,通常只設置n
m: 矩陣列數(shù)
'''
torch.eye(n, m=None, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
import torch
t = torch.eye(3)
h = torch.eye(3,4)
print(f"t:{t}")
print(f"h:{h}")
運行結果如下:
t:tensor([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
h:tensor([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.]])
4.根據(jù)概率創(chuàng)建張量
(1) torch.normal():生成正態(tài)分布(高斯分布)
?返回一個張量,包含從給定參數(shù)means、std的離散正態(tài)分布中抽取的隨機數(shù)。
'''
mean:均值
std:標準差
'''
torch.normal(mean, std, *, generator=None, out=None)
包含4種模式:
- mean為標量,std為標量,此時需要設置size。
import torch
t_normal = torch.normal(0., 1., size=(4,))
# t_normal:tensor([ 0.7098, 1.5432, -0.1568, -0.6350])
print(f"t_normal:{t_normal}")
mean為張量,std為標量。
import torch
mean = torch.arange(1, 5, dtype=torch.float)
std = 1
t_normal = torch.normal(mean, std)
'''
mean:tensor([1., 2., 3., 4.])
std:1
'''
print("mean:{}\nstd:{}".format(mean, std))
#tensor([2.2450, 1.0230, 2.0299, 4.5855])
print(t_normal)
這4個數(shù)采樣分布的均值不同,但是方差都是 1。
- mean為標量,std為張量。
import torch
std = torch.arange(1, 5, dtype=torch.float)
mean = 2
t_normal = torch.normal(mean, std)
'''
mean:2
std:tensor([1., 2., 3., 4.])
'''
print("mean:{}\nstd:{}".format(mean, std))
# tensor([ 1.8482, 4.8143, -3.5074, 4.2010])
print(t_normal)
- mean為張量,std為張量。
import torch
mean = torch.arange(1, 5, dtype=torch.float)
std = torch.arange(1, 5, dtype=torch.float)
t_normal = torch.normal(mean, std)
'''
mean:tensor([1., 2., 3., 4.])
std:tensor([1., 2., 3., 4.])
'''
print("mean:{}\nstd:{}".format(mean, std))
# tensor([ 0.8195, -3.9112, 4.8498, 2.3934])
print(t_normal)
其中0.8195是從正態(tài)分布N(1,1)中采樣得到的,-3.9112是從正態(tài)分布N(2,2)中采樣得到的,其他數(shù)字以此類推。
四、Tensor屬性
1.Tensor形狀
張量具有如下形狀屬性:
- Tensor.ndim:張量的維度,例如向量的維度為1,矩陣的維度為2。
- Tensor.shape:張量每個維度上元素的數(shù)量。
- Tensor.shape[n]:張量第n維的大小。第n維也稱為軸(axis)。
- Tensor.numel:張量中全部元素的個數(shù)。
如下是創(chuàng)建一個四維Tensor,并通過圖形直觀表達以上幾個概念的關系。
import torch
Tensor=torch.ones([2,3,4,5])
print("Number of dimensions:", Tensor.ndim)
print("Shape of Tensor:", Tensor.shape)
print("Elements number along axis 0 of Tensor:", Tensor.shape[0])
print("Elements number along the last axis of Tensor:", Tensor.shape[-1])
print('Number of elements in Tensor: ', Tensor.numel()) #用.numel表示元素個數(shù)
Tensor的axis、shape、dimension、ndim之間的關系如下圖所示。
2.Tensor數(shù)據(jù)類型
torch.dtype屬性標識了torch.Tensor的數(shù)據(jù)類型。PyTorch有八種不同的數(shù)據(jù)類型:
例如:
import torch
Tensor=torch.ones([2,3,4,5])
# Data Type of every element: torch.float32
print("Data Type of every element:", Tensor.dtype)
3.Tensor所在設備
如圖所示,我們可以看到每種類型的數(shù)據(jù)都有一個CPU和一個GPU版本,因此我們對張量進行處理的時候需要指定一個設備,它要么是CPU要么是GPU,這是數(shù)據(jù)被分配的位置,這決定了給定張量的張量計算位置。
Pytorch支持多種設備的使用,我們可以用torch.device來創(chuàng)建一個設備,并指定索引,例如:
device=torch.device('cuda:0')
輸出結果為:device(type='cuda',index=0),可看到類型為'cuda',即GPU,索引0表示為第一個GPU。
五、Tensor操作
1.形狀重置
Tensor的shape可通過torch.reshape接口來改變。例如:
import torch
Tensor =torch.tensor([[[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, 27, 28, 29, 30]]])
print("the shape of Tensor:", Tensor.shape)
#利用reshape改變形狀
reshape_Tensor = torch.reshape(Tensor, [2, 5, 3])
print("After reshape:\n", reshape_Tensor)
從輸出結果看,將張量從[3, 2, 5]的形狀reshape為[2, 5, 3]的形狀時,張量內的數(shù)據(jù)不會發(fā)生改變,元素順序也沒有發(fā)生改變,只有數(shù)據(jù)形狀發(fā)生了改變。
在指定新的shape時存在一些技巧:
- -1 表示這個維度的值是從Tensor的元素總數(shù)和剩余維度自動推斷出來的。因此,有且只有一個維度可以被設置為-1。
- 0 表示該維度的元素數(shù)量與原值相同,因此shape中0的索引值必須小于Tensor的維度(索引值從 0 開始計,如第 1 維的索引值是 0,第二維的索引值是 1)。
例如:
# 直接指定目標 shape
origin:[3, 2, 5] reshape:[3, 10] actual: [3, 10]
# 轉換為 1 維,維度根據(jù)元素總數(shù)推斷出來是 3*2*5=30
origin:[3, 2, 5] reshape:[-1] actual: [30]
# 轉換為 2 維,固定一個維度 5,另一個維度根據(jù)元素總數(shù)推斷出來是 30÷5=6
origin:[3, 2, 5] reshape:[-1, 5] actual: [6, 5]
# reshape:[0, -1]中 0 的索引值為 0,按照規(guī)則
# 轉換后第 0 維的元素數(shù)量與原始 Tensor 第 0 維的元素數(shù)量相同,為3
# 第 1 維的元素數(shù)量根據(jù)元素總值計算得出為 30÷3=10。
origin:[3, 2, 5] reshape:[0, -1] actual: [3, 10]
# reshape:[3, 1, 0]中 0 的索引值為 2
# 但原 Tensor 只有 2 維,無法找到與第 3 維對應的元素數(shù)量,因此出錯。
origin:[3, 2] reshape:[3, 1, 0] error:
另外還可以通過如下方式改變shape:
- torch.squeeze:可實現(xiàn)Tensor的降維操作,即把Tensor中尺寸為1的維度刪除。
- torch.unsqueeze:可實現(xiàn)Tensor的升維操作,即向Tensor中某個位置插入尺寸為1的維度。
- torch.flatten,將Tensor的數(shù)據(jù)在指定的連續(xù)維度上展平。
- torch.transpose,對Tensor的數(shù)據(jù)進行重排。
2.索引和切片
通過索引或切片方式可訪問或修改Tensor。
(1) 「訪問Tensor」
import torch
ndim_2_Tensor = torch.tensor([[0, 1, 2, 3],
[4, 5, 6, 7],
[8, 9, 10, 11]])
print("Origin Tensor:\n", ndim_2_Tensor.numpy())
#索引或切片的第一個值對應第 0 維,第二個值對應第 1 維,
#依次類推,如果某個維度上未指定索引,則默認為 :
#所以下面兩種操作結果一樣
print("First row:", ndim_2_Tensor[0].numpy())
print("First row:", ndim_2_Tensor[0, :].numpy())
print("First column:", ndim_2_Tensor[:, 0].numpy())
print("Last column:", ndim_2_Tensor[:, -1].numpy())
print("All element:\n", ndim_2_Tensor[:].numpy())
print("First row and second column:", ndim_2_Tensor[0, 1].numpy())
(2) 「修改Tensor」
與訪問張量類似,可以在單個或多個軸上通過索引或切片操作來修改張量。
import torch
ndim_2_Tensor = torch.ones([2, 3])
ndim_2_Tensor = ndim_2_Tensor.to(torch.float32)
print('Origin Tensor:\n ', ndim_2_Tensor)
# 修改第1維為0
ndim_2_Tensor[0] = 0
print('change Tensor:\n ', ndim_2_Tensor)
# 修改第1維為2.1
ndim_2_Tensor[0:1] = 2.1
print('change Tensor:\n ', ndim_2_Tensor)
# 修改全部Tensor
ndim_2_Tensor[...] = 3
print('change Tensor:\n ', ndim_2_Tensor)
3.Tensor運算
張量支持包括基礎數(shù)學運算、邏輯運算、矩陣運算等100余種運算操作。
數(shù)學運算:
x.abs() # 逐元素取絕對值
x.ceil() # 逐元素向上取整
x.floor() # 逐元素向下取整
x.round() # 逐元素四舍五入
x.exp() # 逐元素計算自然常數(shù)為底的指數(shù)
x.log() # 逐元素計算x的自然對數(shù)
x.reciprocal() # 逐元素求倒數(shù)
x.square() # 逐元素計算平方
x.sqrt() # 逐元素計算平方根
x.sin() # 逐元素計算正弦
x.cos() # 逐元素計算余弦
x.add(y) # 逐元素加
x.subtract(y) # 逐元素減
x.multiply(y) # 逐元素乘(積)
x.divide(y) # 逐元素除
x.mod(y) # 逐元素除并取余
x.pow(y) # 逐元素冪
x.max() # 指定維度上元素最大值,默認為全部維度
x.min() # 指定維度上元素最小值,默認為全部維度
x.prod() # 指定維度上元素累乘,默認為全部維度
x.sum() # 指定維度上元素的和,默認為全部維度
邏輯運算:
x.isfinite() # 判斷Tensor中元素是否是有限的數(shù)字,即不包括inf與nan
x.equal_all(y) # 判斷兩個Tensor的全部元素是否相等,并返回形狀為[1]的布爾類Tensor
x.equal(y) # 判斷兩個Tensor的每個元素是否相等,并返回形狀相同的布爾類Tensor
x.not_equal(y) # 判斷兩個Tensor的每個元素是否不相等
x.less_than(y) # 判斷Tensor x的元素是否小于Tensor y的對應元素
x.less_equal(y) # 判斷Tensor x的元素是否小于或等于Tensor y的對應元素
x.greater_than(y) # 判斷Tensor x的元素是否大于Tensor y的對應元素
x.greater_equal(y) # 判斷Tensor x的元素是否大于或等于Tensor y的對應元素
x.allclose(y) # 判斷兩個Tensor的全部元素是否接近
矩陣運算:
x.t() # 矩陣轉置
x.transpose([1, 0]) # 交換第 0 維與第 1 維的順序
x.norm('fro') # 矩陣的弗羅貝尼烏斯范數(shù)
x.dist(y, p=2) # 矩陣(x-y)的2范數(shù)
x.matmul(y) # 矩陣乘法
4.Tensor廣播機制
深度學習任務中,通常不可避免會遇到需要使用較小形狀的Tensor與較大形狀的Tensor執(zhí)行計算的情況。此時,則需要將較小形狀的Tensor擴展到與較大形狀的Tensor一樣的形狀,以便于匹配計算,但是又「不會」對較小形狀Tensor進行「數(shù)據(jù)拷貝」操作,從而「提升算法實現(xiàn)的運算效率」。這即是廣播機制。
Tensor廣播機制通常遵循如下規(guī)則:
- 每個 Tensor 至少為一維 Tensor。
- 從最后一個維度向前開始比較兩個Tensor的形狀,需要滿足如下條件才能進行廣播:
- 兩個Tensor的維度大小相等;或者其中一個Tensor的維度為1;或者其中一個Tensor的維度不存在。
例如:
兩個Tensor的形狀一致,可以廣播。
import torch
x = torch.ones((2, 3, 4))
y = torch.ones((2, 3, 4))
z = x + y
print(z)
# tensor([[[2., 2., 2., 2.],
[2., 2., 2., 2.],
[2., 2., 2., 2.]],
[[2., 2., 2., 2.],
[2., 2., 2., 2.],
[2., 2., 2., 2.]]])
print(z.shape)
# torch.Size([2, 3, 4])
從最后一個維度向前依次比較:第一次y的維度大小為1,第二次x的維度大小為1,第三次x和y的維度大小相等,第四次y的維度不存在,所以x和y可以廣播。
import torch
x = torch.ones((2, 3, 1, 5))
y = torch.ones((3, 4, 1))
z = x + y
print(z.shape)
# torch.Size([2, 3, 4, 5])
從最后一個維度向前依次比較:第一次比較:4不等于6,不可廣播。
x = torch.ones((2, 3, 4))
y = torch.ones((2, 3, 6))
# z = x + y
# ValueError: (InvalidArgument) Broadcast dimension mismatch.
兩個Tensor進行廣播后的結果Tensor的形狀計算規(guī)則如下:
- 如果兩個 Tensor 的形狀的長度不一致,會在較小長度的形狀矩陣前部添加 1,直到兩個 Tensor 的形狀長度相等。
- 保證兩個 Tensor 形狀相等之后,每個維度上的結果維度就是當前維度上的較大值。
例如,y的形狀長度為2,小于x的形狀長度3,因此會在 y 的形狀前部添加 1,結果就是 y 的形狀變?yōu)閇1, 3, 1]。廣播之后z的形狀為[2,3,4],且z的每一維度上的尺寸,將取x和y對應維度上尺寸的較大值,如第0維x的尺寸為2,y的尺寸為1,則z的第0維尺寸為2。
import torch
x = torch.ones((2, 1, 4))
y = torch.ones((3, 1))
z = x + y
print(z.shape)
# torch.Size([2, 3, 4])
廣播機制運行過程如下圖:
圖片