【问题标题】:Behaviour of custom NaN floats in Python and NumpyPython 和 Numpy 中自定义 NaN 浮点数的行为
【发布时间】:2014-07-06 22:46:35
【问题描述】:

我需要将一些额外信息打包到浮点 NaN 值中。我在 Python 中使用单精度 IEEE 754 浮点数(32 位浮点数)。 Python 和 NumPy 如何处理这些值?

理论

IEEE 754-2008 标准似乎认为数字实际上不是数字,如果设置了指数位 (23..30),并且至少设置了一个有效位。因此,如果我们将浮点数转换为 32 位整数表示,则任何满足以下条件的都可以:

  • i & 0x7f800000 == 0x7f800000
  • i & 0x007fffff != 0

这会让我有很多选择。但是,标准似乎说有效数字的最高位是is_quiet,应该设置以避免计算中的异常。

实际测试

Python 2.7

为了确定,我进行了一些测试,结果很有趣:

import math
import struct

std_nan = struct.unpack("f4", struct.pack("I", 0x7fc00000))[0]
spec_nan = struct.unpack("f4", struct.pack("I", 0x7f800001))[0]
spec2_nan = struct.unpack("f4", struct.pack("I", 0x7fc00001))[0]

print "{:08x}".format(struct.unpack("I", struct.pack("f4", std_nan))[0])
print "{:08x}".format(struct.unpack("I", struct.pack("f4", spec_nan))[0])
print "{:08x}".format(struct.unpack("I", struct.pack("f4", spec2_nan))[0])

这给出了:

7fc00000
7fc00001 <<< should be 7f800001
7fc00001

这个和一些进一步的测试似乎表明某些东西 (struct.unpack?) 总是设置 is_quiet 位。

NumPy

我对 NumPy 进行了同样的尝试,因为我总是可以依靠转换而不改变一个位:

import numpy as np

intarr = np.array([0x7f800001], dtype='uint32')
f = np.fromstring(intarr.tostring(), dtype='f4')
print np.isnan(f)

这给出了:

RuntimeWarning: invalid value encountered in isnan
[True]

但如果将值替换为0x7fc00001,则没有错误。

假设

如果我设置 is_quiet 并将其余位用于我自己的目的,Python 和 NumPy 都会很高兴。 Python 自行处理,NumPy 依赖于低级语言实现和/或硬件 FP 实现。

问题

我的假设是否正确,是否可以通过一些官方文档来证明或反驳?或者它是那些依赖于平台的东西之一?

我在这里找到了一些非常相关的东西:How to distinguish different types of NaN float in Python,但我找不到任何关于如何在 Python 或 NumPy 中处理携带额外信息的 NaN 的官方消息。

【问题讨论】:

  • 我的第一反应 - 如果您的设计要求您使用自己的实现特定信息破解现有的价值/约定,同时保持向后兼容性,我会称之为“糟糕的设计”并恳请您回到画板并重新设计。必须有更好的方法来做到这一点。如果有这样做的标准方法,它将被记录在案。
  • 这可能有助于查看 struct.unpack: _PyFloat_Unpack4 source 的行为。
  • @TonySuffolk66:我在很大程度上同意你的看法。然而,IEEE 754-2008 第 6.2.1 条:“在编码时,所有 NaN 都有一个符号位和一个位模式,这些位模式需要将编码识别为 NaN 并确定其类型(sNaN 与 qNaN)。其余位,在尾随有效数字字段中,对有效载荷进行编码,这可能是诊断信息(见上文)。”所以,我在我所做的事情中找不到任何不标准的东西。 (加上存档文件格式在某些情况下可能会很奇怪。这不会成为公共 API 的一部分。)
  • @PietroSaccardi:感谢您提醒我查看源代码!我在那里找到的东西——尤其是我在那里没有找到的东西——解决了这个谜题!我会发布一个答案来说明我认为真正发生的事情。
  • @DrV 乐于助人。非常有趣的答案!

标签: python python-2.7 numpy


【解决方案1】:

在考虑了一段时间并查看了源代码广告然后重新思考了一下之后,我想我可以回答我自己的问题。我的假设几乎是正确的,但不是全部。

由于 NumPy 和 Python 处理数字的方式完全不同,因此这个答案分为两部分。

使用 NaN 在 Python 和 NumPy 中真正发生的事情

NumPy

这可能有点特定于平台,但在大多数平台上,NumPy 使用 gcc 内置 isnan,这反过来又可以快速执行某些操作。运行时警告来自更深层次,在大多数情况下来自硬件。 (NumPy 可以使用几种方法来确定 NaN 状态,例如 x != x,它至少在 AMD 64 平台上工作,但使用 gcc 它下降到 gcc,它可能使用一些非常短的代码目的。)

