NumPy 擴張機制(二)


在〈NumPy 擴張機制(一)〉中談到了擴張機制的基本原理,對於使用者而言,運用擴張機制時以直覺易懂容易撰寫就可以了,例如,底下都是很直覺的寫法:

import numpy as np

a = np.array([1, 2, 3]) + 1
b = np.array([[1, 2, 3], [4, 5, 6]]) + 1
c = np.array([[1, 2, 3], [4, 5, 6]]) + np.array([1, 2, 3])
d = np.array([[1, 2, 3], [4, 5, 6]]) + np.array([[1], [2]])
e = np.arange(3 ** 3).reshape(3, 3, 3) + np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

以上運算時,擴張的方式應該不難想像,例如,下圖中右側紅色是原來的左運元,白色是運算時必須擴充的部份:

NumPy 擴張機制(二)

有些場合的底層也運用了擴張,例如〈NumPy 陣列索引〉中談到的 np.ix_,實際上不透過 np.ix_,也可以自行透過陣列索引指定,得到叉積的結果,其實就使用者而言,沒必要探究這該怎麼做,以下純綷個人研究…

先複習一下〈NumPy 陣列索引〉談到的,NumPy 的 [] 可以指定索引陣列:

>>> a = np.arange(1, 6)
>>> a[[0, 1, 4]]
array([1, 2, 5])
>>>

NumPy 陣列索引〉也談到,多維陣列時,可以有多個索引陣列,使用逗號區隔,表示 axis 的分隔:

>>> a = np.arange(25).reshape((5, 5))
>>> a[[0, 1, 4], [0, 3, 4]]
array([ 0,  8, 24])
>>>

[0, 1, 4], [0, 3, 4] 其實是一組彼此搭配的索引陣列,然而可能跟純量索引或範圍的指定混淆,誤以為是 [0, 1, 4][0, 3, 4] 的交叉(叉積),為了避免混淆,可以加上括號建立 tuple

>>> a[([0, 1, 4], [0, 3, 4])] # 相當於 a[tuple([[0, 1, 4], [0, 3, 4]])]
array([ 0,  8, 24])
>>>

因此對於:

>>> a = np.arange(1, 6)
>>> a[[0, 1, 4]]
array([1, 2, 5])
>>>

若要更清楚表示,可以寫為…

>>> idx_arr = ([0, 1, 4], )
>>> a[idx_arr]
array([1, 2, 5])
>>>

(idxarr1, idxarr2, ...) 提供的資料,其實會用來計算出最後的索引陣列,它的形狀,決定了輸出陣列的形狀,而其中的元素,必須作為索引存取來源陣列。

([0, 1, 4], ) 而言,最後的索引陣列當然是 [0, 1, 4],形狀是 (3, ),因而最後輸出陣列形狀會是 (3, ),而索引陣列元素 0、1、4,可以作為索引存取 a,結果就是 [1, 2, 5],這沒有問題,當然,你也可以寫 a[[0, 1, 2, 3, 3, 2, 1]],這會得到 [1 2 3 4 4 3 2]

如果將 idx_arr([[0, 1, 4]], ) 呢?索引陣列的形狀是 (1, 3),而索引陣列元素 0、1、4,取出了 a[0]a[1]a[4],最後結果就是 [[a[0], a[1], a[4]]],也就是 [[1 2 5]]

類似地,如果 idx_arr([[0, 1, 2], [1, 3, 4], [2, 3, 1]], ),最後的輸出陣列就會是 [[a[0], a[1], 2], [a[1], a[3], a[4]], [a[2], a[3], a[1]]],也就是得到:

[[1 2 3]
 [2 4 5]
 [3 4 2]]

方才談到,(idxarr1, idxarr2, ...) 提供的資料,會用來計算出最後的索引陣列,其中的元素,必須作為索引存取來源陣列,也就是說,如果你提供了 n 個索引陣列,你的陣列來源就是必須是 n 維,例如,在二維陣列時,可以使用兩個索引陣列:

