【问题标题】:Increment a Python floating point value by the smallest possible amount将 Python 浮点值增加最小的可能量
【发布时间】:2020-10-12 03:45:52
【问题描述】:

如何在 python 中将浮点值增加最小的数量?


背景:我使用浮点值作为字典键。

偶尔,非常偶尔(也许永远不会,但不一定永远不会),会有碰撞。我想通过尽可能少地增加浮点值来解决这些问题。我该怎么做?

在 C 中,我会旋转尾数位来实现这一点,但我认为这在 Python 中是不可能的。

【问题讨论】:

  • 由于这个问题的活动量很大,在链接/关闭其他“Python 中的下一个浮点值”问题时,它似乎是典型的“重复”问题。但是,它至少有两个不相交的方面:(1)如何增加浮点值,以及(2)在使用浮点数作为字典键时如何防止冲突。可以在here找到更清晰的标题问题陈述,以及更明确的答案

标签: python


【解决方案1】:

从 Python 3.9 开始,stdlib 中有 math.nextafter继续阅读旧 Python 版本中的替代方案。

将python浮点值增加最小的可能量

nextafter(x,y) 函数返回在 y 方向上跟随 x 的下一个离散不同的可表示浮点值。 nextafter() 函数保证在平台上工作或返回一个合理的值以指示下一个值是不可能的。

nextafter() 函数是 POSIX 和 ISO C99 标准的一部分,并且是 _nextafter() in Visual C。符合 C99 标准的数学库、Visual C、C++、Boost 和 Java 都实现了 IEEE 推荐的 nextafter() 函数或方法。 (老实说,我不知道 .NET 是否有 nextafter()。微软不太关心 C99 或 POSIX。)

没有位旋转函数完全或正确处理边缘情况,例如值通过 0.0、负 0.0、次正规、无穷大、负值、上溢或下溢等。@ 987654326@ 给出一个想法,如果这是你的方向,如何做正确的位旋转。

有两个可靠的解决方法可以在 Python nextafter() 或其他排除的 POSIX 数学函数:

使用 Numpy:

>>> import numpy
>>> numpy.nextafter(0,1)
4.9406564584124654e-324
>>> numpy.nextafter(.1, 1)
0.10000000000000002
>>> numpy.nextafter(1e6, -1)
999999.99999999988
>>> numpy.nextafter(-.1, 1)
-0.099999999999999992

直接链接到系统数学 DLL:

import ctypes
import sys
from sys import platform as _platform

if _platform == "linux" or _platform == "linux2":
    _libm = ctypes.cdll.LoadLibrary('libm.so.6')
    _funcname = 'nextafter'
elif _platform == "darwin":
    _libm = ctypes.cdll.LoadLibrary('libSystem.dylib')
    _funcname = 'nextafter'
elif _platform == "win32":
    _libm = ctypes.cdll.LoadLibrary('msvcrt.dll')
    _funcname = '_nextafter'
else:
    # these are the ones I have access to...
    # fill in library and function name for your system math dll
    print("Platform", repr(_platform), "is not supported")
    sys.exit(0)

_nextafter = getattr(_libm, _funcname)
_nextafter.restype = ctypes.c_double
_nextafter.argtypes = [ctypes.c_double, ctypes.c_double]

def nextafter(x, y):
    "Returns the next floating-point number after x in the direction of y."
    return _nextafter(x, y)

assert nextafter(0, 1) - nextafter(0, 1) == 0
assert 0.0 + nextafter(0, 1) > 0.0

如果你真的想要一个纯 Python 解决方案:

# handles edge cases correctly on MY computer 
# not extensively QA'd...
import math
# 'double' means IEEE 754 double precision -- c 'double'
epsilon  = math.ldexp(1.0, -53) # smallest double that 0.5+epsilon != 0.5
maxDouble = float(2**1024 - 2**971)  # From the IEEE 754 standard
minDouble  = math.ldexp(1.0, -1022) # min positive normalized double
smallEpsilon  = math.ldexp(1.0, -1074) # smallest increment for doubles < minFloat
infinity = math.ldexp(1.0, 1023) * 2

def nextafter(x,y):    
    """returns the next IEEE double after x in the direction of y if possible"""
    if y==x:
       return y         #if x==y, no increment

    # handle NaN
    if x!=x or y!=y:
        return x + y       

    if x >= infinity:
        return infinity

    if x <= -infinity:
        return -infinity

    if -minDouble < x < minDouble:
        if y > x:
            return x + smallEpsilon
        else:
            return x - smallEpsilon  

    m, e = math.frexp(x)        
    if y > x:
        m += epsilon
    else:
        m -= epsilon

    return math.ldexp(m,e)

或者,使用Mark Dickinson's优秀solution

显然Numpy 解决方案是最简单的。

