将Python浮点值增加尽可能小的数量


68

如何在python中以尽可能小的增量递增浮点值?


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

偶尔,很偶尔(也许永远,但不能肯定从来没有),就会出现冲突。我想通过将浮点值增加尽可能小的值来解决这些问题。我怎样才能做到这一点?

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


2
由于此问题的活动量很大,因此在链接/关闭其他“ Python中的下一个浮点值”问题时,这似乎是规范的“重复项”。但是,它至少具有两个不相交的方面:(1)如何增加浮点值,以及(2)在使用float作为dict键时如何防止冲突。一个更清晰的标题问题的声明,以及更明确的答案,可以发现在这里。
— 约翰Y

Answers:


8

Python 3.9及更高版本

从2020年10月5日发行的Python 3.9开始,可以使用以下math.nextafter函数:

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()。


94

从Python 3.9开始,math.nextafter在stdlib中。继续阅读以获取旧版本Python中的替代方法。

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

的函数nextafter(X,Y)的功能在y方向上的返回下一个离散地不同的可表示的浮点值下面的X-。保证nextafter()函数可以在平台上工作或返回一个有意义的值,以指示下一个值是不可能的。

这些nextafter()函数是POSIX和ISO C99标准的一部分,在Visual C中是_nextafter()。符合C99的标准数学库,Visual C,C ++,Boost和Java都实现了IEEE建议的nextafter()函数或方法。(我真的不知道.NET是否具有nextafter()。Microsoft不太关心C99或POSIX。)

这里没有任何位旋转功能完全或正确地处理边缘情况,例如值通过0.0,负0.0,子法线,无穷大,负值,上溢或下溢等。这是C中nextafter()的参考实现如果这是您的指导,请给出如何进行正确的旋转的想法。

nextafter()在Python <3.9中,有两种可靠的解决方法来获取或排除其他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)

或者,使用马克·狄金森的出色解决方案

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


6
+1。感谢您成为第一个实际回答该问题的人。
— A. Rex

3
Python具有Decimal.next_plus() stackoverflow.com/questions/5749188/…–
— jfs

1
一个没有讨论的边缘案例:如何提出比x赋予nextafter函数更大的数字。假设您总是提供参数x + 1作为y参数;如果x非常接近最大可能值,将会给您错误的答案。仅考虑y参数的符号nextafter以指示是否需要递增或递减,也许会更方便。
— wberry

1
@wberry对于y使用+ inf或-inf呢?

1
您可能会发现这很有用。from _testcapi import DBL_MAX, DBL_MIN, FLT_MAX, FLT_MIN
— JohnMudd 2014年

9

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

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

您的“哈希探测”算法将必须遍历多个“微小增量”才能解决冲突。

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

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

其次,使用math.frexp和sys.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)无需担心密钥是否已存在就可以进行操作。
— kindall 2011年

我知道各种花哨的多阶段哈希技术,但是我想做些简单快速的事情,我知道在这种情况下就足够了。
— 詹姆斯

另外,我知道没有键值大于当前时间的键,因此递增是解决冲突的明智方法。
— 詹姆斯

2
@Autopulated,可能有大于当前时间的键-如果已经发生碰撞!
— Mark Ransom

是的,所以我的计时器将需要已返回相同的值3倍。我不知道您的计算机有多快,但是我所关心的计算机远没有达到这么快。
— 詹姆斯

4

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


我并不是假设它们将是唯一的,因此是个问题!我以为碰撞是非常罕见的。
— 詹姆斯

4
@Autopulated:“假设碰撞非常罕见”,就像假设碰撞未发生一样糟糕。寻找更好的钥匙。
— S.Lott,

不,碰撞非常罕见。我知道计时器的行为方式,也知道何时将内容添加到字典中:罕见的哈希冲突是完全可以接受的。
— 詹姆斯

4

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

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


4

不用增加值,只需将元组用作冲突键即可。如果需要使它们保持顺序,则每个键都应该是一个元组,而不仅仅是重复项。


4
任何 float小于任何一个 tuple。 4.0 < ()->True
— kindall

1
@kindall,谢谢!我喜欢这个网站的一件事是,当您自己的答案教给您一些新知识时。
— Mark Ransom

@亚当,看来有些人发现了我的错误并撤销了他们的选票。我<strike>经历了一个糟糕的例子,但它仅在IE上显示,因此我的编辑使答案处于非常混乱的状态。现在应该修复。
— Mark Ransom

3
@kindall,不兼容的类型在Python 2中是可排序的,但在Python 3中不是可排序的。TypeError如果<在浮点数和元组之间使用,将会得到a 。
— bignose

