为啥 (inf + 0j)*1 评估为 inf + nanj?

Posted

技术标签:

【中文标题】为啥 (inf + 0j)*1 评估为 inf + nanj?【英文标题】:Why does (inf + 0j)*1 evaluate to inf + nanj?为什么 (inf + 0j)*1 评估为 inf + nanj? 【发布时间】:2020-01-21 17:23:54 【问题描述】:
>>> (float('inf')+0j)*1
(inf+nanj)

为什么?这在我的代码中造成了一个令人讨厌的错误。

为什么1 不是乘法恒等式,而是给(inf + 0j)

【问题讨论】:

我认为您要查找的关键字是“field”。加法和乘法默认定义在单个字段中,在这种情况下,唯一可以容纳您的代码的标准字段是复数字段,因此在操作良好之前需要将两个数字默认视为复数 -定义。这并不是说他们不能扩展这些定义,但显然他们只是遵循标准的东西,并没有特别想扩展定义的冲动。 哦,如果你发现这些特质令人沮丧并想打你的电脑,you have my sympathy。 @Mehrdad 一旦添加了这些非有限元素,它就不再是一个字段。事实上,由于不再存在乘法中性点,因此根据定义它不能是一个字段。 @PaulPanzer:是的,我认为他们只是把这些元素塞进去了。 浮点数(即使您排除无穷大和 NaN)不是一个字段。大多数适用于字段的身份不适用于浮点数。 【参考方案1】:

来自 Python 的有趣定义。如果我们用笔和纸解决这个问题,我会说预期结果将是expected: (inf + 0j),正如您所指出的,因为我们知道我们的意思是1 的规范,所以(float('inf')+0j)*1 =should= ('inf'+0j)

但事实并非如此……当我们运行它时,我们得到:

>>> Complex( float('inf') , 0j ) * 1
result: (inf + nanj)

Python 将这个 *1 理解为一个复数,而不是 1 的范数,因此它解释为 *(1+0j),当我们尝试执行 inf * 0j = nanj 时出现错误,因为 inf*0 无法解决。

你真正想做的事情(假设 1 是 1 的范数):

回想一下,如果z = x + iy 是实部 x 和虚部 y 的复数,z 的复共轭定义为 z* = x − iy,绝对值也称为 norm of z 定义为:

假设11 的规范,我们应该这样做:

>>> c_num = complex(float('inf'),0)
>>> value = 1
>>> realPart=(c_num.real)*value
>>> imagPart=(c_num.imag)*value
>>> complex(realPart,imagPart)
result: (inf+0j)

我知道不是很直观...但有时编码语言的定义方式与我们日常使用的方式不同。

【讨论】:

【参考方案2】:

1 首先被转换为复数1 + 0j,然后导致inf * 0 乘法,得到nan

(inf + 0j) * 1
(inf + 0j) * (1 + 0j)
inf * 1  + inf * 0j  + 0j * 1 + 0j * 0j
#          ^ this is where it comes from
inf  + nan j  + 0j - 0
inf  + nan j

【讨论】:

为了回答“为什么...?”这个问题,可能最重要的一步是第一步,将1 转换为1 + 0j 请注意,C99 指定实际浮点类型在乘以复数类型时提升为复数(标准草案的第 6.3.1.8 节),并且到目前为止据我所知,C++ 的 std::complex 也是如此。这可能部分是出于性能原因,但它也避免了不必要的 NaN。 @benrg 在 NumPy 中,array([inf+0j])*1 也计算为 array([inf+nanj])。假设实际的乘法发生在 C/C++ 代码的某个地方,这是否意味着他们编写了自定义代码来模拟 CPython 行为,而不是使用 _Complex 或 std::complex? @marnix 它比这更复杂。 numpy 有一个中心类ufunc,几乎每个运算符和函数都派生自该类。 ufunc 负责广播管理步幅所有棘手的管理员,这使得使用数组变得如此方便。更准确地说,特定操作员和通用机器之间的劳动分工是特定操作员为它想要处理的输入和输出元素类型的每个组合实现一组“最内层循环”。通用机器负责任何外部循环并选择最佳匹配最内层循环... ...根据需要提升任何不完全匹配的类型。我们可以通过types 属性为np.multiply 访问提供的内部循环列表,这将产生['??->?', 'bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I', 'll->l', 'LL->L', 'qq->q', 'QQ->Q', 'ee->e', 'ff->f', 'dd->d', 'gg->g', 'FF->F', 'DD->D', 'GG->G', 'mq->m', 'qm->m', 'md->m', 'dm->m', 'OO->O'] 我们可以看到几乎没有混合类型,特别是没有混合浮点"efdg" 和复杂"FDG" .【参考方案3】:

从机制上讲,公认的答案当然是正确的,但我认为可以给出更深入的答案。

首先,将问题澄清为 @PeterCordes 在评论中说:“是否有乘法恒等式 对 inf + 0j 有效的复数?” 或者换句话说是什么 OP 发现复数乘法的计算机实现存在弱点,或者是 inf+0j 有一些概念上不合理的地方

简答:

