在MATLAB中,默认情况下变量是否真的是双精度?

73
这个问题源于我调查这个问题时注意到的一些奇怪现象...
我一直以为MATLAB变量默认是双精度的。因此,如果我声明一个变量,在小数点后面有20个数字:
>> num = 2.71828182845904553488;
>> class(num)  % Display the variable type
ans =
double

我期望最后4位数字被忽略,因为浮点数相对精度在10的-16次方数量级上:

>> eps(num)
ans =
    4.440892098500626e-016

如果我尝试使用fprintfsprintf显示小数点后超过16位的数字,我会得到预期结果:

>> fprintf('%0.20f\n', num)
2.71828182845904550000
>> sprintf('%0.20f', num)
ans =
2.71828182845904550000

换句话说,数字17到20都是0。
但当我将num传递给符号工具箱中的变量精度算术函数时,事情变得奇怪起来,我告诉它使用21位数字表示这个数字:
>> vpa(num, 21)
ans =
2.71828182845904553488
什么?!那最后四位数字又出现了!当我输入的原始数字存储为双精度变量num时,它们不应该已经丢失了吗?由于num是一个双精度变量,当它传递给vpa时,vpa怎么知道它们是什么?
我猜测 MATLAB 在内部使用比双倍精度更高的精度来表示num,因为我将其初始化为一个小数点后有更多位数的数字,而这超出了双精度变量的处理范围。这真的是正在发生的事情吗,还是其他事情正在发生?



额外奖励: 如果以上内容还没有让你头痛的话,这里有一个额外的混淆源...

>> num = 2.71828182845904553488;  % Declare with 20 digits past the decimal
>> num = 2.718281828459045531;    % Re-declare with 18 digits past the decimal
>> vpa(num, 21)
ans =
2.71828182845904553488  % It's the original 20-digit number!!!
2个回答

67
它们都是双精度浮点数。vpa()只是选择显示浮点相对精度之外的非显着数字,而printf()和disp()会将它们截断或归零。您仅因为您选择初始化num的文字刚好是二进制双精度值的精确十进制扩展,因为它是从其他问题实际双精度值的扩展输出中复制和粘贴的结果,所以才返回原始的四位数字。它不适用于其他附近的值,如您在“奖金”附录中所示。更准确地说,Matlab中的所有数字文字都产生double类型的值。它们转换为最接近它们表示的十进制值的二进制双精度值。实际上,在double类型的精度限制之外的文字中的数字被默默丢弃。当您复制并粘贴vpa的输出以创建新变量时(正如其他问题的发布者使用e = ...语句所做的那样),您正在从文字中初始化一个值,而不是直接处理先前表达式的结果。这里的区别仅仅在于输出格式。我认为正在发生的是vpa()将双精度二进制双精度数视为精确值。对于给定的二进制幂次-指数值,可以将相应的十进制值计算为任意多的小数位数。如果在二进制值中具有有限的精度(“宽度”),则只有那么多的十进制位数是显着的。printf()和Matlab的默认显示通过截断输出或将非显着数字显示为0来处理此操作。vpa()忽略了精度限制并继续计算您请求的许多小数位数。这些附加数字是虚假的,因为如果使用其他值替换它们以生成附近的十进制值,则所有这些数字都会“四舍五入”到相同的二进制双精度值。下面是一种展示方法。当存储在双精度浮点数中时,这些x值都是相同的,并且将由vpa()表示为相同的值。
x = [
    2.7182818284590455348848081484902650117874145507812500
    2.7182818284590455348848081484902650117874145507819999
    2.7182818284590455348848
    2.71828182845904553488485555555555555555555555555555
    exp(1)
    ]
unique(x)

这里还有一种展示的方法。这是两个非常接近的双精度浮点数。

x0 = exp(1)
x1 = x0 + eps(x0)

vpa(x0)vpa(x1)应该在第16位之后产生非常不同的输出。然而,您不能创建一个双精度值x,使得vpa(x)生成的十进制表示介于vpa(x0)vpa(x1)之间。

(更新:Amro指出可以使用fprintf('%bx \ n',x)以十六进制格式显示基础二进制值的确切表示形式。通过这种方式,您可以确认文字相对应的是相同的双精度数。)

我怀疑vpa()这样做是因为它将其输入视为精确值,并且多态地支持来自符号工具箱的其他Matlab类型,这些类型比双精度更精确。这些值需要通过其他方式进行初始化,而不是使用数字字面量,这就是为什么sym()采用字符串作为输入,vpa(exp(1))vpa(sym('exp(1)'))不同的原因。

