2021年7月10日 星期六

Python 學習筆記 : Numpy 測試 (二) : 陣列的屬性與方法

上一次學 Numpy 是 2019 年底, 停了這麼久都快忘光了, 但 Numpy 實在太重要, 不趕快搞定的話會拖慢後續的 DSP, ML, 與 NLP 的學習. 最近 WordPress 架站完工後, 決心重新開始.    

本系列前一篇測試參考 : 



Numpy 使用 ndarray 物件取代 Python 本身的陣列與串列物件來加速運算, ndarray 物件是一個可以放置相同類型資料的多維陣列容器, 在機器學習中將一維陣列稱為向量 (vector), 二維陣列稱為矩陣 (matix), 而三維以上則稱為張量 (tensor), 但也可以通稱為張量, 即向量是一維張量, 矩陣是二維張量等. 


一. 陣列物件的屬性 : 

Numpy 的 ndarray 物件提供許多陣列運算的屬性與方法 :

>>> import numpy as np   
>>> a=np.array(range(6))                                # 建立 ndarray 物件
>>> print(a)   
array([ 0,  1,  2,  3,  4,  5])    
>>> dir(a)                     # 檢視物件屬性與方法
['T', '__abs__', '__add__', '__and__', '__array__', '__array_finalize__', '__array_function__', '__array_interface__', '__array_prepare__', '__array_priority__', '__array_struct__', '__array_ufunc__', '__array_wrap__', '__bool__', '__class__', '__complex__', '__contains__', '__copy__', '__deepcopy__', '__delattr__', '__delitem__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__iand__', '__ifloordiv__', '__ilshift__', '__imatmul__', '__imod__', '__imul__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__ior__', '__ipow__', '__irshift__', '__isub__', '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__', '__lshift__', '__lt__', '__matmul__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmatmul__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__setitem__', '__setstate__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__xor__', 'all', 'any', 'argmax', 'argmin', 'argpartition', 'argsort', 'astype', 'base', 'byteswap', 'choose', 'clip', 'compress', 'conj', 'conjugate', 'copy', 'ctypes', 'cumprod', 'cumsum', 'data', 'diagonal', 'dot', 'dtype', 'dump', 'dumps', 'fill', 'flags', 'flat', 'flatten', 'getfield', 'imag', 'item', 'itemset', 'itemsize', 'max', 'mean', 'min', 'nbytes', 'ndim', 'newbyteorder', 'nonzero', 'partition', 'prod', 'ptp', 'put', 'ravel', 'real', 'repeat', 'reshape', 'resize', 'round', 'searchsorted', 'setfield', 'setflags', 'shape', 'size', 'sort', 'squeeze', 'std', 'strides', 'sum', 'swapaxes', 'take', 'tobytes', 'tofile', 'tolist', 'tostring', 'trace', 'transpose', 'var', 'view']

Numpy 的 ndarray 物件的常用屬性如下表 :


 ndarray 物件屬性 說明
 dtype 陣列元素之資料型態
 ndim 陣列的軸數 (axis), 表示 n-D 陣列
 shape 以 tuple 表示陣列的形狀 (各軸的元素個數) : (2, 3)=2x3 維
 size 陣列的大小 (全部元素個數)
 itemsize 元素所占的 bytes 數
 nbytes 整個陣列所占的 bytes 數=size*itemsize
 stride 步長 (以 tuple 顯示沿各軸移動一相鄰元素時所需之 bytes 數)
 T 轉置陣列 (行列交換)
 flat 傳回平坦化 (轉成一維陣列) 後之可迭代物件, 可用索引取值
 data 陣列之起始記憶體位址
 real  陣列的實部 (有複數元素時)
 imag 陣列的虛部 (有複數元素時)


屬性 ndim 用來表示陣列的外部結構, 是一個稱為陣列軸數 (axis) 或維度 (dimension) 的整數, 而屬性 shape 用來表示陣列的內部結構的元組 (tuple), 其元素依序表示陣列各軸的元素數目, shape 的元素個數即為軸數 ndim, 亦即 a.ndim=len(a.shape). 所以 shape=(3, 4) 的陣列有兩個軸, 軸 0 有 3 個元素, 軸 1 有 4 個元素. 

習慣上常稱 ndim=2 的陣列是二維陣列, 但在 "Numpy 高速運算徹底解說" 這本書裡認為陣列的 "維" 與 "軸" 這兩個術語很容易讓人搞混, 建議一般說的一維, 二維, 三維陣列應該稱為一軸, 二軸, 三軸或 1D, 2D, 3D 陣列, 而維度應該用來指一個軸所包含的元素數目, 或用來稱呼內部結構 shape, 例如 shape=(2, 3) 應稱為 2x3 維的 2D 陣列. 


1. 實數陣列 :     

實數陣列之元素值僅有 real 屬性值 (整數或浮點數), 其虛部 imag 屬性值為零陣列, 例如 : 

>>> import numpy as np   
>>> a=np.array([[1, 2, 3], [4, 5, 6]])    # 建立 2x3 陣列, 也可用 np.arange(1, 7).reshape(2, 3)
>>> a.dtype                                           # 資料型態 : 32 位元整數 
dtype('int32')    
>>> a.ndim                                            # 軸數=2=len(a.shape)
2
>>> a.shape                                           # 形狀=2x3 
(2, 3)
>>> a.size                                              # 元素個數=6
6
>>> a.itemsize                                      # 每個元素占 4 bytes
4
>>> a.nbytes                                         # 整個陣列占用記憶體 6*4=24 bytes
24
>>> a.strides                                         # 往各軸相鄰元素移動所跳過之 bytes 數 
(12, 4)                                                    # 沿軸 0=3*4=12 bytes, 沿軸 1=1*4=4 bytes
>>> a.T                                                 # 轉置陣列 (行列交換)
array([[1, 4],
       [2, 5],
       [3, 6]])
