LoginSignup
15
9

More than 3 years have passed since last update.

0.1の浮動小数点数は0.1より大きいのに、10回足すと1.0より小さいのはなぜか【前編】

Last updated at Posted at 2020-01-28

こちらの記事で、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 倍精度"のことを指します。

浮動小数点数のフォーマットは、数を以下の形式に変換し、signexpfracを順に並べたものです。

(-1)^{sign} \times 2^{exp - 1023} \times (1 + frac \times 2^{-52})

それぞれの記号の名前とビット数は以下の通りです。

記号 日本語名 英語名 ビット数
sign 符号部 sign 1
exp 指数部 exponent 11
frac 仮数部 fraction 52

0.1を浮動小数点数で表すと

to_float_point_0_1.png
https://tools.m-bsys.com/calculators/ieee754.php

0.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.52に、4.54になります。
4.515です。

保護桁とスティッキビット

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を足していったときの挙動を確認します。参考にしたサイトなども後編にまとめています。

0.1の浮動小数点数は0.1より大きいのに、10回足すと1.0より小さいのはなぜか【後編】

15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9