こちらの記事で、0.1
を浮動小数点数に変換すると0.1
よりわずかに大きい数になることが分かりました。
0.1は浮動小数点数で正確に表せないのに、printしたときに0.1と表示されるのはなぜか
ということは、0.1
を足していくと、プラスの誤差が蓄積していくはず。例えば、0.1
を10回足せば、1
よりもわずかに大きくなると予想しました。
試してみます。
total = 0
for i in range(10):
total += 0.1
print(total)
#=> 0.9999999999999999
予想に反して1
よりも小さな数が出力されました。今回はこの現象について調べました。
環境
この記事ではPython 3.7を使用しています。
【前提】浮動小数点数
この記事で、以降"浮動小数点数"という場合は、"IEEE 754 倍精度"のことを指します。
浮動小数点数のフォーマットは、数を以下の形式に変換し、sign
、exp
、frac
を順に並べたものです。
(-1)^{sign} \times 2^{exp - 1023} \times (1 + frac \times 2^{-52})
それぞれの記号の名前とビット数は以下の通りです。
|記号|日本語名|英語名|ビット数|
|---|---|---|---|---|
|sign|符号部|sign|1|
|exp|指数部|exponent|11|
|frac|仮数部|fraction|52|
0.1を浮動小数点数で表すと
https://tools.m-bsys.com/calculators/ieee754.php0.1
は、浮動小数点数の2進数表現では、
符号 = 0
指数 = 01111111011
仮数 = 1001100110011001100110011001100110011001100110011010
10進数に変換すると、
符号 = 0
指数 = 1019
仮数 = 2702159776422298
です。浮動小数点形式の指数は1023
が足されていますので、これを引くと-4
になります。
浮動小数点数で表された0.1
を10進数に変すると
0.1000000000000000055511151231257827021181583404541015625
です。
0.1ずつ足していったときの数
0.1
の浮動小数点数は0.1
よりわずかに大きいのですが、10回足した時に 1
にならないとは、どこかで逆転現象が起こっているようです。
途中の計算結果を見てみましょう。Decimal
に浮動小数点を渡し、print
すると浮動小数点数を10進数に変換したときの値が表示されます。
from decimal import Decimal
total = 0
d_total = Decimal('0')
for i in range(10):
total += 0.1
d_total += Decimal('0.1')
print(f'{d_total}|{Decimal(total)}')
実行結果がこちらです。
Decimal | 計算結果 |
---|---|
0.1 | 0.1000000000000000055511151231257827021181583404541015625 |
0.2 | 0.200000000000000011102230246251565404236316680908203125 |
0.3 | 0.3000000000000000444089209850062616169452667236328125 |
0.4 | 0.40000000000000002220446049250313080847263336181640625 |
0.5 | 0.5 |
0.6 | 0.59999999999999997779553950749686919152736663818359375 |
0.7 | 0.6999999999999999555910790149937383830547332763671875 |
0.8 | 0.79999999999999993338661852249060757458209991455078125 |
0.9 | 0.899999999999999911182158029987476766109466552734375 |
1.0 | 0.99999999999999988897769753748434595763683319091796875 |
0.1
から0.4
まではそれぞれ10進数として計算したものより多く、0.5
はぴったり、0.6
から1.0
は10進数として計算したものよりも小さいという結果が出ました。
0.1ずつ足していったとき、どのくらいずつ足されているのかを確かめる
では、0.1
ずつ足していったとき、実際にどのくらい加算されているか確認しましょう。プログラムはこちらです。PythonのDecimalの有効桁がデフォルトで28桁なので、事前に十分な大きさに変更しておきます。
from decimal import Decimal, getcontext
# Decimalの有効桁数を64桁に変更
getcontext().prec = 64
total = prev = 0
d_total = Decimal('0')
for i in range(10):
total += 0.1
d_total += Decimal('0.1')
print(f' |{Decimal(total) - Decimal(prev)}')
print(f'{d_total}|{Decimal(total)}')
prev = total
実行結果です。ヘッダを除いた奇数行に浮動小数点数での計算結果、偶数行にそれぞれの差分を表示しました。
Decimal | 計算結果/差分 |
---|---|
0.1 | 0.1000000000000000055511151231257827021181583404541015625 |
0.1000000000000000055511151231257827021181583404541015625 |
|
0.2 | 0.200000000000000011102230246251565404236316680908203125 |
0.100000000000000033306690738754696212708950042724609375 |
|
0.3 | 0.3000000000000000444089209850062616169452667236328125 |
0.09999999999999997779553950749686919152736663818359375 |
|
0.4 | 0.40000000000000002220446049250313080847263336181640625 |
0.09999999999999997779553950749686919152736663818359375 |
|
0.5 | 0.5 |
0.09999999999999997779553950749686919152736663818359375 |
|
0.6 | 0.59999999999999997779553950749686919152736663818359375 |
0.09999999999999997779553950749686919152736663818359375 |
|
0.7 | 0.6999999999999999555910790149937383830547332763671875 |
0.09999999999999997779553950749686919152736663818359375 |
|
0.8 | 0.79999999999999993338661852249060757458209991455078125 |
0.09999999999999997779553950749686919152736663818359375 |
|
0.9 | 0.899999999999999911182158029987476766109466552734375 |
0.09999999999999997779553950749686919152736663818359375 |
|
1.0 | 0.99999999999999988897769753748434595763683319091796875 |
0.1 → 0.2
の差分は0.1
を浮動小数点数で表した値と同じですが、0.2 → 0.3
はそれより大きいです。
0.3
以降は同じ値が足されていて、0.1
を浮動小数点数で表した値より小さくなっています。
ちなみに0.4
以降全ての値において0.1
より小さい数が足されるわけでなく、1.1 → 1.2
で再び大きくなります。
期待値 | 計算結果/差分 |
---|---|
1.0 | 0.99999999999999988897769753748434595763683319091796875 |
0.09999999999999997779553950749686919152736663818359375 |
|
1.1 | 1.0999999999999998667732370449812151491641998291015625 |
0.1000000000000000888178419700125232338905334472656250 |
|
1.2 | 1.1999999999999999555910790149937383830547332763671875 |
0.1000000000000000888178419700125232338905334472656250 |
|
1.3 | 1.3000000000000000444089209850062616169452667236328125 |
0.1000000000000000888178419700125232338905334472656250 |
|
1.4 | 1.4000000000000001332267629550187848508358001708984375 |
0.1000000000000000888178419700125232338905334472656250 |
|
1.5 | 1.5000000000000002220446049250313080847263336181640625 |
なぜ0.1
を足していったときに0.1
の浮動小数点数ずつ増えないのでしょうか。
それには丸め誤差が関係しています。
浮動小数点数の足し算の手順
正の浮動小数点数同士の足し算は以下の手順で行われます。
① 指数が小さい方を指数が大きい方に合わせる
② 指数を大きくしたぶん、仮数を小さくして調整する
③ 仮数を足し合わせる
④ 繰り上げが発生した場合は、指数に1を足しそのぶん仮数を小さくする
⑤ 仮数のあふれてしまった桁を丸める(偶数丸め)
10進数での足し算の例
浮動小数点数の足し算の手順を、10進数の例で示します。
有効数字5桁の10進数の足し算を考えます。
\begin{array}{llcll}
&9.8192 & \times & 10^2 & (= 981.92)\\
+ &4.7533 & \times & 10^1 & (= 47.533)\\
\hline
\end{array}
以下、手順を追って説明します。
① 指数が小さい方を指数が大きい方に合わせる
② 指数を大きくしたぶん、仮数を小さくして調整する
足し算をするために指数をそろえます。今回、指数が大きい方は9.8192 x 10^2
なので、こちらに合うように、4.7533 x 10^1
を変形します。
\begin{array}{llcr}
&4.7533 & \times & 10^1\\
= &0.47533 & \times & 10^2
\end{array}
③ 仮数を足し合わせる
足し算を行います。
\begin{array}{llcr}
&\phantom{1}9.8192 & \times & 10^2\\
+ &\phantom{1}0.47533 & \times & 10^2\\
\hline
& 10.29453 & \times & 10^2
\end{array}
④ 繰り上げが発生した場合は、指数に1を足しそのぶん仮数を小さくする
整数部が10なので、繰り上げが発生しています。整数部分を1桁にするため、指数に1を足し、そのぶん仮数を10で割ります。
\begin{array}{llcr}
&10.29453 & \times & 10^2\\
= &1.029453 & \times & 10^3
\end{array}
⑤ あふれてしまった桁を丸める(偶数丸め)
今回、有効桁は5桁ですので、末尾の53
の処理を考えなくてはなりません。ここでは偶数丸めを採用するので、切り上げられます。
\begin{array}{llcr}
&1.029453 & \times & 10^3\\
\rightarrow &1.0295 & \times & 10^3
\end{array}
浮動小数点数の足し算の手順が10進数で確かめられました。
偶数丸め
偶数丸めは、四捨五入と似ている丸め方法ですが、丸める対象が2つの値のちょうど中間のとき、その上の桁が偶数になるように切り上げ・切り捨てを行う方法です。
例えば、小数点第一位で偶数丸めをするとき、1.5
は2
に、4.5
は4
になります。
4.51
は5
です。
保護桁とスティッキビット
10進数での足し算の例では、指数を合わせたときに、末尾の桁(上の例では53
)が有効桁からあふれてしまいました。これらの値は、最後に丸めを行うので取っておかなければいけません。(手順②の、指数を大きくしたぶん、仮数を小さくして調整する、で有効桁に入りきらないからといって切り捨ててはいけません)
偶数丸めでは、
- 5より大きい → 切り上げ
- 5ぴったり → 偶数方向へ切り上げか切り捨て
- 5より小さい → 切り捨て
なので、あふれてしまった桁の最上位桁だけでなく、それより下の桁の情報も必要です。ただ、最上位桁以外の桁の情報として必要なのは、0ぴったりかどうかなので、真偽値を1つ用意しておけば十分そうです。
保護桁(Guard digits)はあふれてしまった桁を保存する領域です。正の数同士の加算では、保護桁は1桁あれば十分です。(減算やマイナスの数を扱う場合はもう1ビット必要)。保護桁が1桁の場合、上の例では、あふれてしまった桁53
のうち、最上位ビットの5
が格納されます。
保護桁以下の数に1以上の数が含まれているかどうかを表す真偽値をスティッキビット(Sticky bit)といいます。上の例では、あふれてしまった桁の3
は1以上なのでtrue
(もしくは1
)が設定されます。
保護桁とスティッキビットを使用して10進数での足し算を書き換える
上の10進数の足し算の例に保護桁とスティッキビットを適用します。
\begin{array}{llcll}
&9.8192 & \times & 10^2 & (= 981.92)\\
+ &4.7533 & \times & 10^1 & (= 47.533)\\
\hline
\end{array}
① 指数が小さい方を指数が大きい方に合わせる
② 指数を大きくしたぶん、仮数を小さくして調整する
\begin{array}{lrcll}
&4.7533 & \times & 10^1&\\
= &0.47533 & \times & 10^2&(保護桁=3)
\end{array}
末尾の3
があふれてしまったので、保護桁に保存しました。
③ 仮数を足し合わせる
\begin{array}{llcr}
&\phantom{1}9.8192 & \times & 10^2&\\
+ &\phantom{1}0.4753 & \times & 10^2&(保護桁=3)\\
\hline
& 10.2945 & \times & 10^2&(保護桁=3)
\end{array}
④ 繰り上げが発生した場合は、指数に1を足しそのぶん仮数を小さくする
\begin{array}{llcrl}
&10.2945 & \times & 10^2&\\
= &1.0294 & \times & 10^3&(保護桁=5, スティッキビット=true)
\end{array}
末尾の5
があふれてしまったので、保護桁に保存しました。このときもともと保護桁に保存されていた3
はスティッキビットに保存されます。スティッキビットは数が1以上かどうかを保持するので、true
が設定されます。
⑤ あふれてしまった桁を丸める(偶数丸め)
保護桁とスティッキビットを使用して丸めを行います、
保護桁=5
でスティッキビット=true
なので切り上げが行われます。
\begin{array}{llcrl}
&1.0294 & \times & 10^3&(保護桁=5, スティッキビット=true)\\
\rightarrow &1.0295 & \times & 10^3
\end{array}
保護桁とスティッキッビットを用いた浮動小数点数の足し算の手順が10進数で確かめられました。2進数の場合もこれと同様のことを行います。
後編へ
長くなったので記事を分けます。後編では0.1
を足していったときの挙動を確認します。参考にしたサイトなども後編にまとめています。