天天看點

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

接着上篇繼續後面兩個章節,數組變形和數組計算。

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

4 數組的變形

本節介紹四大類數組層面上的操作,具體有

  1. 重塑 (reshape) 和打平 (ravel, flatten)
  2. 合并 (concatenate, stack) 和分裂 (split)
  3. 重複 (repeat) 和拼接 (tile)
  4. 其他操作 (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入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

在 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() 的不同之處是

  1. ravel() 按「行主序」打平時沒有複制原數組,按「列主序」在打平時複制了原數組
  2. 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) 這兩個操作僅僅隻改變數組的分合

  • 合并是多合一
  • 分裂是一分多

合并

使用「合并」函數有三種選擇

  1. 有通用的 concatenate
  2. 有專門的 vstack, hstack, dstack
  3. 有極簡的 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 通道

一圖勝千言:

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

用代碼驗證一下:

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 起的作用 )?沒事,我再畫個圖。

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

還沒懂徹底吧?沒事,我再解釋下。

字元串 ‘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」合并

分裂

使用「分裂」函數有兩種選擇

  1. 有通用的 split
  2. 有專門的 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) 等價。一圖勝千言:

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

為了和上面不重複,我們隻看 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 用兩種方式:

  1. arr.sort()
  2. 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 數組的計算

本節介紹四大類數組計算,具體有

  1. 元素層面 (element-wise) 計算
  2. 線性代數 (linear algebra) 計算
  3. 元素整合 (element aggregation) 計算
  4. 廣播機制 (broadcasting) 計算

5.1 元素層面計算

Numpy 數組元素層面計算包括:

  1. 二進制運算 (binary operation):加減乘除
  2. 數學函數:倒數、平方、指數、對數
  3. 比較運算 (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) 來區分每個次元。讓我們抛棄「行列」這些特殊概念,擁抱「軸」這個通用概念來重看數組 (一到四維) 把。

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

規律: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() 求和函數。

【一維數組】

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

分析結果:

  • 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           

複制

求和一維數組沒什麼難度,而且也看不出如果「按軸求和」的規律。下面看看二維數組。

【二維數組】

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算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]]           

複制

【三維數組】

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

分析結果:

  • 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。

【四維數組】

【幹貨】NumPy入門深度好文 (下篇)4 數組的變形5 數組的計算6 總結

不解釋了,彩色括号畫的人要抓狂了。通用規律:當在某根軸上求和,明晰該軸的元素,再求和。具體說來:

  • 在軸 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 廣播機制計算

當對兩個形狀不同的數組按元素操作時,可能會觸發「廣播機制」。具體做法,先适當複制元素使得這兩個數組形狀相同後再按元素操作,兩個步驟:

  1. 廣播軸 (broadcast axis):比對兩個數組的次元,将形狀小的數組的次元 (軸) 補齊
  2. 複制元素:順着補齊的軸,将形狀小的數組裡的元素複制,使得最終形狀和另一個數組吻合

在給出「廣播機制」需要的嚴謹規則之前,我們先來看看幾個簡單例子。

例一:标量和一維數組

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. 是以,進行廣播機制分兩步。,即從兩個形狀元組最後一個元素,來看。
    1. 它們是否相等
    2. 是否有一個等于 1
  2. 一旦它們形狀相容,确定兩個數組的最終形狀。

例三:次元一樣,形狀不一樣

用個例子來應用以上廣播機制規則

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)           

複制

回顧進行廣播機制的兩步

  1. 檢查數組 a 和 b 形狀是否相容,從兩個形狀元組 (1, 3) 和 (3, 1)最後一個元素開始檢查,發現它們都滿足『有一個等于 1』的條件。
  2. 是以它們形狀相容,兩個數組的最終形狀為 (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 篇終于完結!即上貼讨論過的數組建立、數組存載和數組擷取,本貼讨論了數組變形、數組計算。

數組變形有以下重要操作:

  • 改變次元的重塑和打平
  • 改變分合的合并和分裂
  • 複制本質的重複和拼接
  • 其他排序插入删除複制

數組計算有以下重要操作:

  1. 元素層面:四則運算、函數,比較
  2. 線性代數:務必弄懂點乘函數 dot()
  3. 元素整合:務必弄懂軸這個概念!
  4. 廣播機制:太重要了,神經網絡無處不在!

The End