Programmierkurs
für Naturwissenschaftler/innen

数値計算で注意すべきこと: 誤差入門

最後に、数値計算のプログラムを書く際に注意すべきことを述べておこう。

数値計算で注意すべきことについて、誤差の問題がある。 数値計算は誤差との戦いであると言える。 (他にも、計算時間や記憶容量とも戦っている。敵が多い。)

ここではごくごく簡単なものをいくつか紹介する。

小数に注意

0.1 という数字は一見「綺麗な」 (切りの良い) 数字に見えるかもしれない。 が、それは 10 進法という「特別な」状況にあるからで、他ではそうとは限らない。 実際、0.1 を 2 進数であらわすと、

0.000110011…

という無限小数になる。 コンピューターは内部で 2 進数で処理しており、なおかつコンピューターは無限桁を扱うことはできないので、 どこかで切り上げ/切り捨てしなくてはならない。 そのため誤差が生じる (丸め誤差)。

このため、0.1 を 10 回足しても 1 にならない (場合がある)。

次のプログラムを実行すると何が起こるか (起こるべきか)、考えよ。 その後にこのプログラムをテキストボックスに入力し実行してみよ。

let sum = 0;

for (let i = 0; i < 10; i++) {
  sum += 0.1;
  console.log(sum)
}

実行結果

おそらく以下のような結果になったであろう。

0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999

従って、小数を == で比較した場合、期待通りになるとは限らない。

次のプログラムを実行すると何が起こるか (起こるべきか)、考えよ。 その後にこのプログラムをテキストボックスに入力し実行してみよ。

let sum = 0;

for (let i = 0; i < 20; i++) {
  sum += 0.1;
  if (sum == 1) {
    console.log("Now sum is 1");
  }
}
console.log("sum is ", sum);

実行結果

小数を == で比較してはならない

引き算に注意

ものすごく「近い」(非常に曖昧な表現だが、ここでは深入りしないことにする) 数字の引き算は期待通りの結果にならないことがある。

例えば、$1.00000001 - 1$ を考えてみよう。

  1. $(1.00000001 - 1)$ を紙と鉛筆で計算せよ。

  2. この計算を次のプログラムで実行してみよ。

let n1     = 1.00000001;
let n2     = 1;
let answer = 0.00000001;

console.log(n1 - n2);
console.log("The answer should be:", answer);

実行結果

この例で分かるように、0.00000001 (= 1e-08) という数字を表現できないのではなく (answer は正しく表示できている)、引き算をしたことによって誤差が生じているのである。

この問題のため、一般に数値微分はコンピューターでは「難しい」部類の計算であるといえる。

さて、「ではどのようにすれば良いか」と聞きたくなるだろうが、残念ながらそれは状況によるとしか言えない。 計算順序を変える、アルゴリズムを変える、などがあるが、どの方法が最適かは問題による。

足し算に注意

ものすごく大きい数とものすごく小さい数を足そうとするとうまくいかないことがある。

次のプログラムは 1e7 ($= 10,000,000$) に 1e-10 ($= 0.000 000 000 1$) を100回足したもの (n) を計算するプログラムである。 これを実行してみよ。

let n1 = 1e7;
let n2 = 1e-10;

let n = n1;
for (let i = 0; i < 100; i++) {
  n += n2;
}
console.log(n);

let answer = n1 + n2 * 100;
console.log("The answer should be:", answer);

実行結果

こちらは10進数で考えると分かりやすい。 有効数字 3 桁の場合、10.0 に 0.01 を足すと、 $$ 10.0 + 0.01 = 10.0\cancel{1} = 10.0 $$ となる。 従って 10.0 に何回 0.01 を足しても 10.0 にしかならない。

数値積分で分割数を極端に大きくしすぎると、この問題が発生する可能性がある。