以尽可能小的量增加python浮点值

时间:2011-05-19 19:16:53

标签: python

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

偶尔,非常偶尔(也许永远不会,但绝不会),会发生碰撞。我想通过将浮点值递增尽可能小的数量来解决这些问题。我怎么能这样做?

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

15 个答案:

答案 0 :(得分:86)

  

增加python浮点数   价值尽可能小的数值

你并不疯狂,你应该能够做到这一点。遗憾的是,Python数学库目前的缺点是Python 2.X和Python3000。 Python中应该有一个math.nextafter(x,y)但是没有。由于大多数C编译器都具有这些功能,因此添加它将是微不足道的。

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

nextafter()个功能属于POSIX和ISO C99标准,且为_nextafter() in Visual C。符合C99标准的数学库,Visual C,C ++,Boost和Java都实现了IEEE推荐的nextafter()函数或方法。 (我真的不知道.NET是否有nextafter()。微软并不关心C99或POSIX。)

由于Python似乎正朝着支持数学模块的大多数C99数学函数和行为的方向发展,因此nextafter()的排除很奇怪。幸运的是,有一些简单的解决方法。

这里的比特twiddling函数完全或正确处理边缘情况,例如值为0.0,负0.0,次正规,无穷大,负值,上溢或下溢等。{ {3}}如果这是你的方向,就要知道如何做正确的比特。

在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)

或者,使用Here is a reference implementation of nextafter() in C优秀的Mark Dickinson's

显然solution解决方案最简单。

答案 1 :(得分:9)

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

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

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

已知顺序哈希探测效率低下。

阅读本文: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

答案 2 :(得分:8)

Python 3.9及更高版本

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

math.nextafter(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()

答案 3 :(得分:6)

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

答案 4 :(得分:6)

import sys
>>> sys.float_info.epsilon
2.220446049250313e-16

答案 5 :(得分:6)

我建议不要假设浮动(或时间戳)在任何可能的情况下都是唯一的。使用计数迭代器,数据库序列或其他服务来发出唯一标识符。

答案 6 :(得分:4)

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

但是对于问题域,我分享了大多数响应者对使用浮点数作为字典键的想法的疑虑。如果反对使用Decimal(在主要评论中提出)是一个“重量级”解决方案,我建议自己做一个妥协:弄清楚时间戳上的实际解决方案是什么,选择一些数字为了充分覆盖它,然后将所有时间戳乘以必要的数量,以便您可以使用整数作为键。如果你能够提供超出计时器精度的额外数字或两个数字,那么你可以更加确信没有或更少的碰撞,如果有碰撞,你可以加1(而不是一些rigamarole来找到下一个浮点值。)

答案 7 :(得分:4)

一个更好的答案(现在我只是为了好玩而做这件事......),这是因为这些事情很糟糕。处理负数值部分之间的进位和溢出有点棘手。

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 :(得分:3)

不要修改浮点时间戳,而是将每个键的元组用作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
  3. 虽然2.1(大范围的随机int)对以太网很有用,我会使用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...
    

答案 9 :(得分:3)

对于碰撞键 k ,添加: k / 2 50


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

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

在大多数情况下,对于键 k ,您可以添加 k / 2 53 因为52位分数加上隐藏的位。

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

所以我想说,对于碰撞密钥 k ,只需添加 k / 2 50 并称之为一天。 1功能


1。可能不止一次,直到它不再发生碰撞,至少可以阻止任何恶魔单位测试作者。

答案 10 :(得分:2)

我认为你的意思是“通过尽可能小的数量来避免哈希冲突”,因为例如下一个最高浮点数可能已经是一个关键! =)

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

那说你可能不想使用时间戳作为键。

答案 11 :(得分:2)

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

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

变为

bag = collections.defaultdict(list)
bag[1234.].append('something')
那会有用吗?

答案 12 :(得分:2)

这是它的一部分。这很脏又很慢,但也许你喜欢它。它缺少几个角落的情况,但也许这会让其他人接近。

想法是获取浮点数的十六进制字符串。这给你一个字符串,尾数和指数位旋转。烦恼是一种痛苦,因为你必须手动完成所有操作并继续转换为字符串。无论如何,你为正(负)数字的最后一位数字添加(减去)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 :(得分:1)

在查看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()但是它似乎没有受到伤害。

答案 14 :(得分:0)

事实证明,这实际上非常复杂(也许为什么有七个人在没有真正提供答案的情况下回答了......)。

我认为这是正确的解决方案,它似乎正确处理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)