基礎 | numpy ndarray 之內功心法,理解高維操作!
多維資料的形象表示
import numpy as np
# 一維資料不用贅言
data_1d = np.array([ 0 , 1 , 2 , 3 ])
# 二維資料作為 m 行 n 列的表格,例如 2 行 3 列
data_2d = np.arange( 6 ).reshape( 2 , 3 )
# 三維資料作為 k 層 m 行 n 列 的積木塊, 例如 2 層 3 行 4 列
data_3d = np.arange( 24 ).reshape( 2 , 3 , 4 )
檢查一個 ndarray 資料的維度和大小,分別用 ndim 和 shape 屬性。
>>> print (data_3d.ndim)
3
>>> print (data_3d.shape)
( 2 , 3 , 4 )
shape 是一個很關鍵的屬性,我是這樣把它和各個軸對應的:
shape: ( 2 , 3 , 4 )
k , m , n
z , y , x
心法1: x, y, z 對應的shape元組是從右往左數的。
這是我的個人習慣,也符合主流的用法。
影象資料的小誤會
開啟一幅 640 x 480 的影象:
import numpy as np
import matplotlib.pylab as plt
image = plt.imread( "lena.jpg" )
print (image.shape)
# --- 結果 ---
# (480, 640, 3)
# (y, x, c)
不是 640 x 480 嗎, 怎麼倒過來了?我寫程式碼的時候在這裡總是犯迷糊。
在口頭表達中,我們先說寬640,再說高480,而在計算機中是先高(y) 後寬(x),注意了!
每個畫素有三個顏色分量(color),所以這個維度放在了最右邊,可以理解,順序就是 (y, x, c)
抽象軸上的操作
對於4維及更高維度的資料,無法在3維空間圖示。這個時候,就不要考慮形象思維了,直接按照規則做處理。
用 shape 屬性返回的元組,從左到右,座標軸分別命名為 axis 0, axis 1, ...,請注意,現在是從 左向右數 ,正好是這個元組的 index,在以後的運算中,都按此規定。
>>> print (data.shape)
( 3 , 3 , 2 , 5 )
# axis 0: 3
# axis 1: 3
# axis 2: 2
# axis 3: 5
心法2: 抽象座標軸順序從左向右。指定哪個軸,就只在哪個軸向操作,其他軸不受影響。
排序(sorting)
data = np.array(np.arange( 12 ))
np.random.shuffle(data)
data = data.reshape( 3 , 4 )
print (data)
# [[10 8 3 2]
# [ 5 6 0 7]
# [11 4 9 1]]
print (np.sort(data , axis = 0 ))
# [[ 5 4 0 1] | 小
# [10 6 3 2] | 到
# [11 8 9 7]] | 大
print (np.sort(data , axis = 1 ))
# 小 到 大
# --------->
# [[ 2 3 8 10]
# [ 0 5 6 7]
# [ 1 4 9 11]]
如果你在心中能把抽象軸和 x, y, z 對應起來,則理解軸向排序很容易。
shape: ( 3 , 4 )
axis: 0 , 1
AXIS: y , x
2. 求和、均值、方差、最大、最小、累加、累乘
這幾個函式呼叫,一般會指定軸向,注意心法2
sum,mean,std,var,min,max 會導致這個軸被壓扁,縮減為一個數值
data = np.arange( 24 ).reshape( 2 , 3 , 4 )
print (data)
# [[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
#
# [[12 13 14 15]
# [16 17 18 19]
# [20 21 22 23]]]
print ( np.sum(data , axis = 0 ) )
# 0軸被sum壓扁,1軸2軸不變
# [[12 14 16 18]
# [20 22 24 26]
# [28 30 32 34]]
print ( np.sum(data , axis = 1 ) )
# 1軸被sum壓扁,0軸2軸不變
# [[12 15 18 21]
# [48 51 54 57]]
cumsum,cumprod 不縮減軸向,只在指定軸向操作,請讀者自己試驗。
3. 索引和切片(indexing and slicing)
心法3: 在索引中出現冒號(:),則本軸繼續存在,如果只是一個數值,則本軸消失。
例如,像 :, :1, 1: 這樣的索引,保留此軸, data[:, :1, 2:] 中,三個軸都保留。 data[1, 4, 2] 三個軸都消失,只返回一個數值。
data[1:2, 0:1, 0:1] 中,三個軸都保留,但只有一個數據元素,很神奇吧。
data = np.arange( 24 ).reshape( 2 , 3 , 4 )
print ( data )
# [[[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
#
# [[12 13 14 15]
# [16 17 18 19]
# [20 21 22 23]]]
print ( data[ 0 , : , :] )
# axis 0,即 z 軸,是數值,則 z 軸消失,切了一片 x-y
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print ( data[ 0 , 1 , 2 ] )
# 所有軸都消失,只返回一個標量資料
# 6
print ( data[ 0 : 1 , 1 : 2 , 2 : 3 ] )
# 返回三維資料,雖然只有一個元素
# [[[6]]]
如何檢視 ndarray 的維度呢?可以訪問 shape 屬性;如果打印出來了,那麼就數一數起始的中括號個數,比如 [[[6]]], 有三個 [,那麼就是三維陣列。你記住了嗎?
4. 拼接(concatenating)
同樣遵循心法2,指定哪個軸,就在哪個軸向拼接:
data = np.arange( 4 ).reshape( 2 , 2 )
print ( np.concatenate([data , data] , axis = 0 ) )
# 在軸向 0 拼接,即 y 方向
# [[0 1]
# [2 3]
# [0 1]
# [2 3]]
print ( np.concatenate([data , data] , axis = 1 ) )
# 在軸向 1 拼接,即 x 方向
# [[0 1 0 1]
# [2 3 2 3]]
reshape 之迷亂
你有沒有這個困惑:在 reshape 之後,資料在各個軸上是如何重新分配的?
搞清楚 ndarray 的資料在記憶體裡的存放方式,以及各個維度的訪問方式,reshape 困惑就迎刃而解了。
心法4: ndarray 的資料在記憶體裡以一維線性存放,reshape 前後,資料沒有變化,只是訪問方式變了而已。
資料優先填充 X 軸向,其次 Y 軸,其次 Z 軸 。。。
有 C 語言基礎的,很容易理解 ndarray 的實現,就是 C 中的多維陣列而已。
int data[ 2 ][ 3 ][ 4 ];
int data[ 4 ][ 6 ];
總結
就說這麼多,看了本文請親自動手寫程式碼體驗一下。掌握此心法,可以縱橫 numpy 世界而無大礙。
心法1: x, y, z 對應的shape元組是從右往左數的。
心法2: 抽象座標軸順序從左向右。指定哪個軸,就只在哪個軸向操作,其他軸不受影響。
心法3: 在索引中出現冒號(:),則結果中本軸繼續存在,如果只是一個數值,則本軸消失。
心法4: ndarray 的資料在記憶體裡以一維線性存放,reshape 前後,資料沒有變化,只是訪問方式變了而已。
原文釋出時間為:2018-09-20
本文作者:曲奇