>>> a.flat                                             # 傳回陣列平坦化後的一維可迭代物件起始位址
<numpy.flatiter object at 0x000001FD316F8EA0>
>>> type(a.flat)                                    # 平坦化後為 flatiter 物件
<class 'numpy.flatiter'>
>>> len(a.flat)                                      # flatiter 物件元素長度 (數目)=6
6
>>> a.flat[0]                                         # flatiter 物件的第一個元素
1
>>> a.flat[5]                                         # flatiter 物件的最後一個元素
6
>>> for i in range(6):                           # 走訪 flatiter 物件之元素
    print(a.flat[i])   
    
1
2
3
4
5
6
>>> a.real             # 實部
array([[1, 2, 3],
       [4, 5, 6]])
>>> a.imag           # 虛部 (零陣列)
array([[0, 0, 0],   
       [0, 0, 0]])   

此例建立了一個形狀為 2x3 維之 2D 陣列, 其 shape 屬性值為 (2, 3), 表示此陣列有兩個軸, 軸 0 有 2 個元素, 軸 1 有 3 個元素. 從 shape 元組的元素個數即可知其軸數為 2 (=ndim), 亦即 ndim 的值必為 shape 的長度 : a.ndim=len(a.shape). 




屬性 stride 與 shape 一樣也是傳回一個 tuple,  分別表示沿軸 0, 1, 2, ... 前進時相鄰元素的 bytes 數, 故沿軸 0 會跳過 1, 2, 3 這三個元素, int32 每個元素為 4 bytes (itemsize=4), 4*3=12, 故 strides 第一個元素值為 12, 而沿軸 1 相鄰元素為 4 bytes, 故第二元素值為 4. 


2. 複數陣列 :   

屬性 real 對於實數陣列來說就是陣列本身, 因虛部為 0 故其 imag 屬性值為零陣列, 對於複數陣列而言, 其值的實部與虛部分別放在 real 與 imag 屬性中儲存, 故所占用之記憶體會倍增, 例如 : 

>>> a=np.array([[1+11j, 2+22j, 3+33j], [4+44j, 5+55j, 6+66j]])     # 建立 2x3 複數陣列
>>> a.shape                            # 形狀為 2x3 維
(2, 3)
>>> a.dtype  
dtype('complex128')                # 資料型態 : 128 位元複數
>>> a.real                               # 實部
array([[1., 2., 3.],
       [4., 5., 6.]])
>>> a.imag                              # 虛部
array([[11., 22., 33.],
       [44., 55., 66.]])
>>> a.size                                # 元素個數=6
6
>>> a.itemsize                         # 每個元素占 16 bytes (複數 complex128 型態)
16   
>>> a.nbytes                           # 整個陣列占用記憶體 16*6=96 bytes
96   
>>> a.strides                           # 往各軸相鄰元素移動所跳過之 bytes 數
(48, 16)                                     # 沿軸 0=3*16=48 bytes, 沿軸 1=1*16=16 bytes
>>> a.T                                    # 轉置陣列 (行列交換)
array([[1.+11.j, 4.+44.j],
       [2.+22.j, 5.+55.j],
       [3.+33.j, 6.+66.j]])
>>> type(a.flat)                       # 平坦化後為 flatiter 物件
<class 'numpy.flatiter'>
>>> for i in range(6):              # 走訪 flatiter 物件之元素
    print(a.flat[i])    
    
(1+11j)
(2+22j)
(3+33j)
(4+44j)
(5+55j)
(6+66j)

可見複數陣列所占的記憶體空間較大, 其實部與虛部會拆成兩個陣列分別放在 real 與 imag 屬性中儲存. 注意, Python 的虛數使用 j 而非數學所用的 i, 複數用 R + Ij 表示 (j 在後面不可在前面), 虛部大小若為 1 必須用 1j 表示, 不可省略 1 只寫 j, 例如應該用 2+1j, 不可用 2+j.  


二. 陣列物件的方法 :

Numpy 的 ndarray 物件的常用方法如下表 : 


 ndarray 物件方法 說明
 reshape(shape) 傳回指定形狀為 shape (元組) 的陣列參考 (不)
 copy() 傳回陣列的副本
 astype(dtype) 以指定之 dtype 複製陣列並傳回新陣列
 sort([axis]) 對指定的軸 axis 進行陣列元素排序


注意, 這些方法都可以用同名的 Numpy 函數 (全域方法) 來呼叫, 差別只是使用 np 呼叫全域方法時要將欲操作的陣列當作第一參數傳入而已, 例如取得陣列 a 形狀j為 (2, 3) 的陣列參考可用 b=a.reshape(shape), 也可以用 b=np.reshape(a, (2, 3)). 


1. 重塑 (reshape), 複製 (copy) 與變形 (resize) :    

呼叫 ndarray 物件的 reshape(newshape) 重塑方法會傳回一個指定形狀的陣列參考, 它會指向原陣列 (即共用記憶體), 因此更改新陣列的元素也會改變原陣列元素. 此新陣列可看成是原陣列在記憶體中的一個視角 (view), 亦即用不同結構 (即所記錄之 shape) 來看記憶體中的相同內容. Numpy 中有許多操作傳回的是 view 而不是新的陣列, 除了 reshape() 外還有切片 (slicing) 也是. 

reshape() 的參數結構如下 :

reshape(newshape [, order='C'])    或  np.reshape(arr, newshape [, order='C'])

其中 newshape 為新陣列的形狀, 可以是整數 (用於 1D 陣列) 或 tuple (用於 2D 以上陣列). 選項參數 order 用來指定陣列元素在記憶體中的排列方式是列主序 (row-major order) 還是行主序 (column-major order), 預設 'C' 表示是遵循 C 語言的逐列方式, 也可以指定為 'F', 表示遵循 Fortran 的逐行方式, 以 2D 陣列為例說明如下圖 : 




重塑 reshape 不會產生陣列的副本, 若要產生陣列的副本則需呼叫 copy() 方法, 它會在記憶體中複製與原陣列完全一樣的內容, 並傳回新陣列 (副本) 的參考, 此新陣列與原陣列就不是共用記憶體了, 其參數如下 :

