高度なプログラミングの根底には「数学」があります。検索アルゴリズム、ゲームの物理エンジン、AIといったプログラムの良しあしは数学を理解しているかどうかで決まります。新刊『 プログラミングのための数学大全 』から「コンピューターはどのように数値を扱うのか」を抜粋して紹介します。第3回は実数を扱うときの誤差と表現不可能な数についてです。
前々回 で「基数」、 前回 は「整数と実数」を説明しました。実数は浮動小数点数で表現するのでしたね。
浮動小数点数を演算するときに心しておくべきは、答えが合っていることはまずないものの、普通に使うには困らないということです。任意の2つの浮動小数点数の間で区間(近接する値の差)が最小になるのは、仮数部の最下位ビットが1だけ変化したときです。たとえば、仮数部が3ビットのみで、最下位ビットが2-3=0.125に対応しているときです。この型の2つの浮動小数点数の最小区間はeを現在の指数として、2eの0.125倍です。
では、指数が増えたときの最小区間はどうなるでしょう。表1-5を見てください。
指数が1増えると、表現可能な最小区間は2倍になります。そのため、32ビット浮動小数点数なら23ビット、64ビット浮動小数点なら52ビットというように( 前回参照 )、仮数部のビット数にかかわらず、指数が0から最大でも±1のときに精度を最大にできます。スケーリングが可能で高い精度が求められる課題では、これを思い出すといいでしょう。
丸め誤差はなぜ起きる?
丸め誤差がどう起きるかを見てみましょう。考え方としては、p=πに対して計算を繰り返しても、pはπのまま変わらないというものです。ただし、πは超越数[代数方程式の解ではない数のこと]なので、コンピューターは正確に表現できず誤差を生じます。それを確かめるために、Pythonの有理数演算(分数)で計算を再現します。分数なら、Pythonは必要な精度で表現できます。
プログラムは、はじめにp=πとして以下の手順で必要なだけ計算を実行します。
数学的には、計算の開始時にp=πなら、どんなに計算を繰り返しても最後までp=πは変わりません。この処理は、本書のGitHubサイトにある丸め誤差をデモするプログラム(roundoff.py)で実行できます。試してみましょう。
1行目はPythonから見たπを示しています。2行目は計算を求められた回数だけ繰り返した結果です(ここでは1回だけ)。厳密な有理数演算を使用しています(Pythonのfractionsモジュール)。3行目は標準的な64ビット浮動小数点数演算の結果です。有理数演算の結果は1行目の値と一致していますが、浮動小数点演算の結果は、矢印で示した小数点第15位からすでに違っています。
小数点第15位からのズレは、取り立てて問題にするほどではありません。しかし、計算を5回繰り返したらどうなるでしょうか。
小数点第9位から違ってきました。でもまだ許容範囲です。もう少し繰り返しましょう。
小数点第4位から違いが表れました。そろそろ問題になるレベルです。この際なので、思い切って続けてみましょう。
こうなったらもう手に負えません。丸め誤差は致命的です。
かくして、いとも簡単に深刻な丸め誤差が生じるため、大変な事件につながらないか心配になります。複雑な浮動小数点数の演算を実装するときは、くれぐれも慎重に行ってください。実際、丸め誤差が多くの人命を奪った事例もあります。「パトリオットミサイル 丸め誤差」で検索してみてください。
表現不可能な数である「非数値」と「無限大」
科学技術分野のプログラミングでコンピューターに長く関わっていれば、「nan」とか「inf」といった出力に遭遇するはずです。nanは「非数値」(NaN=Not a Number)のことで、infは正または負の「無限大」を意味します。
IEEE(米国電気電子技術者協会)の浮動小数点数の規格では、意味をなさない計算のことをnanと表します。たとえば、負の数値のlogを要求するとNaNが出力されます。入力した値や数式そのものに間違いがある場合がほとんどです。
無限大は、計算結果が大きすぎて選択したIEEEフォーマットでは表現できないときに示されます。次の例を見てください。
gccで-lmオプションを付けて(数値演算ライブラリをリンクして)コンパイルし実行すると、次のように出力されます。
負の数の対数など存在しませんし、e1,000は64ビット浮動小数点数には収まらないため、納得の結果です。
余談ですが、IEEE754[浮動小数点数をコンピューター上で効率的かつ正確に表現するための国際規格、 前回参照 ]は、NaNにはペイロード(仮数部に格納されたユーザー定義のビット値)が搭載できる、としています。このビット値が何を表現するかはユーザーに任せられているので、なぜNaNになるのかを説明する情報をそこに入れておくことができます。ほとんど使われない機能ですが、役に立つことも少なくありません。
コンピューターと数値について簡単に解説してきましたが、締めくくりに、デイビッド・ゴールドバーグの古典的論文『What Every Computer Scientist Should Know About Floating-Point Arithmetic』[日本語概説『浮動小数点演算について計算機科学者は何を知っておくべきか』の付録]を読まれるよう強くお勧めしておきます。1991年に書かれたものですが、あなどるなかれ。今でもまったく色あせていません。ネット上で公開されています。
これまでのおさらい
この連載では、コンピューターが、整数と実数を格納する方法について解説しました。まずは基数をおさらいし、2、8、10、16といった異なる基数間の変換方法を見てきました( 前々回 )。
次に、2の補数を使った2進数で整数を格納する方法を手始めに、コンピューターがメモリー内で使用する表現について考えました。続いて、メモリー内の実数の表現である浮動小数点数を説明しました。IEEE754形式が一般的なので、その浮動小数点数の格納方法に焦点を当てました( 前回 )。
そして今回、浮動小数点数の計算にまとわりつく悪夢、丸め誤差について、単純な計算を繰り返すことで爆発的に値が暴れ出す様子を実際に見て学びました。最後に、NaN(Not a Number)と無限大にも触れました。
本書『 プログラミングのための数学大全 』の次の章では純粋な数学の世界に飛び込み、集合論と抽象代数の基本概念から基礎を学びます。
数学とプログラミングには深い結びつきがあります。本書を読めば、基本的なコーディングから本格的なソフトウェア開発までに欠かせない「数学全般」が一気に学べます。
ロナルド・T・クナイスル(著)、井上卓馬(監訳)、金井哲夫(翻訳)/日経BP/5500円(税込み)









