廣義通用函數 API#

不僅對純量函數,也對向量(或陣列)函數進行迴圈的需求普遍存在。NumPy 透過推廣通用函數 (ufuncs) 來實現此概念。在常規 ufuncs 中,基本函數僅限於逐元素操作,而廣義版本 (gufuncs) 支援「子陣列」對「子陣列」操作。Perl 向量函式庫 PDL 提供了類似的功能,以下重複使用其術語。

每個廣義 ufunc 都具有與之相關聯的資訊,說明輸入的「核心」維度,以及輸出的相應維度(逐元素 ufuncs 具有零核心維度)。所有引數的核心維度列表稱為 ufunc 的「簽名」。例如,ufunc numpy.add 的簽名為 (),()->(),定義了兩個純量輸入和一個純量輸出。

另一個範例是函數 inner1d(a, b),其簽名為 (i),(i)->()。這沿著每個輸入的最後一個軸應用內積,但保持剩餘索引不變。例如,當 a 的形狀為 (3, 5, N),而 b 的形狀為 (5, N) 時,這將傳回形狀為 (3,5) 的輸出。底層的基本函數被呼叫 3 * 5 次。在簽名中,我們為每個輸入指定一個核心維度 (i),為輸出指定零個核心維度 (),因為它接受兩個 1 維陣列並傳回一個純量。透過使用相同的名稱 i,我們指定兩個對應的維度應具有相同的大小。

超出核心維度的維度稱為「迴圈」維度。在上面的範例中,這對應於 (3, 5)

簽名決定了每個輸入/輸出陣列的維度如何拆分為核心維度和迴圈維度

  1. 簽名中的每個維度都與對應傳入陣列的維度相符,從形狀元組的末尾開始。這些是核心維度,它們必須存在於陣列中,否則會引發錯誤。

  2. 在簽名中分配給相同標籤的核心維度(例如,inner1d(i),(i)->() 中的 i)必須具有完全匹配的大小,不執行廣播。

  3. 核心維度從所有輸入中移除,剩餘維度廣播在一起,定義迴圈維度。

  4. 每個輸出的形狀由迴圈維度加上輸出的核心維度決定

通常,輸出中所有核心維度的大小將由輸入陣列中具有相同標籤的核心維度的大小決定。這不是必要條件,並且可以定義簽名,其中標籤首次出現在輸出中,儘管在呼叫此類函數時必須採取一些預防措施。一個範例是函數 euclidean_pdist(a),其簽名為 (n,d)->(p),給定一個 nd 維向量的陣列,計算它們之間所有唯一的成對歐幾里得距離。因此,輸出維度 p 必須等於 n * (n - 1) / 2,但預設情況下,呼叫者有責任傳入大小正確的輸出陣列。如果無法從傳入的輸入或輸出陣列確定輸出的核心維度的大小,則會引發錯誤。可以透過定義 PyUFunc_ProcessCoreDimsFunc 函數並將其分配給 PyUFuncObject 結構的 proces_core_dims_func 欄位來變更此行為。請參閱下文以瞭解更多詳細資訊。

注意:在 NumPy 1.10.0 之前,檢查較不嚴格:遺失的核心維度透過在形狀前面加上 1 來建立,具有相同標籤的核心維度廣播在一起,未確定的維度以大小 1 建立。

定義#

基本函數

每個 ufunc 都由一個基本函數組成,該函數對陣列引數的最小部分執行最基本的操作(例如,將兩個數字相加是將兩個陣列相加的最基本操作)。ufunc 在陣列的不同部分多次應用基本函數。基本函數的輸入/輸出可以是向量;例如,inner1d 的基本函數將兩個向量作為輸入。

簽名

簽名是一個字串,描述 ufunc 的基本函數的輸入/輸出維度。有關更多詳細資訊,請參閱以下章節。

核心維度

基本函數的每個輸入/輸出的維度由其核心維度定義(零核心維度對應於純量輸入/輸出)。核心維度對應到輸入/輸出陣列的最後幾個維度。

維度名稱

維度名稱表示簽名中的核心維度。不同的維度可以共用一個名稱,表示它們的大小相同。

維度索引

維度索引是一個整數,表示維度名稱。它根據每個名稱在簽名中首次出現的順序枚舉維度名稱。

簽名詳細資訊#

簽名定義了輸入和輸出變數的「核心」維度,並因此定義了維度的縮併。簽名由以下格式的字串表示

  • 每個輸入或輸出陣列的核心維度由括號中的維度名稱列表表示,(i_1,...,i_N);純量輸入/輸出由 () 表示。可以使用任何有效的 Python 變數名稱來代替 i_1i_2 等。

  • 不同引數的維度列表以 "," 分隔。輸入/輸出引數以 "->" 分隔。

  • 如果在多個位置使用相同的維度名稱,則強制對應維度的大小相同。

簽名的正式語法如下

<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. 所有引號僅為清楚起見。

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

  3. 空白字元會被忽略。

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

  5. 如果名稱帶有「?」修飾符,則該維度僅在所有共用它的輸入和輸出上都存在時才為核心維度;否則它將被忽略(並替換為基本函數的大小為 1 的維度)。

以下是一些簽名的範例

名稱

簽名

常用用法

add

(),()->()

二元 ufunc

sum1d

(i)->()

縮減

inner1d

(i),(i)->()

向量-向量乘法

matmat

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

矩陣乘法

vecmat

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

向量-矩陣乘法

matvec

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

矩陣-向量乘法

matmul

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

以上四者的組合

outer_inner

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

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

cross1d

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

(3)->()

叉積,其中最後一個維度被凍結且必須為 3

最後一個是凍結核心維度的實例,可用於提高 ufunc 效能

用於實作基本函數的 C-API#

