NEP 20 — 广义通用函数签名的扩展#

作者

Marten van Kerkwijk < mhvk @astro。乌托伦托加州>

地位

最终的

类型

标准轨道

创建

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 维空间(例如,基础天文学标准中的代码,作者希望使用用于天文学的 gufuncs 进行包装[ 2 ])。签名应该能够表明这一点。例如,将极角转换为二维笛卡尔单位向量的函数的签名目前必须是 ()->(n),但没有办法表明它n必须等于 2。事实上,这个签名特别烦人,因为没有输入输出参数,当前的 gufunc 包装器代码失败,因为它无法确定n.类似地,两个 3 维向量的叉积的签名必须是(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)->()。对于“链式 ufunc”来说,签名似乎更方便;例如,另一个应用程序可能在假定的 ufunc 中实现sumproduct.

    讨论中出现的另一个例子是加权平均值,它可能看起来像,返回平均值及其不确定性。如果签名为,则将被迫始终给出与数据点一样多的 sigma,而广播将允许为所有点给出单个 sigma(这对于计算平均值的不确定性仍然有用)。weighted_mean(y, sigma[, axis, ...])(n),(n)->(),()

执行

所提议的变更已全部实施[ 3 ][ 4 ][ 5 ]。这些 PR 使用两个新字段扩展 ufunc 结构,每个字段的大小等于不同维度的数量,并core_dim_sizes保存可能固定的大小,并core_dim_flags保存指示维度是否可以丢失或广播的标志。为了确保我们可以区分这个新版本和以前的版本,未使用的条目reserved1被重新用作版本号。

在实现中,要注意的是,对于基本函数,标记尺寸与未标记尺寸没有任何区别:例如,固定尺寸尺寸的大小仍然传递给基本函数(但循环现在可以依靠该大小等于签名中给出的固定大小)。

需要决定的实现细节是是否可以方便地获得所有标志的摘要。这可能会存储在core_enabled (当前是 bool)中,非零继续指示 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?)一个 。这样做的缺点是开发人员现在必须确保基本函数可以处理这些不同的签名。此外,扩展很快就会变得很麻烦。例如,对于 的签名,必须有五个条目: 。对于像(来自[ 4 ] 中的测试用例)这样的签名,甚至不值得写出扩展。(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)->()cube_equal

对于广播,建议使用 的替代后缀^(因为广播可以被认为是增加数组的大小)。这似乎不太清楚。此外,有人想知道它是否不应该只是一个全有或全无的标志。情况可能是这样,尽管给出了灵活尺寸的后缀,可以说另一个后缀更清晰(就像实现一样)。

讨论

这里的建议在邮件列表[ 6 ][ 7 ]上进行了相当长的讨论。争论的焦点是用例是否足够强大。特别是,对于冻结尺寸,有人认为可以将对正确数字的检查放入循环选择代码中。这似乎不太清楚,没有任何好处。

对于广播,人们注意到缺乏可能需要它的基本函数的示例,有人质疑类似的事情是否all_equal 最好使用 gufunc 而不是作为np.equal.对此的一个反驳是, all_equal [ 8 ]存在一个实际的 PR 。另一方面,即使要使用一种方法,最好能够表达他们的签名(至少对于reduce和来说是可能的accumulate)。

最后一个论点是我们让 gufunc 变得过于复杂。这可以说适用于可以省略的维度,但也具有最强的用例。冻结维度的实现非常简单,其含义也很明显。一旦支持灵活的尺寸,广播的能力也很简单。

参考文献和脚注#