【讨论】:

  • +1。感谢您成为第一个真正回答问题的人。
  • Python 有 Decimal.next_plus() stackoverflow.com/questions/5749188/…
  • 一个未讨论的极端情况:如何提出比x 更大的数字以提供给nextafter 函数。假设您总是提供x + 1 作为y 参数;如果x 非常接近最大可能值,那会给你错误的答案。也许只考虑 nextaftery 参数的符号会更方便,以指示是否需要递增或递减。
  • @wberry 对 y 使用 +inf 或 -inf 怎么样?
  • 您可能会发现这很有用。 from _testcapi import DBL_MAX, DBL_MIN, FLT_MAX, FLT_MIN
【解决方案2】:

Python 3.9 及以上版本

从 Python 3.9 开始,released 2020-10-05,您可以使用math.nextafter function

math.<strong>nextafter</strong>(x, y)

将 x 之后的下一个浮点值返回到 y。

如果 x 等于 y,则返回 y。

例子:

  • math.nextafter(x, math.inf) 上升:朝向正无穷大。

  • math.nextafter(x, -math.inf) 下降:朝向负无穷大。

  • math.nextafter(x, 0.0) 趋近于零。

  • math.nextafter(x, math.copysign(math.inf, x)) 远离零。

另见math.ulp()

【讨论】:

  • 从零开始更简单,可以使用math.nextafter(x, 2*x)
【解决方案3】:

首先,这种“响应碰撞”是一个非常糟糕的主意。

如果它们发生冲突,字典中的值应该是具有公共键的项目列表,而不是单个项目。

您的“散列探测”算法必须循环通过多个“微小增量”来解决冲突。

众所周知,顺序哈希探测效率低下。

阅读:http://en.wikipedia.org/wiki/Quadratic_probing

其次,使用math.frexpsys.float_info.epsilon分别摆弄尾数和指数。

>>> m, e = math.frexp(4.0)
>>> (m+sys.float_info.epsilon)*2**e
4.0000000000000018

【讨论】:

  • +1 用于使用列表。可以使用defaultdict(list),这样您就可以直接使用mydict[key].append(value),而不必担心密钥是否已经存在。
  • 我知道各种花哨的多阶段散列技术,但我想做一些快速而简单的事情,我知道在这种情况下就足够了.
  • 另外,我知道没有值大于当前时间的键,因此递增是解决冲突的明智方法。
  • @Autopulated,可能有大于当前时间的键 - 如果已经发生冲突!
  • @Autopulated 您没有告诉我们您要插入的事件的性质,所以我不知道每次滴答获得两个以上事件的可能性有多大。另请注意,某些计时器的计数频率不如其精度所暗示的那样频繁。连续三个不是解决此问题的唯一方法,您还可以在下一个刻度线中连续两个,然后再两个。
【解决方案4】:

暂时忘记为什么我们想要增加一个浮点值,我不得不说我认为 Autopulated 自己的答案可能是正确的。

但是对于问题域,我和大多数响应者一样对使用浮点数作为字典键的想法表示疑虑。如果反对使用 Decimal(如主要 cmets 中所提议)是因为它是“重量级”解决方案,我建议自己动手折衷:找出时间戳的实际分辨率,选择一些数字充分覆盖它,然后将所有时间戳乘以必要的数量,以便您可以使用整数作为键。如果您能承受超出计时器精度的额外数字或两位,那么您可以更加确信不会有或更少的碰撞,并且如果有碰撞,您只需加 1(而不是一些 rigamarole 来找到下一个浮点值)。