有道理吗?抱歉说了那么多。

(请注意,我没有符号工具箱,因此无法自行测试vpa()。)


1
啊哈!我碰巧使用了二进制值的精确十进制展开作为我的测试数字。现在一切都有意义了!不过我不确定我是怎么错过它的,也许是因为我女儿长牙了整晚都没睡好。;) - gnovice
@Mikhail:不是的,虽然有一些真正的MathWorks人员在SO上逛荡,比如Loren和MatlabDoug。我花了一些时间构建Matlab开发平台,使用Matlab的外部接口支持集成C、Java和COM。这是一个熟悉您的数据类型和Matlab内部结构的好方法。 - Andrew Janke
4
您还可以检查双精度变量的十六进制表示。尝试使用 fprintf('%bx\n', exp(1), 2.7182818284590455, 2.71828182845904559999999999),它们将返回相同的64位表示4005bf0a8b14576a - Amro
@Amro:太棒了!我以前不知道%bx。这也证实了eps()的确会产生一位差异。fprintf('%bx\n',exp(1),exp(1)+eps(exp(1)))(至少对于这个值)。我将在我的答案中附带这一点。 - Andrew Janke
1
@AndrewJanke:抱歉挖掘这个旧帖子,但似乎VPA(或者确切地说是被VPA调用的SYM)比我们想象的更加棘手。SYM默认尝试将浮点数转换为“有理”形式,以补偿中间计算中涉及的舍入误差... 更多讨论请参见此处 - Amro

3

首先:

看起来,在不同版本的MATLAB中,sprintf和fprintf有不同的行为。 例如,在MATLAB 2018a中

num=2.7182818284590666666666;    
sprintf('%0.70f', num)
ans =
'2.7182818284590668511668809514958411455154418945312500000000000000000000'

第二:

浮点数

MATLAB® 以双精度或单精度格式表示浮点数。默认为双精度,但您可以使用简单的转换函数将任何数字变为单精度。

双精度浮点数

MATLAB 根据 IEEE® 标准 754 构造双精度(或 double)数据类型。存储为双精度的任何值都需要 64 位,格式如下表所示:

位 :63
用途 :符号(0=正数,1=负数)

位 :62 到 52
用途 :指数,偏移量为 1023

位 :51 到 0
用途 :小数 f,表示数字 1.f 的小数部分

请参阅此链接以获取更多信息

在 252=4,503,599,627,370,496 和 253=9,007,199,254,740,992 之间,可表示的数字正好是整数。对于下一个范围从 253 到 254,所有数字都乘以2,因此可表示的数字为偶数。相反,对于前一个范围从 2^51 到 2^52,间距为0.5。
从 2^n 到 2^n+1 范围内的间距作为数字的分数为 2^n−52。将数字四舍五入到最近可表示数字时的最大相对舍入误差(机器epsilon)因此为 2^-53。
因此,在您的情况下,其中n=1(2^1 <= num <= 2^2),间距为2^-51。
我认为可以安全地假设sprintf和sprintf算法用于显示数字,并且MATLAB Double类型基于IEEE标准。

关于VPA:

VPA使用守卫数字来保持精度

digits函数的值指定所使用的最小有效数字位数。在内部,VPA可以使用比digits指定的更多的数字位数。这些额外的数字称为守卫数字,因为它们防止后续计算中的舍入误差。

使用四个有效数字近似计算1/3。

a = vpa(1/3, 4)
a =
0.3333

使用20个数字来近似计算结果a。结果显示,当计算a时,工具箱内部使用了超过4个数字。由于舍入误差,结果中的最后几位是不正确的。
vpa(a, 20)
ans =
0.33333333333303016843

您可能会遇到的问题是由于空格,守卫数字算法和舍入问题引起的。
例如,在使用Matlab 2018时:
 sprintf('%0.28f', 8.0)
 ans =
 '8.0000000000000000000000000000'

但是:
sprintf('%0.28f', 8.1)
ans =
'8.0999999999999996447286321199'

因为这个数字在2的3次方和2的4次方之间,所以间隔是2的-49次方(= 1.77 e-15),因此该数字有效到小数点后15位。

sprintf('%0.28f', 64.1)
ans =
'64.0999999999999943156581139192'

因为这个数字位于2的6次方和2的7次方之间,所以间距为2的负46次方(=1.42e-14)。因此,该数字在小数点后14位是有效的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接