浮動小数点
浮動小数演算には様々な罠があります
その中でも特徴的な罠として結合法則が成り立たないというものがあります
つまり、浮動小数点演算においては
(a+b)+c \neq a+(b+c)
です。
これは、大きな値と小さな値を足す時に顕著で、小さな値の情報が落ちてしまいます。
簡単な例でコレを実感してみましょう
バーゼル問題
\sum_{n=1}^{\infty} \frac{1}{n^2}=\frac{\pi^2}{6}
これはバーゼル問題と呼ばれる有名な問題で、オイラーによって解かれました。
個人的には三角波のフーリエ展開を用いて計算する手法が一番わかり易い気がします。
バーゼル問題の計算
この無限和を有限で打ち切って計算して円周率を計算してみることにします。
どれくらいの速度で収束するのでしょうか?
足していく値が$\mathcal{O}(n^{-2})$であるので、直感的には$\mathcal{O}(n^{-1})$ぐらいで真の値に収束する気がします。(少し手を動かすと証明も出来ます。)
つまり$n$まで計算すれば$\frac{1}{n}$ぐらいの精度で計算できるわけです。
では実際にコレを用いて単精度浮動少数で円周率を計算してみましょう
Haskellでちょろっと書いてみると
Prelude> (\t->sqrt(t*6))$ foldr (+) 0.0 (take 10000000 (map(\x->1/x/x) [1..]))::Float
3.1415925
ちゃんと円周率が計算できたようです。ではfoldrをfoldlに変えてみます。
Prelude> (\t->sqrt(t*6))$ foldl (+) 0.0 (take 10000000 (map(\x->1/x/x) [1..]))::Float
3.1413934
大幅に精度が落ちてしまいました。$10^7$まで足したのに、$10^4$ぐらいの誤差が出ています。わざわざ計算コストをかけて計算したのに非常にもったいない!
積み残し誤差
なぜこのような差が生まれてしまったのでしょうか?
foldrは右から、foldlは左からリストを足し合わせる関数です。
foldlは大きな数字から足し合わせ、foldrは小さな数字から足し合わせることになります。
つまり、foldlでは小さな数字を大きな数字に順次足し合わせることになります。すると小さな数字の情報が次々と欠落してしまい、大きな誤差が生じてしまうのです。
これを積み残し誤差と呼び、同じくらいのスケールの数が足されるように足す順番を工夫することが一般に必要です。
おわりに
浮動小数点演算にはこの他にも様々な罠があります。浮動小数を扱うときはこういった罠に気をつけていきましょう!