6.5. 形狀與步幅

ndarray 內部的資料是一整塊緊湊的數字。位於該區塊前方的描述子決定了這個扁平區塊如何被讀取成一個張量。

6.5.1. 描述子記錄了什麼

有五個值描述如何把資料區塊讀取成張量:

a = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.uint8)

a.ndim       # 2     - number of dimensions
a.shape      # (2, 3)- length along each dimension
a.itemsize   # 1     - bytes per element (from dtype)
a.size       # 6     - total number of elements
a.strides    # (3, 1)- step pattern through the buffer

ndinfo() 輔助函式會在一次呼叫中印出全部這些值,再加上底層緩衝區的位置。兩個緩衝區位置相符的陣列即共享記憶體:

np.ndinfo(a)
# class: ndarray
# shape: (2, 3)
# strides: (3, 1)
# itemsize: 1
# data pointer: 0x...
# type: uint8

6.5.2. 步幅說明

步幅(stride)是指在資料區塊中要移動多少位元組,才能沿某個軸移動一個元素。對於上面那個 2x3 的 uint8 陣列,步幅為 (3, 1):往下移動一列跳過 3 個位元組,往右移動一行跳過 1 個位元組。這等同於說各列由左到右、前後相接地儲存:

memory: [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ][ 6 ]
          ^ row 0          ^ row 1
          <------- 3 bytes ---->

為了讀取 a[i, j]numpy 會從資料區塊的起點計算 i * strides[0] + j * strides[1],並從該處讀取 itemsize 個位元組。同樣的公式可延伸到任意維度。

這種配置——各列首尾相接儲存,且最後軸沿記憶體變化最快——稱為 列主序(row-major)。numpy 在相機上配置的每個陣列都使用這種配置。

6.5.3. 列主序帶來的後果

從「各列前後相接儲存」這件事衍生出兩件事,在相機上塑形緩衝區時很重要。

最後軸是連續的。a[0, 0] 走到 a[0, 1] 會碰到下一個位元組。從 a[0, 0] 走到 a[1, 0] 則跨越一整列。

最後軸是整個陣列數學運算的快速軸。 相機上的 numpy 永遠以最後軸作為最內層走訪,無論哪個軸恰好較長。桌面版的 numpy 函式庫會默默重排其迴圈,把最長的軸放到最內層;相機則不會,因此桌面版 numpy 原本會掩蓋過去的配置選擇,在這裡仍會耗費時間。np.sum(m, axis=1) 會收合最後軸並沿連續方向執行;np.sum(m, axis=0) 則不會。當應用程式可以選擇如何配置緩衝區時,請把長軸放在最後,讓沿著它的運算停留在內層迴圈中。

如果一開始的配置就錯了,transpose()(或 .T 捷徑)可在不複製資料的情況下修正它——它只是交換步幅:

a = b.T            # now iterates fast

效能 有完整的效能討論。

6.5.4. reshape、transpose、切片——描述子編輯

任何只改寫描述子的運算都是免費的。reshape 會在同一個資料區塊上換上新的 shapestridestranspose 會反轉步幅。a[::2] 會把某個步幅加倍。每個都會傳回同一底層緩衝區的 視圖

任何必須走訪資料並寫入新緩衝區的運算則是複本。目前的規則是:描述子編輯是免費的,而資料走訪不是。

6.5.5. 關於 ndim 的說明

相機上的 numpy 在建置時所支援的最大 ndim 為 4。會產生更高階陣列的運算會引發 ValueError。絕大多數相機端的工作都是 1 維或 2 維,因此這個限制很少造成問題。