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


74

这个问题是我进一步调查后发现的奇怪问题

我一直都理解默认情况下MATLAB变量是双精度的。因此,如果我要进行一些操作,例如声明一个小数点后20位数字的变量:

>> num = 2.71828182845904553488;
>> class(num)  % Display the variable type
ans =
double

我希望最后4位数字被忽略,因为浮点相对精度约为10 -16

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

如果我尝试显示小数点后的数字超过16位(使用fprintfsprintf),那么我会看到:

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

换句话说,数字17到20均为0。

但是当我在Symbolic Toolbox中传递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!!!

Answers:


66

他们是双打。vpa()只是选择显示超出浮点相对精度的非有效数字,printf()然后disp()将其截断或归零。

您只得到原始的四位数,因为您选择初始化的文字num恰好是二进制双精度值的精确十进制扩展,因为它是从实际双精度值扩展的输出中复制并粘贴的从另一个问题。如您在“奖金”附录中所示,它不适用于其他附近的值。

更准确地说,Matlab中的所有数字文字都会产生double类型的值。它们将转换为最接近其所代表的十进制值的二进制double值。实际上,超出双精度类型的精度限制的文字中的数字会被静默删除。当您复制并粘贴的输出vpa以创建新变量时,就像另一个问题的发布者对e = ...语句,您正在从文字中初始化一个值,而不是直接处理上一个表达式的结果。

此处的区别仅在于输出格式。我认为这vpa()是采用双精度二进制双精度数并将其视为精确值的情况。对于给定的二进制尾数指数值,您可以计算等效于任意多个小数位的十进制数。如果二进制值的精度(“宽度”)有限(与使用任何固定大小的数据类型一样),那么这些十进制数字中只有很多是有效的。printf()Matlab的默认显示通过截断输出或将不重要的数字显示为0vpa()来处理此问题。它忽略了精度的限制,并继续计算所需的小数位数。

这些额外的数字是伪造的,在某种意义上,如果将它们替换为其他值以产生附近的十进制值,则它们都将被“舍入”为相同的二进制双精度值。

这是一种显示方式。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位相差很大的输出。但是,您不应该创建一个双精度值xvpa(x)产生介于vpa(x0)和之间的十进制表示形式vpa(x1)

(更新:Amro指出,您可以用来fprintf('%bx\n', x)以十六进制格式显示基础二进制值的确切表示形式。您可以使用它来确认文字映射到同一双精度数。)

我怀疑vpa()这样做是因为它会将输入视为精确值,并且多态地支持Symbolic Toolbox中其他Matlab类型,其精度比double更高。这些值将需要使用数字文字之外的其他方式进行初始化,这就是为什么sym()将字符串作为输入并且vpa(exp(1))与有所不同的原因vpa(sym('exp(1)'))

说得通?抱歉,请耐心等待。

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


1
啊哈!所以我只是碰巧使用二进制值的精确十进制扩展作为测试数字。现在一切都说得通了!不过,我不确定我是怎么想念这个的……也许是由于我的女儿长牙使我整夜无法入睡。;)
gnovice

@Mikhail:没错,尽管有一些实际的MathWorks人员在SO周围闲逛,例如Loren和MatlabDoug。我刚刚花了一些时间使用Matlab的外部接口支持构建将C,Java和COM集成在一起的Matlab开发平台。熟悉数据类型和Matlab内部结构的好方法。
安德鲁·扬克

4
您还可以检查双精度变量的十六进制表示形式。试试看fprintf('%bx\n', exp(1), 2.7182818284590455, 2.71828182845904559999999999),他们都将返回相同的64位表示4005bf0a8b14576a
Amro 2010年

@Amro:太好了!我不知道%bx。这也证实eps()给出一位差。fprintf('%bx\n', exp(1), exp(1)+eps(exp(1)))(至少对于此值)。将此纳入我的回答中。
安德鲁·扬克

1
@AndrewJanke:很抱歉要复活这个旧线程,但似乎VPA(或更确切地说是VPA调用的SYM)比我们想象的还要棘手。默认情况下,SYM尝试将浮点数转换为“有理数”形式,以补偿中间评估中涉及的舍入误差...在此处
Amro 2012年

3

首先:

似乎sprintf和fprintf在不同版本的MATLAB上具有不同的行为,例如在MATLAB 2018中

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

第二:

浮点数字

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

双精度浮点

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

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

位:62到52用法:指数,偏差为1023

位:51到0用法:数字1.f的分数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。因此,将数字四舍五入到最接近的可表示数字(机器ε)时的最大相对舍入误差为2 ^ -53。

所以在您的情况下n = 1(2 ^ 1 <= num <= 2 ^ 2)的间距是2 ^ -51,

我认为可以安全地假设用于显示数字的sprintf和sprintf算法是棘手的,而MATLAB Double类型是基于IEEE标准的,


关于VPA:

vpa使用防护位数保持精度

数字功能的值指定所使用的有效数字的最小数量。在内部,vpa可以使用比数字指定数量更多的数字。这些额外的数字称为保护数字,因为它们可以防止后续计算中的舍入误差。

使用四个有效数字在数值上近似1/3。

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

用20位数字近似结果。结果表明,在计算a时,工具箱内部使用了四位数以上的位数。由于舍入错误,结果中的最后一位数字不正确。

vpa(a, 20)
ans =
0.33333333333303016843

您可能遇到的问题是由于间距,gaurd位数算法和舍入问题,

例如使用matlab 2018 a:

 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.42 e-14),因此该数字有效到小数点后14位

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.