copy([order='C'])   或 np.copy(arr, [order='C']) 

檢驗兩個陣列 a, b 是否共用記憶體可呼叫 Numpy 的全域方法 np.may_share_memory(a, b), 呼叫 reshape() 所得到的重塑陣列與原陣列共用記憶體故會傳回 True; 而呼叫 copy() 所得到的副本陣列與原陣列沒有共用記憶體因此會傳回 False, 例如 :  

>>> import numpy as np    
>>> a=np.arange(1, 7)               # 建立一個 1x6 等差數列陣列
>>> print(a)   
[1 2 3 4 5 6]
>>> b=a.reshape(2, 3)                # 傳回 a 陣列指定形狀的陣列參考 b, 也可用 reshape((2, 3))
>>> print(b)   
[[1 2 3]
 [4 5 6]]
>>> b[0, 0]=100                           # 改變新陣列元素
>>> print(b)
array([[100,   2,   3],
       [  4,   5,   6]])
>>> print(a)    
array([100,   2,   3,   4,   5,   6])    # 原陣列的元素也被改變了   
>>> np.may_share_memory(a, b)         # 重塑陣列與原陣列共用記憶體
True
>>> print(a)
array([100,   2,   3,   4,   5,   6])
>>> c=a.copy()                                         # 傳回陣列 a 的副本參考
>>> print(c)   
[100   2   3   4   5   6]
>>> np.may_share_memory(c, a)          # 副本陣列與原陣列沒有共用記憶體 
False    
>>> c[0]=222                                            # 更改副本的元素   
>>> print(c)                                              
array([222,   2,   3,   4,   5,   6])                       
>>> print(a)       
array([100,   2,   3,   4,   5,   6])                  # 原陣列不受影響 (沒有共用記憶體)

注意, 呼叫 reshape() 重塑陣列時所指定之新形狀的元素數目必須與原陣列相等, 否則會出現 ValueError 錯誤, 例如 : 

>>> a=np.arange(1, 7)         # 建立 6 個元素之等差數列陣列
>>> print(a)    
[1 2 3 4 5 6]
>>> b=a.reshape(2, 3)          # 重塑為 2x3 陣列 (元素個數相同)
>>> print(b)   
[[1 2 3]
 [4 5 6]]
>>> b=a.reshape(2, 4)          # 重塑為 2x4 陣列 (元素個數不相同)
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
ValueError: cannot reshape array of size 6 into shape (2,4)

另外還有一個與重塑方法 reshape() 很像的 resize() 變形方法, 其參數結構如下 :

resize(newshape)    或   np.resize(arr, newshape)  

其中 newshape 為新陣列的形狀, 可以是整數 (1D 陣列) 或 tuple (2D 以上陣列). 此方法之功能是直接去更改陣列本身的尺寸, 因此原陣列的 shape 會被改變, 它與 reshape() 不同之處有二 :
  • reshape() 有 order 參數, resize() 則無 order 參數.
  • reshape() 新形狀的元素個數與原陣列不一致時會出現錯誤, 呼叫全域方法 np.resize() 則不會.
注意, 呼叫 ndarray 物件的 resize() 方法會傳回 None, 但呼叫全域方法 np.resize() 卻會複製原陣列並傳回該新陣列的參考 (不共用記憶體), 例如 :

>>> a=np.arange(1, 7)        
>>> print(a)     
[1 2 3 4 5 6]
>>> b=a.reshape((2,3))         # b 是 a 的 shape=(2, 3) 重塑陣列
>>> print(b)     
[[1 2 3]
 [4 5 6]]
>>> print(a)                          # 原陣列不變仍是 shape=(6, )
[1 2 3 4 5 6]
>>> c=a.resize((2, 3))           # 更改原陣列 a 的尺寸 (元素個數須相同才不會有錯誤)
>>> print(c)                          # 呼叫物件的 resize() 方法傳回 None    
None
>>> print(a)                          # 陣列本身的 shape 被改成 (2, 3) 
[[1 2 3]
 [4 5 6]]
>>> a.shape    
(2, 3)
>>> d=np.resize(a, (2, 3))    # 呼叫全域方法傳回新陣列參考 
>>> print(d)       
[[1 2 3]
 [4 5 6]]
>>> np.may_share_memory(a, d)      # 新陣列與原陣列不共用記憶體
False

若呼叫物件的 resize() 方法時新形狀之元素總數與原先的不同會出現 ValueError 錯誤, 必須加上 refcheck=False 參數才可順利變形, 例如 : 

>>> a=np.array([[1, 2, 3], [4, 5, 6]])       # 建立 2x3 陣列
>>> print(a)    
[[1 2 3]
 [4 5 6]]
>>> a.resize((3, 3))                                    # 用物件的 resize() 方法擴大形狀 (錯誤)
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
ValueError: cannot resize an array that references or is referenced
by another array in this way.
Use the np.resize function or refcheck=False    
>>> a.resize((2, 2))                                   # 用物件的 resize() 方法縮小形狀 (錯誤)
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
ValueError: cannot resize an array that references or is referenced
by another array in this way.
Use the np.resize function or refcheck=False    
>>> a.resize((3, 3), refcheck=False)       # 加上 refcheck=False 參數即可順利變形 (擴大)
>>> print(a)                                              # 呼叫物件的 np.resize() 原陣列受影響
[[1 2 3]
 [4 5 6]
 [0 0 0]]
>>> a.resize((2, 2), refcheck=False)       # 加上 refcheck=False 參數即可順利變形 (縮小)
>>> print(a)                                             # 呼叫物件的 np.resize() 原陣列受影響
[[1 2]
 [3 4]]

可見呼叫 resize() 方法時變形的方式是截長補短, 截長就是捨去多的部分; 補短就是元素補 0 直到形狀補滿. 但若使用全域方法的話就不需要加上 refcheck=False 參數, 而且補短的方式是用最前面的元素補在後面直到形狀補滿為止, 例如 :

