若葉の技術メモ

コンピュータやプログラミングに関して調べたり、取り組んだりしたことをまとめる若葉のノート📓。コンピュータ・プログラミング初めてって方も一緒に勉強していきましょう!初心は大事!いつでも若葉☘のような意気込みで!

  • No.   

若葉の技術メモ

コンピュータやプログラミング・数理に関して調べたり、取り組んだりしたことをまとめる若葉のノート。

コンピュータ・プログラミング・数理が初めてって方も一緒に勉強していきましょう!

初心は大事!いつでも若葉☘のような意気込みで!

【プログラミング初心者必見!】数値計算で気をつけるべきこと4選:Pythonも間違える!?

コンピュータといえば...

人間にできない複雑な計算を高速で解くことができる!

どんなに大規模なデータでも完璧に処理してくれる!

って思いますよね?

ですが、どんな高性能なコンピュータを以てしても、どうしても避けられない問題があるのです!

f:id:wakaba-mafin:20181120160622j:plain:w150

今回の記事ではその問題についてご紹介していきたいと思います!


1 = 0.99999999975017!?

百聞は一見に如かず。

まずは実際にどんな問題が生じるかをPython (v3.7.0)で体験してみましょう!

算数

まずは算数です。皆さん、少し考えてみてください。0.0000001を10000000回足すとどうなるでしょう?



そうです。もちろん、これはちょうど1になります。なぜならば、


0.0000001 \times 10000000 = 1

だからです。

では、こちらはどうでしょう?


\frac{1}{3} - 0.3333

こちらは少し煩雑ですが、 1/3 = 0.33333333...であることに注意してもらえれば直感的に0.000033333...になることはわかってもらえると思います。


\frac{1}{3} - 0.3333 = \frac{1-0.9999}{3} = \frac{0.0001}{3} = 0.000033333....

ですね!

Pythonを使った算数

ここまでは小学生で習う計算でした。では、Pythonにこれらの問題を解いてもらいましょう!

先ほどの算数の1つ目の計算は次で実行できます。

x = 0.0
for _ in range(10000000):
    x = x + 0.0000001
print(x)

次々に0.0000001を足していく計算ですね!この結果はどうでしょう?

0.99999999975017

あれ?思ってた結果とちゃいますね💦小学生でも1ってわかるのに...。

まぁまぁ、気を取り直して算数の2つ目の計算。

1.0/3.0 - 0.3333
3.333333333332966e-05

ここでe-05は \times 10^{-5}を表していますが、これでは0.00003333333333332966になってしまいます。こっちも思ってた答えとちゃいますね💦

一応、別の計算でも

0.0001/3.0
3.3333333333333335e-05

んー、惜しいけど、これじゃあ、0.000033333333333333335になってしまう...。しかも、さっきの答えと一緒になるはずですのに、計算方法を変えると計算結果も変わってしまいました...

なぜ、人間よりも複雑の計算ができるはずのコンピュータが間違えてしまうのでしょうか?

ここには実は現在のコンピュータが絶対に乗り越えられない大きな問題が潜んでいるのです。

数値計算における4つの避けられない誤差

コンピュータには実は限界があります。それは次の3つの観点から考えることできます。

  1. コンピュータの中では情報は全て0と1の数字だけで表されている

  2. コンピュータでは有限の桁しか扱うことができない

  3. コンピュータでは有限回の計算しかできない

これら3つの観点から、数値計算における4つの避けられない大きな誤差が生じてくるのです。

※言葉遣い

今後、 \beta t桁保証といったとき、


\pm 0.d_1 d_2 \cdots d_t \times \beta^{m}

が正しいということが保証されているとしてください。例えば、 0.125\times 10^{2}が10進2桁保証とは、 0.12\times 10^{2}=12が確実に正しいことが保証されているということです。

丸め誤差

まずは1つ目、丸め誤差です。これはコンピュータが0と1の2進数しか扱えないこと有限の桁しか扱えないことから生じる誤差です。

私達が普段扱っている数字は基本的には10進数です。例えば、126であれば、


126 = 1\times 10^{2} + 2\times 10^{1} + 6\times 10^{0}

であるから、126と書かれるのです。しかしながら、コンピュータの場合は違います。コンピュータの場合は2進数ですから


126(10) = 1\times 2^{6}+1\times 2^{5}+1\times 2^{4}+1\times 2^{3}+1\times 2^{2}+1\times 2^{1}+0 \times 2^{0} (10)

より、10進数の126(10)は2進数の1111110(2)となるのです。さて、ここまでは2進数で扱われることのお話でした。

では、これが0.1など小数になるとどうなるのでしょうか?

例えば、0.1(10)。これを2進で展開すると


0.1 = 2^{-4} + 2^{-5} + 2^{-8} + 2^{-9} + 2^{-12} + 2^{-13} + \cdots

