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]。
固定大小维度。处理空间向量的代码通常明确地用于二维或三维空间(例如,作者希望使用 gufunc 为 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)->()
。该签名更普遍地适用于“链式 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
修饰符,则允许它与具有相同名称的其他维度进行广播。所有输入维度必须共享此修饰符,而输出维度不应包含它。如果名称后缀有
?
修饰符,则只有当它存在于所有共享该维度的输入和输出上时,该维度才是一个核心维度;否则它将被忽略(并被基本函数的尺寸为 1 的维度替换)。
签名示例 [4]
签名 |
可能用途 |
|
加法 |
|
沿最后一个轴求和 |
|
沿轴测试相等性,允许与标量进行比较 |
|
向量内积 |
|
矩阵乘法 |
|
向量-矩阵乘法 |
|
矩阵-向量乘法 |
|
同时处理以上四种情况,除了向量不能有循环维度(即,像 |
|
3D 向量的叉积 |
|
对最后一个维度进行内积运算,对倒数第二个维度进行外积运算,并对其余维度进行循环/广播。 |
向后兼容性#
一个可能的担忧是 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
而言是可能的)。
最后一个论点是,我们正在使 gufunc 变得过于复杂。这可以说适用于可以省略的维度,但这也是最强烈的用例。固定维度实现非常简单,其含义也显而易见。一旦支持灵活维度,广播能力也变得简单。
参考文献和脚注#
版权#
本文档已进入公共领域。