使用极坐标,我们可以将复数乘法视为缩放和旋转。将无限的“手臂”旋转 0 度,就像乘以 1 一样,我们不能期望以有限的精度放置它的尖端。 所以确实,inf+0j 有一些根本不正确的地方,即, 只要我们处于无穷大,有限的偏移就变得毫无意义。

长答案:

背景:这个问题所围绕的“大事”是问题 扩展数字系统(想想实数或复数)。一个理由 一个人可能想做的是添加一些无限的概念,或者 如果碰巧是一位数学家,请“压缩”。还有其他 也有原因(https://en.wikipedia.org/wiki/Galois_theory、https://en.wikipedia.org/wiki/Non-standard_analysis),但我们对这里的那些不感兴趣。

一点压缩

当然,这种扩展的棘手之处在于,我们想要这些新的 适合现有算术的数字。最简单的方法是添加一个 无穷远处的单个元素 (https://en.wikipedia.org/wiki/Alexandroff_extension) 并使其等于除零除以零以外的任何值。这适用于 实数 (https://en.wikipedia.org/wiki/Projectively_extended_real_line) 和复数 (https://en.wikipedia.org/wiki/Riemann_sphere)。

其他扩展...

虽然单点紧化很简单并且在数学上是合理的,但已经在寻求包含多个无穷大的“更丰富”的扩展。用于实浮点数的 IEEE 754 标准具有 +inf 和 -inf (https://en.wikipedia.org/wiki/Extended_real_number_line)。看起来 自然而直接,但已经迫使我们跳过篮球和 发明像-0https://en.wikipedia.org/wiki/Signed_zero这样的东西

...复平面的

复平面的多于一inf 扩展怎么样?

在计算机中,复数通常通过将两个 fp 实数粘在一起来实现,一个用于实部,一个用于虚部。只要一切都是有限的,那就太好了。然而,一旦考虑到无穷大,事情就会变得棘手。

复平面具有自然的旋转对称性,这与复算术非常吻合,因为将整个平面乘以 e^phij 与围绕 0 的 phi 弧度旋转相同。

那个附件G的东西

现在,为了简单起见,复杂的 fp 只需使用底层实数实现的扩展(+/-inf、nan 等)。这种选择可能看起来很自然,甚至不被视为一种选择,但让我们仔细看看它意味着什么。复平面的这种扩展的简单可视化如下所示(I = 无限,f = 有限,0 = 0)

I IIIIIIIII I
             
I fffffffff I
I fffffffff I
I fffffffff I
I fffffffff I
I ffff0ffff I
I fffffffff I
I fffffffff I
I fffffffff I
I fffffffff I
             
I IIIIIIIII I

但由于真正的复平面是一个尊重复数乘法的平面,因此信息量更大的投影应该是

     III    
 I         I  
    fffff    
   fffffff   
  fffffffff  
I fffffffff I
I ffff0ffff I
I fffffffff I
  fffffffff  
   fffffff   
    fffff    
 I         I 
     III    

在这个投影中,我们看到无穷大的“不均匀分布”,这不仅是丑陋的,而且也是 OP 所遭受的问题的根源:大多数无穷大(形式为 (+/-inf,finite) 和 (有限, +/-inf) 在四个主要方向集中在一起, 所有其他方向仅由四个无穷大 (+/-inf, +-inf) 表示。将复数乘法扩展到该几何并不奇怪是一场噩梦。

C99 规范的附录 G 尽最大努力使其发挥作用,包括改变 infnan 如何交互的规则(基本上 inf 胜过 nan)。 OP 的问题通过不将实数和提议的纯虚数类型提升为复数来回避,但是让实数 1 的行为与复数 1 不同并不能作为解决方案让我感到震惊。很明显,附件 G 没有完全说明两个无穷大的乘积应该是什么。

我们可以做得更好吗?

尝试通过选择更好的无穷大几何来解决这些问题是很诱人的。类似于扩展实线,我们可以为每个方向添加一个无穷大。这种结构类似于投影平面,但不会将相反的方向集中在一起。 无穷大将用极坐标 inf x e^2 omega pi i 表示, 定义产品将很简单。尤其是OP的问题,自然就解决了。

但这就是好消息结束的地方。在某种程度上,我们可以通过——不是不合理地——要求我们的新式无穷支持提取其实部或虚部的函数而被抛回原点。加法是另一个问题;添加两个非对映无穷大,我们必须将角度设置为未定义,即nan(有人可能认为角度必须位于两个输入角度之间,但没有简单的方法来表示“部分 nan-ness”)

黎曼救援

鉴于这一切,旧的一点压缩可能是最安全的做法。或许 Annex G 的作者在强制使用函数 cproj 时也有同感。


a related question 的回答是由比我更有能力的人回答的。

【讨论】:

是的,因为nan != nan。我知道这个答案是半开玩笑的,但我不明白为什么它应该对 OP 的编写方式有所帮助。 鉴于问题正文中的代码实际上并未使用==(并且他们接受了另一个答案),看来这只是OP如何表达标题的问题。我改写了标题以解决这种不一致。 (故意使这个答案的前半部分无效,因为我同意@cmaster:这不是这个问题要问的)。 @PeterCordes 这会很麻烦,因为使用极坐标我们可以将复数乘法视为缩放和旋转。将无限的“手臂”旋转 0 度,就像乘以 1 一样,我们不能期望以有限的精度放置它的尖端。在我看来,这是比公认的解释更深入的解释,也是与 nan != nan 规则相呼应的解释。 C99 指定实际浮点类型在乘以复数类型时不会提升为复数(标准草案的第 6.3.1.8 节),据我所知,C++ 的也是如此标准::复杂。这意味着 1 是那些语言中这些类型的乘法恒等式。 Python 也应该这样做。我认为它当前的行为只是一个错误。 @PaulPanzer:我不知道,但基本概念是一个零(我称之为 Z)将始终支持 x+Z=x 和 x*Z=Z,以及 1 /Z=NaN,一(正无穷小)将支持 1/P=+INF,一(负无穷小)将支持 1/N=-INF,(无符号无穷小)将产生 1/U=NaN。一般来说,x-x 将是 U,除非 x 是一个真正的整数,在这种情况下它会产生 Z。【参考方案4】:

这是在 CPython 中如何实现复数乘法的实现细节。与其他语言(例如 C 或 C++)不同,CPython 采用了一种有点简单的方法:

    整数/浮点数在乘法中被提升为复数 简单的school-formula is used,一旦涉及无限数,它就不会提供所需/预期的结果:
Py_complex
_Py_c_prod(Py_complex a, Py_complex b)

    Py_complex r;
    r.real = a.real*b.real - a.imag*b.imag;
    r.imag = a.real*b.imag + a.imag*b.real;
    return r;

上述代码的一个问题情况是:

(0.0+1.0*j)*(inf+inf*j) = (0.0*inf-1*inf)+(0.0*inf+1.0*inf)j
                        =  nan + nan*j

但是,我们希望得到-inf + inf*j 作为结果。

在这方面其他语言也遥遥领先:复数乘法在很长一段时间内都不是 C 标准的一部分,仅作为附录 G 包含在 C99 中,它描述了应该如何执行复数乘法 - 而且它不是就像上面的学校公式一样简单! C++ 标准没有指定复杂的乘法应该如何工作,因此大多数编译器实现都回退到 C 实现,这可能符合 C99(gcc、clang)或不符合(MSVC)。

对于上述“有问题的”示例,符合 C99 的实现(比学校公式为 more complicated)将给出 (see live) 预期结果:

(0.0+1.0*j)*(inf+inf*j) = -inf + inf*j 

即使使用 C99 标准,也没有为所有输入定义明确的结果,即使对于符合 C99 的版本也可能不同。

float 在 C99 中未被提升为 complex 的另一个副作用是,将inf+0.0j1.01.0+0.0j 相乘可能会导致不同的结果(现场查看):

(inf+0.0j)*1.0 = inf+0.0j (inf+0.0j)*(1.0+0.0j) = inf-nanj,虚部是 -nan 而不是 nan(至于 CPython)在这里不起作用,因为所有安静的 nan 都是等价的(参见 this),甚至其中一些设置了符号位(因此打印为“-”,参见this),有些则不是。

这至少是违反直觉的。


我的主要收获是:“简单的”复数乘法(或除法)并不简单,在语言甚至编译器之间切换时,必须做好准备应对细微的错误/差异。

【讨论】:

我知道有很多 nan 位模式。不过,不知道标志位的事情。但我的意思是语义上 -nan 与 nan 有何不同?或者我应该说 nan 与 nan 的不同之处? @PaulPanzer 这只是 printf 和类似的如何与 double 一起工作的一个实现细节:他们查看符号位来决定是否应该打印“-”(不管它是否是 nan 与否)。所以你是对的,“nan”和“-nan”之间没有有意义的区别,很快就会修复这部分答案。 啊,很好。有点担心我认为我所知道的关于 fp 的一切实际上都不正确...... 对不起,打扰了,但你确定这个“没有想象的1.0,即1.0j,它与0.0+1.0j的乘法不同。”是正确的?该附件 G 似乎指定了一个纯虚构类型(G.2),还规定了它应该如何相乘等。(G.5.1) @PaulPanzer 不,感谢您指出问题!作为 c++ 编码器,我主要通过 C++ 眼镜看到 C99 标准 - 它让我忘记了,C 在这里领先一步 - 你显然是对的,再一次。

以上是关于为啥 (inf + 0j)*1 评估为 inf + nanj?的主要内容,如果未能解决你的问题,请参考以下文章

为啥整数除以零 1/0 会出错但浮点数 1/0.0 返回“Inf”?

为啥 math.inf 是浮点数,为啥我不能将其转换为整数?

为啥 NA_real_ <= Inf 返回 NA?

为啥 1**Inf 的值等于 1,而不是 NaN?

为啥“np.inf // 2”会导致 NaN 而不是无穷大?

为什么整数除以零1/0会给出错误,但浮点1 / 0.0会返回“Inf”?