1. 程式人生 > >資料科學 IPython 筆記本 9.7 陣列上的計算:廣播

資料科學 IPython 筆記本 9.7 陣列上的計算:廣播

9.7 陣列上的計算:廣播

本節是《Python 資料科學手冊》(Python Data Science Handbook)的摘錄。

譯者:飛龍

協議:CC BY-NC-SA 4.0

我們在上一節中看到,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 中釋出的原始碼,經許可而使用)。

Broadcasting Visual

淺色方框代表廣播的值:同樣,這個額外的記憶體實際上並沒有在操作過程中分配,但是在概念上想象它是有用的。

廣播規則

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,) 
'''

注意這裡潛在的混淆:你可以想象使aM相容,比如在右邊填充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();

png

結果是引人注目的二維函式的圖形。