【讨论】:

    【解决方案5】:

    如果可能的话,我建议不要假设浮点数(或时间戳)是唯一的。使用计数迭代器、数据库序列或其他服务来发布唯一标识符。

    【讨论】:

    • 我不认为他们会是独一无二的,因此问题!我假设碰撞是非常罕见的。
    • @Autopulated:“我假设碰撞非常罕见”就像假设它们不会发生一样糟糕。找到更好的钥匙。
    • 不,碰撞非常罕见。我知道我的计时器是如何工作的,并且我知道什么时候将东西添加到我的字典中:罕见的哈希冲突是完全可以接受的。
    【解决方案6】:

    不要增加值,只需使用元组作为碰撞键。如果您需要保持它们的顺序,每个键都应该是一个元组,而不仅仅是重复项。

    【讨论】:

    • Any float 小于 any tuple4.0 &lt; () --> True
    • @kindall,谢谢!我喜欢这个网站的一件事是当你自己的回答教给你一些新东西时。
    • @Adam,似乎有些人发现了我的错误并取消了他们的投票。我有&lt;strike&gt; 经历了不好的例子,但它只显示在 IE 上,所以我的编辑使答案处于非常混乱的状态。现在应该修好了。
    • @kindall,不兼容的类型在 Python 2 中可排序,但在 Python 3 中不可排序。如果在浮点数和元组之间使用 &lt;,您将获得 TypeError
    • @kindall,在 Python 3 中,浮点数和元组不能直接比较。TypeError: unorderable types: float() &lt; tuple()
    【解决方案7】:

    一个更好的答案(现在我只是为了好玩......),动机是玩弄比特。处理负数部分之间的进位和溢出有点棘手。

    import struct
    
    def floatToieee754Bits(f):
        return struct.unpack('<Q', struct.pack('<d', f))[0]
    
    def ieee754BitsToFloat(i):
        return struct.unpack('<d', struct.pack('<Q', i))[0]
    
    def incrementFloat(f):
        i = floatToieee754Bits(f)
        if f >= 0:
            return ieee754BitsToFloat(i+1)
        else:
            raise Exception('f not >= 0: unsolved problem!')
    

    【讨论】:

    【解决方案8】:

    不是通过更改密钥来解决冲突,而是收集冲突如何?即:

    bag = {}
    bag[1234.] = 'something'
    

    变成

    bag = collections.defaultdict(list)
    bag[1234.].append('something')
    

    这行得通吗?

    【讨论】:

      【解决方案9】:

      对于碰撞键k,添加:k / 250


      有趣的问题。您需要添加的数量显然取决于冲突值的大小,因此标准化添加只会影响最低有效位。

      没有必要确定可以添加的最小值。您需要做的就是近似它。 FPU 格式提供 52 个尾数位加上一个隐藏位,以实现 53 位精度。 在接近这种精度水平的任何地方都没有已知的物理常数。没有传感器能够测量它附近的任何东西。所以你没有困难。

      在大多数情况下,对于密钥 k,您可以添加 k/253, 因为那个 52 位小数加上隐藏的位。

      但没有必要冒险触发库错误或通过拍摄最后一点或附近的任何东西来探索舍入问题。

      所以我想说,对于碰撞键 k,只需添加 k / 250 就可以了。1


      1.可能不止一次,直到它不再碰撞,至少要挫败任何恶魔般的单元测试作者。

      【讨论】:

      • 哦,对于零,您可以做一些不同的事情,因为基于 范围(而不是 精度) 的数量要小得多可以使用双精度值。添加类似1 / 2 ** 1020.
      【解决方案10】:
      import sys
      >>> sys.float_info.epsilon
      2.220446049250313e-16
      

      【讨论】:

      • 如果你把这个数字加到 4.0 上,你会得到一个完全相同的值!
      • 我认为正确的用法是x+=x*sys.float_info.epsilon
      • sys.float_info.epsilon 定义为“1.0 与可表示的下一个最大值之间的最小差”,因此相对于其他值使用并不安全。
      • 没错,最小的差取决于指数。
      • @Mark 是正确的,它在大/小浮动时看起来很健壮。此外,对于没有任何 float_info 的版本 epsilon = 2*pow(2, -53)
      【解决方案11】:

      不要修改你的浮点时间戳,而是为每个键使用一个元组Mark Ransom suggests,其中元组(x,y)x=your_unmodified_time_stampy=(extremely unlikely to be a same value twice)组成。

      所以:

      1. x 只是未修改的时间戳,可以多次为同一个值;
      2. y 你可以使用:
        1. 一个大范围的随机整数,
        2. 序列整数(0,1,2等),
        3. UUID

      虽然 2.1(大范围的随机整数)非常适合以太网,但我会使用 2.2(串行器)或 2.3(UUID)。简单,快速,防弹。对于 2.2 和 2.3,您甚至不需要冲突检测(您可能希望像以太网一样在 2.1 中仍然拥有它。)

      2.2 的优点是您还可以分辨和排序具有相同浮点时间戳的数据元素。

      然后只需从元组中提取x 以进行任何排序类型的操作,元组本身就是哈希/字典的无冲突键。

      编辑

      我想示例代码会有所帮助:

      #!/usr/bin/env python
      
      import time
      import sys
      import random
      
      #generator for ints from 0 to maxinteger on system:
      serializer=(sn for sn in xrange(0,sys.maxint))
      
      #a list with guranteed collisions:
      times=[]
      for c in range(0,35):
         t=time.clock()
         for i in range(0,random.choice(range(0,4))):
            times.append(t)
      
      print len(set(times)), "unique items in a list of",len(times)      
      
      #dictionary of tuples; no possibilities of collisions:
      di={}   
      for time in times:
          sn=serializer.next()
          di[(time,sn)]='Element {}'.format(sn)
      
      #for tuples of multiple numbers, Python sorts
      # as you expect: first by t[0] then t[1], until t[n]
      for key in sorted(di.keys()):
          print "{:>15}:{}".format(key, di[key]) 
      

      输出:

      26 unique items in a list of 55
        (0.042289, 0):Element 0
        (0.042289, 1):Element 1
        (0.042289, 2):Element 2
        (0.042305, 3):Element 3
        (0.042305, 4):Element 4
        (0.042317, 5):Element 5
        # and so on until Element n...
      

      【讨论】:

      • 这种方法看起来很笨拙且复杂/不可行。在不知道每个键对应的“y 值”的情况下,如何在 O(1) 中检索 dict 值?它似乎只是将同样的问题转移到其他地方。
      【解决方案12】:

      这里是它的一部分。这既脏又慢,但也许这就是你喜欢的方式。它缺少几个极端案例,但也许这会让其他人接近。

      这个想法是获取浮点数的十六进制字符串。这给了你一个带有尾数和指数位的字符串来旋转。玩弄是一种痛苦,因为您必须手动完成所有操作并不断转换为字符串/从字符串转换。无论如何,您将正(负)数的最后一位数字加(减)1。如果溢出,请确保执行到指数。负数有点棘手,让你不浪费任何位。

      def increment(f):
          h = f.hex()
          # decide if we need to increment up or down
          if f > 0:
              sign = '+'
              inc = 1
          else:
              sign = '-'
              inc = -1
          # pull the string apart
          h = h.split('0x')[-1]
          h,e = h.split('p')
          h = ''.join(h.split('.'))
          h2 = shift(h, inc)
          # increase the exponent if we added a digit
          h2 = '%s0x%s.%sp%s' % (sign, h2[0], h2[1:], e)
          return float.fromhex(h2)
      
      def shift(s, num):
          if not s:
              return ''
          right = s[-1]
          right = int(right, 16) + num
          if right > 15:
              num = right // 16
              right = right%16
          elif right < 0:
              right = 0
              num = -1
          else:
              num = 0
          # drop the leading 0x
          right = hex(right)[2:]
          return shift(s[:-1], num) + right
      
      a = 1.4e4
      print increment(a) - a
      a = -1.4e4
      print increment(a) - a
      
      a = 1.4
      print increment(a) - a
      

      【讨论】:

        【解决方案13】:

        我认为您的意思是“尽可能少地避免哈希冲突”,因为例如下一个最高浮点数可能已经是键! =)

        while toInsert.key in myDict: # assumed to be positive
            toInsert.key *= 1.000000000001
        myDict[toInsert.key] = toInsert
        

        也就是说您可能不想使用时间戳作为键。

        【讨论】:

        • 实际 epsilon 因指数的值而异。我将继续添加,直到获得一个新值,因为碰撞将非常罕见。
        • 啊哎呀,我意识到了,只是没想到;然后我实际上建议乘以;答案已编辑
        【解决方案14】:

        查看 Autopopulated 的答案后,我想出了一个稍微不同的答案:

        import math, sys
        
        def incrementFloatValue(value):
            if value == 0:
                return sys.float_info.min                                
            mant, exponent = math.frexp(value)                                                   
            epsilonAtValue = math.ldexp(1, exponent - sys.float_info.mant_dig)                
            return math.fsum([value, epsilonAtValue])
        

        免责声明:我真的没有我想的那么擅长数学;)请在使用之前验证它是否正确。我也不确定性能

        一些注意事项:

        • epsilonAtValue 计算用于尾数的位数(最大值减去用于指数的位数)。
        • 我不确定是否需要 math.fsum(),但它似乎不痛。

        【讨论】:

        • 似乎工作 :) 我认为你需要特殊情况为零。
        【解决方案15】:

        事实证明,这实际上是相当复杂的(也许为什么有 7 个人在没有实际提供答案的情况下回答了......)。

        我认为这是正确的解决方案,它似乎可以正确处理 0 和正值:

        import math
        import sys
        
        def incrementFloat(f):
            if f == 0.0:
                return sys.float_info.min
            m, e = math.frexp(f)
            return math.ldexp(m + sys.float_info.epsilon / 2, e)
        

        【讨论】:

        • 我提供了问题的答案,而不是问题的答案!对不起,你不喜欢它。
        • @Mark 我很欣赏人们试图提供帮助(我确实对大部分答案投了赞成票),但真正的问题的问题。我的想法部分是这样的:“如果其他人在未来搜索如何将浮点数增加尽可能小的数量”——如果他们发现一个充满元组的页面和关于哈希冲突的讨论,那真的不会太多帮助。
        • 如果提供答案的人认为使用浮点数作为键本质上是一个坏主意,那么他们会建议其他有相同问题的人远离浮点数作为键。
        猜你喜欢
        • 1970-01-01
        • 2013-05-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-18
        • 1970-01-01
        相关资源
        最近更新 更多