NEP 20 — 扩展通用泛函签名#

作者:

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

状态:

最终

类型:

标准跟踪

创建:

2018-06-10

决议:

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

注意

添加固定 (i) 和灵活 (ii) 维度的提案已获接受,而添加可广播 (iii) 维度的提案则被推迟。

摘要#

通用泛函顾名思义,是泛函的泛化:它们作用于非标量元素。它们的签名描述了它们作用的元素的结构,名称链接了应该相同的操作数的维度。这里,建议扩展签名以允许签名指示维度 (i) 具有固定大小;(ii) 可能不存在;以及 (iii) 可以广播。

详细描述#

提案的每个部分都由特定的需求驱动[1]

  1. 固定大小的维度。处理空间向量的代码通常明确地用于 2 维或 3 维空间(例如,来自基本天文学标准的代码,作者希望使用 gufuncs 为 astropy 进行封装[2])。签名应该能够指示这一点。例如,将极角转换为二维笛卡尔单位向量的函数的签名当前必须是()->(n),无法指示n必须等于 2。实际上,这个签名特别烦人,因为如果没有放入输出参数,当前的 gufunc 包装代码会失败,因为它无法确定n。类似地,两个三维向量的叉积的签名必须是(n),(n)->(n),同样无法指示n必须等于 3。因此,这里建议允许除了变量名之外还提供数值。因此,角度到二维单位向量将是()->(2);两个角度到三维单位向量(),()->(3);两个三维向量的叉积将是(3),(3)->(3)

  2. 可能缺失的维度。这部分几乎完全是由希望将matmul包装在 gufunc 中驱动的。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)->()。这个签名对于“链式 ufuncs”来说似乎更方便;例如,另一个应用可能是在实现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中(当前为布尔值),其中非零继续表示通用函数,但特定标志指示通用函数是否使用固定、灵活或广播维度。

有了以上内容,语法的正式定义将变为[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这样的东西是否最好用通用函数来完成,而不是作为np.equal上的特殊方法。对此的一个反驳是,实际上有一个关于all_equal的 PR[8]。另一个论点是,即使要使用方法,也最好能够表达它们的签名(就像至少对于reduceaccumulate那样)。

最后一个论点是我们使通用函数过于复杂。这对于可以省略的维度来说可能成立,但它也具有最强大的用例。冻结维度有一个非常简单的实现,其含义很明显。一旦支持灵活维度,广播的能力也很简单。

参考文献和脚注#