NEP 20 — 廣義通用函數簽名擴展#

作者:

Marten van Kerkwijk <mhvk@astro.utoronto.ca>

狀態:

最終

類型:

標準追蹤

建立於:

2018-06-10

決議:

https://mail.python.org/pipermail/numpy-discussion/2018-April/077959.html, https://mail.python.org/pipermail/numpy-discussion/2018-May/078078.html

注意

添加固定 (i) 和彈性 (ii) 維度的提案已獲接受,而添加可廣播 (iii) 維度的提案則被延遲。

摘要#

廣義通用函數,顧名思義,是通用函數的推廣:它們對非純量元素進行操作。它們的簽名描述了它們操作的元素的結構,名稱連結了應相同的運算元的維度。此處,提案擴展簽名以允許簽名指示維度 (i) 具有固定大小;(ii) 可以不存在;以及 (iii) 可以廣播。

詳細描述#

提案的每個部分都由特定需求驅動 [1]

  1. 固定大小的維度。處理空間向量的程式碼通常明確用於 2 維或 3 維空間(例如,來自 Standards Of Fundamental Astronomy 的程式碼,作者希望使用 gufuncs 為 astropy 包裝 [2])。簽名應能夠指示這一點。例如,將極角轉換為二維笛卡爾單位向量的函數的簽名目前必須是 ()->(n),但無法指示 n 必須等於 2。實際上,此簽名特別令人惱火,因為如果不放入輸出引數,目前的 gufunc 包裝器程式碼會失敗,因為它無法確定 n。同樣地,兩個 3 維向量的叉積的簽名必須是 (n),(n)->(n),同樣無法指示 n 必須等於 3。因此,此處的提案允許除了變數名稱外,還給出數值。因此,角度到二維單位向量將為 ()->(2);兩個角度到三維單位向量 (),()->(3);而兩個三維向量的叉積將為 (3),(3)->(3)

  2. 可能遺失的維度。這部分幾乎完全由希望在 gufunc 中包裝 matmul 驅動。matmul 代表矩陣乘法,如果它只做那件事,可以用簽名 (m,n),(n,p)->(m,p) 涵蓋。但是,當維度遺失時,它有特殊情況,允許將任一引數視為單一向量,因此函數有效地變為向量-矩陣、矩陣-向量或向量-向量乘法(但沒有廣播)。為了支援這一點,建議允許在維度名稱後綴問號,以指示該維度不一定需要存在。

    透過此新增功能,matmul 的簽名可以表示為 (m?,n),(n,p?)->(m?,p?)。這表示,例如,如果第二個運算元只有一個維度,為了基本函數的目的,它將被視為輸入具有核心形狀 (n, 1),並且輸出具有對應的核心形狀 (m, 1)。然而,實際的輸出陣列會移除彈性維度,即它將具有形狀 (..., m)。同樣地,如果兩個引數都只有單一維度,則輸入將呈現為具有形狀 (1, n)(n, 1) 到基本函數,而輸出為 (1, 1),而傳回的實際輸出陣列將具有形狀 ()。透過這種方式,簽名允許對四個相關但不同的簽名使用單一基本函數,(m,n),(n,p)->(m,p)(n),(n,p)->(p)(m,n),(n)->(m)(n),(n)->()

  3. 可以廣播的維度。對於某些應用,運算元之間的廣播是有意義的。例如,比較陣列中向量的 all_equal 函數可以具有簽名 (n),(n)->(),但這會強制兩個運算元都必須是陣列,但檢查向量的所有部分是否為常數(可能為零)也很有用。提案是允許 gufunc 的實作者指示維度可以透過在維度名稱後綴 |1 來廣播。因此,all_equal 的簽名將變為 (n|1),(n|1)->()。簽名似乎更普遍地適用於「鏈式 ufunc」;例如,另一個應用可能是在實現 sumproduct 的假想 ufunc 中。

    討論中出現的另一個範例是加權平均值,它可能看起來像 weighted_mean(y, sigma[, axis, ...]),傳回平均值及其不確定性。使用 (n),(n)->(),() 的簽名,將被迫始終提供與資料點數量一樣多的 sigma,而廣播將允許為所有點提供單一 sigma(這對於計算平均值的不確定性仍然有用)。

實作#

提出的變更都已實作 [3], [4], [5]。這些 PR 使用兩個新欄位擴展了 ufunc 結構,每個欄位的大小都等於不同維度的數量,其中 core_dim_sizes 保留可能固定的尺寸,而 core_dim_flags 保留標誌,指示維度是否可以遺失或廣播。為了確保我們可以區分這個新版本和以前的版本,未使用的條目 reserved1 被重新用作版本號碼。

在實作中,謹慎地確保對基本函數標記的維度與未標記的維度沒有任何不同之處:例如,固定大小維度的尺寸仍然傳遞到基本函數(但迴圈現在可以依賴於該尺寸等於簽名中給定的固定尺寸)。

