了解 Float>>asFraction 及其变体

lur*_*ker 8 floating-point precision smalltalk fractions

我目前对类方法Float>>asFraction及其各种形式提供的响应感到困惑。这里有一些例子:

GNU Smalltalk

0.001 asFraction
1/1000
0.001 asExactFraction
1152921504606847/1152921504606846976
Run Code Online (Sandbox Code Playgroud)

法罗

0.001 asFraction
1152921504606847/1152921504606846976
0.001 asTrueFraction
1152921504606847/1152921504606846976
0.001 asMinimalDecimalFraction
1/1000
0.001 asApproximateFraction
1/1000
Run Code Online (Sandbox Code Playgroud)

由于显而易见的原因,GNU的asFraction和菲罗的asMinimalDecimalFractionasApproximateFraction做的最有意义的我,因为他们生产的,在数学上,更“精确”的结果。我不明白其他人。为什么具有大分子和分母但显然不太精确的值的分数是对 的响应asExactFraction?为什么我想要这样的回应?为什么在 Pharo 中,我选择asFraction还是选择似乎并不重要asTrueFraction?为什么会有这些变体?

如果我想将浮点数表示为一个分数,我想我会想要基于构成分子和分母的整数的精度类,或者可能基于最大分母的近似值。

我查看了蓝皮书,它几乎asFraction没有提到也没有提到任何变体。

Lea*_*lia 8

AFloat是一种编码数字的数据结构,无论我们如何看待或解释它,从数学上讲,它只能是有理量(即整数或分数)。这种编码适用于 CPU 高速执行的算术运算。我们付出的代价是编纂没有展示它所代表的分子和分母。该方法Float >> #asTrueFraction用这些数字回答,换句话说,它解码 的实例中包含的位Float,并用它编码的实际分数回答。

您必须了解的是,当您编写代码时,0.001您是在告诉编译器创建一个Float近似于分数的1/1000. 如果 CPU 使用十进制而不是二进制表示,这将类似于要求它1/3使用有限数量的小数位进行编码,这不可避免地导致0.33333..3,对于某些最大位数3。在分母不是 的幂的情况下2,CPU 必须解决类似的问题并最终逼近提供的数量,以便它适合分配给 的位数Floats。该方法#asTrueFraction反转该过程并揭示近似值的确切值,该值Float隐藏在它打印其实例的方式之后。

在 Pharo 中,Float >> #asFraction与 相同Float >> #asTrueFraction,所以没有区别。

中的注释Float >> #asMinimalDecimalFraction非常清楚,它将给出您通常期望的内容,即转换回 asFloat 时等于 self 的最短小数分数

最后,Float >> #asApproximateFraction使用某种算法来生成可接受的接收器近似值。


Fla*_*int 7

由于显而易见的原因,GNU的asFraction和菲罗的asMinimalDecimalFractionasApproximateFraction做的最有意义的我,因为他们生产的,在数学上,更“精确”的结果。

相反,它们执行的操作是找到输入的近似值。但是他们收到的输入实际上并不是数字 0.001,即使这看起来是你写的——而且这些方法中的任何一个都无法知道你最初写的是什么。

因此,有些方法会准确返回给定的数字(以不同的表示形式),而另一些方法会返回与您最初编写的文本相吻合的近似值(如果令人困惑!)。


稍微改写一下代码可能会有所帮助,以便您了解近似值的实际发生位置。让我们首先关注 GNU Smalltalk。

x := '0.001' asNumber.
y := x asExactFraction.
Run Code Online (Sandbox Code Playgroud)

在这个片段中,'0.001' asNumber是唯一进行任何近似的操作:Float它返回一个Float表示最接近的(IEEE 754 binary64)浮点数,而不是返回一个表示数字 0.001的实例(事实上,没有这样的浮点数!),可进行各种写成1152921504606846976分之1152921504606847,或作为0.001000000000000000020816681711721685132943093776702880859375,或0x1.0624dd2f1a9fcp?在最方便的形式10整整写二进制浮点数字。

只需编写即可获得相同的结果0.001:Smalltalk 将自动舍入到最接近的浮点数。我明确地写了它,'0.001' asNumber以明确表示这是返回对您编写的数字 0.001 的近似值的操作。

然后y := x asExactFraction设置为Fraction代表完全相同数字的实例;同样y := x asTrueFraction在法罗。号码还是1152921504606847/1152921504606846976;asExactFraction永远不会返回一个数字有什么,但两个分母的功率(至少不是一类用于存储二进制浮点数)。


相反,如果您评估(在 GNU Smalltalk 中)

z := x asFraction.
Run Code Online (Sandbox Code Playgroud)

