2018/12/23

TensorFlow 基本運算 2/2

19 TensorFlow 基本運算 2/2

🔶 列向量 (row vector) 與行向量 (column vector)
在深度學習的運算中,tensor 幾乎都是以多維的外形儲存,例如在convolution neural network (CNN)中,饋入的data是以4維的形式表示,所以一定要很清楚tensor的多維結構,否則無法寫程式完成學習的功能。這單元就來談談多維結構。

首先需注意的是列和行在台灣和中國大陸的說法剛好相反,另外有時行也稱為欄,還有在參考程式碼時雖然每一句敘述是以列的方式呈現,但我們是以行來加编號指稱,所以會說第幾行的程式如何如何,也就同等書中不管橫式排版或直式排版,都稱第幾行,只有在矩陣形式才有列行之分。另外因numpy 裡的tensor 和tf 的tensor在結構上是相容的,在tf 只要用convert_to_tensor 函數就可把np 的tensor 轉成 tf tensor,為程式執行方便以下討論有時就不區分是np 還是tf 的tensor.

單一個數字沒有用括號括起來是 tensor 是 0維,一個或更多個的數字或其他資料組成用逗號分開再用方括號(或括號)括起來就組成一維,而這維含有多少元素就稱這維的大小(size),例如[1.,2.,3.] 是一維大小為3 。常見的矩陣如 $2\times 4$ 的矩陣的維度是2,有時也說rank (秩) 或 ndim 是 2,當只有兩維時,指稱方式是列為第 0 維,行為第1維,所以第 0 維的大小是2,第1 維的大小是 4。另一個很重要的名詞是 shape (外形),這詳細的指出 tensor 有多少維和每一維的大小,維有時又稱為軸 (axis),每一維就是多維空間裡的一個軸。對維度的計算比較難以適應的是大小為1 的維,參考下列程式
import numpy as np
a = np.array((1., 2., 3.))
b = a.T
print(a)
print(a.shape)
print(a.ndim)
print(b)
print(b.shape)
print(b.ndim)
#[1. 2. 3.]
#(3,)
#1
#[1. 2. 3.]
#(3,)
#1
可以看出一個含有三個元素的一維「列向量」外形為 (3,) 其中3是大小而其後的逗號表示只有一維,維度是1,但將此向量轉置後還是外形為 (3,)的「列向量」。所以可看出維度一維「躺著的」列向量無法轉置成「站著的」行向量,這和在線性代數對向量和矩陣的認知不同,在線代裡一維的列向量轉置後就是一維的行向量。在np 裡轉置是把各維逆向互換,如外形為(2,3,4,5)的tensor 經轉置後是(5,4,3,2),在一維的情況 shape 就如上例子的 (3,),轉換後還是(3,),所以要能明確表示行向量,tensor 一定要是2維或以上,如
import numpy as np
a = np.array((1., 2., 3.))
ac1 = a[:, np.newaxis]
#ac1 = a[:, None]
# ac1 = a.reshape(3,1)
print(ac3.shape)
print (ac3.ndim)
print (ac3)
print (ac3.T)
#(3, 1)
#2
#[[1.]
# [2.]
# [3.]]
#[[1. 2. 3.]]
可見外形變成 (3,1),是在原來外形的3, 後加上一個1,在tensor的運算中,有時在tensor上加上大小為1的維,如果加上後符合運算的規則,np 會自動幫我們加,這稱為broadcasting (廣播),稍後會討論。程式中列出三種可在右邊加上維度的方法,變成二維的(3,1)後就是我們要的行向量,這時就可進行轉置成列向量。所以行向量、列向量的區別以及其間的轉置只有在維$n\geq 2$時才有意義。

🔶 Broadcasting (廣播)
廣播是指在原來的陣列上加上大小為1的維(軸),要了解廣播需先注意下列事項
  • 陣列的最內兩層是放我們熟悉的二維矩陣,所以如果shape 是(2,3)是指列的大小是2而行的大小是3。
  • 從最基本的一維的列開始,我們可以在陣列數值的左右兩側加上任意個成對的方括號(或括號),這相當於在 shape 的現有數值的前面加上 (prepending) 任意個1,這可稱為左加,一般Broadcasting都是指左加,但也有例外,如2-D矩陣乘上1-D 陣列時,用右加 把列向量變成行向量,使矩陣可以相乘。
  • 廣播是指在運算時,如果自動加上大小為1的所需數目的維後可滿足運算所需,則 np 會自動加上並進行運算。
◾ Rules of addition (加法的規則)
  • 兩個陣列的 shape 完全一樣,或
  • 自動左加維度使維度相同,但兩陣列中對應的每個維的大小要相同,若是不同其中一個必需為1。