となり、0.1(10)=0.000110011001100...(2)となります。ここでコンピュータが有限の桁しか扱えないことが効いてきます。

すると、どうでしょう...例えば、2進5桁の保証しかないコンピュータだと、


0.000110011001100...(2) \rightarrow 0.00011(2)

の正しさしか保証できず、保証できるのは0.09375(10)にすぎません。これは明らかに0.1(10)とは異なります。この誤差が丸め誤差です。

私達がどれだけキリのいい数と思っても、コンピュータの中では2進数に変換されてしまい、しかもその2進数が有限桁で丸められてしまうのです。

桁落ち

桁落ちとは同じような大きさの値の引き算によって有効な桁が落ちてしまうことにより生じる誤差です。

今、 a=0.51376 \times 10^{1} b=0.51369 \times 10^{1} がそれぞれ10進5桁保証で正確に求まっているとしましょう。このとき、 a-bを計算すると、


a-b = 0.0007 = 7\times 10^{-4}

となり、最初は10進5桁保証で正しかったのに、引き算をした後では10進1桁しか正しさを保証できる桁はありません。もしこれで、 a,bの小数点6桁以降の保証できない部分の値が


a = 0.513761234 \times 10^{1}, b=0.513694321 \times 10^{1}

となっているとしたら、


a-b =  0.00066931 = 0.66931\times 10^{-3}

となってしまい、この値で10進5桁保証で正しさを保証しようとすると、全く保証できなかったら値を含むことになってしまいます。

このように大きさの似ている値の引き算によって保証できる桁が小さくなることで誤差が発生するのです。

情報落ち

情報落ちは桁落ちとは異なり、全く大きさが異なる2つの数字を足したり引いたりすることによって生じる誤差です。

今、 a=0.10000 \times 10^{9} b=0.10000\times 10^{-7}がそれぞれ10進5桁保証で正しさが保証されているとしましょう。このとき、 a+bを考えると


a+b = 100000000 + 0.00000001 = 100000000.00000001 = 1.0000000000000001\times 10^{8}

となり、今10進5桁保証でしか正しさが保証されないので、


a+b =  0.10000000000000001\times 10^{9} \rightarrow 0.10000\times 10^{9}

と0じゃないbを足したはずなのに足し算の結果が足し算前と変わらないという摩訶不思議なことが起こってしまいます。

このように、あまりにも大きさの異なる2つの値を足し引きした際に小さい方の情報が有効桁によって無視されても誤差が生じる可能性があるのです

打ち切り誤差

最後に打ち切り誤差です。この誤差はコンピュータが有限回の計算しかできないことから生じる誤差です。

今、三角関数の値 \sin(2)を計算することを考えましょう。 \sin(2)は残念ながら私たちが知っている角度で表すことができませんので、もちろん手では計算できません。

ですが、コンピュータの中では次の式を用いて計算しようとします。


\sin x=x-\frac{x^{3}}{3!}+\frac{x^{5}}{5!}-\frac{x^{7}}{7!}+\cdots

これはマクローリン展開といって sin(x)を"無限"の次数の多項式で表したものです。

しかしながら、先ほども言いましたようにコンピュータでは有限回の計算しかできません。つまり、足し算引き算を途中で止める必要があるのです。

例えば、 \sin(2)の計算を4回の計算で止めてしまうと


2-\frac{2^{3}}{3!}+\frac{2^{5}}{5!}-\frac{2^{7}}{7!}

 \sin(2)の値と扱ってしまい、止めて無視された計算の結果が全く反映されず誤差が生じてしまいます。

このように無限回の計算を有限回で打ち切って無視された計算によって誤差が生じる可能性があるのです。

まとめ

今回の記事ではコンピュータの上で生じる避けられない誤差についてお話しました。

コンピュータは一見絶対に正しく絶対に間違えないと思われがちですが、使い方によっては大きな誤差が乗ってしまう可能性があります。

その誤差として、

  1. 丸め誤差

  2. 桁落ちによる誤差

  3. 情報落ちによる誤差

  4. 打ち切り誤差

の4つの誤差をご紹介しました。これら4つの誤差に気をつけながらプログラミングを行わないと、とんでもない値が出てくる可能性もあるのです。

これらの誤差の避け方は基本的にはケースバイケースではありますが、例として

  • 似たような値の引き算は行わない(有理化の逆の操作で回避)

  • 複数の足し算をするときは値の小さなものから足す

  • 可変長の浮動小数点を用いる

などが挙げられます。

というわけで、これからプログラミングを始める方にぜひ気をつけて欲しい誤差4選でした!コンピュータを過信しないようにしましょう!

間違いのご指摘やご意見等ございましたら、ぜひコメントのほどよろしくお願いします!