>>> a=np.arange(1, 7)           # 建立 1x6 陣列
>>> a.reshape(2, 3)                # 重塑 reshape 
array([[1, 2, 3],
       [4, 5, 6]])
>>> print(a)                            # 重塑不影響原陣列
[1 2 3 4 5 6]
>>> b=np.resize(a, (3, 3))      # 將原陣列 a 擴大變形為 (3, 3) 並傳回新陣列 b
>>> print(b)    
[[1 2 3]
 [4 5 6]
 [1 2 3]]                                    # 補短的方式是用原陣列最前面的元素去補到滿為止
>>> c=np.resize(a, (2, 2))       # 將原陣列 a 縮小變形為 (2, 2) 並傳回新陣列 c
>>> print(c)                            # 截短的方式是將多出來的部分直接截掉
[[1 2]
 [3 4]]
>>> print(a)                            # 呼叫全域的 np.resize() 原陣列不受影響
[1 2 3 4 5 6]

綜合上述測試, 結論是呼叫 ndarray 物件的 resize() 方法會直接更改陣列的形狀 (shape 屬性), 若元素數目有擴大或縮減會出現錯誤, 必須加上 refcheck=False 參數才行, 且其補短方式是補 0. 如果使用全域的 np.resize() 方法則不會改變原陣列, 而是傳回變形後的新陣列, 且元素數目擴大時的補短方式是用原陣列最前面的元素依序補到滿. 


2. 呼叫 transpose() 方法將陣列轉置 :

此方法用來將陣列的指定軸進行轉置 (交換), 它會傳回轉置後的陣列參考, 但新陣列與原陣列共用記憶體, 亦即 transpose() 與 reshape() 一樣都只是傳回原陣列的一種視角 (view) 陣列而已, 不是在記憶體中再產生一個新陣列. 其次是轉置的同時也會將陣列重塑 (reshape). 

求取轉置陣列除了可以呼叫 ndarray 物件的 transpose() 方法外, 也可以呼叫 Numpy 的全域方法 np.transpose(), 參數結構如下 : 

transpose([axes=None])  或 np.transpose(arr [, axes=None])      

注意, 備選參數 axes 是一個用來指定軸交換順序的 tuple (所以是複數的 axes 而非單數的 axis). 這個元組的順序標示了那些軸要交換, 例如 axes=(0, 1, 2) 表示都不交換; axes=(1, 0, 2) 表示軸 1 與軸0 交換; 而 axes=(0, 2, 1) 表示軸 1 與軸 2 交換等等.

1D 陣列因為只有一個軸, 呼叫 transpose() 並無轉置效果, 例如 : 

>>> a=np.arange(1, 7)      
>>> print(a)
[1 2 3 4 5 6]
>>> b=a.transpose()     
>>> print(b)   
[1 2 3 4 5 6]
>>> a.T    
array([1, 2, 3, 4, 5, 6])

2D 陣列只有兩個軸, 轉置就只是行列互換而已, 因此呼叫時不需要傳入 axes 元組參數, 形狀是 (m, n) 的 2D 陣列轉置後形狀變成 (n, m), 其轉置陣列也稱為轉置矩陣, 例如 :

>>> a=np.arange(1, 7).reshape(2, 3)         # 建立 2x3 陣列
>>> print(a)     
[[1 2 3]
 [4 5 6]]
>>> b=a.transpose()                                    # 呼叫 transpose() 傳回轉置陣列 (view)
>>> print(b)     
[[1 4]
 [2 5]
 [3 6]]
>>> np.may_share_memory(a, b)             # 轉置陣列與原陣列共享記憶體
True  
>>> c=np.transpose(a)                                # 呼叫全域方法 np.transpose() 效果一樣
>>> print(c)    
[[1 4]
 [2 5]
 [3 6]]
>>> np.may_share_memory(a, c)              # 轉置陣列與原陣列共享記憶體
True    
>>> a.T                                                         # 呼叫 transpose() 結果與 T 屬性一樣
array([[1, 4],
       [2, 5],
       [3, 6]])

可見 2D 陣列經過轉置後, 行變成列, 列變成行. 不論是呼叫 ndarray 物件的 transpose() 方法還是 Numpy 的全域方法 np.transpose() 都得到同樣的轉置陣列, 而且與物件的 T 屬性值完全一樣. 

但對於 3D 以上陣列而言, 就不是行列交換了, 而是要指定那些軸要交換, 如果沒有傳入 axes 指定, 預設是以對稱方式交換, 例如原陣列軸順序是 (0, 1, 2) 就以 (2, 1, 0) 為 axes 做交換, 即中間的軸 1 不動, 而軸 0 與軸 2 交換, 例如 : 

>>> a=np.arange(1, 19).reshape(3, 3, 2)      # 建立 3x3x2 陣列
>>> print(a)     
[[[ 1  2]
  [ 3  4]
  [ 5  6]]

 [[ 7  8]
  [ 9 10]
  [11 12]]

 [[13 14]
  [15 16]
  [17 18]]]
>>> b=a.transpose()                # 未傳入 axes 參數時相當 T 屬性, 也等於 axes=(2, 1, 0)
>>> print(b)
array([[[ 1,  7, 13],
        [ 3,  9, 15],
        [ 5, 11, 17]],

       [[ 2,  8, 14],
        [ 4, 10, 16],
        [ 6, 12, 18]]])
>>> b.shape    
(2, 3, 3)
>>> a.T                                 # 與 a.transpose() 結果相同
array([[[ 1,  7, 13],
        [ 3,  9, 15],
        [ 5, 11, 17]],

       [[ 2,  8, 14],
        [ 4, 10, 16],
        [ 6, 12, 18]]])
>>> a.transpose((2, 1, 0))    # 與 a.transpose() 結果相同
array([[[ 1,  7, 13],
        [ 3,  9, 15],
        [ 5, 11, 17]],

       [[ 2,  8, 14],
        [ 4, 10, 16],
        [ 6, 12, 18]]])

