1. 數學中的張量
標量(scalar):指的是只具有數值大小,而沒有方向的量,或者說是在坐標變換下保持不變的物理量。
矢量:指的是既有大小又有方向的量。向量可以表示很多東西:表示力、速度甚至平面(作為法向量),不過向量也只表示了幅度與方向兩個要素而已。
介紹張量之前首先搞清楚兩個概念:協變性(covariance)和不變性(invariance)。
協變性指的是,在坐標系作某種變換的情況下,一些東西會根據相應的規則發生變化。最簡單的例子就是向量。向量本身不依賴於坐標系而存在,即大
小和方向不會隨着坐標系的改變而改變。但出於計算的方便我們經常會把一個向量放在坐標系中並寫出它的各個分量。當坐標系發生變換的時候,向量
本身雖然是沒有改變的,但它的分量會根據與坐標系變換方式相對應的規則去發生變化。
不變性指的是:某些量在坐標系變換下保持不變,很多標量(scalar)就是例子,比如一個電子的電荷不取決於你觀察它所用的參照系。但並不是所有
的標量在坐標系變換中都是不變量。
為了描述物理定律(或物理量)不隨坐標系的改變而改變就需引入張量,即這種量具有協變性,在參考系改變時,物理定律不變。
顯然矢量和標量具有協變性,所以它們都是張量,但是張量不止於此,它是基於標量和矢量向更高階的推廣。
首先來理解一下空間維度和張量階數。
我們要描述一個數,首先得確定一個空間,每個數都是空間中的元素,下面我們使用 $3$ 維空間來描述張量,當然 $4$ 維空間,$5$ 維空間等都有張量,
但是不好想象。首先確定 $3$ 維空間中的一組基,這組基由線性無關的 $3$ 個向量構成,最常見的就是笛卡爾坐標系 $x,y,z$ $3$ 個方向的單位向量。
我們可以很容易表示標量和矢量。標量的話只需要確定一個數值就可以了,矢量的話需要用 $3$ 個基向量確定 $3$ 個分量,注意如果一個向量有 $4$ 個
分量,比如 (1,2,3,4),那這個數就不屬於 $3$ 維空間了,而是屬於 $4$ 維空間,所以在 $3$ 維空間中是沒有辦法用向量表示具有 $4$ 個分量的數的。
究其原因,是由於 $3$ 維空間中只有 $3$ 個基向量,如果能在 $3$ 維空間中構造出更多的基向量,那就可以表示更多分量了。
構造方法就是將基向量兩兩組合或三三組合,如下圖:
最左邊的那幅圖,是原始的一組基,基向量的個數為 $3^{1}$,用這組基需要確定 $3$ 個分量。
中間那幅圖是由單個基向量兩兩組合形成的,每個組合當作一個整體看做新的基向量,新的基向量個數為 $3^{2}$,用這組基需要確定 $9$ 個分量。
最右邊那幅圖是由單個基向量三三組合形成的,每個組合也當作一個整體看做新的基向量,新的基向量個數為 $3^{3}$,用這組基需要確定 $27$ 個分量。
顯然 $3$ 維空間在一個確定的參考系中最多只能組合出 $3^{3}$ 個基向量。現在我們就可以在 $3$ 維空間中表示 $4$ 維空間中的一個向量了,即
$$4-dim \; space: \; \begin{bmatrix}
1 & 2 & 3 & 4
\end{bmatrix} \;\; \Leftrightarrow \;\; 3-dim \; space: \;\begin{bmatrix}
1 & 2 & 3\\
4 & 0 & 0\\
0 & 0 & 0
\end{bmatrix}$$
$9$ 個基向量可以表示 $9$ 個分量,表示 $4$ 個分量自然不在話下。右側這個 $3$ 行 $3$ 列的數就是張量。
張量的階數:假設每個基向量有 $n$ 個向量組合而成,那么 $n$ 就是這組基所表示的張量的階數。以上面 $3$ 張圖為例:
1)第一幅圖:每個基向量由 $1$ 個向量組成,分量個數為 $3^{1}$,所以向量是 $1$ 階張量。
2)第二幅圖:每個基向量由 $2$ 個向量組成,分量個數為 $3^{2}$,所以形如 $3$ 階方陣的這個數就是 $2$ 階張量。
3)第三幅圖:每個基向量由 $3$ 個向量組成,分量個數為 $3^{3}$,所以形如立方體的那個數就是 $3$ 階張量。
可以發現,設空間維數為 $m$,該空間中一個張量的階數為 $n$,那么該張量的分量數為 $m^{n}$,且 $n \leq m$,即 $m$ 維空間最多存在 $m$ 階張量。
注意:矩陣不是張量,但是 $n$ 階方陣在形式上與 $2$ 階張量一致。
總結一下,$3$ 維空間的各階張量的基向量和分量:
2. 深度學習中的張量
Pytorch 中的張量 Tensor 就是一個多維矩陣,它是 torch.Tensor 類型的對象,比如二階張量,在數學中就是一個方陣,在 Pytorch 中可以是任意形
狀的矩陣。在 PyTorch 中,張量 Tensor 是最基礎的運算單位,與 NumPy 中的 NDArray 類似,張量表示的是一個多維矩陣。不同的是,PyTorch 中的
Tensor可以運行在 GPU 上,而 NumPy 的 NDArray 只能運行在 CPU 上。由於 Tensor 能在 GPU 上運行,因此大大加快了運算速度。
我們所要創建的是一個 Tensor 對象,有多種不同數據類型的 Tensor,比如可以通過類 torch.FloatTensor 創建 $32$ 位的浮點型數據類型的 Tensor,
可以通過 torch.DoubleTensor 創建 $64$ 位浮點型數據類型的 Tensor,可以通過 torch.ShortTensor 創建 $16$ 位短整形數據類型的 Tensor......
也可以通過 torch.tensor 來指定類型構建 Tensor 對象。因為 Tensor 和 ndarray 類似,有些細節就不介紹了,可以去閱讀博客。
1)torch.tensor():相當於 np.array,使用方法如下:
# data - 可以是list, tuple, numpy array, scalar或其他可以轉化為 tensor 的類型,需要注意的是 torch.tensor 總是會復制 data, # dtype - 可以返回想要的 tensor 類型(元素類型) # device - 可以指定返回的設備 # requires_grad - 可以指定是否進行記錄圖的操作,默認為False torch.tensor(data, dtype=None, device=None, requires_grad=False)
舉個例子
a = torch.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]]) b = torch.tensor([[0.11111, 0.222222, 0.3333333]], dtype=torch.float64, device=torch.device('cuda:0')) # creates a torch.cuda.DoubleTensor c = torch.tensor(3.14159) # Create a scalar (zero-dimensional tensor) d = torch.tensor([]) # Create an empty tensor (of size (0,)) print(a) print(b) print(c) print(d) """ tensor([[ 0.1000, 1.2000], [ 2.2000, 3.1000], [ 4.9000, 5.2000]]) tensor([[ 0.1111, 0.2222, 0.3333]], dtype=torch.float64, device='cuda:0') tensor(3.1416) tensor([]) """
2)torch.randn():返回一個包含了從標准正態分布中抽取的一組隨機數的張量,使用方法如下:
# *size - 定義輸出張量形狀的整數序列,可以是數量可變的參數,也可以是列表或元組之類的集合 # out - 不知道干嘛用 # dtype - 返回張量所需的數據類型,如果未指定,默認為 float64 # layout - 返回張量的期望內存布局,默認值為 torch.strided # device - 指定在哪個設備上分配 tensor 內存,如果沒有,則分配在當前設備上 # requires_grad - 說明當前量是否需要在計算中保留對應的梯度信息,默認為 False torch.randn(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
舉個例子:
input = torch.rand(2,3,4,5) print(input) """ tensor([[[[0.0281, 0.4571, 0.6217, 0.1217, 0.1928], [0.3937, 0.9124, 0.1930, 0.8149, 0.9404], [0.9092, 0.9051, 0.1971, 0.7260, 0.5478], [0.1046, 0.0478, 0.1549, 0.0515, 0.4044]], [[0.6168, 0.4144, 0.7250, 0.9009, 0.7051], [0.6821, 0.1252, 0.7714, 0.7174, 0.6250], [0.9904, 0.8699, 0.3842, 0.8236, 0.8011], [0.7210, 0.4677, 0.4332, 0.3709, 0.0096]], [[0.4517, 0.5964, 0.9885, 0.3398, 0.2666], [0.1888, 0.1724, 0.0055, 0.8332, 0.5358], [0.0345, 0.6532, 0.2302, 0.8023, 0.7079], [0.5355, 0.4972, 0.1681, 0.0654, 0.2454]]], [[[0.8100, 0.1380, 0.1639, 0.3394, 0.2056], [0.7924, 0.1002, 0.9050, 0.4262, 0.6235], [0.7628, 0.0173, 0.5906, 0.0316, 0.7421], [0.7950, 0.2122, 0.9087, 0.7125, 0.8476]], [[0.2795, 0.0515, 0.0831, 0.9508, 0.5300], [0.4314, 0.4104, 0.8774, 0.0929, 0.8994], [0.7634, 0.3764, 0.9259, 0.1446, 0.6277], [0.3010, 0.5013, 0.9632, 0.7023, 0.1693]], [[0.1698, 0.5041, 0.6477, 0.1020, 0.2779], [0.8552, 0.4449, 0.3214, 0.5170, 0.2851], [0.4650, 0.0781, 0.2845, 0.0876, 0.1495], [0.0791, 0.6375, 0.3547, 0.3830, 0.1621]]]]) """
輸出的書寫方式是很有規律的,元素如果是標量,則是橫着寫的,元素如果是矢量,則是豎着寫的,括號的對齊方式以及元素之間的空格都可以
方便觀察。每個數字可以這么理解:
a. $2$:形狀為 $(3,4,5)$ 的張量復制 $2$ 份(里面的元素不一樣,只是結構一樣)。
b. $3$:形狀為 $(4,5)$ 的張量復制 $3$ 份。
c. $4$:形狀為 $(5)$ 的張量(其實就是長度為 $5$ 的向量)復制 $4$ 份。
d. $5$:元素為 $5$ 個標量。
3)torch.from_numpy():把數組轉換成張量,且二者共享內存,對張量進行修改比如重新賦值,那么原始數組也會相應發生改變。
Tensor torch.from_numpy(ndarray)
舉個例子:
import torch import numpy a = numpy.array([1, 2, 3]) t = torch.from_numpy(a) print(t) t[0] = -1 print(a) """ tensor([1, 2, 3], dtype=torch.int32) [-1 2 3] """
3. 張量運算
深度學習中的神經網絡本質就是張量運算,有四類張量運算:
1)重塑形狀:重塑張量的形狀意味着重新排列各個維度的元素個數以匹配目標形狀,重塑形成的張量和初始張量有同樣的元素。
reshape 不更改原張量形狀,而是會生成一個副本。
x = torch.tensor( [[0, 1], [2, 3], [4, 5]], dtype=torch.int) print(x) print(x.shape) x = x.reshape(6, 1) # python 里是按行來獲取元素來排列的 print(x) print(x.shape) x = x.reshape(2, -1) # 這里在 reshape 函數的第二個參數放的是 -1,意思是我不想費力來設定這一維度的元素個數,python 來幫我算出. print(x) print(x.shape) """ tensor([[0, 1], [2, 3], [4, 5]], dtype=torch.int32) torch.Size([3, 2]) tensor([[0], [1], [2], [3], [4], [5]], dtype=torch.int32) torch.Size([6, 1]) tensor([[0, 1, 2], [3, 4, 5]], dtype=torch.int32) torch.Size([2, 3]) """
2)元素層面:用運算符 $+,–, *, /$ 來連接兩個形狀一樣的張量 (要不然觸發廣播機制),只是相同位置的元素進行 $+,–, *, /$ 操作。
x = torch.tensor([2, 3, 4], dtype=torch.int) y = torch.tensor([4, 3, 2], dtype=torch.int) print(x) print(y) print(x + y) print(x - y) print(x * y) print(x / y) """ tensor([2, 3, 4], dtype=torch.int32) tensor([4, 3, 2], dtype=torch.int32) tensor([6, 6, 6], dtype=torch.int32) tensor([-2, 0, 2], dtype=torch.int32) tensor([8, 9, 8], dtype=torch.int32) tensor([0.5000, 1.0000, 2.0000]) """
3)廣播機制:當操作兩個形狀不同的張量時,可能會觸發廣播機制。廣播的張量只會在缺失的維度和長度為 $1$ 的維度上進行。過程如下:
當對兩個張量進行 $+-*/$ 時,我們先要寫出張量的形狀,然后進行右對齊,比較相同位置上的元素個數,如果不同,則操作較少的那個
張量,進行元素復制(元素完全相同),比如有兩個張量的形狀如下所示:
$$s_{1} = (5, 4, 2, 2) \\
s_{2} = (\;\;\; 1, 1, 1)$$
從尾部開始比較維度,$1 < 2$,所以將 $s_{2}$ 中的標量元素再復制一份,使 $s_{2}$ 的形狀變為 $(\;\;\; 1, 1, 2)$;比較倒數第二個元素,$1 < 2$,所以
將 $s_{2}$ 形狀為 $(2)$ 的子張量再復制 $1$ 次,此時 $s_{2}$ 的形狀變為 $(\;\;\; 1, 2, 2)$;比較倒數第 $3$ 個元素,$1 < 4$,所以將 $s_{2}$ 形狀為 $(2,2)$ 的
子張量再復制 $4$ 次,此時 $s_{2}$ 的形狀變為 $(\;\;\; 4, 2, 2)$;最后比較第一個元素,發現缺失,於是將 $s_{2}$ 形狀為 $(4, 2, 2)$ 的子張量復制 $5$ 次,
這樣一來 $s_{1}$ 和 $s_{2}$ 的形狀就一致了,就可以進行對應元素的操作。
圖中有兩行無法進行廣播,因為需要維度拓展的位置長度不為 $1$。
4)張量點乘:
a. torch.dot
x = torch.tensor([1, 2, 3], dtype=torch.int) y = torch.tensor([3, 2, 1], dtype=torch.int) print(x) print(y) print(torch.dot(x,y)) # 計算兩個張量的點積(內積),不能進行廣播(broadcast), 且只允許一維的 tensor, 即向量
b. torch.mm
A = torch.randn(2, 3) B = torch.randn(3, 4) print(A) print(B) print(torch.mm(A,B)) # 執行矩陣乘法,如果 x 為(n x m)張量,y 為(m x p)張量,那么輸出(n x p)張量, 此功能不廣播
c. torch.matmul
x = torch.tensor([1, 2, 3], dtype=torch.int) y = torch.tensor([3, 2, 1], dtype=torch.int) print(torch.matmul(x,y)) # 如果兩個張量都是一維的,則返回點積(標量) A = torch.tensor([[3, 2, 1], [1, 1, 1]], dtype=torch.int) B = torch.tensor([[1, 2], [1, 1], [3, 4]], dtype=torch.int) print(torch.matmul(A,B)) # 如果兩個參數都是二維的,則返回矩陣矩陣乘積 # 果第一個參數是一維的,而第二個參數是二維的,則為了矩陣乘法,會將 1 附加到其維數上。矩陣相乘后,將刪除前置尺寸。 # 也就是讓 x 變成矩陣表示,1x3 的矩陣和 3x2 的矩陣,得到 1x2 的矩陣,然后刪除 1 print(torch.matmul(x,B)) print(torch.matmul(A,x)) # 返回矩陣向量乘積 """ tensor(10, dtype=torch.int32) tensor([[ 8, 12], [ 5, 7]], dtype=torch.int32) tensor([12, 16], dtype=torch.int32) tensor([10, 6], dtype=torch.int32) """
如果兩個自變量至少為一維且至少一個自變量為 $N$ 維(其中 $N > 2$),則返回批處理矩陣乘法。
""" 針對多維數據 matmul 乘法,我們可以認為該 matmul 乘法使用使用兩個參數的后兩個維度來計算,其他的維度都可以認 為是 batch 維度。假設兩個輸入的維度分別是 input(100,50,99,11), other(50, 11, 99),那么我們可以認為 torch.matmul(input, other) 乘法首先是進行后兩位矩陣乘法得到 (99,99) ,然后分析兩個參數的 batch size 分 別是 (100,50) 和 (50) , 可以廣播成為 (100,50), 因此最終輸出的維度是 (100,50,99,99) """ x = torch.randn(100,50,99,11) y = torch.randn(50, 11, 99) print(torch.matmul(x, y).size()) """ torch.Size([100, 50, 99, 99]) """
5)次方和次方根運算:操作的是向量或矩陣的每個元素,是元素層面的運算
a. torch.pow:次方運算
b. **:次方運算重載符號
import torch a = torch.tensor([2,3]) print(a) print(a ** 2) print(a.pow(3)) print(torch.pow(a, 4)) """ tensor([2, 3]) tensor([4, 9]) tensor([ 8, 27]) tensor([16, 81]) """
6)torch.max():這個函數是用來消去維度的,被消去的維度只保留最大值,很抽象,下面舉個例子來解釋一下。
首先需要明白什么是維度 $dim$,一個張量需要幾個變量去訪問到標量元素,那它的 $dim$ 就是幾,比如下面這個張量因為需要 $i,j,k$ 才能
確定一個標量元素,所以 $dim = 3$。在 Pytorch 里面一個張量的維數就是階數,但在數學里面張量的維數和階數是兩個概念,這要注意一下。
怎么寫出來這個張量呢?取決於你怎么定義每個維度的方向。
對於左圖,$j$ 方向為 $dim=0$ 方向,$j=1$ 可以確定一層縱向的數據(橙色部分)。對於右圖,$i$ 方向為 $dim=0$ 方向,$i=2$ 可以得到一平面數據(橙色部分)。
現在我們以左圖為例來解釋一下 torch.max 函數,其實本質就是一種維度規約的規則。torch.max 函數原型如下:
""" input (Tensor) – the input tensor. dim (int) – the dimension to reduce.即所要消去的那個維度 keepdim (bool) – 輸出張量是否保留規約掉的那個維度. Default: False. """ torch.max(input, dim, keepdim=False) -> (Tensor, LongTensor)
現在我們要規約 $dim = 0$,這個張量的維度 $0$ 有三個元素,每個元素都是一個二維矩陣,規約后就只剩下一個元素,即一個二維矩陣。
規約的規則如下:
就是取每個元素相同位置的最大值作為結果矩陣該位置的值,因為規約后這個維度就剩一個元素了,所以該維度可以去掉。代碼如下:
import torch x = torch.tensor([[[10, 20, 30], [11, 21, 31], [12, 22, 32]], [[13, 23, 33], [14, 24, 34], [15, 25, 35]], [[16, 26, 36], [17, 27, 37], [18, 28, 38]]], dtype=torch.int) z = torch.max(x,dim = 0) print(z) """ torch.return_types.max( values=tensor([[16, 26, 36], [17, 27, 37], [18, 28, 38]], dtype=torch.int32), indices=tensor([[2, 2, 2], [2, 2, 2], [2, 2, 2]])) """
現在我們要規約 $dim = 1$,當維度 $0$ 確定的時候,維度 $1$ 有 $3$ 個元素,每個元素是一個向量,現在要把 $3$ 個向量變成一個向量。
就是取每個元素相同位置的最大值作為結果向量該位置的值,舉得例子比較極端,剛好最后一個向量就是結果。代碼如下:
import torch x = torch.tensor([[[10, 20, 30], [11, 21, 31], [12, 22, 32]], [[13, 23, 33], [14, 24, 34], [15, 25, 35]], [[16, 26, 36], [17, 27, 37], [18, 28, 38]]], dtype=torch.int) z = torch.max(x,dim = 1) print(z) """ torch.return_types.max( values=tensor([[12, 22, 32], [15, 25, 35], [18, 28, 38]], dtype=torch.int32), indices=tensor([[2, 2, 2], [2, 2, 2], [2, 2, 2]])) """
現在我們要規約 $dim = 2$,當維度 $0,1$ 都確定的時候,維度 $2$ 有 $3$ 個元素,每個元素是一個標量,現在要把 $3$ 個標量變成一個標量。
就是每組向量取最大的那個元素,設置 keepdim=True,代碼如下:
import torch x = torch.tensor([[[10, 20, 30], [11, 21, 31], [12, 22, 32]], [[13, 23, 33], [14, 24, 34], [15, 25, 35]], [[16, 26, 36], [17, 27, 37], [18, 28, 38]]], dtype=torch.int) z = torch.max(x,dim = 2,keepdim=True) print(z[0]) """ tensor([[[30], [31], [32]], [[33], [34], [35]], [[36], [37], [38]]], dtype=torch.int32) """
7)torch.cat:將兩個張量(tensor)拼接在一起。函數原型如下:
""" tensors (sequence of Tensors) – 同類型的 Tensor 序列。除了要拼接的維度外,其它維度的形狀必須一樣。 dim (int, optional) – 需要拼接的維度 """ torch.cat(tensors, dim=0) -> Tensor
舉個例子:
import torch A = torch.ones(2,3) B = 2 * torch.ones(2,4) print(A) print(B) C = torch.cat((A, B), 1) # 拼接維度 1 print(C) """ tensor([[1., 1., 1.], [1., 1., 1.]]) tensor([[2., 2., 2., 2.], [2., 2., 2., 2.]]) tensor([[1., 1., 1., 2., 2., 2., 2.], [1., 1., 1., 2., 2., 2., 2.]]) """