目前的介面保持不變,並且 PyUFunc_FromFuncAndData 仍然可以用於實作(專門的)ufuncs,由純量基本函數組成。

可以使用 PyUFunc_FromFuncAndDataAndSignature 來宣告更通用的 ufunc。PyUFunc_FromFuncAndData 的引數列表相同,但額外增加了一個引數,用於將簽名指定為 C 字串。

此外,回呼函數的型別與以前相同,void (*foo)(char **args, intp *dimensions, intp *steps, void *func)。調用時,args 是長度為 nargs 的列表,其中包含所有輸入/輸出引數的資料。對於純量基本函數,steps 的長度也為 nargs,表示用於引數的步幅。dimensions 是指向單個整數的指標,該整數定義要迴圈的軸的大小。

對於非平凡簽名,dimensions 也將包含核心維度的大小,從第二個條目開始。每個唯一維度名稱僅提供一個大小,並且大小根據維度名稱在簽名中首次出現的順序給出。

steps 的前 nargs 個元素與純量 ufuncs 保持不變。以下元素依序包含所有引數的所有核心維度的步幅。

例如,考慮簽名為 (i,j),(i)->() 的 ufunc。在這種情況下,args 將包含三個指向輸入/輸出陣列 abc 資料的指標。此外,dimensions 將為 [N, I, J],以定義迴圈的大小 N 和核心維度 ij 的大小 IJ。最後,steps 將為 [a_N, b_N, c_N, a_i, a_j, b_i],其中包含所有必要的步幅。

自訂核心維度大小處理#

  • 類型為 PyUFunc_ProcessCoreDimsFunc 的選用函數儲存在 ufunc 的 process_core_dims_func 屬性上,為 ufunc 的作者提供了一個「hook」,用於處理傳遞給 ufunc 的陣列的核心維度。此「hook」的兩個主要用途是

  • 檢查 ufunc 所需的核心維度約束是否滿足(如果未滿足,則設定例外狀況)。

計算任何未由輸入陣列確定的輸出核心維度的輸出形狀。

int minmax_process_core_dims(PyUFuncObject ufunc,
                             npy_intp *core_dim_sizes)
{
    npy_intp n = core_dim_sizes[0];
    if (n == 0) {
        PyExc_SetString("minmax requires the core dimension "
                        "to be at least 1.");
        return -1;
    }
    return 0;
}

作為第一個用途的範例,請考慮簽名為 (n)->(2) 的廣義 ufunc minmax,它同時計算序列的最小值和最大值。它應要求 n > 0,因為長度為 0 的序列的最小值和最大值沒有意義。在這種情況下,ufunc 作者可能會像這樣定義函數

在這種情況下,陣列 core_dim_sizes 的長度將為 2。陣列中的第二個值將始終為 2,因此函數無需檢查它。核心維度 n 儲存在第一個元素中。如果函數發現 n 為 0,則會設定例外狀況並傳回 -1。

「hook」的第二個用途是在呼叫者未提供輸出陣列,且輸出的一個或多個核心維度也不是輸入核心維度時,計算輸出陣列的大小。如果 ufunc 沒有在 process_core_dims_func 屬性上定義函數,則未指定的輸出核心維度大小將導致引發例外狀況。透過 process_core_dims_func 提供的「hook」,ufunc 的作者可以根據 ufunc 設定適合的輸出大小。

在傳遞給「hook」函數的陣列中,未由輸入確定的核心維度透過在 core_dim_sizes 陣列中具有值 -1 來指示。函數可以使用適合 ufunc 的任何值替換 -1,基於輸入陣列中出現的核心維度。

警告

函數絕不能變更輸入時 core_dim_sizes 中不是 -1 的值。變更不是 -1 的值通常會導致 ufunc 的輸出不正確,並可能導致 Python 直譯器崩潰。

例如,考慮廣義 ufunc conv1d,其基本函數計算長度分別為 mn 的兩個一維陣列 xy 的「完整」卷積。此卷積的輸出長度為 m + n - 1。若要將其實作為廣義 ufunc,簽名設定為 (m),(n)->(p),並且在「hook」函數中,如果發現核心維度 p 為 -1,則將其替換為 m + n - 1。如果 p *不是* -1,則必須驗證給定的值是否等於 m + n - 1。如果不是,則函數必須設定例外狀況並傳回 -1。為了獲得有意義的結果,操作還需要 m + n 至少為 1,即兩個輸入的長度都不能為 0。

int conv1d_process_core_dims(PyUFuncObject *ufunc,
                             npy_intp *core_dim_sizes)
{
    // core_dim_sizes will hold the core dimensions [m, n, p].
    // p will be -1 if the caller did not provide the out argument.
    npy_intp m = core_dim_sizes[0];
    npy_intp n = core_dim_sizes[1];
    npy_intp p = core_dim_sizes[2];
    npy_intp required_p = m + n - 1;

    if (m == 0 && n == 0) {
        // Disallow both inputs having length 0.
        PyErr_SetString(PyExc_ValueError,
            "conv1d: both inputs have core dimension 0; the function "
            "requires that at least one input has size greater than 0.");
        return -1;
    }
    if (p == -1) {
        // Output array was not given in the call of the ufunc.
        // Set the correct output size here.
        core_dim_sizes[2] = required_p;
        return 0;
    }
    // An output array *was* given.  Validate its core dimension.
    if (p != required_p) {
        PyErr_Format(PyExc_ValueError,
                "conv1d: the core dimension p of the out parameter "
                "does not equal m + n - 1, where m and n are the "
                "core dimensions of the inputs x and y; got m=%zd "
                "and n=%zd so p must be %zd, but got p=%zd.",
                m, n, required_p, p);
        return -1;
    }
    return 0;
}