來看看幾個實例
import numpy as np
x = np.arange(27)
y = np.ones(27)
mat1 = x.reshape(3,3,3)
mat2 = y.reshape(3,3,3)
print(mat1+mat2)
[[[ 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.]]]
上例是兩個(3,3,3)陣列元素對元素相加,陣列一個是0~26,另個為全1,故相加後為1~27。再看看下例
import numpy as np
x = np.arange(27)
y = np.array([2,2,2])
mat1 = x.reshape(3,3,3)
mat2 = y
print(mat1+mat2)
[[[ 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]]]
因y是(3,),相加時mat2自動變成(1,1,3)再和mat1相加,亦即在mat1的3個$3\times 3$的每一列都加上 [2 2 2]。再看一個不能相加的例子,
import numpy as np
x = np.arange(18)
y = np.array([2,2,2])
mat1 = x.reshape(3,3,2)
mat2 = y
print(mat1+mat2)
#ValueError: operands could not be broadcast together with shapes (3,3,2) (3,)
因mat2是(3,)而mat1的最後一維是2, 所以無法broadcast。

◾ Rules of  multiplication (乘法的規則)
從v1.10起np 新增一個np.matmul() 可以計算array相乘,大致功能和原有的np.dot相同,以下說明以 np.matmul()為例。從官網的解說中可知在2-D的陣列相乘,此函數和傳統矩陣相乘一樣。此函數兩陣列相乘的規則如下
  • 若兩個都是2-D,兩陣列需滿矩陣可相乘的 shape 條件。亦即第一個行數 要等於第二個的列數。
  • 若有一是2-D(或以上),另一為1-D,若1-D在前,則其大小要等於2-D的列數 ,若1-D在後,則其大小要等於2-D的行數
  • 若兩個都是2-D以上,除最內層兩維要滿陣列乘法條件外,共同存在大於2的維的大小要一樣或是其中一個大小為1。
  • 若兩個都是1-D,則大小要一樣,求內積。
上列第一點就是傳統的矩陣乘法,第二點前項1-D在前,相乘時左加1 維,如下例
import numpy as np
a = [[1,2],[3,4],[5,6]]
b = [5,6,7]
# print(np.matmul(a,b))  /error
print(np.matmul(b,a))
print(np.matmul(b,a).shape)
#[58 76]
#(2,)
因a是(3,2)所以b的大小一定要是3,相乘時加1維變成(1,3),乘後加入的1維又拿掉,回到1維。此例如果是b在後,則會錯,b的大小不是2。再看一個b在後的例子
import numpy as np
a = [[1,2],[3,4],[5,6]]
b = [5,6]
print(np.matmul(a,b))  
print(np.matmul(a,b).shape)
#[17 39 61]
#(3,)
例中a是(3,2),所以b會變成(2,1),亦即變成站著的行向量,乘後得到(3,1),但加入的1維又被拿掉形成(3,),亦即回到列向量。第 3 點的情形就像是加法廣播的情形,看以下的例子
import numpy as np
A = np.array([[[1.,2.,3.],[4.,5.,6.],[7.,8.,9.]],
    [[9.,9.,9.],[4.,5.,6.],[7.,8.,9.]]])
B = np.array([[1,2,3],[4.,5.,6.],[7.,8.,9.]])
C = np.matmul(A,B)
D = np.matmul(B,A)
print(A.shape)
print(B.shape)
print(C.shape)
print(D.shape)
print(C)
print('**********')
print(D)
(2, 3, 3)
(3, 3)
(2, 3, 3)
(2, 3, 3)
[[[ 30.  36.  42.]
  [ 66.  81.  96.]
  [102. 126. 150.]]

 [[108. 135. 162.]
  [ 66.  81.  96.]
  [102. 126. 150.]]]
**********
[[[ 30.  36.  42.]
  [ 66.  81.  96.]
  [102. 126. 150.]]

 [[ 38.  43.  48.]
  [ 98. 109. 120.]
  [158. 175. 192.]]]