上面 a.transpose() 的轉置結果可以這樣來理解, 首先是形狀問題, 原陣列 a 的形狀是 (3, 3, 2), 由於轉置時是軸 0 (最外層) 與軸 2 (最內層) 維度交換, 因此轉置後的陣列形狀應該是 (2, 3, 3), 因此要準備一個 2x3x3 的目標陣列, 也就是 2 個 3x3 表格. 接著將軸 0 與軸 2 交換, 亦即軸 1 不動, 依序拜訪軸 0 的每個軸 1 元素, 將其填入軸 2 的空格中, 如下圖所示 : 




其他指定軸的轉置方式也是如此, 例如 : 

>>> a=np.arange(1, 19).reshape(3, 3, 2)       # 建立 3x3x2 陣列
>>> print(a)     
[[[ 1  2]
  [ 3  4]
  [ 5  6]]

 [[ 7  8]
  [ 9 10]
  [11 12]]

 [[13 14]
  [15 16]
  [17 18]]]
>>> c=a.transpose((0, 2, 1))        # 指定軸 2 與軸 1 轉置
>>> print(c)     
[[[ 1  3  5]
  [ 2  4  6]]

 [[ 7  9 11]
  [ 8 10 12]]

 [[13 15 17]
  [14 16 18]]]

此例原陣列 a 的形狀是 (3, 3, 2), 由於是軸 1 (中間層) 與軸 2 維度交換, 因此轉置後的陣列形狀應該是 (3, 2, 3), 因此要準備一個 3x2x3 的空陣列, 然後軸 0 不動 (不跳躍), 依序走訪軸 1 並將其填到目標空陣列的軸 2 內, 如下圖所示 : 



其他傳置方式例如軸 0 與軸 1 交換 transpose((1, 0, 2)), 或三軸互相交換 transpose((1, 2, 0)), 方法都是一樣, 結果如下 :

>>> d=a.transpose((1, 0, 2))    
>>> print(d)
[[[ 1  2]
  [ 7  8]
  [13 14]]

 [[ 3  4]
  [ 9 10]
  [15 16]]

 [[ 5  6]
  [11 12]
  [17 18]]]
>>> e=a.transpose((1, 2, 0))    
>>> print(e)
[[[ 1  7 13]
  [ 2  8 14]]

 [[ 3  9 15]
  [ 4 10 16]]

 [[ 5 11 17]
  [ 6 12 18]]]

高階的陣列轉置更為複雜, 但都是遵循相同方式, 例如 4D 陣列 : 

>>> a=np.arange(1, 25).reshape(2, 3, 1, 4)     # 建立 2x3x1x4 陣列 
>>> print(a)   
[[[[ 1  2  3  4]]

  [[ 5  6  7  8]]

  [[ 9 10 11 12]]]


 [[[13 14 15 16]]

  [[17 18 19 20]]

  [[21 22 23 24]]]]
>>> b=a.transpose()         # 未指定 axes, 預設用 axes=(3, 2, 1, 0) 做對稱轉置=a.T
>>> print(b)   
[[[[ 1 13]
   [ 5 17]
   [ 9 21]]]


 [[[ 2 14]
   [ 6 18]
   [10 22]]]


 [[[ 3 15]
   [ 7 19]
   [11 23]]]


 [[[ 4 16]
   [ 8 20]
   [12 24]]]]
>>> b.shape   
(4, 1, 3, 2)

此例中的 4D 陣列 shape=(2, 3, 1, 4), 轉置時未傳入 axes 參數, 預設會用 axes=(3, 2, 1, 0) 做對稱轉置 (以軸 1 與軸 2 中線為準左右交換), 因此轉置後的陣列形狀為 (4, 1, 3, 2).


2. 呼叫 astype() 方法更改資料型態 :

呼叫 astype(dtype) 方法會將陣列的元素轉成指定的 dtype 類型後傳回一個副本陣列, 例如 :

>>> import numpy as np     
>>> a=np.arange(1, 7)                  # 建立一個 1x6 等差數列陣列
>>> print(a)  
[1 2 3 4 5 6]
>>> b=a.astype(float)                    # 複製 a 陣列並將新陣列元素型態改為 float
>>> print(b)   
[1. 2. 3. 4. 5. 6.]
>>> b.dtype    
dtype('float64')
>>> np.may_share_memory(a, b)     # astype() 會建立新陣列 
False   

可見 astype() 會傳回一個新的陣列副本. 


3. 呼叫 sort() 方法對元素排序 :

呼叫 sort([axis]) 方法可對陣列元素進行由小到大排序 (ascending), 這會改變陣列元素在記憶體中的排列順序, 傳回值為 None. 對於 1D 陣列不需傳入 axis 參數, 例如 : 

>>> import numpy as np   
>>> a=np.array([3, 4, 1, 5, 2, 0])    
>>> print(a)   
array([3, 4, 1, 5, 2, 0])   
>>> a.sort()       
>>> print(a)   
array([0, 1, 2, 3, 4, 5])       # 已由小到大排序

對於 2D 陣列 , 若不傳入參數, 預設是對最後那一軸排序, 對 2D 陣列即軸 1, 若要對軸 0 排序需傳入 0 或 axis=0, 例如 :

>>> a=np.array([[5, 4, 7, 1], [2, 6, 0, 3]])    
>>> print(a)
array([[5, 4, 7, 1],
       [2, 6, 0, 3]])
>>> a.sort()                   # 預設沿軸 1 由小到大排序
>>> print(a)   
array([[1, 4, 5, 7],
       [0, 2, 3, 6]])
>>> a.sort(axis=0)        # 指定沿軸 0 排序
>>> print(a)   
array([[0, 2, 3, 6],           
       [1, 4, 5, 7]])

對於 3D 以上陣列也是如此, 未指定 axis 參數預設是對最後一軸排序, 例如 :

>>> a=np.random.randint(0, 24, (3, 4, 2))   # 建立一個隨機整數 3D 陣列
>>> print(a)
array([[[ 1,  4],
        [ 8, 21],
        [ 3, 12],
        [ 7, 10]],

       [[ 5, 21],
        [ 3, 13],
        [21,  7],
        [10, 21]],

       [[12,  5],
        [12, 14],
        [ 7, 18],
        [ 4, 19]]])
