浮動小数点の誤差 — 丸め・桁落ち・安定な計算

数学では同じ式でも、計算機では答えが違うことがあります。二進の目盛り、近い数の差、加算の順序をたどり、二次方程式と分散の計算を安定な形へ書き換えます。

この単元の目標
  • 二進浮動小数点で有限桁に収まる数と、丸めが必要な数を区別できる。
  • 絶対誤差と相対誤差を使い、桁落ちや情報落ちの影響を説明できる。
  • 問題そのものの敏感さと、計算手順の不安定さを区別できる。
  • 二次方程式・分散を等価な別の式で計算し、独立した根拠で結果を点検できる。
📑 目次
  1. 1.数字をしまう場所には、飛び飛びの目盛りがある
  2. 2.丸めは「いつでも四捨五入」ではない
  3. 3.桁落ち:引き算が、隠れていた誤差を目立たせる
  4. 4.例題2:二次方程式の小さい根を救う
  5. 5.加算にも落とし穴:小さい項が吸収される
  6. 6.例題3:分散は大きな二数の差にしない
  7. 7.「等しい」を何のために判定するか
  8. まとめ:出力の桁数より、計算の道筋を点検する

1.数字をしまう場所には、飛び飛びの目盛りがある

定規に目盛りがなければ、その位置をぴったり記録できません。計算機も有限個のビットしか持たないため、すべての実数を保存できません。浮動小数点数は「細かさを固定する定規」ではなく、「数の大きさに応じて目盛りの間隔を変える定規」です。大きな数ほど目盛りも粗くなります。まず入力を目盛りに合わせ、その後の演算結果も再び目盛りに合わせます。これが丸め誤差の出発点です。

以下の実行例はJavaScriptのNumber(IEEE 754のbinary64、いわゆる倍精度)をNode.jsで計算したものです。数値は教材用で、特記しない変数は無次元。実測データの不確かさや反復法の打切り誤差ではなく、表現と演算の丸めを中心に扱います。出力の小数表記は格納値を識別する表示であり、十進の数学的な真値そのものとは限りません。

通常の正規化数は、符号・有効数字部分・指数に分けて表されます。binary64では符号に1ビット、指数部に11ビット、小数部に52ビットを使い、先頭の1を暗黙に補って有効精度は53ビットです。次の式のeは格納された指数部そのものではなく、補正後の指数です。

x=(−1)s(1.b1b2⋯b52)2 2e,bi∈{0,1},−1022≤e≤1023x=(-1)^s(1.b_1b_2\cdots b_{52})_2\,2^e,\qquad b_i\in\{0,1\},\quad -1022\le e\le1023

例えば十進の0.125は1/8なので二進では0.001と有限桁で表せます。一方、0.1=1/10は二進では循環小数です。既約分数の分母が2のべきだけなら二進で有限桁になりますが、実際にbinary64へ正確に格納するには精度と指数の範囲にも収まる必要があります。十進で短く書けることと、二進で正確に格納できることは別なのです。

上の形は0や極小の非正規化数にはそのまま使えません。範囲を超える計算ではInfinity、実数として定まらない演算ではNaNが生じることもあります。以下の相対誤差モデルを、アンダーフロー・オーバーフローやNaNにまで無条件に適用しないでください。

2.丸めは「いつでも四捨五入」ではない

最も近い目盛りを選ぶと、ずれは通常その近くの目盛り間隔の半分以下です。ちょうど中間なら、binary64の通常の丸めでは末尾ビットが偶数側、つまり0になる方を選びます。十進の「5なら必ず切り上げる」とは異なります。1以上2未満の目盛り間隔は2⁻⁵²、2以上4未満では2⁻⁵¹です。Number.EPSILONは1とその次の数との間隔であって、全域共通の間隔ではありません。

ε=2−52≈2.22×10−16,u=2−53≈1.11×10−16\varepsilon=2^{-52}\approx2.22\times10^{-16},\qquad u=2^{-53}\approx1.11\times10^{-16}