那么你得到的是一个Fraction代表最简单的有理数的实例,它四舍五入到 - 非常粗略地,区间 [ ? ulp()/2, + ulp()/2], 其中 ulp() ? 2 ?52是浮点表示的最低有效数字的大小(在间隔边缘周围有警告,何时等于 2 的幂)。这里区间内“最简单”的有理数是分母最小的有理数。这个近似是通过扩展 的连分数表示直到第一个收敛到 的收敛。1

这可能(尽管我没有仔细观察以进行验证)与Pharo 对asApproximateFraction. 相比之下,PharoasMinimalDecimalFraction不返回最简单的有理数;相反,它只考虑分母中 10 = 2?5 次幂的有理数,并返回将四舍五入为 的分子最小的那个。


总之:

  • x := '0.001' asNumber集到一个Float代表(IEEE 754 binary64)实例最接近0.001的浮点数,其是1152921504606846976分之1152921504606847= 0.001000000000000000020816681711721685132943093776702880859375 = 0x1.0624dd2f1a9fcp 10?; 你通过写作得到同样的效果,x := 0.001但这使得近似值的发生变得更加模糊
  • y := x asExactFraction在Smalltalk中GNU,或y := x asTrueFractiony := asFraction在菲罗,套到一个Fraction实例表示完全相同的数目作为
  • z := x asFraction在 GNU Smalltalk 或z := x asApproximateFractionPharo 中设置为Fraction代表最简单的有理数的实例,该实例将四舍五入为
  • w := x asMinimalDecimalFraction在 Pharo 中设置为一个Fraction实例,该实例表示将四舍五入为的最短小数扩展的数字;如果您想以十进制表示法写入浮点数并确保返回相同的数字,而不会写入比您需要的数字更多的数字,则可以使用它

(如您所见,GNU Smalltalk 和 Pharo 在是否asFraction应该返回近似值上存在分歧:在 GNU Smalltalk 中是这样,而在 Pharo 中则不是。这很不幸,因为这是两者共享的唯一名称!)


为了好玩,请在 Pharo 中尝试以下示例:

3.141592653589793 asApproximateFractionAtOrder: 1
3.141592653589793 asApproximateFractionAtOrder: 2
3.141592653589793 asApproximateFractionAtOrder: 3
3.141592653589793 asApproximateFractionAtOrder: 4
3.141592653589793 asApproximateFractionAtOrder: 5
3.141592653589793 asApproximateFraction
3.141592653589793 asMinimalDecimalFraction
3.141592653589793 asTrueFraction

1.618033988749895 asApproximateFractionAtOrder: 1
1.618033988749895 asApproximateFractionAtOrder: 2
1.618033988749895 asApproximateFractionAtOrder: 3
1.618033988749895 asApproximateFractionAtOrder: 4
1.618033988749895 asApproximateFractionAtOrder: 5
1.618033988749895 asApproximateFraction
1.618033988749895 asMinimalDecimalFraction
1.618033988749895 asTrueFraction
Run Code Online (Sandbox Code Playgroud)

看看你是否注意到关于输出的任何东西——也许你会认出一些分数;看看它们与真实分数的绝对和相对误差有多远;看看分母有多大。


1 这就是GNU Smalltalk 的定义asFraction目前所做的。从技术上讲,文档没有对近似的性质做出任何承诺,但这是 最自然的方法Fraction,因为它提供了独立于任何基数选择的最佳有理近似。见 A. Ya。Khinchin, Continued Fractions , University of Chicago Press, 1964, §6 “作为最佳近似的收敛”进一步讨论作为最佳有理近似的连续分数收敛。连分数是数学中一个美丽的角落,但可悲的是在现代教育中被忽视了!


Ber*_*erg 5

虽然其他答案深入探讨了为什么分数1/1000不等于 64 位二进制 float 0.001,但这里的答案略有不同:

0.001 printStringBase: 2
"=>" '1.00000110001001001101110100101111000110101001111111e-10'
Run Code Online (Sandbox Code Playgroud)

这就是引擎盖下的0.001 真实情况,作为有限精度的二进制浮点数(仅限64 位)。这就是为什么它等于1/1000

1/1000 = 0.001
"=>" false
Run Code Online (Sandbox Code Playgroud)

如果您想要具有无限精度的精确小数,则需要告诉系统。十进制数确实完全等于分数:0.001s1/1000

0.001s asFraction
"=>" (1/1000)

1/1000 = 0.001s
"=>" true
Run Code Online (Sandbox Code Playgroud)

我们不经常使用小数的原因是它们效率较低 - 64 位二进制浮点数学在硬件中实现,精确数学在软件中实现,使其慢几个数量级。