廣播 (Broadcasting)#

術語「廣播 (broadcasting)」描述 NumPy 在算術運算期間如何處理不同形狀的陣列。在特定約束條件下,較小的陣列會「廣播 (broadcast)」到較大的陣列上,使它們具有相容的形狀。廣播提供了一種向量化陣列運算的方法,使迴圈發生在 C 語言中而不是 Python 中。它在不製作不必要資料副本的情況下實現了這一點,並且通常能帶來高效的演算法實作。然而,在某些情況下,廣播並不是一個好主意,因為它會導致記憶體使用效率低下,從而減慢計算速度。

NumPy 運算通常在成對的陣列上逐元素進行。在最簡單的情況下,兩個陣列必須具有完全相同的形狀,如下例所示

>>> import numpy as np
>>> a = np.array([1.0, 2.0, 3.0])
>>> b = np.array([2.0, 2.0, 2.0])
>>> a * b
array([2.,  4.,  6.])

當陣列的形狀滿足某些約束條件時,NumPy 的廣播規則放寬了這個限制。最簡單的廣播範例發生在陣列和純量值在運算中結合時

>>> import numpy as np
>>> a = np.array([1.0, 2.0, 3.0])
>>> b = 2.0
>>> a * b
array([2.,  4.,  6.])

結果等同於先前的範例,其中 b 是一個陣列。我們可以認為純量 b 在算術運算期間被延展 成與 a 相同形狀的陣列。圖 1 中顯示的 b 中的新元素只是原始純量的副本。延展類比僅是概念性的。NumPy 非常聰明,可以直接使用原始純量值,而無需實際製作副本,從而使廣播運算盡可能地在記憶體和計算上都高效。

A scalar is broadcast to match the shape of the 1-d array it is being multiplied to.

圖 1#

在最簡單的廣播範例中,純量 b 被延展成與 a 相同形狀的陣列,因此形狀與逐元素乘法相容。

第二個範例中的程式碼比第一個範例更有效率,因為廣播在乘法運算期間移動的記憶體更少(b 是一個純量而不是陣列)。

通用廣播規則#

當對兩個陣列進行運算時,NumPy 會逐元素比較它們的形狀。它從尾部(即最右邊的)維度開始,然後向左進行。當滿足以下條件時,兩個維度是相容的:

  1. 它們相等,或者

  2. 其中一個是 1。

如果這些條件不滿足,則會拋出 ValueError: operands could not be broadcast together 異常,表示陣列具有不相容的形狀。

輸入陣列不需要具有相同的維度數量。結果陣列將具有與輸入陣列中維度數量最多的陣列相同的維度數量,其中每個維度的大小是輸入陣列中對應維度的最大大小。請注意,缺失的維度被假定為大小為 1。

例如,如果您有一個 256x256x3 的 RGB 值陣列,並且您想要按不同的值縮放圖像中的每種顏色,您可以將圖像乘以一個具有 3 個值的一維陣列。根據廣播規則對齊這些陣列的尾軸大小,顯示它們是相容的

Image  (3d array): 256 x 256 x 3
Scale  (1d array):             3
Result (3d array): 256 x 256 x 3

當比較的維度之一為 1 時,則使用另一個維度。換句話說,大小為 1 的維度會被延展或「複製」以匹配另一個維度。

在以下範例中,AB 陣列都具有長度為 1 的軸,這些軸在廣播運算期間被擴展到更大的尺寸

A      (4d array):  8 x 1 x 6 x 1
B      (3d array):      7 x 1 x 5
Result (4d array):  8 x 7 x 6 x 5

可廣播陣列#

如果上述規則產生有效結果,則一組陣列稱為「可廣播 (broadcastable)」到相同的形狀。

例如,如果 a.shape 是 (5,1),b.shape 是 (1,6),c.shape 是 (6,),且 d.shape 是 (),因此 d 是一個純量,則 abcd 都是可廣播到維度 (5,6) 的;並且

  • a 的行為就像一個 (5,6) 的陣列,其中 a[:,0] 被廣播到其他列,

  • b 的行為就像一個 (5,6) 的陣列,其中 b[0,:] 被廣播到其他行,

  • c 的行為就像一個 (1,6) 的陣列,因此就像一個 (5,6) 的陣列,其中 c[:] 被廣播到每一行,最後,

  • d 的行為就像一個 (5,6) 的陣列,其中單個值被重複。

以下是一些更多範例

A      (2d array):  5 x 4
B      (1d array):      1
Result (2d array):  5 x 4

A      (2d array):  5 x 4
B      (1d array):      4
Result (2d array):  5 x 4

A      (3d array):  15 x 3 x 5
B      (3d array):  15 x 1 x 5
Result (3d array):  15 x 3 x 5

A      (3d array):  15 x 3 x 5
B      (2d array):       3 x 5
Result (3d array):  15 x 3 x 5

A      (3d array):  15 x 3 x 5
B      (2d array):       3 x 1
Result (3d array):  15 x 3 x 5

以下是一些形狀無法廣播的範例

A      (1d array):  3
B      (1d array):  4 # trailing dimensions do not match