εはここではNumber.EPSILON、uは単位丸め誤差と呼び分けます。文献によって「機械イプシロン」の指す量が違うため、定義の確認が必要です。正確な演算結果zが正規化範囲にあり、通常の最近接丸めを行う基本演算では、次の相対誤差モデルを使えます。flは「計算機の目盛りに丸めた値」です。

fl⁡(z)=z(1+δ),∣δ∣≤u\operatorname{fl}(z)=z(1+\delta),\qquad |\delta|\le u
例題1:0.1+0.2と0.3を比較する

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円なら、その数円は重大です。近い数を引くと共通する上位桁が消え、もともとの入力誤差が答えに対して大きく見える現象を桁落ちと呼びます。「引き算の丸めだけが大きい」のではなく、入力までに失われた情報が差の計算で露わになる場合もあります。

Eabs=∣y^−y∣,Erel=∣y^−y∣∣y∣(y≠0)E_{\mathrm{abs}}=|\widehat y-y|,\qquad E_{\mathrm{rel}}=\frac{|\widehat y-y|}{|y|}\quad(y\ne0)

yは基準となる正確な値、ŷは計算結果です。絶対誤差の単位はyと同じで、相対誤差は無次元です。真値が0なら相対誤差は定義できないため、絶対誤差などで評価します。近似入力a+Δaとb+Δbの差は、厳密に引けたとしても本来の差a−bからΔa−Δbだけずれます。

∣Δa−Δb∣∣a−b∣≤∣Δa∣+∣Δb∣∣a−b∣≤η∣a∣+∣b∣∣a−b∣(a≠b)\frac{|\Delta a-\Delta b|}{|a-b|}\le\frac{|\Delta a|+|\Delta b|}{|a-b|}\le\eta\frac{|a|+|b|}{|a-b|}\quad(a\ne b)

最後の不等式は各入力の絶対誤差がそれぞれη|a|、η|b|以下という仮定です。差が小さいほど誤差を増幅する係数が大きくなります。これは「独立に誤差を含む二つの入力の差」という問題自体の敏感さです。一方、一つの入力から作った式に不要な引き算があるなら、式変形で避けられることがあります。入力の敏感さを条件の悪さ、手順が余計な誤差を増幅する性質を不安定さとして区別しましょう。

近い数の引き算が必ず不正確というわけではありません。例えば1.5−1=0.5はすべて正確に表せます。差を取る前の値が正確かが重要です。また、既に丸められた二つの測定値から、失った桁を式変形だけで復元することはできません。

4.例題2:二次方程式の小さい根を救う

解の公式は数学的には正しくても、二つの大きな数を引いて小さな根を得る形になることがあります。そこで小さい差を直接作らず、先に大きい根を足し算で求め、根の積から小さい根を求めます。ここではx²−Bx+1=0、B=10⁶を扱います。Bは正で、判別式も正です。

🔍 同じ根を、引き算なしで表す

  1. 解の公式をそのまま使う
    r−=B−B2−42,r+=B+B2−42r_- = \frac{B-\sqrt{B^2-4}}{2},\qquad r_+=\frac{B+\sqrt{B^2-4}}{2}

    Bが大きいと平方根がBに近く、小さい根の分子で上位桁が消えます。

  2. 分子を有理化する
    r−=2B+B2−4=1r+r_- = \frac{2}{B+\sqrt{B^2-4}}=\frac{1}{r_+}

    B>2なら分母は正で0ではありません。根の積r₋r₊=1からも得られます。

例題2:二通りの出力を比較する

次の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になったことだけで全桁正確とは証明できません。

この形はB>2という条件での改善です。極端に大きなBではB * B自体がInfinityになるため、これだけで万能にはなりません。一般の二次方程式では係数の符号、重根、0の根、オーバーフローも別に扱います。「代数的に同じ式」から「計算機でも同じ出力」は導けません。

5.加算にも落とし穴:小さい項が吸収される

大きい値に小さい値を足すと、小さい値が目盛りの半分に届かず消えることがあります。これは情報落ちとも呼ばれ、近い数の引き算による桁落ちと区別できます。Node.jsでは(1e16 + 1) - 1e16は0ですが、(1e16 - 1e16) + 1は1です。数学では両方1でも、途中の丸めが違います。加算の結合法則を計算機の出力へ無条件に持ち込まないことが大切です。