@kindall在Python 3中不能直接比较浮点数和元组。TypeError: unorderable types: float() < tuple()
— Brian Minton 2013年

3

一个更好的答案(现在我只是为了好玩而已...),这是通过旋转位来实现的。处理部分负值之间的进位和溢出有些棘手。

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!')

我喜欢这个,尽管我一开始并不了解。(我读过:docs.python.org/library/struct.html,现在对它有了更好的理解。)我不是唯一的迷,是函数命名。;)
— James Khoury

1
这就是我来这里发布的内容。这是最简单,最可靠且证明正确的答案。当然,如果您在Inf发生撞车事故,那将会缠住……
— Gabe

这也基本上是马克·狄金森的答案在这里。
— 约翰Y


2

与其通过更改密钥来解决冲突,不如收集冲突?IE浏览器:

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

变成

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

那行得通吗?


2

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


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

不必确定可以添加的最小值。您需要做的只是大致估计。FPU格式提供52个尾数位和一个隐藏位(53位精度)。在这一精度水平附近,没有任何物理常数是已知的。没有传感器能够测量附近的任何东西。这样您就不会遇到难题。

在大多数情况下,关键ķ,你就可以添加K / 2 53,因为52位小数加上隐藏位。

但是,不必冒着引发库错误或通过射击最后一点或附近任何东西来探索舍入问题的风险。

所以我想说,要碰撞键k,只需加k / 2 50并称其为一天。1个


1.可能不止一次,直到不再冲突为止,至少可以挫败任何恶魔般的单元测试作者。


哦,对于零,您可以做一些不同的事情,因为可以使用基于双精度值的范围(而不是精度)的小得多的数量。添加类似内容1 / 2 ** 1020.
— DigitalRoss

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

4
如果将此数字加到4.0,您将获得一个完全相同的值!
— 詹姆斯

3
我认为正确的用法是x+=x*sys.float_info.epsilon
— 马克·勒索姆

7
sys.float_info.epsilon定义为“ 1.0与可表示的下一个最大值之间的最小差异”,因此相对于其他值而言,使用它并不安全。
— martineau 2011年

3
没错,最小的差异取决于指数。
— 亚当·伯特克

3
@Mark是正确的,它在使用大/小浮点数时看起来很健壮。此外,对于<2.6版本中没有任何float_info的情况,您可以定义epsilon = 2*pow(2, -53)
— Mike T

1

不必修改浮动时间戳记,而是对每个键使用一个元组,因为Mark Ransom建议该元组(x,y)由x=your_unmodified_time_stamp和组成y=(extremely unlikely to be a same value twice)。

所以:

  1. x 这是未修改的时间戳,可以多次相同。
  2. y 您可以使用:
    1. 大范围内的随机整数,
    2. 序列整数(0,1,2,etc),
    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)中检索字典值?似乎只是将相同的问题转移到其他地方。
— wim

1

这是它的一部分。这很脏而且很慢,但也许就是您喜欢的方式。它缺少几个极端的情况,但这也许会使其他人靠近。

这个想法是得到一个浮点数的十六进制字符串。这样就可以得到一个带有尾数和指数位的字符串。由于您必须手动完成所有操作并保持与字符串之间的相互转换,因此缠绕非常麻烦。无论如何,您要在正数(负数)的最后一位数字上加上(减去)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

0

我认为您的意思是“尽可能避免散列冲突”,因为例如第二高的浮点数可能已经是关键!=)

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

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


在实际小量的指数虽然价值变化。我将继续添加直到获得新的值,因为碰撞将非常罕见。
— 詹姆斯

哎呀,我才意识到,只是没有在想;然后我实际上建议乘以;编辑的答案
— ninjagecko 2011年

0

在查看自动填充的答案后,我得出了一个略有不同的答案:

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()需要该功能,但嘿,似乎没有什么问题。

1
似乎可以工作:)我认为您需要将特殊情况设为零。
— 詹姆斯,

-1

事实证明,这实际上是非常复杂的(也许是为什么七个人没有提供答案的答案……)。

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

2
我提供了该问题的答案,但没有提供该问题的答案!抱歉,您不喜欢它。
— Mark Ransom

4
@Mark我很感谢人们努力提供帮助(我对大多数答案都表示赞同),但真正的问题是问题。我的想法部分是基于“如果其他人在将来寻找如何以最小的可能的增量增加浮点数”的想法-如果他们找到一个充满元组的页面并讨论散列冲突,那将不是很多救命。
— 詹姆斯

1
如果提供答案的人认为使用浮点数作为键本质上是一个坏主意,那么他们将向其他有相同问题的人建议不要使用浮点数作为键。
— 约翰Y
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.