>>> a.sort()              # 未傳入 axis 參數預設對最後一軸 (軸 2, 最內層) 元素排序
>>> print(a)
array([[[ 1,  4],
        [ 8, 21],
        [ 3, 12],
        [ 7, 10]],

       [[ 5, 21],
        [ 3, 13],
        [ 7, 21],
        [10, 21]],

       [[ 5, 12],
        [12, 14],
        [ 7, 18],
        [ 4, 19]]])
>>> a.sort(axis=1)         # 指定沿軸 1 (中間層) 排序
>>> print(a)
array([[[ 1,  4],
        [ 3, 10],
        [ 7, 12],
        [ 8, 21]],

       [[ 3, 13],
        [ 5, 21],
        [ 7, 21],
        [10, 21]],

       [[ 4, 12],
        [ 5, 14],
        [ 7, 18],
        [12, 19]]])
>>> a.sort(axis=0)           # 指定沿軸 0 (最外層) 排序
>>> a
array([[[ 1,  4],
        [ 3, 10],
        [ 7, 12],
        [ 8, 19]],

       [[ 3, 12],
        [ 5, 14],
        [ 7, 18],
        [10, 21]],

       [[ 4, 13],
        [ 5, 21],
        [ 7, 21],
        [12, 21]]])

以上使用 sort() 排序都是遞增排序 (ascending), 此方法並沒有提供遞減排序的參數, 如果要進行由大到小的遞減排序可用陣列的 [::-1] 切片去呼叫 sort(), 例如 :

>>> a=np.random.randint(1, 10, 9)       # 建立 1~9 的 1x9 一維隨機陣列
>>> print(a)    
array([5, 6, 2, 7, 1, 2, 3, 2, 7])  
>>> a[::-1].sort()    
>>> print(a)
array([7, 7, 6, 5, 3, 2, 2, 2, 1])

參考 :



4. 呼叫 all() 與 any() 方法判斷元素值真假 :

這兩個方法用來判定陣列元素的真假值 (除了 0 以外的數值都是 True, 只有 0 是 False), 當所有元素值均為 True 時 all() 才會傳回 True; 而 any() 則是元素中只要有一個為 True 就傳回 True. 不過這兩個方法的傳回值除了 True/False 外, 也可以是布林陣列 (指定 axis), 其參數結構如下  :  

all([axis=None, out=None, keepdims=False]) 或
np.all(arr, [axis=None, out=None, keepdims=False])     

any([axis=None, out=None, keepdims=False])  或 
np.any(arr, [axis=None, out=None, keepdims=False])  

參數 out 用來指定一個儲存結果的陣列變數 (極為少用), 而 keepdims 值為 True/False (預設), 用來決定傳回的布林陣列是否要保存原陣列之軸數, 例如 : 

>>> a=np.array([[-1, 0, 2], [9, 4, 0], [-5, 0, 1]])    
>>> print(a)      
[[-1  0  2]
 [ 9  4  0]
 [-5  0  1]]
>>> a.all()      
False
>>> np.all(a)     
False
>>> a.all(axis=0)                   # 沿軸 0 判斷, 僅第 0 行無 0 故為 True, False, False
array([ True, False, False])     
>>> a.all(axis=1)                   # 沿軸 1 判斷, 每一列都有 0 故為 False, False, False 
array([False, False, False])    
>>> a.any(axis=0)                 # 沿軸 0 判斷, 每一列至少都有一個非零故為 True, True, True
array([ True,  True,  True])   
>>> a.any(axis=1)                 # 沿軸 1 判斷, 每一行至少都有一個非零故為 True, True, True
array([ True,  True,  True])   

注意, 若有指定 axis, 則傳回的布林陣列在該軸就會收縮消失, 所以 (3, 3) 的原陣列傳回 (3, ) 的布林陣列, 原本是兩軸變成一軸. 如果要讓傳回的布林陣列保持原陣列的軸數, 可指定 keepdims=True 來保持傳回的布林陣列軸數與原陣列相同, 例如 : 

>>> a=np.array([[-1, 0, 2], [9, 4, 0], [-5, 0, 1]])    
>>> print(a)    
[[-1  0  2]
 [ 9  4  0]
 [-5  0  1]]
>>> a.all(axis=0, keepdims=True)         # keepdims=True 保留軸數不變=2
array([[ True, False, False]])   
>>> a.all(axis=1, keepdims=True)    
array([[False],
       [False],
       [False]])
>>> a.any(axis=0, keepdims=True)      
array([[ True,  True,  True]])
>>> a.any(axis=1, keepdims=True)   
array([[ True],
       [ True],
       [ True]])

可見傳回的布林陣列都是兩個方括號, 軸數與原陣列同樣都是 2. 

也可以呼叫全域方法 np.all() 與 np.any() 來判斷真假, 第一參數 arr 也可以先做關係運算, 例如 :

>>> a=np.random.randint(10, size=(2, 3))      #建立 2x3 隨機整數陣列 (元素 0~9)
>>> print(a)    
[[7 0 8]
 [5 9 3]]
>>> np.all(a)                           # 檢查全部元素是否都為 True (不為 0), 有 0 故 False
False
>>> np.any(a)                         # 檢查有任一元素為 True (不為 0), 有故 True
True
>>> np.all(a, axis=0)              # 沿軸 0 檢查真假 a[0,1]=0 (False) 故第二行為 False
array([ True, False,  True])
>>> np.all(a, axis=1)              # 沿軸 1 檢查真假 a[0,1]=0 (False) 故第一列為 False
array([False,  True])
>>> np.all(a, axis=0, keepdims=True)     # 沿軸 0 檢查真假並且保留軸數
array([[ True, False,  True]])
>>> np.all(a, axis=1, keepdims=True)     # 沿軸 1 檢查真假並且保留軸數
array([[False],
       [ True]])