A      (2d array):      2 x 1
B      (3d array):  8 x 4 x 3 # second from last dimensions mismatched

當將一維陣列添加到二維陣列時的廣播範例

>>> import numpy as np
>>> a = np.array([[ 0.0,  0.0,  0.0],
...               [10.0, 10.0, 10.0],
...               [20.0, 20.0, 20.0],
...               [30.0, 30.0, 30.0]])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a + b
array([[  1.,   2.,   3.],
        [11.,  12.,  13.],
        [21.,  22.,  23.],
        [31.,  32.,  33.]])
>>> b = np.array([1.0, 2.0, 3.0, 4.0])
>>> a + b
Traceback (most recent call last):
ValueError: operands could not be broadcast together with shapes (4,3) (4,)

圖 2 所示,b 被添加到 a 的每一行。在 圖 3 中,由於形狀不相容,因此引發了例外。

A 1-d array with shape (3) is stretched to match the 2-d array of shape (4, 3) it is being added to, and the result is a 2-d array of shape (4, 3).

圖 2#

如果一維陣列的元素數量與二維陣列的列數相符,則將一維陣列添加到二維陣列會導致廣播。

A huge cross over the 2-d array of shape (4, 3) and the 1-d array of shape (4) shows that they can not be broadcast due to mismatch of shapes and thus produce no result.

圖 3#

當陣列的尾部維度不相等時,廣播會失敗,因為不可能將第一個陣列的行中的值與第二個陣列的元素對齊以進行逐元素加法。

廣播提供了一種方便的方式來取得兩個陣列的外積(或任何其他外部運算)。以下範例顯示了兩個一維陣列的外部加法運算

>>> import numpy as np
>>> a = np.array([0.0, 10.0, 20.0, 30.0])
>>> b = np.array([1.0, 2.0, 3.0])
>>> a[:, np.newaxis] + b
array([[ 1.,   2.,   3.],
       [11.,  12.,  13.],
       [21.,  22.,  23.],
       [31.,  32.,  33.]])
A 2-d array of shape (4, 1) and a 1-d array of shape (3) are stretched to match their shapes and produce a resultant array of shape (4, 3).

圖 4#

在某些情況下,廣播會延展兩個陣列以形成比任何一個初始陣列都大的輸出陣列。

此處,newaxis 索引運算子將新軸插入 a,使其成為二維的 4x1 陣列。將 4x1 陣列與形狀為 (3,)b 結合,會產生一個 4x3 陣列。

實際範例:向量量化#

廣播在現實世界的問題中經常出現。一個典型的範例發生在資訊理論、分類和其他相關領域中使用的向量量化 (VQ) 演算法中。VQ 中的基本運算是找到一組點(在 VQ 行話中稱為 codes)中最接近給定點(稱為 observation)的點。在下面顯示的非常簡單的二維情況中,observation 中的值描述了要分類的運動員的體重和身高。codes 代表不同類別的運動員。[1] 找到最接近的點需要計算 observation 與每個 code 之間的距離。最短的距離提供最佳匹配。在此範例中,codes[0] 是最接近的類別,表示該運動員很可能是籃球運動員。

>>> from numpy import array, argmin, sqrt, sum
>>> observation = array([111.0, 188.0])
>>> codes = array([[102.0, 203.0],
...                [132.0, 193.0],
...                [45.0, 155.0],
...                [57.0, 173.0]])
>>> diff = codes - observation    # the broadcast happens here
>>> dist = sqrt(sum(diff**2,axis=-1))
>>> argmin(dist)
0

在此範例中,observation 陣列被延展以匹配 codes 陣列的形狀

Observation      (1d array):      2
Codes            (2d array):  4 x 2
Diff             (2d array):  4 x 2
A height versus weight graph that shows data of a female gymnast, marathon runner, basketball player, football lineman and the athlete to be classified. Shortest distance is found between the basketball player and the athlete to be classified.

圖 5#

向量量化的基本運算是計算要分類的物件(深色方塊)與多個已知碼(灰色圓圈)之間的距離。在這個簡單的例子中,碼代表個別類別。更複雜的情況下,每個類別使用多個碼。

通常,大量 observations(可能從資料庫讀取)會與一組 codes 進行比較。考慮以下情境

Observation      (2d array):      10 x 3
Codes            (3d array):   5 x 1 x 3
Diff             (3d array):  5 x 10 x 3

三維陣列 diff 是廣播的結果,而不是計算的必要條件。大型資料集將產生一個大型的中間陣列,這在計算上是低效的。相反,如果使用圍繞上述二維範例中程式碼的 Python 迴圈單獨計算每個 observation,則會使用小得多的陣列。

廣播是編寫簡短且通常直觀的程式碼的強大工具,它可以在 C 語言中非常有效率地執行計算。但是,在某些情況下,廣播會為特定演算法使用不必要的大量記憶體。在這些情況下,最好在 Python 中編寫演算法的外部迴圈。這也可能產生更具可讀性的程式碼,因為隨著廣播中維度數量的增加,使用廣播的演算法往往會變得更難以理解。

腳註