
接着上篇繼續後面兩個章節,數組變形和數組計算。
4 數組的變形
本節介紹四大類數組層面上的操作,具體有
- 重塑 (reshape) 和打平 (ravel, flatten)
- 合并 (concatenate, stack) 和分裂 (split)
- 重複 (repeat) 和拼接 (tile)
- 其他操作 (sort, insert, delete, copy)
4.1 重塑和打平
重塑 (reshape) 和打平 (ravel, flatten) 這兩個操作僅僅隻改變數組的次元
- 重塑是從低維到高維
- 打平是從高維到低維
重塑
用reshape()函數将一維數組 arr 重塑成二維數組。
arr = np.arange(12)
print( arr )
print( arr.reshape((4,3)) )
複制
[ 0 1 2 3 4 5 6 7 8 9 10 11]
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
複制
思考:為什麼重塑後的數組不是
[[ 0 4 8]
[ 1 5 9]
[ 2 6 10]
[ 3 7 11]]
複制
當你重塑高維矩陣時,不想花時間算某一次元的元素個數時,可以用「-1」取代,程式會自動幫你計算出來。比如把 12 個元素重塑成 (2, 6),你可以寫成 (2,-1) 或者 (-1, 6)。
print( arr.reshape((2,-1)) )
print( arr.reshape((-1,6)) )
複制
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]]
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]]
複制
打平
用 ravel() 或flatten() 函數将二維數組 arr 打平成一維數組
arr = np.arange(12).reshape((4,3))
print( arr )
ravel_arr = arr.ravel()
print( ravel_arr )
flatten_arr = arr.flatten()
print( flatten_arr )
複制
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
[ 0 1 2 3 4 5 6 7 8 9 10 11]
[ 0 1 2 3 4 5 6 7 8 9 10 11]
複制
思考:為什麼打平後的數組不是
[ 0 3 6 9 1 4 7 10 2 5 8 11]
複制
要回答本節兩個問題,需要了解 numpy 數組在記憶體塊的存儲方式。
【行主序和列主序】
行主序 (row-major order) 指每行的元素在記憶體塊中彼此相鄰,而列主序 (column-major order) 指每列的元素在記憶體塊中彼此相鄰。
在衆多計算機語言中,
- 預設行主序的有 C 語言(下圖 order=‘C’ 等價于行主序)
- 預設列主序的有 Fortran 語言(下圖 order=‘F’ 等價于列主序)
在 numpy 數組中,預設的是行主序,即 order ='C'。現在可以回答本節那兩個問題了。
如果你真的想在「重塑」和「打平」時用列主序,隻用把 order 設為 'F',以重塑舉例:
print( arr.reshape((4,3), order='F') )
複制
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
複制
細心的讀者可能已經發現為什麼「打平」需要兩個函數 ravel() 或 flatten()?它們的差別在哪裡?
知識點
函數 ravel() 或 flatten() 的不同之處是
- ravel() 按「行主序」打平時沒有複制原數組,按「列主序」在打平時複制了原數組
- flatten() 在打平時複制了原數組
用代碼驗證一下,首先看 flatten(),将打平後的數組 flatten 第一個元素更新為 10000,并沒有對原數組 arr 産生任何影響 (證明 flatten() 是複制了原數組)
arr = np.arange(6).reshape(2,3)
print( arr )
flatten = arr.flatten()
print( flatten )
flatten_arr[0] = 10000
print( arr )
複制
[[0 1 2]
[3 4 5]]
[0 1 2 3 4 5]
[[0 1 2]
[3 4 5]]
複制
再看 ravel() 在「列主序」打平,将打平後的數組 ravel_F 第一個元素更新為 10000,并沒有對原數組 arr 産生任何影響 (證明 ravel(order='F') 是複制了原數組)
ravel_F = arr.ravel( order='F' )
ravel_F[0] = 10000
print( ravel_F )
print( arr )
複制
[10000 3 1 4 2 5]
[[0 1 2]
[3 4 5]]
複制
最後看 ravel() 在「行主序」打平,将打平後的數組 ravel_C 第一個元素更新為 10000,原數組 arr[0][0] 也變成了 10000 (證明 ravel() 沒有複制原數組)
ravel_C = arr.ravel()
ravel_C[0] = 10000
print( ravel_C )
print( arr )
複制
[10000 1 2 3 4 5]
[[10000 1 2]
[ 3 4 5]]
複制
4.2 合并和分裂
合并 (concatenate, stack) 和分裂 (split) 這兩個操作僅僅隻改變數組的分合
- 合并是多合一
- 分裂是一分多
合并
使用「合并」函數有三種選擇
- 有通用的 concatenate
- 有專門的 vstack, hstack, dstack
- 有極簡的 r_, c_
用下面兩個數組來舉例:
arr1 = np.array([[1, 2, 3], [4, 5, 6]])
arr2 = np.array([[7, 8, 9], [10, 11, 12]])
複制
concatenate
np.concatenate([arr1, arr2], axis=0)
np.concatenate([arr1, arr2], axis=1)
複制
[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]
[10 11 12]]
[[ 1 2 3 7 8 9]
[ 4 5 6 10 11 12]]
複制
在 concatenate() 函數裡通過設定軸,來對數組進行豎直方向合并 (軸 0) 和水準方向合并 (軸 1)。
vstack, hstack, dstack
通用的東西是好,但是可能效率不高,NumPy 裡還有專門合并的函數
- vstack:v 代表 vertical,豎直合并,等價于 concatenate(axis=0)
- hstack:h 代表 horizontal,水準合并,等價于 concatenate(axis=1)
- dstack:d 代表 depth-wise,按深度合并,深度有點像彩色照片的 RGB 通道
一圖勝千言:
用代碼驗證一下:
print( np.vstack((arr1, arr2)) )
print( np.hstack((arr1, arr2)) )
print( np.dstack((arr1, arr2)) )
複制
[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]
[10 11 12]]
-----------------------
[[ 1 2 3 7 8 9]
[ 4 5 6 10 11 12]]
-----------------------
[[[ 1 7]
[ 2 8]
[ 3 9]]
[[ 4 10]
[ 5 11]
[ 6 12]]]
複制
和 vstack, hstack 不同,dstack 将原數組的次元增加了一維。
np.dstack((arr1, arr2)).shape
複制
(2, 3, 2)
複制
r_, c_
此外,還有一種更簡單的在豎直和水準方向合并的函數,r_() 和 c_()。
print( np.r_[arr1,arr2] )
print( np.c_[arr1,arr2] )
複制
[[ 1 2 3]
[ 4 5 6]
[ 7 8 9]
[10 11 12]]
[[ 1 2 3 7 8 9]
[ 4 5 6 10 11 12]]
複制
除此之外,r_() 和 c_() 有什麼特别之處麼?(如果完全和 vstack() 和hstack() 一樣,那也沒有存在的必要了)
知識點
1. 參數可以是切片。
print( np.r_[-2:2:1, [0]*3, 5, 6] )
複制
[-2 -1 0 1 0 0 0 5 6]
複制
2. 第一個參數可以是控制參數,如果它用 'r' 或 'c' 字元可生成線性代數最常用的 matrix (和二維 numpy array 稍微有些不同)
np.r_['r', [1,2,3], [4,5,6]]
複制
matrix([[1, 2, 3, 4, 5, 6]])
複制
3. 第一個參數可以是控制參數,如果它寫成 ‘a,b,c’ 的形式,其中
a:代表軸,按「軸 a」來合并
b:合并後數組次元至少是 b
c:在第 c 維上做次元提升
看不懂吧?沒事,先用程式感受一下:
print( np.r_['0,2,0', [1,2,3], [4,5,6]] )
print( np.r_['0,2,1', [1,2,3], [4,5,6]] )
print( np.r_['1,2,0', [1,2,3], [4,5,6]] )
print( np.r_['1,2,1', [1,2,3], [4,5,6]] )
複制
[[1]
[2]
[3]
[4]
[5]
[6]]
----------------
[[1 2 3]
[4 5 6]]
----------------
[[1 4]
[2 5]
[3 6]]
----------------
[[1 2 3 4 5 6]]
複制
還看不懂吧 (但至少知道完事後的次元是 2,即字元串 ‘a,b,c’ 的 b 起的作用 )?沒事,我再畫個圖。
還沒懂徹底吧?沒事,我再解釋下。
字元串 ‘a,b,c’ 總共有四類,分别是
- '0, 2, 0'
- '0, 2, 1'
- '1, 2, 0'
- '1, 2, 1'
函數裡兩個數組 [1,2,3], [4,5,6] 都是一維
- c = 0 代表在「軸 0」上升一維,是以得到 [[1],[2],[3]] 和 [[4],[5],[6]]
- c = 1 代表在「軸 1」上升一維,是以得到 [[1,2,3]] 和 [[4,5,6]]
接下來如何合并就看 a 的值了
- a = 0, 沿着「軸 0」合并
- a = 1, 沿着「軸 1」合并
分裂
使用「分裂」函數有兩種選擇
- 有通用的 split
- 有專門的 hsplit, vsplit
用下面數組來舉例:
arr = np.arange(25).reshape((5,5))
print( arr )
複制
[[ 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]]
複制
split
和 concatenate() 函數一樣,我們可以在 split() 函數裡通過設定軸,來對數組沿着豎直方向分裂 (軸 0) 和沿着水準方向分裂 (軸 1)。
first, second, third = np.split(arr,[1,3])
print( 'The first split is', first )
print( 'The second split is', second )
print( 'The third split is', third )
複制
The first split is [[0 1 2 3 4]]
The second split is [[ 5 6 7 8 9]
[10 11 12 13 14]]
The third split is [[15 16 17 18 19]
[20 21 22 23 24]]
複制
split() 預設沿着軸 0 分裂,其第二個參數 [1, 3] 相當于是個切片操作,将數組分成三部分:
- 第一部分 - :1 (即第 1 行)
- 第二部分 - 1:3 (即第 2 到 3 行)
- 第二部分 - 3: (即第 4 到 5 行)
hsplit, vsplit
vsplit() 和 split(axis=0) 等價,hsplit() 和 split(axis=1) 等價。一圖勝千言:
為了和上面不重複,我們隻看 hsplit。
first, second, third = np.hsplit(arr,[1,3])
print( 'The first split is', first )
print( 'The second split is', second )
print( 'The third split is', third )
複制
The first split is [[ 0]
[ 5]
[10]
[15]
[20]]
The second split is [[ 1 2]
[ 6 7]
[11 12]
[16 17]
[21 22]]
The third split is [[ 3 4]
[ 8 9]
[13 14]
[18 19]
[23 24]]
複制
4.3 重複和拼接
重複 (repeat) 和拼接 (tile) 這兩個操作本質都是複制
- 重複是在元素層面複制
- 拼接是在數組層面複制
重複
函數 repeat() 複制的是數組的每一個元素,參數有幾種設定方法:
- 一維數組:用标量和清單來複制元素的個數
- 多元數組:用标量和清單來複制元素的個數,用軸來控制複制的行和列
【标量】
arr = np.arange(3)
print( arr )
print( arr.repeat(3) )
複制
[0 1 2]
[0 0 0 1 1 1 2 2 2]
複制
标量參數 3 - 數組 arr 中每個元素複制 3 遍。
【清單】
print( arr.repeat([2,3,4]) )
複制
[0 0 1 1 1 2 2 2 2]
複制
清單參數 [2,3,4] - 數組 arr 中每個元素分别複制 2, 3, 4 遍。
【标量和軸】
arr2d = np.arange(6).reshape((2,3))
print( arr2d )
print( arr2d.repeat(2, axis=0) )
複制
[[0 1 2]
[3 4 5]]
[[0 1 2]
[0 1 2]
[3 4 5]
[3 4 5]]
複制
标量參數 2 和軸 0 - 數組 arr2d 中每個元素沿着軸 0 複制 2 遍。
【清單和軸】
print( arr2d.repeat([2,3,4], axis=1) )
複制
[[0 0 1 1 1 2 2 2 2]
[3 3 4 4 4 5 5 5 5]]
複制
清單參數 [2,3,4] 和軸 1 - 數組 arr2d 中每個元素沿着軸 1 分别複制 2, 3, 4 遍。
拼接
函數 tile() 複制的是數組本身,參數有幾種設定方法:
- 标量:把數組當成一個元素,一列一列複制
- 形狀:把數組當成一個元素,按形狀複制
【标量】
arr2d = np.arange(6).reshape((2,3))
print( arr2d )
print( np.tile(arr2d,2) )
複制
[[0 1 2]
[3 4 5]]
[[0 1 2 0 1 2]
[3 4 5 3 4 5]]
複制
标量參數 2 - 數組 arr 按列複制 2 遍。
【形狀】
print( np.tile(arr2d, (2,3)) )
複制
[[0 1 2 0 1 2 0 1 2]
[3 4 5 3 4 5 3 4 5]
[0 1 2 0 1 2 0 1 2]
[3 4 5 3 4 5 3 4 5]]
複制
标量參數 (2,3) - 數組 arr 按形狀複制 6 (2×3) 遍,并以 (2,3) 的形式展現。
4.4 其他操作
本節讨論數組的其他操作,包括排序 (sort),插入 (insert),删除 (delete) 和複制 (copy)。
【排序】
排序包括直接排序 (direct sort) 和間接排序 (indirect sort)。
直接排序
arr = np.array([5,3,2,6,1,4])
print( 'Before sorting', arr )
arr.sort()
print( 'After sorting', arr )
複制
Before sorting [5 3 2 6 1 4]
After sorting [1 2 3 4 5 6]
複制
sort()函數是按升序 (ascending order) 排列的,該函數裡沒有參數可以控制 order,是以你想要按降序排列的數組,隻需
print( arr[::-1] )
複制
[6 5 4 3 2 1]
複制
現在讓人困惑的地方來了。
知識點
用來排序 numpy 用兩種方式:
- arr.sort()
- np.sort( arr )
第一種 sort 會改變 arr,第二種 sort 在排序時建立了 arr 的一個複制品,不會改變 arr。看下面代碼,用一個形狀是 (3, 4) 的「二維随機整數」數組來舉例,用整數是為了便于讀者好觀察排序前後的變化:
arr = np.random.randint( 40, size=(3,4) )
print( arr )
複制
[[24 32 23 30]
[26 27 28 0]
[ 9 14 24 13]]
複制
第一種 arr.sort(),對第一列排序,發現 arr 的元素改變了。
arr[:, 0].sort()
print( arr )
複制
[[ 9 32 23 30]
[24 27 28 0]
[26 14 24 13]]
複制
第二種 np.sort(arr),對第二列排序,但是 arr 的元素不變。
np.sort(arr[:,1])
複制
array([ 14, 27, 32])
複制
print( arr )
複制
[[ 9 32 23 30]
[24 27 28 0]
[26 14 24 13]]
複制
此外也可以在不同的軸上排序,對于二維數組,在「軸 0」上排序是「跨行」排序,在「軸 1」上排序是「跨列」排序。
arr.sort(axis=1)print( arr )
複制
[[ 9 23 30 32]
[ 0 24 27 28]
[13 14 24 26]]
複制
間接排序
有時候我們不僅僅隻想排序數組,還想在排序過程中提取每個元素在原數組對應的索引(index),這時 argsort() 就派上用場了。以排列下面五個學生的數學分數為例:
score = np.array([100, 60, 99, 80, 91])
idx = score.argsort()
print( idx )
複制
[1 3 4 2 0]
複制
這個 idx = [1 3 4 2 0] 怎麼了解呢?很簡單,排序完之後分數應該是 [60 80 91 99 100],
- 60,即 score[1] 排在第0位, 是以 idx[0] =1
- 80,即 score[3] 排在第1 位, 是以 idx[1] =3
- 91,即 score[4] 排在第2 位, 是以 idx[2] =4
- 99,即 score[2] 排在第3 位, 是以 idx[3] =2
- 100,即 score[0] 排在第4 位, 是以 idx[4] =0
用這個 idx 對 score 做一個「花式索引」得到
print( score[idx] )
複制
[ 60 80 91 99 100]
複制
再看一個二維數組的例子。
arr = np.random.randint( 40, size=(3,4) )
print( arr )
複制
[[24 32 23 30]
[26 27 28 0]
[ 9 14 24 13]]
複制
對其第一行 arr[0] 排序,擷取索引,在應用到所用行上。
arr[:, arr[0].argsort()]
複制
array([[23, 24, 30, 32],
[28, 26, 0, 27],
[24, 9, 13, 14]])
複制
這不就是「花式索引」嗎?來我們分解一下以上代碼,先看看索引。
print( arr[0].argsort() )
複制
[2, 0, 3, 1]
複制
「花式索引」來了,結果和上面一樣的。
arr[:, [2, 0, 3, 1]]
複制
array([[23, 24, 30, 32],
[28, 26, 0, 27],
[24, 9, 13, 14]])
複制
【插入和删除】
和清單一樣,我們可以給 numpy 數組
- 用insert()函數在某個特定位置之前插入元素
- 用delete()函數删除某些特定元素
a = np.arange(6)
print( a )
print( np.insert(a, 1, 100) )
print( np.delete(a, [1,3]) )
複制
[0 1 2 3 4 5]
[ 0 100 1 2 3 4 5]
[0 2 4 5]
複制
【複制】
用copy()函數來複制數組 a 得到 a_copy,很明顯,改變 a_copy 裡面的元素不會改變 a。
a = np.arange(6)
a_copy = a.copy()
print( 'Before changing value, a is', a )
print( 'Before changing value, a_copy is', a_copy )a_copy[-1] = 99
print( 'After changing value, a_copy is', a_copy )
print( 'After changing value, a is', a )
複制
Before changing value, a is [0 1 2 3 4 5]
Before changing value, a_copy is [0 1 2 3 4 5]
After changing value, a_copy is [ 0 1 2 3 4 99]
After changing value, a is [0 1 2 3 4 5]
複制
5 數組的計算
本節介紹四大類數組計算,具體有
- 元素層面 (element-wise) 計算
- 線性代數 (linear algebra) 計算
- 元素整合 (element aggregation) 計算
- 廣播機制 (broadcasting) 計算
5.1 元素層面計算
Numpy 數組元素層面計算包括:
- 二進制運算 (binary operation):加減乘除
- 數學函數:倒數、平方、指數、對數
- 比較運算 (comparison)
先定義兩個數組 arr1 和 arr2。
arr1 = np.array([[1., 2., 3.], [4., 5., 6.]])
arr2 = np.ones((2,3)) * 2
print( arr1 )
print( arr2 )
複制
[[1. 2. 3.]
[4. 5. 6.]]
[[2. 2. 2.]
[2. 2. 2.]]
複制
【加、減、乘、除】
print( arr1 + arr2 + 1 )
print( arr1 - arr2 )
print( arr1 * arr2 )
print( arr1 / arr2 )
複制
[[4. 5. 6.]
[7. 8. 9.]]
[[-1. 0. 1.]
[ 2. 3. 4.]]
[[ 2. 4. 6.]
[ 8. 10. 12.]]
[[0.5 1. 1.5]
[2. 2.5 3. ]]
複制
【倒數、平方、指數、對數】
print( 1 / arr1 )
print( arr1 ** 2 )
print( np.exp(arr1) )
print( np.log(arr1) )
複制
[[1. 0.5 0.33333333]
[0.25 0.2 0.16666667]]
[[ 1. 4. 9.]
[16. 25. 36.]]
[[ 2.71828183 7.3890561 20.08553692]
[ 54.59815003 148.4131591 403.42879349]]
[[0. 0.69314718 1.09861229]
[1.38629436 1.60943791 1.79175947]]
複制
【比較】
arr1 > arr2arr1 > 3
複制
array([[False, False, True],
[ True, True, True]])
array([[False, False, False],
[ True, True, True]])
複制
從上面結果可知
- 「數組和數組間的二進制運算」都是在元素層面上進行的
- 「作用在數組上的數學函數」都是作用在數組的元素層面上的。
- 「數組和數組間的比較」都是在元素層面上進行的
但是在「數組和标量間的比較」時,python 好像先把 3 複制了和 arr1 形狀一樣的數組 [[3,3,3], [3,3,3]],然後再在元素層面上作比較。上述這個複制标量的操作叫做「廣播機制」,是 NumPy 裡最重要的一個特點,在下一節會詳細講到。
5.2 線性代數計算
在機器學習、金融工程和量化投資的程式設計過程中,因為運作速度的要求,通常會向量化 (vectorization) 而涉及大量的線性代數運算,尤其是矩陣之間的乘積運算。
但是,在 NumPy 預設不采用矩陣運算,而是數組 (ndarray) 運算。矩陣隻是二維,而數組可以是任何次元,是以數組運算更通用些。
如果你非要二維數組 arr2d 進項矩陣運算,那麼可以通過調用以下函數來實作:
- A = np.mat(arr2d)
- A = np.asmatrix(arr2d)
下面我們分别對「數組」和「矩陣」從建立、轉置、求逆和相乘四個方面看看它們的同異。
【建立】
建立數組 arr2d 和矩陣 A,注意它們的輸出有 array 和 matrix 的關鍵詞。
arr2d = np.array([[1,2],[3,1]])
arr2d
複制
array([[1, 2],
[3, 1]])
複制
A = np.asmatrix(arr2d)
A
複制
matrix([[1, 2],
[3, 1]])
複制
【轉置】
數組用 arr2d.T 操作或 arr.tranpose() 函數,而矩陣用 A.T 操作。主要原因就是 .T 隻适合二維資料,這時就需要用函數 arr2d.tranpose(1, 0, 2) 來實作了。
print( arr2d.T )
print( arr2d.transpose() )
print( A.T )
複制
[[1 3]
[2 1]]
[[1 3]
[2 1]]
[[1 3]
[2 1]]
複制
【求逆】
數組用 np.linalg.inv() 函數,而矩陣用 A.I 和 A**-1 操作。
print( np.linalg.inv(arr2d) )
print( A.I )
print( A**-1 )
複制
[[-0.2 0.4]
[ 0.6 -0.2]]
[[-0.2 0.4]
[ 0.6 -0.2]]
[[-0.2 0.4]
[ 0.6 -0.2]]
複制
【相乘】
相乘是個很模棱兩可的概念
- 數組相乘是在元素層面進行,
- 矩陣相乘要就是數學定義的矩陣相乘 (比如第一個矩陣的列要和第二個矩陣的行一樣)
看個例子,「二維數組」相乘「一維數組」,「矩陣」相乘「向量」,看看有什麼有趣的結果。
首先定義「一維數組」arr 和 「列向量」b:
arr = np.array([1,2])
b = np.asmatrix(arr).T
print( arr.shape, b.shape )
複制
(2,) (2, 1)
複制
由上面結果看出, arr 的形狀是 (2,),隻含一個元素的元組隻說明 arr 是一維,數組是不分行數組或列數組的。而 b 的形狀是 (2,1),顯然是列向量。
相乘都是用 * 符号,
print( arr2d*arr )
print( A*b )
複制
[[1 4]
[3 2]]
[[5]
[5]]
複制
由上面結果可知,
- 二維數組相乘一維數組得到的還是個二維數組,解釋它需要用到「廣播機制」,這是下節的重點讨論内容。現在大概知道一維數組 [1 2] 第一個元素 1 乘上 [1 3] 得到 [1 3],而第二個元素 2 乘上 [2 1] 得到 [4 2]。
- 而矩陣相乘向量的結果和我們學了很多年的線代結果很吻合。
再看一個例子,「二維數組」相乘「二維數組」,「矩陣」相乘「矩陣」
print( arr2d*arr2d )
print( A*A )
複制
[[1 4]
[9 1]]
[[7 4]
[6 7]]
複制
由上面結果可知,
- 雖然兩個二維數組相乘得到二維數組,但不是根據數學上矩陣相乘的規則得來的,而且由元素層面相乘得到的。兩個 [[1 2], [3,1]] 的元素相乘确實等于 [[1 4], [9,1]]。
- 而矩陣相乘矩陣的結果和我們學了很多年的線代結果很吻合。
問題來了,那麼怎麼才能在數組上實作「矩陣相乘向量」和「矩陣相乘矩陣」呢?用點乘函數 dot()。
print( np.dot(arr2d,arr) )
print( np.dot(arr2d,arr2d) )
複制
[5 5]
[[7 4]
[6 7]]
複制
結果對了,但還有一個小小的差異
- 矩陣相乘列向量的結果是個列向量,寫成 [[5],[5]],形狀是 (2,1)
- 二維數組點乘一維數組結果是個一維數組,寫成 [5, 5],形狀是 (2,)
由此我們來分析下 NumPy 裡的 dot() 函數,計算數組和數組之間的點乘結果。
點乘函數
本節的内容也來自〖張量 101〗,通常我們也把 n 維數組稱為張量,點乘左右兩邊最常見的數組就是
- 向量 (1D) 和向量 (1D)
- 矩陣 (2D) 和向量 (1D)
- 矩陣 (2D) 和矩陣 (2D)
分别看看三個簡單例子。
例一:np.dot(向量, 向量) 實際上做的就是内積,即把兩個向量每個元素相乘,最後再加總。點乘結果 10 是個标量 (0D 數組),形狀 = ()。
x = np.array( [1, 2, 3] )
y = np.array( [3, 2, 1] )
z = np.dot(x,y)
print( z.shape )
print( z )
複制
()
10
複制
例二:np.dot(矩陣, 向量) 實際上做的就是普通的矩陣乘以向量。點乘結果是個向量 (1D 數組),形狀 = (2, )。
x = np.array( [1, 2, 3] )
y = np.array( [[3, 2, 1], [1, 1, 1]] )
z = np.dot(y,x)
print( z.shape )
print( z )
複制
(2,)
[10 6]
複制
例三:np.dot(矩陣, 矩陣) 實際上做的就是普通的矩陣乘以矩陣。點乘結果是個矩陣 (2D 數組),形狀 = (2, 3)。
x = np.array( [[1, 2, 3], [1, 2, 3], [1, 2, 3]] )
y = np.array( [[3, 2, 1], [1, 1, 1]] )
z = np.dot(y,x)
print( z.shape )
print( z )
複制
(2, 3)
[[ 6 12 18]
[ 3 6 9]]
複制
從例二和例三看出,當 x 第二個次元的元素 (x.shape[1]) 和 y 第一個次元的元素 (y.shape[0]) 個數相等時,np.dot(X, Y) 才有意義,點乘得到的結果形狀 = (X.shape[0], y.shape[1])。
上面例子都是低維數組 (次元 ≤ 2) 的點乘運算,接下來我們看兩個稍微複雜的例子。
例四:當 x 是 3D 數組,y 是 1D 數組,np.dot(x, y) 是将 x 和 y 最後一維的元素相乘并加總。此例 x 的形狀是 (2, 3, 4),y 的形狀是 (4, ),是以點乘結果的形狀是 (2, 3)。
x = np.ones( shape=(2, 3, 4) )
y = np.array( [1, 2, 3, 4] )
z = np.dot(x,y)
print( z.shape )
print( z )
複制
(2, 3)
[[10. 10. 10]
[10. 10. 10]]
複制
例五:當 x 是 3D 數組,y 是 2D 數組,np.dot(x, y) 是将 x 的最後一維和 y 的倒數第二維的元素相乘并加總。此例 x 的形狀是 (2, 3, 4),y 的形狀是 (4, 2),是以點乘結果的形狀是 (2, 3, 2)。
x = np.random.normal( 0, 1, size=(2, 3, 4) )
y = np.random.normal( 0, 1, size=(4, 2) )
z = np.dot(x,y)
print( z.shape )
print( z )
複制
(2, 3, 2)
[[[ 2.11753451 -0.27546168]
[-1.23348676 0.42524653]
[-4.349676 -0.3030879 ]]
[[ 0.15537744, 0.44865273]
[-3.09328194, -0.43473885]
[ 0.27844225, -0.48024693]]]
複制
例五的規則也适用于 nD 數組和 mD 數組 (當 m ≥ 2 時) 的點乘。
5.3 元素整合計算
在數組中,元素可以以不同方式整合 (aggregation)。拿求和 (sum) 函數來說,我們可以對數組
- 所有的元素求和
- 在某個軸 (axis) 上的元素求和
先定義數組
arr = np.arange(1,7).reshape((2,3))
arr
複制
array([[1, 2, 3],
[4, 5, 6]])
複制
不難看出它是一個矩陣,分别對全部元素、跨行 (across rows)、跨列 (across columns) 求和:
print( 'The total sum is', arr.sum() )
print( 'The sum across rows is', arr.sum(axis=0) )
print( 'The sum across columns is', arr.sum(axis=1) )
複制
The total sum is 21
The sum across rows is [5 7 9]
The sum across columns is [ 6 15]
複制
分析上述結果:
- 1, 2, 3, 4, 5, 6 的總和是 21
- 跨行求和 = [1 2 3] + [4 5 6] = [5 7 9]
- 跨列求和 = [1+2+3 4+5+6] = [6 15]
行和列這些概念對矩陣 (二維矩陣) 才适用,高維矩陣還是要用軸 (axis) 來區分每個次元。讓我們抛棄「行列」這些特殊概念,擁抱「軸」這個通用概念來重看數組 (一到四維) 把。
規律:n 維數組就有 n 層方括号。最外層方括号代表「軸 0」即 axis=0,依次往裡方括号對應的 axis 的計數加 1。
嚴格來說,numpy 列印出來的數組可以想象帶有多層方括号的一行數字。比如二維矩陣可想象成
[[1, 2, 3],[4, 5, 6]]
三維矩陣可想象成
[[[1,2,3], [4,5,6]], [[7,8,9], [10,11,12]]]
由于螢幕的寬度不夠,我們才把它們寫成一列列的,如下
[ [ [1, 2, 3]
[4, 5, 6] ]
[ [7, 8, 9]
[10, 11, 12] ] ]
但在你腦海裡,應該把它們想成一整行。這樣會便于你了解如何按不同軸做整合運算。
有了軸的概念,我們再來看看 sum() 求和函數。
【一維數組】
分析結果:
- 1, 2, 3 的總和是 6
- 在軸 0(隻有一個軸) 上的元素求和是 6
用代碼驗證一下:
arr = np.array([1,2,3])
print( 'The total sum is', arr.sum() )
print( 'The sum on axis0 is', arr.sum(axis=0) )
複制
The total sum is 6
The sum on axis0 is 6
複制
求和一維數組沒什麼難度,而且也看不出如果「按軸求和」的規律。下面看看二維數組。
【二維數組】
分析結果:
- 1 到 6 的總和是 6
- 軸 0 上的元素 (被一個紅方括号[]包住的) 是[1, 2, 3]和[4, 5, 6],求和得到[[5, 6, 7]]
- 軸 1 上的元素 (被兩個藍方括号[] 包住的) 分别是 1, 2, 3 和 4, 5, 6,求和得到 [[1+2+3, 4+5+6]]= [[6, 15]]
用代碼驗證一下:
arr = np.arange(1,7).reshape((2,3))
print( arr )
複制
[[1 2 3]
[4 5 6]]
複制
print( 'The total sum is', arr.sum() )
print( 'The sum on axis0 is', arr.sum(axis=0) )
print( 'The sum on axis1 is', arr.sum(axis=1) )
複制
The total sum is 21
The sum on axis0 is [5 7 9]
The sum on axis1 is [ 6 15]
複制
結果是對的,但是好像括号比上圖推導出來的少一個。原因np.sum()裡面有個參數是 keepdims,意思是「保留次元」,預設值時 False,是以會去除多餘的括号,比如 [[5, 7, 9]] 會變成 [5, 7, 9]。
如果把 keepdims 設定為 True,那麼列印出來的結果和上圖推導的一模一樣。
print( arr.sum(axis=0, keepdims=True) )
print( arr.sum(axis=1, keepdims=True) )
複制
[[5 7 9]]
[[ 6]
[15]]
複制
【三維數組】
分析結果:
- 1 到 12 的總和是 78
- 軸 0 上的元素是一個紅方括号[] 包住的兩個 [[ ]],對其求和得到一個 [ [[ ]] ]
- 軸 1 上的元素是兩個藍方括号[] 包住的兩個[ ],對其求和得到兩個 [[ ]],即 [ [[ ]], [[ ]] ]
- 軸 2 上的元素是四個綠方括号[] 包住的三個标量,對其求和得到四個[],即 [ [[ ], [ ]], [[ ], [ ]] ]
用代碼驗證一下:
arr = np.arange(1,13).reshape((2,2,3))
print(arr)
複制
[[[ 1 2 3]
[ 4 5 6]]
[[ 7 8 9]
[10 11 12]]]
複制
print( 'The total sum is', arr.sum() )
print( 'The sum on axis0 is', arr.sum(axis=0) )
print( 'The sum on axis1 is', arr.sum(axis=1) )
print( 'The sum on axis2 is', arr.sum(axis=2) )
複制
The total sum is 78
The sum on axis0 is [[ 8 10 12] [14 16 18]]
The sum on axis1 is [[ 5 7 9] [17 19 21]]
The sum on axis2 is [[ 6 15] [24 33]]
複制
列印出來的結果比上圖推導結果少一個括号,也是因為 keepdims 預設為 False。
【四維數組】
不解釋了,彩色括号畫的人要抓狂了。通用規律:當在某根軸上求和,明晰該軸的元素,再求和。具體說來:
- 在軸 0上求和,它包含是兩個[],對其求和
- 在軸 1 上求和,它包含是兩個 [],對其求和
- 在軸 2 上求和,它包含是兩個 [],對其求和
- 在軸 3 上求和,它包含是三個标量,對其求和
用代碼驗證一下:
arr = np.arange(1,25).reshape((2,2,2,3))
print(arr)
複制
[[[[ 1 2 3]
[ 4 5 6]]
[[ 7 8 9]
[10 11 12]]]
[[[13 14 15]
[16 17 18]]
[[19 20 21]
[22 23 24]]]
複制
print( 'The total sum is', arr.sum() )
print( 'The sum on axis0 is', arr.sum(axis=0) )
print( 'The sum on axis1 is', arr.sum(axis=1) )
print( 'The sum on axis2 is', arr.sum(axis=2) )
print( 'The sum on axis3 is', arr.sum(axis=3) )
複制
The total sum is 300
The sum on axis0 is [[[14 16 18] [20 22 24]]
[[26 28 30] [32 34 36]]]
The sum on axis1 is [[[ 8 10 12] [14 16 18]]
[[32 34 36] [38 40 42]]]
The sum on axis2 is [[[ 5 7 9] [17 19 21]]
[[29 31 33] [41 43 45]]]
The sum on axis3 is [[[ 6 15] [24 33]]
[[42 51] [60 69]]]
複制
列印出來的結果比上圖推導結果少一個括号,也是因為 keepdims 預設為 False。
小節
除了 sum 函數,整合函數還包括 min, max, mean, std 和 cumsum,分别是求最小值、最大值、均值、标準差和累加,這些函數對數組裡的元素整合方式和 sum 函數相同,就不多講了。總結來說我們可以對數組
- 所有的元素整合
- 在某個軸 (axis) 上的元素整合
整合函數= {sum, min, max, mean, std, cumsum}
5.4 廣播機制計算
當對兩個形狀不同的數組按元素操作時,可能會觸發「廣播機制」。具體做法,先适當複制元素使得這兩個數組形狀相同後再按元素操作,兩個步驟:
- 廣播軸 (broadcast axis):比對兩個數組的次元,将形狀小的數組的次元 (軸) 補齊
- 複制元素:順着補齊的軸,将形狀小的數組裡的元素複制,使得最終形狀和另一個數組吻合
在給出「廣播機制」需要的嚴謹規則之前,我們先來看看幾個簡單例子。
例一:标量和一維數組
arr = np.arange(5)
print( arr )
print( arr + 2 )
複制
[0 1 2 3 4]
[2 3 4 5 6]
複制
元素 2 被廣播到數組 arr 的所有元素上。
例二:一維數組和二維數組
arr = np.arange(12).reshape((4,3))
print( arr )
print( arr.mean(axis=0) )
print( arr - arr.mean(axis=0) )
複制
[[ 0 1 2]
[ 3 4 5]
[ 6 7 8]
[ 9 10 11]]
[4.5 5.5 6.5]
[[-4.5 -4.5 -4.5]
[-1.5 -1.5 -1.5]
[ 1.5 1.5 1.5]
[ 4.5 4.5 4.5]]
複制
沿軸 0 的均值的一維數組被廣播到數組 arr 的所有的行上。
現在我們來看看「廣播機制」的規則:
廣播機制的規則
知識點
當我們對兩個數組操作時,如果它們的形狀
- 不相容 (incompatible),廣播機制不能進
- 相容 (compatible),廣播機制可以進行
是以,進行廣播機制分兩步
- 是以,進行廣播機制分兩步。,即從兩個形狀元組最後一個元素,來看。
- 它們是否相等
- 是否有一個等于 1
- 一旦它們形狀相容,确定兩個數組的最終形狀。
例三:次元一樣,形狀不一樣
用個例子來應用以上廣播機制規則
a = np.array([[1,2,3]])
b = np.array([[4],[5],[6]])
print( 'The shape of a is', a.shape )
print( 'The shape of b is', b.shape )
複制
The shape of a is (1, 3)
The shape of b is (3, 1)
複制
回顧進行廣播機制的兩步
- 檢查數組 a 和 b 形狀是否相容,從兩個形狀元組 (1, 3) 和 (3, 1)最後一個元素開始檢查,發現它們都滿足『有一個等于 1』的條件。
- 是以它們形狀相容,兩個數組的最終形狀為 (max(1,3), max(3,1)) = (3, 3)
到此,a 和 b 被擴充成 (3, 3) 的數組,讓我們看看 a + b 等于多少
c = a + bprint( 'The shape of c is', c.shape )
print( 'a is', a )
print( 'b is', b )
print( 'c = a + b =', c )
複制
The shape of c is (3, 3)
a is [[1 2 3]]
b is [[4]
[5]
[6]]
c = a + b = [[5 6 7]
[6 7 8]
[7 8 9]]
複制
例四:次元不一樣
a = np.arange(5)
b = np.array(2)
print( 'The dimension of a is', a.ndim, 'and the shape of a is', a.shape )
print( 'The dimension of b is', b.ndim, 'and the shape of b is', b.shape )
複制
The dimension of a is 1 and the shape of a is (5,)
The dimension of b is 0 and the shape of b is ()
複制
數組 a 和 b 形狀分别為 (5,) 和 (),首先我們把缺失的次元用 1 補齊得到 (5,) 和 (1,),再根據廣播機制那套流程得到這兩個形狀是相容的,而且最終形狀為 (5,)。
用代碼來看看 a + b 等于多少
c = a + bprint( 'The dimension of c is', c.ndim, 'and the shape of c is', c.shape, '\n' )
print( 'a is', a )
print( 'b is', b )
print( 'c = a + b =', c )
複制
The dimension of c is 1 and the shape of c is (5,)
a is [0 1 2 3 4]
b is 2
c = a + b = [2 3 4 5 6]
複制
現在對廣播機制有概念了吧,來趁熱打鐵搞清楚下面這五個例子,你就完全弄懂它了。
a = np.array( [[[1,2,3], [4,5,6]]] )
b1 = np.array( [[1,1,1], [2,2,2], [3,3,3]] )
b2 = np.arange(3).reshape((1,3))
b3 = np.arange(6).reshape((2,3))
b4 = np.arange(12).reshape((2,2,3))
b5 = np.arange(6).reshape((2,1,3))
print( 'The dimension of a is', a.ndim, 'and the shape of a is', a.shape )
print( 'The dimension of b1 is', b.ndim, 'and the shape of b1 is', b1.shape, '\n')
print( 'The dimension of a is', a.ndim, 'and the shape of a is', a.shape )
print( 'The dimension of b2 is', b.ndim, 'and the shape of b2 is', b2.shape, '\n' )
print( 'The dimension of a is', a.ndim, 'and the shape of a is', a.shape )
print( 'The dimension of b3 is', b.ndim, 'and the shape of b3 is', b3.shape, '\n' )
print( 'The dimension of a is', a.ndim, 'and the shape of a is', a.shape )
print( 'The dimension of b4 is', b.ndim, 'and the shape of b4 is', b4.shape, '\n' )
print( 'The dimension of a is', a.ndim, 'and the shape of a is', a.shape )
print( 'The dimension of b5 is', b.ndim, 'and the shape of b5 is', b5.shape )
複制
The dimension of a is 3 and the shape of a is (1, 2, 3)
The dimension of b1 is 0 and the shape of b1 is (3, 3)
The dimension of a is 3 and the shape of a is (1, 2, 3)
The dimension of b2 is 0 and the shape of b2 is (1, 3)
The dimension of a is 3 and the shape of a is (1, 2, 3)
The dimension of b3 is 0 and the shape of b3 is (2, 3)
The dimension of a is 3 and the shape of a is (1, 2, 3)
The dimension of b4 is 0 and the shape of b4 is (2, 2, 3)
The dimension of a is 3 and the shape of a is (1, 2, 3)
The dimension of b5 is 0 and the shape of b5 is (2, 1, 3)
複制
對于數組 a 和 b1,它們形狀是 (1, 2, 3) 和 (3, 3)。元組最後一個都是 3,相容;倒數第二個是 3 和 2,即不相等,也沒有一個是 1,不相容!a 和 b1 不能進行廣播機制。不行就看看下面代碼:
c1 = a + b1
print( c1 )
print( c1.shape )
複制
ValueError: operands could not be broadcast
together with shapes (1,2,3) (3,3)
複制
a 和其他 b2, b3, b4, b5 都可以進行廣播機制,自己分析吧。
c2 = a + b2
print( c2 )
print( c2.shape )
複制
[[[1 3 5]
[4 6 8]]]
(1, 2, 3)
複制
c3 = a + b3
print( c3 )
print( c3.shape )
複制
[[[ 1 3 5]
[ 7 9 11]]]
(1, 2, 3)
複制
c4 = a + b4
print( c4 )
print( c4.shape )
複制
[[[ 1 3 5]
[ 7 9 11]]
[[ 7 9 11]
[13 15 17]]]
(2, 2, 3)
複制
c5 = a + b5
print( c5 )
print( c5.shape )
複制
[[[ 1 3 5]
[ 4 6 8]]
[[ 4 6 8]
[ 7 9 11]]]
(2, 2, 3)
複制
6 總結
NumPy 篇終于完結!即上貼讨論過的數組建立、數組存載和數組擷取,本貼讨論了數組變形、數組計算。
數組變形有以下重要操作:
- 改變次元的重塑和打平
- 改變分合的合并和分裂
- 複制本質的重複和拼接
- 其他排序插入删除複制
數組計算有以下重要操作:
- 元素層面:四則運算、函數,比較
- 線性代數:務必弄懂點乘函數 dot()
- 元素整合:務必弄懂軸這個概念!
- 廣播機制:太重要了,神經網絡無處不在!
The End