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)维度的提案已被推迟。
摘要#
广义通用函数(Generalized universal functions)顾名思义,是通用函数(universal functions)的泛化:它们作用于非标量元素。它们的签名描述了它们所作用元素的结构,名称链接了应相同的操作数维度。在此,我们提议扩展签名,以允许签名指示一个维度(i)具有固定大小;(ii)可以不存在;和(iii)可以被广播。
详细描述#
提案的每个部分都由具体需求驱动 [1]。
固定大小的维度。处理空间向量的代码通常明确指出是用于 2 维或 3 维空间(例如,来自天文学基本标准的代码,作者希望使用 gufuncs 为 astropy 包装 [2])。签名应该能够指示这一点。例如,一个将极角转换为二维笛卡尔单位向量的函数,其签名目前必须是
()->(n),而无法指示n必须等于 2。事实上,这个签名尤其令人讨厌,因为如果不放入输出参数,当前的 gufunc 包装代码就会失败,因为它无法确定n。同样,一个两个三维向量的叉积的签名必须是(n),(n)->(n),同样无法指示n必须等于 3。因此,这里提出允许除了变量名之外,还可以赋予数值。因此,极角到二维单位向量将是()->(2);两个角度到三维单位向量(),()->(3);以及两个三维向量的叉积将是(3),(3)->(3)。可能缺失的维度。这部分几乎完全由包装
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)->()。可以广播的维度。对于某些应用程序,操作数之间的广播是有意义的。例如,一个比较数组中向量的
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]。这些 PRs 通过两个新字段扩展了 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修饰符,则它允许与其他具有相同名称的维度进行广播。所有输入维度都必须共享此修饰符,而没有输出维度应该具有它。如果名称后附加了
?修饰符,则当且仅当它存在于所有共享它的输入和输出上时,该维度才是一个核心维度;否则它将被忽略(并为基本函数替换为大小为 1 的维度)。
签名示例 [4]
签名 |
可能的用途 |
|
添加 |
|
对最后一个轴求和 |
|
沿轴测试是否相等,允许与标量进行比较 |
|
内积 |
|
矩阵乘法 |
|
向量-矩阵乘法 |
|
矩阵-向量乘法 |
|
以上所有四种情况同时发生,但向量不能有循环维度(即,像 |
|
三维向量的叉积 |
|
内积在最后一个维度上,外积在倒数第二个维度上,并在其余维度上进行循环/广播。 |
向后兼容性#
一个可能的担忧是 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]。另一个论点是,即使使用方法,能够表达它们的签名(就像至少对于 reduce 和 accumulate 那样)也是很有益的。
最后的论点是,我们正在使 gufuncs 变得过于复杂。这对于可以省略的维度来说可能是正确的,但这些维度也有最强的用例。固定维度非常简单地实现,并且其含义很明显。一旦支持了灵活维度,广播的能力也很简单。
参考文献和脚注#
版权#
本文档已置于公共领域。