MATLAB 数值计算中的精度与误差问题:浮点运算、误差传播与数值稳定性
文章目录
- [MATLAB 数值计算中的精度与误差问题:浮点运算、误差传播与数值稳定性](#MATLAB 数值计算中的精度与误差问题:浮点运算、误差传播与数值稳定性)
在 MATLAB 中进行数值计算时,有些结果可能与数学上的预期不一致。例如,0.1 + 0.2 与 0.3 在数值比较中并不相等;同一个数学表达式经过代数变换后,计算精度可能发生明显变化;求解线性方程组时,残差已经非常小,计算解却仍然存在较大误差。
这些现象涉及数值计算中的一个基本问题:计算机执行的是有限精度运算,而数学分析通常建立在实数运算的基础上。两者之间的差异,使数值计算不可避免地受到舍入误差、截断误差以及输入扰动的影响。
计算结果的误差并不完全取决于浮点数具有多少位有效数字。对于相同的输入数据,不同算法可能具有不同的数值表现;即使采用数值稳定的算法,某些数学问题仍然可能对微小扰动十分敏感。
因此,分析 MATLAB 数值计算中的精度问题,需要同时考虑浮点数的表示机制、误差的来源与传播,以及数学问题本身的敏感性和计算算法的稳定性。
一、浮点数体系与有限精度计算
双精度浮点数的表示机制
MATLAB 默认使用 double 类型存储数值数据。该类型采用 IEEE 754 双精度浮点格式,以 64 位二进制数据表示一个浮点数。
这 64 位数据由符号位、指数域和小数部分组成,其中符号位占 1 位,指数域占 11 位,小数部分占 52 位。
对于非零的规格化浮点数,其数学表示形式为
x = ( − 1 ) s ( 1. f ) 2 2 e x=(-1)^s(1.f)_2 2^e x=(−1)s(1.f)22e
其中, s s s 为符号位, f f f 表示二进制小数部分, e e e 为经过指数偏置还原后的指数。
由于规格化浮点数具有一个隐含的最高有效位,双精度浮点数实际上具有 53 位二进制有效精度,大约对应 15~16 位十进制有效数字。
这种表示方式能够以有限的存储空间覆盖很大的数值范围,但并不是所有实数都可以被精确表示。
以十进制小数 0.1 为例,其二进制展开是无限循环的,因此无法使用有限位二进制小数精确存储。MATLAB 只能将其舍入到附近的一个可表示浮点数。
下面的程序可以展示这一问题:
matlab
clc;
clear;
a = 0.1;
b = 0.2;
c = 0.3;
fprintf('a + b = %.17g\n', a+b);
fprintf('c = %.17g\n', c);
disp((a+b) == c);
典型输出为:
text
a + b = 0.30000000000000004
c = 0.29999999999999999
0
从实数运算的角度看,
0.1 + 0.2 = 0.3 0.1+0.2=0.3 0.1+0.2=0.3
但计算机实际参与运算的三个数都是经过舍入的浮点数。加法结果需要再次舍入,因此 a+b 与 c 最终对应两个不同的浮点数。
这里的差异很小,却反映了有限精度计算的基本特征:计算机保存和处理的数值通常只是数学实数的近似表示。
浮点数还包括零、次正规数、无穷大和 NaN 等特殊情形,它们并不全部符合上述规格化数的表达式。对于一般科学计算,理解规格化浮点数的表示机制已经能够解释许多常见的精度现象。
机器精度与浮点数间距
MATLAB 提供了 eps 函数,用于查询浮点数的间距。
执行:
matlab
format long e
eps(1)
得到:
text
2.220446049250313e-16
这个数等于
ε = 2 − 52 \varepsilon=2^{-52} ε=2−52
它表示双精度浮点数中,1 与下一个大于 1 的可表示浮点数之间的距离。
在数值分析中,还经常使用单位舍入误差(unit roundoff),记为 u u u。对于采用最近舍入方式的 IEEE 754 双精度运算,
u = 2 − 53 u=2^{-53} u=2−53
MATLAB 中的 eps(1) 与单位舍入误差相差一个因子 2。二者经常出现在相近的讨论中,但在严格的误差分析中需要区分。
浮点数间距并不是固定不变的。可以通过以下程序进行比较:
matlab
format long e
eps(1)
eps(100)
eps(1e10)
得到:
text
2.220446049250313e-16
1.421085471520200e-14
1.907348632812500e-06
随着数值数量级增大,相邻规格化浮点数之间的绝对间距通常也会增大。
因此,浮点数具有大约 16 位十进制有效精度,并不意味着所有数量级上的数值都具有相同的绝对精度。
这一特征也解释了为什么大数与小数相加时,较小的数有时无法影响最终结果。
matlab
a = 1e16;
b = 1;
c = (a+b)-a;
disp(c);
计算结果为:
text
0
数学上有
( 10 16 + 1 ) − 10 16 = 1 (10^{16}+1)-10^{16}=1 (1016+1)−1016=1
但在双精度浮点数中, 10 16 10^{16} 1016 附近的浮点数间距已经达到 2。加上 1 后,结果在舍入过程中仍然等于原来的数值,随后相减得到 0。
这类现象通常称为小量被大数掩盖(swamping)。
浮点运算的基本误差模型
为了分析浮点运算对计算结果的影响,数值分析通常采用一个简化的舍入误差模型。
设 fl \operatorname{fl} fl 表示浮点运算,对于加、减、乘、除等基本操作,可以写为
fl ( a ∘ b ) = ( a ∘ b ) ( 1 + δ ) \operatorname{fl}(a\circ b)=(a\circ b)(1+\delta) fl(a∘b)=(a∘b)(1+δ)
其中, ∘ \circ ∘ 表示相应的算术操作, δ \delta δ 为舍入产生的相对误差。
对于正确舍入的基本运算,在精确结果处于正常浮点范围内、没有发生上溢或下溢等特殊情况时,有
∣ δ ∣ ≤ u |\delta|\leq u ∣δ∣≤u
该模型表明,单次浮点运算通常具有很小的相对舍入误差。
但实际数值算法往往由大量运算组成。前一步产生的误差可能作为后一步的输入继续参与计算,误差的最终大小取决于整个计算过程。
仅凭单次运算具有较小的舍入误差,并不能保证复杂数值算法具有较高的准确性。
二、数值误差的来源及其数学描述
分析数值结果之前,需要明确误差的含义。
设某个数学问题的精确结果为 x x x,计算得到的近似结果为 x ^ \widehat{x} x ,则两者之间的差异可以通过不同的误差指标进行描述。
绝对误差、相对误差与有效数字
绝对误差定义为
E a b s = ∣ x − x ^ ∣ E_{\mathrm{abs}}=|x-\widehat{x}| Eabs=∣x−x ∣
它直接衡量计算结果与精确值之间的距离。
当 x ≠ 0 x\neq 0 x=0 时,相对误差定义为
E r e l = ∣ x − x ^ ∣ ∣ x ∣ E_{\mathrm{rel}}=\frac{|x-\widehat{x}|}{|x|} Erel=∣x∣∣x−x ∣
相对误差能够反映误差相对于目标数值大小的比例。
例如,对于精确值 x = 1000 x=1000 x=1000,如果计算值为 x ^ = 1000.01 \widehat{x}=1000.01 x =1000.01,则
E a b s = 0.01 E_{\mathrm{abs}}=0.01 Eabs=0.01
而相对误差为
E r e l = 10 − 5 E_{\mathrm{rel}}=10^{-5} Erel=10−5
如果精确值只有 0.01,同样大小的绝对误差就可能意味着完全不同的准确程度。
因此,在比较不同数量级的计算结果时,相对误差通常更有参考价值。
不过,当真实值为零或非常接近零时,相对误差可能不存在或变得十分敏感,此时需要结合绝对误差进行判断。
有效数字从另一个角度描述近似数值的准确程度。在非零实数的常见定义下,如果相对误差满足
E r e l ≤ 1 2 10 1 − p E_{\mathrm{rel}}\leq \frac{1}{2}10^{1-p} Erel≤21101−p
则可以按照这一误差判据认为近似值至少具有 p p p 位正确的十进制有效数字。
需要注意,这里的有效数字描述的是近似结果相对于精确值的准确程度,与浮点数据类型能够存储多少位有效数字有关,但并不相同。
舍入误差与截断误差
数值计算中的误差可以根据产生机制进行区分。
舍入误差来自有限精度的数值表示与算术运算。例如,十进制小数转换为二进制浮点数时可能发生舍入,两个浮点数相加后也可能需要再次舍入。
截断误差则来源于使用有限的计算过程近似某个无限数学过程。
以指数函数为例,
e x = ∑ k = 0 ∞ x k k ! e^x=\sum_{k=0}^{\infty}\frac{x^k}{k!} ex=k=0∑∞k!xk
如果采用前 n + 1 n+1 n+1 项近似,
S n ( x ) = ∑ k = 0 n x k k ! S_n(x)=\sum_{k=0}^{n}\frac{x^k}{k!} Sn(x)=k=0∑nk!xk
则截断误差为
R n ( x ) = e x − S n ( x ) R_n(x)=e^x-S_n(x) Rn(x)=ex−Sn(x)
当 x x x 固定且展开阶数逐渐增加时,截断误差趋于零。
但实际计算得到的是浮点近似值 S ^ n \widehat{S}_n S n,因此总误差可以分解为
e x − S ^ n = ( e x − S n ) + ( S n − S ^ n ) e^x-\widehat{S}_n=(e^x-S_n)+(S_n-\widehat{S}_n) ex−S n=(ex−Sn)+(Sn−S n)
其中, e x − S n e^x-S_n ex−Sn 表示截断误差, S n − S ^ n S_n-\widehat{S}_n Sn−S n 表示有限精度运算产生的误差。
这一分解说明,提高展开阶数主要用于减小截断误差,却不一定能够消除浮点计算误差。
可以通过下面的 MATLAB 程序观察两类误差共同作用时的表现:
matlab
clc;
clear;
close all;
x = 1;
ref = exp(x);
N = 0:20;
err = zeros(size(N));
for n = N
k = 0:n;
approx = sum(x.^k ./ factorial(k));
err(n+1) = abs(approx-ref);
end
semilogy(N, max(err,realmin), ...
'o-', 'LineWidth', 1.5);
grid on;
xlabel('Taylor 展开阶数');
ylabel('与参考值的绝对差异');
title('Taylor 级数近似 exp(1)');
这里的 exp(1) 是双精度参考值,并非数学意义上的精确值。程序主要用于观察有限项近似与 MATLAB 内置函数结果之间的差异。
在展开阶数较低时,误差主要来自被忽略的高阶项。随着展开阶数增加,截断误差不断减小,但当差异接近浮点精度限制时,继续增加项数的作用逐渐减弱。
输入误差与模型误差
实际科学计算中的输入数据往往来自实验测量、传感器采集或其他数值处理过程,本身可能已经包含误差。
设真实输入为 x x x,实际使用的输入为
x ~ = x + Δ x \widetilde{x}=x+\Delta x x =x+Δx
对于函数 y = f ( x ) y=f(x) y=f(x),输入扰动引起的输出变化为
Δ y = f ( x + Δ x ) − f ( x ) \Delta y=f(x+\Delta x)-f(x) Δy=f(x+Δx)−f(x)
如果函数在附近可微,则当 Δ x \Delta x Δx 足够小时,
Δ y ≈ f ′ ( x ) Δ x \Delta y\approx f'(x)\Delta x Δy≈f′(x)Δx
因此,
∣ Δ y ∣ ≈ ∣ f ′ ( x ) ∣ ∣ Δ x ∣ |\Delta y|\approx |f'(x)|\,|\Delta x| ∣Δy∣≈∣f′(x)∣∣Δx∣
可以看到,即使输入误差很小,当 ∣ f ′ ( x ) ∣ |f'(x)| ∣f′(x)∣ 较大时,输出变化仍然可能十分明显。
此外,实际数学模型通常需要对物理系统作出简化。例如忽略高阶效应、采用理想边界条件,或者使用近似材料参数,都可能造成模型误差。
输入误差、模型误差和浮点舍入误差处于不同层面。提高运算精度可以减小部分浮点误差,却无法恢复测量中缺失的信息,也不能自动修正数学模型本身的偏差。
三、误差传播与数值稳定性
明确误差的来源之后,还需要研究误差在计算过程中如何传播。
数值计算中一个容易混淆的问题是:计算误差大,究竟是数学问题本身对扰动敏感,还是算法在执行过程中放大了误差?
这两种情形需要分别分析。
浮点运算中的误差积累
考虑对一组数进行求和:
S = ∑ i = 1 n x i S=\sum_{i=1}^{n}x_i S=i=1∑nxi
如果采用逐项累加的方式,计算过程可以表示为
s 1 = x 1 s_1=x_1 s1=x1
s k = fl ( s k − 1 + x k ) s_k=\operatorname{fl}(s_{k-1}+x_k) sk=fl(sk−1+xk)
其中, k = 2 , ... , n k=2,\ldots,n k=2,...,n。
每次加法都可能产生新的舍入误差。
对于满足标准浮点误差模型的顺序求和,可以得到如下形式的绝对误差界:
∣ S ^ − S ∣ ≤ γ n − 1 ∑ i = 1 n ∣ x i ∣ |\widehat{S}-S|\leq \gamma_{n-1}\sum_{i=1}^{n}|x_i| ∣S −S∣≤γn−1i=1∑n∣xi∣
其中,
γ n − 1 = ( n − 1 ) u 1 − ( n − 1 ) u \gamma_{n-1}=\frac{(n-1)u}{1-(n-1)u} γn−1=1−(n−1)u(n−1)u
这一关系要求 ( n − 1 ) u < 1 (n-1)u<1 (n−1)u<1,且计算过程中没有发生溢出或其他破坏误差模型的特殊情况。
误差界说明,求和误差不仅与单位舍入误差有关,还与求和项数及各项绝对值之和有关。
当大量正负数相互抵消时,最终的和可能很小,但 ∑ ∣ x i ∣ \sum |x_i| ∑∣xi∣ 仍然很大,因此相对误差可能十分明显。
相消与有效数字损失
考虑函数
f ( x ) = 1 + x − 1 f(x)=\sqrt{1+x}-1 f(x)=1+x −1
当 x x x 很小时, 1 + x \sqrt{1+x} 1+x 非常接近 1。
直接计算两者之差时,操作数中的微小舍入误差可能占最终结果的较大比例。
利用分子有理化,可以得到数学上等价的表达式:
f ( x ) = x 1 + x + 1 f(x)=\frac{x}{\sqrt{1+x}+1} f(x)=1+x +1x
在 MATLAB 中比较:
matlab
clc;
clear;
format long e
x = 1e-12;
f1 = sqrt(1+x)-1;
f2 = x/(sqrt(1+x)+1);
fprintf('直接计算:%.16e\n', f1);
fprintf('有理化后:%.16e\n', f2);
直接计算需要将两个非常接近的数相减,容易发生明显的相对精度损失。这种现象通常称为灾难性消去(catastrophic cancellation)。
有理化后的表达式避免了接近数直接相减,在 x x x 很小且相关运算正常的情况下,通常具有更好的数值表现。
需要注意,相消本身并不必然产生较大的绝对误差。当两个数相减时,真正的问题往往是操作数此前已有的误差相对于很小的最终结果被显著放大。
数学上等价的表达式也不意味着在所有输入范围内具有相同的数值适用性,因此仍然需要结合具体输入范围进行分析。
运算顺序与补偿求和
浮点运算还可能受到运算顺序的影响。
在实数运算中,加法满足结合律:
( a + b ) + c = a + ( b + c ) (a+b)+c=a+(b+c) (a+b)+c=a+(b+c)
但有限精度浮点运算通常不严格满足这一性质。
考虑下面的程序:
matlab
clc;
clear;
a = 1e16;
b = -1e16;
c = 1;
r1 = (a+b)+c;
r2 = a+(b+c);
fprintf('r1 = %.0f\n', r1);
fprintf('r2 = %.0f\n', r2);
结果为:
text
r1 = 1
r2 = 0
第一个表达式先将两个大小相等、符号相反的数相加,得到 0,随后再加上 1。
第二个表达式先计算 b+c,其中较小的数值 1 在舍入过程中被掩盖,因此最终得到 0。
对于大型数组求和,不同的累加顺序可能导致末位数字发生变化。在并行计算中,如果数据分组或归约顺序发生变化,也可能出现类似差异。
对于需要控制求和舍入误差的场景,可以考虑补偿求和方法,例如 Kahan 求和算法。
下面给出一个适用于普通实数数组的简化实现:
matlab
function s = kahanSum(x)
s = 0;
c = 0;
for k = 1:numel(x)
y = x(k)-c;
t = s+y;
c = (t-s)-y;
s = t;
end
end
将函数保存为 kahanSum.m 后,可以使用以下数据进行测试:
matlab
x = [1e16, 1, 1, -1e16];
s1 = sum(x);
s2 = kahanSum(x);
fprintf('MATLAB sum:%.0f\n', s1);
fprintf('Kahan 求和:%.0f\n', s2);
这组数据的精确数学和为 2。由于 MATLAB 内置 sum 的具体归约方式可能与版本及运行环境有关,不应仅凭这一组实验断言它总是采用普通顺序累加。
补偿求和在许多场景中能够降低误差,但同样不能保证所有输入都获得精确结果。它主要改善求和算法中的舍入误差,并不能恢复输入数据在存储之前已经丢失的信息。
问题的条件与算法的稳定性
数值分析中,条件(conditioning)与稳定性(stability)是两个需要严格区分的概念。
问题的条件描述数学问题本身对输入扰动的敏感程度。
设问题可以表示为
y = f ( x ) y=f(x) y=f(x)
当输入发生微小变化 Δ x \Delta x Δx 时,输出变化为
Δ y = f ( x + Δ x ) − f ( x ) \Delta y=f(x+\Delta x)-f(x) Δy=f(x+Δx)−f(x)
如果函数可微,对于足够小的扰动,
Δ y ≈ f ′ ( x ) Δ x \Delta y\approx f'(x)\Delta x Δy≈f′(x)Δx
在标量且 x ≠ 0 x\neq0 x=0、 f ( x ) ≠ 0 f(x)\neq0 f(x)=0 的情况下,可以定义局部相对条件数:
κ f ( x ) = ∣ x f ′ ( x ) f ( x ) ∣ \kappa_f(x)=\left|\frac{xf'(x)}{f(x)}\right| κf(x)= f(x)xf′(x)
由此得到一阶近似关系:
∣ Δ y ∣ ∣ y ∣ ≈ κ f ( x ) ∣ Δ x ∣ ∣ x ∣ \frac{|\Delta y|}{|y|} \approx \kappa_f(x)\frac{|\Delta x|}{|x|} ∣y∣∣Δy∣≈κf(x)∣x∣∣Δx∣
条件数越大,意味着输入中的微小相对扰动越可能造成较大的输出相对变化。
算法的稳定性则描述计算过程是否会过度放大舍入误差,以及计算结果能否被解释为某个邻近问题的准确解。
一个数学问题可能条件良好,但所采用的计算公式不稳定;也可能问题本身高度病态,即使使用稳定算法,计算结果仍然对输入误差十分敏感。
因此,判断数值结果是否可靠,需要同时考虑问题的条件与算法的稳定性。
四、典型数值问题中的误差分析
前面的讨论建立了浮点误差与数值稳定性的基本概念。下面通过线性方程组和数值微分两个问题,进一步观察这些概念在 MATLAB 实际计算中的表现。
线性方程组的条件数与解误差
考虑线性方程组
A x = b Ax=b Ax=b
其中, A A A 为非奇异矩阵, x x x 为待求解向量。
对于给定的诱导矩阵范数,矩阵条件数定义为
κ ( A ) = ∥ A ∥ ∥ A − 1 ∥ \kappa(A)=\|A\|\|A^{-1}\| κ(A)=∥A∥∥A−1∥
当采用二范数时,
κ 2 ( A ) = σ max ( A ) σ min ( A ) \kappa_2(A)=\frac{\sigma_{\max}(A)}{\sigma_{\min}(A)} κ2(A)=σmin(A)σmax(A)
其中, σ max \sigma_{\max} σmax 和 σ min \sigma_{\min} σmin 分别是矩阵的最大和最小奇异值。
条件数可以用于衡量线性系统对输入扰动的敏感程度。
为了观察这一现象,考虑一个接近奇异的矩阵:
A = 1 1 1 1 + ε A= \begin{bmatrix} 1&1\\ 1&1+\varepsilon \end{bmatrix} A=1111+ε
当 ε \varepsilon ε 很小时,两行数据十分接近,矩阵的条件数会增大。
如果取
x = 1 1 x= \begin{bmatrix} 1\\ 1 \end{bmatrix} x=11
则右端项为
b = A x = 2 2 + ε b=Ax= \begin{bmatrix} 2\\ 2+\varepsilon \end{bmatrix} b=Ax=22+ε
假设右端项第二个分量发生微小扰动:
Δ b = 0 η \Delta b= \begin{bmatrix} 0\\ \eta \end{bmatrix} Δb=0η
那么解的变化满足
A Δ x = Δ b A\Delta x=\Delta b AΔx=Δb
可以直接求得
Δ x = − η / ε η / ε \Delta x= \begin{bmatrix} -\eta/\varepsilon\\ \eta/\varepsilon \end{bmatrix} Δx=−η/εη/ε
当 ε \varepsilon ε 很小时,即使 η \eta η 很小,解的变化仍可能十分明显。
在 MATLAB 中,可以通过以下程序验证:
matlab
clc;
clear;
epsilon = 1e-8;
eta = 1e-10;
A = [1, 1;
1, 1+epsilon];
x_true = [1;1];
b = A*x_true;
b_perturbed = b+[0;eta];
x1 = A\b;
x2 = A\b_perturbed;
fprintf('矩阵条件数:%.6e\n', cond(A));
fprintf('输入相对扰动:%.6e\n', ...
norm(b_perturbed-b)/norm(b));
fprintf('解的相对变化:%.6e\n', ...
norm(x2-x1)/norm(x1));
这一实验反映的是输入扰动引起的解变化。其中还包含有限精度浮点运算的影响,因此具体数值可能与理想的解析结果存在细微差异。
对于高条件数问题,单纯提高算法的数值稳定性并不能消除输入扰动造成的敏感性。这属于数学问题本身的性质。
线性系统求解方法的数值差异
MATLAB 中求解线性方程组,常见写法有:
matlab
x = A\b;
以及:
matlab
x = inv(A)*b;
对于非奇异方阵,两者在数学上都对应
x = A − 1 b x=A^{-1}b x=A−1b
但数值实现过程不同。
MATLAB 的反斜杠运算符会根据矩阵的结构和数值性质选择合适的求解方法。对于一般稠密方阵,通常采用基于矩阵分解的算法进行求解。
显式计算 inv(A) 再进行矩阵向量乘法,则需要先形成逆矩阵。这通常增加了不必要的计算工作,也可能引入额外的数值误差。
下面的程序比较两种方法:
matlab
clc;
clear;
rng(1);
n = 300;
A = randn(n);
x_true = ones(n,1);
b = A*x_true;
tic;
x1 = A\b;
t1 = toc;
tic;
x2 = inv(A)*b;
t2 = toc;
err1 = norm(x1-x_true)/norm(x_true);
err2 = norm(x2-x_true)/norm(x_true);
res1 = norm(A*x1-b)/norm(b);
res2 = norm(A*x2-b)/norm(b);
fprintf('反斜杠耗时:%.6f s\n', t1);
fprintf('显式求逆耗时:%.6f s\n', t2);
fprintf('反斜杠相对误差:%.6e\n', err1);
fprintf('显式求逆相对误差:%.6e\n', err2);
fprintf('反斜杠相对残差:%.6e\n', res1);
fprintf('显式求逆相对残差:%.6e\n', res2);
程序中的 x_true 是构造右端项时使用的预设解,b 本身由浮点矩阵乘法产生,因此比较结果也会受到右端项构造误差的影响。
具体运行时间及误差大小取决于矩阵、MATLAB 版本和计算环境,不能保证每次实验都具有相同的误差比例。
不过,当计算目标仅仅是求解线性方程组时,通常应优先使用 A\b,而没有必要显式构造逆矩阵。
数值微分中的截断误差与舍入误差
线性方程组展示了问题条件与求解误差之间的关系。数值微分则能够说明舍入误差和截断误差如何相互制约。
考虑一阶导数的前向差分近似:
f ′ ( x ) ≈ f ( x + h ) − f ( x ) h f'(x)\approx\frac{f(x+h)-f(x)}{h} f′(x)≈hf(x+h)−f(x)
其中, h h h 为差分步长。
对足够光滑的函数进行泰勒展开:
f ( x + h ) = f ( x ) + h f ′ ( x ) + h 2 2 f ′ ′ ( x ) + O ( h 3 ) f(x+h)=f(x)+hf'(x)+\frac{h^2}{2}f''(x)+O(h^3) f(x+h)=f(x)+hf′(x)+2h2f′′(x)+O(h3)
代入差分公式:
f ( x + h ) − f ( x ) h = f ′ ( x ) + h 2 f ′ ′ ( x ) + O ( h 2 ) \frac{f(x+h)-f(x)}{h} =f'(x)+\frac{h}{2}f''(x)+O(h^2) hf(x+h)−f(x)=f′(x)+2hf′′(x)+O(h2)
因此,前向差分的截断误差为
E t r u n c = O ( h ) E_{\mathrm{trunc}}=O(h) Etrunc=O(h)
从这一结果看,减小步长有利于降低截断误差。
但浮点运算会引入另一种影响。
当 h h h 很小时, f ( x + h ) f(x+h) f(x+h) 与 f ( x ) f(x) f(x) 十分接近,两者相减容易产生有效数字损失。由于差值还需要除以 h h h,舍入误差可能进一步被放大。
在常见的误差模型下,前向差分的总误差可以用如下形式估计:
E ( h ) ≲ C 1 h + C 2 u h E(h)\lesssim C_1h+C_2\frac{u}{h} E(h)≲C1h+C2hu
其中,第一项反映截断误差,第二项反映浮点舍入误差的影响, C 1 C_1 C1、 C 2 C_2 C2 为与函数及计算过程有关的非负常数。
当两项都具有代表性时,可以通过平衡它们的数量级得到一个近似的最佳步长:
h o p t ≈ C 2 u C 1 h_{\mathrm{opt}}\approx\sqrt{\frac{C_2u}{C_1}} hopt≈C1C2u
如果相关常数和变量尺度均为正常数量级,则有
h o p t = O ( u ) h_{\mathrm{opt}}=O(\sqrt{u}) hopt=O(u )
这说明,差分步长并不是越小越好。
以函数
f ( x ) = sin x f(x)=\sin x f(x)=sinx
为例,其真实导数为
f ′ ( x ) = cos x f'(x)=\cos x f′(x)=cosx
可以在 MATLAB 中进行实验:
matlab
clc;
clear;
close all;
x = 1;
h = logspace(-1,-16,100);
df_true = cos(x);
df_num = (sin(x+h)-sin(x))./h;
err = abs(df_num-df_true);
loglog(h,err,'o-','LineWidth',1.2);
grid on;
xlabel('步长 h');
ylabel('绝对误差');
title('数值微分误差随步长的变化');
随着步长逐渐减小,数值微分误差通常先下降,达到一定程度后又开始增大。
对于特别小的 h h h,x+h 甚至可能在浮点舍入后等于 x,此时前向差分将无法反映真实的函数变化。
这一实验说明,理论截断误差随步长减小而下降,并不意味着实际计算误差也会一直下降。
类似的现象还存在于数值积分、微分方程求解以及部分迭代算法中。计算参数的选择需要兼顾近似误差与浮点误差。
五、计算结果的精度评估与误差控制
在实际 MATLAB 计算中,通常无法直接获得数学问题的精确解,因此也就不能总是通过比较真实值来评价结果。
此时需要引入能够从已知数据和计算结果出发进行评估的方法,例如残差分析、后向误差分析、条件数估计和独立数值验证。
残差、前向误差与后向误差
对于线性方程组
A x = b Ax=b Ax=b
设精确解为 x x x,计算解为 x ^ \widehat{x} x 。
前向误差(forward error)描述计算解与精确解之间的差异:
e = x − x ^ e=x-\widehat{x} e=x−x
其相对形式为
∥ x − x ^ ∥ ∥ x ∥ \frac{\|x-\widehat{x}\|}{\|x\|} ∥x∥∥x−x ∥
但实际问题中,精确解往往未知,因此前向误差通常不能直接计算。
残差(residual)则定义为
r = b − A x ^ r=b-A\widehat{x} r=b−Ax
它可以通过已知矩阵、右端项和计算解进行评估。
由于
A x = b Ax=b Ax=b
有
A ( x − x ^ ) = r A(x-\widehat{x})=r A(x−x )=r
因此,
x − x ^ = A − 1 r x-\widehat{x}=A^{-1}r x−x =A−1r
进一步得到
∥ x − x ^ ∥ ∥ x ∥ ≤ κ ( A ) ∥ r ∥ ∥ b ∥ \frac{\|x-\widehat{x}\|}{\|x\|} \leq \kappa(A)\frac{\|r\|}{\|b\|} ∥x∥∥x−x ∥≤κ(A)∥b∥∥r∥
这里假设 A A A 非奇异、 b ≠ 0 b\neq0 b=0,并采用相容的向量范数与诱导矩阵范数。
该不等式说明,残差与前向误差之间存在由条件数控制的关系。
当矩阵条件数很大时,小残差并不足以保证小的前向误差。
后向误差(backward error)则从另一个角度评价计算结果。
对于计算解 x ^ \widehat{x} x ,可以考虑是否存在较小的扰动 Δ A \Delta A ΔA 和 Δ b \Delta b Δb,使其成为邻近问题的精确解:
( A + Δ A ) x ^ = b + Δ b (A+\Delta A)\widehat{x}=b+\Delta b (A+ΔA)x =b+Δb
如果所需扰动很小,就说明计算结果可以被解释为某个与原问题十分接近的问题的准确解。
对于线性方程组,一种常用的范数化后向误差指标为
η = ∥ b − A x ^ ∥ ∥ A ∥ ∥ x ^ ∥ + ∥ b ∥ \eta=\frac{\|b-A\widehat{x}\|}{\|A\|\|\widehat{x}\|+\|b\|} η=∥A∥∥x ∥+∥b∥∥b−Ax ∥
在允许矩阵和右端项共同扰动、且采用相容范数的适当定义下,这一指标可以衡量使计算解满足邻近线性系统所需的相对扰动大小。
在 MATLAB 中可以进行如下计算:
matlab
clc;
clear;
A = [4, 1, 2;
1, 3, 0;
2, 0, 5];
b = [7; 4; 3];
x = A\b;
r = b-A*x;
eta = norm(r) / ...
(norm(A)*norm(x)+norm(b));
fprintf('相对后向误差指标:%.6e\n', eta);
fprintf('矩阵条件数:%.6e\n', cond(A));
需要注意,程序计算的残差本身仍然包含浮点舍入误差。在需要严格误差界的场景中,还需要进一步考虑残差计算的精度。
前向误差、后向误差与条件数之间的关系,是理解数值稳定性的关键。
后向稳定算法通常能够得到某个邻近问题的精确解。对于条件良好的数学问题,小的后向误差通常能够转化为较小的前向误差。
但对于高度病态的问题,即使后向误差很小,计算解仍然可能与原问题的精确解存在明显差异。
因此,评价数值结果时,需要同时检查算法的误差表现和数学问题本身的敏感性。
浮点数比较与容差选择
在有限精度计算中,直接使用 == 比较两个由浮点运算得到的近似结果,有时会产生不符合实际需求的判断。
例如:
matlab
a = 0.1+0.2;
b = 0.3;
disp(a==b);
结果为 false。
对于需要比较近似数值的场景,可以根据问题要求设置绝对容差和相对容差。
一种常见的比较准则为
∣ a − b ∣ ≤ a t o l + r t o l max ( ∣ a ∣ , ∣ b ∣ ) |a-b|\leq\mathrm{atol}+\mathrm{rtol}\max(|a|,|b|) ∣a−b∣≤atol+rtolmax(∣a∣,∣b∣)
其中, a t o l \mathrm{atol} atol 为绝对容差, r t o l \mathrm{rtol} rtol 为相对容差。
绝对容差主要用于控制接近零的数值差异,相对容差主要用于控制不同数量级数据的相对差异。
可以编写如下 MATLAB 函数:
matlab
function tf = isNearlyEqual(a,b,atol,rtol)
d = abs(a-b);
scale = max(abs(a),abs(b));
tf = d <= atol + rtol.*scale;
end
将函数保存为 isNearlyEqual.m 后,可以调用:
matlab
a = 0.1+0.2;
b = 0.3;
tf = isNearlyEqual(a,b,1e-12,1e-10);
上述函数适用于常规有限实数的比较。对于数组、复数、NaN 和无穷大等情况,应根据具体需求进一步规定比较方式。
容差的选择需要考虑数据数量级、输入精度以及算法误差,不能简单将 eps 作为所有计算问题的统一容差。
对于明确要求数值完全相同的离散状态或特定数据结构,精确比较仍然具有合理用途。
计算公式、数值类型与可变精度
提高计算结果的准确程度,通常应优先从计算公式和数值算法入手。
对于容易发生相消的表达式,可以通过代数变换降低舍入误差的影响;对于线性方程组,可以选择稳定的矩阵分解方法;对于数值微分和数值积分,则需要合理选择步长及误差容差。
这些方法针对的是算法的数值性质,往往能够在不增加运算位数的情况下改善结果。
MATLAB 中常用的浮点数据类型包括 single 和 double。
| 数据类型 | 二进制有效精度 | 十进制有效数字(约) | 存储空间 |
|---|---|---|---|
single |
24 位 | 7 位 | 4 字节 |
double |
53 位 | 16 位 | 8 字节 |
单精度能够减少数据存储需求,在某些大规模计算中也可能具有性能优势。
双精度具有更高的数值分辨能力,通常更适合对精度有较高要求的科学计算。
但即使采用双精度,也不能保证所有问题都具有 16 位准确结果。实际有效精度仍然受到问题条件和算法稳定性的影响。
当双精度不足以满足计算要求时,可以考虑使用 Symbolic Math Toolbox 提供的可变精度计算功能。
例如:
matlab
digits(50);
x = vpa(sym(1)/7);
disp(x);
这段程序将计算精度设置为 50 位十进制有效数字。
使用可变精度时,输入数据的构造方式尤其重要。
例如:
matlab
digits(50);
a = vpa('0.123456789123456789');
b = vpa(sym(1)/7);
disp(a);
disp(b);
这里使用十进制字符串和符号有理数构造输入,可以避免在进入高精度计算之前就将数据舍入为普通双精度浮点数。
对于部分常见双精度表达式,vpa 可能尝试恢复额外精度,但这种行为不能作为恢复任意输入精度的通用方法。
format long 只控制显示格式,并不会增加变量的存储精度;将已经丢失有效数字的数据转换为更高精度类型,也不能自动恢复原始信息。
数值结果的独立验证
对于具有解析解的问题,最直接的验证方法是比较数值结果与解析解。
对于没有解析解的问题,可以通过不同算法、不同离散参数或不同计算精度下的结果进行交叉检验。
在线性方程组中,可以结合残差、后向误差和条件数评价计算结果。
对于数值微分,可以改变差分步长,观察误差或近似结果的变化趋势。
对于微分方程和偏微分方程数值求解,则可以通过网格加密、时间步长收敛性以及守恒关系等方式检查结果。
需要注意,不同方法得到相近的结果,并不必然意味着计算结果准确。如果不同算法使用了相同的不准确输入或存在共同的模型误差,结果仍然可能出现一致的偏差。
因此,数值验证的可靠性不仅取决于比较了多少种计算方法,还取决于这些验证方法能否提供相对独立的误差信息。
六、结语
MATLAB 中的数值精度问题,涉及浮点数表示、误差度量、误差传播以及数学问题本身的敏感性。单次浮点运算的舍入误差通常很小,但在大量运算、严重相消或病态问题中,误差可能对最终结果产生明显影响。
分析这类问题时,需要区分数学问题的条件与计算算法的稳定性。条件数反映输入扰动对结果的影响,算法稳定性描述计算过程中的误差表现,而前向误差、后向误差和残差则从不同角度评价计算结果。
对于实际计算,提高精度也不应仅仅理解为增加浮点数的有效位数。合理选择计算公式、采用稳定算法、控制离散误差以及验证输入数据,同样可能影响最终结果的可靠性。
MATLAB 能够输出很多位数字,但这些数字并不必然具有相同的可信程度。只有结合误差分析、问题条件和数值验证,才能判断计算结果在多大程度上能够代表原数学问题的解。
参考资料
1 MathWorks. Floating-Point Numbers . MATLAB Documentation.
https://www.mathworks.com/help/matlab/matlab_prog/floating-point-numbers.html
2 MathWorks. eps --- Floating-point relative accuracy . MATLAB Documentation.
https://www.mathworks.com/help/matlab/ref/eps.html
3 MathWorks. cond --- Condition number for inversion . MATLAB Documentation.
https://www.mathworks.com/help/matlab/ref/cond.html
4 MathWorks. inv --- Matrix inverse . MATLAB Documentation.
https://www.mathworks.com/help/matlab/ref/inv.html
5 MathWorks. vpa --- Variable-precision arithmetic . Symbolic Math Toolbox Documentation.
https://www.mathworks.com/help/symbolic/sym.vpa.html
6 Higham, N. J. Accuracy and Stability of Numerical Algorithms. 2nd ed. Philadelphia: SIAM, 2002.
7 Trefethen, L. N., and Bau, D. III. Numerical Linear Algebra. Philadelphia: SIAM, 1997.