資料科學 IPython 筆記本 9.7 陣列上的計算:廣播
9.7 陣列上的計算:廣播
本節是《Python 資料科學手冊》(Python Data Science Handbook)的摘錄。
譯者:飛龍
我們在上一節中看到,NumPy 的通用函式如何用於向量化操作,從而消除緩慢的 Python 迴圈。向量化操作的另一種方法是使用 NumPy 的廣播功能。廣播只是一組規則,用於在不同大小的陣列上應用二元ufunc
(例如,加法,減法,乘法等)。
廣播簡介
回想一下,對於相同大小的陣列,二元操作是逐元素執行的:
import numpy as np
a = np. array([0, 1, 2])
b = np.array([5, 5, 5])
a + b
# array([5, 6, 7])
廣播允許在不同大小的陣列上執行這類二元操作 - 例如,我們可以輕鬆將陣列和標量相加(將其視為零維陣列):
a + 5
# array([5, 6, 7])
我們可以將此視為一個操作,將值5
拉伸或複製為陣列[5,5,5]
,並將結果相加。
NumPy 廣播的優勢在於,這種值的重複實際上並沒有發生,但是當我們考慮廣播時,它是一種有用的心理模型。
我們可以類似地,將其擴充套件到更高維度的陣列。 將兩個二維陣列相加時觀察結果:
M = np.ones( (3, 3))
M
'''
array([[ 1., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.]])
'''
M + a
'''
array([[ 1., 2., 3.],
[ 1., 2., 3.],
[ 1., 2., 3.]])
'''
這裡,一維陣列a
被拉伸,或者在第二維上廣播,來匹配M
的形狀。
雖然這些示例相對容易理解,但更復雜的情況可能涉及兩個陣列的廣播。請考慮以下示例:
a = np.arange(3)
b = np.arange(3)[:, np. newaxis]
print(a)
print(b)
'''
[0 1 2]
[[0]
[1]
[2]]
'''
a + b
'''
array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4]])
'''
就像之前我們拉伸或廣播一個值來匹配另一個的形狀,這裡我們拉伸a```和
b``來匹配一個共同的形狀,結果是二維陣列!
這些示例的幾何圖形為下圖(產生此圖的程式碼可以在“附錄”中找到,並改編自 astroML 中釋出的原始碼,經許可而使用)。
淺色方框代表廣播的值:同樣,這個額外的記憶體實際上並沒有在操作過程中分配,但是在概念上想象它是有用的。
廣播規則
NumPy 中的廣播遵循一套嚴格的規則來確定兩個陣列之間的互動:
- 規則 1:如果兩個陣列的維數不同,則維數較少的陣列的形狀,將在其左側填充。
- 規則 2:如果兩個陣列的形狀在任何維度上都不匹配,則該維度中形狀等於 1 的陣列將被拉伸來匹配其他形狀。
- 規則 3:如果在任何維度中,大小不一致且都不等於 1,則會引發錯誤。
為了講清楚這些規則,讓我們詳細考慮幾個例子。
廣播示例 1
讓我們看一下將二維陣列和一維陣列相加:
M = np.ones((2, 3))
a = np.arange(3)
讓我們考慮這兩個陣列上的操作。陣列的形狀是。
M.shape = (2, 3)
a.shape = (3,)
我們在規則 1 中看到陣列a
的維數較少,所以我們在左邊填充它:
M.shape -> (2, 3)
a.shape -> (1, 3)
根據規則 2,我們現在看到第一個維度不一致,因此我們將此維度拉伸來匹配:
M.shape -> (2, 3)
a.shape -> (2, 3)
形狀匹配了,我們看到最終的形狀將是(2, 3)
M + a
'''
array([[ 1., 2., 3.],
[ 1., 2., 3.]])
'''
廣播示例 2
我們來看一個需要廣播兩個陣列的例子:
a = np.arange(3).reshape((3, 1))
b = np.arange(3)
同樣,我們將首先寫出陣列的形狀:
a.shape = (3, 1)
b.shape = (3,)
規則 1 說我們必須填充b
的形狀:
a.shape -> (3, 1)
b.shape -> (1, 3)
規則 2 告訴我們,我們更新這些中的每一個,來匹配另一個數組的相應大小:
a.shape -> (3, 3)
b.shape -> (3, 3)
因為結果匹配,所以這些形狀是相容的。我們在這裡可以看到:
a + b
'''
array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4]])
'''
廣播示例 3
現在讓我們來看一個兩個陣列不相容的例子:
M = np.ones((3, 2))
a = np.arange(3)
這與第一個例子略有不同:矩陣M
是轉置的。這對計算有何影響?陣列的形狀是
M.shape = (3, 2)
a.shape = (3,)
同樣,規則 1 告訴我們必須填充a
的形狀:
M.shape -> (3, 2)
a.shape -> (1, 3)
根據規則 2,a
的第一個維度被拉伸來匹配M
:
M.shape -> (3, 2)
a.shape -> (3, 3)
現在我們到了規則 3 - 最終的形狀不匹配,所以這兩個陣列是不相容的,正如我們可以通過嘗試此操作來觀察:
M + a
'''
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-13-9e16e9f98da6> in <module>()
----> 1 M + a
ValueError: operands could not be broadcast together with shapes (3,2) (3,)
'''
注意這裡潛在的混淆:你可以想象使a
和M
相容,比如在右邊填充a
的形狀,而不是在左邊。但這不是廣播規則的運作方式!
在某些情況下,這種靈活性可能會有用,但這會導致潛在的二義性。如果在右側填充是你想要的,你可以通過陣列的形狀調整,來明確地執行此操作(我們將使用“NumPy 陣列基礎”中介紹的np.newaxis
關鍵字):
a[:, np.newaxis].shape
# (3, 1)
M + a[:, np.newaxis]
'''
array([[ 1., 1.],
[ 2., 2.],
[ 3., 3.]])
'''
還要注意,雖然我們一直專注於+
運算子,但這些廣播規則適用於任何二元ufunc
。
例如,這裡是logaddexp(a, b)
函式,它比原始方法更精確地計算log(exp(a) + exp(b))
:
np.logaddexp(M, a[:, np.newaxis])
'''
array([[ 1.31326169, 1.31326169],
[ 1.69314718, 1.69314718],
[ 2.31326169, 2.31326169]])
'''
對於可用的通用函式的更多資訊,請參閱“NumPy 陣列上的計算:通用函式”。
實戰中的廣播
廣播操作是我們將在本書中看到的許多例子的核心。我們現在來看一些它們可能有用的簡單示例。
陣列中心化
在上一節中,我們看到ufunc
允許 NumPy 使用者不再需要顯式編寫慢速 Python 迴圈。廣播擴充套件了這種能力。一個常見的例子是資料陣列的中心化。
想象一下,你有一組 10 個觀測值,每個觀測值由 3 個值組成。使用標準約定(參見“Scikit-Learn 中的資料表示”),我們將其儲存在10x3
陣列中:
X = np.random.random((10, 3))
我們可以使用第一維上的“均值”聚合,來計算每個特徵的平均值:
Xmean = X.mean(0)
Xmean
# array([ 0.53514715, 0.66567217, 0.44385899])
現在我們可以通過減去均值(這是一個廣播操作)來中心化X
陣列:
X_centered = X - Xmean
要仔細檢查我們是否已正確完成此操作,我們可以檢查中心化的陣列是否擁有接近零的均值:
X_centered.mean(0)
# array([ 2.22044605e-17, -7.77156117e-17, -1.66533454e-17])
在機器精度範圍內,平均值現在為零。
繪製二維函式
廣播非常有用的一個地方是基於二維函式展示影象。如果我們想要定義一個函式z = f(x, y)
,廣播可用於在網格中計算函式:
# x 和 y 是從 0 到 5 的 50 步
x = np.linspace(0, 5, 50)
y = np.linspace(0, 5, 50)[:, np.newaxis]
z = np.sin(x) ** 10 + np.cos(10 + y * x) * np.cos(x)
我們將使用 Matplotlib 繪製這個二維陣列(這些工具將在“密度和等高線圖”中完整討論):
%matplotlib inline
import matplotlib.pyplot as plt
plt.imshow(z, origin='lower', extent=[0, 5, 0, 5],
cmap='viridis')
plt.colorbar();
結果是引人注目的二維函式的圖形。