待決定的實作細節是,是否有一個所有標誌的摘要會很方便。這可能會儲存在 core_enabled 中(目前是一個布林值),非零值繼續指示 gufunc,但特定標誌指示 gufunc 是否使用固定、彈性或可廣播的維度。

透過以上內容,語法的正式定義將變為 [4]

<Signature>            ::= <Input arguments> "->" <Output arguments>
<Input arguments>      ::= <Argument list>
<Output arguments>     ::= <Argument list>
<Argument list>        ::= nil | <Argument> | <Argument> "," <Argument list>
<Argument>             ::= "(" <Core dimension list> ")"
<Core dimension list>  ::= nil | <Core dimension> |
                           <Core dimension> "," <Core dimension list>
<Core dimension>       ::= <Dimension name> <Dimension modifier>
<Dimension name>       ::= valid Python variable name | valid integer
<Dimension modifier>   ::= nil | "|1" | "?"
  1. 所有引號都是為了清楚起見。

  2. 共享相同名稱的未修改核心維度必須具有相同的大小。每個維度名稱通常對應於基本函數實作中的一個迴圈層級。

  3. 空白字元會被忽略。

  4. 整數作為維度名稱會將該維度凍結為該值。

  5. 如果名稱後綴 |1 修飾符,則允許針對具有相同名稱的其他維度進行廣播。所有輸入維度都必須共享此修飾符,而沒有輸出維度應具有它。

  6. 如果名稱後綴 ? 修飾符,則維度僅在它存在於共享它的所有輸入和輸出上時才是核心維度;否則它會被忽略(並由基本函數的大小為 1 的維度取代)。

簽名範例 [4]

簽名

可能用途

(),()->()

加法

(i)->()

對最後一個軸求和

(i|1),(i|1)->()

沿軸測試相等性,允許與純量比較

(i),(i)->()

向量內積

(m,n),(n,p)->(m,p)

矩陣乘法

(n),(n,p)->(p)

向量-矩陣乘法

(m,n),(n)->(m)

矩陣-向量乘法

(m?,n),(n,p?)->(m?,p?)

以上所有四種情況一次處理,但向量不能有迴圈維度 (例如,像 matmul)

(3),(3)->(3)

3 維向量的叉積

(i,t),(j,t)->(i,j)

最後一個維度內積,倒數第二個維度外積,其餘維度迴圈/廣播。

向後相容性#

一個可能的擔憂是 ufunc 結構的變更。對於大多數呼叫 PyUFunc_FromDataAndSignature 的應用程式來說,這是完全透明的。此外,透過將 reserved1 重新用作版本號碼,針對舊版本 numpy 編譯的程式碼將繼續運作(儘管在使用較新版本的 numpy 匯入該程式碼時會收到警告),除非程式碼明確變更了 reserved1 條目。

替代方案#

有人建議不要擴展簽名,而是進行多重調度,這樣,例如,matmul 將簡單地具有它支援的多個簽名,即,而不是 (m?,n),(n,p?)->(m?,p?),您將擁有 (m,n),(n,p)->(m,p) | (n),(n,p)->(p) | (m,n),(n)->(m) | (n),(n)->()。這樣做的缺點是開發人員現在必須確保基本函數可以處理這些不同的簽名。此外,擴展很快變得繁瑣。例如,對於 all_equal 的簽名 (n|1),(n|1)->(),您必須有五個條目:(n),(n)->() | (n),(1)->() | (1),(n)->() | (n),()->() | (),(n)->()。對於像 (m|1,n|1,o|1),(m|1,n|1,o|1)->() 這樣的簽名(來自 [4] 中的 cube_equal 測試案例),甚至不值得寫出擴展。

對於廣播,建議使用替代後綴 ^(因為廣播可以被認為是增加陣列的大小)。這似乎不太清楚。此外,有人想知道它是否不應該只是一個全有或全無的標誌。情況可能是這樣,但考慮到彈性維度的後綴,可以說另一個後綴更清楚(實作也是如此)。

討論#

此處的提案在郵件清單 [6], [7] 上進行了相當長時間的討論。爭議的主要點在於用例是否足夠強大。特別是,對於凍結的維度,有人認為可以將對正確數量的檢查放在迴圈選擇程式碼中。對於沒有好處的情況,這似乎不太清楚。

對於廣播,有人指出缺乏可能需要它的基本函數的範例,並質疑像 all_equal 這樣的東西是否最好使用 gufunc 而不是作為 np.equal 的特殊方法來完成。對此的一個反駁論點是,有一個實際的 all_equal PR [8]。另一個論點是,即使要使用方法,最好也能夠表達它們的簽名(至少對於 reduceaccumulate 來說是可能的)。

最後一個論點是我們使 gufunc 變得太複雜了。對於可以省略的維度來說,這可以說是成立的,但這也具有最強大的用例。凍結的維度具有非常簡單的實作,並且其含義是顯而易見的。一旦支援彈性維度,廣播的能力也很簡單。

參考文獻和腳註#