数値的安定性と誤差
数値的安定性と誤差
(0.1 + 0.2) == 0.3 という式は、一見すると極めて当たり前のように思える。しかし、コンピュータにおける浮動小数点数の保持方法が原因で、多くのプログラミング言語においてこの等式は成り立たない。つまり、結果は false となる。
だが、もう一つの疑問が生じる。なぜ (0.5 + 0.25) == 0.75 という等式なら成り立つのか?
浮動小数点演算を行う際、多かれ少なかれ誤差は発生するものだ。ここでは、なぜ誤差が生じるのか、そして計算上で誤差を回避するために何ができるのかについて議論していく。
コンピュータにおける浮動小数点数の保持方法
コンピュータ内部で浮動小数点数はビット列として表現され、IEEE 754 によって標準化されている。単精度浮動小数点数は 32 ビット、倍精度は 64 ビットで構成される。その構造は、正負を表す符号部、仮数部(小数部)、指数部に分かれている。
- 符号部(1ビット):0 は正の数、1 は負の数
- 仮数部
- 指数部
| 型 | サイズ | 指数部 | 仮数部 |
|---|---|---|---|
| 単精度 | 32bit | 8bit | 23bit |
| 倍精度 | 64bit | 11bit | 52bit |
単精度を例にとると、指数部は 8 ビットあるため、表現可能な範囲は 0 〜 255 となる。しかし負の指数も表現する必要があるため、IEEE 754 の仕様ではバイアス値(オフセット)を加えることで、最終的に指数部に格納される数値を決定する。単精度の場合、バイアス値は 127 となる。
例えば、-14.75 を浮動小数点数で表現すると次のようになる:
- 符号部:負の数なので
1 - 指数部:まず 14.75 を2進数で表現すると
1110.11となり、正規化すると1.11011 * 2^3となる。指数部は 3 に 127 を足して 130 となり、2進数表記では10000010となる - 仮数部:
11011、残りのビットは0
したがって、-14.75 の浮動小数点数表現は 1 10000010 11011000000000000000000 となる。
ここから、誤差がどこから生じるのかも理解できる。指数が負になると得られる数値はすべて 0 < x < 1 の範囲となるため、2^-x の組み合わせで表現できない数値は、限りなく近づけることはできても完全に一致させることはできない。冒頭で触れた 0.5 + 0.25 は、これらの数値が 2^-1 + 2^-2 と表せるため、計算上で誤差が生じないのだ。
なぜこのように格納するのか?
科学的記数法(Scientific notation)を用いることで、様々なスケールの数値を扱いながら、一定の精度を維持することができる。例えば 0.00000012345 や 1234567890000 は、科学的記数法を使えば 1.2345E-7 や 1.23456789E12 と表せる。コンピュータ内部では通常、浮動小数点形式で格納されるが、これは科学的記数法と似ており、10進数を2進数に置き換えたものに過ぎない。
実数は無限に存在するが、コンピュータの記憶領域には限りがある。そのため、どのような方法で格納しても必ず誤差は生じる。僕たちにできるのは、精度と表現可能な数値範囲との間でトレードオフを行うことだけだ。
有効数字
有効数字は、精度を判断するための指標となる。例えば 0.001 や 0.0135 といった数値は、 や と書き表すことができる。このとき 0.001 の有効数字は 1 桁、1.35 の有効数字は 3 桁となる。有効数字の桁数が多いほど、精度は高くなる。
Wikipedia には、分かりやすい判定ルールが記載されている:
- すべてのゼロ以外の数字は有効である
- ゼロ以外の数字の間にあるゼロは有効である
- 先頭のゼロは常に無効である
- 小数点を必要とする数値において、末尾のゼロ(最後のゼロ以外の数字の後に続くゼロ)は有効である
- 小数点を必要としない数値において、末尾のゼロは有効である場合もあれば無効である場合もある。追加の記号や誤差情報に基づいて判断する必要がある。
Cancellation of significant digits(桁落ち)
絶対値が非常に近い2つの浮動小数点数同士で引き算を行うと、大半の数値が同一であるため多くの 0 が残り、有効数字が減少してしまう。この現象を cancellation of significant digits(桁落ち)と呼ぶ。
例えば、(1.234567890 - 1.234567889) の結果は本来 0.000000001 であるが、精度が不足している状況では 0 になってしまう可能性がある。
これは数値計算において特に注意すべき点だ。例として半角の公式を見てみよう:
角度が比較的小さいとき、cos は限りなく 1 に近くなるため、1 から引くと有効数字の喪失(桁落ち)が非常に起きやすくなる。例えば、角度が 1 度のときに半角の公式を使用すると(有効数字 6 桁の場合):
一方、右辺の公式を使わずに直接テーブル引きなどから計算すると:
両者の間にかなり明確な誤差が出ていることがわかる。数値計算においては細心の注意が必要だ。桁落ちを防ぐにはいくつかのアプローチがある:
-
絶対値が近い2つの数値同士での減算を極力避ける
-
別の公式を用いて計算する(上述の半角公式の変形など)
- 本質的には、絶対値が近い2つの数値同士で減算させないようにすることだ
-
計算精度を上げる
結論
ある程度経験のあるエンジニアなら、浮動小数点演算に誤差が生じることや、0.1 + 0.2 != 0.3 となる理由はなんとなく知っているはずだ。この記事では小数の保持方法、IEEE 754、そして有効数字の喪失(桁落ち)について一歩踏み込んで掘り下げてみた。
半角の公式は高校の三角関数で頻出の問題であり、僕にとっても単に公式に当てはめるだけのものだった。
しかし、実際のアプリケーションではすべてコンピュータが計算しているし、現実世界には 30 度や 60 度といった計算しやすい数値ばかりが存在するわけでもない。そして、学校の先生が半角の公式に有効数字の問題が潜んでいることなど教えてくれるはずもないのだ。
関連記事
- 測定が目標になるとき:窓税からPull Request数まで かつて僕は小さなツールを自作し、四半期で自分がどれだけPRに貢献したか、レビューコメントをどれだけ残したか、チケットをどれだけ消化したかを集計して、上司にアウトプットを証明しようとしたことがある。上司は淡々と、評価はアウトプットだけで見るものではないと言った。数年後、僕はようやく理解した――測定が目標になるとき、それはもはや良い測定ではなくなるのだ。英国の窓税、ハノイのネズミ駆除の報奨金から、現代のPR数による開発者評価に至るまで、そのメカニズムはまったく同じだ。
- Cloudflare Images を画像ストレージ・変換ソリューションとして使う ウェブページに画像を1枚置くのはフロントエンドにとって最も簡単なことだが、リサイズや各種フォーマットの生成、さらにはトラフィックの負荷に耐えることまで完璧にやろうとすると、実際には一つの包括的なソリューションが必要になる。僕はその後、すべて Cloudflare Images に任せるようになり、オリジナル画像1枚だけを渡すようにしている。
- もう AWS Access Key を使うのはやめよう Access Key は AWS において見落とされがちなセキュリティリスクだ。OIDC と IAM Role を組み合わせることで、GitHub Actions にシークレットを一切保持させることなく、安全に AWS リソースを操作できるようにする。
- データベース主キー:AUTO_INCREMENT、UUID、そしてUUIDv7 バックエンド開発で度々直面する主キーの決定。auto incrementを使うべきか、それともUUIDか?衝突への懸念は?UUIDv7とcreated_at + インデックスの性能差はどれほどか?実際に2,000万件のデータで検証したベンチマークと設計上の意思決定を解説する。