>>> a = np.arange(25).reshape((5, 5))
>>> idx_arr = ([0, 1, 4], [0, 3, 4])
>>> a[idx_arr]
array([ 0,  8, 24])
>>>

最後的索引陣列相當於 [[0, 0], [1, 3], [4, 4]](類似 zip 的結果),結果就是 [a[0, 0], a[1, 3], a[4, 4]],也就是 [0, 8, 24]

有沒有辦法用 (idxarr1, idxarr2) 取得叉積呢?那麼,計算後的索引陣列最後的形狀,必須是二維才行,我們一步一步來,首先:

import numpy as np

a = np.arange(25).reshape((5, 5))
idx_arr = ([0, 1, 4], [0])
print(a[idx_arr])

提供的兩個索引陣列形狀不同,這時擴張機制運作了,第二個陣列 [0] 變成了 [0, 0, 0],結果得到 [[0, 0], [1, 0], [4, 0]],最後結果是 [0, 5, 20]

相對地,底下提供了 ([0], [0, 3, 4])

import numpy as np

a = np.arange(25).reshape((5, 5))
idx_arr = ([0], [0, 3, 4])
print(a[idx_arr])

第一個陣列 [0] 擴張後變成了 [0, 0, 0],結果得到了 [[0, 0], [0, 3], [0, 4]],結果就是 [0, 3, 4]

如果是這個呢?

import numpy as np

a = np.arange(25).reshape((5, 5))
idx_arr = ([[0]], [0, 3, 4])
print(a[idx_arr])

[[0]] 的形狀是 (1, 1)[0, 3, 4] 形狀為 (3,),後者擴張成為 (1, 3),也就是 [[0, 3, 4]],現在維度相同了,處理同一層的部份,也就是 [0][0, 3, 4],依上頭談到的,結果得到了 [[0, 0], [0, 3], [0, 4]],也就是最後計算出來的索引陣列會是 [[[0, 0], [0, 3], [0, 4]]],最後輸出的陣最形狀會是 (1, 3),相當於 [[a[0, 0], a[1, 3], a[4, 4]]],也就是 [[0 3 4]]

方才範例的 [0, 3, 4] 擴張後會成為 [[0, 3, 4]],因此如下直接寫 [[0, 3, 4]] 也可以:

import numpy as np

a = np.arange(25).reshape((5, 5))
idx_arr = ([[0]], [[0, 3, 4]])
print(a[idx_arr])

既然如此,那如果寫以下,不就可以得到叉積的結果?

import numpy as np

a = np.arange(25).reshape((5, 5))
idx_arr = ([[0], [1], [4]], [[0, 3, 4]])
print(a[idx_arr])

最後,[0] 會與 [0, 3, 4] 計算索引陣列得到 [[0, 0], [0, 3], [0, 4]][1] 會與 [0, 3, 4] 計算索引陣列得到 [[1, 0], [1, 3], [1, 4]][4] 會與 [0, 3, 4] 計算索引陣列得到 [[4, 0], [4, 3], [4, 4]],最後得到的索引陣列就是:

[
    [[0, 0], [0, 3], [0, 4]],
    [[1, 0], [1, 3], [1, 4]],
    [[4, 0], [4, 3], [4, 4]]
]

最內層的 [0, 0] 等會用來存取來源陣列,因而得到的才會是叉積的結果,觀察一下以上的過程,若指定了 [a, b, c][e, f, g],將之轉換為 [[a], [b], [c]], [[e, f, g]] 不就好了?這就是 np.ix_ 做的事囉!

import numpy as np
print(np.ix_([0, 1, 4], [0, 3, 4]))

上例會顯示:

(array([[0],
       [1],
       [4]]), array([[0, 3, 4]]))

這純綷就是探究陣列索引、擴張機制與 np.ix_ 之間的關係罷了,就叉積而言,直接使用 np.ix_ 就可以了,當然,若能掌握陣列索引、擴張機制,就可以依需求來處理資料,太複雜的陣列索引、擴張機制,就封裝個函式,取個好名稱以方便使用。