浮動小数点の誤差 — 丸め・桁落ち・安定な計算
数学では同じ式でも、計算機では答えが違うことがあります。二進の目盛り、近い数の差、加算の順序をたどり、二次方程式と分散の計算を安定な形へ書き換えます。
- 二進浮動小数点で有限桁に収まる数と、丸めが必要な数を区別できる。
- 絶対誤差と相対誤差を使い、桁落ちや情報落ちの影響を説明できる。
- 問題そのものの敏感さと、計算手順の不安定さを区別できる。
- 二次方程式・分散を等価な別の式で計算し、独立した根拠で結果を点検できる。
📑 目次
1.数字をしまう場所には、飛び飛びの目盛りがある
定規に目盛りがなければ、その位置をぴったり記録できません。計算機も有限個のビットしか持たないため、すべての実数を保存できません。浮動小数点数は「細かさを固定する定規」ではなく、「数の大きさに応じて目盛りの間隔を変える定規」です。大きな数ほど目盛りも粗くなります。まず入力を目盛りに合わせ、その後の演算結果も再び目盛りに合わせます。これが丸め誤差の出発点です。
通常の正規化数は、符号・有効数字部分・指数に分けて表されます。binary64では符号に1ビット、指数部に11ビット、小数部に52ビットを使い、先頭の1を暗黙に補って有効精度は53ビットです。次の式のeは格納された指数部そのものではなく、補正後の指数です。
例えば十進の0.125は1/8なので二進では0.001と有限桁で表せます。一方、0.1=1/10は二進では循環小数です。既約分数の分母が2のべきだけなら二進で有限桁になりますが、実際にbinary64へ正確に格納するには精度と指数の範囲にも収まる必要があります。十進で短く書けることと、二進で正確に格納できることは別なのです。
2.丸めは「いつでも四捨五入」ではない
最も近い目盛りを選ぶと、ずれは通常その近くの目盛り間隔の半分以下です。ちょうど中間なら、binary64の通常の丸めでは末尾ビットが偶数側、つまり0になる方を選びます。十進の「5なら必ず切り上げる」とは異なります。1以上2未満の目盛り間隔は2⁻⁵²、2以上4未満では2⁻⁵¹です。Number.EPSILONは1とその次の数との間隔であって、全域共通の間隔ではありません。
εはここではNumber.EPSILON、uは単位丸め誤差と呼び分けます。文献によって「機械イプシロン」の指す量が違うため、定義の確認が必要です。正確な演算結果zが正規化範囲にあり、通常の最近接丸めを行う基本演算では、次の相対誤差モデルを使えます。flは「計算機の目盛りに丸めた値」です。
Node.jsでconsole.log(0.1 + 0.2)とconsole.log(0.1 + 0.2 === 0.3)を実行するとどうなるか。またconsole.log(0.125 + 0.125 === 0.25)との違いはなぜか。
解答・解説
出力は順に0.30000000000000004、false、true。0.1と0.2は入力時点で二進の近似値になり、加算でも丸めます。0.3も近似値ですが、加算結果とは隣の格納値になります。対して1/8+1/8=1/4は入力と結果がいずれも正確に表現できます。これは加算器の故障でも、0.1+0.2=0.3という数学の否定でもありません。
手を動かす確認:console.log(1 + 2 -53 === 1)はtrue、console.log(1 + 2 -52 === 1)はfalseです。前者は1と次の数のちょうど中間を1へ丸め、後者は次の数そのものになります。「0でない値を足したのに変わらない」ことが有限の目盛りから説明できます。
3.桁落ち:引き算が、隠れていた誤差を目立たせる
残高が約100万円の二つの口座を比較するとき、各残高が数円ずれていても割合としては小さい誤差です。しかし差が10円なら、その数円は重大です。近い数を引くと共通する上位桁が消え、もともとの入力誤差が答えに対して大きく見える現象を桁落ちと呼びます。「引き算の丸めだけが大きい」のではなく、入力までに失われた情報が差の計算で露わになる場合もあります。
yは基準となる正確な値、ŷは計算結果です。絶対誤差の単位はyと同じで、相対誤差は無次元です。真値が0なら相対誤差は定義できないため、絶対誤差などで評価します。近似入力a+Δaとb+Δbの差は、厳密に引けたとしても本来の差a−bからΔa−Δbだけずれます。
最後の不等式は各入力の絶対誤差がそれぞれη|a|、η|b|以下という仮定です。差が小さいほど誤差を増幅する係数が大きくなります。これは「独立に誤差を含む二つの入力の差」という問題自体の敏感さです。一方、一つの入力から作った式に不要な引き算があるなら、式変形で避けられることがあります。入力の敏感さを条件の悪さ、手順が余計な誤差を増幅する性質を不安定さとして区別しましょう。
4.例題2:二次方程式の小さい根を救う
解の公式は数学的には正しくても、二つの大きな数を引いて小さな根を得る形になることがあります。そこで小さい差を直接作らず、先に大きい根を足し算で求め、根の積から小さい根を求めます。ここではx²−Bx+1=0、B=10⁶を扱います。Bは正で、判別式も正です。
🔍 同じ根を、引き算なしで表す
- 解の公式をそのまま使う
Bが大きいと平方根がBに近く、小さい根の分子で上位桁が消えます。
- 分子を有理化する
B>2なら分母は正で0ではありません。根の積r₋r₊=1からも得られます。
次のJavaScriptを順に実行する。const B = 1e6; const d = Math.sqrt(B * B - 4); const big = (B + d) / 2; const direct = (B - d) / 2; const stable = 1 / big; console.log(big, direct, stable); 小さい根としてどちらが適切か。
解答・解説
実行結果はbig=999999.999999、direct=0.00000100000761449337、stable=0.000001000000000001。高精度の独立計算では小さい根は約0.000001000000000001000000000002なので、stableの方が近くなります。directの相対誤差は約7.61×10⁻⁶。さらにdirect * bigは1.00000761449237、stable * bigは1となり、根の積による点検でも改善が見えます。ただし積が1になったことだけで全桁正確とは証明できません。
5.加算にも落とし穴:小さい項が吸収される
大きい値に小さい値を足すと、小さい値が目盛りの半分に届かず消えることがあります。これは情報落ちとも呼ばれ、近い数の引き算による桁落ちと区別できます。Node.jsでは(1e16 + 1) - 1e16は0ですが、(1e16 - 1e16) + 1は1です。数学では両方1でも、途中の丸めが違います。加算の結合法則を計算機の出力へ無条件に持ち込まないことが大切です。
対策として、小さい項どうしを先にまとめる、二つずつ組にするペアワイズ加算、失われた分を補助変数で追う補償和などがあります。ただし正負の打ち消しが大きいデータでは、単に小さい順へ並べるだけで常に最善とは限りません。順序を変えて答えが大きく変わるなら、問題の条件と算法を調べる合図です。
6.例題3:分散は大きな二数の差にしない
散らばりを知りたいのに、巨大な平均の二乗同士を引く必要はありません。中心からの偏差を先に作れば、小さい散らばりを直接計算できます。ここでの分散は与えたn個のデータについて分母をnとする記述統計量で、不偏分散ではありません。数学的には次の二式が一致します。
架空データ[100000000, 100000001, 100000002, 100000003]を使う。const xs = [1e8, 1e8 + 1, 1e8 + 2, 1e8 + 3]; const n = xs.length; const m = xs.reduce((s, x) => s + x, 0) / n; const v1 = xs.reduce((s, x) => s + x * x, 0) / n - m * m; const v2 = xs.reduce((s, x) => s + (x - m) ** 2, 0) / n; console.log(m, v1, v2); を実行し、手計算と比較しよう。
解答・解説
出力は100000001.5、2、1.25。正確な偏差は−1.5、−0.5、0.5、1.5なので、分散は(2.25+0.25+0.25+2.25)/4=1.25です。v1は約10¹⁶の数どうしを引くため、その前の二乗や加算の丸めが表に出ます。v1の相対誤差は|2−1.25|/1.25=0.6、つまり60%。平均を出してから偏差平方を足す二段階のv2では、この例の値は正確に計算できます。
この例の入力整数はすべてbinary64で正確に表せます。したがって差は測定ミスでなく演算手順の問題です。ただし二段階法でも、一般には平均や和の丸めが残り、常に正確ではありません。単位のあるデータなら平均の単位は入力と同じ、分散の単位はその二乗です。入力段階で違いが丸め落とされていれば、分散の式を変えても違いを復元できません。
7.「等しい」を何のために判定するか
近似計算の比較では、誤差を何桁まで許すかを仕様から決めます。小さい数には絶対許容誤差、大きい数には相対許容誤差を使うと考えやすくなります。例えば有限の実数a、bについて、次の形があります。atolは値と同じ単位、rtolは無次元で、両方とも非負です。
0.375と0.2のうち、binary64で正確に表現できるのはどちらか。既約分数から説明しよう。
解答・解説
0.375=3/8は二進の0.011であり、精度・指数の範囲にも収まるので正確です。0.2=1/5は分母に2以外の素因数があるため二進で循環し、有限のbinary64では丸めが必要です。
正確な値y=0.0002をŷ=0.00021と計算した。絶対誤差と相対誤差を求め、y=0ならどう評価するか述べよう。
解答・解説
絶対誤差は0.00001=10⁻⁵。相対誤差は0.00001/0.0002=0.05=5%です。y=0なら相対誤差の分母が0になるので、絶対誤差や別に定めた基準尺度で評価します。
正確な実数上でx²−100x+1=0の大きい根をrとする。小さい根を桁落ちの少ない形で表そう。またrが0でない理由は?
解答・解説
r=(100+√9996)/2を先に求め、小さい根を1/rとします。根の積が1であり、rは正なので0でありません。大きい正数どうしの差を作る小さい根の直接公式を避けています。丸め誤差が完全に消えるという意味ではありません。
a=1000、b=1000.001、atol=0.000001、rtol=0.000001として、本文の許容誤差の式を正確な十進数で評価せよ。
解答・解説
差は0.001。右辺は0.000001+0.000001×1000.001=0.001001001なので、許容範囲内です。これはこの仕様で近いと判定するだけで、数学的に等しいという宣言ではありません。
問1. binary64の1以上2未満の目盛り間隔に等しいのは?
問2. 桁落ちについて最も適切な説明は?
問3. 例題3で分散2より1.25を信頼できる独立した根拠は?
問4. 二つの近似計算結果を比べる際の方針として適切なのは?
まとめ:出力の桁数より、計算の道筋を点検する
- 入力の表現を確認する。二進で正確か、既に丸められているか、単位と範囲は適切か。
- 近い数の差、大きい数と小さい数の加算、途中の二乗による範囲超過を探す。
- 条件の悪い問題なのか、式変形や加算方法で改善できる手順の問題なのかを区別する。
- 既知の恒等式、簡単な分数の手計算、高精度計算など別の経路で点検する。等式の一つが成立するだけで全桁正確とは断定しない。