因此,理论上没有办法保证 NumPy 如何处理 NaN,但实际上在更常见的平台上它会按照标准所说的那样做,因为这就是硬件所做的。 NumPy 本身根本不关心 NaN 类型。 (某些 NumPy 特定的非硬件支持的数据类型和平台除外。)

Python

这里的故事变得有趣了。如果平台支持 IEEE 浮点数(大多数都支持),Python 使用 C 库进行浮点运算,因此在大多数情况下几乎直接使用硬件指令。所以与 NumPy 应该没有任何区别。

除了... 在 Python 中通常没有 32 位浮点数这样的东西。 Python 浮点对象使用 C double,这是一种 64 位格式。如何在这些格式之间转换特殊的 NaN?为了看看在实践中会发生什么,下面的小 C 代码会有所帮助:

/* nantest.c - Test floating point nan behaviour with type casts */

#include <stdio.h>
#include <stdint.h>

static uint32_t u1 = 0x7fc00000;
static uint32_t u2 = 0x7f800001;
static uint32_t u3 = 0x7fc00001;

int main(void)
    {
    float f1, f2, f3;
    float f1p, f2p, f3p;
    double d1, d2, d3;
    uint32_t u1p, u2p, u3p;
    uint64_t l1, l2, l3;

    // Convert uint32 -> float
    f1 = *(float *)&u1; f2 = *(float *)&u2; f3 = *(float *)&u3;

    // Convert float -> double (type cast, real conversion)
    d1 = (double)f1; d2 = (double)f2; d3 = (double)f3;

    // Convert the doubles into long ints
    l1 = *(uint64_t *)&d1; l2 = *(uint64_t *)&d2; l3 = *(uint64_t *)&d3;

    // Convert the doubles back to floats
    f1p = (float)d1; f2p = (float)d2; f3p = (float)d3;

    // Convert the floats back to uints
    u1p = *(uint32_t *)&f1p; u2p = *(uint32_t *)&f2p; u3p = *(uint32_t *)&f3p;

    printf("%f (%08x) -> %lf (%016llx) -> %f (%08x)\n", f1, u1, d1, l1, f1p, u1p);
    printf("%f (%08x) -> %lf (%016llx) -> %f (%08x)\n", f2, u2, d2, l2, f2p, u2p);
    printf("%f (%08x) -> %lf (%016llx) -> %f (%08x)\n", f3, u3, d3, l3, f3p, u3p);

    return 0;
    }

打印出来:

nan (7fc00000) -> nan (7ff8000000000000) -> nan (7fc00000)
nan (7f800001) -> nan (7ff8000020000000) -> nan (7fc00001)
nan (7fc00001) -> nan (7ff8000020000000) -> nan (7fc00001)

通过查看第 2 行,很明显我们遇到了与 Python 相同的现象。因此,在 64 位版本中,在指数之后立即引入了额外的 is_quiet 位是到 double 的转换。

这听起来有点奇怪,但实际上标准说(IEEE 754-2008,第 6.2.3 节):

将安静的 NaN 从较窄的格式转换为相同基数的较宽格式,然后再转换回相同的较窄格式,不应以任何方式更改安静的 NaN 有效负载,除非使其成为规范。

这并没有说明信号 NaN 的传播。但是,第 6.2.1 节对此进行了解释:

对于二进制格式,有效负载编码在尾随有效位字段的 p - 2 个最低有效位中。

上面的 p 是精度,32 位浮点数是 24 位。所以,我的错误是使用带信号的 NaN 作为有效载荷。

总结

我得到了以下带回家的积分:

  • IEEE 754-2008 支持和鼓励使用 qNaN(安静的 NaN)
  • 奇怪的结果是因为我尝试使用 sNaN 并且类型转换导致 is_quiet 位被设置
  • NumPy 和 Python 在最常见的平台上都根据 IEEE 754 运行
  • 该实现在很大程度上依赖于底层 C 实现,因此保证很少(Python 中甚至有一些代码承认 NaN 没有像在某些平台上那样被处理)
  • 处理此问题的唯一安全方法是对有效负载进行一些 DIY

然而,有一件事既不是用 Python 也不是 NumPy 实现的(也不是我遇到的任何其他语言)。第 5.12.1 节:

语言标准应提供可选的将支持格式的 NaN 转换为外部字符序列的方法,外部字符序列会在基本 NaN 字符序列上附加一个可以表示 NaN 有效负载的后缀(参见 6.2)。有效载荷后缀的形式和解释是语言定义的。语言标准应要求接受任何此类可选输出序列作为将外部字符序列转换为支持格式的输入。

【讨论】:

    猜你喜欢
    • 2012-03-09
    • 1970-01-01
    • 2019-05-10
    • 2011-02-26
    • 2012-11-19
    • 2013-05-05
    • 1970-01-01
    • 2020-11-08
    • 2020-11-11
    相关资源
    最近更新 更多