在 numpy 中使用单精度浮点数的不便

Inconvenience to use single precision floats in numpy

在numpy中使用单精度(float32)编写代码时,太难写了。

首先,单精度浮标标量过长。我们必须按如下方式键入所有变量。

a = np.float32(5)

但是其他一些语言使用更简单的表示。

a = 5.f

其次,算术运算也不方便。

b = np.int32(5)+np.float32(5)

我预计 b 的类型是 numpy.float32 但它是 numpy.float64.

当然,

b = np.add(np.int32(5), np.float32(5), dtype=np.float32)

returns我想要的。但是替换所有操作太长了

有没有更简单的方法在 numpy 中使用单精度?

问题是当您在操作中使用不同类型时,NumPy 会提升类型。如果另一个数字操作数的数据类型为:

,则 float32 仅保留 float32
  • float32 或更少
  • int16 或更少
  • uint16 或更少

如果另一个操作数具有另一个 dtype,则结果将为 float64(如果另一个操作数是复数,则结果为 complex)。上面列出的数据类型不是最常见的,所以几乎所有操作(尤其是当另一个是 Python integer/float 时)都使用标准运算符 +-/*、...会将您的 float32 提升为 float64

不幸的是,您无法避免这种情况。在很多情况下,NumPy 这样做是可以的,因为:

  • 大多数架构处理双精度的速度与处理单精度浮点数的速度一样快。 Python 中的算术运算在 Python 类型上运行速度很快,但在其他类型上运行速度较慢。
import numpy as np
a32 = np.float32(1)
a64 = np.float64(1)
a = 1.
%timeit [a32 + a32 for _ in range(20000)]  # 100 loops, best of 3: 4.58 ms per loop
%timeit [a64 + a64 for _ in range(20000)]  # 100 loops, best of 3: 4.83 ms per loop
%timeit [a + a for _ in range(20000)]      # 100 loops, best of 3: 2.72 ms per loop
  • Python 类型的开销如此之大,以至于 标量 双精度浮点数的内存开销几乎可以忽略不计。
import sys
import numpy as np

sys.getsizeof(np.float32(1))  # 28
sys.getsizeof(np.float64(1))  # 32
sys.getsizeof(1.)             # 24  # that's also a double on my computer!

但是,如果您有大量数组并且 运行 遇到内存问题,或者如果您与其他需要单精度浮点数的库(机器学习、GPU、 ...)。

但如上所述,您几乎总是会与强制规则作斗争,这会防止您 运行 陷入意想不到的问题。

int32 + float32的例子实际上是一个很好的例子!您希望结果为 float32 - 但存在一个问题:您不能将每个 int32 表示为 float32:

np.iinfo(np.int32(1))             # iinfo(min=-2147483648, max=2147483647, dtype=int32)
int(np.float32(2147483647))       # 2147483648
np.int32(np.float32(2147483647))  # -2147483648

是的,只需将值转换为单精度浮点数并将其转换回整数即可更改其值。这就是 NumPy 使用双精度的原因,这样您就不会得到 意外 结果!这就是为什么你需要 强制 NumPy 做一些可能出错的事情(从一般用户的角度来看)。


因为没有(据我所知)限制 Numpy 类型提升的方法,你必须自己发明。

例如,您可以创建一个 class 来包装 NumPy 数组并使用特殊方法为运算符实现 dtype-d 函数:

import numpy as np

class Arr32:
    def __init__(self, arr):
        self.arr = arr

    def __add__(self, other):
        other_arr = other
        if isinstance(other, Arr32):
            other_arr = other.arr
        return self.__class__(np.add(self.arr, other_arr, dtype=np.float32))

    def __sub__(self, other):
        other_arr = other
        if isinstance(other, Arr32):
            other_arr = other.arr
        return self.__class__(np.subtract(self.arr, other_arr, dtype=np.float32))

    def __mul__(self, other):
        other_arr = other
        if isinstance(other, Arr32):
            other_arr = other.arr
        return self.__class__(np.multiply(self.arr, other_arr, dtype=np.float32))

    def __truediv__(self, other):
        other_arr = other
        if isinstance(other, Arr32):
            other_arr = other.arr
        return self.__class__(np.divide(self.arr, other_arr, dtype=np.float32))

但这只实现了 NumPy 功能的一小部分,并且会很快导致大量代码和边缘情况可能被遗忘。现在使用 __array_ufunc__ or __array_function__ 可能有更聪明的方法,但我自己没有使用过这些方法,所以我无法评论工作量或适用性。

所以我的首选解决方案是为所需的函数创建辅助函数:

import numpy as np

def arr32(a):
    return np.float32(a)

def add32(a1, a2):
    return np.add(a1, a2, dtype=np.float32)

def sub32(a1, a2):
    return np.subtract(a1, a2, dtype=np.float32)

def mul32(a1, a2):
    return np.multiply(a1, a2, dtype=np.float32)

def div32(a1, a2):
    return np.divide(a1, a2, dtype=np.float32)

或者只使用就地操作,因为这些操作不会提升类型:

>>> import numpy as np

>>> arr = np.float32([1,2,3])
>>> arr += 2
>>> arr *= 3
>>> arr
array([ 9., 12., 15.], dtype=float32)