20. 浮動小数点数の補足

  • 教科書3.4節。

20.1. 丸め誤差

丸め誤差

コンピュータは0,1の2進数ですべての情報を処理するため、少数点数を厳密に表現することは難しい。できなくはないが、プログラミング言語や書き方の工夫が必要であり、一般的には「このぐらいの精度で表現できていれば一般用途上問題ない」ように処理していることが多い。

2進数を扱うことに加えて、その副次的結果としてコンピュータでは少数点数を表現するために 浮動小数点数 (floating point numbers) と呼ばれる方式を採用している。浮動小数点数では、数値を 仮数部, 基数, 指数部 の組み合わせで \(仮数部 \times 基数^{指数部}\) として表現する。例えば、0.5を10進数の浮動小数点数で表すと \(5.0 \times 10^{-1}\) となる。元の値は0.5なのに対し、浮動小数点数では小数点の位置がズレている。このように小数点の位置が動くため「浮動小数点」数と呼ばれている。

ここでは浮動小数点数を扱う際の問題点について紹介する。1つ目が丸め誤差だ。 これは、小数点のある数は必ずしも厳密には表現できない(ことがある)ことを指している。スライド内のコード例では0.1を10回足しており、その結果は 1.0 になることを期待する。しかし 1.0 と等しいかどうかを確認すると False となり、print出力すると 0.99999,,, という数字が出力されてしまっている。


20.2. 丸め誤差を踏まえた数値演算

浮動小数点数には丸め誤差が起こりうるため、「N回足した値が1.0と等しければ」というように特定の値と等しいことを条件式として書いてしまうと不具合を起こしてしまうことが 極めて多い。例えば以下のコードを実行してみよう。気持ちとしては変数 sum が1.0になった時点でループを抜けてほしいが、実際には抜けること無く最後までループを繰り返してしまう。

sum = 0
for i in range(20):
    sum += 0.1
    if sum == 1.0:
        print(sum)
        break
    else:
        print(sum)

この問題を避けるため、浮動小数点数を用いた条件式は0に近いある小さな数値を設定した上で「特定の範囲内に入れば」のように指定することが多い。例えば上記の例は以下のように記述する。数学用語としてのepsilon(イプシロン)は「非常に小さな数」を表す記号として用いることが多い。1e-5指数表記 であり 1.0 * 10**(-5) == 0.00001 である。この小さな数値を用いて \(特定の値 \pm epsion\) で下限と上限を設けることで範囲を設定し、その範囲内にいることを「特定の値に十分等しい」と看做すようにコードを書く。

epsilon = 1e-5
sum = 0
for i in range(20):
    sum += 0.1
    if 1.0 - epsilon < sum < 1 + epsilon:
        print(sum)
        break
    else:
        print(sum)

Tip

上記に近い実装がmath.iscloseとして提供されています。もしこのような「丸め誤差を踏まえた近似処理」ではなく、正確な10進数表現が必要な場合にはdecimal, fractionsモジュールの利用を検討してみてください。


20.3. オーバーフロー

オーバーフロー

浮動小数点数2つ目の問題点は オーバーフロー(桁あふれ) と呼ばれている。浮動小数点形式は固定小数点形式と比べると幅広い数値を扱うことが可能だが、それでも大きすぎる数字と小さすぎる数字を同時に扱うことは困難である。

図では2つの例を示している。1つ目の例は「扱うことすらできないケース」であり、数値として保存することもできず OverflowError と返されている。2つ目の例は、1つ目よりは小さな数値(100桁+少数点数)を扱っているが、演算結果は想定と大きく異なっている。たかだか0.1を足しただけなのに、結果はそうなっていない。これは誤差と捉えても良いだろうか。

一般的には問題にならないことが多いが、このようなケースが起こりうることを認識しておこう。