>>> np.any(a > 5)           # 檢查 a 陣列是否有任一元素 > 5  (是)
True
>>> np.any(a < 3)           # 檢查 a 陣列是否有任一元素 < 3  (是)
True
>>> np.any(a == 4)         # 檢查 a 陣列是否有任一元素等於 4 (無)  
False
>>> np.any(a%2==0, axis=0)     # 檢查 a 陣列沿軸 0 是否有任一元素可被 2 整除
array([False,  True,  True])
>>> np.any(a%2==0, axis=1)     # 檢查 a 陣列沿軸 1 是否有任一元素可被 2 整除
array([ True, False])


三. 必須使用全域方法的陣列操作 :

有一些陣列操作必須使用 Numpy 的全域方法 (用套件名稱 numpy 直接呼叫), 因為這些操作不適合放在 ndarray 物件的方法中, 例如添加元素或陣列合併等. 


1. 呼叫 np.append() 添加元素 :

呼叫 append() 方法會在陣列後面添加元素, 但 ndarray() 物件沒有 append() 方法, 必須呼叫 Numpy 的全域方法, 其參數結構如下 :

np.append(arr, values [axis=None]) 

必要參數 arr 為要操作的陣列, values 為要添加到陣列後面的元素, 可以是元組, 串列或陣列. 第二參數 axis 用來指定要添加到哪一個軸. 注意, 若未指定 axis, 則不論原陣列的形狀為何, append() 一律傳回 1D 陣列. 

如果有指定 axis 參數, 注意所添加的元組, 串列, 或陣列之軸數必須與被添加的陣列 arr 軸數相同, 且元素數目須相同, 否則會出現 ValueError 與例外, 例如 :

>>> a=np.arange(1, 7)                             # 建立一個 1x6 陣列
>>> b=np.append(a, [7, 8, 9])                 # 將 [1, 2, 3] 添加到 a 陣列後面
>>> print(b)
[1 2 3 4 5 6 7 8 9]     
>>> c=a.reshape((2, 3))                           # 將陣列 a 重塑為 2x3 陣列 c
>>> print(c)    
[[1 2 3]
 [4 5 6]]
>>> d=np.append(c, [7, 8, 9])                 # 未指定 axis, 傳回 1D 陣列 
>>> print(d)     
[1 2 3 4 5 6 7 8 9]                                      # 將陣列 c 拉平後把 [7, 8, 9] 添加到後面
>>> e=np.append(c, [[7, 8, 9]], axis=0)  # 將串列 [[7, 8, 9]] 添加到 c 陣列的軸 0  
>>> print(e)  
[[1 2 3]
 [4 5 6]
 [7 8 9]]
>>> f=np.append(c, [[7], [8]], axis=1)    # 將串列 [[7], [8]] 添加到 c 陣列的軸 1
>>> print(f)   
[[1 2 3 7]
 [4 5 6 8]]
>>> print(a)                                              # np.append() 建立新陣列, 不影響原陣列
[1 2 3 4 5 6]
>>> print(c)                                              # np.append() 建立新陣列, 不影響原陣列
[[1 2 3]
 [4 5 6]]

注意上面範例中的陣列 e 與 f, 陣列 e 是在 c 陣列的軸 0 上添加元素, 陣列 c 軸數為 2, 因此所添加的串列也必須是兩軸, 因此須為 [[7, 8, 9]] 而非 [7, 8, 9] . 其次, 陣列 f 是在 c 陣列的軸 1 上添加元素, 陣列 c 軸數為 2, 因此所添加的串列也必須是兩軸, 且該軸元素維度是 2, 因此須為 [[7], [8]] 而非 [7, 8] 或 [[7, 8]].


2. 呼叫 np.vstack() 與 np.hstack() 合併陣列 :

在 Python 中要合併兩個串列只要用 + 運算子就可以了, 例如 :

>>> a=[[1, 2], [3, 4]]          # 建立 2x2 串列
>>> print(a)    
[[1, 2], [3, 4]]
>>> b=[[5, 6], [7, 8]]          # 建立 2x2 串列
>>> print(b)    
[[5, 6], [7, 8]]
>>> a + b                            # 用 + 合併兩串列
[[1, 2], [3, 4], [5, 6], [7, 8]]

但在 Numpy 中 + 運算子的作用是做元素相加, 例如 :

>>> a=np.array([[1, 2], [3, 4]])          # 建立 2x2 陣列
>>> print(a)    
[[1 2]
 [3 4]]
>>> b=np.array([[5, 6], [7, 8]])          # 建立 2x2 陣列
>>> print(b)    
[[5 6]
 [7 8]]
>>> a + b                                             # 陣列的 + 運算子是做逐元素加法, 不是陣列合併
array([[ 6,  8],
       [10, 12]])

在 Numpy 中要合併兩個陣列必須使用全域方法 np.hstack() 與 np.vstack(), 分別是做水平與垂直合併動作, 嚴謹地說, 對於 2D 以上陣列, np.hstack() 是沿軸1 做合併; 而 np.vstack() 則是沿軸 0 做合併. 這兩個方法只有一個元組參數, 就是將要合併的陣列 a, b 放在一個 tuple 中傳進去 :

np.hstack((a, b))      # 沿軸 1 方向合併         
np.vstack((a, b))      # 沿軸 0 方向合併

這兩個全域方法都會傳回一個合併後的新陣列. 陣列合併在機器學習中很常用, 例如強化學習經常會用到 np.hstack() 來合併陣列以進行結果統計. 

但要順利呼叫這兩個方法有兩個條件 :
  • 被合併的兩個陣列之軸數必須相同.
  • 沿某軸合併時其它軸的維度 (元素個數) 必須相同. 
亦即必須同 D 的陣列才能合併, 例如 1D 陣列不能與 2D 陣列合併. 

對 1D 陣列來說, 因為是列陣列, 所以 1D 陣列只能只能呼叫 hstack() 做水平合併, 不可呼叫 vstack() 做垂直合併, 否則會出現 ValueErrir 錯誤與例外, 例如 : 

