如何在python中以尽可能小的增量递增浮点值?
背景:我使用浮点值作为字典键。
偶尔,很偶尔(也许永远,但不能肯定从来没有),就会出现冲突。我想通过将浮点值增加尽可能小的值来解决这些问题。我怎样才能做到这一点?
在C语言中,我会旋转尾数来实现这一点,但是我认为这在Python中是不可能的。
背景:我使用浮点值作为字典键。
偶尔,很偶尔(也许永远,但不能肯定从来没有),就会出现冲突。我想通过将浮点值增加尽可能小的值来解决这些问题。我怎样才能做到这一点?
在C语言中,我会旋转尾数来实现这一点,但是我认为这在Python中是不可能的。
Answers:
从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()。
从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解决方案是最简单的。
Decimal.next_plus() stackoverflow.com/questions/5749188/…–
x赋予nextafter函数更大的数字。假设您总是提供参数x + 1作为y参数;如果x非常接近最大可能值,将会给您错误的答案。仅考虑y参数的符号nextafter以指示是否需要递增或递减,也许会更方便。
from _testcapi import DBL_MAX, DBL_MIN, FLT_MAX, FLT_MIN
首先,这种“响应碰撞”是一个很糟糕的主意。
如果它们发生冲突,则字典中的值应该是具有公共键的项目列表,而不是单个项目。
您的“哈希探测”算法将必须遍历多个“微小增量”才能解决冲突。
并且已知顺序哈希探测效率低下。
阅读此: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
defaultdict(list)可以使用,因此您mydict[key].append(value)无需担心密钥是否已存在就可以进行操作。
不用增加值,只需将元组用作冲突键即可。如果需要使它们保持顺序,则每个键都应该是一个元组,而不仅仅是重复项。
float小于任何一个 tuple。 4.0 < ()->True
<strike>经历了一个糟糕的例子,但它仅在IE上显示,因此我的编辑使答案处于非常混乱的状态。现在应该修复。
TypeError如果<在浮点数和元组之间使用,将会得到a 。
TypeError: unorderable types: float() < tuple()
一个更好的答案(现在我只是为了好玩而已...),这是通过旋转位来实现的。处理部分负值之间的进位和溢出有些棘手。
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!')
有趣的问题。您需要添加的数量显然取决于冲突值的大小,以便规范化的添加只会影响最低有效位。
不必确定可以添加的最小值。您需要做的只是大致估计。FPU格式提供52个尾数位和一个隐藏位(53位精度)。在这一精度水平附近,没有任何物理常数是已知的。没有传感器能够测量附近的任何东西。这样您就不会遇到难题。
在大多数情况下,关键ķ,你就可以添加K / 2 53,因为52位小数加上隐藏位。
但是,不必冒着引发库错误或通过射击最后一点或附近任何东西来探索舍入问题的风险。
1.可能不止一次,直到不再冲突为止,至少可以挫败任何恶魔般的单元测试作者。
1 / 2 ** 1020.
import sys
>>> sys.float_info.epsilon
2.220446049250313e-16
x+=x*sys.float_info.epsilon
sys.float_info.epsilon定义为“ 1.0与可表示的下一个最大值之间的最小差异”,因此相对于其他值而言,使用它并不安全。
epsilon = 2*pow(2, -53)
不必修改浮动时间戳记,而是对每个键使用一个元组,因为Mark Ransom建议该元组(x,y)由x=your_unmodified_time_stamp和组成y=(extremely unlikely to be a same value twice)。
所以:
x 这是未修改的时间戳,可以多次相同。y 您可以使用:
虽然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...
这是它的一部分。这很脏而且很慢,但也许就是您喜欢的方式。它缺少几个极端的情况,但这也许会使其他人靠近。
这个想法是得到一个浮点数的十六进制字符串。这样就可以得到一个带有尾数和指数位的字符串。由于您必须手动完成所有操作并保持与字符串之间的相互转换,因此缠绕非常麻烦。无论如何,您要在正数(负数)的最后一位数字上加上(减去)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
我认为您的意思是“尽可能避免散列冲突”,因为例如第二高的浮点数可能已经是关键!=)
while toInsert.key in myDict: # assumed to be positive
toInsert.key *= 1.000000000001
myDict[toInsert.key] = toInsert
也就是说,您可能不想使用时间戳作为键。
在查看自动填充的答案后,我得出了一个略有不同的答案:
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()需要该功能,但嘿,似乎没有什么问题。事实证明,这实际上是非常复杂的(也许是为什么七个人没有提供答案的答案……)。
我认为这是正确的解决方案,它肯定可以正确处理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)