例中A是((2,3,3) B是(3,3),相乘時B會變成 (1,3,3),也就是拿B的 (3,3) 去分別和A的兩個(3,3)相乘,乘後得到的是兩個 (3,3) 也就是 (2,3,3)。若把B也改成(2,3,3),乘後仍然(2,3,3),因是拿B的兩個 (3,3) 去和A的兩個 (3,3) 抓對相乘,得到的也是兩個 (3,3)。

🔶 進一步認識多維 tensor
要輸入多維列的值時最簡單的方法是先輸入所需的個數,再用reshape改成想要的維度及大小,但當要判讀或指參到陣列的某些值時需要對陣列的表示有很清楚的了解,特別是深度學習中陣列大多數是多維,計算處理時不能找錯對象。

◾ 不同維的資料指參
多維陣列的shape 表示方法是最右側的數字代表最內層的行向量,右2的數字就是最內層的列向量,右3表示有多少組2維的陣列,右4表示有多少組3維的陣列,依此類推。要指參陣列內的資料則是從最外層(最左)的維(軸)起算,由0開始,若是4維陣列則最內層的行向量是第3維,以下列例子為例
#四維例子
import numpy as np
a1 = np.array([[[[1, 0, 0, 0],
  [0, 2, 0, 0],
  [0, 0, 3, 0]],
  [[5, 0, 0, 0],
  [0, 6, 0, 0],
  [0, 0, 7, 0]]],
                  
  [[[1, 0, 0, 0],
  [0, 2, 0, 0],
  [0, 0, 3, 0]],
  [[5, 0, 0, 0],
  [0, 6, 0, 0],
  [0, 0, 7, 0]]],
                  
  [[[1, 0, 0, 0],
  [0, 222, 0, 0],
  [0, 0, 3, 0]],
  [[5, 0, 0, 0],
  [0, 6, 0, 0],
  [0, 0, 7, 0]]],
                  
 [[[100, 0, 0, 0],
  [0, 2, 0, 0],
  [0, 0, 3, 0]],
  [[5, 0, 0, 0],
  [0, 6, 0, 0],
  [0, 0, 71, 50]]],
                  
   [[[1, 0, 0, 0],
  [0, 2, 0, 0],
  [0, 0, 3, 0]],
  [[5, 0, 0, 0],
  [0, 6, 0, 0],
  [0, 0, 7, 0]]]]
)
print(a1.shape)
print(a1[2,0,1,1]) #取出元素方法
print(a1[1])
print("************")
print(a1[:,:,1])
(5, 2, 3, 4)
222
[[[1 0 0 0]
  [0 2 0 0]
  [0 0 3 0]]

 [[5 0 0 0]
  [0 6 0 0]
  [0 0 7 0]]]
************
[[[  0   2   0   0]
  [  0   6   0   0]]

 [[  0   2   0   0]
  [  0   6   0   0]]

 [[  0 222   0   0]
  [  0   6   0   0]]

 [[  0   2   0   0]
  [  0   6   0   0]]

 [[  0   2   0   0]
  [  0   6   0   0]]]
例中是個4維的陣列,shape 為(5,2,3,4),第0維大小為5表示內含5個3維的陣列,第1維大小為2表示每個3維的block中有2個2維的矩陣,所以這陣列中共有10個2維矩陣。取出元素的方法中若方括號內只有一個數字,表示要取出第0維內含的第幾個block,此例因有5個blocks, 可用0~4來表示。若括內數字用逗號分開向右增加表示指參的圍漸漸縮小,小至如例中的 a1[2,0,1,1]只取出一個元素。另外,冒號表示指參全部,如a1[:,:,1]指稱到所有10個二陣列的第一列,冒號前後加數字表示範圍,注意是「下閉上開」,這種切分的取法可取出想要的小block。

◾ 不同維的資料解讀
有時我們需要判讀顯示出來的資料,亦即判斷出是幾維以及各維的大小。以上列4維的資料為例,我們可以先查看左右端點有幾個成對的連續括號,此例是 4個,表示是4維,接著找看看內部有無成對的連續3個括號,注意若有3個括號則最靠近左右端點的3連括號是和端點的4連括號中的3個配成對,所以最靠近上一層的括號是和上一層配對,若有找到則計數看看有幾個,如有n個就表示第0維的大小是 n,若找不到成對的3連續括號表示第 0維的大小為1,此例可找到5個,表示第0維的大小為5。接著在任何一對3連括號內找,若沒3連括號對就找全部,找出成對的2連括號,找出 j 對就表示第1維的大小是 j,此例的5個3連block中都有2對2連括號,表示第1維的大小是 2,接著依相同的方式在任一對 2 連括號內找看看有幾個成對的1個括號對,這就表示第3維的大小,此例是 3,所以第 3維的大小是3,最後看看1括號對內有多少元素,這是最後一維,也就是第3 維的大小,此例是 4,所以確認此陣列是(5,2,3,4)。需注意的一點是當陣列很大時,顯示出來的資料會部份被省略,但省略的一般會是某一維的大小中的一部份 ,我們可以列印該陣列的 shape來查知被省略維度的大小。我們再以下例進一步說明
a=np.array([[[[1]],[[2]]]]) 
print(a.shape)
#(1, 2, 1, 1)
此例中含有很多大小為1的維,依上述方法首先找出左右兩端點是4連成對,表示此陣列是4維,接著找不到內部有任何3連成對,表示第0維(最外層)的大小是1,續找有2個2連成對,表示第1維的大小2,接著找不到1括號對,表示第3維是1,再接著最內層就只有一個元素,表示第3維的行向量的大小為 1,因而確認shape是 (1,2,1,1)。再看一個例子
b = np.array([[[[1,1]],[[2,2]]] ,[[[3,3]],[[4,4]]]])
print(b.shape)
#(2, 2, 1, 2)
例中最左右的端點是4連對,所維度4,再找可找到有2個3連配對,因而第0維大小2,再由任一個兩個3連配對的 block中找,可找到兩個2連配對,表示第1 維大小是2,接著沒有1括號對,而最內層的元素有兩個,以是 (2,2,1,2)。

沒有留言:

張貼留言