之前一直想学数值分析,网上查阅了下,发现一个斯坦福的公开课还不错,讲义,作业,视频都有,所以决定10周时间学完,后续会做一些笔记。

课程主页:https://graphics.stanford.edu/courses/cs205a-13-fall/schedule.html

这次回顾第一讲,这一讲主要介绍数值分析的基本概念。

Chapter 1 数值与误差分析

存储带小数的数字

计算机中的数字都是以二进制形式存储,例如$463$可以表示为:

表格的形式如下:

$1$ $1$ $1$ $0$ $0$ $1$ $1$ $1$ $1$
$2^8$ $2^7$ $2^6$ $2^5$ $2^4$ $2^3$ $2^2$ $2^1$ $2^0$

小数也同理,只要加上$2^{-n}$次方即可,例如$463.25$:

$1$ $1$ $1$ $0$ $0$ $1$ $1$ $1$ $1.$ $0$ $1$
$2^8$ $2^7$ $2^6$ $2^5$ $2^4$ $2^3$ $2^2$ $2^1$ $2^0$ $2^{-1}$ $2^{-2}$

但是很多小数会表示为循环小数,例如:

这种形式的小数无法用有限二进制字符串表示,所以在计算机中的计算就会有近似的问题,考虑如下代码:

double x = 1.0;
double y = x / 3.0;
if (x == y *3.0) cout << " They are equal !";
else cout << " They are NOT equal .";

从数学上来说,上述结果应该输出” They are equal !”,但实际上会输出” They are NOT equal .”,原因是我们刚刚提到的问题。为了避免上述问题,我们应该使用如下代码:

double x = 1.0;
double y = x / 3.0;
if ( fabs (x-y *3.0) < numeric_limits < double >:: epsilon ) cout << " They are equal !";
else cout << " They are NOT equal .";

接下来讨论几种表示数的方式。

定点数表示

定点数的存储方式很简单,我们存储从$2^{-k}$到$2^l$之间的$2$的幂对应的$0/1$系数,其中$k,l \in \mathbb N$。对$127.75$来说,$k=2, l=7​$。定点数存储的优势在于可以使用整数的加减法对其运算,例如

但是定点数的精度是一个很大的问题,如果我们只有一个小数点的精度($k=1$),那么对于

我们会将其近似为

这就会产生很大的误差。

浮点数表示

浮点数表示的想法源于科学计数法,我们定义如下参数:

  • 基数$\beta \in \mathbb N$,对于常见的科学计数法,$\beta =10$。
  • 精度$p \in \mathbb N​$表示十进制中的小数点的位数。
  • 指数范围$[L,U]$代表$b​$的可能值。

那么浮点数表示如下:

其中$d_{k}\in [0,\beta-1], b\in [L,U]$。注意浮点数的间隔是不均匀的,这是因为通常我们有

我们还定义机器精度为最小的$\epsilon_m >0$,使得$1+\epsilon_m$可以表示。

其他

其他表示方式使用的较少,这里稍作介绍,例如用整数对$(a,b)​$表示有理数,定义加法和加法和除法:

还有一种比较有用的表示方式为$(a,\epsilon ),a,\epsilon \in \mathbb R$,其中$a$表示真值,$\epsilon $表示误差。可以理解为$(a,\epsilon )$表示范围$(a-\epsilon ,a+\epsilon)$,注意此时加减法时要同时更新误差:

理解误差

计算过程中有很多误差,常见的误差形式如下:

  • 截断误差
  • 离散化误差
  • 建模误差
  • 经验常数
  • 用户输入

更具体的介绍可以参考讲义。

误差分类

考虑如下例子:

虽然都是$0.01$的误差,但是显然后者的影响更小,这就引入了绝对误差和相对误差的概念:

  • 绝对误差:测量的绝对误差是近似值与其真值之间的差值。
  • 相对误差:测量的相对误差是绝对误差除以真值。

不过在绝大多数情况下,真值都不知道(否则无需测量),解决方式是进行估计。

为了介绍后面的内容,考虑求根问题:

在实际处理中,假设我们找到$x$,因为我们不知道根,所以我们只能根据$|f(x)|$有多小来判断$x$是否接近根,这就引入了后向误差的概念:

  • 向后误差:Backward error is given by the amount a problem statement would have to change to realize a given approximation of its solution.

这段话比较难翻译,大意就是可以通过一个间接问题来估计原来的误差,例如之前的例子,我们很难估计$|x’-x|​$,但是$|f(x)|​$就很好计算。再来看一个例子,考虑$\sqrt x​$的计算,我们要判断$1.4​$是否接近$\sqrt 2​$,此处向后误差为:

与之相对应的,直接计算$|x- x’|$被称为向前误差

敏感性

因为有时候很小的向后误差并不能保证很小的向前误差,这就引出了条件数的概念:

  • 条件数:微小扰动下向前误差的改变量和向后误差的改变量的比值。

来看一个例子,考虑一元函数$f(x)=0$的求根问题:

实际问题

实际中经常会碰到数值溢出的问题,例如下面计算向量范数的问题:

double normSquared = 0;
for (int i = 0; i < n; i++)
	normSquared += x[i]*x[i];
return sqrt ( normSquared );

如果$n$很大,那么求平方和时就会溢出,一个改进方法是使用如下事实:

因为$|\frac{x_i} M| \le 1$,所以就可以避免溢出的问题,代码如下:

double maxElement = epsilon ; // don 't want to divide by zero !
for (int i = 0; i < n; i++)
		maxElement = max( maxElement , fabs (x[i ]));
for (int i = 0; i < n; i++) {
        double scaled = x[i] / maxElement ;
        normSquared += scaled * scaled ;
}
return sqrt ( normSquared ) * maxElement ;

实际中还有一个比较重要的算法为Kahan summation,考虑累加法:

double sum = 0;
for (int i = 0; i < n; i++)
    sum += x[i];

当$n$很大时,sum会变得非常大,以至于加上x[i]不变(因为精度为题),从而会产生较大的误差,Kahan summation的思路是考虑累加时产生的误差:

对应的代码如下:

double sum = 0;
double compensation = 0; // an approximation of the error

for (int i = 0; i < n; i++) {
    // try to add back to both x[i] and the missing part
    double nextTerm = x[i] + compensation ;
    
    // compute the summation result of this iteration
    double nextSum = sum + nextTerm ;
    
    // compute the compensation as the difference between the term you wished
    // to add and the actual result
    compensation = nextTerm - ( nextSum - sum );
    
    sum = nextSum ;
}