対策として、小さい項どうしを先にまとめる、二つずつ組にするペアワイズ加算、失われた分を補助変数で追う補償和などがあります。ただし正負の打ち消しが大きいデータでは、単に小さい順へ並べるだけで常に最善とは限りません。順序を変えて答えが大きく変わるなら、問題の条件と算法を調べる合図です。

6.例題3:分散は大きな二数の差にしない

散らばりを知りたいのに、巨大な平均の二乗同士を引く必要はありません。中心からの偏差を先に作れば、小さい散らばりを直接計算できます。ここでの分散は与えたn個のデータについて分母をnとする記述統計量で、不偏分散ではありません。数学的には次の二式が一致します。

xˉ=1n∑i=1nxi,V=1n∑i=1nxi2−xˉ2=1n∑i=1n(xi−xˉ)2(n≥1)\bar x=\frac1n\sum_{i=1}^n x_i,\qquad V=\frac1n\sum_{i=1}^n x_i^2-\bar x^2=\frac1n\sum_{i=1}^n(x_i-\bar x)^2\quad(n\ge1)
例題3:分散が2と1.25に分かれる理由

架空データ[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は無次元で、両方とも非負です。

∣a−b∣≤atol+rtolmax⁡(∣a∣,∣b∣)|a-b|\le\mathrm{atol}+\mathrm{rtol}\max(|a|,|b|)
Number.EPSILONを何にでも足せば解決、ではありません。例えば金額の確定処理で必要なのは曖昧な近似一致ではなく、最小通貨単位の整数化や所定の十進丸め規則です。整数化もNumberの安全整数範囲を超えないことが条件で、必要ならBigIntなどの適切な表現を選びます。また近似一致は一般に推移的でないため、識別子の一致判定の代わりにしてはいけません。
練習:原因と対策をセットで答える
  1. 0.375と0.2のうち、binary64で正確に表現できるのはどちらか。既約分数から説明しよう。

    解答・解説

    0.375=3/8は二進の0.011であり、精度・指数の範囲にも収まるので正確です。0.2=1/5は分母に2以外の素因数があるため二進で循環し、有限のbinary64では丸めが必要です。

  2. 正確な値y=0.0002をŷ=0.00021と計算した。絶対誤差と相対誤差を求め、y=0ならどう評価するか述べよう。

    解答・解説

    絶対誤差は0.00001=10⁻⁵。相対誤差は0.00001/0.0002=0.05=5%です。y=0なら相対誤差の分母が0になるので、絶対誤差や別に定めた基準尺度で評価します。

  3. 正確な実数上でx²−100x+1=0の大きい根をrとする。小さい根を桁落ちの少ない形で表そう。またrが0でない理由は?

    解答・解説

    r=(100+√9996)/2を先に求め、小さい根を1/rとします。根の積が1であり、rは正なので0でありません。大きい正数どうしの差を作る小さい根の直接公式を避けています。丸め誤差が完全に消えるという意味ではありません。

  4. 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. 二つの近似計算結果を比べる際の方針として適切なのは?

まとめ:出力の桁数より、計算の道筋を点検する

  1. 入力の表現を確認する。二進で正確か、既に丸められているか、単位と範囲は適切か。
  2. 近い数の差、大きい数と小さい数の加算、途中の二乗による範囲超過を探す。
  3. 条件の悪い問題なのか、式変形や加算方法で改善できる手順の問題なのかを区別する。
  4. 既知の恒等式、簡単な分数の手計算、高精度計算など別の経路で点検する。等式の一つが成立するだけで全桁正確とは断定しない。
根拠:IEEE Std 754-2019のbinary64と最近接・偶数丸め、およびECMAScriptのNumber型の定義に基づく説明です。例題の式は根の積・有理化・分散の平方展開から導出しました。掲載したJavaScriptはNode.jsで実行確認し、例題2の比較値は十進多倍長計算、例題3は正確な偏差の手計算でも検算しています。特定の入力での改善は、すべての入力について誤差ゼロを保証するものではありません。
📝 メモこの端末のみに保存
メモ欄はページ読み込み後に使えるようになります。