使用 NumPy 在處理資料時,可以將資料看成一個整體進行運算,例如,可以對一組數字全部加 1:
import numpy as np
nums1 = np.array([1, 2, 3])
nums2 = nums1 + 10
從程式設計者的角度來看,nums
是一組數字,怎麼可以直接加上一個純量呢?然而,換個角度來想,
你是要對這組數字進行相同的操作,直接加 10,在表達上不是就夠了?
好吧!以上的說明文字,是從〈陣列程式設計〉中複製過來的,當時純綷是從程式碼的表述來談,那麼你有沒有想過,為什麼 NumPy 可以這麼做?
你可能會從純量計算的角度來看這件事,例如:
nums1 = [1, 2, 3]
nums2 = [n + 10 for n in nums1]
然而,NumPy 也可以將不同維度的陣列進行運算:
import numpy as np
a = np.arange(4).reshape(2, 2)
print(a + 10)
print(a + np.array([1, 3]))
print(a + np.array([[2, 4], [6, 8]]))
print(a + np.array([[10, 20]]))
print(a + np.array([[100]]))
print(a + np.array([200]))
想想看以上的計算各會什麼結果呢?而且並非各種維度都能混在一起計算,底下就會出錯:
import numpy as np
a = np.arange(4).reshape(2, 2)
print(a + np.array([1, 2, 3]))
錯誤訊息會是:
ValueError: operands could not be broadcast together with shapes (2,2) (3,)
從訊息中可以看到 broadcast 這個字眼,對於不同維度的資料,NumPy 有一套複雜的運算機制,稱為 Broadcasting,中文常翻為擴張,在〈Broadcasting〉可以看到擴張的四個規則,以及輸入陣列可擴張的(broadcastable)三個條件…呃…我相信第一次看的人,都會冒出一個念頭「這啥鬼?」
你有沒有注意到,〈Broadcasting〉的說明文件,是〈Universal functions〉的一部份呢?Universal functions 文件的一開頭就寫到:
A universal function (or ufunc for short) is a function that operates on ndarrays in an element-by-element fashion, supporting array broadcasting, type casting, and several other standard features.
理解擴張的出發點,其實就在 operates on ndarrays in an element-by-element fashion 這段,無論是幾維,形狀為何,Universal 函式底層關心的,都是單一元素如何處理,在〈NumPy 的 Universal 函式〉中,透過 frompyfunc
包裹普通的 Python 函式時,你指定的普通 Python 函式就是這麼做的。
先從簡單的開始好了,對於 a = np.array([1, 2, 3])
與 b = np.array([4, 5, 6])
,若透過 np.add
相加(也就是相當於 a + b
),np.add
這個 Universal 函式,會一對一地處理元素相加,這沒有問題!
如果是 a = np.array([1, 2, 3])
與 b = 1
呢?兩者維度不同,np.add
找出維度較大的陣列 a
,讓 b
的維度與之相同,你可以想像 b
被視為 np.array([1])
了。
現在維度相同了,而形狀不同,一個是 (3, )
,一個是 (1, )
,還無法讓 np.add
可以一對一處理,為了能一對一處理,只好在每取出 a
的一個元素時,b
的 1
就用一次,你可以想像 b
被視為 np.array([1, 1, 1])
了。
就方才的說明,a
的維度是 1,形狀為 (3, )
,b
原本維度是 0,形狀為 ()
(純量 n
可視為 np.array(n)
),計算過程就像是在低維度的陣列增加維度,形狀上從 ()
變為 (1, )
,然後改變形狀為 (3, )
,就使用者的角度來看,就像是 b
被擴充了:
不過這邊只說了「你可以想像 b
被視為…」,是因為實際上不會真的擴充為陣列,只是就使用者的角度來看會如上圖罷了。
如果是 a = np.array([1, 2, 3])
與 b = np.array([1])
呢?兩者維度相同,np.add
想要一對一處理時發現形狀不同,只好在每取出 a
的一個元素時,b
中唯一的元素 1
就用一次,你可以想像 b
被視為 np.array([1, 1, 1])
了:
就方才的說明,a
的維度是 1,形狀為 (3, )
,b
原本維度是 1,形狀為 (1, )
,然後改變形狀為 (3, )
。
接著使用二維陣列 a = np.array([[1, 2, 3], [4, 5, 6]])
,而 b = np.array([1])
,為了便於說明,就都用「想像」後的結果來討論了。首先,兩者維度不同,先增加 b
的維度,也就是視 b
相當於 np.array([[1]])
,就形狀而言,b
相當於從 (1, )
,變成了 (1, 1)
,也就是新維度的軸(axis 0)先設 1(下圖加上陰影表示二維陣列):
維度相同了,np.add
接著想逐一處理元素,然而形狀一個是 (2, 3)
,一個是 (1, 1)
,首先處理 axis 0,令其成為 (2, 1)
:
接著處理 axis 1,形狀成為 (2, 3)
:
如果是 a = np.array([[1, 2, 3], [4, 5, 6]])
,而 b = np.array([[7, 8, 9]])
呢?首先,兩者維度相同,然而形狀一個是 (2, 3)
,一個是 (1, 3)
,首先處理 axis 0,令其成為 (2, 3)
:
接著就可以逐一運算元素了,可以發現,在形狀不同時,都是依 axis 0、axis 1 這樣的順序來處理。
如果是 a = np.array([[1, 2, 3], [4, 5, 6]])
,而 b = np.array([[7], [8]])
呢?首先,兩者維度相同,然而形狀一個是 (2, 3)
,一個是 (2, 1)
,axis 0 相同,因此處理 axis 1:
如果是 a = np.array([[1, 2, 3], [4, 5, 6]])
,而 b = np.array([[7], [8], [9]])
呢?兩者維度相同,然而形狀一個是 (2, 3)
,一個是 (3, 1)
,從 axis 0 開始,NumPy 就不知道怎麼調整為相同的形狀,這時你就會看到 could not be broadcast 的錯誤了:
簡單來說,從低維度的軸開始調整形狀時,若不是 1,或相同大小,擴張機制就無法運作,這樣的概念可以擴充至更多維度,記得,理解擴張的出發點,其實就在〈Universal functions〉中的 operates on ndarrays in an element-by-element fashion 這段,而〈Broadcasting〉中談到的規則,可以說是歸納後的結論。
來歸納一下我自己覺得容易判斷的方式:
- 記得 Universal 函式是逐元素處理
- 將低維度的陣列維度增加(形狀前加 1,直到維度相同)
- 從低維度的軸開始調整形狀,必須是 1 或相同大小