CTC 教育サービス
[IT研修]注目キーワード Python UiPath(RPA) 最新技術動向 Microsoft Azure Docker Kubernetes
「お金」の計算をしたいと思います。
コンピュータは蓄積プログラム方式(ストアド・プログラム)を採用した汎用の電子計算機(電算機、電脳)です。この電子計算機(安価なパソコン)を使ってお金の計算をしてみることにします。
計算するのは「七掛け」です。
「七掛け」とは定価から卸値を求める割合のことで、卸値(取引価格)が定価(販売価格)の七割だという意味。「八掛け」は定価の八割となります。
商材の定価から卸値を算出するのです。
「百聞は一見に如かず、図して方略を上らむ」ことにしましょう。
とある商材の定価は「九万円」ですので「七掛け」すると卸値は以下のようになります。
| puts 90000 * 0.7 # Ruby programming language | # => 62999.99999999999
とてもシンプルですが、何か間違っているような気がします。念のためJavaでも同じ計算をしてみます。
| System.out.println(90000 * 0.7); // Java programming language | // => 62999.99999999999
結果が同じなのでこれで「オーケー、コンピュータ(OK, Computer)」としたいのですが、どうしても疑いが晴れないのでビジコンが開発したパソコンの祖先である高性能な電子式卓上計算機(電卓)をお借りして再計算してみました(笑わないでください)。
| 90000 × 0.7 = 63000
七掛けした卸値は「63000」円。先程のプログラムが出力した結果とは異なります。
この差異の原因は誤差なのです。では何故、誤差はどこで生じるのでしょうか?
順を追って推理しましょう。
まず一人目の被疑者は「コンピュータでの数の表現は2進数である」です。
人間が日常使う数は10進数(decimal)ですが、コンピュータは2進数(binary)です。コンピュータは電気で動きます。電気が流れたり流れなかったりすることでオン/オフ(ON/OFF)となるスイッチです。
このスイッチは電流がオンの時に1、オフの時に0、と意味合い付けることで数を表現できます。つまり、電流が流れる線が複数あり(桁数)通電したか否かで1と0を数えることで、コンピュータは2を底とする2進数で表現するのです。
ですが、コンピュータに合わせて人間も2進数で会話するとなると「私は、0011 0000 歳です。」、「小銭無いから 0110 0100円貸して。」では相当量練習しないと意味が通じないかもしれません。
利便性のために人間が10進数で表現した数はコンピュータでは処理のために内部で2進数に変換します。ここで問題が発生します。
例えば10進数でも「1 / 3(1割る3)」が「0.33333333....」と割り切れないのと似ていますが、10進数で「0.1」を2進数に変換すると「0.000110011001100110011...」と無限級数になってしまいます。
つまり実数(小数)である10進数を変換すると2進数では正確に表現できない場合も生じるのです。
ここでもう一人の被疑者である「コンピュータでの数の表現には限りがある」ことが登場します。
人間よりは格段に物覚えが良いコンピュータにおいても表現できる数(桁数)には限りがあります。そのため有限の桁数で効率よく数を表現するためコンピュータ内部では(科学の計算で使う)指数表記を用いています。
10進数では、0がいっぱい並んだ「0.000000001」(オーナイン・システム、エヴァ初号機の起動確率)を指数表記にすると「1.0E-9」(1.0×10の-9乗)と簡潔に表記できます。
上記のように、固定長の仮数(mantissa)と指数(exponent)で小数を表現することで桁数を大幅に節約できるため、より広い絶対値の範囲の数を表現可能になるのです。
同様に2進数でも指数表記が可能です。それ故にコンピュータはこの表記法を採用しています。これを小数点の位置が移動することから「浮動小数点(Floating Point)」と呼ばれます。
この浮動小数点方式による数値表現(浮動小数点数)を専門に計算する装置のFPU(Floating-Point Unit、浮動小数点演算装置、数値演算コプロセッサ)は、パソコンのカタログで見かける単語です。
また浮動小数点数のフォーマットは幾つかありますが、IEEE方式(IEEE 754形式)が多くのコンピュータ(及びプログラミング言語)で採用されているのです。
問題は先程例示した2進数に変換した「0.000110011001100110011...」です。
如何な浮動小数点数を用いても無限に続く数を正確に表現できないのは自明です。
| puts sprintf("%.50f", 0.1) # Ruby programming language | # => 0.10000000000000000555111512312578270211815834045410
Rubyでは浮動小数点数はFloatクラスで表現されますが、実装はC言語のdoubleです。
| printf("%.50f¥n", 0.1); /* C programming language */ | /* => 0.10000000000000000555111512312578270211815834045410 */
(上記はLinuxで確認しました。最初にCygwin環境で実行すると結果が食い違いましたが、エミュレーション環境のためかと思われます。)
精度に関しては「(倍精度の場合)一般にはせいぜい15桁です」とRubyのドキュメントにも記載されています(詳細はIEEE 754のドキュメントをご参照ください)。
これらから仮数部の桁数が有限であるので収まらない部分を零捨一入(0捨1入、10進数の四捨五入と同義)して格納します。つまり、実数を浮動小数点数、すなわち有限桁数の2進数によって表現するため、場合によってはどうしても近似値として表現せざるを得ないために誤差が生じる。
これが「丸め誤差」(主犯格)なのです。
| puts 1.1 + 2.2 # Ruby programming language | #=> 3.3000000000000003
浮動小数点数では「丸め誤差」があるのです。
「丸め誤差」を含むため複数回の計算を繰り返すと更なる問題が生じます。
| puts "%.50f" % (0.1 * 26) # Ruby programming language | # => 2.60000000000000008881784197001252323389053344726562 | | sum = 0; 26.times { sum += 0.1 }; puts "%.50f" % sum | # => 2.60000000000000097699626167013775557279586791992188
上記の例では、同じ値を導くはずの足し算と掛け算の計算で結果が異なりました。
これは、掛け算は1回の演算なのに対して足し算は複数回の演算を実行しているためです。つまり、丸め誤差が「蓄積」しているのです。
このような場合を考慮すると浮動小数点数同士を単純に値で比較をしてはいけないことも分かります。この誤差の蓄積以外にも浮動小数点数の計算では「情報落ち」、「桁落ち」、「オーバフロー」など幾つかの要因で誤差が発生します。計算では特に注意が必要となるのです。
被疑者を突き止めたことで犯人像が浮かび上がりました。
罪を憎んで人を憎まず。そこで回避策も試してみたいと思います。
実はこのような浮動小数点数の問題を解決するためにRubyでは標準添付ライブラリが用意されています。BigDecimalクラスは内部において10進数で表記された数値として格納するのです。つまり問題であった2進数への変換を行わないので問題を回避することとなり、BigDecimalクラス同士の演算で常に正確な値を得ることが可能となります。
| # Ruby programming language | require 'bigdecimal' | | selling_price = BigDecimal::new("90000") | rate = BigDecimal::new("0.7") | trade_price = (selling_price * rate).to_f | puts "%.50f" % trade_price | # => 63000.00000000000000000000000000000000000000000000000000
結果が綺麗になりましたね。
| // Java programming language | import java.math.BigDecimal; | | class TheDriftingCloud { | public static void main(String args[]){ | | BigDecimal selling_price = new BigDecimal("90000"); | BigDecimal rate = new BigDecimal("0.7"); | BigDecimal trade_price = selling_price.multiply(rate); | System.out.printf("%.50f¥n", trade_price.doubleValue()); | // => 63000.00000000000000000000000000000000000000000000000000 | } | }
Javaでは同名異人のBigDecimalクラス・ライブラリが提供されています。
各々のBigDecimalがどのように内部で10進数を保持しているのかは、ライブラリのマニュアルをご覧になると宜しいでしょう。加えてBigDecimalを使う際には、コンピュータの内部表現は2進数なのですから(変換を行わないため)計算速度が遅くなってしまうことも配慮すべきかもしれません。
コンピュータでお金の計算をする際には気を付けなければいけないことが判明しました。近々、消費税の変更が相次ぐことが案内されていることですし、お金の計算をしているプログラムは再確認するのが得策かもしれません。
そして何故このように誤差を含むのかを理解するには、コンピュータの特性を把握する必要がありました。「コンピュータの数の表現は2進数である」何度も繰り返しますが、基礎が何より大事なのです。
ところで「浮雲」は、そよ風に流される雲の有体を些細な要因が積み重なることで誤差が蓄積するかの如く頼りなく気持ちが揺れ動き、その反動で本心とは裏腹に所在無げに儚く彷徨してしまう人間の様を浮いた雲に準えたのでありましょう。
明治末期に発表された「浮雲」は二葉亭四迷が著作で日本の近代小説と称されますが、内容は兎も角、本当にモダンな文体で驚かされます。ラノベ(ライトノベル)を読んだことはないですが、明治のラノベと言いえるのかもしれません。
同名異人となる映画「浮雲」は全く異なるストーリーで優柔不断の男とそれに翻弄される女の話です。高峰秀子が主演のこの映画は彼女の代表作に留まらず日本映画の名作です。高峰秀子といえば「カルメン故郷に帰る」や「二十四の瞳」、「名も無く貧しく美しく」と名画揃いでありますが、この「浮雲」もお薦めさせていただきます。邦画が好きな方は是非どうぞ。
次回もお楽しみに。
[IT研修]注目キーワード Python UiPath(RPA) 最新技術動向 Microsoft Azure Docker Kubernetes