>>> a=np.arange(1, 7)    
>>> print(a)
[1 2 3 4 5 6]
>>> b=np.arange(1, 4)    
>>> print(b)
[1 2 3]
>>> c=np.hstack((a, b))    
>>> print(c)
[1 2 3 4 5 6 1 2 3]

2D 以上陣列就可以呼叫 np.hstack() 或 np.vstack() 來進行合併, 而且在幾何意義上 np.hstack() 可理解為水平方向合併 (沿軸 1); 而 np.vstack() 則可理解為沿垂直方向合併 (沿軸 0), 例如 : 

>>> a=np.arange(1, 5).reshape(2, 2)       # 建立 2x2 陣列
>>> print(a)    
[[1 2]
 [3 4]]
>>> b=np.arange(5, 9).reshape(2, 2)       # 建立 2x2 陣列
>>> print(b)    
[[5 6]
 [7 8]]
>>> c=np.hstack((a, b))                            # 沿軸 1 (水平方向) 將 a, b 陣列合併
>>> print(c)     
[[1 2 5 6]
 [3 4 7 8]]
>>> d=np.vstack((a, b))                            # 沿軸 0  (垂直方向) 將 a, b 陣列合併
>>> print(d)     
[[1 2]
 [3 4]
 [5 6]
 [7 8]]

可見對 2D 陣列來說, 水平方向合併就是沿著軸 1 方向合併, 而垂直方向合併就是沿著軸 0 合併. 但要注意的是, 沿著軸 1 做水平合併時, 另一個軸 (即軸 0) 的維度必須相同, 反之亦然. 

上面的範例中, 兩個陣列都是 2x2, 因此不論沿軸 1 還是軸 0 合併都沒有問題, 但下面的例子則只能沿軸 1 合併, 亦即只能呼叫 np.hstack() :

>>> a=np.arange(1, 7).reshape(2, 3)         # 建立一個 2x3 陣列
>>> print(a)    
[[1 2 3]
 [4 5 6]]
>>> b=np.arange(7, 11).reshape(2, 2)       # 建立一個 2x2 陣列
>>> print(b)     
[[ 7  8]
 [ 9 10]]
>>> c=np.hstack((a, b))                              # 沿軸 1 將 a, b 陣列水平合併
>>> print(c)     
[[ 1  2  3  7  8]
 [ 4  5  6  9 10]]

此例 a 陣列為 2x3 維, b 陣列維 2x2 維, 呼叫 np.hstack() 沿軸 1 合併時, 兩個陣列的軸 0 都是 2 維, 因此可順利合併; 但若呼叫 np.vstack() 沿軸 0 合併時, 另一個軸 (軸 1) 維度卻不同, a 陣列是 3 維, b 陣列是 2 維, 兩個不一致無法合併, 會出現錯誤與例外. 

事實上所謂水平與垂直合併在概念上僅適用於 2D 陣列, 對於多軸的高維度陣列就無法用水平與垂直的概念來理解這兩個方法, 而必須從下列合併規則來看, 即 np.hstack() 就是沿軸 1 合併; 而 np.vstack() 則是沿軸 0 合併, 不論是幾 D 的高維度陣列皆是如此, 例如下面的 3D 陣列可以呼叫 np.hstack() 進行軸 1 合併, 但不可呼叫 np.vstack() 進行軸 0 合併 :

>>> a=np.arange(1, 13).reshape(2, 2, 3)      # 建立 2x2x3 陣列
>>> print(a)   
[[[ 1  2  3]
  [ 4  5  6]]

 [[ 7  8  9]
  [10 11 12]]]
>>> b=np.arange(13, 19).reshape(2, 1, 3)    # 建立 2x1x3 陣列
>>> print(b)
[[[13 14 15]]

 [[16 17 18]]]
>>> c=np.hstack((a, b))                                 # 沿軸 1 合併
>>> print(c)     
[[[ 1  2  3]
  [ 4  5  6]
  [13 14 15]]    

 [[ 7  8  9]
  [10 11 12]
  [16 17 18]]]

此例兩個陣列都是 3D 陣列, 符合合併的第一個條件. 陣列 a 維度是 2x2x3, 陣列 b 維度是 2x1x3, 軸 0 與軸 3 的維度一致 (分別是 2 與 3), 故只能沿軸 1 (即第二個方括號) 合併. 所以陣列 b 第二個方括號內的元素就分別與陣列 a 第二個方括號內的元素合併了. 

由上可知 1D 陣列只能做水平合併, 要 2D 以上陣列才能做垂直合併, 下面是 3D 陣列呼叫 np.vstack() 做垂直合併的範例 :

>>> a=np.arange(1, 19).reshape(3, 3, 2)     # 建立一個 3x3x2 陣列
>>> print(a)   
[[[ 1  2]
  [ 3  4]
  [ 5  6]]

 [[ 7  8]
  [ 9 10]
  [11 12]]

 [[13 14]
  [15 16]
  [17 18]]]
>>> b=np.arange(19, 25).reshape(1, 3, 2)       # 建立一個 1x3x2 陣列
>>> print(b)   
[[[19 20]
  [21 22]
  [23 24]]]
>>> c=np.vstack((a, b))                                    # 沿軸 0 合併 a, b 兩陣列
>>> print(c)   
[[[ 1  2]
  [ 3  4]
  [ 5  6]]

 [[ 7  8]
  [ 9 10]
  [11 12]]

 [[13 14]
  [15 16]
  [17 18]]

 [[19 20]
  [21 22]
  [23 24]]]

此例陣列 a, b 均為三軸, 符合合併第一要件; 其次 np.vstack() 是沿軸 0 (最外層方括號) 合併, 除軸 0 外的其他軸維度都一致 (軸 1 是 3 維, 軸 2 是 2 維), 符合合併第二要件, 因此可以順利合併. 由於是沿軸 0 合併, 因此合併時就把陣列 b 最外層方括號 (代表軸 0) 的元素整個垂直併到陣列 a 最外層方括號的最後面即可. 

沒有留言 :